UNPKG

infamous

Version:

A CSS3D/WebGL UI library.

282 lines (243 loc) 11.1 kB
import Class from 'lowclass' import Mixin from './Mixin' import XYZNumberValues from './XYZNumberValues' import Sizeable from './Sizeable' import {isInstanceof} from './Utility' import { props } from './props' export default Mixin(Base => { const Parent = Sizeable.mixin(Base) // Transformable extends TreeNode (indirectly through Sizeable) because it // needs to be aware of its `parent` when calculating align adjustments. const Transformable = Class('Transformable').extends( Parent, ({ Super }) => ({ static: { props: { ...Parent.props, position: props.XYZNumberValues, rotation: props.XYZNumberValues, scale: props.XYZNumberValues, origin: props.XYZNumberValues, align: props.XYZNumberValues, mountPoint: props.XYZNumberValues, opacity: props.number, }, }, constructor(options = {}) { const self = Super(this).constructor(options) self._worldMatrix = null return self }, _setDefaultProperties() { Super(this)._setDefaultProperties() Object.assign(this._properties, { position: new XYZNumberValues(0, 0, 0), rotation: new XYZNumberValues(0, 0, 0), scale: new XYZNumberValues(1, 1, 1), origin: new XYZNumberValues(0.5, 0.5, 0.5), align: new XYZNumberValues(0, 0, 0), mountPoint: new XYZNumberValues(0, 0, 0), opacity: 1, transform: new window.DOMMatrix, // untracked by SkateJS }) }, _setPropertyObservers() { Super(this)._setPropertyObservers() this._properties.position.on('valuechanged', () => this.trigger('propertychange', 'position')) this._properties.rotation.on('valuechanged', () => this.trigger('propertychange', 'rotation')) this._properties.scale.on('valuechanged', () => this.trigger('propertychange', 'scale')) this._properties.origin.on('valuechanged', () => this.trigger('propertychange', 'origin')) this._properties.align.on('valuechanged', () => this.trigger('propertychange', 'align')) this._properties.mountPoint.on('valuechanged', () => this.trigger('propertychange', 'mountPoint')) }, /** * Takes all the current component values (position, rotation, etc) and * calculates a transformation DOMMatrix from them. See "W3C Geometry * Interfaces" to learn about DOMMatrix. * * @method * @private * @memberOf Node * * TODO #66: make sure this is called after size calculations when we * move _calcSize to a render task. */ _calculateMatrix () { // NOTE The only way to get an identity matrix with DOMMatrix API // in the current spec is to make a new DOMMatrix. This is wasteful // because it creates new memory. It'd be nice to have an // identity() method or similar. const matrix = new window.DOMMatrix // TODO FIXME For some reason, the root node (i.e. the Scene) // should not be translated or else the WebGL rendering glitches // out (this happened with my vanilla WebGL implementation as well // as with Three.js), so we return Identity if there's no parent. if (!this.parent) return matrix const properties = this._properties const thisSize = this._calculatedSize // THREE-COORDS-TO-DOM-COORDS // translate the "mount point" back to the top/left of the element. // We offset this in ElementOperations#applyTransform. The Y value // is inverted because we invert it below. const threeJsPostAdjustment = [ thisSize.x/2, thisSize.y/2, 0 ] const alignAdjustment = [0,0,0] // TODO If a Scene has a `parent`, it is not mounted directly into a // regular DOM element but rather it is child of a Node. In this // case we don't want the scene size to be based on observed size // of a regular DOM element, but relative to a parent Node just // like for all other Nodes. const parentSize = this._getParentSize() // THREE-COORDS-TO-DOM-COORDS // translate the "align" back to the top/left of the parent element. // We offset this in ElementOperations#applyTransform. The Y // value is inverted because we invert it below. threeJsPostAdjustment[0] += -parentSize.x/2 threeJsPostAdjustment[1] += -parentSize.y/2 const {align} = properties alignAdjustment[0] = parentSize.x * align.x alignAdjustment[1] = parentSize.y * align.y alignAdjustment[2] = parentSize.z * align.z const mountPointAdjustment = [0,0,0] const {mountPoint} = properties mountPointAdjustment[0] = thisSize.x * mountPoint.x mountPointAdjustment[1] = thisSize.y * mountPoint.y mountPointAdjustment[2] = thisSize.z * mountPoint.z const appliedPosition = [] const {position} = properties appliedPosition[0] = position.x + alignAdjustment[0] - mountPointAdjustment[0] appliedPosition[1] = position.y + alignAdjustment[1] - mountPointAdjustment[1] appliedPosition[2] = position.z + alignAdjustment[2] - mountPointAdjustment[2] matrix.translateSelf( appliedPosition[0] + threeJsPostAdjustment[0], // THREE-COORDS-TO-DOM-COORDS negate the Y value so that // Three.js' positive Y is downward. -(appliedPosition[1] + threeJsPostAdjustment[1]), appliedPosition[2] + threeJsPostAdjustment[2] ) // origin calculation will go here: // - move by negative origin before rotating. // apply each axis rotation, in the x,y,z order. // THREE-COORDS-TO-DOM-COORDS: X rotation is negated here so that // Three rotates on X in the same direction as CSS 3D. It is // negated again when applied to DOM elements so they rotate as // expected in CSS 3D. // TODO #151: make rotation order configurable const {rotation} = properties matrix.rotateAxisAngleSelf(1,0,0, rotation.x) matrix.rotateAxisAngleSelf(0,1,0, rotation.y) matrix.rotateAxisAngleSelf(0,0,1, rotation.z) // origin calculation will go here: // - move by positive origin after rotating. return matrix }, // TODO: fix _isIdentity in DOMMatrix, it is returning true even if false. _calculateWorldMatricesInSubtree() { this._calculateWorldMatrixFromParent() const children = this.subnodes for (let i=0, l=children.length; i<l; i+=1) { children[i]._calculateWorldMatricesInSubtree() } }, _calculateWorldMatrixFromParent() { const parent = this.parent if (parent) //this._worldMatrix = parent._worldMatrix.multiply(this._properties.transform) this._worldMatrix = this._properties.transform.multiply(parent._worldMatrix) else this._worldMatrix = this._properties.transform this.trigger('worldMatrixUpdate') }, // TODO rename "render" to "update". _render() { if ( Super(this)._render ) Super(this)._render() // TODO: only run this when necessary (f.e. not if only opacity // changed) this._properties.transform = this._calculateMatrix() }, /** * Set the position of the Transformable. * * @param {Object} newValue * @param {number} [newValue.x] The x-axis position to apply. * @param {number} [newValue.y] The y-axis position to apply. * @param {number} [newValue.z] The z-axis position to apply. */ set position(newValue) { this._setPropertyXYZ(Transformable, 'position', newValue) }, get position() { return Super(this).position }, /** * @param {Object} newValue * @param {number} [newValue.x] The x-axis rotation to apply. * @param {number} [newValue.y] The y-axis rotation to apply. * @param {number} [newValue.z] The z-axis rotation to apply. */ set rotation(newValue) { this._setPropertyXYZ(Transformable, 'rotation', newValue) }, get rotation() { return Super(this).rotation }, /** * @param {Object} newValue * @param {number} [newValue.x] The x-axis scale to apply. * @param {number} [newValue.y] The y-axis scale to apply. * @param {number} [newValue.z] The z-axis scale to apply. */ set scale(newValue) { this._setPropertyXYZ(Transformable, 'scale', newValue) }, get scale() { return Super(this).scale }, /** * Set this Node's opacity. * * @param {number} opacity A floating point number between 0 and 1 * (inclusive). 0 is fully transparent, 1 is fully opaque. */ set opacity(newValue) { this._setPropertySingle('opacity', newValue) }, get opacity() { return Super(this).opacity }, /** * Set the alignment of the Node. This determines at which point in this * Node's parent that this Node is mounted. * * @param {Object} newValue * @param {number} [newValue.x] The x-axis align to apply. * @param {number} [newValue.y] The y-axis align to apply. * @param {number} [newValue.z] The z-axis align to apply. */ set align(newValue) { this._setPropertyXYZ(Transformable, 'align', newValue) }, get align() { return Super(this).align }, /** * Set the mount point of the Node. * * @param {Object} newValue * @param {number} [newValue.x] The x-axis mountPoint to apply. * @param {number} [newValue.y] The y-axis mountPoint to apply. * @param {number} [newValue.z] The z-axis mountPoint to apply. */ set mountPoint(newValue) { this._setPropertyXYZ(Transformable, 'mountPoint', newValue) }, get mountPoint() { return Super(this).mountPoint }, })) return Transformable })