// WHAT A CHART PAINTED, for the e2e specs: a screenshot of the chart read back // as pixels in the page (the browser's own PNG decoder, an drawn to a // canvas), so a test checks the rendered geometry — where the stack's top // is, which colours show — rather than what a style says a stroke is. // // No Playwright import (common/ does not depend on it): the page and the // locator are typed by the two methods this uses. // eslint-disable-next-line @typescript-eslint/no-explicit-any type Shooter = { screenshot(opts?: any): Promise }; // eslint-disable-next-line @typescript-eslint/no-explicit-any type Evaluator = { evaluate(fn: any, arg: any): Promise }; export type Rgb = [number, number, number]; // "rgb(1, 2, 3)" / "rgba(1, 2, 3, 0.5)" → [1, 2, 3]. export function rgbOf(css: string): Rgb { const m = css.match(/[\d.]+/g); if (!m || m.length < 3) throw new Error(`not an rgb() colour: ${css}`); return [Number(m[0]), Number(m[1]), Number(m[2])]; } // `alpha` of `fg` over `bg`, as the browser composites a translucent fill. export function over(fg: Rgb, alpha: number, bg: Rgb): Rgb { return [0, 1, 2].map((c) => Math.round(fg[c] * alpha + bg[c] * (1 - alpha))) as Rgb; } export type Painted = { // Per requested column: the first row, from the top, of a run of `run` rows // that each differ from `ground` by more than `tol` in some channel; null // when the column is all ground. tops: (number | null)[]; // Per requested colour: how many pixels are within `tol` of it, and in how // many distinct columns. counts: { pixels: number; columns: number }[]; width: number; height: number; }; export async function painted( page: Evaluator, target: Shooter, opts: { columns: number[]; ground: Rgb; colours: Rgb[]; tol?: number; run?: number }, ): Promise { const png = await target.screenshot({ scale: "css", animations: "disabled" }); return page.evaluate( async ({ b64, columns, ground, colours, tol, run, }: { b64: string; columns: number[]; ground: Rgb; colours: Rgb[]; tol: number; run: number; }) => { const img = new Image(); img.src = `data:image/png;base64,${b64}`; await img.decode(); const c = document.createElement("canvas"); c.width = img.naturalWidth; c.height = img.naturalHeight; const ctx = c.getContext("2d")!; ctx.drawImage(img, 0, 0); const { data, width, height } = ctx.getImageData(0, 0, c.width, c.height); const at = (x: number, y: number) => (y * width + x) * 4; const differs = (x: number, y: number) => { const p = at(x, y); return [0, 1, 2].some((k) => Math.abs(data[p + k] - ground[k]) > tol); }; const tops = columns.map((cx) => { const x = Math.min(width - 1, Math.max(0, Math.round(cx))); for (let y = 0; y + run <= height; y++) { let solid = true; for (let d = 0; d < run && solid; d++) solid = differs(x, y + d); if (solid) return y; } return null; }); const counts = colours.map((col) => { let pixels = 0; const cols = new Set(); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const p = at(x, y); if ([0, 1, 2].every((k) => Math.abs(data[p + k] - col[k]) <= tol)) { pixels++; cols.add(x); } } } return { pixels, columns: cols.size }; }); return { tops, counts, width, height }; }, { b64: png.toString("base64"), columns: opts.columns, ground: opts.ground, colours: opts.colours, tol: opts.tol ?? 12, run: opts.run ?? 3, }, ); }