UNPKG

matrix3d

Version:

Decompose, interpolate, compose 3d matrices.

682 lines (548 loc) 18.4 kB
/* Matrix3d A port of Webkit's source code of 4x4 Matrices operations. Implements methods from both SkMatrix44 (types, concat, invert, ...) and transform_util (decomposition, composition, ...) Differences between implementations: - All operations are non desctructive, applied on a new Matrix3d instance. - Convenience methods toString, clone, round, ... mostly specific to JavaScript. - Some speed optimizations have been removed for simplicity and brevity. */ 'use strict'; var prime = require('prime'); var Vector3 = require('./Vector3'); var Vector4 = require('./Vector4'); var stringify = function(array, places) { if (places == null || places > 20) places = 20; var strings = []; for (var i = 0; i < array.length; i++) strings[i] = array[i].toFixed(10).replace(/\.?0+$/, ''); return strings; }; var TypeMask = { Identity: 0, Translate: 0x01, //!< set if the matrix has translation Scale: 0x02, //!< set if the matrix has any scale != 1 Affine: 0x04, //!< set if the matrix skews or rotates Perspective: 0x08, //!< set if the matrix is in perspective All: 0xF, Unknown: 0x80 }; var Matrix3d = prime({ constructor: function Matrix3d() { // m.m11, m.m12, m.m13, m.m14, // m.m21, m.m22, m.m23, m.m24, // m.m31, m.m32, m.m33, m.m34, // m.m41, m.m42, m.m43, m.m44 var values = arguments; if (values.length === 1) values = values[0]; // matrix as list if (!values.length) values = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]; var i = 0, j, k = 0; if (values.length === 6) { // 2d matrix var a = values[0]; var b = values[1]; var c = values[2]; var d = values[3]; var e = values[4]; var f = values[5]; values = [ a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, e, f, 0, 1 ]; } if (values.length !== 16) throw new Error('invalid matrix'); // always 16 for (i = 0; i < 4; i++) { var col = this[i] = []; for (j = 0; j < 4; j++) { col[j] = values[k++]; } } }, // get 2x3 get a() { return this.m11; }, get b() { return this.m12; }, get c() { return this.m21; }, get d() { return this.m22; }, get e() { return this.m41; }, get f() { return this.m42; }, // set 2x3 set a(value) { this.m11 = value; }, set b(value) { this.m12 = value; }, set c(value) { this.m21 = value; }, set d(value) { this.m22 = value; }, set e(value) { this.m41 = value; }, set f(value) { this.m42 = value; }, // get 4x4 get m11() { return this[0][0]; }, get m12() { return this[0][1]; }, get m13() { return this[0][2]; }, get m14() { return this[0][3]; }, get m21() { return this[1][0]; }, get m22() { return this[1][1]; }, get m23() { return this[1][2]; }, get m24() { return this[1][3]; }, get m31() { return this[2][0]; }, get m32() { return this[2][1]; }, get m33() { return this[2][2]; }, get m34() { return this[2][3]; }, get m41() { return this[3][0]; }, get m42() { return this[3][1]; }, get m43() { return this[3][2]; }, get m44() { return this[3][3]; }, // set 4x4 set m11(value) { this[0][0] = value; }, set m12(value) { this[0][1] = value; }, set m13(value) { this[0][2] = value; }, set m14(value) { this[0][3] = value; }, set m21(value) { this[1][0] = value; }, set m22(value) { this[1][1] = value; }, set m23(value) { this[1][2] = value; }, set m24(value) { this[1][3] = value; }, set m31(value) { this[2][0] = value; }, set m32(value) { this[2][1] = value; }, set m33(value) { this[2][2] = value; }, set m34(value) { this[2][3] = value; }, set m41(value) { this[3][0] = value; }, set m42(value) { this[3][1] = value; }, set m43(value) { this[3][2] = value; }, set m44(value) { this[3][3] = value; }, // get shortcuts get transX() { return this[3][0]; }, get transY() { return this[3][1]; }, get transZ() { return this[3][2]; }, get scaleX() { return this[0][0]; }, get scaleY() { return this[1][1]; }, get scaleZ() { return this[2][2]; }, get perspX() { return this[0][3]; }, get perspY() { return this[1][3]; }, get perspZ() { return this[2][3]; }, // set shortcuts set transX(value) { this[3][0] = value; }, set transY(value) { this[3][1] = value; }, set transZ(value) { this[3][2] = value; }, set scaleX(value) { this[0][0] = value; }, set scaleY(value) { this[1][1] = value; }, set scaleZ(value) { this[2][2] = value; }, set perspX(value) { this[0][3] = value; }, set perspY(value) { this[1][3] = value; }, set perspZ(value) { this[2][3] = value; }, // type getter get type() { var m = this; var mask = 0; if (0 !== m.perspX || 0 !== m.perspY || 0 !== m.perspZ || 1 !== m[3][3]) { return TypeMask.Translate | TypeMask.Scale | TypeMask.Affine | TypeMask.Perspective; } if (0 !== m.transX || 0 !== m.transY || 0 !== m.transZ) { mask |= TypeMask.Translate; } if (1 !== m.scaleX || 1 !== m.scaleY || 1 !== m.scaleZ) { mask |= TypeMask.Scale; } if (0 !== m[1][0] || 0 !== m[0][1] || 0 !== m[0][2] || 0 !== m[2][0] || 0 !== m[1][2] || 0 !== m[2][1]) { mask |= TypeMask.Affine; } return mask; }, // W3C is2d: function() { var m = this; return m.m31 === 0 && m.m32 === 0 && m.m13 === 0 && m.m14 === 0 && m.m23 === 0 && m.m24 === 0 && m.m33 === 1 && m.m34 === 0 && m.m43 === 0 && m.m44 === 1; }, equals: function(m2) { var m1 = this; return m1.m11 === m2.m11 && m1.m12 === m2.m12 && m1.m13 === m2.m13 && m1.m14 === m2.m14 && m1.m21 === m2.m21 && m1.m22 === m2.m22 && m1.m23 === m2.m23 && m1.m24 === m2.m24 && m1.m31 === m2.m31 && m1.m32 === m2.m32 && m1.m33 === m2.m33 && m1.m34 === m2.m34 && m1.m41 === m2.m41 && m1.m42 === m2.m42 && m1.m43 === m2.m43 && m1.m44 === m2.m44; }, clone: function() { var m = this; return new Matrix3d( m.m11, m.m12, m.m13, m.m14, m.m21, m.m22, m.m23, m.m24, m.m31, m.m32, m.m33, m.m34, m.m41, m.m42, m.m43, m.m44 ); }, /** * Return true if the matrix is identity. */ isIdentity: function() { return this.type === TypeMask.Identity; }, /** * Return true if the matrix contains translate or is identity. */ isTranslate: function() { return !(this.type & ~TypeMask.Translate); }, /** * Return true if the matrix only contains scale or translate or is identity. */ isScaleTranslate: function() { return !(this.type & ~(TypeMask.Scale | TypeMask.Translate)); }, concat: function(m2) { if (this.isIdentity()) return m2.clone(); if (m2.isIdentity()) return this.clone(); var m = new Matrix3d; for (var j = 0; j < 4; j++) { for (var i = 0; i < 4; i++) { var value = 0; for (var k = 0; k < 4; k++) { value += this[k][i] * m2[j][k]; } m[j][i] = value; } } return m; }, translate: function(v3) { var translationMatrix = new Matrix3d; translationMatrix.m41 = v3[0]; translationMatrix.m42 = v3[1]; translationMatrix.m43 = v3[2]; return this.concat(translationMatrix); }, scale: function(v3) { var m = new Matrix3d; m.m11 = v3[0]; m.m22 = v3[1]; m.m33 = v3[2]; return this.concat(m); }, rotate: function(v4q) { var rotationMatrix = new Matrix3d; var x = v4q[0]; var y = v4q[1]; var z = v4q[2]; var w = v4q[3]; rotationMatrix.m11 = 1 - 2 * (y * y + z * z); rotationMatrix.m21 = 2 * (x * y - z * w); rotationMatrix.m31 = 2 * (x * z + y * w); rotationMatrix.m12 = 2 * (x * y + z * w); rotationMatrix.m22 = 1 - 2 * (x * x + z * z); rotationMatrix.m32 = 2 * (y * z - x * w); rotationMatrix.m13 = 2 * (x * z - y * w); rotationMatrix.m23 = 2 * (y * z + x * w); rotationMatrix.m33 = 1 - 2 * (x * x + y * y); return this.concat(rotationMatrix); }, skew: function(v3) { var skewMatrix = new Matrix3d; skewMatrix[1][0] = v3[0]; skewMatrix[2][0] = v3[1]; skewMatrix[2][1] = v3[2]; return this.concat(skewMatrix); }, perspective: function(v4) { var perspectiveMatrix = new Matrix3d; perspectiveMatrix.m14 = v4[0]; perspectiveMatrix.m24 = v4[1]; perspectiveMatrix.m34 = v4[2]; perspectiveMatrix.m44 = v4[3]; return this.concat(perspectiveMatrix); }, map: function(v4) { var result = new Vector4; for (var i = 0; i < 4; i++) { var value = 0; for (var j = 0; j < 4; j++) { value += this[j][i] * v4[j]; } result[i] = value; } return result; }, determinant: function() { if (this.isIdentity()) return 1; if (this.isScaleTranslate()) return this[0][0] * this[1][1] * this[2][2] * this[3][3]; var a00 = this[0][0]; var a01 = this[0][1]; var a02 = this[0][2]; var a03 = this[0][3]; var a10 = this[1][0]; var a11 = this[1][1]; var a12 = this[1][2]; var a13 = this[1][3]; var a20 = this[2][0]; var a21 = this[2][1]; var a22 = this[2][2]; var a23 = this[2][3]; var a30 = this[3][0]; var a31 = this[3][1]; var a32 = this[3][2]; var a33 = this[3][3]; var b00 = a00 * a11 - a01 * a10; var b01 = a00 * a12 - a02 * a10; var b02 = a00 * a13 - a03 * a10; var b03 = a01 * a12 - a02 * a11; var b04 = a01 * a13 - a03 * a11; var b05 = a02 * a13 - a03 * a12; var b06 = a20 * a31 - a21 * a30; var b07 = a20 * a32 - a22 * a30; var b08 = a20 * a33 - a23 * a30; var b09 = a21 * a32 - a22 * a31; var b10 = a21 * a33 - a23 * a31; var b11 = a22 * a33 - a23 * a32; // Calculate the determinant return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; }, normalize: function() { var m44 = this.m44; // Cannot normalize. if (m44 === 0) return false; var normalizedMatrix = new Matrix3d; var scale = 1 / m44; for (var i = 0; i < 4; i++) for (var j = 0; j < 4; j++) normalizedMatrix[j][i] = this[j][i] * scale; return normalizedMatrix; }, decompose: function() { // We'll operate on a copy of the matrix. var matrix = this.normalize(); // If we cannot normalize the matrix, then bail early as we cannot decompose. if (!matrix) return false; var perspectiveMatrix = matrix.clone(); var i, j; for (i = 0; i < 3; i++) perspectiveMatrix[i][3] = 0; perspectiveMatrix[3][3] = 1; // If the perspective matrix is not invertible, we are also unable to // decompose, so we'll bail early. Constant taken from SkMatrix44::invert. if (Math.abs(perspectiveMatrix.determinant()) < 1e-8) return false; var perspective; if (matrix[0][3] !== 0 || matrix[1][3] !== 0 || matrix[2][3] !== 0) { // rhs is the right hand side of the equation. var rightHandSide = new Vector4( matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3] ); // Solve the equation by inverting perspectiveMatrix and multiplying // rightHandSide by the inverse. var inversePerspectiveMatrix = perspectiveMatrix.invert(); if (!inversePerspectiveMatrix) return false; var transposedInversePerspectiveMatrix = inversePerspectiveMatrix.transpose(); perspective = transposedInversePerspectiveMatrix.map(rightHandSide); } else { // No perspective. perspective = new Vector4(0, 0, 0, 1); } var translate = new Vector3; for (i = 0; i < 3; i++) translate[i] = matrix[3][i]; var row = []; for (i = 0; i < 3; i++) { var v3 = row[i] = new Vector3; for (j = 0; j < 3; ++j) v3[j] = matrix[i][j]; } // Compute X scale factor and normalize first row. var scale = new Vector3; scale[0] = row[0].length(); row[0] = row[0].normalize(); // Compute XY shear factor and make 2nd row orthogonal to 1st. var skew = new Vector3; skew[0] = row[0].dot(row[1]); row[1] = row[1].combine(row[0], 1.0, -skew[0]); // Now, compute Y scale and normalize 2nd row. scale[1] = row[1].length(); row[1] = row[1].normalize(); skew[0] /= scale[1]; // Compute XZ and YZ shears, orthogonalize 3rd row skew[1] = row[0].dot(row[2]); row[2] = row[2].combine(row[0], 1.0, -skew[1]); skew[2] = row[1].dot(row[2]); row[2] = row[2].combine(row[1], 1.0, -skew[2]); // Next, get Z scale and normalize 3rd row. scale[2] = row[2].length(); row[2] = row[2].normalize(); skew[1] /= scale[2]; skew[2] /= scale[2]; // 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. var pdum3 = row[1].cross(row[2]); if (row[0].dot(pdum3) < 0) { for (i = 0; i < 3; i++) { scale[i] *= -1; for (j = 0; j < 3; ++j) row[i][j] *= -1; } } var quaternion = new Vector4( 0.5 * Math.sqrt(Math.max(1 + row[0][0] - row[1][1] - row[2][2], 0)), 0.5 * Math.sqrt(Math.max(1 - row[0][0] + row[1][1] - row[2][2], 0)), 0.5 * Math.sqrt(Math.max(1 - row[0][0] - row[1][1] + row[2][2], 0)), 0.5 * Math.sqrt(Math.max(1 + row[0][0] + row[1][1] + row[2][2], 0)) ); if (row[2][1] > row[1][2]) quaternion[0] = -quaternion[0]; if (row[0][2] > row[2][0]) quaternion[1] = -quaternion[1]; if (row[1][0] > row[0][1]) quaternion[2] = -quaternion[2]; return new DecomposedMatrix(perspective, translate, quaternion, skew, scale); }, invert: function() { var a00 = this[0][0]; var a01 = this[0][1]; var a02 = this[0][2]; var a03 = this[0][3]; var a10 = this[1][0]; var a11 = this[1][1]; var a12 = this[1][2]; var a13 = this[1][3]; var a20 = this[2][0]; var a21 = this[2][1]; var a22 = this[2][2]; var a23 = this[2][3]; var a30 = this[3][0]; var a31 = this[3][1]; var a32 = this[3][2]; var a33 = this[3][3]; var b00 = a00 * a11 - a01 * a10; var b01 = a00 * a12 - a02 * a10; var b02 = a00 * a13 - a03 * a10; var b03 = a01 * a12 - a02 * a11; var b04 = a01 * a13 - a03 * a11; var b05 = a02 * a13 - a03 * a12; var b06 = a20 * a31 - a21 * a30; var b07 = a20 * a32 - a22 * a30; var b08 = a20 * a33 - a23 * a30; var b09 = a21 * a32 - a22 * a31; var b10 = a21 * a33 - a23 * a31; var b11 = a22 * a33 - a23 * a32; // Calculate the determinant var det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; // If det is zero, we want to return false. However, we also want to return false // if 1/det overflows to infinity (i.e. det is denormalized). Both of these are // handled by checking that 1/det is finite. if (det === 0 || !isFinite(det)) return false; var invdet = 1.0 / det; b00 *= invdet; b01 *= invdet; b02 *= invdet; b03 *= invdet; b04 *= invdet; b05 *= invdet; b06 *= invdet; b07 *= invdet; b08 *= invdet; b09 *= invdet; b10 *= invdet; b11 *= invdet; return new Matrix3d( a11 * b11 - a12 * b10 + a13 * b09, a02 * b10 - a01 * b11 - a03 * b09, a31 * b05 - a32 * b04 + a33 * b03, a22 * b04 - a21 * b05 - a23 * b03, a12 * b08 - a10 * b11 - a13 * b07, a00 * b11 - a02 * b08 + a03 * b07, a32 * b02 - a30 * b05 - a33 * b01, a20 * b05 - a22 * b02 + a23 * b01, a10 * b10 - a11 * b08 + a13 * b06, a01 * b08 - a00 * b10 - a03 * b06, a30 * b04 - a31 * b02 + a33 * b00, a21 * b02 - a20 * b04 - a23 * b00, a11 * b07 - a10 * b09 - a12 * b06, a00 * b09 - a01 * b07 + a02 * b06, a31 * b01 - a30 * b03 - a32 * b00, a20 * b03 - a21 * b01 + a22 * b00 ); }, // W3C transpose: function() { var m = this; return new Matrix3d( m.m11, m.m21, m.m31, m.m41, m.m12, m.m22, m.m32, m.m42, m.m13, m.m23, m.m33, m.m43, m.m14, m.m24, m.m34, m.m44 ); }, interpolation: function(matrix) { return new MatrixInterpolation(this, matrix); }, toArray: function() { return this.is2d() ? this.toArray2d() : this.toArray3d(); }, toArray3d: function() { var m = this; return [ m.m11, m.m12, m.m13, m.m14, m.m21, m.m22, m.m23, m.m24, m.m31, m.m32, m.m33, m.m34, m.m41, m.m42, m.m43, m.m44 ]; }, toArray2d: function() { var m = this; return [ m.a, m.b, m.c, m.d, m.e, m.f ]; }, toString: function(places) { return this.is2d() ? this.toString2d(places) : this.toString3d(places); }, toString3d: function(places) { return 'matrix3d(' + stringify(this.toArray3d()).join(', ') + ')'; }, toString2d: function(places) { return 'matrix(' + stringify(this.toArray2d()).join(', ') + ')'; } }); var DecomposedMatrix = prime({ constructor: function DecomposedMatrix(perspective, translate, quaternion, skew, scale) { this.perspective = perspective; this.translate = translate; this.quaternion = quaternion; this.skew = skew; this.scale = scale; }, interpolate: function(to, delta) { var from = this; var perspective = from.perspective.lerp(to.perspective, delta); var translate = from.translate.lerp(to.translate, delta); var quaternion = from.quaternion.slerp(to.quaternion, delta); var skew = from.skew.lerp(to.skew, delta); var scale = from.scale.lerp(to.scale, delta); return new DecomposedMatrix(perspective, translate, quaternion, skew, scale); }, compose: function() { return new Matrix3d() .perspective(this.perspective) .translate(this.translate) .rotate(this.quaternion) .skew(this.skew) .scale(this.scale); } }); var MatrixInterpolation = prime({ constructor: function MatrixInterpolation(from, to) { this.matrix1 = from; this.matrix2 = to; this.from = from.decompose(); this.to = to.decompose(); }, step: function(delta) { if (delta === 0) return this.matrix1; if (delta === 1) return this.matrix2; return this.from.interpolate(this.to, delta).compose(); } }); Matrix3d.Decomposed = DecomposedMatrix; Matrix3d.Interpolation = MatrixInterpolation; module.exports = Matrix3d;