infamous
Version:
A CSS3D/WebGL UI library.
173 lines (146 loc) • 6.51 kB
JavaScript
import Class from 'lowclass'
import {native} from 'lowclass/native'
import Node from './Node'
import Motor from './Motor'
// fallback to experimental CSS transform if browser doesn't have it (fix for Safari 9)
if (typeof document.createElement('div').style.transform == 'undefined') {
if ( typeof CSSStyleDeclaration !== 'undefined' ) { // doesn't exist in Jest+@skatejs/ssr environment
Object.defineProperty(CSSStyleDeclaration.prototype, 'transform', {
set(value) {
this.webkitTransform = value
},
get() {
return this.webkitTransform
},
enumerable: true,
})
}
}
/**
* Manages a DOM element. Exposes a set of recommended APIs for working with
* DOM efficiently. Currently doesn't do much yet...
*/
export default
Class('ElementOperations', {
element: null,
constructor(element) {
this.element = element
},
/**
* @param {Array.string} classes An array of class names to add to the
* managed element.
*
* Note: updating class names with `el.classList.add()` won't thrash the
* layout. See: http://www.html5rocks.com/en/tutorials/speed/animations
*/
setClasses (...classes) {
if (classes.length) this.element.classList.add(...classes)
return this
},
/**
* Apply a style property to the element.
*
* @private
* @param {string} property The CSS property we will a apply.
* @param {string} value The value the CSS property wil have.
*/
applyStyle(property, value) {
this.element.style[property] = value
},
add(child) {
this.element.appendChild(child)
},
remove(child) {
// This conditional check is needed incase the element was already
// removed from the HTML-API side.
if (child.parentNode === this.element)
this.element.removeChild(child)
},
connectChildElement(child) {
if (
// When using the imperative API, this statement is
// true, so the DOM elements need to be connected.
!child.parentNode
// This condition is irrelevant when strictly using the
// imperative API. However, it is possible that when
// using the HTML API that the HTML-API node can be placed
// somewhere that isn't another HTML-API node, and the
// imperative Node can be gotten and used to add the
// node to another imperative Node. In this case, the
// HTML-API node will be added to the proper HTMLparent.
|| (child.parentElement &&
child.parentElement !== this.element)
// When an HTML-API node is already child of the
// relevant parent, or it is child of a shadow root of
// the relevant parent, there there's nothing to do,
// everything is already as expected, so the following
// conditional body is skipped.
) {
this.add(child)
}
},
disconnectChildElement(child) {
// If DeclarativeBase#remove was called first, we don't need to
// call this again.
if (!child.parentNode) return
this.remove(child)
},
/**
* Apply the DOMMatrix value to the style of this Node's element.
*/
applyTransform (domMatrix) {
// for now, template strings need to be on one line, otherwise Meteor
// users will have bugs from Meteor's injected line numbers. See:
// https://github.com/meteor/meteor/issues/9160
//
// THREE-COORDS-TO-DOM-COORDS
// -- We negate the 13th matrix value to make the DOM's positive Y
// direction downward again because we first negated the value in
// Transformable when calculating world transforms so that
// Three.js positive Y would go downward like DOM.
// -- We also translate the DOM element into the middle of the view
// (similar to align and mountPoint values of 0.5) so that the DOM
// element is aligned with the Three mesh in the middle of the view,
// then in Transformable#_calculateMatrix we adjust the world matrix
// back into DOM coordinates at the top/left.
// -- We apply opposite X rotation to counter the negated X rotation in
// Transformable for the Three.js objects.
//
// TODO #66: moving _calcSize to a render task affets this code
const el = this.element
const elSize = el._calculatedSize
const parentSize = el.parent._calculatedSize
// THREE-COORDS-TO-DOM-COORDS: moves DOM elements to the Three.js
// coordinate space (align and mountPoint are in the middle of the
// view). The threeJsPostAdjustment in Transformable moves both the
// pre-adjusted DOM element and the Three objects into the top/left
// coordinate space.
const threeJsPreAdjustment = `translate3d(calc(${parentSize.x/2}px - ${elSize.x/2}px), calc(${parentSize.y/2}px - ${elSize.y/2}px), 0px)`
const cssMatrixString = `${threeJsPreAdjustment} matrix3d( ${ domMatrix.m11 }, ${ domMatrix.m12 }, ${ domMatrix.m13 }, ${ domMatrix.m14 }, ${ domMatrix.m21 }, ${ domMatrix.m22 }, ${ domMatrix.m23 }, ${ domMatrix.m24 }, ${ domMatrix.m31 }, ${ domMatrix.m32 }, ${ domMatrix.m33 }, ${ domMatrix.m34 }, ${ domMatrix.m41 }, ${ -domMatrix.m42 }, ${ domMatrix.m43 }, ${ domMatrix.m44 })`;
// THREE-COORDS-TO-DOM-COORDS: rotate X and Z the opposite direction for Three.js
domMatrix.rotateAxisAngleSelf( 0, 0, 1, -2 * el.rotation.z )
domMatrix.rotateAxisAngleSelf( 1, 0, 0, -2 * el.rotation.x )
this.applyStyle('transform', cssMatrixString)
},
/**
* [applySize description]
*/
applySize (size) {
const {x,y} = size
this.applyStyle('width', `${x}px`)
this.applyStyle('height', `${y}px`)
// NOTE: we ignore the Z axis on elements, since they are flat.
},
applyOpacity(opacity) {
this.applyStyle('opacity', opacity)
},
applyImperativeNodeProperties(node) {
// Only Node is Transformable
if (node instanceof Node) {
this.applyOpacity(node._properties.opacity)
this.applyTransform(node._properties.transform)
}
// But both Node and Scene are Sizeable
this.applySize(node._calculatedSize)
},
})