import { Box } from './Box' import { clampRadians, HALF_PI, toDomPrecision } from './utils' import { Vec, VecLike } from './Vec' /** @public */ export type MatLike = MatModel | Mat /** @public */ export interface MatModel { a: number b: number c: number d: number e: number f: number } // function getIdentity() { // return new Mat(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) // } /** @public */ export class Mat { constructor(a: number, b: number, c: number, d: number, e: number, f: number) { this.a = a this.b = b this.c = c this.d = d this.e = e this.f = f } a = 1.0 b = 0.0 c = 0.0 d = 1.0 e = 0.0 f = 0.0 equals(m: Mat | MatModel) { return ( this === m || (this.a === m.a && this.b === m.b && this.c === m.c && this.d === m.d && this.e === m.e && this.f === m.f) ) } identity() { this.a = 1.0 this.b = 0.0 this.c = 0.0 this.d = 1.0 this.e = 0.0 this.f = 0.0 return this } multiply(m: Mat | MatModel) { const m2: MatModel = m const { a, b, c, d, e, f } = this this.a = a * m2.a + c * m2.b this.c = a * m2.c + c * m2.d this.e = a * m2.e + c * m2.f + e this.b = b * m2.a + d * m2.b this.d = b * m2.c + d * m2.d this.f = b * m2.e + d * m2.f + f return this } rotate(r: number, cx?: number, cy?: number) { if (r === 0) return this if (cx === undefined) return this.multiply(Mat.Rotate(r)) return this.translate(cx, cy!).multiply(Mat.Rotate(r)).translate(-cx, -cy!) } translate(x: number, y: number): Mat { return this.multiply(Mat.Translate(x, y!)) } scale(x: number, y: number) { return this.multiply(Mat.Scale(x, y)) } invert() { const { a, b, c, d, e, f } = this const denom = a * d - b * c this.a = d / denom this.b = b / -denom this.c = c / -denom this.d = a / denom this.e = (d * e - c * f) / -denom this.f = (b * e - a * f) / denom return this } applyToPoint(point: VecLike) { return Mat.applyToPoint(this, point) } applyToPoints(points: VecLike[]) { return Mat.applyToPoints(this, points) } rotation() { return Mat.Rotation(this) } point() { return Mat.Point(this) } decomposed() { return Mat.Decompose(this) } toCssString() { return Mat.toCssString(this) } setTo(model: MatModel) { Object.assign(this, model) return this } decompose() { return Mat.Decompose(this) } clone() { return new Mat(this.a, this.b, this.c, this.d, this.e, this.f) } /* --------------------- Static --------------------- */ static Identity() { return new Mat(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) } static Translate(x: number, y: number) { return new Mat(1.0, 0.0, 0.0, 1.0, x, y) } static Rotate(r: number, cx?: number, cy?: number) { if (r === 0) return Mat.Identity() const cosAngle = Math.cos(r) const sinAngle = Math.sin(r) const rotationMatrix = new Mat(cosAngle, sinAngle, -sinAngle, cosAngle, 0.0, 0.0) if (cx === undefined) return rotationMatrix return Mat.Compose(Mat.Translate(cx, cy!), rotationMatrix, Mat.Translate(-cx, -cy!)) } static Scale: { (x: number, y: number): MatModel (x: number, y: number, cx: number, cy: number): MatModel } = (x: number, y: number, cx?: number, cy?: number) => { const scaleMatrix = new Mat(x, 0, 0, y, 0, 0) if (cx === undefined) return scaleMatrix return Mat.Compose(Mat.Translate(cx, cy!), scaleMatrix, Mat.Translate(-cx, -cy!)) } static Multiply(m1: MatModel, m2: MatModel): MatModel { return { a: m1.a * m2.a + m1.c * m2.b, c: m1.a * m2.c + m1.c * m2.d, e: m1.a * m2.e + m1.c * m2.f + m1.e, b: m1.b * m2.a + m1.d * m2.b, d: m1.b * m2.c + m1.d * m2.d, f: m1.b * m2.e + m1.d * m2.f + m1.f, } } static Inverse(m: MatModel): MatModel { const denom = m.a * m.d - m.b * m.c return { a: m.d / denom, b: m.b / -denom, c: m.c / -denom, d: m.a / denom, e: (m.d * m.e - m.c * m.f) / -denom, f: (m.b * m.e - m.a * m.f) / denom, } } static Absolute(m: MatLike): MatModel { const denom = m.a * m.d - m.b * m.c return { a: m.d / denom, b: m.b / -denom, c: m.c / -denom, d: m.a / denom, e: (m.d * m.e - m.c * m.f) / denom, f: (m.b * m.e - m.a * m.f) / -denom, } } static Compose(...matrices: MatLike[]) { const matrix = Mat.Identity() for (let i = 0, n = matrices.length; i < n; i++) { matrix.multiply(matrices[i]) } return matrix } static Point(m: MatLike) { return new Vec(m.e, m.f) } static Rotation(m: MatLike): number { let rotation if (m.a !== 0 || m.c !== 0) { const hypotAc = (m.a * m.a + m.c * m.c) ** 0.5 rotation = Math.acos(m.a / hypotAc) * (m.c > 0 ? -1 : 1) } else if (m.b !== 0 || m.d !== 0) { const hypotBd = (m.b * m.b + m.d * m.d) ** 0.5 rotation = HALF_PI + Math.acos(m.b / hypotBd) * (m.d > 0 ? -1 : 1) } else { rotation = 0 } return clampRadians(rotation) } static Decompose(m: MatLike) { let scaleX, scaleY, rotation if (m.a !== 0 || m.c !== 0) { const hypotAc = (m.a * m.a + m.c * m.c) ** 0.5 scaleX = hypotAc scaleY = (m.a * m.d - m.b * m.c) / hypotAc rotation = Math.acos(m.a / hypotAc) * (m.c > 0 ? -1 : 1) } else if (m.b !== 0 || m.d !== 0) { const hypotBd = (m.b * m.b + m.d * m.d) ** 0.5 scaleX = (m.a * m.d - m.b * m.c) / hypotBd scaleY = hypotBd rotation = HALF_PI + Math.acos(m.b / hypotBd) * (m.d > 0 ? -1 : 1) } else { scaleX = 0 scaleY = 0 rotation = 0 } return { x: m.e, y: m.f, scaleX, scaleY, rotation: clampRadians(rotation), } } static Smooth(m: MatLike, precision = 10000000000) { m.a = Math.round(m.a * precision) / precision m.b = Math.round(m.b * precision) / precision m.c = Math.round(m.c * precision) / precision m.d = Math.round(m.d * precision) / precision m.e = Math.round(m.e * precision) / precision m.f = Math.round(m.f * precision) / precision return m } static toCssString(m: MatLike) { return `matrix(${toDomPrecision(m.a)}, ${toDomPrecision(m.b)}, ${toDomPrecision( m.c )}, ${toDomPrecision(m.d)}, ${toDomPrecision(m.e)}, ${toDomPrecision(m.f)})` } static applyToPoint(m: MatLike, point: VecLike) { return new Vec( m.a * point.x + m.c * point.y + m.e, m.b * point.x + m.d * point.y + m.f, point.z ) } static applyToXY(m: MatLike, x: number, y: number) { return [m.a * x + m.c * y + m.e, m.b * x + m.d * y + m.f] } static applyToPoints(m: MatLike, points: VecLike[]): Vec[] { return points.map( (point) => new Vec(m.a * point.x + m.c * point.y + m.e, m.b * point.x + m.d * point.y + m.f, point.z) ) } static applyToBounds(m: MatLike, box: Box) { return new Box(m.e + box.minX, m.f + box.minY, box.width, box.height) } static From(m: MatLike) { return new Mat(m.a, m.b, m.c, m.d, m.e, m.f) } static Cast(m: MatLike) { return m instanceof Mat ? m : Mat.From(m) } } /** @public */ export function decomposeMatrix(m: MatLike) { return { x: m.e, y: m.f, scaleX: Math.sqrt(m.a * m.a + m.b * m.b), scaleY: Math.sqrt(m.c * m.c + m.d * m.d), rotation: Math.atan2(m.b, m.a), } }