game-maker / suite (push) Successful in 9m5s
Four new rows on sprite, text, collider and container (block TRANSFORM): scale, rotation in degrees clockwise, mirrorX, mirrorY. As properties they get override/reset/apply, loading of old projects, write-back after play and the Properties node for free; camera and behavior have none. The hierarchy composes T*R*S per level (composeAlongChain replaces sumAlongChain), so children rotate and scale with their parent. Reparenting and grouping keep the look (carriedTransform). Drawing goes through placeNode, the gizmo is a polygon, cull and objectRect use the AABB, drags are computed in the parent's system. Colliders are oriented boxes: SAT for box/box, box-circle in the box frame; exitDistance replaces axisSeparation/axisCorrection, moveAndSlide moves along the object's own axes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
144 lines
4.4 KiB
TypeScript
144 lines
4.4 KiB
TypeScript
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;
|
||
}
|