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