// Deterministic, vibrant gradients for placeholder avatars. // // Everything is derived from the actor string alone, so the same actor always // gets the same gradient and nothing needs to be stored anywhere. The colors // are the seven chromatic entries of the base16 material darker palette, so // every stop is a saturated, high-contrast color instead of a washed-out tint. // 32-bit FNV-1a. The old placeholder used a rolling hash to pick a flat color; // this keeps the same "same actor, same result" property while giving the PRNG // below a well-distributed seed. export const hashString = (str) => { let hash = 2166136261; for (let i = 0; i < str.length; i++) { hash ^= str.charCodeAt(i); hash = Math.imul(hash, 16777619); } return hash >>> 0; }; // mulberry32: tiny, fast, and more than good enough for picking pretty colors. const mulberry32 = (seed) => { let a = seed >>> 0; return () => { a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }; const round = (n, places = 1) => Number(n.toFixed(places)); // The seven chromatic colors of base16 material darker, ordered around the // wheel so a run of them stays harmonious: red, orange, yellow, green, cyan, // blue, purple. const PALETTE = [ "#f07178", // red "#f78c6c", // orange "#ffcb6b", // yellow "#c3e88d", // green "#89ddff", // cyan "#82aaff", // blue "#c792ea" // purple ]; /** * Pick all the parameters for an actor's gradient. * @returns {{ angle: number, stops: {offset: number, color: string}[], * glow: {x: number, y: number, r: number, opacity: number, color: string} }} */ export const gradientFor = (actor) => { const rand = mulberry32(hashString(actor)); const angle = Math.round(rand() * 360); const stopCount = 3 + Math.floor(rand() * 3); // 3..5 stops const direction = rand() < 0.5 ? -1 : 1; const start = Math.floor(rand() * PALETTE.length); // walk the palette from a random entry, wrapping, so the stops are a run of // neighbouring hues rather than a random collage const stops = []; for (let i = 0; i < stopCount; i++) { const index = (((start + direction * i) % PALETTE.length) + PALETTE.length) % PALETTE.length; stops.push({ offset: stopCount === 1 ? 0 : i / (stopCount - 1), color: PALETTE[index] }); } // A soft light source for atmosphere, drawn from another palette color. const glow = { x: round(0.2 + rand() * 0.6, 2), y: round(0.15 + rand() * 0.5, 2), r: round(0.55 + rand() * 0.35, 2), opacity: round(0.25 + rand() * 0.2, 2), color: PALETTE[Math.floor(rand() * PALETTE.length)] }; return { angle, stops, glow }; }; /** * Render an actor's placeholder as a self-contained SVG string. */ export const gradientSvg = (actor, size = 128) => { const { angle, stops, glow } = gradientFor(actor); const id = hashString(actor).toString(36); const stopMarkup = stops .map( (s) => `` ) .join(""); return ( `` + `` + `${stopMarkup}` + `` + `` + `` + `` + `` + `` + `` + `` ); };