// 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 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, ICON_PALETTES, 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: ICON_PALETTES.archilyzer.ground, dim: ICON_PALETTES.archilyzer.dim, 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 => 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; // 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): 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 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 = ``; if (!doc.startsWith(open)) throw new Error("nestedMark: markSvg's root changed shape"); return ( `` + 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(); for (const p of g.glyphs) { const fill = palette[p.tone].toLowerCase(); const list = byTone.get(fill) ?? []; list.push( ``, ); byTone.set(fill, list); } const text = [...byTone].map(([fill, paths]) => `${paths.join("")}`).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 ( `` + lockupBody(g, palette, 0, g.inkTop) + `` ); } // ── 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, 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): 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(seq: ReadonlyArray): 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]) => ``) .join(""); const lit = palette.lit.toLowerCase(); const found = `` + ``; const { geometry, x, y } = mediaBannerLayout(); return ( `` + `` + `${lines}` + found + lockupBody(geometry, palette, x, y) + `` ); } // ── 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 }); }