/** * Geometry for the ornament kit, as pure functions so every page draws the * same shapes and tests can pin them down. Randomness is seeded, so a star * path looks the same on every build. */ export const colors = { sky: "#0E1522", midnight: "#131C2B", lines: "#26344A", linesStrong: "#3A4C66", starlight: "#F4F2ED", text: "#ECE9E2", soft: "#C8D3D8", muted: "#9FB3BD", teal: "#14A39A", tealBright: "#2BC2B7", gold: "#C9A24A", goldBright: "#D8B35A", violetOnDark: "#9A70F5", /** Light mode only. */ violet: "#6A2FE0", goldDeep: "#8A6516", tealDeep: "#0A726B", } as const; /** A small, fast seeded generator (mulberry32). */ export function seeded(seed: number): () => number { let a = seed >>> 0; return () => { a = (a + 0x6d2b79f5) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } /** A string to a seed, so components can be seeded by a name. */ export function seedOf(text: string): number { let h = 2166136261; for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619); return h >>> 0; } const r1 = (n: number) => Math.round(n * 10) / 10; /** * The thin four-point star used everywhere: points of a polygon centered on * (cx, cy) with outer radius r. The waist stays at 16% so it never reads as a * cross. */ export function starPoints(cx: number, cy: number, r: number): string { const w = r * 0.16; const pts: [number, number][] = [ [cx, cy - r], [cx + w, cy - w], [cx + r, cy], [cx + w, cy + w], [cx, cy + r], [cx - w, cy + w], [cx - r, cy], [cx - w, cy - w], ]; return pts.map(([x, y]) => `${r1(x)},${r1(y)}`).join(" "); } export type StarPathMark = { kind: "dot"; x: number; y: number; r: number } | { kind: "star"; x: number; y: number; r: number; gold: boolean }; export interface StarPath { /** Dotted segments between marks, stopping short of each. */ links: { x1: number; y1: number; x2: number; y2: number }[]; marks: StarPathMark[]; } export const strengths = { calm: 0.1, standard: 0.22, wild: 0.42 } as const; /** * Greg's star path: a straight line replaced by evenly spread stars, each * nudged off the line, joined by dotted segments. About a third of the points * are small violet stars, the rest Starlight dots. Ends stay on the line. */ export function starPath(o: { x1: number; x2: number; y: number; steps: number; seed: number; strength?: keyof typeof strengths; /** Put a large gold star at the middle point. */ centerStar?: boolean; /** Leave the end points off, when a gold line or star already marks them. */ openEnds?: boolean; gap?: number; dot?: number; star?: number; }): StarPath { const rand = seeded(o.seed); const step = (o.x2 - o.x1) / o.steps; const nudge = strengths[o.strength ?? "standard"] * Math.abs(step); const gap = o.gap ?? 4; const points = Array.from({ length: o.steps + 1 }, (_, i) => { const ends = i === 0 || i === o.steps; return { x: o.x1 + step * i, y: ends ? o.y : o.y + (rand() * 2 - 1) * nudge }; }); const mid = Math.round(o.steps / 2); const marks: StarPathMark[] = points.map((p, i) => { if (o.centerStar && i === mid) return { kind: "star", x: r1(p.x), y: r1(p.y), r: (o.star ?? 4) * 3, gold: true }; // Every third point, offset by the seed, is a small star. const isStar = (i + (o.seed % 3)) % 3 === 1; return isStar ? { kind: "star", x: r1(p.x), y: r1(p.y), r: o.star ?? 4, gold: false } : { kind: "dot", x: r1(p.x), y: r1(p.y), r: o.dot ?? 1.5 }; }); const links = points.slice(1).map((b, i) => { const a = points[i]!; const dx = b.x - a.x; const dy = b.y - a.y; const len = Math.hypot(dx, dy) || 1; const ux = dx / len; const uy = dy / len; const ga = gap + (marks[i]!.kind === "star" ? marks[i]!.r * 0.5 : 0); const gb = gap + (marks[i + 1]!.kind === "star" ? marks[i + 1]!.r * 0.5 : 0); return { x1: r1(a.x + ux * ga), y1: r1(a.y + uy * ga), x2: r1(b.x - ux * gb), y2: r1(b.y - uy * gb) }; }); const drop = (i: number) => o.openEnds && (i === 0 || i === points.length - 1); return { links, marks: marks.filter((_, i) => !drop(i)) }; } /** Tick marks for a scale: a long tick every `major`, short ones between. */ export function ticks(width: number, count: number, major = 5): { x: number; long: boolean }[] { const step = width / count; return Array.from({ length: count + 1 }, (_, i) => ({ x: r1(0.5 + step * i), long: i % major === 0 })); } /** Ticks around a circle, for tick rings on step numbers and medallions. */ export function ringTicks(size: number, count = 48, major = 4): { x1: number; y1: number; x2: number; y2: number; long: boolean }[] { const c = size / 2; const outer = c - 1; return Array.from({ length: count }, (_, i) => { const a = (i / count) * Math.PI * 2; const long = i % major === 0; const inner = outer - (long ? size * 0.066 : size * 0.033); const f = (n: number) => Math.round(n * 100) / 100; return { x1: f(c + Math.cos(a) * outer), y1: f(c + Math.sin(a) * outer), x2: f(c + Math.cos(a) * inner), y2: f(c + Math.sin(a) * inner), long }; }); } /** * Constellation dust: sparse specks for a page background as CSS * radial-gradients, about one in five gold. Static by design. */ export function dust(count: number, seed: number): string { const rand = seeded(seed); const specks: string[] = []; for (let i = 0; i < count; i++) { const size = [1, 1, 1.2, 1.6][Math.floor(rand() * 4)]; const alpha = [0.25, 0.35, 0.6][Math.floor(rand() * 3)]; const gold = rand() < 0.2; const color = gold ? `rgba(216,179,90,${alpha})` : `rgba(236,233,226,${alpha})`; specks.push(`radial-gradient(${size}px ${size}px at ${r1(rand() * 100)}% ${r1(rand() * 100)}%, ${color}, transparent)`); } return specks.join(", "); }