Quaternion.js

(function() {

    'use strict';

    var Vec3 = require('./Vec3');
    var Mat33 = require('./Mat33');
    var Mat44 = require('./Mat44');

    /**
     * Instantiates a Quaternion object.
     * @class Quaternion
     * @classdesc A quaternion representing an orientation.
     */
    function Quaternion() {
        switch ( arguments.length ) {
            case 1:
                // array or Quaternion argument
                var argument = arguments[0];
                if ( argument.w !== undefined ) {
                    this.w = argument.w;
                } else if ( argument[0] !== undefined ) {
                    this.w = argument[0];
                } else {
                    this.w = 1.0;
                }
                this.x = argument.x || argument[1] || 0.0;
                this.y = argument.y || argument[2] || 0.0;
                this.z = argument.z || argument[3] || 0.0;
                break;
            case 4:
                // individual component arguments
                this.w = arguments[0];
                this.x = arguments[1];
                this.y = arguments[2];
                this.z = arguments[3];
                break;
            default:
                this.w = 1;
                this.x = 0;
                this.y = 0;
                this.z = 0;
                break;
        }
    }

    /**
     * Returns a quaternion that represents an oreintation matching
     * the identity matrix.
     * @memberof Quaternion
     *
     * @returns {Quaternion} The identity quaternion.
     */
    Quaternion.identity = function() {
        return new Quaternion( 1, 0, 0, 0 );
    };

    /**
     * Returns a new Quaternion with each component negated.
     * @memberof Quaternion
     *
     * @returns {Quaternion} The negated quaternion.
     */
     Quaternion.prototype.negate = function() {
        return new Quaternion( -this.w, -this.x, -this.y, -this.z );
    };

    /**
     * Concatenates the rotations of the two quaternions, returning
     * a new Quaternion object.
     * @memberof Quaternion
     *
     * @param {Quaternion|Array} that - The quaterion to concatenate.
     *
     * @returns {Quaternion} The resulting concatenated quaternion.
     */
    Quaternion.prototype.multQuat = function( that ) {
        that = ( that instanceof Array ) ? new Quaternion( that ) : that;
        var w = (that.w * this.w) - (that.x * this.x) - (that.y * this.y) - (that.z * this.z),
            x = this.y*that.z - this.z*that.y + this.w*that.x + this.x*that.w,
            y = this.z*that.x - this.x*that.z + this.w*that.y + this.y*that.w,
            z = this.x*that.y - this.y*that.x + this.w*that.z + this.z*that.w;
        return new Quaternion( w, x, y, z );
    };

    /**
     * Applies the orientation of the quaternion as a rotation
     * matrix to the provided vector, returning a new Vec3 object.
     * @memberof Quaternion
     *
     * @param {Vec3|Vec4|Array} that - The vector to rotate.
     *
     * @returns {Vec3} The resulting rotated vector.
     */
    Quaternion.prototype.rotate = function( that ) {
        that = ( that instanceof Array ) ? new Vec3( that ) : that;
        var vq = new Quaternion( 0, that.x, that.y, that.z ),
            r = this.multQuat( vq ).multQuat( this.inverse() );
        return new Vec3( r.x, r.y, r.z );
    };

    /**
     * Returns the x-axis of the rotation matrix that the quaternion represents.
     * @memberof Quaternion
     *
     * @returns {Vec3} The x-axis of the rotation matrix represented by the quaternion.
     */
    Quaternion.prototype.xAxis = function() {
        var yy = this.y*this.y,
            zz = this.z*this.z,
            xy = this.x*this.y,
            xz = this.x*this.z,
            yw = this.y*this.w,
            zw = this.z*this.w;
        return new Vec3(
            1 - 2*yy - 2*zz,
            2*xy + 2*zw,
            2*xz - 2*yw ).normalize();
    };

    /**
     * Returns the y-axis of the rotation matrix that the quaternion represents.
     * @memberof Quaternion
     *
     * @returns {Vec3} The y-axis of the rotation matrix represented by the quaternion.
     */
    Quaternion.prototype.yAxis = function() {
        var xx = this.x*this.x,
            zz = this.z*this.z,
            xy = this.x*this.y,
            xw = this.x*this.w,
            yz = this.y*this.z,
            zw = this.z*this.w;
        return new Vec3(
            2*xy - 2*zw,
            1 - 2*xx - 2*zz,
            2*yz + 2*xw ).normalize();
    };

    /**
     * Returns the z-axis of the rotation matrix that the quaternion represents.
     * @memberof Quaternion
     *
     * @returns {Vec3} The z-axis of the rotation matrix represented by the quaternion.
     */
    Quaternion.prototype.zAxis = function() {
        var xx = this.x*this.x,
            yy = this.y*this.y,
            xz = this.x*this.z,
            xw = this.x*this.w,
            yz = this.y*this.z,
            yw = this.y*this.w;
        return new Vec3(
            2*xz + 2*yw,
            2*yz - 2*xw,
            1 - 2*xx - 2*yy ).normalize();
    };

    /**
     * Returns the axes of the rotation matrix that the quaternion represents.
     * @memberof Quaternion
     *
     * @returns {Object} The axes of the matrix represented by the quaternion.
     */
    Quaternion.prototype.axes = function() {
        var xx = this.x*this.x,
            yy = this.y*this.y,
            zz = this.z*this.z,
            xy = this.x*this.y,
            xz = this.x*this.z,
            xw = this.x*this.w,
            yz = this.y*this.z,
            yw = this.y*this.w,
            zw = this.z*this.w;
        return {
            x: new Vec3( 1 - 2*yy - 2*zz, 2*xy + 2*zw, 2*xz - 2*yw ),
            y: new Vec3( 2*xy - 2*zw, 1 - 2*xx - 2*zz, 2*yz + 2*xw ),
            z: new Vec3( 2*xz + 2*yw, 2*yz - 2*xw, 1 - 2*xx - 2*yy )
        };
    };

    /**
     * Returns the rotation matrix that the quaternion represents.
     * @memberof Quaternion
     *
     * @returns {Mat33} The rotation matrix represented by the quaternion.
     */
    Quaternion.prototype.matrix = function() {
        var xx = this.x*this.x,
            yy = this.y*this.y,
            zz = this.z*this.z,
            xy = this.x*this.y,
            xz = this.x*this.z,
            xw = this.x*this.w,
            yz = this.y*this.z,
            yw = this.y*this.w,
            zw = this.z*this.w;
        return new Mat33([
            1 - 2*yy - 2*zz, 2*xy + 2*zw, 2*xz - 2*yw,
            2*xy - 2*zw, 1 - 2*xx - 2*zz, 2*yz + 2*xw,
            2*xz + 2*yw, 2*yz - 2*xw, 1 - 2*xx - 2*yy ]);
    };

    /**
     * Returns a quaternion representing the rotation defined by an axis
     * and an angle.
     * @memberof Quaternion
     *
     * @param {number} angle - The angle of the rotation, in radians.
     * @param {Vec3|Array} axis - The axis of the rotation.
     *
     * @returns {Quaternion} The quaternion representing the rotation.
     */
    Quaternion.rotation = function( angle, axis ) {
        if ( axis instanceof Array ) {
            axis = new Vec3( axis );
        }
        // normalize arguments
        axis = axis.normalize();
        // set quaternion for the equivolent rotation
        var modAngle = ( angle > 0 ) ? angle % (2*Math.PI) : angle % (-2*Math.PI),
            sina = Math.sin( modAngle/2 ),
            cosa = Math.cos( modAngle/2 );
        return new Quaternion(
            cosa,
            axis.x * sina,
            axis.y * sina,
            axis.z * sina ).normalize();
    };

    /**
     * Returns a rotation matrix to rotate a vector from one direction to
     * another.
     * @memberof Quaternion
     *
     * @param {Vec3} from - The starting direction.
     * @param {Vec3} to - The ending direction.
     *
     * @returns {Quaternion} The quaternion representing the rotation.
     */
    Quaternion.rotationFromTo = function( fromVec, toVec ) {
        fromVec = new Vec3( fromVec );
        toVec = new Vec3( toVec );
        var cross = fromVec.cross( toVec );
        var dot = fromVec.dot( toVec );
        var fLen = fromVec.length();
        var tLen = toVec.length();
        var w = Math.sqrt( ( fLen * fLen ) * ( tLen * tLen ) ) + dot;
        return new Quaternion( w, cross.x, cross.y, cross.z ).normalize();
    };

    /**
     * Returns a quaternion that has been spherically interpolated between
     * two provided quaternions for a given t value.
     * @memberof Quaternion
     *
     * @param {Quaternion} fromRot - The rotation at t = 0.
     * @param {Quaternion} toRot - The rotation at t = 1.
     * @param {number} t - The t value, from 0 to 1.
     *
     * @returns {Quaternion} The quaternion representing the interpolated rotation.
     */
    Quaternion.slerp = function( fromRot, toRot, t ) {
        if ( fromRot instanceof Array ) {
            fromRot = new Quaternion( fromRot );
        }
        if ( toRot instanceof Array ) {
            toRot = new Quaternion( toRot );
        }
        // calculate angle between
        var cosHalfTheta = ( fromRot.w * toRot.w ) +
            ( fromRot.x * toRot.x ) +
            ( fromRot.y * toRot.y ) +
            ( fromRot.z * toRot.z );
        // if fromRot=toRot or fromRot=-toRot then theta = 0 and we can return from
        if ( Math.abs( cosHalfTheta ) >= 1 ) {
            return new Quaternion(
                fromRot.w,
                fromRot.x,
                fromRot.y,
                fromRot.z );
        }
        // cosHalfTheta must be positive to return the shortest angle
        if ( cosHalfTheta < 0 ) {
            fromRot = fromRot.negate();
            cosHalfTheta = -cosHalfTheta;
        }
        var halfTheta = Math.acos( cosHalfTheta );
        var sinHalfTheta = Math.sqrt( 1 - cosHalfTheta * cosHalfTheta );
        var scaleFrom = Math.sin( ( 1.0 - t ) * halfTheta ) / sinHalfTheta;
        var scaleTo = Math.sin( t * halfTheta ) / sinHalfTheta;
        return new Quaternion(
            fromRot.w * scaleFrom + toRot.w * scaleTo,
            fromRot.x * scaleFrom + toRot.x * scaleTo,
            fromRot.y * scaleFrom + toRot.y * scaleTo,
            fromRot.z * scaleFrom + toRot.z * scaleTo );
    };

    /**
     * Returns true if the vector components match those of a provided vector.
     * An optional epsilon value may be provided.
     * @memberof Quaternion
     *
     * @param {Quaternion|Array} - The vector to calculate the dot product with.
     * @param {number} - The epsilon value. Optional.
     *
     * @returns {boolean} Whether or not the vector components match.
     */
    Quaternion.prototype.equals = function( that, epsilon ) {
        var w = that.w !== undefined ? that.w : that[0],
            x = that.x !== undefined ? that.x : that[1],
            y = that.y !== undefined ? that.y : that[2],
            z = that.z !== undefined ? that.z : that[3];
        epsilon = epsilon === undefined ? 0 : epsilon;
        return ( this.w === w || Math.abs( this.w - w ) <= epsilon ) &&
            ( this.x === x || Math.abs( this.x - x ) <= epsilon ) &&
            ( this.y === y || Math.abs( this.y - y ) <= epsilon ) &&
            ( this.z === z || Math.abs( this.z - z ) <= epsilon );
    };

    /**
     * Returns a new Quaternion of unit length.
     * @memberof Quaternion
     *
     * @returns {Quaternion} The quaternion of unit length.
     */
    Quaternion.prototype.normalize = function() {
        var mag = Math.sqrt(
                this.x*this.x +
                this.y*this.y +
                this.z*this.z +
                this.w*this.w );
        if ( mag !== 0 ) {
            return new Quaternion(
                this.w / mag,
                this.x / mag,
                this.y / mag,
                this.z / mag );
        }
        return new Quaternion();
    };

    /**
     * Returns the conjugate of the quaternion.
     * @memberof Quaternion
     *
     * @returns {Quaternion} The conjugate of the quaternion.
     */
    Quaternion.prototype.conjugate = function() {
         return new Quaternion( this.w, -this.x, -this.y, -this.z );
    };

    /**
     * Returns the inverse of the quaternion.
     * @memberof Quaternion
     *
     * @returns {Quaternion} The inverse of the quaternion.
     */
    Quaternion.prototype.inverse = function() {
        return this.conjugate();
    };

    /**
     * Returns a random Quaternion of unit length.
     * @memberof Quaternion
     *
     * @returns {Quaternion} A random vector of unit length.
     */
    Quaternion.random = function() {
        var axis = Vec3.random().normalize(),
            angle = Math.random();
        return Quaternion.rotation( angle, axis );
    };

    /**
     * Returns a string representation of the quaternion.
     * @memberof Quaternion
     *
     * @returns {String} The string representation of the quaternion.
     */
    Quaternion.prototype.toString = function() {
        return this.x + ', ' + this.y + ', ' + this.z + ', ' + this.w;
    };

    /**
     * Returns an array representation of the quaternion.
     * @memberof Quaternion
     *
     * @returns {Array} The quaternion as an array.
     */
    Quaternion.prototype.toArray = function() {
        return [  this.w, this.x, this.y, this.z ];
    };

    /**
     * Decomposes the matrix into the corresponding rotation, and scale components.
     * a scale.
     * @memberof Mat33
     *
     * @returns {Object} The decomposed components of the matrix.
     */
    Mat33.prototype.decompose = function() {
        // axis vectors
        var x = new Vec3( this.data[0], this.data[1], this.data[2] );
        var y = new Vec3( this.data[3], this.data[4], this.data[5] );
        var z = new Vec3( this.data[6], this.data[7], this.data[8] );
        // scale needs unnormalized vectors
        var scale = new Vec3( x.length(), y.length(), z.length() );
        // rotation needs normalized vectors
        x = x.normalize();
        y = y.normalize();
        z = z.normalize();
        var trace = x.x + y.y + z.z;
        var s;
        var rotation;
        if ( trace > 0 ) {
            s = 0.5 / Math.sqrt( trace + 1.0 );
            rotation = new Quaternion(
                0.25 / s,
                ( y.z - z.y ) * s,
                ( z.x - x.z ) * s,
                ( x.y - y.x ) * s );
        } else if ( x.x > y.y && x.x > z.z ) {
            s = 2.0 * Math.sqrt( 1.0 + x.x - y.y - z.z );
            rotation = new Quaternion(
                ( y.z - z.y ) / s,
                0.25 * s,
                ( y.x + x.y ) / s,
                ( z.x + x.z ) / s );
        } else if ( y.y > z.z ) {
            s = 2.0 * Math.sqrt( 1.0 + y.y - x.x - z.z );
            rotation = new Quaternion(
                ( z.x - x.z ) / s,
                ( y.x + x.y ) / s,
                0.25 * s,
                ( z.y + y.z ) / s );
        } else {
            s = 2.0 * Math.sqrt( 1.0 + z.z - x.x - y.y );
            rotation = new Quaternion(
                ( x.y - y.x ) / s,
                ( z.x + x.z ) / s,
                ( z.y + y.z ) / s,
                0.25 * s );
        }
        return {
            rotation: rotation,
            scale: scale
        };
    };

    /**
     * Decomposes the matrix into the corresponding rotation, translation, and scale components.
     * @memberof Mat44
     *
     * @returns {Object} The decomposed components of the matrix.
     */
    Mat44.prototype.decompose = function() {
        // translation
        var translation = new Vec3( this.data[12], this.data[13], this.data[14] );
        // axis vectors
        var x = new Vec3( this.data[0], this.data[1], this.data[2] );
        var y = new Vec3( this.data[4], this.data[5], this.data[6] );
        var z = new Vec3( this.data[8], this.data[9], this.data[10] );
        // scale needs unnormalized vectors
        var scale = new Vec3( x.length(), y.length(), z.length() );
        // rotation needs normalized vectors
        x = x.normalize();
        y = y.normalize();
        z = z.normalize();
        var trace = x.x + y.y + z.z;
        var s;
        var rotation;
        if ( trace > 0 ) {
            s = 0.5 / Math.sqrt( trace + 1.0 );
            rotation = new Quaternion(
                0.25 / s,
                ( y.z - z.y ) * s,
                ( z.x - x.z ) * s,
                ( x.y - y.x ) * s );
        } else if ( x.x > y.y && x.x > z.z ) {
            s = 2.0 * Math.sqrt( 1.0 + x.x - y.y - z.z );
            rotation = new Quaternion(
                ( y.z - z.y ) / s,
                0.25 * s,
                ( y.x + x.y ) / s,
                ( z.x + x.z ) / s );
        } else if ( y.y > z.z ) {
            s = 2.0 * Math.sqrt( 1.0 + y.y - x.x - z.z );
            rotation = new Quaternion(
                ( z.x - x.z ) / s,
                ( y.x + x.y ) / s,
                0.25 * s,
                ( z.y + y.z ) / s );
        } else {
            s = 2.0 * Math.sqrt( 1.0 + z.z - x.x - y.y );
            rotation = new Quaternion(
                ( x.y - y.x ) / s,
                ( z.x + x.z ) / s,
                ( z.y + y.z ) / s,
                0.25 * s );
        }
        return {
            rotation: rotation,
            scale: scale,
            translation: translation
        };
    };

    module.exports = Quaternion;

}());