pen.test.ts
4.4 kB · typescript · 98 lines
1import { expect, test } from "bun:test";2import sharp from "sharp";3import * as core from "mrlyjs/core";4import { field, ink, raster, svg, type Pen, type Pixels } from "mrlyjs/view";5import { dark, light } from "../site/kit/theme/theme.js";67const SIZE = 256;8const LEVELS = 2;9const EDGE = 64;10const SHARE = 0.04;11const theme = ink(dark);1213const png = (p: Pixels) => sharp(p.colors, { raw: { width: p.shape[1], height: p.shape[0], channels: 4 } }).png().toBuffer();14const wave = Array.from({ length: 200 }, (_, i): [number, number] => [10 + i * 1.18, 128 + 80 * Math.sin(i / 20)]);15const star = [0, 1, 2, 3, 4].map((i): [number, number] => [128 + 110 * Math.cos(-Math.PI / 2 + (i * 4 * Math.PI) / 5), 128 + 110 * Math.sin(-Math.PI / 2 + (i * 4 * Math.PI) / 5)]);16const spots = Array.from({ length: 64 }, (_, i) => Math.sin(i * 0.7) + Math.cos(i * 0.3));1718const scenes: Record<string, (pen: Pen) => void> = {19 rect: (p) => p.rect(20.3, 30.7, 150.4, 90.2, theme.fg),20 round_rect: (p) => p.round_rect(20.3, 30.7, 200, 150, 30, theme.fg),21 disc: (p) => p.disc(128.3, 127.6, 90, theme.fg),22 ring: (p) => p.ring(128.3, 127.6, 80, 6, theme.fg),23 segment: (p) => p.segment([20.5, 30.25], [230.1, 200.7], 7, theme.fg),24 polyline: (p) => p.polyline(wave, 5, theme.fg),25 triangle: (p) => p.triangle([30, 220], [128.4, 25.3], [230.2, 210.1], theme.fg),26 polygon: (p) => p.polygon(star, theme.fg),27 arc: (p) => p.arc([128.3, 127.6], 80, [0.3, 4.1], 9, theme.fg),28 image: (p) => field.draw(p, p.area(0.125), 8, 8, spots, theme.Ramp.heat()),29 mixed: (p) => {30 p.rect(10, 10, 236, 236, theme.panel);31 field.draw(p, p.area(0.3), 8, 8, spots, theme.Ramp.fire());32 p.disc(90, 100, 60, theme.blue);33 p.ring(160, 150, 50, 8, theme.fade(theme.orange, 0.6));34 p.polyline(wave, 3, theme.yellow);35 p.triangle([20, 240], [120, 140], [240, 236], theme.fade(theme.green, 0.5));36 p.arc([128, 128], 100, [-1, 2], 5, theme.pink);37 p.round_rect(150, 20, 80, 60, 12, theme.indigo);38 p.segment([20, 20], [100, 60], 4, theme.fg);39 p.polygon(star.map(([x, y]) => [x * 0.4 + 140, y * 0.4 + 120]), theme.fade(theme.fg, 0.4));40 },41};4243async function compare(scene: (pen: Pen) => void) {44 const ours = raster(SIZE, SIZE, theme.ground);45 scene(ours);46 const pen = svg(SIZE, SIZE, theme.ground);47 scene(pen);48 const theirs = await sharp(Buffer.from(await pen.text({ png }))).ensureAlpha().raw().toBuffer();49 const a = ours.pixels().colors;50 const row = { max: 0, over: 0, edges: 0, inside: 0, edge: 0 };51 for (let y = 0; y < SIZE; y++) {52 for (let x = 0; x < SIZE; x++) {53 const i = (y * SIZE + x) * 4;54 let d = 0;55 for (let k = 0; k < 4; k++) d = Math.max(d, Math.abs(a[i + k] - theirs[i + k]));56 let flat = true;57 for (let j = Math.max(y - 1, 0); j <= Math.min(y + 1, SIZE - 1); j++) {58 for (let k = Math.max(x - 1, 0); k <= Math.min(x + 1, SIZE - 1); k++) {59 const n = (j * SIZE + k) * 4;60 if (a[n] !== a[i] || a[n + 1] !== a[i + 1] || a[n + 2] !== a[i + 2] || a[n + 3] !== a[i + 3]) flat = false;61 }62 }63 row.max = Math.max(row.max, d);64 if (d > LEVELS) row.over++;65 if (flat) row.inside = Math.max(row.inside, d);66 else {67 row.edges++;68 row.edge = Math.max(row.edge, d);69 }70 }71 }72 return row;73}7475// PARITY7677for (const [name, scene] of Object.entries(scenes)) {78 test(`${name} through svg and sharp meets raster within ${LEVELS} levels off its edges`, async () => {79 const row = await compare(scene);80 const share = row.over / (SIZE * SIZE);81 console.log(`${name.padEnd(10)} max ${String(row.max).padStart(3)} over ${LEVELS} ${(share * 100).toFixed(3).padStart(6)}% inside max ${row.inside} edge max ${String(row.edge).padStart(3)} over ${row.edges} edge pixels`);82 expect(row.inside).toBeLessThanOrEqual(LEVELS);83 expect(row.edge).toBeLessThanOrEqual(EDGE);84 expect(share).toBeLessThanOrEqual(SHARE);85 });86}8788// INK8990test("ink from the Rust theme equals ink from the kit theme", async () => {91 const bytes = await Bun.file(new URL(import.meta.resolve("mrlyjs/pkg/core/mrlyjs_core_bg.wasm"))).arrayBuffer();92 core.initSync({ module: bytes });93 for (const [rust, kit] of [[core.colors.DARK(), dark], [core.colors.LIGHT(), light]]) {94 const ours = ink(rust);95 const theirs = ink(kit);96 for (const key of Object.keys(theirs)) if (Array.isArray(theirs[key as keyof typeof theirs])) expect([key, ours[key as keyof typeof ours]]).toEqual([key, theirs[key as keyof typeof theirs]]);97 }98});