chrysopedia/frontend/src/components/CreatorAvatar.tsx
jlightner 4b0914b12b fix: restore complete project tree from ub01 canonical state
Auto-mode commit 7aa33cd accidentally deleted 78 files (14,814 lines) during M005
execution. Subsequent commits rebuilt some frontend files but backend/, alembic/,
tests/, whisper/, docker configs, and prompts were never restored in this repo.

This commit restores the full project tree by syncing from ub01's working directory,
which has all M001-M007 features running in production containers.

Restored: backend/ (config, models, routers, database, redis, search_service, worker),
alembic/ (6 migrations), docker/ (Dockerfiles, nginx, compose), prompts/ (4 stages),
tests/, whisper/, README.md, .env.example, chrysopedia-spec.md
2026-03-31 02:10:41 +00:00

113 lines
3.1 KiB
TypeScript

/**
* Deterministic generative avatar for creators.
*
* Bars arranged as a waveshape across 3 phases:
* 1. Q1 (top-left): hash-generated bars above center (left half)
* 2. Flip Q1 vertically → bottom-left (same order, below center)
* 3. Flip that horizontally → bottom-right (reversed order, below center)
* 4. Remove the bottom-left, keeping only Q1 + bottom-right
*
* Result: top-left positive bars flow into bottom-right negative bars
* (reversed), producing a single oscillator cycle across the full width.
*/
interface CreatorAvatarProps {
creatorId: string;
name: string;
imageUrl?: string | null;
size?: number;
}
function hashBytes(str: string, count: number): number[] {
const bytes: number[] = [];
let h = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
for (let i = 0; i < count; i++) {
h = Math.imul(h, 0x01000193) ^ i;
bytes.push(((h >>> 0) % 256));
}
return bytes;
}
export default function CreatorAvatar({ creatorId, name, imageUrl, size = 32 }: CreatorAvatarProps) {
if (imageUrl) {
return (
<img
src={imageUrl}
alt={name}
className="creator-avatar creator-avatar--img"
style={{ width: size, height: size }}
/>
);
}
const b = hashBytes(creatorId, 32);
const g = (i: number) => b[i] ?? 0;
const hue1 = g(0) * 1.41;
const hue2 = (hue1 + 35 + g(1) * 0.4) % 360;
const sat = 58 + (g(2) % 25);
const lum = 48 + (g(3) % 18);
const c1 = `hsl(${hue1}, ${sat}%, ${lum}%)`;
const c2 = `hsl(${hue2}, ${sat - 8}%, ${lum - 8}%)`;
// Half the bars from hash — this is Q1 (positive, left side)
const half = 11;
const total = half * 2;
const pad = 1.2;
const usable = 24 - pad * 2;
const step = usable / total;
const barW = step * 0.65;
const maxAmp = 11;
const q1: number[] = [];
for (let i = 0; i < half; i++) {
q1.push(1 + (g(4 + i) / 255) * (maxAmp - 1));
}
// Bottom-right: Q1 flipped vertically then flipped horizontally
// = reversed order, negated heights
const br = [...q1].reverse().map((h) => -h);
// Final: Q1 (positive, left) then BR (negative, right)
const allHeights = [...q1, ...br];
const gradId = `ag-${creatorId.slice(0, 8)}`;
return (
<svg
viewBox="0 0 24 24"
className="creator-avatar creator-avatar--gen"
style={{ width: size, height: size }}
role="img"
aria-label={`Avatar for ${name}`}
>
<defs>
<linearGradient id={gradId} x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stopColor={c1} />
<stop offset="100%" stopColor={c2} />
</linearGradient>
</defs>
<rect width="24" height="24" rx="4" fill={`url(#${gradId})`} opacity="0.1" />
{allHeights.map((h, i) => {
const x = pad + i * step + (step - barW) / 2;
const absH = Math.abs(h);
const y = h >= 0 ? 12 - absH : 12;
return (
<rect
key={i}
x={x}
y={y}
width={barW}
height={absH}
rx={barW / 2}
fill={`url(#${gradId})`}
/>
);
})}
</svg>
);
}