import type { Vec2 } from './Properties'; /** * A 2D affine transform in Pixi's layout: x' = a·x + c·y + tx, y' = b·x + d·y + ty. Every transform * in the hierarchy is a similarity (uniform scale, rotation, an optional mirror), so their products * are too – which is what lets `decompose` recover rotation and scale from one. */ export interface Mat { a: number; b: number; c: number; d: number; tx: number; ty: number; } export const IDENTITY: Mat = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }; export function translation(x: number, y: number): Mat { return { a: 1, b: 0, c: 0, d: 1, tx: x, ty: y }; } /** * Translate ∘ rotate ∘ scale: a point is scaled (a mirror flips the axis first), then turned, then * moved. Rotation in degrees, positive clockwise on screen because y grows downwards. */ export function transformMatrix( x: number, y: number, rotation: number, scale: number, mirrorX: boolean, mirrorY: boolean, ): Mat { const r = (rotation * Math.PI) / 180; const cos = Math.cos(r); const sin = Math.sin(r); const sx = mirrorX ? -scale : scale; const sy = mirrorY ? -scale : scale; return { a: cos * sx, b: sin * sx, c: -sin * sy, d: cos * sy, tx: x, ty: y }; } /** `outer ∘ inner`: apply `inner` first. */ export function multiply(outer: Mat, inner: Mat): Mat { return { a: outer.a * inner.a + outer.c * inner.b, b: outer.b * inner.a + outer.d * inner.b, c: outer.a * inner.c + outer.c * inner.d, d: outer.b * inner.c + outer.d * inner.d, tx: outer.a * inner.tx + outer.c * inner.ty + outer.tx, ty: outer.b * inner.tx + outer.d * inner.ty + outer.ty, }; } export function apply(m: Mat, p: Vec2): Vec2 { return { x: m.a * p.x + m.c * p.y + m.tx, y: m.b * p.x + m.d * p.y + m.ty }; } /** The linear part only – for directions and deltas, which no translation touches. */ export function applyVector(m: Mat, v: Vec2): Vec2 { return { x: m.a * v.x + m.c * v.y, y: m.b * v.x + m.d * v.y }; } export function linearOf(m: Mat): Mat { return { ...m, tx: 0, ty: 0 }; } export function determinant(m: Mat): number { return m.a * m.d - m.b * m.c; } /** A transform with scale 0 has no inverse; its translation is undone and the rest left alone. */ export function invert(m: Mat): Mat { const det = determinant(m); if (Math.abs(det) < 1e-12) return translation(-m.tx, -m.ty); const a = m.d / det; const b = -m.b / det; const c = -m.c / det; const d = m.a / det; return { a, b, c, d, tx: -(a * m.tx + c * m.ty), ty: -(b * m.tx + d * m.ty) }; } /** The uniform scale factor of a similarity. */ export function scaleOf(m: Mat): number { return Math.hypot(m.a, m.b); } /** The angle the x axis was turned by, in radians. */ export function angleOf(m: Mat): number { return Math.atan2(m.b, m.a); } export function isMirrored(m: Mat): boolean { return determinant(m) < 0; } /** * Rotation (degrees), scale and mirror that rebuild the linear part of a similarity. A mirror * always comes out as `mirrorX`: mirroring y is mirroring x turned by a half circle. */ export function decompose(m: Mat): { rotation: number; scale: number; mirrorX: boolean } { const mirrorX = isMirrored(m); const a = mirrorX ? -m.a : m.a; const b = mirrorX ? -m.b : m.b; return { rotation: (Math.atan2(b, a) * 180) / Math.PI, scale: scaleOf(m), mirrorX }; } export function sameLinear(p: Mat, q: Mat, epsilon = 1e-9): boolean { return ( Math.abs(p.a - q.a) < epsilon && Math.abs(p.b - q.b) < epsilon && Math.abs(p.c - q.c) < epsilon && Math.abs(p.d - q.d) < epsilon ); } /** The four corners of a `w`×`h` box centred at (`cx`, `cy`) in local units, taken through `m`. */ export function boxCorners(m: Mat, w: number, h: number, cx = 0, cy = 0): Vec2[] { return [ apply(m, { x: cx - w / 2, y: cy - h / 2 }), apply(m, { x: cx + w / 2, y: cy - h / 2 }), apply(m, { x: cx + w / 2, y: cy + h / 2 }), apply(m, { x: cx - w / 2, y: cy + h / 2 }), ]; } export interface Extent { left: number; top: number; right: number; bottom: number; } export function extentOfPoints(points: readonly Vec2[]): Extent { const extent = { left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity }; for (const p of points) { extent.left = Math.min(extent.left, p.x); extent.top = Math.min(extent.top, p.y); extent.right = Math.max(extent.right, p.x); extent.bottom = Math.max(extent.bottom, p.y); } return extent; }