import { describe, expect, it } from "vitest"; import { DEFAULT_GEOMETRY, createGlassSlices, sliceRects } from "./lens"; // A plain hairline, without the darkened edge or glints; those have their own tests. const slices = createGlassSlices(27, 1, { bezel: 16, edge: 0, glint: 0 }); const centre = 27; function refraction(x: number, y: number) { const index = (y * slices.size + x) * 4; return { r: slices.refraction[index], g: slices.refraction[index + 1] }; } function alpha(layer: keyof typeof slices.masks, x: number, y: number) { return slices.masks[layer][(y * slices.size + x) * 4 + 3] ?? 0; } describe("createGlassSlices", () => { it("renders two corners and one pixel of straight edge", () => { expect(slices.size).toBe(55); expect(createGlassSlices(27, 2).size).toBe(110); }); it("leaves the flat centre undistorted so labels stay sharp", () => { expect(refraction(centre, centre)).toEqual({ r: 128, g: 128 }); }); it("bends the rim toward the centre", () => { expect(refraction(centre, 1).g).toBeGreaterThan(128); expect(refraction(centre, 53).g).toBeLessThan(128); expect(refraction(1, centre).r).toBeGreaterThan(128); expect(refraction(53, centre).r).toBeLessThan(128); }); it("never folds content back on itself across the rim", () => { // Default lens. Each row inward must sample the backdrop further in than the last, // or neighbouring pixels swap order and content shows up as mirrored echoes. const lens = createGlassSlices(27, 1); let previous = -Infinity; for (let row = 0; row <= 27; row++) { const green = lens.refraction[(row * lens.size + 27) * 4 + 1] ?? 128; const sampled = row + 0.5 + ((green - 127.5) / 255) * lens.scale; expect(sampled).toBeGreaterThan(previous); previous = sampled; } }); it("bends the rounded ends far more than the straight edges, still without folding", () => { const lens = createGlassSlices(27, 1); const shiftAt = (x: number, y: number, channel: 0 | 1) => (((lens.refraction[(y * lens.size + x) * 4 + channel] ?? 128) - 127.5) / 255) * lens.scale; // Six pixels in: across the top edge versus into the side. expect(shiftAt(6, 27, 0)).toBeGreaterThan(shiftAt(27, 6, 1) * 2); let previous = -Infinity; for (let column = 0; column <= 27; column++) { const sampled = column + 0.5 + shiftAt(column, 27, 0); expect(sampled).toBeGreaterThan(previous); previous = sampled; } }); it("keeps pixels outside the rounded corner neutral and unmasked", () => { expect(refraction(0, 0)).toEqual({ r: 128, g: 128 }); expect(alpha("highlight", 0, 0)).toBe(0); }); it("keeps every edge feature about a point wide", () => { expect(alpha("highlight", centre, 0)).toBeGreaterThan(200); expect(alpha("highlight", centre, 3)).toBe(0); expect(alpha("shade", 0, centre)).toBeGreaterThan(200); expect(alpha("shade", 3, centre)).toBe(0); }); it("lights the top and bottom rims and darkens only the sides", () => { expect(alpha("highlight", centre, 0)).toBeGreaterThan(alpha("highlight", 0, centre) * 4); expect(alpha("shade", centre, 0)).toBe(0); }); it("glows just inside the top rim more than the bottom, fading by its reach", () => { expect(alpha("glow", centre, 2)).toBeGreaterThan(alpha("glow", centre, 52)); expect(alpha("glow", centre, DEFAULT_GEOMETRY.glowReach + 1)).toBe(0); }); it("draws the darkened edge as a contour all the way round, and none without one", () => { const lens = createGlassSlices(27, 1); const edge = (x: number, y: number) => lens.masks.edge[(y * lens.size + x) * 4 + 3] ?? 0; expect(edge(centre, 0)).toBeGreaterThan(150); expect(edge(0, centre)).toBeGreaterThan(150); expect(edge(centre, 2)).toBe(0); expect(slices.masks.edge.every(value => value === 0)).toBe(true); }); it("gathers the hairline into a glint at the top-left, fainter at the bottom-right", () => { const lens = createGlassSlices(27, 1, { edge: 0 }); const highlight = (x: number, y: number) => lens.masks.highlight[(y * lens.size + x) * 4 + 3] ?? 0; // Points on the rim at 45° in each corner. const topLeft = highlight(8, 8); const bottomRight = highlight(46, 46); const topRight = highlight(46, 8); expect(topLeft).toBeGreaterThan(bottomRight); expect(bottomRight).toBeGreaterThan(topRight); }); it("bends a gentle lens less steeply at the rim", () => { const steepest = (maxFall: number) => { const lens = createGlassSlices(27, 1, { maxFall }); let fall = 0; for (let row = 0; row < 27; row++) { const at = (y: number) => lens.refraction[(y * lens.size + 27) * 4 + 1] ?? 128; fall = Math.max(fall, ((at(row) - at(row + 1)) / 255) * lens.scale); } return fall; }; expect(steepest(0.3)).toBeLessThan(steepest(0.8)); expect(steepest(0.3)).toBeLessThanOrEqual(0.3 + 0.1); }); }); describe("sliceRects", () => { it("covers the square without overlap", () => { const covered = sliceRects({ radius: 2, pixelRatio: 1 }).reduce((sum, [, , w, h]) => sum + w * h, 0); // A 5px square: everything but the one-pixel centre. expect(covered).toBe(24); }); });