Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
6.5 kB · 167 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168// Renders the wordmark: "sub" and "standard" snapped apart and hinged at// the break — whitespace opens where the word gave way, each piece pivots// there ("sub" leans back, "standard" falls forward) so the far ends do// the drooping and the mark slides downhill toward the underline's tail,// echoing the two misaligned blobs of brand/mark.svg. Beneath it, a// spellcheck underline that ends in the mark's give-out tail.//// Writes brand/wordmark.svg (the composed source; other compositions nest// it) and transparent-background PNGs at a few widths under brand/wordmark/.//// Usage: node scripts/render-wordmark.mjs (also: npm run wordmark)// Set CHROME to point at a specific Chrome/Chromium binary.
import { execFileSync } from 'node:child_process'import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'import { tmpdir } from 'node:os'import { join, resolve } from 'node:path'import { pathToFileURL } from 'node:url'
import { findChrome, pngSize } from './render-icons.mjs'import { rasterize } from './rasterize.mjs'
// The snap (variant 9.2 of the brand board, "hinged at the break"): both// pieces pivot at the gap — tilts in degrees, gap/drop in ems of the font// size. Keeping the hinge at the break means no baseline step at the gap;// the far ends do the drooping.const SNAP = { gap: 0.12, sub: { tilt: -2, drop: 0 }, std: { tilt: 2, drop: 0.02 },}
const F = 128const FONT = `700 ${F}px system-ui, sans-serif`const MARGIN = 14const BASELINE = 138const HEIGHT = 240
/** * Measure the two pieces in the rendering Chrome itself, so the layout and * the underline are sized to the font actually used — estimating average * character widths let the pieces overlap and eat the gap. */function measureWords(chrome, work) { const wrapper = join(work, 'measure.html') writeFileSync( wrapper, `<!doctype html><meta charset="utf-8"><body><script> const ctx = document.createElement('canvas').getContext('2d') ctx.font = ${JSON.stringify(FONT)} ctx.letterSpacing = '${-0.01 * F}px' document.body.textContent = 'RESULT ' + JSON.stringify({ sub: ctx.measureText('sub').width, standard: ctx.measureText('standard').width, }) </script></body>`, ) const dom = execFileSync( chrome, [ '--headless=new', '--disable-gpu', '--no-sandbox', `--user-data-dir=${join(work, 'profile-measure')}`, '--virtual-time-budget=2000', '--dump-dom', pathToFileURL(wrapper).href, ], { encoding: 'utf8', maxBuffer: 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] }, ) const m = dom.match(/RESULT (\{[^<]+\})/) if (!m) throw new Error('measure: no result in page output') const { sub, standard } = JSON.parse(m[1]) return { subW: Math.round(sub), stdW: Math.round(standard) }}
/** * A spellcheck underline sized to the word: even humps, then the mark's * give-out tail (same segment ratios as brand/mark.svg, scaled to hump * size). */export function underline(x0, y, width, hump, amp) { const tail = hump const n = Math.max(2, Math.round((width - tail) / hump)) let d = `M${x0},${y} q${hump / 2},${-amp} ${hump},0` for (let i = 1; i < n; i++) d += ` t${hump},0` d += ` q${hump / 2},${-amp * 0.65} ${hump},${amp * 0.35}` d += ` q${(hump * 3) / 8},${amp * 0.65} ${hump / 2},${amp * 1.15}` return d}
export function composeWordmark({ subW, stdW }) { const gap = Math.round(SNAP.gap * F) const width = MARGIN * 2 + subW + gap + stdW const stdX = MARGIN + subW + gap const subY = BASELINE + Math.round(SNAP.sub.drop * F) const stdY = BASELINE + Math.round(SNAP.std.drop * F) // Each piece hinges at its edge of the break, at mid-letter height (which // is what the brand board's CSS transform-origin used). const hingeY = BASELINE - Math.round(0.35 * F) const text = (body, x, y, transform) => `<text x="${x}" y="${y}" font-size="${F}" font-weight="700"` + ` font-family="system-ui, sans-serif" letter-spacing="-0.01em" fill="#26282b"` + (transform ? ` transform="${transform}"` : '') + `>${body}</text>` const ul = underline( MARGIN, BASELINE + Math.round(0.33 * F), subW + gap + stdW, Math.round(0.42 * F), Math.round(0.13 * F), ) return ( `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${HEIGHT}">` + `<!-- generated by scripts/render-wordmark.mjs - edit the script, not this file -->` + text('sub', MARGIN, subY, `rotate(${SNAP.sub.tilt} ${MARGIN + subW} ${hingeY})`) + text('standard', stdX, stdY, `rotate(${SNAP.std.tilt} ${stdX} ${hingeY})`) + `<path d="${ul}" fill="none" stroke="#d9482b" stroke-width="11"` + ` stroke-linecap="round"/>` + `</svg>` )}
// PNG widths, like the icon sizes; heights follow the viewBox aspect.const PNG_WIDTHS = [320, 640, 1280]
// The wordmark is ink lines on a transparent ground; a healthy render is// well above this, a blank or partial one far below.const MIN_OPAQUE_FRACTION = 0.04
function main() { const chrome = findChrome() const work = mkdtempSync(join(tmpdir(), 'substandard-wordmark-')) try { const metrics = measureWords(chrome, work) console.log(`measured: sub ${metrics.subW}, standard ${metrics.stdW}`) const svg = composeWordmark(metrics) writeFileSync(resolve('brand/wordmark.svg'), svg + '\n') console.log(`wordmark.svg: ${svg.length + 1} bytes -> brand/wordmark.svg`) const vbWidth = Number(svg.match(/viewBox="0 0 (\d+)/)[1])
for (const w of PNG_WIDTHS) { const h = Math.round((w * HEIGHT) / vbWidth) const sized = svg.replace('<svg ', `<svg width="${w}" height="${h}" `) const { opaque, png } = rasterize(chrome, work, `wm${w}`, sized, w, h) const dim = pngSize(png) if (dim.width !== w || dim.height !== h) { throw new Error(`wordmark-${w}: rendered ${dim.width}x${dim.height}`) } const fraction = opaque / (w * h) if (fraction < MIN_OPAQUE_FRACTION) { throw new Error( `wordmark-${w}: only ${(fraction * 100).toFixed(1)}% opaque — misrender`, ) } const dest = `brand/wordmark/wordmark-${w}.png` writeFileSync(resolve(dest), png) console.log(`wordmark-${w}: ${png.length} bytes, ${(fraction * 100).toFixed(0)}% opaque -> ${dest}`) } } finally { rmSync(work, { recursive: true, force: true }) }}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { main()}