// 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 = 128 const FONT = `700 ${F}px system-ui, sans-serif` const MARGIN = 14 const BASELINE = 138 const 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, ``, ) 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) => `${body}` const ul = underline( MARGIN, BASELINE + Math.round(0.33 * F), subW + gap + stdW, Math.round(0.42 * F), Math.round(0.13 * F), ) return ( `` + `` + text('sub', MARGIN, subY, `rotate(${SNAP.sub.tilt} ${MARGIN + subW} ${hingeY})`) + text('standard', stdX, stdY, `rotate(${SNAP.std.tilt} ${stdX} ${hingeY})`) + `` + `` ) } // 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(' ${dest}`) } } finally { rmSync(work, { recursive: true, force: true }) } } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { main() }