Show:
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * @license MPL 2.0
 * @copyright Famous Industries, Inc. 2014
 */

/* Modified work copyright © 2015-2016 David Valdman */

define(function(require, exports, module) {

    /**
     * A library for creating and composing CSS3 matrix transforms.
     *  A Transform is a 16 element float array `t = [t0, ..., t15]`
     *  that corresponds to a 4x4 transformation matrix (in row-major order)
     *
     *  ```
     *    ┌               ┐
     *    │ t0  t1  t2  0 │
     *    │ t4  t5  t6  0 │
     *    │ t8  t9  t10 0 │
     *    │ t12 t13 t14 1 │
     *    └               ┘
     *  ```
     *
     *  This matrix is a data structure encoding a combination of translation,
     *  scale, skew and rotation components.
     *
     *  Note: these matrices are transposes from their mathematical counterparts.
     *
     *  @example
     *
     *      var layoutNode = var LayoutNode({
     *          transform : Transform.translate([100,200,50])
     *      });
     *
     *  @example
     *
     *      var transitionable = new Transitionable(0);
     *
     *      var transform = transitionable.map(function(value){
     *          return Transform.scaleX(value);
     *      });
     *
     *      var layoutNode = var LayoutNode({
     *          transform : transform
     *      });
     *
     *      transitionable.set(100, {duration : 500});
     *
     * @class Transform
     * @static
     */
    var Transform = {};

    /**
     * Identity transform.
     *
     * @property identity {Array}
     * @static
     * @final
     */
    Transform.identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];

    //TODO: why do inFront/behind need to translate by >1 to overcome DOM order?
    /**
     * Transform for moving a renderable in front of another renderable in the z-direction.
     *
     * @property inFront {Array}
     * @static
     * @final
     */
    Transform.inFront = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1.001, 1];

    /**
     * Transform for moving a renderable behind another renderable in the z-direction.
     *
     * @property behind {Array}
     * @static
     * @final
     */
    Transform.behind = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -1.001, 1];

    /**
     * Compose Transform arrays via matrix multiplication.
     *
     * @method compose
     * @static
     * @param t1 {Transform} Left Transform
     * @param t2 {Transform} Right Transform
     * @return {Array}
     */
    Transform.compose = function multiply(t1, t2) {
        if (t1 === Transform.identity) return t2.slice();
        if (t2 === Transform.identity) return t1.slice();
        return [
            t1[0] * t2[0] + t1[4] * t2[1] + t1[8] * t2[2],
            t1[1] * t2[0] + t1[5] * t2[1] + t1[9] * t2[2],
            t1[2] * t2[0] + t1[6] * t2[1] + t1[10] * t2[2],
            0,
            t1[0] * t2[4] + t1[4] * t2[5] + t1[8] * t2[6],
            t1[1] * t2[4] + t1[5] * t2[5] + t1[9] * t2[6],
            t1[2] * t2[4] + t1[6] * t2[5] + t1[10] * t2[6],
            0,
            t1[0] * t2[8] + t1[4] * t2[9] + t1[8] * t2[10],
            t1[1] * t2[8] + t1[5] * t2[9] + t1[9] * t2[10],
            t1[2] * t2[8] + t1[6] * t2[9] + t1[10] * t2[10],
            0,
            t1[0] * t2[12] + t1[4] * t2[13] + t1[8] * t2[14] + t1[12],
            t1[1] * t2[12] + t1[5] * t2[13] + t1[9] * t2[14] + t1[13],
            t1[2] * t2[12] + t1[6] * t2[13] + t1[10] * t2[14] + t1[14],
            1
        ];
    };

    /**
     * Convenience method to Compose several Transform arrays.
     *
     * @method composeMany
     * @static
     * @param {...Transform}    Transform arrays
     * @return {Array}
     */
    Transform.composeMany = function composeMany(){
        if (arguments.length > 2){
            var first = arguments[0];
            var second = arguments[1];
            Array.prototype.shift.call(arguments);
            arguments[0] = Transform.compose(first, second);
            return Transform.composeMany.apply(null, arguments);
        }
        else return Transform.compose.apply(null, arguments);
    };

    /**
     * Translate a Transform after the Transform is applied.
     *
     * @method thenMove
     * @static
     * @param t {Transform}     Transform
     * @param v {Number[]}      Array of [x,y,z] translation components
     * @return {Array}
     */
    Transform.thenMove = function thenMove(t, v) {
        if (!v[2]) v[2] = 0;
        return [t[0], t[1], t[2], 0, t[4], t[5], t[6], 0, t[8], t[9], t[10], 0, t[12] + v[0], t[13] + v[1], t[14] + v[2], 1];
    };

    /**
     * Translate a Transform before the Transform is applied.
     *
     * @method moveThen
     * @static
     * @param v {Number[]}      Array of [x,y,z] translation components
     * @param t {Transform}     Transform
     * @return {Array}
     */
    Transform.moveThen = function moveThen(v, t) {
        if (!v[2]) v[2] = 0;
        var t0 = v[0] * t[0] + v[1] * t[4] + v[2] * t[8];
        var t1 = v[0] * t[1] + v[1] * t[5] + v[2] * t[9];
        var t2 = v[0] * t[2] + v[1] * t[6] + v[2] * t[10];
        return Transform.thenMove(t, [t0, t1, t2]);
    };

    /**
     * Return a Transform which represents translation by a translation vector.
     *
     * @method translate
     * @static
     * @param v {Number[]}      Translation vector [x,y,z]
     * @return {Array}
     */
    Transform.translate = function translate(v) {
        var x = v[0] || 0;
        var y = v[1] || 0;
        var z = v[2] || 0;
        return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1];
    };

    /**
     * Return a Transform which represents translation in the x-direction.
     *
     * @method translateX
     * @static
     * @param x {Number}        Translation amount
     */
    Transform.translateX = function translateX(x) {
        return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, 0, 0, 1];
    };

    /**
     * Return a Transform which represents translation in the y-direction.
     *
     * @method translateY
     * @static
     * @param y {Number}        Translation amount
     */
    Transform.translateY = function translateY(y) {
        return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, y, 0, 1];
    };

    /**
     * Return a Transform which represents translation in the z-direction.
     *
     * @method translateZ
     * @static
     * @param z {Number}        Translation amount
     */
    Transform.translateZ = function translateZ(z) {
        return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, z, 1];
    };

    /**
     * Return a Transform which represents a scaling by specified amounts in each dimension.
     *
     * @method scale
     * @static
     * @param v {Number[]}      Scale vector [x,y,z]
     * @return {Array}
     */
    Transform.scale = function scale(v) {
        var x = (v[0] !== undefined) ? v[0] : 1;
        var y = (v[1] !== undefined) ? v[1] : 1;
        var z = (v[2] !== undefined) ? v[2] : 1;
        return [x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1];
    };

    /**
     * Return a Transform which represents scaling in the x-direction.
     *
     * @method scaleX
     * @static
     * @param x {Number}        Scale amount
     */
    Transform.scaleX = function scaleX(x) {
        return [x, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
    };

    /**
     * Return a Transform which represents scaling in the y-direction.
     *
     * @method scaleY
     * @static
     * @param y {Number}        Scale amount
     */
    Transform.scaleY = function scaleY(y) {
        return [1, 0, 0, 0, 0, y, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
    };

    /**
     * Return a Transform which represents scaling in the z-direction.
     *
     * @method scaleZ
     * @static
     * @param z {Number}        Scale amount
     */
    Transform.scaleZ = function scaleZ(z) {
        return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, z, 0, 0, 0, 0, 1];
    };

    /**
     * Scale a Transform after the Transform is applied.
     *
     * @method thenScale
     * @static
     * @param t {Transform}     Transform
     * @param v {Number[]}      Array of [x,y,z] scale components
     * @return {Array}
     */
    Transform.thenScale = function thenScale(t, v) {
        var x = (v[0] !== undefined) ? v[0] : 1;
        var y = (v[1] !== undefined) ? v[1] : 1;
        var z = (v[2] !== undefined) ? v[2] : 1;
        return [
            x * t[0],  y * t[1],  z * t[2],  0,
            x * t[4],  y * t[5],  z * t[6],  0,
            x * t[8],  y * t[9],  z * t[10], 0,
            x * t[12], y * t[13], z * t[14], 1
        ];
    };

    /**
     * Return a Transform representing a clockwise rotation around the x-axis.
     *
     * @method rotateX
     * @static
     * @param angle {Number}    Angle in radians
     * @return {Array}
     */
    Transform.rotateX = function rotateX(angle) {
        var cosTheta = Math.cos(angle);
        var sinTheta = Math.sin(angle);
        return [1, 0, 0, 0, 0, cosTheta, sinTheta, 0, 0, -sinTheta, cosTheta, 0, 0, 0, 0, 1];
    };

    /**
     * Return a Transform representing a clockwise rotation around the y-axis.
     *
     * @method rotateY
     * @static
     * @param angle {Number}    Angle in radians
     * @return {Array}
     */
    Transform.rotateY = function rotateY(angle) {
        var cosTheta = Math.cos(angle);
        var sinTheta = Math.sin(angle);
        return [cosTheta, 0, -sinTheta, 0, 0, 1, 0, 0, sinTheta, 0, cosTheta, 0, 0, 0, 0, 1];
    };

    /**
     * Return a Transform representing a clockwise rotation around the z-axis.
     *
     * @method rotateX
     * @static
     * @param angle {Number}    Angle in radians
     * @return {Array}
     */
    Transform.rotateZ = function rotateZ(theta) {
        var cosTheta = Math.cos(theta);
        var sinTheta = Math.sin(theta);
        return [cosTheta, sinTheta, 0, 0, -sinTheta, cosTheta, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
    };

    /**
     * Return a Transform representation of a skew in the x-direction
     *
     * @method skewX
     * @static
     * @param angle {Number}    The angle between the top and left sides
     * @return {Array}
     */
    Transform.skewX = function skewX(angle) {
        return [1, 0, 0, 0, Math.tan(angle), 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
    };

    /**
     * Return a Transform representation of a skew in the y-direction
     *
     * @method skewY
     * @static
     * @param angle {Number}    The angle between the bottom and right sides
     * @return {Array}
     */
    Transform.skewY = function skewY(angle) {
        return [1, Math.tan(angle), 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
    };

    /**
     * Return a Transform which represents an axis-angle rotation.
     *
     * @method rotateAxis
     * @static
     * @param v {Number[]}   Unit vector representing the axis to rotate about
     * @param angle {Number} Radians to rotate clockwise about the axis
     * @return {Array}
     */
    Transform.rotateAxis = function rotateAxis(v, angle) {
        var sinTheta = Math.sin(angle);
        var cosTheta = 1 - Math.cos(angle);
        var verTheta = 1 - cosTheta; // versine of theta

        var xxV = v[0] * v[0] * verTheta;
        var xyV = v[0] * v[1] * verTheta;
        var xzV = v[0] * v[2] * verTheta;
        var yyV = v[1] * v[1] * verTheta;
        var yzV = v[1] * v[2] * verTheta;
        var zzV = v[2] * v[2] * verTheta;
        var xs = v[0] * sinTheta;
        var ys = v[1] * sinTheta;
        var zs = v[2] * sinTheta;

        return [
            xxV + cosTheta, xyV + zs, xzV - ys, 0,
            xyV - zs, yyV + cosTheta, yzV + xs, 0,
            xzV + ys, yzV - xs, zzV + cosTheta, 0,
            0, 0, 0, 1
        ];
    };

    /**
     * Return a Transform which represents a Transform applied about an origin point.
     *  Useful for rotating and scaling relative to an origin.
     *
     * @method aboutOrigin
     * @static
     * @param v {Number[]}          Origin point [x,y,z]
     * @param t {Transform}         Transform
     * @return {Array}
     */
    Transform.aboutOrigin = function aboutOrigin(v, t) {
        v[2] = v[2] || 0;
        var t0 = v[0] - (v[0] * t[0] + v[1] * t[4] + v[2] * t[8]);
        var t1 = v[1] - (v[0] * t[1] + v[1] * t[5] + v[2] * t[9]);
        var t2 = v[2] - (v[0] * t[2] + v[1] * t[6] + v[2] * t[10]);
        return Transform.thenMove(t, [t0, t1, t2]);
    };

    /**
     * Returns a perspective Transform.
     *
     * @method perspective
     * @static
     * @param focusZ {Number}       z-depth of focal point
     * @return {Array}
     */
    Transform.perspective = function perspective(focusZ) {
        return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -1 / focusZ, 0, 0, 0, 1];
    };

    /**
     * Return translation vector component of the given Transform.
     *
     * @method getTranslate
     * @static
     * @param t {Transform}         Transform
     * @return {Number[]}
     */
    Transform.getTranslate = function getTranslate(t) {
        return [t[12], t[13], t[14]];
    };

    /**
     * Return inverse Transform for given Transform.
     *   Note: will provide incorrect results if Transform is not invertible.
     *
     * @method inverse
     * @static
     * @param t {Transform} Transform
     * @return {Array}
     */
    Transform.inverse = function inverse(t) {
        // only need to consider 3x3 section for affine
        var c0 = t[5] * t[10] - t[6] * t[9];
        var c1 = t[4] * t[10] - t[6] * t[8];
        var c2 = t[4] * t[9] - t[5] * t[8];
        var c4 = t[1] * t[10] - t[2] * t[9];
        var c5 = t[0] * t[10] - t[2] * t[8];
        var c6 = t[0] * t[9] - t[1] * t[8];
        var c8 = t[1] * t[6] - t[2] * t[5];
        var c9 = t[0] * t[6] - t[2] * t[4];
        var c10 = t[0] * t[5] - t[1] * t[4];
        var detM = t[0] * c0 - t[1] * c1 + t[2] * c2;
        var invD = 1 / detM;
        var result = [
            invD * c0, -invD * c4, invD * c8, 0,
            -invD * c1, invD * c5, -invD * c9, 0,
            invD * c2, -invD * c6, invD * c10, 0,
            0, 0, 0, 1
        ];
        result[12] = -t[12] * result[0] - t[13] * result[4] - t[14] * result[8];
        result[13] = -t[12] * result[1] - t[13] * result[5] - t[14] * result[9];
        result[14] = -t[12] * result[2] - t[13] * result[6] - t[14] * result[10];
        return result;
    };

    /**
     * Returns the transpose of a Transform.
     *
     * @method transpose
     * @static
     * @param t {Transform}     Transform
     * @return {Array}
     */
    Transform.transpose = function transpose(t) {
        return [t[0], t[4], t[8], t[12], t[1], t[5], t[9], t[13], t[2], t[6], t[10], t[14], t[3], t[7], t[11], t[15]];
    };

    function _normSquared(v) {
        return (v.length === 2)
            ? v[0] * v[0] + v[1] * v[1]
            : v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
    }
    function _norm(v) {
        return Math.sqrt(_normSquared(v));
    }
    function _sign(n) {
        return (n < 0) ? -1 : 1;
    }

    /**
     * Decompose Transform into separate `translate`, `rotate`, `scale` and `skew` components.
     *
     * @method interpret
     * @static
     * @private
     * @param t {Transform}     Transform
     * @return {Object}
     */
    Transform.interpret = function interpret(t) {
        // QR decomposition via Householder reflections
        // FIRST ITERATION

        //default Q1 to the identity matrix;
        var x = [t[0], t[1], t[2]];                // first column vector
        var sgn = _sign(x[0]);                     // sign of first component of x (for stability)
        var xNorm = _norm(x);                      // norm of first column vector
        var v = [x[0] + sgn * xNorm, x[1], x[2]];  // v = x + sign(x[0])|x|e1
        var mult = 2 / _normSquared(v);            // mult = 2/v'v

        //bail out if our Matrix is singular
        if (mult >= Infinity) {
            return {
                translate: Transform.getTranslate(t),
                rotate: [0, 0, 0],
                scale: [0, 0, 0],
                skew: [0, 0, 0]
            };
        }

        //evaluate Q1 = I - 2vv'/v'v
        var Q1 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];

        //diagonals
        Q1[0]  = 1 - mult * v[0] * v[0];    // 0,0 entry
        Q1[5]  = 1 - mult * v[1] * v[1];    // 1,1 entry
        Q1[10] = 1 - mult * v[2] * v[2];    // 2,2 entry

        //upper diagonal
        Q1[1] = -mult * v[0] * v[1];        // 0,1 entry
        Q1[2] = -mult * v[0] * v[2];        // 0,2 entry
        Q1[6] = -mult * v[1] * v[2];        // 1,2 entry

        //lower diagonal
        Q1[4] = Q1[1];                      // 1,0 entry
        Q1[8] = Q1[2];                      // 2,0 entry
        Q1[9] = Q1[6];                      // 2,1 entry

        //reduce first column of M
        var MQ1 = Transform.compose(Q1, t);

        // SECOND ITERATION on (1,1) minor
        var x2 = [MQ1[5], MQ1[6]];
        var sgn2 = _sign(x2[0]);                    // sign of first component of x (for stability)
        var x2Norm = _norm(x2);                     // norm of first column vector
        var v2 = [x2[0] + sgn2 * x2Norm, x2[1]];    // v = x + sign(x[0])|x|e1
        var mult2 = 2 / _normSquared(v2);           // mult = 2/v'v

        //evaluate Q2 = I - 2vv'/v'v
        var Q2 = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];

        //diagonal
        Q2[5]  = 1 - mult2 * v2[0] * v2[0]; // 1,1 entry
        Q2[10] = 1 - mult2 * v2[1] * v2[1]; // 2,2 entry

        //off diagonals
        Q2[6] = -mult2 * v2[0] * v2[1];     // 2,1 entry
        Q2[9] = Q2[6];                      // 1,2 entry

        //calc QR decomposition. Q = Q1*Q2, R = Q'*M
        var Q = Transform.compose(Q2, Q1);      //note: really Q transpose
        var R = Transform.compose(Q, t);

        //remove negative scaling
        var remover = Transform.scale(R[0] < 0 ? -1 : 1, R[5] < 0 ? -1 : 1, R[10] < 0 ? -1 : 1);
        R = Transform.compose(R, remover);
        Q = Transform.compose(remover, Q);

        //decompose into rotate/scale/skew matrices
        var result = {};
        result.translate = Transform.getTranslate(t);
        result.rotate = [Math.atan2(-Q[6], Q[10]), Math.asin(Q[2]), Math.atan2(-Q[1], Q[0])];
        if (!result.rotate[0]) {
            result.rotate[0] = 0;
            result.rotate[2] = Math.atan2(Q[4], Q[5]);
        }
        result.scale = [R[0], R[5], R[10]];
        result.skew = [Math.atan2(R[9], result.scale[2]), Math.atan2(R[8], result.scale[2]), Math.atan2(R[4], result.scale[0])];

        //double rotation workaround
        if (Math.abs(result.rotate[0]) + Math.abs(result.rotate[2]) > 1.5 * Math.PI) {
            result.rotate[1] = Math.PI - result.rotate[1];
            if (result.rotate[1] > Math.PI) result.rotate[1] -= 2 * Math.PI;
            if (result.rotate[1] < -Math.PI) result.rotate[1] += 2 * Math.PI;
            if (result.rotate[0] < 0) result.rotate[0] += Math.PI;
            else result.rotate[0] -= Math.PI;
            if (result.rotate[2] < 0) result.rotate[2] += Math.PI;
            else result.rotate[2] -= Math.PI;
        }

        return result;
    };

    /**
     * Compose .translate, .rotate, .scale and .skew components into a Transform matrix.
     *  The "inverse" of .interpret.
     *
     * @method build
     * @static
     * @private
     * @param spec {Object} Object with keys "translate, rotate, scale, skew" and their vector values
     * @return {Array}
     */
    Transform.build = function build(spec) {
        var scaleMatrix = Transform.scale(spec.scale[0], spec.scale[1], spec.scale[2]);
        var skewMatrix = Transform.skew(spec.skew[0], spec.skew[1], spec.skew[2]);
        var rotateMatrix = Transform.rotate(spec.rotate[0], spec.rotate[1], spec.rotate[2]);
        return Transform.thenMove(
            Transform.compose(Transform.compose(rotateMatrix, skewMatrix), scaleMatrix),
            spec.translate
        );
    };

    /**
     * Weighted average between two matrices by averaging their
     *  translation, rotation, scale, skew components.
     *  f(M1,M2,t) = (1 - t) * M1 + t * M2
     *
     * @method average
     * @static
     * @param M1 {Transform}    M1 = f(M1,M2,0) Transform
     * @param M2 {Transform}    M2 = f(M1,M2,1) Transform
     * @param [t=1/2] {Number}
     * @return {Array}
     */
    Transform.average = function average(M1, M2, t) {
        t = (t === undefined) ? 0.5 : t;
        var specM1 = Transform.interpret(M1);
        var specM2 = Transform.interpret(M2);

        var specAvg = {
            translate: [0, 0, 0],
            rotate: [0, 0, 0],
            scale: [0, 0, 0],
            skew: [0, 0, 0]
        };

        for (var i = 0; i < 3; i++) {
            specAvg.translate[i] = (1 - t) * specM1.translate[i] + t * specM2.translate[i];
            specAvg.rotate[i] = (1 - t) * specM1.rotate[i] + t * specM2.rotate[i];
            specAvg.scale[i] = (1 - t) * specM1.scale[i] + t * specM2.scale[i];
            specAvg.skew[i] = (1 - t) * specM1.skew[i] + t * specM2.skew[i];
        }

        return Transform.build(specAvg);
    };

    /**
     * Determine if two Transforms are component-wise equal.
     *
     * @method equals
     * @static
     * @param a {Transform}     Transform
     * @param b {Transform}     Transform
     * @return {Boolean}
     */
    Transform.equals = function equals(a, b) {
        return !Transform.notEquals(a, b);
    };

    /**
     * Determine if two Transforms are component-wise unequal
     *
     * @method notEquals
     * @static
     * @param a {Transform}     Transform
     * @param b {Transform}     Transform
     * @return {Boolean}
     */
    Transform.notEquals = function notEquals(a, b) {
        if (a === b) return false;

        return !(a && b) ||
            a[12] !== b[12] || a[13] !== b[13] || a[14] !== b[14] ||
            a[0] !== b[0] || a[1] !== b[1] || a[2] !== b[2] ||
            a[4] !== b[4] || a[5] !== b[5] || a[6] !== b[6] ||
            a[8] !== b[8] || a[9] !== b[9] || a[10] !== b[10];
    };

    module.exports = Transform;
});