/** * The fill matrix that makes a sprite wear one frame of a sheet. Pinned against the identity * `uv = inverse(matrix) ∘ normalise(shapeBounds)` rather than against the formula it uses, so * the checks invert the matrix themselves and ask where the shape's corners land in the image. */ import { describe, expect, it } from 'vitest'; import type { FrameRect } from '../../src/core/model/texture'; import { frameFillMatrix, type MatrixValues } from '../../src/viewport/textureFill'; /** * Where the normalised shape point `(u, v)` samples the image: Pixi's `inverse(style.matrix)`, * written out for a general affine matrix so the test does not assume an axis-aligned one. */ function sampleAt(m: MatrixValues, u: number, v: number): { u: number; v: number } { const det = m.a * m.d - m.b * m.c; const dx = u - m.tx; const dy = v - m.ty; return { u: (m.d * dx - m.c * dy) / det, v: (m.a * dy - m.b * dx) / det }; } /** Asserts that the shape's four corners sample exactly the four corners of `rect`. */ function expectCovers(m: MatrixValues, rect: FrameRect, sourceWidth: number, sourceHeight: number): void { const left = rect.x / sourceWidth; const right = (rect.x + rect.w) / sourceWidth; const top = rect.y / sourceHeight; const bottom = (rect.y + rect.h) / sourceHeight; for (const [u, v, expectedU, expectedV] of [ [0, 0, left, top], [1, 0, right, top], [0, 1, left, bottom], [1, 1, right, bottom], ] as const) { const got = sampleAt(m, u, v); expect(got.u).toBeCloseTo(expectedU, 12); expect(got.v).toBeCloseTo(expectedV, 12); } } /** * Number by number rather than `toEqual`: a frame on the top or left edge produces a negative * zero, which `toEqual` counts as a different value. */ function expectMatrix(m: MatrixValues, expected: MatrixValues): void { for (const key of ['a', 'b', 'c', 'd', 'tx', 'ty'] as const) expect(m[key]).toBeCloseTo(expected[key], 12); } describe('frameFillMatrix', () => { it('declines when the image size is unknown, so the sprite keeps showing the whole picture', () => { const rect = { x: 0, y: 0, w: 32, h: 32 }; // What an upload that has not reported its size yet looks like in the model. expect(frameFillMatrix(rect, 0, 0)).toBeNull(); expect(frameFillMatrix(rect, 64, 0)).toBeNull(); expect(frameFillMatrix(rect, -64, 64)).toBeNull(); }); it('declines for a frame with no area, which cannot be spread over anything', () => { expect(frameFillMatrix({ x: 0, y: 0, w: 0, h: 32 }, 64, 64)).toBeNull(); expect(frameFillMatrix({ x: 0, y: 0, w: 32, h: 0 }, 64, 64)).toBeNull(); }); it('maps the shape onto the top-right quadrant of a four-quadrant sheet', () => { const rect = { x: 32, y: 0, w: 32, h: 32 }; const m = frameFillMatrix(rect, 64, 64)!; // Half the sheet across: stretched twofold, shifted by one shape-width to the midpoint. expectMatrix(m, { a: 2, b: 0, c: 0, d: 2, tx: -1, ty: 0 }); expectCovers(m, rect, 64, 64); }); it('stretches the axes independently for a non-square frame', () => { const rect = { x: 0, y: 0, w: 16, h: 64 }; const m = frameFillMatrix(rect, 128, 64)!; expectMatrix(m, { a: 8, b: 0, c: 0, d: 1, tx: 0, ty: 0 }); expectCovers(m, rect, 128, 64); }); it('offsets in frame-widths, not in pixels or UV', () => { // Pixels, UV and frame-widths all differ here, so an offset in the wrong unit is caught. const rect = { x: 48, y: 16, w: 16, h: 8 }; const m = frameFillMatrix(rect, 256, 128)!; expectMatrix(m, { a: 16, b: 0, c: 0, d: 16, tx: -3, ty: -2 }); expectCovers(m, rect, 256, 128); }); it('is the identity for a frame covering the whole sheet', () => { const rect = { x: 0, y: 0, w: 200, h: 100 }; const m = frameFillMatrix(rect, 200, 100)!; expectMatrix(m, { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }); expectCovers(m, rect, 200, 100); }); });