w3d
Version:
Framework to create 3D web pages.
840 lines (736 loc) • 21 kB
JavaScript
/*
* object-utils.js
* Copyright 2017 Lucas Neves <lcneves@gmail.com>
*
* Helper functions and classes for the w3d extension of three.js's Object3D.
*/
'use strict';
const THREE = require('three');
const windowUtils = require('./window-utils.js');
const objectCommons = require('./object-commons.js');
const units = require('./units.js');
const AXES = ['x', 'y', 'z'];
const CSS_AXES = ['width', 'height', 'depth'];
class Background extends THREE.Mesh {
constructor (object) {
if (object && object._isw3dObject) {
var size = object.size;
var material = new THREE.MeshLambertMaterial({
color: object.getStyle('background-color')
});
var geometry = new THREE.PlaneGeometry(size.x, size.y);
super(geometry, material);
this._isBackground = true;
this._ignoreSize = true;
this.receiveShadow = true;
}
else {
throw new Error('Invalid object!');
}
}
}
const backgroundPrototype = {
resize () {},
w3dAllNeedUpdate () {},
set w3dNeedsUpdate (property) {},
get size () {
return makeInitialVirtualBox();
},
get innerSize () {
return this.size;
},
get outerSize () {
return this.size;
}
};
importPrototype(Background.prototype, objectCommons);
importPrototype(Background.prototype, backgroundPrototype);
function getAxes (object) {
var direction = object.getStyle('direction');
var axes;
switch (direction) {
case 'row':
axes = { main: 'x', cross: 'y', other: 'z' };
break;
case 'stack':
axes = { main: 'z', cross: 'x', other: 'y' };
break;
default:
axes = { main: 'y', cross: 'x', other: 'z' };
break;
}
return axes;
}
function getWorldDimensions (object, prefix) {
prefix = prefix ? prefix + '-' : '';
var dimensions = {};
for (let i = 0; i < CSS_AXES.length; i++) {
dimensions[AXES[i]] =
units.convert(object, prefix + CSS_AXES[i], 'world');
}
return dimensions;
}
function getSizeFromBbox (bbox) {
return {
x: bbox.max.x - bbox.min.x,
y: bbox.max.y - bbox.min.y,
z: bbox.max.z - bbox.min.z
};
}
function makeBboxFromImage (image) {
const worldToPixels = windowUtils.worldToPixels;
return {
min: {
x: - image.width / (2 * worldToPixels),
y: - image.height / (2 * worldToPixels),
z: 0
},
max: {
x: image.width / (2 * worldToPixels),
y: image.height / (2 * worldToPixels),
z: 0
}
};
}
function getBboxFromObject (object) {
if (object.geometry) {
if (object.geometry.boundingBox === null) {
object.geometry.computeBoundingBox();
}
var bBox = JSON.parse(JSON.stringify(object.geometry.boundingBox));
// Adjust bounding box to current scale
for (let parameter in bBox) {
for (let axis in bBox[parameter]) {
bBox[parameter][axis] *= object.scale[axis];
}
}
return bBox;
}
else if (object._isSpriteFromCanvas) {
return makeBboxFromImage(object.material.map.image);
}
// Last resort
else {
return new THREE.Box3().setFromObject(object);
}
}
function makeInitialVirtualBox () {
return {
x: 0,
y: 0,
z: 0
};
}
function getSpacer (object, direction, onlyMargin) {
if (object._isw3dObject) {
var padding = onlyMargin
? 0 : units.convert(object, 'padding-' + direction, 'world');
return units.convert(object, 'margin-' + direction, 'world') + padding;
}
else {
return 0;
}
}
function getSpacers (object, type) {
if (object._isw3dObject) {
return {
x: units.convert(object, type + '-left', 'world') +
units.convert(object, type + '-right', 'world'),
y: units.convert(object, type + '-top', 'world') +
units.convert(object, type + '-bottom', 'world'),
z: units.convert(object, type + '-far', 'world') +
units.convert(object, type + '-near', 'world')
};
}
else {
return makeInitialVirtualBox();
}
}
function getSizesFromStyle (object) {
if (object._isw3dObject) {
var sizes = { x: {}, y: {}, z: {} };
for (let prefix of [ '', 'min-', 'max-' ]) {
let propName = prefix ? prefix.substring(0, 3) : 'fixed';
for (let i = 0; i < AXES.length; i++) {
sizes[AXES[i]][propName] = units.convert(
object,
prefix + CSS_AXES[i],
'world'
);
}
}
return sizes;
}
else {
throw new Error('Object is not a w3d object!');
}
}
function addSpacers (originalBox, spacers) {
var box = JSON.parse(JSON.stringify(originalBox));
for (let axis of AXES) {
box[axis] += spacers[axis];
}
return box;
}
function removeSpacers (originalBox, spacers) {
var box = JSON.parse(JSON.stringify(originalBox));
for (let axis of AXES) {
box[axis] -= spacers[axis];
}
return box;
}
function getMargins (object, axis) {
switch (axis) {
case 'width':
case 'x':
return units.convert(object, 'margin-left', 'world') +
units.convert(object, 'margin-right', 'world');
case 'height':
case 'y':
return units.convert(object, 'margin-top', 'world') +
units.convert(object, 'margin-bottom', 'world');
case 'depth':
case 'z':
return units.convert(object, 'margin-far', 'world') +
units.convert(object, 'margin-near', 'world');
default:
throw new Error('Invalid axis! Received: ' + JSON.stringify(axis));
}
}
function getContainerSpace (object) {
return removeSpacers(
removeSpacers(object.availableSpace, getSpacers(object, 'margin')),
getSpacers(object, 'padding')
);
}
function alignChildren (parent) {
for (let child of parent.children) {
child.align();
if (child._isw3dObject) {
child.alignChildren();
}
}
}
function align (object) {
const parent = object._parent || object.parent;
const crossAxis = getAxes(parent)['cross'];
const crossSize = parent.innerSize[crossAxis];
const freeSpace = Math.max(crossSize - object.outerSize[crossAxis], 0);
const sizeSign = crossAxis === 'y' ? -1 : 1;
const alignSelf = object._isw3dObject
? object.getStyle('align-self') : 'initial';
const align = alignSelf !== 'initial'
? alignSelf : parent.getStyle('align-items');
switch (align) {
case 'start':
break;
case 'center':
object.position[crossAxis] += freeSpace * sizeSign / 2;
break;
case 'end':
object.position[crossAxis] += freeSpace * sizeSign;
break;
case 'stretch':
if (object._isw3dObject) {
var size = object.outerSize;
size[crossAxis] += freeSpace * sizeSign;
object.outerSize = size;
}
break;
}
}
function getSize (object) {
if (object._isw3dObject) {
return removeSpacers(object.outerSize, getSpacers(object, 'margin'));
}
else {
return getSizeFromBbox(getBboxFromObject(object));
}
}
function getInnerSize (object) {
if (object._isw3dObject) {
return removeSpacers(object.size, getSpacers(object, 'padding'));
}
else {
return object.size;
}
}
function getOuterSize (object) {
if (object._isw3dObject) {
const axes = getAxes(object);
var dimensions = {};
for (let i = 0; i < AXES.length; i++) {
let size = units.convert(object, CSS_AXES[i], 'world');
if (size !== undefined) {
dimensions[AXES[i]] = size + getMargins(object, CSS_AXES[i]);
}
else {
if (AXES[i] === axes['main']) {
dimensions[AXES[i]] = Math.max(
object.minContentContribution[AXES[i]],
Math.min(
object.maxContentContribution[AXES[i]],
object.availableSpace[AXES[i]]
)
);
}
else {
dimensions[AXES[i]] = undefined;
}
}
}
return dimensions;
}
else {
return object.size;
}
}
function getMinContentContribution (object, wrap, mainAxis) {
var virtualBox = makeInitialVirtualBox();
for (let child of object.children) {
if (!child._ignoreSize) {
for (let axis of AXES) {
if (
axis === mainAxis && wrap === 'nowrap'
) {
virtualBox[axis] += child.minContentContribution[axis];
}
else {
virtualBox[axis] = Math.max(
virtualBox[axis],
child.minContentContribution[axis]
);
}
}
}
}
return virtualBox;
}
function getMaxContentContribution (object, wrap, mainAxis) {
var virtualBox = makeInitialVirtualBox();
for (let child of object.children) {
if (!child._ignoreSize) {
for (let axis of AXES) {
if (axis === mainAxis || wrap === 'wrap') {
virtualBox[axis] += child.maxContentContribution[axis];
}
else {
virtualBox[axis] = Math.max(
virtualBox[axis],
child.maxContentContribution[axis]
);
}
}
}
}
return virtualBox;
}
function getContentContribution (object, minMax) {
if (minMax !== 'min' && minMax !== 'max') {
throw new Error('Expected parameter to be \'min\' or \'max\', got: ' +
JSON.stringify(minMax));
}
if (!object._isw3dObject) {
return object.size;
}
const wrap = object.getStyle('wrap');
const mainAxis = getAxes(object)['main'];
var virtualBox = minMax === 'min'
? getMinContentContribution(object, wrap, mainAxis)
: getMaxContentContribution(object, wrap, mainAxis);
virtualBox = addSpacers(virtualBox, getSpacers(object, 'padding'));
const dimensions = getWorldDimensions(object);
const minDimensions = getWorldDimensions(object, 'min');
const maxDimensions = getWorldDimensions(object, 'max');
for (let axis of AXES) {
if (dimensions[axis] !== undefined) {
virtualBox[axis] = dimensions[axis];
}
else {
virtualBox[axis] = Math.min(
Math.max(virtualBox[axis], minDimensions[axis]),
maxDimensions[axis]
);
}
}
virtualBox = addSpacers(virtualBox, getSpacers(object, 'margin'));
return virtualBox;
}
/*
* Gives the object's world boundaries relative to its position point.
* Three.js uses the right-hand coordinate system, so:
* - the x axis grows to the right;
* - the y axis grows to the top;
* - the z axis grows to the near.
*/
function getBoundaries (object) {
if (object._isw3dObject) {
var size = object.size;
return {
left: 0,
right: size.x,
top: size.y,
bottom: 0,
far: 0,
near: size.z
};
}
else {
const bbox = getBboxFromObject(object);
return {
left: - bbox.min.x,
right: bbox.max.x,
top: bbox.max.y,
bottom: - bbox.min.y,
far: - bbox.min.z,
near: bbox.max.z
};
}
}
function makeInitialOffset (object, onlyMargin) {
var offset = {
x: { reference: 'left', distance: 0 },
y: { reference: 'top', distance: 0 },
z: { reference: 'far', distance: 0 }
};
if (object && object._isw3dObject) {
offset.x.distance += getSpacer(object, 'left', onlyMargin);
offset.y.distance += getSpacer(object, 'top', onlyMargin);
offset.z.distance += getSpacer(object, 'far', onlyMargin);
}
return offset;
}
function makePosition (object, offset) {
var position = {};
for (let axis of AXES) {
position[axis] = offset[axis].distance;
if (!object._isw3dObject) {
position[axis] += object.boundaries[offset[axis].reference];
}
switch (offset[axis].reference) {
case 'right':
case 'top':
case 'near':
position[axis] = - position[axis];
break;
}
}
return position;
}
function assignPosition (object, position) {
for (let axis of AXES) {
object.position[axis] = position[axis];
}
}
function getMaxMinDifference (object, axis) {
return Math.max(
object.maxContentContribution[axis] -
object.minContentContribution[axis],
0);
}
function getJustifyOffset (
child,
firstChild,
childrenCount,
availableToJustify,
justifyRule
) {
if (!availableToJustify) {
return 0;
}
switch (justifyRule) {
case 'end':
return child === firstChild ? availableToJustify : 0;
case 'center':
return child === firstChild ? availableToJustify / 2 : 0;
case 'space-between':
return child === firstChild
? 0
: availableToJustify / (childrenCount - 1);
case 'space-around':
return child === firstChild
? availableToJustify / (childrenCount * 2)
: availableToJustify / childrenCount;
case 'space-evenly':
return availableToJustify / (childrenCount + 1);
case 'start':
default:
return 0;
}
}
function positionLine (
object,
receivedOffset,
objectNewSizes,
minContributions,
firstChild,
lastChild,
justifyRule
) {
const axes = getAxes(object);
const availableSpace = Math.max(
object.containerSpace[axes['main']] - minContributions, 0);
var totalGrowthToMax = 0;
for (let i = firstChild; i <= lastChild; i++) {
let child = object.children[i];
totalGrowthToMax += getMaxMinDifference(child, axes['main']);
}
const availableToMax = Math.min(totalGrowthToMax, availableSpace);
const maxGrowthFactor = totalGrowthToMax > 0
? availableToMax / totalGrowthToMax : 0;
for (let i = firstChild; i <= lastChild; i++) {
let child = object.children[i];
if (child._isBackground) {
continue;
}
let childAvailableSpace = {};
childAvailableSpace[axes['main']] =
child.minContentContribution[axes['main']] +
getMaxMinDifference(child, axes['main']) * maxGrowthFactor;
for (let axis of ['cross', 'other']) {
childAvailableSpace[axes[axis]] = Math.max(
object.containerSpace[axes[axis]] - objectNewSizes[axis], 0);
}
child.availableSpace = childAvailableSpace;
if (child._isw3dObject) {
child.arrange();
}
}
var childrenSizeMain = 0;
var totalCSSGrow = 0;
for (let i = firstChild; i <= lastChild; i++) {
let child = object.children[i];
childrenSizeMain += child.outerSize[axes['main']];
totalCSSGrow += child._isw3dObject ? child.getStyle('grow') : 0;
}
var mainSize = 0;
var crossSize = 0;
var otherSize = 0;
var offset = JSON.parse(JSON.stringify(receivedOffset));
offset[axes['cross']].distance += objectNewSizes.cross;
var availableToGrow = Math.max(
object.containerSpace[axes['main']] - childrenSizeMain, 0);
for (let i = firstChild; i <= lastChild; i++) {
let child = object.children[i];
if (child._isw3dObject) {
let grow = child._isw3dObject
? child.getStyle('grow') : 0;
let growFactorCSS = totalCSSGrow
? grow / totalCSSGrow : 0;
let newOuterDimensions = {};
newOuterDimensions[axes['main']] = child.outerSize[axes['main']] +
availableToGrow * growFactorCSS;
for (let axis of ['cross', 'other']) {
newOuterDimensions[axes[axis]] = child.outerSize[axes[axis]];
}
child.outerSize = newOuterDimensions;
}
}
childrenSizeMain = 0;
var childrenCount = 0;
for (let i = firstChild; i <= lastChild; i++) {
let child = object.children[i];
if (!child._isBackground) {
childrenSizeMain += child.outerSize[axes['main']];
childrenCount++;
}
}
var availableToJustify = Math.max(
object.containerSpace[axes['main']] - childrenSizeMain, 0);
for (let i = firstChild; i <= lastChild; i++) {
let child = object.children[i];
if (!child._isBackground) {
offset[axes['main']].distance += getJustifyOffset(
i,
firstChild,
childrenCount,
availableToJustify,
justifyRule
);
let childPosition = makePosition(child, offset);
assignPosition(child, childPosition);
offset[axes['main']].distance += child.outerSize[axes['main']];
mainSize += child.outerSize[axes['main']];
crossSize = Math.max(crossSize, child.outerSize[axes['cross']]);
otherSize = Math.max(otherSize, child.outerSize[axes['other']]);
}
}
return { mainSize: mainSize, crossSize: crossSize, otherSize: otherSize };
}
function getAlignmentRules (object) {
// text-align supercedes justify-content
var general;
var last;
const textAlign = object.getStyle('text-align');
const justifyContent = object.getStyle('justify-content');
if (textAlign !== 'initial') {
switch (textAlign) {
case 'end':
case 'right':
general = last = 'end';
break;
case 'center':
general = last = 'center';
break;
case 'justify-all':
general = last = 'space-between';
break;
case 'justify':
general = 'space-between';
last = 'start';
break;
case 'start':
case 'left':
default:
general = last = 'start';
break;
}
}
else {
general = last = justifyContent;
}
return {
general: general,
lastLine: last
};
}
function positionChildren (object) {
const objectAxes = getAxes(object);
const wrap = (object.getStyle('wrap') === 'wrap');
const alignmentRules = getAlignmentRules(object);
var initialOffset = makeInitialOffset(object);
// At this time, we can only know for sure the object's main axis
// dimensions. After positioning the children we will know the rest.
var objectNewSizes = {
main: 0,
cross: 0,
other: 0
};
var minContributions = 0;
var lastPositionedChild = -1;
for (let i = 0; i < object.children.length; i++) {
let child = object.children[i];
if (child._isBackground) {
continue;
}
if (wrap &&
minContributions + child.minContentContribution[objectAxes.main] >
object.containerSpace[objectAxes.main]
) {
let lineDimensions = positionLine(
object,
initialOffset,
objectNewSizes,
minContributions,
lastPositionedChild + 1,
i - 1,
alignmentRules.general
);
objectNewSizes.main = Math.max(
objectNewSizes.main, lineDimensions.mainSize);
objectNewSizes.cross += lineDimensions.crossSize;
objectNewSizes.other += lineDimensions.otherSize;
lastPositionedChild = i - 1;
minContributions = child.minContentContribution[objectAxes.main];
}
else {
minContributions += child.minContentContribution[objectAxes.main];
}
}
var finalLineDimensions = positionLine(
object,
initialOffset,
objectNewSizes,
minContributions,
lastPositionedChild + 1,
object.children.length - 1,
alignmentRules.lastLine
);
objectNewSizes.main = Math.max(
objectNewSizes.main, finalLineDimensions.mainSize);
objectNewSizes.cross += finalLineDimensions.crossSize;
objectNewSizes.other += finalLineDimensions.otherSize;
var newOuterDimensions = {};
const spacers =
addSpacers(getSpacers(object, 'margin'), getSpacers(object, 'padding'));
for (let axis of ['main', 'cross', 'other']) {
newOuterDimensions[objectAxes[axis]] = objectNewSizes[axis] +
spacers[objectAxes[axis]];
}
object.outerSize = newOuterDimensions;
}
function getFontSize (object) {
const parsed = units.parse(object.getStyle('font-size'));
if (parsed.unit === 'em') {
return object._parent.fontSize * parsed.quantum;
}
else {
return units.convert(object, 'font-size');
}
}
function updateBackground (object) {
if (object._hasBackground) {
var newBg = new Background(object);
var bgIndex;
for (let index = 0; index < object.children.length; index++) {
if (object.children[index]._isBackground) {
bgIndex = index;
break;
}
}
if (bgIndex !== undefined) {
newBg.parent = object;
object.children.splice(bgIndex, 1, newBg);
}
else {
object.add(newBg);
}
assignPosition(newBg, makePosition(newBg, makeInitialOffset(object, true)));
}
}
function forceUpdate (object, property) {
property = '_' + property;
if (object.hasOwnProperty(property)) {
delete object[property];
}
}
function importPrototype (object, prototype) {
for (let key in prototype) {
let descriptor = Object.getOwnPropertyDescriptor(prototype, key);
if (descriptor !== undefined) {
Object.defineProperty(object, key, descriptor);
}
}
}
function isHeader (object) {
switch (object.getProperty('tag')) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return true;
default:
return false;
}
}
Object.assign(module.exports, {
Background: Background,
align: align,
alignChildren: alignChildren,
forceUpdate: forceUpdate,
getBoundaries: getBoundaries,
getContentContribution: getContentContribution,
getContainerSpace: getContainerSpace,
getSize: getSize,
getSizesFromStyle: getSizesFromStyle,
getOuterSize: getOuterSize,
getAxes: getAxes,
getFontSize: getFontSize,
getInnerSize: getInnerSize,
importPrototype: importPrototype,
isHeader: isHeader,
positionChildren: positionChildren,
updateBackground: updateBackground
});