SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
6.0 KB · 107 lines typescript
Raw Blame History
1/**2 * Company Atlas mark — single source of geometry.3 *4 * The "atlas plate": a rounded square plate carrying a globe drawn as a sparse grid (ring, one meridian ellipse, two5 * parallels) crossed by a strong equator — the timeline — that ends on a bright "now" node sitting on the horizon,6 * with a short pulse trail behind it. One 64 × 64 geometry; only stroke weights change with the rendered size so the7 * mark stays crisp at 16 px (favicon) and rich at 1200 px (share image).8 *9 * Pure TypeScript, no JSX and no DOM: imported by the React components AND by `scripts/build-icons.mjs` (Node strips10 * the types), which writes the standalone SVGs, the favicon set and the PNGs from exactly this description.11 */1213export const MARK_VIEWBOX = 64;14export const MARK_PLATE_RADIUS = 16;1516const C = 32; // globe centre17const R = 20; // globe radius18const NODE_X = 52; // "now" node sits on the horizon, at the right end of the equator1920function circle(cx: number, cy: number, r: number): string {21  return `M${cx - r} ${cy}a${r} ${r} 0 1 0 ${2 * r} 0a${r} ${r} 0 1 0 ${-2 * r} 0Z`;22}23function ellipse(cx: number, cy: number, rx: number, ry: number): string {24  return `M${cx - rx} ${cy}a${rx} ${ry} 0 1 0 ${2 * rx} 0a${rx} ${ry} 0 1 0 ${-2 * rx} 0Z`;25}26const PAR_DY = 10;27const PAR_HALF = Math.sqrt(R * R - PAR_DY * PAR_DY); // 17.322829/** Path data, in 64-unit coordinates. */30export const MARK_PATHS = {31  ring: circle(C, C, R),32  meridian: ellipse(C, C, 8.5, R),33  parallels: `M${(C - PAR_HALF).toFixed(2)} ${C - PAR_DY}H${(C + PAR_HALF).toFixed(2)}M${(C - PAR_HALF).toFixed(2)} ${C + PAR_DY}H${(C + PAR_HALF).toFixed(2)}`,34  equator: `M12 ${C}H${NODE_X}`,35  /** Tapered pulse trail running along the equator into the node. */36  trail: `M35 30.9L48.6 28.3V35.7L35 33.1Z`,37  node: circle(NODE_X, C, 5),38  halo: circle(NODE_X, C, 8.6),39} as const;4041export type MarkColors = { plate: string; ink: string; grid: string; accent: string };4243/** Dark plate (for light surfaces, print, favicon). */44export const MARK_DARK: MarkColors = { plate: '#0f1419', ink: '#f7f7f4', grid: 'rgba(247,247,244,0.30)', accent: '#3fd07a' };45/** Light plate (for dark surfaces). */46export const MARK_LIGHT: MarkColors = { plate: '#e8ebf1', ink: '#0a0d12', grid: 'rgba(10,13,18,0.26)', accent: '#16a34a' };47/** Theme-following plate through the `--brand-*` tokens declared in globals.css. */48export const MARK_TOKENS: MarkColors = { plate: 'var(--brand-plate)', ink: 'var(--brand-ink)', grid: 'var(--brand-grid)', accent: 'var(--brand-accent)' };4950/** Detail level: `tiny` ≤ 20 px (favicon 16), `small` ≤ 40 px (favicon 32, header), `full` above. */51export type MarkLevel = 'tiny' | 'small' | 'full';52export function markLevel(px: number): MarkLevel {53  return px <= 20 ? 'tiny' : px <= 40 ? 'small' : 'full';54}5556type Weights = { ring: number; grid: number; equator: number; halo: number; node: number; parallels: boolean; halo_on: boolean; bevel: boolean };57const WEIGHTS: Record<MarkLevel, Weights> = {58  tiny: { ring: 5.2, grid: 3.6, equator: 5.2, halo: 0, node: 6.6, parallels: false, halo_on: false, bevel: false },59  small: { ring: 4, grid: 2.6, equator: 4, halo: 2, node: 5.6, parallels: true, halo_on: true, bevel: false },60  full: { ring: 3, grid: 1.9, equator: 3, halo: 1.6, node: 5, parallels: true, halo_on: true, bevel: true },61};6263export type Shape = { tag: 'rect' | 'path'; attrs: Record<string, string | number> };6465export type MarkOptions = {66  level?: MarkLevel;67  /** Draw the plate (false → globe only, e.g. mono variant in text). */68  plate?: boolean;69  /** Plate corner radius in 64-unit coordinates (0 for full-bleed icons that get masked by the OS). */70  radius?: number;71};7273/** The ordered list of shapes that make the mark — the one place its drawing is defined. */74export function markShapes(colors: MarkColors, { level = 'full', plate = true, radius = MARK_PLATE_RADIUS }: MarkOptions = {}): Shape[] {75  const w = WEIGHTS[level];76  const out: Shape[] = [];77  if (plate) {78    out.push({ tag: 'rect', attrs: { x: 0, y: 0, width: MARK_VIEWBOX, height: MARK_VIEWBOX, rx: radius, fill: colors.plate } });79    if (w.bevel && radius > 0) out.push({ tag: 'rect', attrs: { x: 0.75, y: 0.75, width: MARK_VIEWBOX - 1.5, height: MARK_VIEWBOX - 1.5, rx: Math.max(0, radius - 0.75), fill: 'none', stroke: colors.grid, 'stroke-width': 1, opacity: 0.55 } });80  }81  out.push({ tag: 'path', attrs: { d: MARK_PATHS.meridian, fill: 'none', stroke: colors.grid, 'stroke-width': w.grid } });82  if (w.parallels) out.push({ tag: 'path', attrs: { d: MARK_PATHS.parallels, fill: 'none', stroke: colors.grid, 'stroke-width': w.grid, 'stroke-linecap': 'round' } });83  out.push({ tag: 'path', attrs: { d: MARK_PATHS.ring, fill: 'none', stroke: colors.ink, 'stroke-width': w.ring } });84  out.push({ tag: 'path', attrs: { d: MARK_PATHS.equator, fill: 'none', stroke: colors.ink, 'stroke-width': w.equator, 'stroke-linecap': 'round' } });85  out.push({ tag: 'path', attrs: { d: MARK_PATHS.trail, fill: colors.accent, opacity: 0.55 } });86  if (w.halo_on) out.push({ tag: 'path', attrs: { d: MARK_PATHS.halo, fill: 'none', stroke: colors.accent, 'stroke-width': w.halo, opacity: 0.45 } });87  out.push({ tag: 'path', attrs: { d: circle(NODE_X, C, w.node), fill: colors.accent } });88  return out;89}9091function attrString(attrs: Record<string, string | number>): string {92  return Object.entries(attrs)93    .map(([k, v]) => `${k}="${v}"`)94    .join(' ');95}9697/** Standalone SVG document of the mark (used for icon.svg, logo-mark.svg and Satori `<img>` data URIs). */98export function markSvgString(colors: MarkColors, opts: MarkOptions & { size?: number } = {}): string {99  const shapes = markShapes(colors, opts);100  const dim = opts.size ? ` width="${opts.size}" height="${opts.size}"` : '';101  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${MARK_VIEWBOX} ${MARK_VIEWBOX}"${dim} fill="none">${shapes.map((s) => `<${s.tag} ${attrString(s.attrs)}/>`).join('')}</svg>`;102}103104export function markDataUri(colors: MarkColors, opts?: MarkOptions & { size?: number }): string {105  return `data:image/svg+xml;utf8,${encodeURIComponent(markSvgString(colors, opts))}`;106}107