Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 723d785bb8c91d7a287d73bcbbc4ba3302487ed4
parent 318e0c46e54b014a745186ff7d61b270d4da2a15
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 26 Sep 2026 02:26:29 -0400

common: brandMedia — the Archilyzer Media lockup, banner and mark M2 as pure SVG; the lockup's letters are outlines generated by bin/gen-media-glyphs.py (fontTools instancer, Archivo at wdth 118), MEDIA at 1.035x spread to the wordmark's measured 5.334 em, the banner's field a port of the canvas generator (CPython's Random(11), pinned line for line)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Acommon/bin/gen-media-glyphs.py | 241+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/lib/brandMedia.test.ts | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/lib/brandMedia.ts | 479+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/lib/brandMediaGlyphs.ts | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 968 insertions(+), 0 deletions(-)

diff --git a/common/bin/gen-media-glyphs.py b/common/bin/gen-media-glyphs.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +"""Generate common/lib/brandMediaGlyphs.ts: the Archilyzer Media lockup's letters as outlines. + +The lockup (common/lib/brandMedia.ts mediaLockupSvg) is the `Archi|lyzer` wordmark over +`MEDIA`, all Archivo at wdth 118. It is drawn as PATHS, not <text>, so anything that +rasterises it -- rsvg-convert, satori's <img>, ImageMagick -- needs no font installed and +cannot substitute one. This script is where those paths come from. + +For each run (`Archi` at wght 720, `lyzer` at 380, `MEDIA` at 700) it pins the variable +font to that instance with the fontTools instancer and records, per glyph, the advance and +the outline, plus the GPOS pair kerning between neighbours inside the run. (A browser does +not kern ACROSS two spans of different weight -- they are two shaping runs -- so there is +no kerning between `Archi` and `lyzer`, and none is recorded.) + +Coordinates are font units (the em is `unitsPerEm`), with y pointing DOWN and the baseline +at 0, which is what an SVG `translate(x baseline) scale(px / unitsPerEm)` wants. + +The font is the copy vendored for the umtool report-to-video preset, +umtool/report-to-video/fonts/Archivo[wdth,wght].ttf, so the outlines and the Pango-rendered +titles beside them come from the same bytes. `--font` takes another path; `--download` +fetches google/fonts' copy instead (into a temp dir, not the repo). + + python3 common/bin/gen-media-glyphs.py # rewrite common/lib/brandMediaGlyphs.ts + python3 common/bin/gen-media-glyphs.py --stdout # print it (the parity test diffs this) + +Needs fontTools (>= 4.40, for the instancer's partial-instance API). +""" +import argparse +import hashlib +import os +import sys +import tempfile +import urllib.request + +from fontTools.pens.svgPathPen import SVGPathPen +from fontTools.pens.transformPen import TransformPen +from fontTools.ttLib import TTFont +from fontTools.varLib import instancer + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO = os.path.normpath(os.path.join(HERE, "..", "..")) +VENDORED = os.path.join(REPO, "umtool", "report-to-video", "fonts", "Archivo[wdth,wght].ttf") +OUT = os.path.join(REPO, "common", "lib", "brandMediaGlyphs.ts") +FONT_URL = "https://raw.githubusercontent.com/google/fonts/main/ofl/archivo/Archivo%5Bwdth%2Cwght%5D.ttf" + +WDTH = 118 +# key, text, weight -- the three runs of plans/brand-and-themes.md "S4": the wordmark's +# lead and suffix, and MEDIA. +RUNS = [ + ("archi", "Archi", 720), + ("lyzer", "lyzer", 380), + ("media", "MEDIA", 700), +] + + +def num(v): + """Font units to one decimal, without a trailing .0 -- stable text for the diff.""" + r = round(float(v), 1) + if r == int(r): + return str(int(r)) + return f"{r:.1f}" + + +def glyph_path(glyphset, name): + """The outline as SVG path data, y flipped so the baseline is 0 and down is +y.""" + pen = SVGPathPen(glyphset, ntos=num) + glyphset[name].draw(TransformPen(pen, (1, 0, 0, -1, 0, 0))) + return pen.getCommands() + + +def _value_x(v): + return (getattr(v, "XAdvance", 0) or 0) if v is not None else 0 + + +def _pair_subtables(font): + """Every PairPos subtable of the GPOS 'kern' feature, in lookup order, unwrapped + from Extension (type 9) lookups.""" + if "GPOS" not in font: + return [] + gpos = font["GPOS"].table + if not gpos.FeatureList or not gpos.LookupList: + return [] + indices = set() + for rec in gpos.FeatureList.FeatureRecord: + if rec.FeatureTag == "kern": + indices.update(rec.Feature.LookupListIndex) + subs = [] + for li in sorted(indices): + lookup = gpos.LookupList.Lookup[li] + for sub in lookup.SubTable: + kind = lookup.LookupType + if kind == 9: + kind = sub.ExtensionLookupType + sub = sub.ExtSubTable + if kind == 2: + subs.append(sub) + return subs + + +def pair_kerning(font, left, right): + """The XAdvance adjustment GPOS 'kern' applies to `left` followed by `right`. + + Resolved the way a shaper resolves one pair: the first subtable that covers + `left` and answers for `right` wins. A format-1 subtable with no record for the + pair passes; a format-2 (class) subtable that covers `left` always answers, + zero included. + """ + for sub in _pair_subtables(font): + cov = sub.Coverage.glyphs + if left not in cov: + continue + if sub.Format == 1: + for rec in sub.PairSet[cov.index(left)].PairValueRecord: + if rec.SecondGlyph == right: + return _value_x(rec.Value1) + elif sub.Format == 2: + c1 = sub.ClassDef1.classDefs.get(left, 0) + c2 = sub.ClassDef2.classDefs.get(right, 0) + return _value_x(sub.Class1Record[c1].Class2Record[c2].Value1) + return 0 + + +def run_data(varfont_path, text, wght): + font = TTFont(varfont_path) + inst = instancer.instantiateVariableFont(font, {"wght": wght, "wdth": WDTH}) + cmap = inst.getBestCmap() + hmtx = inst["hmtx"] + glyphset = inst.getGlyphSet() + glyphs = [] + for ch in text: + name = cmap[ord(ch)] + g = inst["glyf"][name] + g.recalcBounds(inst["glyf"]) # the instancer moves points; the stored bbox is the default master's + # Ink extent: `left` / `right` from the glyph origin, and y down, so `top` is + # how far it rises above the baseline (negative) and `bottom` how far it + # descends (positive for a descender). + top, bottom = (-g.yMax, -g.yMin) if g.numberOfContours else (0, 0) + left, right = (g.xMin, g.xMax) if g.numberOfContours else (0, 0) + glyphs.append({"char": ch, "name": name, "advance": hmtx[name][0], "left": left, "right": right, + "top": top, "bottom": bottom, "d": glyph_path(glyphset, name)}) + kerning = [pair_kerning(inst, a["name"], b["name"]) for a, b in zip(glyphs, glyphs[1:])] + os2 = inst["OS/2"] + return { + "glyphs": glyphs, + "kerning": kerning, + "capHeight": os2.sCapHeight, + "upem": inst["head"].unitsPerEm, + } + + +def render_ts(font_path): + sha = hashlib.sha256(open(font_path, "rb").read()).hexdigest() + runs = {key: run_data(font_path, text, wght) for key, text, wght in RUNS} + upem = {r["upem"] for r in runs.values()} + caps = {r["capHeight"] for r in runs.values()} + assert len(upem) == 1, f"unitsPerEm differs across instances: {upem}" + assert len(caps) == 1, f"cap height differs across weights: {caps}" + out = [ + "// GENERATED by common/bin/gen-media-glyphs.py -- DO NOT EDIT. Re-run it instead:", + "// python3 common/bin/gen-media-glyphs.py", + "// brandMedia.test.ts diffs this file against a fresh run.", + "//", + "// The Archilyzer Media lockup's letters as outlines: Archivo[wdth,wght].ttf pinned to", + f"// wdth {WDTH}, one instance per run. Font units, y down, baseline at 0; `kerning[i]` is", + "// the GPOS pair adjustment between glyph i and glyph i + 1 of the same run.", + "", + "// `left` / `right` / `top` / `bottom`: the glyph's ink box from its origin, y down", + "// (`top` < 0 is above the baseline).", + "export type MediaGlyph = {", + " char: string;", + " advance: number;", + " left: number;", + " right: number;", + " top: number;", + " bottom: number;", + " d: string;", + "};", + "export type MediaRun = {", + " text: string;", + " wght: number;", + " glyphs: ReadonlyArray<MediaGlyph>;", + " kerning: ReadonlyArray<number>;", + "};", + "", + "export const MEDIA_GLYPH_SOURCE = {", + ' font: "Archivo[wdth,wght].ttf",', + f' sha256: "{sha}",', + f" wdth: {WDTH},", + "} as const;", + "", + f"export const MEDIA_UNITS_PER_EM = {upem.pop()};", + f"export const MEDIA_CAP_HEIGHT = {caps.pop()};", + "", + "export const MEDIA_RUNS = {", + ] + for key, text, wght in RUNS: + r = runs[key] + out.append(f" {key}: {{") + out.append(f' text: "{text}",') + out.append(f" wght: {wght},") + out.append(" glyphs: [") + for g in r["glyphs"]: + out.append( + f' {{ char: "{g["char"]}", advance: {g["advance"]}, left: {g["left"]}, right: {g["right"]}, ' + f'top: {g["top"]}, bottom: {g["bottom"]},' + ) + out.append(f' d: "{g["d"]}" }},') + out.append(" ],") + out.append(f" kerning: [{', '.join(str(k) for k in r['kerning'])}],") + out.append(" },") + out.append("} as const satisfies Record<string, MediaRun>;") + out.append("") + return "\n".join(out) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + ap.add_argument("--font", help="Archivo[wdth,wght].ttf to read (default: the umtool vendored copy)") + ap.add_argument("--download", action="store_true", help="fetch google/fonts' copy into a temp dir") + ap.add_argument("--stdout", action="store_true", help="print instead of writing " + os.path.relpath(OUT, REPO)) + args = ap.parse_args() + + font = args.font or VENDORED + if args.download: + tmp = tempfile.mkdtemp(prefix="archivo-") + font = os.path.join(tmp, "Archivo[wdth,wght].ttf") + urllib.request.urlretrieve(FONT_URL, font) + if not os.path.exists(font): + sys.exit(f"no font at {font} -- pass --font <path> or --download") + + ts = render_ts(font) + if args.stdout: + sys.stdout.write(ts) + else: + with open(OUT, "w", encoding="utf-8") as f: + f.write(ts) + print(f"wrote {os.path.relpath(OUT, REPO)}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/common/lib/brandMedia.test.ts b/common/lib/brandMedia.test.ts @@ -0,0 +1,160 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { ACCENTS, ICON_PALETTES, markSvg } from "./brand"; +import { + MEDIA_BANNER, + MEDIA_MARK_PALETTE, + MEDIA_PALETTE, + MEDIA_RATIO, + PyRandom, + mediaBannerField, + mediaBannerLayout, + mediaBannerSvg, + mediaLockupGeometry, + mediaLockupSvg, + mediaMarkSvg, + mediaWordAdvanceEm, + mediaWordmarkAdvanceEm, +} from "./brandMedia"; +import { MEDIA_CAP_HEIGHT, MEDIA_GLYPH_SOURCE, MEDIA_RUNS } from "./brandMediaGlyphs"; + +const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); + +test("palette: mark M2 is the parent mark's slate, lit in Signal", () => { + assert.deepEqual(MEDIA_MARK_PALETTE, { + ground: ICON_PALETTES.archilyzer.ground, + dim: ICON_PALETTES.archilyzer.dim, + lit: ACCENTS.signal.onDark, + }); + assert.deepEqual(MEDIA_PALETTE, { + ground: "#151b20", + dim: "#3f4c56", + lit: "#5fa8a0", + fg: "#e7edf1", + muted: "#8496a2", + media: "#5fa8a0", + amber: "#e3b15c", + }); + assert.equal(MEDIA_RATIO, 1.035); + // The mark is markSvg's, not a second drawing. + assert.equal(mediaMarkSvg("maskable"), markSvg(MEDIA_MARK_PALETTE, { variant: "maskable" })); + assert.equal(mediaMarkSvg(), markSvg(MEDIA_MARK_PALETTE)); +}); + +test("glyphs: the measured widths of plans/brand-and-themes.md S4 (5.334 em, 3.953 em, caps 0.686 em)", () => { + assert.equal(MEDIA_GLYPH_SOURCE.wdth, 118); + assert.deepEqual( + [MEDIA_RUNS.archi, MEDIA_RUNS.lyzer, MEDIA_RUNS.media].map((r) => [r.text, r.wght]), + [["Archi", 720], ["lyzer", 380], ["MEDIA", 700]], + ); + assert.equal(Number(mediaWordmarkAdvanceEm().toFixed(3)), 5.334); + assert.equal(Number(mediaWordAdvanceEm().toFixed(3)), 3.953); + assert.equal(MEDIA_CAP_HEIGHT, 686); +}); + +test("lockup: MEDIA spans the wordmark's advance (within 0.5 %) with 0.30 em between its letters", () => { + for (const px of [18, 28, 64, 124]) { + const g = mediaLockupGeometry(px); + assert.ok( + Math.abs(g.mediaAdvance - g.wordmarkAdvance) / g.wordmarkAdvance < 0.005, + `px ${px}: MEDIA ${g.mediaAdvance} vs wordmark ${g.wordmarkAdvance}`, + ); + assert.ok(Math.abs(g.mediaAdvance - g.textWidth) < 1e-6); + assert.ok(Math.abs(g.mediaSpacing / g.mediaPx - 0.3) < 0.005, `spacing ${g.mediaSpacing / g.mediaPx} em`); + assert.ok(Math.abs(g.mediaPx - px * MEDIA_RATIO) < 1e-9); + // The mark is as tall as the block, which is about 1.70 x the wordmark. + assert.equal(g.markSize, g.blockHeight); + assert.ok(Math.abs(g.blockHeight / px - 1.7) < 0.01, `block ${g.blockHeight / px} x`); + // The ink of both lines starts at the text origin and ends within a hair of + // the shared advance: flush left and right. + const lines = [g.glyphs.filter((p) => p.tone !== "media"), g.glyphs.filter((p) => p.tone === "media")]; + for (const line of lines) { + const first = line[0]; + const last = line[line.length - 1]; + assert.ok(first.glyph.left * first.scale < 0.1 * px, "left bearing"); + const right = last.x + last.glyph.right * last.scale; + assert.ok(Math.abs(g.textWidth - right) < 0.08 * px, `right edge ${right} vs ${g.textWidth}`); + } + } +}); + +test("lockup on the banner: the approved numbers, inside the safe area", () => { + const { geometry: g, block, ink } = mediaBannerLayout(); + assert.equal(MEDIA_BANNER.wordmarkPx, 124); + assert.equal(Math.round(g.mediaPx), 128); + assert.equal(Math.round(g.textWidth), 661); + assert.equal(Math.round(g.markSize), 210); + const S = MEDIA_BANNER.safe; + for (const box of [block, ink]) { + assert.ok(box.x >= S.x && box.y >= S.y, `${JSON.stringify(box)} starts inside the safe area`); + assert.ok(box.x + box.width <= S.x + S.width, "ends inside horizontally"); + assert.ok(box.y + box.height <= S.y + S.height, "ends inside vertically"); + } + // Centred on it. + assert.ok(Math.abs(block.x + block.width / 2 - (S.x + S.width / 2)) < 1e-6); + assert.ok(Math.abs(block.y + block.height / 2 - (S.y + S.height / 2)) < 1e-6); + // And the field keeps out of the gutter round it. + for (const [x, y, w] of mediaBannerField()) { + const inBandY = y + MEDIA_BANNER.line.height > S.y - MEDIA_BANNER.gutter.y && y < S.y + S.height + MEDIA_BANNER.gutter.y; + if (!inBandY) continue; + const clear = x + w <= S.x - MEDIA_BANNER.gutter.x || x >= S.x + S.width + MEDIA_BANNER.gutter.x; + assert.ok(clear, `line ${x},${y} +${w} crosses the gutter`); + } +}); + +test("PyRandom is CPython's random.Random: the canvas field, line for line", () => { + // python3 -c "import random; r = random.Random(11); print([r.getrandbits(32) for _ in range(5)])" + const r = new PyRandom(11); + assert.deepEqual( + Array.from({ length: 5 }, () => r.genrandUint32()), + [1942955373, 3718334796, 2404204071, 3680198571, 3969454221], + ); + // r = random.Random(11); [r.choice([0, 0, 0, 96, 192]) for _ in range(8)] + const c = new PyRandom(11); + assert.deepEqual( + Array.from({ length: 8 }, () => c.choice([0, 0, 0, 96, 192])), + [96, 192, 96, 96, 192, 192, 0, 0], + ); + // The canvas generator's banner() field (gen_media.py), 87 lines. + const field = mediaBannerField(); + assert.equal(field.length, 87); + assert.deepEqual(field.slice(0, 3), [[160, 40, 300], [488, 40, 380], [896, 40, 380]]); + assert.deepEqual(field.slice(-3), [[1658, 1384, 190], [1876, 1384, 300], [2204, 1384, 172]]); +}); + +test("SVGs: well-formed roots, colours validated, the safe crop is a viewBox", () => { + const lockup = mediaLockupSvg({ px: 28 }); + assert.match(lockup, /^<svg xmlns="http:\/\/www\.w3\.org\/2000\/svg" width="[\d.]+" height="[\d.]+" viewBox="0 0 [\d.]+ [\d.]+">/); + assert.ok(lockup.includes(`fill="${MEDIA_PALETTE.fg}"`) && lockup.includes(`fill="${MEDIA_PALETTE.muted}"`)); + assert.equal((lockup.match(/<path /g) ?? []).length, 15); + assert.doesNotMatch(lockup, /<text/); + assert.throws(() => mediaLockupSvg({ px: 28, palette: { ...MEDIA_PALETTE, fg: "red" } }), /palette\.fg/); + assert.throws(() => mediaLockupGeometry(0), /px must be > 0/); + + const banner = mediaBannerSvg(); + assert.match(banner, /^<svg [^>]*width="2560" height="1440" viewBox="0 0 2560 1440">/); + assert.equal((banner.match(/<rect x="\d+" y="\d+" width="\d+" height="44" rx="22"\/>/g) ?? []).length, 87); + const safe = mediaBannerSvg({ view: MEDIA_BANNER.safe }); + assert.match(safe, /^<svg [^>]*width="1546" height="423" viewBox="507 508.5 1546 423">/); + assert.equal(safe.slice(safe.indexOf(">")), banner.slice(banner.indexOf(">"))); +}); + +// The glyph file is generated; a hand edit or a font swap without a re-run is a +// failure. Needs python3 + fontTools, which a test machine may not have. +test("brandMediaGlyphs.ts is exactly what gen-media-glyphs.py writes", (t) => { + const probe = spawnSync("python3", ["-c", "import fontTools.varLib.instancer"], { stdio: "ignore" }); + if (probe.status !== 0) { + t.skip("python3 with fontTools is not available"); + return; + } + const fresh = execFileSync("python3", [path.join(REPO, "common/bin/gen-media-glyphs.py"), "--stdout"], { + encoding: "utf8", + maxBuffer: 1 << 24, + }); + const committed = readFileSync(path.join(REPO, "common/lib/brandMediaGlyphs.ts"), "utf8"); + assert.equal(committed, fresh); +}); diff --git a/common/lib/brandMedia.ts b/common/lib/brandMedia.ts @@ -0,0 +1,479 @@ +// ARCHILYZER MEDIA — the YouTube channel's brand, as data and pure SVG. +// +// plans/brand-and-themes.md "S4 — Archilyzer Media" is the source of every +// value here, and the approved canvas (row "Archilyzer Media · the YouTube +// channel") is what each drawing reproduces: +// +// - the mark is M2 "Signal": the parent mark's geometry (lib/brand.ts MARK, +// drawn by markSvg — never redrawn here), lit in Signal; +// - the lockup is "flush · caps matched": the `Archi|lyzer` wordmark over +// `MEDIA`, both Archivo at wdth 118, MEDIA at MEDIA_RATIO × the wordmark's +// size and letter-spaced to exactly the wordmark's width, the mark as tall +// as the text block beside it; +// - the banner is a field of dim transcript lines wrapping round a clear +// gutter about YouTube's safe area, the lockup centred in it, and one lit +// found line in the desktop strip to its right. +// +// The letters are OUTLINES from lib/brandMediaGlyphs.ts (generated by +// common/bin/gen-media-glyphs.py from the Archivo the umtool preset vendors), +// so rendering any of this needs no font: rsvg-convert, satori's <img> and +// ImageMagick all draw the same shapes. +// +// PURE: no I/O, no framework imports. The PNG side is common/bin/brand-media.ts. + +import { ACCENTS, MARK_VIEWBOX, markSvg, type IconPalette, type MarkVariant } from "./brand"; +import { + MEDIA_CAP_HEIGHT, + MEDIA_RUNS, + MEDIA_UNITS_PER_EM, + type MediaGlyph, + type MediaRun, +} from "./brandMediaGlyphs"; + +// ── Palette ───────────────────────────────────────────────────────────────── + +// The slate of the parent mark (lib/brand.ts ICON_PALETTES.archilyzer), lit in +// Signal. `media` is MEDIA's ink; `amber` is the annotation colour the video +// preset carries for kickers and markers. +export type MediaPalette = { + ground: string; + dim: string; + lit: string; + fg: string; + muted: string; + media: string; +}; + +export const MEDIA_PALETTE = { + ground: "#151b20", + dim: "#3f4c56", + lit: ACCENTS.signal.onDark, + fg: "#e7edf1", + muted: "#8496a2", + media: ACCENTS.signal.onDark, + amber: "#e3b15c", +} as const satisfies MediaPalette & { amber: string }; + +// Mark M2 · Signal. +export const MEDIA_MARK_PALETTE: IconPalette = { + ground: MEDIA_PALETTE.ground, + dim: MEDIA_PALETTE.dim, + lit: MEDIA_PALETTE.lit, +}; + +// The dim lines of the banner's field: a step above the ground, below the +// mark's own dim, so the field reads as texture and the mark as the object. +export const MEDIA_FIELD_LINE = "#1e262d"; + +// ── The lockup's proportions ──────────────────────────────────────────────── + +// MEDIA's size over the wordmark's. At this ratio MEDIA, spread to the +// wordmark's width, has 0.30 em between its letters. +export const MEDIA_RATIO = 1.035; +// The wordmark's tracking, as the sites set it (common/components/Wordmark.tsx). +// Its measured width — the advances of `Archi` at 720 and `lyzer` at 380, less +// this per letter — is the width both lines share: 5.334 em. +export const MEDIA_WORDMARK_TRACKING_EM = -0.01; +// From the wordmark's baseline to MEDIA's cap top, in wordmark em. +export const MEDIA_LINE_GAP_EM = 0.3; +// From the mark's right edge to the text, in wordmark em. +export const MEDIA_MARK_GAP_EM = 0.46; + +const EM = MEDIA_UNITS_PER_EM; +const HEX_COLOR_RE = /^#[0-9a-f]{6}$/i; + +const sum = (xs: ReadonlyArray<number>): number => xs.reduce((a, b) => a + b, 0); +const advances = (r: MediaRun): number => sum(r.glyphs.map((g) => g.advance)); + +// The wordmark's shared width in em: the spec's measurement, from the glyphs. +export function mediaWordmarkAdvanceEm(): number { + const letters = MEDIA_RUNS.archi.glyphs.length + MEDIA_RUNS.lyzer.glyphs.length; + return (advances(MEDIA_RUNS.archi) + advances(MEDIA_RUNS.lyzer)) / EM + MEDIA_WORDMARK_TRACKING_EM * letters; +} + +// MEDIA's natural advance (no spacing) in em of its own size. +export function mediaWordAdvanceEm(): number { + return (advances(MEDIA_RUNS.media) + sum(MEDIA_RUNS.media.kerning)) / EM; +} + +export type PlacedGlyph = { glyph: MediaGlyph; x: number; baseline: number; scale: number; tone: "fg" | "muted" | "media" }; + +export type MediaLockupGeometry = { + px: number; + // The block: wordmark cap top (y 0) to MEDIA's baseline, mark to text end. + blockWidth: number; + blockHeight: number; + markSize: number; + textX: number; + textWidth: number; + wordmarkBaseline: number; + mediaPx: number; + mediaBaseline: number; + // MEDIA's letter gap, px. + mediaSpacing: number; + // The ink's overshoot above the block (the ascenders of h, i and l rise + // past the cap line), px; the SVG adds it on top so nothing is clipped. + inkTop: number; + glyphs: ReadonlyArray<PlacedGlyph>; + // Where each line's advance ends, px from the text origin. + wordmarkAdvance: number; + mediaAdvance: number; +}; + +// Place the lockup at a wordmark size of `px`. Block coordinates: the mark's +// top-left is (0, 0), which is also the wordmark's cap line. +export function mediaLockupGeometry(px: number): MediaLockupGeometry { + if (!(px > 0) || !Number.isFinite(px)) throw new Error(`mediaLockupGeometry: px must be > 0, got ${px}`); + const s = px / EM; + const mediaPx = px * MEDIA_RATIO; + const sm = mediaPx / EM; + const cap = MEDIA_CAP_HEIGHT / EM; + + const wordmarkBaseline = cap * px; + const mediaBaseline = wordmarkBaseline + MEDIA_LINE_GAP_EM * px + cap * mediaPx; + const blockHeight = mediaBaseline; + const markSize = blockHeight; + const textX = markSize + MEDIA_MARK_GAP_EM * px; + const width = mediaWordmarkAdvanceEm() * px; + + const glyphs: PlacedGlyph[] = []; + + // The wordmark: kerned inside each run (a browser kerns inside a span, not + // across two spans of different weight), then one uniform adjustment per + // gap brings the whole line to the measured width. That is the canvas's + // `textLength` + `lengthAdjust="spacing"`. + const word: Array<{ glyph: MediaGlyph; tone: "fg" | "muted"; kernAfter: number }> = []; + for (const [run, tone] of [ + [MEDIA_RUNS.archi, "fg"], + [MEDIA_RUNS.lyzer, "muted"], + ] as const) { + run.glyphs.forEach((glyph, i) => word.push({ glyph, tone, kernAfter: run.kerning[i] ?? 0 })); + } + const natural = sum(word.map((w, i) => w.glyph.advance + (i < word.length - 1 ? w.kernAfter : 0))) * s; + const perGap = (width - natural) / (word.length - 1); + let x = 0; + word.forEach((w, i) => { + glyphs.push({ glyph: w.glyph, x, baseline: wordmarkBaseline, scale: s, tone: w.tone }); + x += w.glyph.advance * s + (i < word.length - 1 ? w.kernAfter * s + perGap : 0); + }); + const wordmarkAdvance = x; + + // MEDIA: its own advances (and kerning, if the font has any for it) at + // MEDIA_RATIO × px, the rest of the width shared equally between its gaps. + const media = MEDIA_RUNS.media; + const mediaSpacing = (width - mediaWordAdvanceEm() * mediaPx) / (media.glyphs.length - 1); + x = 0; + media.glyphs.forEach((glyph, i) => { + glyphs.push({ glyph, x, baseline: mediaBaseline, scale: sm, tone: "media" }); + x += glyph.advance * sm + (i < media.glyphs.length - 1 ? (media.kerning[i] ?? 0) * sm + mediaSpacing : 0); + }); + const mediaAdvance = x; + + const inkTop = Math.max(0, -Math.min(...glyphs.map((g) => g.baseline + g.glyph.top * g.scale))); + + return { + px, + blockWidth: textX + width, + blockHeight, + markSize, + textX, + textWidth: width, + wordmarkBaseline, + mediaPx, + mediaBaseline, + mediaSpacing, + inkTop, + glyphs, + wordmarkAdvance, + mediaAdvance, + }; +} + +const fmt = (n: number): string => String(Number(n.toFixed(3))); + +function checkPalette(fn: string, palette: Record<string, string>): void { + for (const [slot, colour] of Object.entries(palette)) { + if (!HEX_COLOR_RE.test(colour)) { + throw new Error(`${fn}: palette.${slot} must be #rrggbb, got ${JSON.stringify(colour)}`); + } + } +} + +// markSvg's document, re-rooted as a nested <svg> at (x, y) and `size` square. +function nestedMark(palette: IconPalette, x: number, y: number, size: number, variant: MarkVariant = "any"): string { + const doc = markSvg(palette, { variant }); + const open = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${MARK_VIEWBOX} ${MARK_VIEWBOX}">`; + if (!doc.startsWith(open)) throw new Error("nestedMark: markSvg's root changed shape"); + return ( + `<svg x="${fmt(x)}" y="${fmt(y)}" width="${fmt(size)}" height="${fmt(size)}" viewBox="0 0 ${MARK_VIEWBOX} ${MARK_VIEWBOX}">` + + doc.slice(open.length) + ); +} + +// The lockup's shapes, positioned with its block's top-left at (x, y). +function lockupBody(g: MediaLockupGeometry, palette: MediaPalette, x: number, y: number): string { + const mark = nestedMark({ ground: palette.ground, dim: palette.dim, lit: palette.lit }, x, y, g.markSize); + const byTone = new Map<string, string[]>(); + for (const p of g.glyphs) { + const fill = palette[p.tone].toLowerCase(); + const list = byTone.get(fill) ?? []; + list.push( + `<path transform="translate(${fmt(x + g.textX + p.x)} ${fmt(y + p.baseline)}) scale(${fmt(p.scale)})" d="${p.glyph.d}"/>`, + ); + byTone.set(fill, list); + } + const text = [...byTone].map(([fill, paths]) => `<g fill="${fill}">${paths.join("")}</g>`).join(""); + return mark + text; +} + +// The lockup as a standalone SVG document at a wordmark size of `px`. Its +// box is the block plus the ascenders' overshoot on top; `mediaLockupGeometry` +// says where the block sits in it (y = inkTop). +export function mediaLockupSvg({ px, palette = MEDIA_PALETTE }: { px: number; palette?: MediaPalette }): string { + checkPalette("mediaLockupSvg", palette); + const g = mediaLockupGeometry(px); + const w = fmt(g.blockWidth); + const h = fmt(g.blockHeight + g.inkTop); + return ( + `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">` + + lockupBody(g, palette, 0, g.inkTop) + + `</svg>` + ); +} + +// ── The video kit ─────────────────────────────────────────────────────────── + +// What umtool's report-to-video preset needs from here, as JSON: its scripts +// are plain `node` .mjs and cannot import TypeScript, so common/bin/ +// brand-media.ts --video-kit writes this to +// umtool/report-to-video/brands/archilyzer-media.json and brandMedia.test.ts +// holds the committed file to it. The lockup is drawn at `px` and scaled by +// the renderer; `blockTop` is where its design box starts inside the SVG. +export const MEDIA_VIDEO_KIT_PX = 100; + +export function mediaVideoKit() { + const g = mediaLockupGeometry(MEDIA_VIDEO_KIT_PX); + return { + $comment: + "GENERATED by common/bin/brand-media.ts --video-kit from common/lib/brandMedia.ts -- do not edit; re-run it.", + id: "archilyzer-media", + palette: { + bg: MEDIA_PALETTE.ground, + fg: MEDIA_PALETTE.fg, + muted: MEDIA_PALETTE.muted, + accent: MEDIA_PALETTE.lit, + amber: MEDIA_PALETTE.amber, + dim: MEDIA_PALETTE.dim, + }, + mark: { any: mediaMarkSvg("any") }, + lockup: { + px: MEDIA_VIDEO_KIT_PX, + width: Number(fmt(g.blockWidth)), + height: Number(fmt(g.blockHeight + g.inkTop)), + blockTop: Number(fmt(g.inkTop)), + blockHeight: Number(fmt(g.blockHeight)), + svg: mediaLockupSvg({ px: MEDIA_VIDEO_KIT_PX }), + }, + }; +} + +// ── The banner ────────────────────────────────────────────────────────────── + +// YouTube's channel banner: 2560 × 1440, of which every device shows the +// centred 1546 × 423 (the "safe area"); a desktop shows the full-width strip +// through it, a TV the whole image. +export const MEDIA_BANNER = { + width: 2560, + height: 1440, + safe: { x: 507, y: 508.5, width: 1546, height: 423 }, + // The lockup's wordmark size on the banner: MEDIA comes out at 128 px, the + // text at 661 px wide, the mark 210 px — the approved canvas's numbers. + wordmarkPx: 124, + // The field's lines stay out of a gutter this wide round the safe area. + gutter: { x: 48, y: 36 }, + line: { height: 44, pitch: 84, first: 40, last: 1400, left: 64, right: 2496, gap: 28, minWidth: 60 }, + // The one lit line: a play head and a bar, right of the safe area. + found: { y: 712, head: [[2150, 710], [2210, 734], [2150, 758]] as ReadonlyArray<readonly [number, number]>, x: 2226, width: 236 }, + seed: 11, +} as const; + +// CPython's `random.Random(seed).choice`, exactly: MT19937 seeded by +// init_by_array, and `_randbelow` by rejection over `getrandbits(k)`. The +// canvas generator drew the field with Random(11); porting the generator +// rather than transcribing its output keeps the banner a function, not a +// table, and the test pins the two against each other. +export class PyRandom { + private readonly mt = new Uint32Array(624); + private mti = 625; + + constructor(seed: number) { + if (!Number.isInteger(seed) || seed < 0 || seed > 0xffffffff) { + throw new Error(`PyRandom: seed must be a 32-bit non-negative integer, got ${seed}`); + } + this.initByArray([seed]); + } + + private initGenrand(s: number): void { + const mt = this.mt; + mt[0] = s >>> 0; + for (let i = 1; i < 624; i++) { + mt[i] = (Math.imul(1812433253, mt[i - 1] ^ (mt[i - 1] >>> 30)) + i) >>> 0; + } + this.mti = 624; + } + + private initByArray(key: ReadonlyArray<number>): void { + const mt = this.mt; + this.initGenrand(19650218); + let i = 1; + let j = 0; + for (let k = Math.max(624, key.length); k > 0; k--) { + mt[i] = ((mt[i] ^ Math.imul(mt[i - 1] ^ (mt[i - 1] >>> 30), 1664525)) + key[j] + j) >>> 0; + i++; + j++; + if (i >= 624) { + mt[0] = mt[623]; + i = 1; + } + if (j >= key.length) j = 0; + } + for (let k = 623; k > 0; k--) { + mt[i] = ((mt[i] ^ Math.imul(mt[i - 1] ^ (mt[i - 1] >>> 30), 1566083941)) - i) >>> 0; + i++; + if (i >= 624) { + mt[0] = mt[623]; + i = 1; + } + } + mt[0] = 0x80000000; + } + + genrandUint32(): number { + const mt = this.mt; + if (this.mti >= 624) { + for (let kk = 0; kk < 624; kk++) { + const y = (mt[kk] & 0x80000000) | (mt[(kk + 1) % 624] & 0x7fffffff); + mt[kk] = mt[(kk + 397) % 624] ^ (y >>> 1) ^ (y & 1 ? 0x9908b0df : 0); + } + this.mti = 0; + } + let y = mt[this.mti++]; + y ^= y >>> 11; + y ^= (y << 7) & 0x9d2c5680; + y ^= (y << 15) & 0xefc60000; + y ^= y >>> 18; + return y >>> 0; + } + + getrandbits(k: number): number { + if (!Number.isInteger(k) || k < 0 || k > 32) throw new Error(`getrandbits: k must be 0..32, got ${k}`); + return k === 0 ? 0 : this.genrandUint32() >>> (32 - k); + } + + randbelow(n: number): number { + const k = Math.floor(Math.log2(n)) + 1; // n.bit_length() + let r = this.getrandbits(k); + while (r >= n) r = this.getrandbits(k); + return r; + } + + choice<T>(seq: ReadonlyArray<T>): T { + return seq[this.randbelow(seq.length)]; + } +} + +// The field of dim lines: [x, y, width] per line, in the canvas generator's +// order. +export function mediaBannerField(): Array<[number, number, number]> { + const B = MEDIA_BANNER; + const rnd = new PyRandom(B.seed); + const zx0 = B.safe.x - B.gutter.x; + const zy0 = B.safe.y - B.gutter.y; + const zx1 = B.safe.x + B.safe.width + B.gutter.x; + const zy1 = B.safe.y + B.safe.height + B.gutter.y; + const L = B.line; + const out: Array<[number, number, number]> = []; + for (let y: number = L.first; y < L.last; y += L.pitch) { + const inBand = y + L.height > zy0 && y < zy1; + let x = L.left + rnd.choice([0, 0, 0, 96, 192]); + let end = L.right - rnd.choice([0, 120, 360, 640]); + // The found line's row: the field stops short of the gutter, and the lit + // line is drawn in the desktop strip instead. + if (y === B.found.y) end = Math.min(end, zx0); + while (x < end) { + let bw = Math.min(rnd.choice([140, 190, 240, 300, 380, 460]), end - x); + if (inBand && x < zx1 && x + bw > zx0) { + bw = zx0 - x; + if (bw >= L.minWidth) out.push([x, y, bw]); + x = zx1; + continue; + } + if (bw >= L.minWidth) out.push([x, y, bw]); + x += bw + L.gap; + } + } + return out; +} + +// Where the lockup lands on the banner: centred on the safe area. `block` is +// the lockup's design box (mark top to MEDIA baseline); `ink` adds the +// ascenders. +export function mediaBannerLayout(): { + geometry: MediaLockupGeometry; + x: number; + y: number; + block: { x: number; y: number; width: number; height: number }; + ink: { x: number; y: number; width: number; height: number }; +} { + const B = MEDIA_BANNER; + const g = mediaLockupGeometry(B.wordmarkPx); + const cx = B.safe.x + B.safe.width / 2; + const cy = B.safe.y + B.safe.height / 2; + const x = cx - g.blockWidth / 2; + const y = cy - g.blockHeight / 2; + return { + geometry: g, + x, + y, + block: { x, y, width: g.blockWidth, height: g.blockHeight }, + ink: { x, y: y - g.inkTop, width: g.blockWidth, height: g.blockHeight + g.inkTop }, + }; +} + +// The banner, 2560 × 1440. `view` crops it (the safe-area PNG is the same +// drawing with a viewBox of the safe rectangle). +export function mediaBannerSvg( + opts: { palette?: MediaPalette; view?: { x: number; y: number; width: number; height: number } } = {}, +): string { + const palette = opts.palette ?? MEDIA_PALETTE; + checkPalette("mediaBannerSvg", palette); + const B = MEDIA_BANNER; + const view = opts.view ?? { x: 0, y: 0, width: B.width, height: B.height }; + const L = B.line; + const lines = mediaBannerField() + .map(([x, y, w]) => `<rect x="${x}" y="${y}" width="${w}" height="${L.height}" rx="${L.height / 2}"/>`) + .join(""); + const lit = palette.lit.toLowerCase(); + const found = + `<polygon points="${B.found.head.map(([x, y]) => `${x},${y}`).join(" ")}" fill="${lit}"/>` + + `<rect x="${B.found.x}" y="${B.found.y}" width="${B.found.width}" height="${L.height}" rx="${L.height / 2}" fill="${lit}"/>`; + const { geometry, x, y } = mediaBannerLayout(); + return ( + `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" ` + + `viewBox="${view.x} ${view.y} ${view.width} ${view.height}">` + + `<rect width="${B.width}" height="${B.height}" fill="${palette.ground.toLowerCase()}"/>` + + `<g fill="${MEDIA_FIELD_LINE}">${lines}</g>` + + found + + lockupBody(geometry, palette, x, y) + + `</svg>` + ); +} + +// ── The mark on its own ───────────────────────────────────────────────────── + +// Mark M2 as a standalone SVG: `maskable` is the avatar (YouTube crops it to +// a circle, and the 0.8 scale keeps the lines inside it), `any` the Studio +// watermark (rounded square, transparent corners). +export function mediaMarkSvg(variant: MarkVariant = "any"): string { + return markSvg(MEDIA_MARK_PALETTE, { variant }); +} diff --git a/common/lib/brandMediaGlyphs.ts b/common/lib/brandMediaGlyphs.ts @@ -0,0 +1,88 @@ +// GENERATED by common/bin/gen-media-glyphs.py -- DO NOT EDIT. Re-run it instead: +// python3 common/bin/gen-media-glyphs.py +// brandMedia.test.ts diffs this file against a fresh run. +// +// The Archilyzer Media lockup's letters as outlines: Archivo[wdth,wght].ttf pinned to +// wdth 118, one instance per run. Font units, y down, baseline at 0; `kerning[i]` is +// the GPOS pair adjustment between glyph i and glyph i + 1 of the same run. + +// `left` / `right` / `top` / `bottom`: the glyph's ink box from its origin, y down +// (`top` < 0 is above the baseline). +export type MediaGlyph = { + char: string; + advance: number; + left: number; + right: number; + top: number; + bottom: number; + d: string; +}; +export type MediaRun = { + text: string; + wght: number; + glyphs: ReadonlyArray<MediaGlyph>; + kerning: ReadonlyArray<number>; +}; + +export const MEDIA_GLYPH_SOURCE = { + font: "Archivo[wdth,wght].ttf", + sha256: "0e094a7d3c7c4c25cf1310c4b30014f1dae9332220b1c2c88f4fa996f0b05053", + wdth: 118, +} as const; + +export const MEDIA_UNITS_PER_EM = 1000; +export const MEDIA_CAP_HEIGHT = 686; + +export const MEDIA_RUNS = { + archi: { + text: "Archi", + wght: 720, + glyphs: [ + { char: "A", advance: 867, left: 16, right: 851, top: -687, bottom: 0, + d: "M15.8 0 334.8 -687.1H532.2L851.4 0H664.3L609.1 -124.2H248.1L193.1 0ZM306.3 -256.8H550.8L483.3 -413.3Q479 -423.5 471.5 -441.8Q464.1 -460.2 456.4 -480.5Q448.6 -500.8 442.1 -518Q435.5 -535.3 432.6 -542.8H425.4Q418 -523.5 408.2 -498.6Q398.4 -473.6 389.3 -450.4Q380.2 -427.2 373.8 -412.8Z" }, + { char: "r", advance: 439, left: 67, right: 428, top: -539, bottom: 0, + d: "M66.7 0V-527.1H194.4L205.7 -438.5H213.6Q226.2 -467.5 247.1 -490.4Q268 -513.2 297.3 -526.3Q326.6 -539.4 362.6 -539.4Q381.2 -539.4 397.9 -536.5Q414.7 -533.7 427.6 -528.8V-396.2H358.5Q322.8 -396.2 297.3 -384.9Q271.8 -373.6 255.6 -353.5Q239.4 -333.3 231.6 -307.2Q223.8 -281.2 223.8 -251.1V0Z" }, + { char: "c", advance: 702, left: 45, right: 657, top: -539, bottom: 12, + d: "M359.3 12Q260.8 12 190.3 -18.3Q119.7 -48.6 82.2 -109.9Q44.6 -171.2 44.6 -263.8Q44.6 -356.7 82.3 -417.7Q119.9 -478.8 190.5 -509Q261 -539.2 359.3 -539.2Q422.3 -539.2 476.3 -525.7Q530.4 -512.2 571.1 -484.8Q611.8 -457.3 634.2 -416.5Q656.5 -375.7 656.5 -321.2H498.4Q498.4 -354.6 480.2 -377.6Q462 -400.6 430.6 -412.6Q399.2 -424.6 358.9 -424.6Q309 -424.6 274.9 -406.9Q240.9 -389.3 223.6 -355.8Q206.4 -322.4 206.4 -274.4V-252.9Q206.4 -205.6 224 -171.9Q241.7 -138.1 276.9 -120.3Q312.2 -102.5 364.6 -102.5Q404.4 -102.5 435.9 -115.3Q467.4 -128.1 486 -151.7Q504.6 -175.3 504.6 -206.7H656.5Q656.5 -151.9 633.9 -110.7Q611.3 -69.5 571 -42.3Q530.8 -15.1 476.5 -1.6Q422.3 12 359.3 12Z" }, + { char: "h", advance: 700, left: 67, right: 637, top: -724, bottom: 0, + d: "M66.7 0V-724.1H223.8V-460.6H231Q255.7 -488.9 287 -506Q318.3 -523.2 353.5 -531.1Q388.8 -539.1 424.4 -539.1Q493.7 -539.1 540.9 -516.5Q588.1 -494 612.4 -447.8Q636.7 -401.7 636.7 -330.2V0H479.4V-306Q479.4 -334.8 471.6 -355.5Q463.8 -376.2 449.2 -389.2Q434.6 -402.2 413.6 -408.2Q392.5 -414.3 366 -414.3Q327.3 -414.3 295.1 -397.9Q262.8 -381.6 243.3 -352.7Q223.8 -323.9 223.8 -285.8V0Z" }, + { char: "i", advance: 290, left: 67, right: 224, top: -724, bottom: 0, + d: "M66.7 -605.3V-724.1H223.8V-605.3ZM66.7 0V-527.1H223.8V0Z" }, + ], + kerning: [0, -14, 0, 0], + }, + lyzer: { + text: "lyzer", + wght: 380, + glyphs: [ + { char: "l", advance: 241, left: 75, right: 166, top: -723, bottom: 0, + d: "M74.8 0V-723.4H166.4V0Z" }, + { char: "y", advance: 576, left: 9, right: 566, top: -526, bottom: 188, + d: "M122.5 187.7Q100.1 187.7 81 184.7Q62 181.6 46.7 177.4V113.2H99.5Q142.7 113.2 170.5 102.3Q198.3 91.3 217.2 66.6Q236.1 41.9 253.5 0L9.2 -526.4H109.3L232.3 -251.9Q241.3 -233.3 253.1 -204.8Q264.8 -176.3 277.5 -146.4Q290.1 -116.5 299.5 -93.6H306.1Q309.9 -105 316.8 -124.9Q323.7 -144.8 332.5 -168.7Q341.4 -192.5 350.4 -215.9Q359.4 -239.2 366.4 -257.4L470.9 -526.4H566.4L357.1 -15.1Q336.4 34.3 315.4 72.3Q294.5 110.2 268.7 136Q242.9 161.8 207.4 174.8Q171.9 187.7 122.5 187.7Z" }, + { char: "z", advance: 574, left: 34, right: 540, top: -526, bottom: 0, + d: "M33.7 0V-48.6L384.7 -451.4H59.2V-526.4H525.6V-480L172.2 -74.9H540.3V0Z" }, + { char: "e", advance: 665, left: 52, right: 613, top: -538, bottom: 12, + d: "M338 12Q247.2 12 183.4 -17.7Q119.6 -47.4 86 -108.5Q52.5 -169.7 52.5 -263Q52.5 -355.5 85.5 -416.5Q118.5 -477.6 182.2 -508Q245.9 -538.4 337.4 -538.4Q430.4 -538.4 491.6 -507.4Q552.7 -476.5 582.7 -420.1Q612.7 -363.7 612.7 -286.4V-239H148.9Q150.3 -175.2 174.5 -136.1Q198.8 -97 241.9 -79.7Q285 -62.4 342.2 -62.4Q381.1 -62.4 412.4 -72.1Q443.6 -81.9 466.4 -98Q489.2 -114.1 502.3 -135Q515.4 -155.9 516.9 -178.3H608Q607.5 -142.1 591.1 -108Q574.6 -73.9 541.6 -46.9Q508.6 -20 457.7 -4Q406.9 12 338 12ZM149.2 -306.6H516.5Q516.5 -351.4 501.9 -381.4Q487.3 -411.5 461.9 -429.7Q436.6 -447.9 403.9 -455.9Q371.3 -463.8 334.8 -463.8Q282.4 -463.8 241.8 -447.3Q201.2 -430.7 177.4 -395.8Q153.5 -360.9 149.2 -306.6Z" }, + { char: "r", advance: 380, left: 75, right: 363, top: -538, bottom: 0, + d: "M74.8 0V-526.4H148.3L155.8 -431.1H162.6Q169.9 -454.8 186.9 -479.5Q204 -504.1 233.4 -521.3Q262.8 -538.4 304.8 -538.4Q320.9 -538.4 336.3 -536.2Q351.8 -534.1 362.5 -530.5V-449.3H316Q273.7 -449.3 245 -433.8Q216.3 -418.2 199 -392.2Q181.8 -366.2 174.1 -335.4Q166.4 -304.7 166.4 -274.1V0Z" }, + ], + kerning: [0, 0, -20, 0], + }, + media: { + text: "MEDIA", + wght: 700, + glyphs: [ + { char: "M", advance: 1055, left: 87, right: 968, top: -687, bottom: 0, + d: "M87.4 0V-687H344.8L474 -350.2Q480.8 -332.7 490.3 -305.1Q499.8 -277.4 509.7 -248.2Q519.7 -218.9 527.2 -195.6H534.5Q541.1 -216.2 550.3 -243.8Q559.6 -271.5 569.4 -299.8Q579.2 -328.2 586.8 -349.3L716.1 -687H967.7V0H802V-374.7Q802 -400.8 802.6 -430Q803.3 -459.1 804.3 -484.8Q805.4 -510.4 806 -525H798Q793.4 -509.8 785.4 -484.9Q777.3 -460.1 768.9 -434.4Q760.6 -408.6 753.5 -389.6L601.1 0H447.1L294 -389.4Q285.6 -411.5 277.1 -436.6Q268.6 -461.8 261.8 -485.3Q254.9 -508.9 249.7 -524.9H241.7Q242.7 -508.2 243.4 -482.6Q244 -456.9 244.6 -428.9Q245.2 -400.8 245.2 -374.7V0Z" }, + { char: "E", advance: 814, left: 88, right: 755, top: -687, bottom: 0, + d: "M87.6 0V-687H747.7V-554.3H254V-414.7H690.5V-283.9H254V-132.7H755.2V0Z" }, + { char: "D", advance: 879, left: 87, right: 822, top: -687, bottom: 0, + d: "M87.4 0V-687H434.6Q555.6 -687 642.3 -648.1Q728.9 -609.2 775.4 -532.9Q821.8 -456.6 821.8 -343.2Q821.8 -230.7 775.4 -154.2Q728.9 -77.8 642.3 -38.9Q555.6 0 434.6 0ZM253.6 -132.7H427.5Q477.7 -132.7 518.6 -145.6Q559.5 -158.5 589.1 -183.4Q618.6 -208.3 634.5 -245.4Q650.5 -282.5 650.5 -331.1V-356Q650.5 -404.8 634.5 -441.8Q618.6 -478.9 589.1 -503.8Q559.5 -528.7 518.6 -541.5Q477.7 -554.3 427.5 -554.3H253.6Z" }, + { char: "I", advance: 341, left: 87, right: 254, top: -687, bottom: 0, + d: "M87.4 0V-687H253.6V0Z" }, + { char: "A", advance: 864, left: 16, right: 847, top: -687, bottom: 0, + d: "M16.5 0 336.2 -687H527.3L847.3 0H666.5L609.7 -127.1H244.1L187.4 0ZM301.5 -256.6H552.2L482.5 -416.9Q478.1 -427.3 470.4 -445.9Q462.8 -464.6 454.9 -485.1Q446.9 -505.7 440.2 -522.9Q433.5 -540.2 430.8 -547.1H423.7Q416.1 -527.7 406.1 -502.5Q396.1 -477.3 386.8 -454Q377.6 -430.6 371.2 -416.3Z" }, + ], + kerning: [0, 0, 0, 0], + }, +} as const satisfies Record<string, MediaRun>;