232 lines · 9.2 KB
/**
 * Deterministic lava-lamp avatars, a nod to Cloudflare's wall of entropy lamps. Everything is derived
 * from the handle, so the same handle renders the same lamp on every client and deployment with
 * nothing stored. Output must stay stable once shipped: changing it changes everyone's avatar.
 *
 * The static lamp is the identity. The animated variant starts on the exact static frame and loops
 * back to it, so swapping between them never jumps. Animation draws from its own random stream, so
 * tuning motion can never change what a lamp looks like at rest.
 */

const VIEW = 64;
const COMPLEMENTARY_CHANCE = 0.6;
const TOP_CAP_CHANCE = 0.45;
const TWO_TONE_CHANCE = 0.35;
const FINE_BUBBLES_CHANCE = 0.3;
const MAX_CACHED = 500;
const CLEARANCE = 8;
const MIN_SPEED = 3.5;
const MAX_SPEED = 5.5;
const MIN_LOOP_SECONDS = 14;
const MAX_LOOP_SECONDS = 30;

function cyrb53(value: string, seed = 0): [number, number] {
  let h1 = 0xdeadbeef ^ seed;
  let h2 = 0x41c6ce57 ^ seed;
  for (let i = 0; i < value.length; i++) {
    const code = value.charCodeAt(i);
    h1 = Math.imul(h1 ^ code, 2654435761);
    h2 = Math.imul(h2 ^ code, 1597334677);
  }
  h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
  h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
  return [h1 >>> 0, h2 >>> 0];
}

function mulberry32(seed: number): () => number {
  let state = seed >>> 0;
  return () => {
    state = (state + 0x6d2b79f5) >>> 0;
    let t = state;