/** * A generator that renders one flat colour is the failure this file exists to * catch, because nothing else reports it. The image is valid, the canvas is * painted, no exception is thrown, and the only symptom is a swatch in the * picker that looks like the Solid pattern. * * Reaction diffusion shipped that way. Gray-Scott only makes patterns along a * narrow diagonal band of the feed/kill plane, it drew the two rates * independently and mostly missed the band, and it seeded a fixed number of * nucleation sites regardless of image area, which flooded the half-size * thumbnail even when the rates were good. Both failures are silent, and both * depend on the seed and the sliders, so a spot check of one render would not * have found either. * * The sweep is therefore over parameters rather than over one image: every * generator, at both the sizes the editor actually renders, across the corners * and middle of the slider space and a spread of seeds. Solid is excluded by * name — one colour is what it is for. * * Run with `npm test`. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; const run = promisify(execFile); const web = join(new URL(".", import.meta.url).pathname, ".."); const dir = await mkdtemp(join(tmpdir(), "camo-patterns-")); const bundle = join(dir, "fixture.mjs"); await run( join(web, "node_modules/.bin/rolldown"), [ join(web, "scripts/camo-patterns.fixture.ts"), "--format", "esm", "--platform", "node", "--file", bundle, ], { cwd: web }, ); const { GENERATORS } = await import(bundle); /** Node has no ImageData, and a generator only ever touches these three. */ const image = (width, height) => ({ width, height, data: new Uint8ClampedArray(width * height * 4), }); /** The camo, and the half-size thumbnail the picker draws beside each name. */ const SIZES = [ [84, 72], [42, 36], ]; /** Four distinct greys: a generator that bands is visible, a flat one is not. */ const PALETTE = [ [0, 0, 0], [85, 85, 85], [170, 170, 170], [255, 255, 255], ]; const SEEDS = [1, 20260807, 3189639203]; const SLIDER = [0, 0.5, 1]; /** Distinct colours in the render, and the share held by the commonest one. */ function spread(img) { const counts = new Map(); for (let i = 0; i < img.data.length; i += 4) { const key = (img.data[i] << 16) | (img.data[i + 1] << 8) | img.data[i + 2]; counts.set(key, (counts.get(key) ?? 0) + 1); } const pixels = img.width * img.height; return { colours: counts.size, top: Math.max(...counts.values()) / pixels }; } for (const gen of GENERATORS) { if (gen.id === "solid") continue; test(`${gen.name} renders more than one colour`, () => { for (const [w, h] of SIZES) { for (const seed of SEEDS) { for (const scale of SLIDER) { for (const detail of SLIDER) { const params = { seed, scale, contrast: 0.6, detail, angle: 0 }; const img = image(w, h); gen.render(img, PALETTE, params); const { colours, top } = spread(img); const where = `${gen.id} ${w}x${h} ${JSON.stringify(params)}`; assert.ok(colours > 1, `${where}: one flat colour`); // Two colours where one covers all but a few pixels is the same // failure wearing a disguise, so the dominant colour is capped. assert.ok( top < 0.97, `${where}: ${Math.round(top * 100)}% one colour`, ); } } } } }); } test.after(() => rm(dir, { recursive: true, force: true }));