Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
4.6 kB · 120 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121// Renders the Chrome Web Store listing images: the promo tiles from the brand// SVGs, and one 1280x800 listing screenshot per `store` entry in// scripts/shots.mjs. The// 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.//// 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 { tmpdir } from 'node:os'import { join, resolve } from 'node:path'import { pathToFileURL } from 'node:url'
import { compose, nest, nestPng, nestWordmark } from './compose.mjs'import { findChrome, pngSize } from './render-icons.mjs'import { rasterize } from './rasterize.mjs'import { capturePath, storeShots, storeTilePath } from './shots.mjs'
/** * One listing screenshot: the real popup capture from the status docs * (docs/img/, re-captured by scripts/capture-status-docs.mjs) beside the brand * lockup, framed like the site's cards — ink border, solid offset shadow, a * slight tilt. The frame follows the capture's own aspect ratio, so a * re-capture of a taller or wider popup is never squashed to fit. * * The captures come from the same list the frontpage gallery renders * (scripts/shots.mjs), so the store and substandard.blog cannot advertise two * different popups. */function screenshotTile(shot) { const capture = capturePath(shot) return { width: 1280, height: 800, dest: storeTilePath(shot), body: () => { const size = pngSize(readFileSync(resolve(capture))) const [x, w] = [570, 680] const h = Math.round((w * size.height) / size.width) const y = Math.round((800 - h) / 2) return ( nest('brand/sticker.svg', 135, 190, 300) + nestWordmark(65, 580, 440) + `<g transform="rotate(-1 ${x + w / 2} ${y + h / 2})">` + `<rect x="${x + 7}" y="${y + 8}" width="${w}" height="${h}" fill="#dedcd3"/>` + nestPng(capture, x, y, w, h) + `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="#26282b" stroke-width="3"/>` + '</g>' ) }, }}
const TILES = { 'promo-small': { width: 440, height: 280, dest: 'brand/cws/promo-small.png', // Sticker over wordmark, the stack optically centred: at 440x280 there is // no room to set the wordmark beside the sticker without the name coming // out smaller than the mark, which is backwards for a tile whose whole job // is to say what the thing is called. The marquee, at 2.5:1, has the width // for the horizontal lockup and keeps it. body: () => nest('brand/sticker.svg', 160, 25, 120) + nestWordmark(70, 212, 300), }, marquee: { width: 1400, height: 560, dest: 'brand/cws/marquee.png', body: () => nest('brand/sticker.svg', 150, 110, 340) + nestWordmark(560, 280, 760), }, // A screenshot tile per store shot, keyed by capture name so a shot that // leaves the list takes its tile's name with it. ...Object.fromEntries(storeShots().map((shot) => [shot.name, screenshotTile(shot)])),}
function main() { const chrome = findChrome() const work = mkdtempSync(join(tmpdir(), 'substandard-cws-')) try { for (const [name, tile] of Object.entries(TILES)) { const { opaque, png } = rasterize( chrome, work, name, compose(tile), tile.width, tile.height, ) const dim = pngSize(png) if (dim.width !== tile.width || dim.height !== tile.height) { throw new Error(`${name}: rendered ${dim.width}x${dim.height}`) } // The paper ground makes a healthy tile fully opaque. const fraction = opaque / (tile.width * tile.height) if (fraction < 0.99) { throw new Error(`${name}: only ${(fraction * 100).toFixed(1)}% opaque — misrender`) } writeFileSync(resolve(tile.dest), png) console.log(`${name}: ${png.length} bytes -> ${tile.dest}`) } } finally { rmSync(work, { recursive: true, force: true }) }}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { main()}