'use strict' const util = require('util') // DOMMatrix per https://drafts.fxtf.org/geometry/#DOMMatrix class DOMPoint { constructor (x, y, z, w) { if (typeof x === 'object' && x !== null) { w = x.w z = x.z y = x.y x = x.x } this.x = typeof x === 'number' ? x : 0 this.y = typeof y === 'number' ? y : 0 this.z = typeof z === 'number' ? z : 0 this.w = typeof w === 'number' ? w : 1 } } // Constants to index into _values (col-major) const M11 = 0; const M12 = 1; const M13 = 2; const M14 = 3 const M21 = 4; const M22 = 5; const M23 = 6; const M24 = 7 const M31 = 8; const M32 = 9; const M33 = 10; const M34 = 11 const M41 = 12; const M42 = 13; const M43 = 14; const M44 = 15 const DEGREE_PER_RAD = 180 / Math.PI const RAD_PER_DEGREE = Math.PI / 180 function parseMatrix (init) { let parsed = init.replace('matrix(', '') parsed = parsed.split(',', 7) // 6 + 1 to handle too many params if (parsed.length !== 6) throw new Error(`Failed to parse ${init}`) parsed = parsed.map(parseFloat) return [ parsed[0], parsed[1], 0, 0, parsed[2], parsed[3], 0, 0, 0, 0, 1, 0, parsed[4], parsed[5], 0, 1 ] } function parseMatrix3d (init) { let parsed = init.replace('matrix3d(', '') parsed = parsed.split(',', 17) // 16 + 1 to handle too many params if (parsed.length !== 16) throw new Error(`Failed to parse ${init}`) return parsed.map(parseFloat) } function parseTransform (tform) { const type = tform.split('(', 1)[0] switch (type) { case 'matrix': return parseMatrix(tform) case 'matrix3d': return parseMatrix3d(tform) // TODO This is supposed to support any CSS transform value. default: throw new Error(`${type} parsing not implemented`) } } class DOMMatrix { constructor (init) { this._is2D = true this._values = new Float64Array([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]) let i if (typeof init === 'string') { // parse CSS transformList if (init === '') return // default identity matrix const tforms = init.split(/\)\s+/, 20).map(parseTransform) if (tforms.length === 0) return init = tforms[0] for (i = 1; i < tforms.length; i++) init = multiply(tforms[i], init) } i = 0 if (init && init.length === 6) { setNumber2D(this, M11, init[i++]) setNumber2D(this, M12, init[i++]) setNumber2D(this, M21, init[i++]) setNumber2D(this, M22, init[i++]) setNumber2D(this, M41, init[i++]) setNumber2D(this, M42, init[i++]) } else if (init && init.length === 16) { setNumber2D(this, M11, init[i++]) setNumber2D(this, M12, init[i++]) setNumber3D(this, M13, init[i++]) setNumber3D(this, M14, init[i++]) setNumber2D(this, M21, init[i++]) setNumber2D(this, M22, init[i++]) setNumber3D(this, M23, init[i++]) setNumber3D(this, M24, init[i++]) setNumber3D(this, M31, init[i++]) setNumber3D(this, M32, init[i++]) setNumber3D(this, M33, init[i++]) setNumber3D(this, M34, init[i++]) setNumber2D(this, M41, init[i++]) setNumber2D(this, M42, init[i++]) setNumber3D(this, M43, init[i++]) setNumber3D(this, M44, init[i]) } else if (init !== undefined) { throw new TypeError('Expected string or array.') } } toString () { return this.is2D ? `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})` : `matrix3d(${this._values.join(', ')})` } multiply (other) { return newInstance(this._values).multiplySelf(other) } multiplySelf (other) { this._values = multiply(other._values, this._values) if (!other.is2D) this._is2D = false return this } preMultiplySelf (other) { this._values = multiply(this._values, other._values) if (!other.is2D) this._is2D = false return this } translate (tx, ty, tz) { return newInstance(this._values).translateSelf(tx, ty, tz) } translateSelf (tx, ty, tz) { if (typeof tx !== 'number') tx = 0 if (typeof ty !== 'number') ty = 0 if (typeof tz !== 'number') tz = 0 this._values = multiply([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, tx, ty, tz, 1 ], this._values) if (tz !== 0) this._is2D = false return this } scale (scaleX, scaleY, scaleZ, originX, originY, originZ) { return newInstance(this._values).scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ) } scale3d (scale, originX, originY, originZ) { return newInstance(this._values).scale3dSelf(scale, originX, originY, originZ) } scale3dSelf (scale, originX, originY, originZ) { return this.scaleSelf(scale, scale, scale, originX, originY, originZ) } scaleSelf (scaleX, scaleY, scaleZ, originX, originY, originZ) { // Not redundant with translate's checks because we need to negate the values later. if (typeof originX !== 'number') originX = 0 if (typeof originY !== 'number') originY = 0 if (typeof originZ !== 'number') originZ = 0 this.translateSelf(originX, originY, originZ) if (typeof scaleX !== 'number') scaleX = 1 if (typeof scaleY !== 'number') scaleY = scaleX if (typeof scaleZ !== 'number') scaleZ = 1 this._values = multiply([ scaleX, 0, 0, 0, 0, scaleY, 0, 0, 0, 0, scaleZ, 0, 0, 0, 0, 1 ], this._values) this.translateSelf(-originX, -originY, -originZ) if (scaleZ !== 1 || originZ !== 0) this._is2D = false return this } rotateFromVector (x, y) { return newInstance(this._values).rotateFromVectorSelf(x, y) } rotateFromVectorSelf (x, y) { if (typeof x !== 'number') x = 0 if (typeof y !== 'number') y = 0 const theta = (x === 0 && y === 0) ? 0 : Math.atan2(y, x) * DEGREE_PER_RAD return this.rotateSelf(theta) } rotate (rotX, rotY, rotZ) { return newInstance(this._values).rotateSelf(rotX, rotY, rotZ) } rotateSelf (rotX, rotY, rotZ) { if (rotY === undefined && rotZ === undefined) { rotZ = rotX rotX = rotY = 0 } if (typeof rotY !== 'number') rotY = 0 if (typeof rotZ !== 'number') rotZ = 0 if (rotX !== 0 || rotY !== 0) this._is2D = false rotX *= RAD_PER_DEGREE rotY *= RAD_PER_DEGREE rotZ *= RAD_PER_DEGREE let c, s c = Math.cos(rotZ) s = Math.sin(rotZ) this._values = multiply([ c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ], this._values) c = Math.cos(rotY) s = Math.sin(rotY) this._values = multiply([ c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1 ], this._values) c = Math.cos(rotX) s = Math.sin(rotX) this._values = multiply([ 1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1 ], this._values) return this } rotateAxisAngle (x, y, z, angle) { return newInstance(this._values).rotateAxisAngleSelf(x, y, z, angle) } rotateAxisAngleSelf (x, y, z, angle) { if (typeof x !== 'number') x = 0 if (typeof y !== 'number') y = 0 if (typeof z !== 'number') z = 0 // Normalize axis const length = Math.sqrt(x * x + y * y + z * z) if (length === 0) return this if (length !== 1) { x /= length y /= length z /= length } angle *= RAD_PER_DEGREE const c = Math.cos(angle) const s = Math.sin(angle) const t = 1 - c const tx = t * x const ty = t * y // NB: This is the generic transform. If the axis is a major axis, there are // faster transforms. this._values = multiply([ tx * x + c, tx * y + s * z, tx * z - s * y, 0, tx * y - s * z, ty * y + c, ty * z + s * x, 0, tx * z + s * y, ty * z - s * x, t * z * z + c, 0, 0, 0, 0, 1 ], this._values) if (x !== 0 || y !== 0) this._is2D = false return this } skewX (sx) { return newInstance(this._values).skewXSelf(sx) } skewXSelf (sx) { if (typeof sx !== 'number') return this const t = Math.tan(sx * RAD_PER_DEGREE) this._values = multiply([ 1, 0, 0, 0, t, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ], this._values) return this } skewY (sy) { return newInstance(this._values).skewYSelf(sy) } skewYSelf (sy) { if (typeof sy !== 'number') return this const t = Math.tan(sy * RAD_PER_DEGREE) this._values = multiply([ 1, t, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ], this._values) return this } flipX () { return newInstance(multiply([ -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ], this._values)) } flipY () { return newInstance(multiply([ 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ], this._values)) } inverse () { return newInstance(this._values).invertSelf() } invertSelf () { const m = this._values const inv = m.map(v => 0) inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] + m[9] * m[7] * m[14] + m[13] * m[6] * m[11] - m[13] * m[7] * m[10] inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15] - m[8] * m[7] * m[14] - m[12] * m[6] * m[11] + m[12] * m[7] * m[10] inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15] + m[8] * m[7] * m[13] + m[12] * m[5] * m[11] - m[12] * m[7] * m[9] inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14] - m[8] * m[6] * m[13] - m[12] * m[5] * m[10] + m[12] * m[6] * m[9] // If the determinant is zero, this matrix cannot be inverted, and all // values should be set to NaN, with the is2D flag set to false. const det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12] if (det === 0) { this._values = m.map(v => NaN) this._is2D = false return this } inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15] - m[9] * m[3] * m[14] - m[13] * m[2] * m[11] + m[13] * m[3] * m[10] inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15] + m[8] * m[3] * m[14] + m[12] * m[2] * m[11] - m[12] * m[3] * m[10] inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15] - m[8] * m[3] * m[13] - m[12] * m[1] * m[11] + m[12] * m[3] * m[9] inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14] + m[8] * m[2] * m[13] + m[12] * m[1] * m[10] - m[12] * m[2] * m[9] inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15] + m[5] * m[3] * m[14] + m[13] * m[2] * m[7] - m[13] * m[3] * m[6] inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15] - m[4] * m[3] * m[14] - m[12] * m[2] * m[7] + m[12] * m[3] * m[6] inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15] + m[4] * m[3] * m[13] + m[12] * m[1] * m[7] - m[12] * m[3] * m[5] inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14] - m[4] * m[2] * m[13] - m[12] * m[1] * m[6] + m[12] * m[2] * m[5] inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11] - m[5] * m[3] * m[10] - m[9] * m[2] * m[7] + m[9] * m[3] * m[6] inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11] + m[4] * m[3] * m[10] + m[8] * m[2] * m[7] - m[8] * m[3] * m[6] inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11] - m[4] * m[3] * m[9] - m[8] * m[1] * m[7] + m[8] * m[3] * m[5] inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10] + m[4] * m[2] * m[9] + m[8] * m[1] * m[6] - m[8] * m[2] * m[5] inv.forEach((v, i) => { inv[i] = v / det }) this._values = inv return this } setMatrixValue (transformList) { const temp = new DOMMatrix(transformList) this._values = temp._values this._is2D = temp._is2D return this } transformPoint (point) { point = new DOMPoint(point) const x = point.x const y = point.y const z = point.z const w = point.w const values = this._values const nx = values[M11] * x + values[M21] * y + values[M31] * z + values[M41] * w const ny = values[M12] * x + values[M22] * y + values[M32] * z + values[M42] * w const nz = values[M13] * x + values[M23] * y + values[M33] * z + values[M43] * w const nw = values[M14] * x + values[M24] * y + values[M34] * z + values[M44] * w return new DOMPoint(nx, ny, nz, nw) } toFloat32Array () { return Float32Array.from(this._values) } toFloat64Array () { return this._values.slice(0) } static fromMatrix (init) { if (!(init instanceof DOMMatrix)) throw new TypeError('Expected DOMMatrix') return new DOMMatrix(init._values) } static fromFloat32Array (init) { if (!(init instanceof Float32Array)) throw new TypeError('Expected Float32Array') return new DOMMatrix(init) } static fromFloat64Array (init) { if (!(init instanceof Float64Array)) throw new TypeError('Expected Float64Array') return new DOMMatrix(init) } [util.inspect.custom || 'inspect'] (depth, options) { if (depth < 0) return '[DOMMatrix]' return `DOMMatrix [ a: ${this.a} b: ${this.b} c: ${this.c} d: ${this.d} e: ${this.e} f: ${this.f} m11: ${this.m11} m12: ${this.m12} m13: ${this.m13} m14: ${this.m14} m21: ${this.m21} m22: ${this.m22} m23: ${this.m23} m23: ${this.m23} m31: ${this.m31} m32: ${this.m32} m33: ${this.m33} m34: ${this.m34} m41: ${this.m41} m42: ${this.m42} m43: ${this.m43} m44: ${this.m44} is2D: ${this.is2D} isIdentity: ${this.isIdentity} ]` } } /** * Checks that `value` is a number and sets the value. */ function setNumber2D (receiver, index, value) { if (typeof value !== 'number') throw new TypeError('Expected number') return (receiver._values[index] = value) } /** * Checks that `value` is a number, sets `_is2D = false` if necessary and sets * the value. */ function setNumber3D (receiver, index, value) { if (typeof value !== 'number') throw new TypeError('Expected number') if (index === M33 || index === M44) { if (value !== 1) receiver._is2D = false } else if (value !== 0) receiver._is2D = false return (receiver._values[index] = value) } Object.defineProperties(DOMMatrix.prototype, { m11: { get () { return this._values[M11] }, set (v) { return setNumber2D(this, M11, v) } }, m12: { get () { return this._values[M12] }, set (v) { return setNumber2D(this, M12, v) } }, m13: { get () { return this._values[M13] }, set (v) { return setNumber3D(this, M13, v) } }, m14: { get () { return this._values[M14] }, set (v) { return setNumber3D(this, M14, v) } }, m21: { get () { return this._values[M21] }, set (v) { return setNumber2D(this, M21, v) } }, m22: { get () { return this._values[M22] }, set (v) { return setNumber2D(this, M22, v) } }, m23: { get () { return this._values[M23] }, set (v) { return setNumber3D(this, M23, v) } }, m24: { get () { return this._values[M24] }, set (v) { return setNumber3D(this, M24, v) } }, m31: { get () { return this._values[M31] }, set (v) { return setNumber3D(this, M31, v) } }, m32: { get () { return this._values[M32] }, set (v) { return setNumber3D(this, M32, v) } }, m33: { get () { return this._values[M33] }, set (v) { return setNumber3D(this, M33, v) } }, m34: { get () { return this._values[M34] }, set (v) { return setNumber3D(this, M34, v) } }, m41: { get () { return this._values[M41] }, set (v) { return setNumber2D(this, M41, v) } }, m42: { get () { return this._values[M42] }, set (v) { return setNumber2D(this, M42, v) } }, m43: { get () { return this._values[M43] }, set (v) { return setNumber3D(this, M43, v) } }, m44: { get () { return this._values[M44] }, set (v) { return setNumber3D(this, M44, v) } }, a: { get () { return this.m11 }, set (v) { return (this.m11 = v) } }, b: { get () { return this.m12 }, set (v) { return (this.m12 = v) } }, c: { get () { return this.m21 }, set (v) { return (this.m21 = v) } }, d: { get () { return this.m22 }, set (v) { return (this.m22 = v) } }, e: { get () { return this.m41 }, set (v) { return (this.m41 = v) } }, f: { get () { return this.m42 }, set (v) { return (this.m42 = v) } }, is2D: { get () { return this._is2D } }, // read-only isIdentity: { get () { const values = this._values return (values[M11] === 1 && values[M12] === 0 && values[M13] === 0 && values[M14] === 0 && values[M21] === 0 && values[M22] === 1 && values[M23] === 0 && values[M24] === 0 && values[M31] === 0 && values[M32] === 0 && values[M33] === 1 && values[M34] === 0 && values[M41] === 0 && values[M42] === 0 && values[M43] === 0 && values[M44] === 1) } } }) /** * Instantiates a DOMMatrix, bypassing the constructor. * @param {Float64Array} values Value to assign to `_values`. This is assigned * without copying (okay because all usages are followed by a multiply). */ function newInstance (values) { const instance = Object.create(DOMMatrix.prototype) instance.constructor = DOMMatrix instance._is2D = true instance._values = values return instance } function multiply (A, B) { const dest = new Float64Array(16) for (let i = 0; i < 4; i++) { for (let j = 0; j < 4; j++) { let sum = 0 for (let k = 0; k < 4; k++) { sum += A[i * 4 + k] * B[k * 4 + j] } dest[i * 4 + j] = sum } } return dest } module.exports = { DOMMatrix, DOMPoint }