// the avatar worker imports this file too and draws the same gradient // 32-bit FNV-1a export const hashString = (str: string): number => { let hash = 2166136261; for (let i = 0; i < str.length; i++) { hash ^= str.charCodeAt(i); hash = Math.imul(hash, 16777619); } return hash >>> 0; }; // mulberry32 const mulberry32 = (seed: number) => { 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: number, places = 1) => Number(n.toFixed(places)); // the chromatic entries of base16 material darker, in wheel order. a run of them stays harmonious const PALETTE = ["#f07178", "#f78c6c", "#ffcb6b", "#c3e88d", "#89ddff", "#82aaff", "#c792ea"]; export interface Gradient { angle: number; // offsets are percentages stops: { offset: number; color: string }[]; glow: { x: number; y: number; r: number; opacity: number; color: string }; } export const gradientFor = (seed: string): Gradient => { const rand = mulberry32(hashString(seed)); const angle = Math.round(rand() * 360); const stopCount = 3 + Math.floor(rand() * 3); const direction = rand() < 0.5 ? -1 : 1; const start = Math.floor(rand() * PALETTE.length); const stops = Array.from({ length: stopCount }, (_, i) => ({ offset: round((i / (stopCount - 1)) * 100), color: PALETTE[ (((start + direction * i) % PALETTE.length) + PALETTE.length) % PALETTE.length ] })); 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 }; }; // ogre's satori reads rgb(r g b / 0) as opaque black but handles hex alpha const alpha = (opacity: number) => Math.round(opacity * 255) .toString(16) .padStart(2, "0"); // the same picture as the worker's svg, as a css background-image export const gradientCss = (seed: string): string => { const { angle, stops, glow } = gradientFor(seed); // the svg gradient line runs corner to corner whatever the angle. css spans the box along its angle const radians = ((angle + 135) * Math.PI) / 180; const scale = Math.SQRT2 / (Math.abs(Math.sin(radians)) + Math.abs(Math.cos(radians))); const linear = stops .map((s) => `${s.color} ${round(50 + (s.offset - 50) * scale)}%`) .join(", "); const radius = round(glow.r * 100); return ( `radial-gradient(${radius}% ${radius}% at ${round(glow.x * 100)}% ${round(glow.y * 100)}%, ` + `${glow.color}${alpha(glow.opacity)}, ${glow.color}00), ` + `linear-gradient(${(angle + 135) % 360}deg, ${linear})` ); };