From a9560d31b2f86e763539fb63a9f82fa8de438d18 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Sat, 8 Aug 2026 18:00:16 -0400 Subject: [PATCH] refactor: extract the shared svg composition helpers render-cws.mjs grew nest/markY/nestWordmark/compose so the store tiles could be assembled from the canonical brand files at render time. Move them to scripts/compose.mjs so other renderers can compose the same way; the tiles re-render byte-identical. Co-Authored-By: Claude Fable 5 --- prek.toml | 2 +- scripts/compose.mjs | 46 ++++++++++++++++++++++++++++++++++++ scripts/render-cws.mjs | 52 ++++++----------------------------------- src/lib/compose.test.ts | 46 ++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 46 deletions(-) create mode 100644 scripts/compose.mjs create mode 100644 src/lib/compose.test.ts diff --git a/prek.toml b/prek.toml index ea06e70..2e3f8b1 100644 --- a/prek.toml +++ b/prek.toml @@ -68,7 +68,7 @@ id = "cws-tiles" name = "render chrome web store tiles from brand svgs" entry = "npm run cws" language = "system" -files = '^(brand/(mark|sticker|wordmark)\.svg|scripts/(render-cws|render-icons|rasterize)\.mjs|brand/cws/.+\.png)$' +files = '^(brand/(mark|sticker|wordmark)\.svg|scripts/(render-cws|render-icons|rasterize|compose)\.mjs|brand/cws/.+\.png)$' pass_filenames = false [[repos.hooks]] diff --git a/scripts/compose.mjs b/scripts/compose.mjs new file mode 100644 index 0000000..5d93acf --- /dev/null +++ b/scripts/compose.mjs @@ -0,0 +1,46 @@ +// Shared SVG composition helpers for the brand render scripts. Compositions +// are assembled at render time by nesting the canonical brand SVGs into a +// paper-colored canvas, so derived art can never drift from the logo files. + +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +export const PAPER = '#f4f3ee' + +/** Inline a brand SVG as a nested positioned inside a composition. */ +export function nest(file, x, y, width, height = width) { + return readFileSync(resolve(file), 'utf8') + .replace('` + + `` + + tile.body() + + '' + ) +} diff --git a/scripts/render-cws.mjs b/scripts/render-cws.mjs index a93b82f..5da318e 100644 --- a/scripts/render-cws.mjs +++ b/scripts/render-cws.mjs @@ -1,9 +1,9 @@ // Renders the Chrome Web Store listing images from the brand SVGs. The -// compositions are assembled here at render time by nesting brand/mark.svg, -// brand/sticker.svg, and the generated brand/wordmark.svg into a -// paper-colored canvas, so the store art can never drift from the canonical -// logo files. Listing copy lives in the store listing itself, not in art; -// the only text here is the wordmark. +// compositions are assembled in scripts/compose.mjs terms by nesting +// brand/mark.svg, brand/sticker.svg, and the generated brand/wordmark.svg +// into a paper-colored canvas, so the store art can never drift from the +// canonical logo files. Listing copy lives in the store listing itself, not +// in art; the only text here is the wordmark. // // Run npm run wordmark first when brand sources change (the prek hooks // order this correctly): the marquee nests the generated wordmark file. @@ -11,44 +11,15 @@ // Usage: node scripts/render-cws.mjs (also: npm run cws) // Set CHROME to point at a specific Chrome/Chromium binary. -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { pathToFileURL } from 'node:url' +import { compose, markY, nest, nestWordmark } from './compose.mjs' import { findChrome, pngSize } from './render-icons.mjs' import { rasterize } from './rasterize.mjs' -const PAPER = '#f4f3ee' - -/** Inline a brand SVG as a nested positioned inside a composition. */ -function nest(file, x, y, width, height = width) { - return readFileSync(resolve(file), 'utf8') - .replace('` + - `` + - tile.body() + - '' - ) -} - function main() { const chrome = findChrome() const work = mkdtempSync(join(tmpdir(), 'substandard-cws-')) diff --git a/src/lib/compose.test.ts b/src/lib/compose.test.ts new file mode 100644 index 0000000..10ab8af --- /dev/null +++ b/src/lib/compose.test.ts @@ -0,0 +1,46 @@ +// The composition helpers' logic that runs without Chrome: geometry and SVG +// nesting against the real brand files. +import { describe, expect, it } from 'vitest' + +// @ts-expect-error plain-node script, no type declarations +import { compose, markY, nest, nestWordmark } from '../../scripts/compose.mjs' + +describe('nest', () => { + it('positions the brand svg and keeps it a single nested element', () => { + const svg = nest('brand/mark.svg', 10, 20, 64) + expect(svg).toContain(' { + expect(nest('brand/mark.svg', 0, 0, 64, 32)).toContain('width="64" height="32"') + }) +}) + +describe('markY', () => { + it('centers the ink, not the box', () => { + // Optical center at 75/128 of the box: top = canvas/2 - 75/128 * size. + expect(markY(280, 176)).toBe(37) + expect(markY(630, 340)).toBe(116) + }) +}) + +describe('nestWordmark', () => { + it('derives height from the wordmark viewBox and centers on cy', () => { + const source = nest('brand/wordmark.svg', 0, 0, 1) + const [, vw, vh] = source.match(/viewBox="0 0 (\d+) (\d+)"/)! + const height = Math.round((560 * Number(vh)) / Number(vw)) + expect(nestWordmark(540, 315, 560)).toContain( + `x="540" y="${Math.round(315 - height / 2)}" width="560" height="${height}"`, + ) + }) +}) + +describe('compose', () => { + it('wraps the body in a paper canvas of the tile size', () => { + const svg = compose({ width: 1200, height: 630, body: () => '' }) + expect(svg).toContain('width="1200" height="630"') + expect(svg).toContain('fill="#f4f3ee"') + expect(svg).toContain('') + }) +}) -- 2.51.2