/** * A shader's Padding as geometry (#69): the quad grows by so many pixels of the picture and its * UVs grow with it, so the picture stays where it was and the margin reads past its border. */ import { describe, expect, it } from 'vitest'; import type { NineSlice } from '../../src/core/model/texture'; import { paddedSpans, sliceGeometry, sliceKey, sliceSpans } from '../../src/viewport/nineSlice'; import { paddedQuad, paddingKey, quadPadding } from '../../src/viewport/quadPadding'; const slice = (scale = 1): NineSlice => ({ border: { left: 8, top: 4, right: 8, bottom: 4 }, fill: 'stretch', sourceW: 24, sourceH: 16, scale, }); describe('quadPadding', () => { it('counts in pixels of the picture, per axis', () => { expect(quadPadding(2, 16, 32)).toEqual({ u: 0.125, v: 0.0625 }); }); it('is nothing without padding, and without a picture to count in', () => { expect(quadPadding(0, 16, 16)).toBeNull(); expect(quadPadding(-1, 16, 16)).toBeNull(); expect(quadPadding(2, 0, 16)).toBeNull(); expect(quadPadding(2, 16, 0)).toBeNull(); expect(quadPadding(Number.NaN, 16, 16)).toBeNull(); }); it('keys two margins apart, and no margin as the shared quad', () => { expect(paddingKey(null)).toBe(''); expect(paddingKey(quadPadding(2, 16, 16))).not.toBe(paddingKey(quadPadding(2, 16, 32))); expect(paddingKey(quadPadding(2, 16, 16))).toBe(paddingKey(quadPadding(4, 32, 32))); }); }); describe('paddedQuad', () => { it('grows on every side and reads past the border by the same amount', () => { const { positions, uvs, indices } = paddedQuad({ u: 0.125, v: 0.25 }); expect([...positions]).toEqual([-0.125, -0.25, 1.125, -0.25, 1.125, 1.25, -0.125, 1.25]); // Same numbers: 0..1 of the box is still 0..1 of the picture, so the picture does not move. expect([...uvs]).toEqual([...positions]); expect([...indices]).toEqual([0, 1, 2, 0, 2, 3]); }); it('is the plain unit quad without a margin', () => { const { positions, uvs } = paddedQuad(null); expect([...positions]).toEqual([0, 0, 1, 0, 1, 1, 0, 1]); expect([...uvs]).toEqual([...positions]); }); }); describe('a sliced sprite with padding', () => { it('gets a ring as wide as that many border pixels, reading that far past the frame', () => { const spans = paddedSpans(sliceSpans(100, 24, 8, 8, 'stretch'), 100, 24, 3); expect(spans.map((s) => [s.p0, s.p1])).toEqual([ [-3, 0], [0, 8], [8, 92], [92, 100], [100, 103], ]); expect(spans[0]).toMatchObject({ t0: -3 / 24, t1: 0 }); expect(spans.at(-1)).toMatchObject({ t0: 1, t1: 1 + 3 / 24 }); }); it('draws the ring at the scale the borders are drawn at', () => { const spans = paddedSpans(sliceSpans(100, 24, 8, 8, 'stretch', 2), 100, 24, 3, 2); expect([spans[0]!.p0, spans.at(-1)!.p1]).toEqual([-6, 106]); // Twice as wide on screen, the same three pixels of the picture. expect(spans[0]!.t0).toBe(-3 / 24); }); it('leaves the parts alone without padding, and adds nothing to a sprite of no size', () => { const plain = sliceSpans(100, 24, 8, 8, 'stretch'); expect(paddedSpans(plain, 100, 24, 0)).toBe(plain); expect(paddedSpans([], 0, 24, 3)).toEqual([]); }); it('builds the ring into the geometry: sixteen more quads around the nine', () => { const plain = sliceGeometry(slice(), 100, 50); const padded = sliceGeometry(slice(), 100, 50, 2); expect(plain.indices.length / 6).toBe(9); expect(padded.indices.length / 6).toBe(25); const xs = [...padded.positions].filter((_, i) => i % 2 === 0); const ys = [...padded.positions].filter((_, i) => i % 2 === 1); expect([Math.min(...xs), Math.max(...xs)]).toEqual([-2 / 100, 1 + 2 / 100].map(Math.fround)); expect([Math.min(...ys), Math.max(...ys)]).toEqual([-2 / 50, 1 + 2 / 50].map(Math.fround)); const us = [...padded.uvs].filter((_, i) => i % 2 === 0); const vs = [...padded.uvs].filter((_, i) => i % 2 === 1); expect([Math.min(...us), Math.max(...us)]).toEqual([-2 / 24, 1 + 2 / 24].map(Math.fround)); expect([Math.min(...vs), Math.max(...vs)]).toEqual([-2 / 16, 1 + 2 / 16].map(Math.fround)); }); it('keeps the key of an unpadded slice as it was', () => { expect(sliceKey(slice(), 100, 50)).toBe(sliceKey(slice(), 100, 50)); expect(sliceGeometry(slice(), 100, 50, 0).positions).toEqual(sliceGeometry(slice(), 100, 50).positions); }); });