Source: css3d.element.js

/**
 * CSS 3D engine
 *
 * @category    css3d
 * @package     css3d.element
 * @author      Jan Fischer, bitWorking <info@bitworking.de>
 * @copyright   2014 Jan Fischer
 * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
 */

/**
 * 
 * @name css3d.element
 * @class
 * @param {DOMElement} domElement
 * @returns {css3d.element}
 */
css3d.element = (function()
{

    /**
     * 
     * @param {DOMElement} domElement
     * @returns {css3d.element}
     */
    var element = function(domElement)
    {
        this._domElement = domElement;
        this._pivot = new css3d.vector3(0, 0, 0);
        this._world = css3d.matrix4.identity();
        this._scale = new css3d.vector3(1, 1, 1);
        this._rotation = css3d.matrix4.identity();
        this._translation = new css3d.vector3();
        this._parent = null;
        this._children = [];
        
        this._shaderElement = null;
        this._backfaceCulling = false;
        this._backfaceCullingOld = null;
        
        this._isDirty = true;
        this._isScaled = false;
        this._isRotated = false;
        this._isTranslated = false;
        this._isPivotChanged = false;
        
        /**
         * Set shading on/off
         * @type {Boolean}
         * @memberof! css3d.element
         * @instance
         */
        this.shading = true;
        this.backfaceCullingDirty = false;
        this.worldView = null;
        this.normal = new css3d.vector3(0, 0, 1);
        this.normalWorld = new css3d.vector3(0, 0, 1);
        
        /**
         * Indicates if the element inherits the scaling from an parent element.
         * @type {Boolean}
         * @memberof! css3d.element
         * @instance
         */
        this.inheritScaling = false;
        
        /**
         * Element will get centered in the container element. This makes it easier to position it in 3D space.
         * @type {Boolean}
         * @memberof! css3d.element
         * @instance
         */
        this.autoCenter = true;
        
        /**
         * Set the custom zIndex. Only used if browser doesn't support preserve-3d (IE10)
         * @type {Integer}
         * @memberof! css3d.element
         * @instance
         */
        this.zIndex = null;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {Boolean} hasFilter
     * @param {DOMElement} containerElement
     * @returns {css3d.element}
     */
    element.prototype.init = function(hasFilter, containerElement)
    {
        if (!hasFilter) {
            this._createShaderElement();
        }

        if (null != this._domElement && this.autoCenter) {
            var containerWidth = containerElement.offsetWidth;
            var containerHeight = containerElement.offsetHeight;
            var elementWidth = this._domElement.offsetWidth;
            var elementHeight = this._domElement.offsetHeight;
            this._domElement.style.position = 'absolute';
            this._domElement.style.left = ((containerWidth - elementWidth) / 2) + 'px';
            this._domElement.style.top = ((containerHeight - elementHeight) / 2) + 'px';
        }

        return this;
    };

    element.prototype._createShaderElement = function()
    {
        // TODO: prevents IE10 from clicking links on element

        if (null == this._domElement) {
            return;
        }
        var shaderElement = document.createElement('div');
    	shaderElement.style.position = 'absolute';
    	shaderElement.style.top = '0';
    	shaderElement.style.left = '0';
    	shaderElement.style.width = '100%';
    	shaderElement.style.height = '100%';
        shaderElement.style.backgroundColor = '#000000';
        shaderElement.style.opacity = 0;
        // works only in safari and mozilla
        shaderElement.style.pointerEvents = 'none';
        shaderElement.className = 'css3d-shader';
        this._domElement.appendChild(shaderElement);
        this._shaderElement = shaderElement;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {DOMElement}
     */
    element.prototype.getDomElement = function()
    {
        return this._domElement;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {DOMElement}
     */
    element.prototype.getShaderElement = function()
    {
        return this._shaderElement;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {css3d.element} parent
     * @returns {css3d.element}
     */
    element.prototype.setParent = function(parent)
    {
        this._parent = parent;
        parent.addChild(this);
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {css3d.element} child
     * @returns {css3d.element}
     */
    element.prototype.addChild = function(child)
    {
        this._children.push(child);
        child._parent = this;
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {Array}
     */
    element.prototype.getChildren = function()
    {
        return this._children;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {css3d.element}
     */
    element.prototype.setChildrenDirty = function()
    {
        for (var i=0;i<this._children.length;i++) {
            this._children[i]._isDirty = true;
        }
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {Boolean} value
     * @returns {css3d.element}
     */
    element.prototype.setBackfaceCulling = function(value)
    {
        this._backfaceCulling = value;
        if (value != this._backfaceCullingOld) {
            this.backfaceCullingDirty = true;
            this._backfaceCullingOld = value;
        }
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {Boolean}
     */
    element.prototype.getBackfaceCulling = function()
    {
        return this._backfaceCulling;
    }

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {Array}
     */
    element.prototype.getWorldMatrix = function()
    {
        return this._world;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {Number} x
     * @param {Number} y
     * @param {Number} z
     * @returns {css3d.element}
     */
    element.prototype.setPivot = function(x, y, z)
    {
        this._pivot.x = x;
        this._pivot.y = y;
        this._pivot.z = z;
        if (!this._pivot.isZero()) {
            this._isPivotChanged = true;
        }
        else {
            this._isPivotChanged = false;
        }
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {css3d.vector3}
     */
    element.prototype.getPivot = function()
    {
        return this._pivot;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {css3d.vector3}
     */
    element.prototype.getPivotTransformed = function()
    {
        var transformed = this.getPivot().transform(this._world).toVector3();
        return transformed;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {Number} x
     * @param {Number} y
     * @param {Number} z
     * @returns {css3d.element}
     */
    element.prototype.setScale = function(x, y, z)
    {
        this._scale.x = x;
        this._scale.y = y;
        this._scale.z = z;
        this._isDirty = true;        
        this._isScaled = (x == 1 && y == 1 && z == 1) ? false : true;
        this.setChildrenDirty();
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {css3d.vector3}
     */
    element.prototype.getScale = function()
    {
        return this._scale;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {Number} axis
     * @param {Number} angle
     * @returns {css3d.element}
     */
    element.prototype.setRotation = function(axis, angle)
    {
        this._isRotated = (angle == 0) ? false : true;
        if (this._isRotated) {
            this._rotation = css3d.matrix4.rotationAxis(axis, angle);
        }
        else {
            this._rotation = css3d.matrix4.identity();
        }
        this._isDirty = true;
        this.setChildrenDirty();
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {Number} x
     * @param {Number} y
     * @param {Number} z
     * @returns {css3d.element}
     */
    element.prototype.setRotationXYZ = function(x, y, z)
    {
        x = x || 0;
        y = y || 0;
        z = z || 0;

        this._isRotated = (x == 0 && y == 0 && z == 0) ? false : true;

        var rotation = null;

        if (this._isRotated) {            
            rotation = css3d.matrix4.rotationZ(z);
            rotation = css3d.matrix4.multiply(rotation, css3d.matrix4.rotationY(y));
            rotation = css3d.matrix4.multiply(rotation, css3d.matrix4.rotationX(x));
        }
        else {
            rotation = css3d.matrix4.identity();
        }

        this._rotation = rotation;
        this._isDirty = true;
        this.setChildrenDirty();
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {Array} m
     * @returns {css3d.element}
     */
    element.prototype.setRotationMatrix = function(m)
    {
        this._rotation = m;
        this._isDirty = true;
        this.setChildrenDirty();
        this._isRotated = true;        
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {Array}
     */
    element.prototype.getRotation = function()
    {
        return this._rotation;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {Array}
     */
    element.prototype.getTotalRotation = function()
    {
        var rotation = this._rotation;
        if (null != this._parent) {
            rotation = css3d.matrix4.multiply(this._parent.getTotalRotation(), rotation);
        }
        return rotation;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {Number} x
     * @param {Number} y
     * @param {Number} z
     * @returns {css3d.element}
     */
    element.prototype.setTranslation = function(x, y, z)
    {
        this._translation.x = x;
        this._translation.y = y;
        this._translation.z = z;
        this._isDirty = true;
        this.setChildrenDirty();
        this._isTranslated = (x == 0 && y == 0 && z == 0) ? false : true;
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {css3d.vector3}
     */
    element.prototype.getTranslation = function()
    {
        return this._translation;
    };
    
    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {css3d.vector3}
     */
    element.prototype.getTotalTranslation = function()
    {
        return new css3d.vector3(this._world[3], this._world[7], this._world[11]);
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {css3d.vector3}
     */
    element.prototype.backVector = function()
    {
        return css3d.matrix4.back(this._world).normalize();        
    };
    
    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {css3d.vector3}
     */
    element.prototype.forwardVector = function()
    {
        return css3d.matrix4.forward(this._world).normalize();        
    };

    /**
     * Move forward
     * 
     * @memberof! css3d.element
     * @instance
     * @param {Number} steps
     * @returns {css3d.element}
     */
    element.prototype.forward = function(steps)
    {
        var forwardVector = this.forwardVector();
        this.setTranslation(
            this._translation.x + (forwardVector.x * steps),
            this._translation.y + (forwardVector.y * steps),
            this._translation.z + (forwardVector.z * steps)
        );
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {css3d.vector3}
     */
    element.prototype.rightVector = function()
    {
        return css3d.matrix4.right(this._world).normalize();
    };

    /**
     * Move left
     * 
     * @memberof! css3d.element
     * @instance
     * @param {Number} steps
     * @returns {css3d.element}
     */
    element.prototype.left = function(steps)
    {
        var rightVector = this.rightVector();
        this.setTranslation(
            this._translation.x - (rightVector.x * steps),
            this._translation.y - (rightVector.y * steps),
            this._translation.z - (rightVector.z * steps)
        );
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {css3d.vector3}
     */
    element.prototype.upVector = function()
    {
        return css3d.matrix4.up(this._world).normalize();
    };

    /**
     * Move up
     * 
     * @memberof! css3d.element
     * @instance
     * @param {Number} steps
     * @returns {css3d.element}
     */
    element.prototype.up = function(steps)
    {
        var upVector = this.upVector();
        this.setTranslation(
            this._translation.x + (upVector.x * steps),
            this._translation.y + (upVector.y * steps),
            this._translation.z + (upVector.z * steps)
        );
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {String} styleTransform
     * @param {String} styleBackfaceVisibility
     * @returns {css3d.element}
     */
    element.prototype.setMatrix = function(styleTransform, styleBackfaceVisibility)
    {
        if (null == this._domElement) {
            return this;
        }

        var m = css3d.matrix4.translation(this._translation.x, this._translation.y, this._translation.z);
        m = css3d.matrix4.multiply(m, css3d.matrix4.scale(this._scale.x, this._scale.y, this._scale.z));
        m = css3d.matrix4.multiply(m, this._rotation);

        var s = "matrix3d(";
        s += m[0].toFixed(10) + "," + m[4].toFixed(10) + "," + m[8].toFixed(10) + "," + m[12].toFixed(10) + ",";
        s += m[1].toFixed(10) + "," + m[5].toFixed(10) + "," + m[9].toFixed(10) + "," + m[13].toFixed(10) + ",";
        s += m[2].toFixed(10) + "," + m[6].toFixed(10) + "," + m[10].toFixed(10) + "," + m[14].toFixed(10) + ",";
        s += m[3].toFixed(10) + "," + m[7].toFixed(10) + "," + m[11].toFixed(10) + "," + m[15].toFixed(10);
        s += ")";

        this._domElement.style[styleTransform] = s;

        // backface culling
        var backfaceCulling = 'visible';
        if (this.getBackfaceCulling()) {
            backfaceCulling = 'hidden';
        }
        this._domElement.style[styleBackfaceVisibility] = backfaceCulling;
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {css3d.vector3} from
     * @param {css3d.vector3} to
     * @param {Number} t    0-1
     * @returns {css3d.element}
     */
    element.prototype.lerpScale = function(from, to, t)
    {
        this.setScale(
            css3d.math.lerp(from.x, to.x, t),
            css3d.math.lerp(from.y, to.y, t),
            css3d.math.lerp(from.z, to.z, t)
        );
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {css3d.vector3} from
     * @param {css3d.vector3} to
     * @param {Number} t    0-1
     * @returns {css3d.element}
     */
    element.prototype.lerpTranslation = function(from, to, t)
    {
        this.setTranslation(
            css3d.math.lerp(from.x, to.x, t),
            css3d.math.lerp(from.y, to.y, t),
            css3d.math.lerp(from.z, to.z, t)
        );
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {Array} fromMatrix
     * @param {css3d.quaternion} toQuaternion
     * @param {Number} t    0-1
     * @returns {css3d.element}
     */
    element.prototype.lerpRotation = function(fromMatrix, toQuaternion, t)
    {
        var fromQuaternion = css3d.quaternion.prototype.fromMatrix4(fromMatrix);
        var out = css3d.quaternion.prototype.slerp(fromQuaternion, toQuaternion, t);
        this.setRotationMatrix(out.toMatrix4());
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {css3d.vector3} fromScale
     * @param {css3d.vector3} toScale
     * @param {css3d.vector3} fromTranslation
     * @param {css3d.vector3} toTranslation
     * @param {Array} fromRotationMatrix
     * @param {css3d.quaternion} toQuaternion
     * @param {Number} t    0-1
     * @returns {css3d.element}
     */
    element.prototype.lerp = function(fromScale, toScale, fromTranslation, toTranslation, fromRotationMatrix, toQuaternion, t)
    {
        if (t == null) {
            return this;
        }
        if (fromScale && toScale) {
            this.lerpScale(fromScale, toScale, t);
        }
        if (fromTranslation && toTranslation) {
            this.lerpTranslation(fromTranslation, toTranslation, t);
        }
        if (fromRotationMatrix && toQuaternion) {
            this.lerpRotation(fromRotationMatrix, toQuaternion, t);
        }
        return this;
    };

    /**
     * 
     * @memberof! css3d.element
     * @instance
     * @param {css3d.vector3} toScale
     * @param {css3d.vector3} toTranslation
     * @param {css3d.quaternion} toQuaternion
     * @returns {Function}
     */
    element.prototype.getLerpFunction = function(toScale, toTranslation, toQuaternion)
    {
        var fromScale = this.getScale().clone();
        var fromTranslation = this.getTranslation().clone();
        var fromRotation = this.getRotation();

        var self = this;

        return function(t)
        {
            self.lerp(fromScale, toScale, fromTranslation, toTranslation, fromRotation, toQuaternion, t);
        };
    };

    /**
     * Build the world matrix
     * 
     * @memberof! css3d.element
     * @instance
     * @returns {css3d.element}
     */
    element.prototype.update = function()
    {
        if (this._isDirty) {
            this._world = css3d.matrix4.translation(this._translation.x, this._translation.y, this._translation.z);
            if (this._isScaled) {
                this._world = css3d.matrix4.multiply(this._world, css3d.matrix4.scale(this._scale.x, this._scale.y, this._scale.z));
            }
            if (this._isRotated) {
                if (this._isPivotChanged) {
                    this._world = css3d.matrix4.multiply(this._world, css3d.matrix4.translation(this._pivot.x, this._pivot.y, this._pivot.z));
                }
                this._world = css3d.matrix4.multiply(this._world, this._rotation);
                if (this._isPivotChanged) {
                    this._world = css3d.matrix4.multiply(this._world, css3d.matrix4.translation(-this._pivot.x, -this._pivot.y, -this._pivot.z));
                }
            }

            if (null != this._parent) {
                if (!this.inheritScaling) {
                    if (this._parent._isScaled) {
                        this._world = css3d.matrix4.multiply(
                            css3d.matrix4.scale(1/this._parent.getScale().x, 1/this._parent.getScale().y, 1/this._parent.getScale().z),
                            this._world
                        );
                    }
                }
                // TODO: condition if parent is dirty?
                this._parent.update(); // this seems to be needed if you only call engine.update().render()
                this._world = css3d.matrix4.multiply(this._parent.getWorldMatrix(), this._world);
            }
            
            // transform normal
            // isn't it always the forward vector?
            this.normalWorld = this.normal.transform(this.getTotalRotation());
            this.normalWorld = this.normalWorld.toVector3().normalize();

            this._isDirty = false;

            // TODO: reset other dirty flags ?
        }
        return this;
    };

    return element;

}());