evOo-local/packages/core/src/avatar-shape.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

188 lines
4.8 KiB
TypeScript

type AvatarPoint = readonly [number, number];
let organicAvatarTemplates: readonly (readonly AvatarPoint[])[] | undefined;
export function avatarIdentitySeed(identity: string): number {
let hash = 0;
for (let index = 0; index < identity.length; index++) {
hash = (hash << 5) - hash + identity.charCodeAt(index);
hash |= 0;
}
return Math.abs(hash);
}
export function organicAvatarPath(seed: number, phaseOffset = 0): string {
const phase = ((seed % 360) * Math.PI) / 180 + phaseOffset;
const family = seed % 10;
if (!organicAvatarTemplates) {
organicAvatarTemplates = [
[
[-43, 22],
[-51, 16],
[-53, 6],
[-49, -4],
[-40, -12],
[-30, -13],
[-27, -24],
[-18, -34],
[-6, -36],
[4, -32],
[12, -42],
[25, -45],
[37, -38],
[43, -27],
[42, -17],
[51, -11],
[56, 0],
[54, 12],
[46, 20],
[30, 24],
[0, 25],
[-29, 25],
],
[
[0, -52],
[12, -35],
[28, -17],
[39, 2],
[42, 21],
[34, 38],
[19, 49],
[0, 52],
[-19, 49],
[-34, 38],
[-42, 21],
[-39, 2],
[-28, -17],
[-12, -35],
],
[
[-42, -23],
[-27, -40],
[-7, -46],
[14, -40],
[33, -27],
[46, -8],
[46, 14],
[35, 34],
[14, 46],
[-8, 46],
[-29, 38],
[-43, 21],
[-48, 0],
],
Array.from({ length: 40 }, (_, index) => {
const angle = (index / 40) * Math.PI * 2;
const radius = 44 + Math.cos(angle * 5) * 5;
return [Math.cos(angle) * radius, Math.sin(angle) * radius];
}),
Array.from({ length: 36 }, (_, index) => {
const angle = (index / 36) * Math.PI * 2;
const radius = 43 + Math.cos(angle * 6) * 7;
return [Math.cos(angle) * radius, Math.sin(angle) * radius];
}),
Array.from({ length: 32 }, (_, index) => {
const angle = (index / 32) * Math.PI * 2;
const x = Math.cos(angle);
const y = Math.sin(angle);
return [
Math.sign(x) * Math.sqrt(Math.abs(x)) * 46,
Math.sign(y) * Math.sqrt(Math.abs(y)) * 43,
];
}),
[
[0, 48],
[-14, 36],
[-32, 20],
[-45, 0],
[-43, -20],
[-29, -36],
[-10, -39],
[0, -27],
[10, -39],
[29, -36],
[43, -20],
[45, 0],
[32, 20],
[14, 36],
],
[
[-48, 35],
[-38, 22],
[-34, -8],
[-27, -31],
[-12, -44],
[0, -48],
[12, -44],
[27, -31],
[34, -8],
[38, 22],
[48, 35],
[26, 42],
[0, 44],
[-26, 42],
],
[
[-45, -19],
[-28, -38],
[-4, -46],
[22, -41],
[42, -25],
[49, -2],
[42, 23],
[22, 42],
[-4, 47],
[-29, 38],
[-46, 18],
[-50, 0],
],
[
[-50, 0],
[-45, -20],
[-30, -38],
[0, -48],
[30, -38],
[45, -20],
[50, 0],
[35, 10],
[18, 12],
[16, 38],
[0, 46],
[-16, 38],
[-18, 12],
[-35, 10],
],
] as const;
}
const templates = organicAvatarTemplates;
const xScale = 1 + Math.sin(phase) * 0.035;
const yScale = 1 + Math.cos(phase) * 0.025;
const shear = Math.sin(phase * 1.7) * 0.025;
let points = (templates[family] ?? templates[5]!).map(([x, y]) => ({
x: x * xScale + y * shear,
y: y * yScale,
}));
const smoothingPasses = family >= 3 && family <= 5 ? 1 : 2;
for (let pass = 0; pass < smoothingPasses; pass++) {
points = points.flatMap((point, index) => {
const next = points[(index + 1) % points.length]!;
return [
{ x: point.x * 0.75 + next.x * 0.25, y: point.y * 0.75 + next.y * 0.25 },
{ x: point.x * 0.25 + next.x * 0.75, y: point.y * 0.25 + next.y * 0.75 },
];
});
}
const pointCount = points.length;
const round = (value: number) => Math.round(value * 100) / 100;
const first = points[0]!;
let path = `M${round(first.x)} ${round(first.y)}`;
for (let index = 0; index < pointCount; index++) {
const before = points[(index - 1 + pointCount) % pointCount]!;
const current = points[index]!;
const next = points[(index + 1) % pointCount]!;
const after = points[(index + 2) % pointCount]!;
path += `C${round(current.x + (next.x - before.x) / 6)} ${round(current.y + (next.y - before.y) / 6)} ${round(next.x - (after.x - current.x) / 6)} ${round(next.y - (after.y - current.y) / 6)} ${round(next.x)} ${round(next.y)}`;
}
return `${path}Z`;
}