concept-collection / numbl-image-filter
numbl-image-filter / src / synthetic.ts
90 lines · 2.8 KBBlameHistoryRaw
1/**
2 * Procedurally-generated sample images. These need no network and are the
3 * guaranteed-available samples (the remote photos in samples.ts may be
4 * blocked offline or by cross-origin policy).
5 */
6import type { RawImage } from "./imageConvert.ts";
8function makeCanvas(w: number, h: number): {
9 canvas: HTMLCanvasElement;
10 ctx: CanvasRenderingContext2D;
11} {
12 const canvas = document.createElement("canvas");
13 canvas.width = w;
14 canvas.height = h;
15 const ctx = canvas.getContext("2d", { willReadFrequently: true });
16 if (!ctx) throw new Error("Could not get a 2D canvas context.");
17 return { canvas, ctx };
20function readBack(
21 canvas: HTMLCanvasElement,
22 ctx: CanvasRenderingContext2D
23): RawImage {
24 const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
25 return { width: canvas.width, height: canvas.height, rgba: data.data };
28/** Smooth RGB field: red rises left→right, green top→bottom, blue diagonal. */
29export function gradientImage(w = 320, h = 320): RawImage {
30 const { canvas, ctx } = makeCanvas(w, h);
31 const img = ctx.createImageData(w, h);
32 for (let y = 0; y < h; y++) {
33 for (let x = 0; x < w; x++) {
34 const i = (y * w + x) * 4;
35 img.data[i] = Math.round((255 * x) / (w - 1));
36 img.data[i + 1] = Math.round((255 * y) / (h - 1));
37 img.data[i + 2] = Math.round((255 * (x + y)) / (w + h - 2));
38 img.data[i + 3] = 255;
39 }
40 }
41 ctx.putImageData(img, 0, 0);
42 return readBack(canvas, ctx);
45/** High-contrast colored shapes on white — good for edge-detection demos. */
46export function shapesImage(w = 360, h = 280): RawImage {
47 const { canvas, ctx } = makeCanvas(w, h);
48 ctx.fillStyle = "#ffffff";
49 ctx.fillRect(0, 0, w, h);
51 ctx.fillStyle = "#e23b3b";
52 ctx.beginPath();
53 ctx.arc(w * 0.32, h * 0.42, Math.min(w, h) * 0.22, 0, Math.PI * 2);
54 ctx.fill();
56 ctx.fillStyle = "#2f7de1";
57 ctx.fillRect(w * 0.5, h * 0.18, w * 0.34, h * 0.34);
59 ctx.fillStyle = "#2fae57";
60 ctx.beginPath();
61 ctx.moveTo(w * 0.62, h * 0.92);
62 ctx.lineTo(w * 0.42, h * 0.58);
63 ctx.lineTo(w * 0.86, h * 0.58);
64 ctx.closePath();
65 ctx.fill();
67 ctx.strokeStyle = "#222222";
68 ctx.lineWidth = Math.max(2, w * 0.012);
69 ctx.beginPath();
70 ctx.moveTo(0, h * 0.5);
71 ctx.bezierCurveTo(w * 0.25, h * 0.1, w * 0.75, h * 0.95, w, h * 0.45);
72 ctx.stroke();
74 return readBack(canvas, ctx);
77/** Colored checkerboard — exercises many per-region filters. */
78export function checkerImage(w = 320, h = 320, tiles = 8): RawImage {
79 const { canvas, ctx } = makeCanvas(w, h);
80 const palette = ["#222831", "#e2b53b", "#c0392b", "#16a085"];
81 const tw = w / tiles;
82 const th = h / tiles;
83 for (let ty = 0; ty < tiles; ty++) {
84 for (let tx = 0; tx < tiles; tx++) {
85 ctx.fillStyle = palette[(tx + ty) % palette.length];
86 ctx.fillRect(tx * tw, ty * th, Math.ceil(tw), Math.ceil(th));
87 }
88 }
89 return readBack(canvas, ctx);