Skip to content

Instantly share code, notes, and snippets.

@jarek-foksa
Created June 18, 2016 10:06
Show Gist options
  • Select an option

  • Save jarek-foksa/e379ee0414cf9fb03f38d4bed9140cae to your computer and use it in GitHub Desktop.

Select an option

Save jarek-foksa/e379ee0414cf9fb03f38d4bed9140cae to your computer and use it in GitHub Desktop.

Revisions

  1. jarek-foksa created this gist Jun 18, 2016.
    933 changes: 933 additions & 0 deletions decomposed-transform.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,933 @@

    // @copyright
    // © 2005, 2006, 2013 Apple Inc. All rights reserved.
    // © 2009 Torch Mobile, Inc.
    // © 2016 Jarosław Foksa
    // @src
    // https://drafts.csswg.org/css-transforms-1/#recomposing-to-a-3d-matrix
    // http://trac.webkit.org/browser/trunk/Source/WebCore/platform/graphics/transforms/TransformationMatrix.cpp

    let {abs, sqrt, asin, sin, cos, tan, atan2} = Math;

    const SMALL_NUMBER = 1.e-8;

    export default class DecomposedTransform {
    constructor(rotation = "eulerAngles") {
    this._translateX = 0;
    this._translateY = 0;
    this._translateZ = 0;
    this._scaleX = 1;
    this._scaleY = 1;
    this._scaleZ = 1;
    this._perspectiveX = 0;
    this._perspectiveY = 0;
    this._perspectiveZ = 0;
    this._perspectiveW = 1;
    this._skewXY = 0;
    this._skewXZ = 0;
    this._skewYZ = 0;
    this._rotation = rotation;

    if (rotation === "eulerAngles") {
    this._rotateX = 0;
    this._rotateY = 0;
    this._rotateZ = 0;
    }
    else if (rotation === "quaternions" ) {
    this._quaternionX = 0;
    this._quaternionY = 0;
    this._quaternionZ = 0;
    this._quaternionW = 0;
    }
    }

    static fromSVGMatrix(svgMatrix, rotation = "eulerAngles") {
    let transform = new DecomposedTransform(rotation);

    let matrix = [
    [svgMatrix.a, svgMatrix.b, 0, 0],
    [svgMatrix.c, svgMatrix.d, 0, 0],
    [ 0, 0, 1, 0],
    [svgMatrix.e, svgMatrix.f, 0, 1]
    ];

    // Normalize the matrix.

    if (matrix[3][3] == 0) {
    return false;
    }

    for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
    matrix[i][j] /= matrix[3][3];
    }
    }

    // perspectiveMatrix is used to solve for perspective, but it also provides an easy way to test for singularity
    // of the upper 3x3 component.

    let perspectiveMatrix = [];

    for (let i = 0; i < 4; i++) {
    perspectiveMatrix[i] = [];

    for (let j = 0; j < 4; j++) {
    perspectiveMatrix[i][j] = matrix[i][j];
    }
    }

    for (let i = 0; i < 3; i++) {
    perspectiveMatrix[i][3] = 0;
    }

    perspectiveMatrix[3][3] = 1;

    if (determinant4x4(perspectiveMatrix) == 0) {
    return false;
    }

    // First, isolate perspective. This is the messiest.

    if (matrix[0][3] != 0 || matrix[1][3] != 0 || matrix[2][3] != 0) {
    // rightHandSide is the right hand side of the equation.
    let rightHandSide = [0, 0, 0, 0];
    rightHandSide[0] = matrix[0][3];
    rightHandSide[1] = matrix[1][3];
    rightHandSide[2] = matrix[2][3];
    rightHandSide[3] = matrix[3][3];

    // Solve the equation by inverting perspectiveMatrix and multiplying rightHandSide by the inverse. (This is
    // the easiest way, not necessarily the best.)

    let inversePerspectiveMatrix = inverse(perspectiveMatrix);
    let transposedInversePerspectiveMatrix = transposeMatrix4(inversePerspectiveMatrix);

    let perspectivePoint = v4MulPointByMatrix(rightHandSide, transposedInversePerspectiveMatrix);

    transform.perspectiveX = perspectivePoint[0];
    transform.perspectiveY = perspectivePoint[1];
    transform.perspectiveZ = perspectivePoint[2];
    transform.perspectiveW = perspectivePoint[3];

    // Clear the perspective partition
    matrix[0][3] = matrix[1][3] = matrix[2][3] = 0;
    matrix[3][3] = 1;
    }
    else {
    // No perspective.
    transform.perspectiveX = transform.perspectiveY = transform.perspectiveZ = 0;
    transform.perspectiveW = 1;
    }

    // Next take care of translation (easy).

    transform.translateX = matrix[3][0];
    matrix[3][0] = 0;
    transform.translateY = matrix[3][1];
    matrix[3][1] = 0;
    transform.translateZ = matrix[3][2];
    matrix[3][2] = 0;

    // Vector4 type and functions need to be added to the common set.
    let row = [[], [], []];
    let pdum3 = [0, 0, 0];

    // Now get scale and shear.
    for (let i = 0; i < 3; i++) {
    row[i][0] = matrix[i][0];
    row[i][1] = matrix[i][1];
    row[i][2] = matrix[i][2];
    }

    // Compute X scale factor and normalize first row.
    transform.scaleX = v3Length(row[0]);
    v3Scale(row[0], 1.0);

    // Compute XY shear factor and make 2nd row orthogonal to 1st.
    transform.skewXY = v3Dot(row[0], row[1]);
    v3Combine(row[1], row[0], row[1], 1.0, -transform.skewXY);

    // Now, compute Y scale and normalize 2nd row.
    transform.scaleY = v3Length(row[1]);
    v3Scale(row[1], 1.0);
    transform.skewXY /= transform.scaleY;

    // Compute XZ and YZ shears, orthogonalize 3rd row.
    transform.skewXZ = v3Dot(row[0], row[2]);
    v3Combine(row[2], row[0], row[2], 1.0, -transform.skewXZ);
    transform.skewYZ = v3Dot(row[1], row[2]);
    v3Combine(row[2], row[1], row[2], 1.0, -transform.skewYZ);

    // Next, get Z scale and normalize 3rd row.
    transform.scaleZ = v3Length(row[2]);
    v3Scale(row[2], 1.0);
    transform.skewXZ /= transform.scaleZ;
    transform.skewYZ /= transform.scaleZ;

    // At this point, the matrix (in rows[]) is orthonormal.
    // Check for a coordinate system flip. If the determinant is -1, then negate the matrix and the scaling factors.
    v3Cross(row[1], row[2], pdum3);

    if (v3Dot(row[0], pdum3) < 0) {
    transform.scaleX *= -1;
    transform.scaleY *= -1;
    transform.scaleZ *= -1;

    for (let i = 0; i < 3; i++) {
    row[i][0] *= -1;
    row[i][1] *= -1;
    row[i][2] *= -1;
    }
    }

    // Rotation

    if (rotation === "eulerAngles") {
    transform.rotateY = asin(-row[0][2]);

    if (cos(transform.rotateY) !== 0) {
    transform.rotateX = atan2(row[1][2], row[2][2]);
    transform.rotateZ = atan2(row[0][1], row[0][0]);
    }
    else {
    transform.rotateX = atan2(-row[2][0], row[1][1]);
    transform.rotateZ = 0;
    }
    }

    else if (rotation === "quaternions") {
    let s, t, x, y, z, w;

    t = row[0][0] + row[1][1] + row[2][2] + 1.0;

    if (t > 1e-4) {
    s = 0.5 / sqrt(t);
    w = 0.25 / s;
    x = (row[2][1] - row[1][2]) * s;
    y = (row[0][2] - row[2][0]) * s;
    z = (row[1][0] - row[0][1]) * s;
    }
    else if (row[0][0] > row[1][1] && row[0][0] > row[2][2]) {
    s = sqrt(1.0 + row[0][0] - row[1][1] - row[2][2]) * 2.0; // S = 4 * qx.
    x = 0.25 * s;
    y = (row[0][1] + row[1][0]) / s;
    z = (row[0][2] + row[2][0]) / s;
    w = (row[2][1] - row[1][2]) / s;
    }
    else if (row[1][1] > row[2][2]) {
    s = sqrt(1.0 + row[1][1] - row[0][0] - row[2][2]) * 2.0; // S = 4 * qy.
    x = (row[0][1] + row[1][0]) / s;
    y = 0.25 * s;
    z = (row[1][2] + row[2][1]) / s;
    w = (row[0][2] - row[2][0]) / s;
    }
    else {
    s = sqrt(1.0 + row[2][2] - row[0][0] - row[1][1]) * 2.0; // S = 4 * qz.
    x = (row[0][2] + row[2][0]) / s;
    y = (row[1][2] + row[2][1]) / s;
    z = 0.25 * s;
    w = (row[1][0] - row[0][1]) / s;
    }

    transform.quaternionX = x;
    transform.quaternionY = y;
    transform.quaternionZ = z;
    transform.quaternionW = w;
    }

    return transform;
    }

    // @info
    // Recompose the decomposed tranasform back into a transformation matrix.
    // @type
    // () => SVGMatrix
    toSVGMatrix() {
    let matrix = [
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1]
    ];

    // Perspective

    matrix[0][3] = this.perspectiveX;
    matrix[1][3] = this.perspectiveY;
    matrix[2][3] = this.perspectiveZ;
    matrix[3][3] = this.perspectiveW;

    // Translate

    translate3d(matrix, this.translateX, this.translateY, this.translateZ);

    // Rotate

    if (this.rotation === "eulerAngles") {
    let a = cos(this.rotateX);
    let b = sin(this.rotateX);
    let c = cos(this.rotateY);
    let d = sin(this.rotateY);
    let e = cos(this.rotateZ);
    let f = sin(this.rotateZ);

    let ce = c * e;
    let cf = c * f;
    let de = d * e;
    let df = d * f;

    let rotationMatrix = [
    [ce - df * b, cf + de * b, -a * d, 0],
    [ -a * f, a * e, b, 0],
    [de + cf * b, df - ce * b, a * c, 0],
    [ 0, 0, 0, 1]
    ];

    matrix = multiply(matrix, rotationMatrix);
    }
    else if (this.rotation === "quaternions") {
    let xx = this.quaternionX * this.quaternionX;
    let xy = this.quaternionX * this.quaternionY;
    let xz = this.quaternionX * this.quaternionZ;
    let xw = this.quaternionX * this.quaternionW;
    let yy = this.quaternionY * this.quaternionY;
    let yz = this.quaternionY * this.quaternionZ;
    let yw = this.quaternionY * this.quaternionW;
    let zz = this.quaternionZ * this.quaternionZ;
    let zw = this.quaternionZ * this.quaternionW;

    let rotationMatrix = [
    [1 - 2 * (yy + zz), 2 * (xy - zw), 2 * (xz + yw), 0],
    [ 2 * (xy + zw), 1 - 2 * (xx + zz), 2 * (yz - xw), 0],
    [ 2 * (xz - yw), 2 * (yz + xw), 1 - 2 * (xx + yy), 0],
    [ 0, 0, 0, 1]
    ];

    matrix = multiply(matrix, rotationMatrix);
    }

    // Skew

    if (this.skewYZ) {
    let skewMatrix = [
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1]
    ];

    skewMatrix[2][1] = this.skewYZ;
    matrix = multiply(matrix, skewMatrix);
    }

    if (this.skewXZ) {
    let skewMatrix = [
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1]
    ];

    skewMatrix[2][1] = 0
    skewMatrix[2][0] = this.skewXZ;
    matrix = multiply(matrix, skewMatrix);
    }

    if (this.skewXY) {
    let skewMatrix = [
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1]
    ];

    skewMatrix[2][0] = 0
    skewMatrix[1][0] = this.skewXY;
    matrix = multiply(matrix, skewMatrix);
    }

    // Scale

    scale3d(matrix, this.scaleX, this.scaleY, this.scaleZ);

    // ...

    return new SVGMatrix([matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1], matrix[3][0], matrix[3][1]]);
    }

    clone() {
    let transform = new DecomposedTransform(this._rotation);

    transform.translateX = this._translateX;
    transform.translateY = this._translateY;
    transform.translateZ = this._translateZ;
    transform.scaleX = this._scaleX;
    transform.scaleY = this._scaleY;
    transform.scaleZ = this._scaleZ;
    transform.perspectiveX = this._perspectiveX;
    transform.perspectiveY = this._perspectiveY;
    transform.perspectiveZ = this._perspectiveZ;
    transform.perspectiveW = this._perspectiveW;
    transform.skewXY = this._skewXY;
    transform.skewXZ = this._skewXZ;
    transform.skewYZ = this._skewYZ;

    if (this._rotation === "eulerAngles") {
    transform.rotateX = this._rotateX;
    transform.rotateY = this._rotateY;
    transform.rotateZ = this._rotateZ;
    }
    else if (this._rotation === "quaternions" ) {
    transform.quaternionX = this._quaternionX;
    transform.quaternionY = this._quaternionY;
    transform.quaternionZ = this._quaternionZ;
    transform.quaternionW = this._quaternionW;
    }

    return transform;
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    // @info
    // Translation along X axis.
    // @type
    // number
    get translateX() {
    return this._translateX;
    }
    set translateX(translateX) {
    this._translateX = translateX;
    }

    // @info
    // Translation along Y axis.
    // @type
    // number
    get translateY() {
    return this._translateY;
    }
    set translateY(translateY) {
    this._translateY = translateY;
    }

    // @info
    // Translation along Z axis.
    // @type
    // number
    get translateZ() {
    return this._translateZ;
    }
    set translateZ(translateZ) {
    this._translateZ = translateZ;
    }

    // @info
    // Scale along X axis
    // @type
    // number
    get scaleX() {
    return this._scaleX;
    }
    set scaleX(scaleX) {
    this._scaleX = scaleX;
    }

    // @info
    // Scale along Y axis
    // @type
    // number
    get scaleY() {
    return this._scaleY;
    }
    set scaleY(scaleY) {
    this._scaleY = scaleY;
    }

    // @info
    // Scale along Z axis
    // @type
    // number
    get scaleZ() {
    return this._scaleZ;
    }
    set scaleZ(scaleZ) {
    this._scaleZ = scaleZ;
    }

    // @info
    // Perspective, X component.
    // @type
    // number
    get perspectiveX() {
    return this._perspectiveX;
    }
    set perspectiveX(perspectiveX) {
    this._perspectiveX = perspectiveX;
    }

    // @info
    // Perspective, Y component.
    // @type
    // number
    get perspectiveY() {
    return this._perspectiveY;
    }
    set perspectiveY(perspectiveY) {
    this._perspectiveY = perspectiveY;
    }

    // @info
    // Perspective, Z component.
    // @type
    // number
    get perspectiveZ() {
    return this._perspectiveZ;
    }
    set perspectiveZ(perspectiveZ) {
    this._perspectiveZ = perspectiveZ;
    }

    // @info
    // Perspective, W component.
    // @type
    // number
    get perspectiveW() {
    return this._perspectiveW;
    }
    set perspectiveW(perspectiveW) {
    this._perspectiveW = perspectiveW;
    }

    // @info
    // Skew the XY plane
    // @type
    // number
    get skewXY() {
    return this._skewXY;
    }
    set skewXY(skewXY) {
    this._skewXY = skewXY;
    }

    // @info
    // Skew the XZ plane
    // @type
    // number
    get skewXZ() {
    return this._skewXZ;
    }
    set skewXZ(skewXZ) {
    this._skewXZ = skewXZ;
    }

    // @info
    // Skew the YZ plane
    // @type
    // number
    get skewYZ() {
    return this._skewYZ;
    }
    set skewYZ(skewYZ) {
    this._skewYZ = skewYZ;
    }

    // @info
    // Rotation representation. Either Euler angles which are easier to work with in 2D space or quaternions which
    // are more flexible in 3D space (no gimbal locks).
    // @type
    // "eulerAngles" || "quaternions"
    get rotation() {
    return this._rotation;
    }
    set rotation(rotation) {
    throw new Error(`"rotation" property is read-only.`);
    }

    // @info
    // Rotation along the X axis, in Euler angles.
    // @type
    // number?
    get rotateX() {
    if (this._rotation === "eulerAngles") {
    return this._rotateX;
    }
    else {
    throw new Error(`Can't get "rotateX" propery - decomposed transform does not use Euler angles.`);
    }
    }
    set rotateX(rotateX) {
    if (this._rotation === "eulerAngles") {
    this._rotateX = rotateX;
    }
    else {
    throw new Error(`Can't set "rotateX" propery - decomposed transform does not use Euler angles.`);
    }
    }

    // @info
    // Rotation along the Y axis, in Euler angles.
    // @type
    // number?
    get rotateY() {
    if (this._rotation === "eulerAngles") {
    return this._rotateY;
    }
    else {
    throw new Error(`Can't get "rotateY" propery - decomposed transform does not use Euler angles.`);
    }
    }
    set rotateY(rotateY) {
    if (this._rotation === "eulerAngles") {
    this._rotateY = rotateY;
    }
    else {
    throw new Error(`Can't set "rotateY" propery - decomposed transform does not use Euler angles.`);
    }
    }

    // @info
    // Rotation along the Z axis, in Euler angles.
    // @type
    // number?
    get rotateZ() {
    if (this._rotation === "eulerAngles") {
    return this._rotateZ;
    }
    else {
    throw new Error(`Can't get "rotateZ" propery - decomposed transform does not use Euler angles.`);
    }
    }
    set rotateZ(rotateZ) {
    if (this._rotation === "eulerAngles") {
    this._rotateZ = rotateZ;
    }
    else {
    throw new Error(`Can't set "rotateZ" propery - decomposed transform does not use Euler angles.`);
    }
    }

    // @info
    // Quaternion, X component.
    // @type
    // number?
    get quaternionX() {
    if (this._rotation === "quaternions") {
    return this._quaternionX;
    }
    else {
    throw new Error(`Can't get "quaternionX" propery - decomposed transform does not use quaternions.`);
    }
    }
    set quaternionX(quaternionX) {
    if (this._rotation === "quaternions") {
    this._quaternionX = quaternionX;
    }
    else {
    throw new Error(`Can't set "quaternionX" propery - decomposed transform does not use quaternions.`);
    }
    }

    // @info
    // Quaternion, Y component.
    // @type
    // number?
    get quaternionY() {
    if (this._rotation === "quaternions") {
    return this._quaternionY;
    }
    else {
    throw new Error(`Can't get "quaternionY" propery - decomposed transform does not use quaternions.`);
    }
    }
    set quaternionY(quaternionY) {
    if (this._rotation === "quaternions") {
    this._quaternionY = quaternionY;
    }
    else {
    throw new Error(`Can't set "quaternionY" propery - decomposed transform does not use quaternions.`);
    }
    }

    // @info
    // Quaternion, Z component.
    // @type
    // number?
    get quaternionZ() {
    if (this._rotation === "quaternions") {
    return this._quaternionZ;
    }
    else {
    throw new Error(`Can't get "quaternionZ" propery - decomposed transform does not use quaternions.`);
    }
    }
    set quaternionZ(quaternionZ) {
    if (this._rotation === "quaternions") {
    this._quaternionX = quaternionZ;
    }
    else {
    throw new Error(`Can't set "quaternionZ" propery - decomposed transform does not use quaternions.`);
    }
    }

    // @info
    // Quaternion, W component.
    // @type
    // number?
    get quaternionW() {
    if (this._rotation === "quaternions") {
    return this._quaternionW;
    }
    else {
    throw new Error(`Can't get "quaternionW" propery - decomposed transform does not use quaternions.`);
    }
    }
    set quaternionW(quaternionW) {
    if (this._rotation === "quaternions") {
    this._quaternionW = quaternionW;
    }
    else {
    throw new Error(`Can't set "quaternionW" propery - decomposed transform does not use quaternions.`);
    }
    }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    let determinant2x2 = (a, b, c, d) => {
    return a * d - b * c;
    };

    let determinant3x3 = (a1, a2, a3, b1, b2, b3, c1, c2, c3) => {
    return a1 * determinant2x2(b2, b3, c2, c3)
    - b1 * determinant2x2(a2, a3, c2, c3)
    + c1 * determinant2x2(a2, a3, b2, b3);
    };

    let determinant4x4 = (m) => {
    let a1 = m[0][0];
    let b1 = m[0][1];
    let c1 = m[0][2];
    let d1 = m[0][3];

    let a2 = m[1][0];
    let b2 = m[1][1];
    let c2 = m[1][2];
    let d2 = m[1][3];

    let a3 = m[2][0];
    let b3 = m[2][1];
    let c3 = m[2][2];
    let d3 = m[2][3];

    let a4 = m[3][0];
    let b4 = m[3][1];
    let c4 = m[3][2];
    let d4 = m[3][3];

    return a1 * determinant3x3(b2, b3, b4, c2, c3, c4, d2, d3, d4)
    - b1 * determinant3x3(a2, a3, a4, c2, c3, c4, d2, d3, d4)
    + c1 * determinant3x3(a2, a3, a4, b2, b3, b4, d2, d3, d4)
    - d1 * determinant3x3(a2, a3, a4, b2, b3, b4, c2, c3, c4);
    };

    let adjoint = (matrix) => {
    let result = [[], [], [], []];

    // Assign to individual variable names to aid selecting correct values
    let a1 = matrix[0][0];
    let b1 = matrix[0][1];
    let c1 = matrix[0][2];
    let d1 = matrix[0][3];

    let a2 = matrix[1][0];
    let b2 = matrix[1][1];
    let c2 = matrix[1][2];
    let d2 = matrix[1][3];

    let a3 = matrix[2][0];
    let b3 = matrix[2][1];
    let c3 = matrix[2][2];
    let d3 = matrix[2][3];

    let a4 = matrix[3][0];
    let b4 = matrix[3][1];
    let c4 = matrix[3][2];
    let d4 = matrix[3][3];

    // Row column labeling reversed since we transpose rows & columns
    result[0][0] = determinant3x3(b2, b3, b4, c2, c3, c4, d2, d3, d4);
    result[1][0] = - determinant3x3(a2, a3, a4, c2, c3, c4, d2, d3, d4);
    result[2][0] = determinant3x3(a2, a3, a4, b2, b3, b4, d2, d3, d4);
    result[3][0] = - determinant3x3(a2, a3, a4, b2, b3, b4, c2, c3, c4);

    result[0][1] = - determinant3x3(b1, b3, b4, c1, c3, c4, d1, d3, d4);
    result[1][1] = determinant3x3(a1, a3, a4, c1, c3, c4, d1, d3, d4);
    result[2][1] = - determinant3x3(a1, a3, a4, b1, b3, b4, d1, d3, d4);
    result[3][1] = determinant3x3(a1, a3, a4, b1, b3, b4, c1, c3, c4);

    result[0][2] = determinant3x3(b1, b2, b4, c1, c2, c4, d1, d2, d4);
    result[1][2] = - determinant3x3(a1, a2, a4, c1, c2, c4, d1, d2, d4);
    result[2][2] = determinant3x3(a1, a2, a4, b1, b2, b4, d1, d2, d4);
    result[3][2] = - determinant3x3(a1, a2, a4, b1, b2, b4, c1, c2, c4);

    result[0][3] = - determinant3x3(b1, b2, b3, c1, c2, c3, d1, d2, d3);
    result[1][3] = determinant3x3(a1, a2, a3, c1, c2, c3, d1, d2, d3);
    result[2][3] = - determinant3x3(a1, a2, a3, b1, b2, b3, d1, d2, d3);
    result[3][3] = determinant3x3(a1, a2, a3, b1, b2, b3, c1, c2, c3);

    return;
    };

    let inverse = (matrix) => {
    // Calculate the adjoint matrix
    let result = adjoint(matrix);

    // Calculate the 4x4 determinant. If the determinant is zero, then the inverse matrix is not unique.
    let det = determinant4x4(matrix);

    if (abs(det) < SMALL_NUMBER) {
    return false;
    }

    // Scale the adjoint matrix to get the inverse

    for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
    result[i][j] = result[i][j] / det;
    }
    }

    return result;
    };

    let transposeMatrix4 = (a) => {
    let b = [[], [], [], []];

    for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
    b[i][j] = a[j][i];
    }
    }

    return b;
    };

    let v4MulPointByMatrix = (p, m) => {
    let result = [];

    result[0] = (p[0] * m[0][0]) + (p[1] * m[1][0]) +
    (p[2] * m[2][0]) + (p[3] * m[3][0]);
    result[1] = (p[0] * m[0][1]) + (p[1] * m[1][1]) +
    (p[2] * m[2][1]) + (p[3] * m[3][1]);
    result[2] = (p[0] * m[0][2]) + (p[1] * m[1][2]) +
    (p[2] * m[2][2]) + (p[3] * m[3][2]);
    result[3] = (p[0] * m[0][3]) + (p[1] * m[1][3]) +
    (p[2] * m[2][3]) + (p[3] * m[3][3]);

    return result;
    };

    let v3Length = (a) => {
    return sqrt((a[0] * a[0]) + (a[1] * a[1]) + (a[2] * a[2]));
    }

    let v3Scale = (v, desiredLength) => {
    let len = v3Length(v);

    if (len != 0) {
    let l = desiredLength / len;
    v[0] *= l;
    v[1] *= l;
    v[2] *= l;
    }
    };

    let v3Dot = (a, b) => {
    return (a[0] * b[0]) + (a[1] * b[1]) + (a[2] * b[2]);
    };

    // Make a linear combination of two vectors and return the result.
    // result = (a * ascl) + (b * bscl)
    let v3Combine = (a, b, result, ascl, bscl) => {
    result[0] = (ascl * a[0]) + (bscl * b[0]);
    result[1] = (ascl * a[1]) + (bscl * b[1]);
    result[2] = (ascl * a[2]) + (bscl * b[2]);
    };

    // Return the cross product result = a cross b */
    let v3Cross = (a, b, result) => {
    result[0] = (a[1] * b[2]) - (a[2] * b[1]);
    result[1] = (a[2] * b[0]) - (a[0] * b[2]);
    result[2] = (a[0] * b[1]) - (a[1] * b[0]);
    };

    let translate3d = (m_matrix, tx, ty, tz) => {
    m_matrix[3][0] += tx * m_matrix[0][0] + ty * m_matrix[1][0] + tz * m_matrix[2][0];
    m_matrix[3][1] += tx * m_matrix[0][1] + ty * m_matrix[1][1] + tz * m_matrix[2][1];
    m_matrix[3][2] += tx * m_matrix[0][2] + ty * m_matrix[1][2] + tz * m_matrix[2][2];
    m_matrix[3][3] += tx * m_matrix[0][3] + ty * m_matrix[1][3] + tz * m_matrix[2][3];
    };

    let multiply = (mat1, mat2) => {
    let result = [[], [], [], []];

    result[0][0] = (mat2[0][0] * mat1[0][0] + mat2[0][1] * mat1[1][0]
    + mat2[0][2] * mat1[2][0] + mat2[0][3] * mat1[3][0]);
    result[0][1] = (mat2[0][0] * mat1[0][1] + mat2[0][1] * mat1[1][1]
    + mat2[0][2] * mat1[2][1] + mat2[0][3] * mat1[3][1]);
    result[0][2] = (mat2[0][0] * mat1[0][2] + mat2[0][1] * mat1[1][2]
    + mat2[0][2] * mat1[2][2] + mat2[0][3] * mat1[3][2]);
    result[0][3] = (mat2[0][0] * mat1[0][3] + mat2[0][1] * mat1[1][3]
    + mat2[0][2] * mat1[2][3] + mat2[0][3] * mat1[3][3]);

    result[1][0] = (mat2[1][0] * mat1[0][0] + mat2[1][1] * mat1[1][0]
    + mat2[1][2] * mat1[2][0] + mat2[1][3] * mat1[3][0]);
    result[1][1] = (mat2[1][0] * mat1[0][1] + mat2[1][1] * mat1[1][1]
    + mat2[1][2] * mat1[2][1] + mat2[1][3] * mat1[3][1]);
    result[1][2] = (mat2[1][0] * mat1[0][2] + mat2[1][1] * mat1[1][2]
    + mat2[1][2] * mat1[2][2] + mat2[1][3] * mat1[3][2]);
    result[1][3] = (mat2[1][0] * mat1[0][3] + mat2[1][1] * mat1[1][3]
    + mat2[1][2] * mat1[2][3] + mat2[1][3] * mat1[3][3]);

    result[2][0] = (mat2[2][0] * mat1[0][0] + mat2[2][1] * mat1[1][0]
    + mat2[2][2] * mat1[2][0] + mat2[2][3] * mat1[3][0]);
    result[2][1] = (mat2[2][0] * mat1[0][1] + mat2[2][1] * mat1[1][1]
    + mat2[2][2] * mat1[2][1] + mat2[2][3] * mat1[3][1]);
    result[2][2] = (mat2[2][0] * mat1[0][2] + mat2[2][1] * mat1[1][2]
    + mat2[2][2] * mat1[2][2] + mat2[2][3] * mat1[3][2]);
    result[2][3] = (mat2[2][0] * mat1[0][3] + mat2[2][1] * mat1[1][3]
    + mat2[2][2] * mat1[2][3] + mat2[2][3] * mat1[3][3]);

    result[3][0] = (mat2[3][0] * mat1[0][0] + mat2[3][1] * mat1[1][0]
    + mat2[3][2] * mat1[2][0] + mat2[3][3] * mat1[3][0]);
    result[3][1] = (mat2[3][0] * mat1[0][1] + mat2[3][1] * mat1[1][1]
    + mat2[3][2] * mat1[2][1] + mat2[3][3] * mat1[3][1]);
    result[3][2] = (mat2[3][0] * mat1[0][2] + mat2[3][1] * mat1[1][2]
    + mat2[3][2] * mat1[2][2] + mat2[3][3] * mat1[3][2]);
    result[3][3] = (mat2[3][0] * mat1[0][3] + mat2[3][1] * mat1[1][3]
    + mat2[3][2] * mat1[2][3] + mat2[3][3] * mat1[3][3]);

    return result;
    };

    let scaleNonUniform = (m_matrix, sx, sy) => {
    m_matrix[0][0] *= sx;
    m_matrix[0][1] *= sx;
    m_matrix[0][2] *= sx;
    m_matrix[0][3] *= sx;

    m_matrix[1][0] *= sy;
    m_matrix[1][1] *= sy;
    m_matrix[1][2] *= sy;
    m_matrix[1][3] *= sy;
    };

    let scale3d = (m_matrix, sx, sy, sz) => {
    scaleNonUniform(m_matrix, sx, sy);

    m_matrix[2][0] *= sz;
    m_matrix[2][1] *= sz;
    m_matrix[2][2] *= sz;
    m_matrix[2][3] *= sz;
    };