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});