raster.test.js
5.5 kB · javascript · 133 lines
1import { expect, test } from "bun:test";2import { dark } from "../../../site/kit/theme/theme.js";3import { ink, raster } from "./index.js";45const BLACK = [0, 0, 0, 255];6const WHITE = [255, 255, 255, 255];7const theme = ink(dark);8const red = (pen, x, y) => pen.pixels().colors[(y * pen.width + x) * 4];9const at = (pen, x, y) => Array.from(pen.pixels().colors.slice((y * pen.width + x) * 4, (y * pen.width + x) * 4 + 4));1011function board(draw) {12 const pen = raster(64, 64, BLACK);13 draw(pen);14 return pen;15}1617function lit(pen) {18 const { colors } = pen.pixels();19 let sum = 0;20 for (let i = 0; i < colors.length; i += 4) sum += (colors[i] - theme.ground[0]) / (theme.fg[0] - theme.ground[0]);21 return sum;22}2324// VERBS2526test("rect feathers its edge by the signed distance", () => {27 const pen = board((p) => p.rect(10.25, 8, 20, 10, WHITE));28 expect([red(pen, 20, 12), red(pen, 10, 12), red(pen, 9, 12), red(pen, 30, 12)]).toEqual([255, 191, 0, 64]);29});3031test("round_rect cuts its corner on the circle of its radius", () => {32 const pen = board((p) => p.round_rect(8.5, 8.5, 40, 40, 10, WHITE));33 expect([red(pen, 12, 10), red(pen, 8, 8), red(pen, 28, 9)]).toEqual([128, 0, 255]);34});3536test("disc covers inside, feathers its rim and leaves outside", () => {37 const pen = board((p) => p.disc(32.25, 32.5, 10, WHITE));38 expect([red(pen, 32, 32), red(pen, 41, 32), red(pen, 42, 32), red(pen, 43, 32)]).toEqual([255, 255, 64, 0]);39});4041test("a tie rounds half away from zero as Rust does", () => {42 const pen = board((p) => p.disc(32.5, 32.5, 10, [253, 253, 253, 255]));43 expect(red(pen, 42, 32)).toBe(127);44});4546test("ring strokes about its radius and leaves its centre", () => {47 const pen = board((p) => p.ring(32.25, 32.5, 10, 2, WHITE));48 expect([red(pen, 42, 32), red(pen, 43, 32), red(pen, 21, 32), red(pen, 32, 32)]).toEqual([255, 64, 191, 0]);49});5051test("segment strokes its width with round caps", () => {52 const pen = board((p) => p.segment([10, 20.5], [50.25, 20.5], 4, WHITE));53 expect([red(pen, 30, 20), red(pen, 30, 22), red(pen, 30, 23), red(pen, 52, 20)]).toEqual([255, 128, 0, 64]);54});5556test("polyline blends its joint once where two segments blend twice", () => {57 const glass = [255, 255, 255, 128];58 const pts = [[10, 20.5], [40.5, 20.5], [40.5, 50]];59 const one = board((p) => p.polyline(pts, 4, glass));60 const two = board((p) => {61 p.segment(pts[0], pts[1], 4, glass);62 p.segment(pts[1], pts[2], 4, glass);63 });64 expect([red(one, 40, 20), red(two, 40, 20)]).toEqual([128, 192]);65});6667test("triangle feathers its edge by the signed distance", () => {68 const pen = board((p) => p.triangle([10, 10.25], [50, 10.25], [10, 50], WHITE));69 expect([red(pen, 20, 20), red(pen, 12, 10), red(pen, 12, 9)]).toEqual([255, 191, 0]);70});7172test("polygon fills by the even-odd rule", () => {73 const star = [0, 1, 2, 3, 4].map((i) => [32 + 28 * Math.cos(-Math.PI / 2 + (i * 4 * Math.PI) / 5), 32 + 28 * Math.sin(-Math.PI / 2 + (i * 4 * Math.PI) / 5)]);74 const pen = board((p) => p.polygon(star, WHITE));75 expect([red(pen, 31, 12), red(pen, 32, 32)]).toEqual([255, 0]);76});7778test("arc strokes its sweep alone with round caps", () => {79 const pen = board((p) => p.arc([32, 32.5], 10, [0, Math.PI / 2], 2, WHITE));80 expect([red(pen, 42, 32), red(pen, 38, 39), red(pen, 42, 31), red(pen, 21, 32)]).toEqual([255, 255, 97, 0]);81});8283test("image paints whole pixels nearest sampled and blends its alpha", () => {84 const pixels = { shape: [2, 2], colors: Uint8Array.from([255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 128]) };85 const pen = board((p) => p.image(8, 8, 4, 4, pixels));86 expect([at(pen, 8, 8), at(pen, 11, 8), at(pen, 8, 11), at(pen, 10, 10)]).toEqual([[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [128, 128, 128, 255]]);87 const shifted = board((p) => p.image(8.5, 8, 4, 4, pixels));88 expect([red(shifted, 8, 8), red(shifted, 9, 8), at(shifted, 11, 8), red(shifted, 12, 8)]).toEqual([0, 255, [0, 255, 0, 255], 0]);89});9091// BOARD9293test("frame cells tile the frame exactly", () => {94 const frame = raster(1024, 1024, theme.ground).frame(0.08);95 expect(Math.abs(frame.cell(81) * 81 - frame.w)).toBeLessThan(1e-9);96});9798test("a disc covers its own area", () => {99 const pen = raster(256, 256, theme.ground);100 pen.disc(128, 128, 90, theme.fg);101 const want = Math.PI * 90 * 90;102 expect(Math.abs(lit(pen) - want) / want).toBeLessThan(0.02);103});104105test("a polyline covers its stroke area", () => {106 const pen = raster(512, 512, theme.ground);107 const thick = 6;108 const pts = Array.from({ length: 1000 }, (_, i) => {109 const x = 6 + (500 * i) / 999;110 return [x, 256 + 100 * Math.sin((2 * Math.PI * x) / 250)];111 });112 pen.polyline(pts, thick, theme.fg);113 let length = 0;114 for (let i = 1; i < pts.length; i++) length += Math.sqrt((pts[i][0] - pts[i - 1][0]) ** 2 + (pts[i][1] - pts[i - 1][1]) ** 2);115 const want = length * thick + Math.PI * (thick / 2) ** 2;116 expect(Math.abs(lit(pen) - want) / want).toBeLessThan(0.03);117});118119test("a two point polyline is a segment", () => {120 const [a, b] = [[17.3, 40.9], [190.7, 123.4]];121 const one = raster(256, 192, theme.ground);122 one.segment(a, b, 7, theme.fg);123 const two = raster(256, 192, theme.ground);124 two.polyline([a, b], 7, theme.fg);125 expect(two.pixels().colors).toEqual(one.pixels().colors);126});127128test("the pixels are rows of rgba, shape height then width", () => {129 const { shape, colors } = raster(1200, 630, theme.ground).pixels();130 expect(shape).toEqual([630, 1200]);131 expect(colors).toBeInstanceOf(Uint8ClampedArray);132 expect(colors.length).toBe(630 * 1200 * 4);133});