/** * @factory/shared — realtime presence palette (PRODUCT-SPEC realtime row, * phase-07 TASK-0092). * * `presenceColor(userId)` is the single deterministic source of a peer's * cursor/avatar color, shared so the server (presence broadcasts) and * clients agree without negotiating. 20 distinct colors; the a11y rule is * that color is never the only identifier (the peer's name label is * mandatory — ux-requirements §12), so a color collision is harmless. * * Deterministic FNV-1a 32-bit hash → palette index. No allocation, no * Math.random (rule 25: deterministic behavior). */ /** The 20-color presence palette (closed set; names, not raw hex, on the wire). */ export const PRESENCE_PALETTE: readonly string[] = [ "red", "orange", "amber", "yellow", "lime", "green", "teal", "cyan", "sky", "blue", "indigo", "violet", "purple", "fuchsia", "pink", "rose", "slate", "gray", "zinc", "stone", ]; /** * FNV-1a 32-bit hash of a string. Deterministic across platforms (32-bit * math is exact in JS doubles). */ export function fnv1a32(input: string): number { let hash = 0x811c9dc5; for (let i = 0; i < input.length; i += 1) { hash ^= input.charCodeAt(i); // hash *= 16777619 (FNV prime) — via multiply + >>> 0 (32-bit wrap) hash = Math.imul(hash, 0x01000193) >>> 0; } return hash >>> 0; } /** * The deterministic presence color name for a user. * * Distribution guarantee (unit-tested): over any 1,000-user fixture no * color is assigned to more than 10 % of users (palette of 20 ⇒ expected * 5 %; the test asserts the FNV-1a mix is uniform enough). */ export function presenceColor(userId: string): string { const index = fnv1a32(userId) % PRESENCE_PALETTE.length; return PRESENCE_PALETTE[index] ?? "gray"; }