// 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})`
);
};