kit.test.js
8.1 kB · javascript · 200 lines
1import { expect, test } from "bun:test";2import { dark, light } from "../../../site/kit/theme/theme.js";3import { Grid, fade, field, frame, grid, hex, ink, iso, mix, plot, raster } from "./index.js";45const HUES = {6 red: [255, 61, 64],7 orange: [255, 143, 44],8 yellow: [255, 209, 0],9 green: [50, 204, 88],10 mint: [0, 209, 187],11 teal: [0, 202, 216],12 cyan: [30, 201, 243],13 blue: [0, 140, 255],14 indigo: [103, 104, 250],15 purple: [211, 50, 233],16 pink: [255, 50, 90],17 brown: [177, 132, 98],18 gray: [142, 142, 147],19};20const RUST = {21 dark: { ground: [0, 0, 0], panel: [17, 17, 18], line: [31, 31, 32], fg: [255, 255, 255], dim: [142, 142, 147], ...HUES },22 light: { ground: [255, 255, 255], panel: [241, 241, 242], line: [221, 221, 223], fg: [0, 0, 0], dim: [85, 85, 88], ...HUES },23};24const theme = ink(dark);2526function recorder() {27 const calls = [];28 const pen = new Proxy({}, { get: (_, key) => (...args) => calls.push([key, ...args]) });29 return { pen, calls };30}3132// INK3334test("the dark and light inks are ink.rs's", () => {35 for (const [name, palette] of [["dark", dark], ["light", light]]) {36 const read = ink(palette);37 for (const [key, rgb] of Object.entries(RUST[name])) expect([name, key, read[key]]).toEqual([name, key, [...rgb, 255]]);38 }39});4041test("ink reads hex strings, byte arrays and serde colors alike", () => {42 const bytes = Object.fromEntries(Object.entries(theme).filter(([, v]) => v.length === 4 && typeof v[0] === "number"));43 const serde = Object.fromEntries(Object.entries(bytes).map(([k, [r, g, b, a]]) => [k, { r, g, b, a }]));44 expect(ink(bytes).blue).toEqual(theme.blue);45 expect(ink(serde).dim).toEqual(theme.dim);46 expect(() => ink({ ...bytes, fg: undefined })).toThrow("no fg");47});4849test("ramp ends are its end stops", () => {50 const ramp = theme.Ramp.heat();51 expect(ramp.at(0)).toEqual(theme.ground);52 expect(ramp.at(1)).toEqual(theme.fg);53});5455test("an empty ramp reads the ground", () => {56 expect(new theme.Ramp([]).at(0.5)).toEqual(theme.ground);57});5859test("mix halfway sits between the two", () => {60 expect(mix([0, 0, 0, 255], [255, 255, 255, 255], 0.5)[0]).toBe(128);61});6263test("fade sets the alpha to its share of opaque", () => {64 expect(fade(theme.blue, 0.5)).toEqual([0, 140, 255, 128]);65});6667test("the inks are the theme's six", () => {68 expect(theme.inks).toEqual([theme.blue, theme.orange, theme.yellow, theme.green, theme.pink, theme.indigo]);69});7071// FRAME7273test("a frame splits into columns and rows that tile it", () => {74 const box = frame(10, 20, 300, 200);75 expect(box.cols(3).map((f) => f.x)).toEqual([10, 110, 210]);76 expect(box.rows(4).at(-1)).toMatchObject({ y: 170, h: 50, w: 300 });77 expect(box.square()).toMatchObject({ x: 60, y: 20, w: 200, h: 200 });78 expect(box.center()).toEqual([160, 120]);79});8081// GRID8283test("the logo at level two is twenty five wide and squares its ones", () => {84 const rows = grid.mask(grid.LOGO, 2);85 expect([rows.length, rows[0].length, rows.flat().filter(Boolean).length]).toEqual([25, 25, 21 * 21]);86});8788test("at gap zero neighbouring cells meet on a whole pixel", () => {89 const lattice = new Grid(frame(3.3, 0, 389.12, 389.12), 5, 5, 0);90 const [x0, , w0] = lattice.cell(0, 0);91 const [x1] = lattice.cell(1, 0);92 expect(x0 + w0).toBe(x1);93 expect(x1).toBe(Math.round(x1));94});9596test("at gap zero a carpet leaves no seam on the raster", () => {97 const pen = raster(400, 400, [0, 0, 0, 255]);98 grid.carpet(pen, frame(3.3, 0.7, 389.12, 389.12), grid.mask(["11", "11"], 3), 0, [255, 255, 255, 255]);99 const { colors } = pen.pixels();100 const seams = [];101 for (let x = 3; x < 392; x++) if (colors[(200 * 400 + x) * 4] !== 255) seams.push(["x", x]);102 for (let y = 1; y < 390; y++) if (colors[(y * 400 + 200) * 4] !== 255) seams.push(["y", y]);103 expect(seams).toEqual([]);104});105106test("paint fills the cells whose type the ink maps", () => {107 const { pen, calls } = recorder();108 const cells = { shape: [2, 3], types: Uint8Array.from([1, 0, 1, 0, 2, 0]) };109 new Grid(frame(0, 0, 30, 20), 3, 2, 0).paint(pen, cells, (t) => (t === 1 ? theme.blue : t === 2 ? theme.orange : null));110 expect(calls).toEqual([["rect", 0, 0, 10, 10, theme.blue], ["rect", 20, 0, 10, 10, theme.blue], ["rect", 10, 10, 10, 10, theme.orange]]);111});112113// HEX114115test("a hexagon of side s has six s squared triangles", () => {116 for (let n = 1; n < 8; n++) {117 let rows = 0;118 for (let row = 0; row < 2 * n; row++) rows += hex.row_len(n, row);119 expect(rows).toBe(hex.count(n));120 }121});122123test("every triangle of a hexagon is offered to the ink", () => {124 const pen = raster(128, 128, theme.ground);125 let seen = 0;126 hex.hexagon(pen, pen.frame(0.1), 3, 0, () => void seen++);127 expect(seen).toBe(hex.count(3));128});129130test("a hex slice draws one triangle per inked site and wants a hexagon", () => {131 const { pen, calls } = recorder();132 const cell = { cell: { shape: [2, 3], types: Uint8Array.from([1, 1, 0, 1, 0, 1]) }, start: 0 };133 hex.draw(pen, frame(0, 0, 100, 100), cell, 1, (t) => (t ? theme.fg : null));134 expect(calls.map(([key]) => key)).toEqual(["triangle", "triangle", "triangle", "triangle"]);135 expect(() => hex.draw(pen, frame(0, 0, 100, 100), { cell: { shape: [2, 2], types: [1, 1, 1, 1] }, start: 0 }, 0, () => theme.fg)).toThrow("hexagon");136});137138// ISO139140test("the projection lifts z straight up the screen", () => {141 const ground = iso.project(0, 0, 0);142 const above = iso.project(0, 0, 1);143 expect(Math.abs(above[0] - ground[0])).toBeLessThan(1e-12);144 expect(above[1]).toBeLessThan(ground[1]);145});146147test("iso drops faces turned away and paints back to front in three tones", () => {148 const { pen, calls } = recorder();149 const face = (normal, z) => ({ normal, verts: [{ x: 0, y: 0, z }, { x: 1, y: 0, z }, { x: 1, y: 1, z }, { x: 0, y: 1, z }] });150 const quads = [face({ x: 0, y: 0, z: 1 }, 1), face({ x: 0, y: 0, z: -1 }, 0), face({ x: 0, y: 1, z: 0 }, 0.5), face({ x: 1, y: 0, z: 0 }, -1)];151 const shade = [theme.fg, theme.blue, theme.dim];152 iso.draw(pen, frame(0, 0, 400, 400), quads, shade, theme.line);153 expect(calls.filter(([key]) => key === "polygon").map((call) => call[2])).toEqual([theme.dim, theme.blue, theme.fg]);154 expect(calls.filter(([key]) => key === "polyline").map((call) => [call[1].length, call[2]])).toEqual([[5, 1], [5, 1], [5, 1]]);155});156157// PLOT158159test("bars stand on the foot of the frame, the tallest filling it", () => {160 const { pen, calls } = recorder();161 plot.bars(pen, frame(0, 0, 40, 100), [1, -4, 2, 0], 0.5, theme.blue);162 expect(calls.map((call) => call.slice(1, 5))).toEqual([[2.5, 75, 5, 25], [12.5, 0, 5, 100], [22.5, 50, 5, 50], [32.5, 100, 5, 0]]);163});164165test("a curve and a staircase span their frame", () => {166 const { pen, calls } = recorder();167 const box = frame(10, 10, 100, 50);168 plot.curve(pen, box, [0, 1, 2], [5, 9, 7], 2, theme.fg);169 plot.staircase(pen, box, [3, 1], 2, theme.fg);170 expect(calls[0][1]).toEqual([[10, 60], [60, 10], [110, 35]]);171 expect(calls[1][1]).toEqual([[10, 10], [60, 10], [60, 60], [110, 60]]);172});173174// FIELD175176test("a flat field paints the ramp's low end everywhere", () => {177 const pen = raster(64, 64, theme.ground);178 field.sample(pen, frame(0, 0, 64, 64), 8, () => 1, theme.Ramp.tone(theme.blue, theme.yellow));179 expect(Array.from(pen.pixels().colors.slice(0, 4))).toEqual([...theme.blue.slice(0, 3), 255]);180});181182test("a field paints the pixels field.rs paints", () => {183 const [w, h] = [7, 5];184 const values = Array.from({ length: w * h }, (_, i) => Math.sin(i) * 3);185 const box = frame(3.6, 2.2, 50.3, 41.7);186 const ramp = theme.Ramp.heat();187 const pen = raster(64, 64, theme.panel);188 field.draw(pen, box, w, h, values, ramp);189 const want = raster(64, 64, theme.panel);190 const lo = Math.min(...values);191 const reach = Math.max(...values) - lo;192 for (let py = Math.ceil(box.y); py < Math.floor(box.y + box.h); py++) {193 for (let px = Math.ceil(box.x); px < Math.floor(box.x + box.w); px++) {194 const col = Math.min(Math.floor(((px + 0.5 - box.x) / box.w) * w), w - 1);195 const row = Math.min(Math.floor(((py + 0.5 - box.y) / box.h) * h), h - 1);196 want.rect(px, py, 1, 1, ramp.at((values[row * w + col] - lo) / reach));197 }198 }199 expect(pen.pixels().colors).toEqual(want.pixels().colors);200});