/** * The room a shader's Padding adds around what wears it (#69): no Pixi and no project, so it stays * unit-testable. The quad grows and its UVs grow with it, so the picture keeps its place and size * and the margin reads past its border – what is there is the Output's Edge. */ /** The margin per side as a fraction of the picture: what both the positions and the UVs grow by. */ export interface QuadPadding { u: number; v: number; } /** * `padding` pixels of a picture `w`×`h` pixels big. A wearer without a picture hands in its own * size, so one pixel is one of its units. */ export function quadPadding(padding: number, w: number, h: number): QuadPadding | null { if (!(padding > 0) || !(w > 0) || !(h > 0)) return null; return { u: padding / w, v: padding / h }; } /** What a mesh geometry is built from. */ interface QuadArrays { positions: Float32Array; uvs: Float32Array; indices: Uint32Array; } /** The unit quad with that margin around it, or none; 0..1 is still exactly the picture. */ export function paddedQuad(pad: QuadPadding | null): QuadArrays { const [u, v] = [pad?.u ?? 0, pad?.v ?? 0]; // `0 - u` rather than `-u`: without a margin that is the plain quad, not one of -0. const corners = [0 - u, 0 - v, 1 + u, 0 - v, 1 + u, 1 + v, 0 - u, 1 + v]; return { positions: new Float32Array(corners), uvs: new Float32Array(corners), indices: new Uint32Array([0, 1, 2, 0, 2, 3]), }; } /** * The same rectangle cut into `cells` by `cells` (#79), so a shader's Vertex output has corners to * move inside it. Positions and UVs stay one and the same, as on the quad. */ export function gridQuad(pad: QuadPadding | null, cells: number): QuadArrays { const n = Math.max(1, Math.round(cells)); const [u, v] = [pad?.u ?? 0, pad?.v ?? 0]; const corners = new Float32Array((n + 1) * (n + 1) * 2); for (let row = 0; row <= n; row++) { for (let column = 0; column <= n; column++) { const at = (row * (n + 1) + column) * 2; corners[at] = -u + ((1 + 2 * u) * column) / n; corners[at + 1] = -v + ((1 + 2 * v) * row) / n; } } const indices = new Uint32Array(n * n * 6); for (let row = 0; row < n; row++) { for (let column = 0; column < n; column++) { const corner = row * (n + 1) + column; const below = corner + n + 1; indices.set([corner, corner + 1, below + 1, corner, below + 1, below], (row * n + column) * 6); } } return { positions: corners, uvs: new Float32Array(corners), indices }; } /** Pixels per side a Pass's picture may have (#301), as many as a field's (#170). */ export const MAX_PASS_PIXELS = 2048; /** * The pixels of a Pass's picture (#301): the wearer's picture of `w`×`h` pixels with its margin, at * `scale` of them per pixel – the Pass's Resolution. Never below one, so there is always a pixel. */ export function passPixels( w: number, h: number, pad: QuadPadding | null, scale: number, ): { w: number; h: number } { const side = (n: number, share: number) => Math.min(MAX_PASS_PIXELS, Math.max(1, Math.round(n * (1 + 2 * share) * scale))); return { w: side(w, pad?.u ?? 0), h: side(h, pad?.v ?? 0) }; } export function paddingKey(pad: QuadPadding | null): string { return pad ? `${pad.u},${pad.v}` : ''; }