import { fuzzyIsNull, fuzzyEqual } from '../math/float'; import { interpolate } from '../math/functions'; export interface DecomposedType { scaleX: number; scaleY: number; angle: number; remainderA: number; remainderB: number; remainderC: number; remainderD: number; translateX: number; translateY: number; } /*! \class Matrix Affine transformations matrix. a = m11 = horizontal scaling b = m12 = horizontal skewing c = m21 = vertical skewing d = m22 = vertical scaling e = dx = horizontal moving f = dy = vertical moving | a | c | e | | m11 | m21 | dx | | 0 | 2 | 4 | | b | d | f | <=> | m12 | m22 | dy | <=> | 1 | 3 | 5 | | 0 | 0 | 1 | | 0 | 0 | 1 | | - | - | - | \see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform \see https://chromium.googlesource.com/chromium/blink/+/master/Source/platform/transforms/AffineTransform.h \see https://drafts.fxtf.org/geometry/#DOMMatrix \see https://stackoverflow.com/questions/3093455/3d-geometry-how-to-interpolate-a-matrix */ export class Matrix { a!: number; b!: number; c!: number; d!: number; e!: number; f!: number; constructor(a?: number, b?: number, c?: number, d?: number, e?: number, f?: number) { this.reset(a, b, c, d, e, f); } get m11() { return this.a; } set m11(a: number) { this.a = a; } get m12() { return this.b; } set m12(b: number) { this.b = b; } get m21() { return this.c; } set m21(c: number) { this.c = c; } get m22() { return this.d; } set m22(d: number) { this.d = d; } get dx() { return this.e; } set dx(e: number) { this.e = e; } get dy() { return this.f; } set dy(f: number) { this.f = f; } clone() { return new Matrix(this.a, this.b, this.c, this.d, this.e, this.f); } reset(a = 1, b = 0, c = 0, d = 1, e = 0, f = 0) { this.a = a; this.b = b; this.c = c; this.d = d; this.e = e; this.f = f; return this; } isIdentity() { return this.a === 1 && this.b === 0 && this.c === 0 && this.d === 1 && this.e === 0 && this.f === 0; } fuzzyIsIdentity(epsilon?: number) { return fuzzyEqual(this.a, 1, epsilon) && fuzzyIsNull(this.b, epsilon) && fuzzyIsNull(this.c, epsilon) && fuzzyEqual(this.d, 1, epsilon) && fuzzyIsNull(this.e, epsilon) && fuzzyIsNull(this.f, epsilon); } determinant() { return this.a * this.d - this.b * this.c; } isInvertible() { return this.determinant() !== 0; } fuzzyIsInvertible(epsilon?: number) { return !fuzzyIsNull(this.determinant(), epsilon); } equals(other: Matrix) { return this.a === other.a && this.b === other.b && this.c === other.c && this.d === other.d && this.e === other.e && this.f === other.f; } fuzzyEquals(other: Matrix, epsilon?: number) { return fuzzyEqual(this.a, other.a, epsilon) && fuzzyEqual(this.b, other.b, epsilon) && fuzzyEqual(this.c, other.c, epsilon) && fuzzyEqual(this.d, other.d, epsilon) && fuzzyEqual(this.e, other.e, epsilon) && fuzzyEqual(this.f, other.f, epsilon); } xScale() { return Math.sqrt(this.a * this.a + this.b * this.b); } yScale() { return Math.sqrt(this.c * this.c + this.d * this.d); } xSkew() { return -Math.atan2(-this.c, this.d); } ySkew() { return Math.atan2(this.b, this.a); } invert(epsilon?: number) { const det = this.determinant(); if (fuzzyIsNull(det, epsilon)) return this.reset(); const ta = this.a; const tb = this.b; const tc = this.c; const td = this.d; const te = this.e; const tf = this.f; const dinv = 1.0 / det; this.a = td * dinv; this.b = -tb * dinv; this.c = -tc * dinv; this.d = ta * dinv; this.e = (tc * tf - td * te) * dinv; this.f = (tb * te - ta * tf) * dinv; return this; } inverted(epsilon?: number) { const det = this.determinant(); if (fuzzyIsNull(det, epsilon)) return new Matrix(); const dinv = 1.0 / det; return new Matrix(this.d * dinv, -this.b * dinv, -this.c * dinv, this.a * dinv, (this.c * this.f - this.d * this.e) * dinv, (this.b * this.e - this.a * this.f) * dinv); } translate(tx: number, ty: number) { this.e += tx * this.a + ty * this.c; this.f += ty * this.d + tx * this.b; return this; } translated(tx: number, ty: number) { return new Matrix(this.a, this.b, this.c, this.d, this.e + tx * this.a + ty * this.c, this.f + ty * this.d + tx * this.b); } scale(sx: number, sy = sx) { this.a *= sx; this.b *= sx; this.c *= sy; this.d *= sy; return this; } scaled(sx: number, sy = sx) { return new Matrix(this.a * sx, this.b * sx, this.c * sy, this.d * sy, this.e, this.f); } flipX() { return this.scale(-1, 1); } flipY() { return this.scale(1, -1); } flippedX() { return this.scaled(-1, 1); } flippedY() { return this.scaled(1, -1); } rotate(rad: number) { const sina = Math.sin(rad); const cosa = Math.cos(rad); const ta = cosa * this.a + sina * this.c; const tb = cosa * this.b + sina * this.d; const tc = -sina * this.a + cosa * this.c; const td = -sina * this.b + cosa * this.d; this.a = ta; this.b = tb; this.c = tc; this.d = td; return this; } rotated(rad: number) { const sina = Math.sin(rad); const cosa = Math.cos(rad); return new Matrix(cosa * this.a + sina * this.c, cosa * this.b + sina * this.d, -sina * this.a + cosa * this.c, -sina * this.b + cosa * this.d, this.e, this.f); } shear(sx: number, sy: number) { const ta = sy * this.c; const tb = sy * this.d; const tc = sx * this.a; const td = sx * this.b; this.a += ta; this.b += tb; this.c += tc; this.d += td; return this; } sheared(sx: number, sy: number) { return new Matrix(this.a + sy * this.c, this.b + sy * this.d, this.c + sx * this.a, this.d + sx * this.b, this.e, this.f); } skew(rx: number, ry: number) { return this.shear(Math.tan(rx), Math.tan(ry)); } skewed(rx: number, ry: number) { return this.sheared(Math.tan(rx), Math.tan(ry)); } mul(other: number): Matrix; mul(other: Matrix): Matrix; mul(other: any) { switch (other.constructor) { case Number: return new Matrix(this.a * other, this.b * other, this.c * other, this.d * other, this.e * other, this.f * other); case Matrix: { const ta = this.a * other.a + this.b * other.c; const tb = this.a * other.b + this.b * other.d; const tc = this.c * other.a + this.d * other.c; const td = this.c * other.b + this.d * other.d; const te = this.e * other.a + this.f * other.c + other.e; const tf = this.e * other.b + this.f * other.d + other.f; return new Matrix(ta, tb, tc, td, te, tf); } default: return new Matrix(); } } div(other: number) { return new Matrix(this.a / other, this.b / other, this.c / other, this.d / other, this.e / other, this.f / other); } decompose(decomp: any = {}): DecomposedType { const m = this.clone(); // Compute scaling factors let sx = this.xScale(); let sy = this.yScale(); // Compute cross product of transformed unit vectors. If negative, // one axis was flipped. if (m.determinant() < 0) { // Flip axis with minimum unit vector dot product if (m.a < m.d) sx = -sx; else sy = -sy; } // Remove scale from matrix m.scale(1 / sx, 1 / sy); // Compute rotation const angle = Math.atan2(m.b, m.a); // Remove rotation from matrix m.rotate(-angle); // Return results decomp.scaleX = sx; decomp.scaleY = sy; decomp.angle = angle; decomp.remainderA = m.a; decomp.remainderB = m.b; decomp.remainderC = m.c; decomp.remainderD = m.d; decomp.translateX = m.e; decomp.translateY = m.f; return decomp; } recompose(decomp: DecomposedType) { this.a = decomp.remainderA; this.b = decomp.remainderB; this.c = decomp.remainderC; this.d = decomp.remainderD; this.e = decomp.translateX; this.f = decomp.translateY; this.rotate(decomp.angle); this.scale(decomp.scaleX, decomp.scaleY); return this; } interpolated(other: Matrix, t: number) { const from = this.decompose(); const to = other.decompose(); from.scaleX = interpolate(from.scaleX, to.scaleX, t); from.scaleY = interpolate(from.scaleY, to.scaleY, t); from.angle = interpolate(from.angle, to.angle, t); from.translateX = interpolate(from.translateX, to.translateX, t); from.translateY = interpolate(from.translateY, to.translateY, t); from.remainderA = interpolate(from.remainderA, to.remainderA, t); from.remainderB = interpolate(from.remainderB, to.remainderB, t); from.remainderC = interpolate(from.remainderC, to.remainderC, t); from.remainderD = interpolate(from.remainderD, to.remainderD, t); return new Matrix().recompose(from); } map(mapfn: (value: number, index: number, matrix: Matrix) => number, thisArg?: any) { return new Matrix(mapfn.call(thisArg, this.a, 0, this), mapfn.call(thisArg, this.b, 1, this), mapfn.call(thisArg, this.c, 2, this), mapfn.call(thisArg, this.d, 3, this), mapfn.call(thisArg, this.e, 4, this), mapfn.call(thisArg, this.f, 5, this)); } static create(a?: number, b?: number, c?: number, d?: number, e?: number, f?: number) { return new Matrix(a, b, c, d, e, f); } static equal(lhs: Matrix, rhs: Matrix) { return lhs.equals(rhs); } static fuzzyEqual(lhs: Matrix, rhs: Matrix, epsilon?: number) { return lhs.fuzzyEquals(rhs, epsilon); } static interpolate(from: Matrix, to: Matrix, progress: number) { return from.interpolated(to, progress); } }