/* eslint-disable @typescript-eslint/naming-convention */
import { cached } from "@app/lib/cached";
type AtomType = "A" | "B" | "C" | "D";
type ShapeModel =
| "rose"
| "starburst"
| "ringed"
| "tip"
| "notched"
| "hollow";
type AtomMode = "bands-ABC" | "angle-stripes" | "parity-ACB" | "balanced-rand";
const PALETTE = [
"#00D4DA", // teal
"#886BF2", // purple
"#FFA5FF", // pink
"#009F67", // green
"#CCFF38", // lime
"#585600", // olive
];
const REPO_CONFIG = {
GRID_SIZE: 16,
CELL_SIZE: 32,
PIXEL_DENSITY: 2,
GLYPH: {
WIDTH: 5,
HEIGHT: 7,
SPACING: 2,
SCALE_2X_WIDTH: 10,
SCALE_2X_HEIGHT: 14,
},
ATOMS: {
CIRCLE_B_RATIO: 0.55,
CIRCLE_C_RATIO: 0.67,
},
} as const;
const USER_CONFIG = {
TILE_SIZE: 32,
DEFAULT_GRID: 10,
PIXEL_DENSITY: 2,
ATOMS: {
ELLIPSE_B_SIZE: 17,
ELLIPSE_C_SIZE: 21,
},
TOLERANCE: {
ANGLE_NEAR_BASE: Math.PI / 28,
ANGLE_FAR_BASE: Math.PI / 7,
NEAR_RANGE: { MIN: 0.7, MAX: 1.2 },
FAR_RANGE: { MIN: 0.7, MAX: 1.2 },
},
SOFTNESS: {
MIN: 1.2,
MAX: 4.2,
},
RING_PHASE: {
MIN: 0.2,
MAX: 0.8,
},
} as const;
function hash32(str: string): number {
let h = 2166136261 >>> 0;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return h >>> 0;
}
function xmur3(str: string): () => number {
let h = 1779033703 ^ str.length;
for (let i = 0; i < str.length; i++) {
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
h = (h << 13) | (h >>> 19);
}
return function () {
h = Math.imul(h ^ (h >>> 16), 2246822507);
h = Math.imul(h ^ (h >>> 13), 3266489909);
return (h ^= h >>> 16) >>> 0;
};
}
function mulberry32(a: number): () => number {
return function () {
let t = (a += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function makeRNG(key: string): () => number {
return mulberry32(xmur3(key)());
}
function chooseK<T>(arr: T[], k: number, rnd: () => number): T[] {
const pool = arr.slice();
const out = [];
for (let i = 0; i < k; i++) {
const idx = Math.floor(rnd() * pool.length);
out.push(pool.splice(idx, 1)[0]);
}
return out;
}
function pick<T>(rng: () => number, arr: T[]): T {
return arr[Math.floor(rng() * arr.length)];
}
function createOffscreenCanvas(
w: number,
h: number,
density: number = 2,
): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } {
const canvas = document.createElement("canvas");
canvas.width = w * density;
canvas.height = h * density;
const ctx = canvas.getContext("2d")!;
ctx.scale(density, density);
ctx.imageSmoothingEnabled = false;
return { canvas, ctx };
}
function fillCircle(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
diameter: number,
color: string,
): void {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, diameter / 2, 0, Math.PI * 2);
ctx.fill();
}
function fillEllipse(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
color: string,
): void {
ctx.fillStyle = color;
ctx.beginPath();
ctx.ellipse(x, y, w / 2, h / 2, 0, 0, Math.PI * 2);
ctx.fill();
}
interface AtomDrawConfig {
cellSize: number;
circleB: number;
circleC: number;
useEllipse?: boolean;
}
function createAtomRenderer(config: AtomDrawConfig) {
const drawCircleOrEllipse = (
ctx: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
color: string,
) => {
if (config.useEllipse) {
fillEllipse(ctx, x, y, w, h, color);
} else {
fillCircle(ctx, x, y, w, color); // w is diameter for circles
}
};
return {
drawAtomA(
ctx: CanvasRenderingContext2D,
gx: number,
gy: number,
c1: string,
): void {
const x = gx * config.cellSize;
const y = gy * config.cellSize;
ctx.fillStyle = c1;
ctx.fillRect(x, y, config.cellSize, config.cellSize);
},
drawAtomB(
ctx: CanvasRenderingContext2D,
gx: number,
gy: number,
c1: string,
c2: string,
): void {
const x = gx * config.cellSize;
const y = gy * config.cellSize;
ctx.fillStyle = c1;
ctx.fillRect(x, y, config.cellSize, config.cellSize);
drawCircleOrEllipse(
ctx,
x + config.cellSize / 2,
y + config.cellSize / 2,
config.circleB,
config.circleB,
c2,
);
},
drawAtomC(
ctx: CanvasRenderingContext2D,
gx: number,
gy: number,
c2: string,
c3: string,
): void {
const x = gx * config.cellSize;
const y = gy * config.cellSize;
ctx.fillStyle = c2;
ctx.fillRect(x, y, config.cellSize, config.cellSize);
drawCircleOrEllipse(
ctx,
x + config.cellSize / 2,
y + config.cellSize / 2,
config.circleC,
config.circleC,
c3,
);
},
drawAtomD(
ctx: CanvasRenderingContext2D,
gx: number,
gy: number,
c3: string,
): void {
const x = gx * config.cellSize;
const y = gy * config.cellSize;
ctx.fillStyle = c3;
ctx.fillRect(x, y, config.cellSize, config.cellSize);
},
drawAtom(
ctx: CanvasRenderingContext2D,
atom: AtomType,
gx: number,
gy: number,
c1: string,
c2: string,
c3: string,
): void {
switch (atom) {
case "A":
this.drawAtomA(ctx, gx, gy, c1);
break;
case "B":
this.drawAtomB(ctx, gx, gy, c1, c2);
break;
case "C":
this.drawAtomC(ctx, gx, gy, c2, c3);
break;
case "D":
this.drawAtomD(ctx, gx, gy, c3);
break;
}
},
};
}
// 5x7 pixel font glyphs (cached at module level for performance)
function createGlyphs5x7(): Record<string, number[][]> {
const L: Record<string, number[][]> = {};
const r = (s: string[]) =>
s.map(row => row.split("").map(ch => (ch === "1" ? 1 : 0)));
L["A"] = r(["01110", "10001", "10001", "11111", "10001", "10001", "10001"]);
L["B"] = r(["11110", "10001", "10001", "11110", "10001", "10001", "11110"]);
L["C"] = r(["01111", "10000", "10000", "10000", "10000", "10000", "01111"]);
L["D"] = r(["11110", "10001", "10001", "10001", "10001", "10001", "11110"]);
L["E"] = r(["11111", "10000", "10000", "11110", "10000", "10000", "11111"]);
L["F"] = r(["11111", "10000", "10000", "11110", "10000", "10000", "10000"]);
L["G"] = r(["01111", "10000", "10000", "10111", "10001", "10001", "01111"]);
L["H"] = r(["10001", "10001", "10001", "11111", "10001", "10001", "10001"]);
L["I"] = r(["11111", "00100", "00100", "00100", "00100", "00100", "11111"]);
L["J"] = r(["11111", "00001", "00001", "00001", "10001", "10001", "01110"]);
L["K"] = r(["10001", "10010", "10100", "11000", "10100", "10010", "10001"]);
L["L"] = r(["10000", "10000", "10000", "10000", "10000", "10000", "11111"]);
L["M"] = r(["10001", "11011", "10101", "10001", "10001", "10001", "10001"]);
L["N"] = r(["10001", "11001", "10101", "10011", "10001", "10001", "10001"]);
L["O"] = r(["01110", "10001", "10001", "10001", "10001", "10001", "01110"]);
L["P"] = r(["11110", "10001", "10001", "11110", "10000", "10000", "10000"]);
L["Q"] = r(["01110", "10001", "10001", "10001", "10101", "10010", "01101"]);
L["R"] = r(["11110", "10001", "10001", "11110", "10100", "10010", "10001"]);
L["S"] = r(["01111", "10000", "11110", "00001", "00001", "10001", "11110"]);
L["T"] = r(["11111", "00100", "00100", "00100", "00100", "00100", "00100"]);
L["U"] = r(["10001", "10001", "10001", "10001", "10001", "10001", "01110"]);
L["V"] = r(["10001", "10001", "10001", "01010", "01010", "00100", "00100"]);
L["W"] = r(["10001", "10001", "10001", "10101", "10101", "11011", "10001"]);
L["X"] = r(["10001", "01010", "00100", "00100", "00100", "01010", "10001"]);
L["Y"] = r(["10001", "01010", "00100", "00100", "00100", "00100", "00100"]);
L["Z"] = r(["11111", "00001", "00010", "00100", "01000", "10000", "11111"]);
L["0"] = r(["01110", "10001", "10011", "10101", "11001", "10001", "01110"]);
L["1"] = r(["00100", "01100", "00100", "00100", "00100", "00100", "01110"]);
L["2"] = r(["01110", "10001", "00001", "00110", "01000", "10000", "11111"]);
L["3"] = r(["11110", "00001", "01110", "00001", "00001", "00001", "11110"]);
L["4"] = r(["10010", "10010", "10010", "11111", "00010", "00010", "00010"]);
L["5"] = r(["11111", "10000", "11110", "00001", "00001", "00001", "11110"]);
L["6"] = r(["01110", "10000", "11110", "10001", "10001", "10001", "01110"]);
L["7"] = r(["11111", "00001", "00010", "00100", "01000", "01000", "01000"]);
L["8"] = r(["01110", "10001", "01110", "10001", "10001", "10001", "01110"]);
L["9"] = r(["01110", "10001", "10001", "01111", "00001", "00001", "11110"]);
L["?"] = r(["11111", "00001", "01110", "00000", "00100", "00000", "00100"]);
return L;
}
const LETTER_5X7 = createGlyphs5x7();
function getInitials(name: string): string[] {
if (!name || typeof name !== "string") return ["?"];
const cleaned = name.trim().replace(/\s+/g, " ");
const parts = cleaned.split(/[^A-Za-z0-9]+/).filter(Boolean);
const first = parts[0] ? parts[0][0].toUpperCase() : "?";
const second = parts[1] ? parts[1][0].toUpperCase() : null;
return second ? [first, second] : [first];
}
function polarFromCell(
gx: number,
gy: number,
cx: number,
cy: number,
): { r: number; a: number } {
const x = gx - cx + 0.5;
const y = gy - cy + 0.5;
const r = Math.hypot(x, y);
let a = Math.atan2(y, x);
if (a < 0) a += 2 * Math.PI;
return { r, a };
}
function shapeRose(theta: number, petals: number, tol: number): boolean {
const sector = Math.PI / petals;
const nearest = Math.round(theta / sector) * sector;
let diff = Math.abs(theta - nearest);
diff = Math.min(diff, 2 * Math.PI - diff);
return diff <= tol;
}
function shapeStarburst(
theta: number,
petals: number,
softness: number,
): boolean {
const period = (2 * Math.PI) / petals;
const local = theta % period;
const d = Math.min(local, period - local) / (period / 2);
const response = Math.pow(Math.cos((d * Math.PI) / 2), softness);
return response > 0.5;
}
function shapeRinged(
theta: number,
petals: number,
ringPhase: number,
tol: number,
): boolean {
const sector = (2 * Math.PI) / petals;
const k = Math.floor(theta / sector);
const center = k * sector + sector * ringPhase;
let diff = Math.abs(theta - center);
diff = Math.min(diff, 2 * Math.PI - diff);
return diff <= tol;
}
function shapeTip(
theta: number,
petals: number,
tol: number,
t: number,
): boolean {
return shapeRose(theta, petals, tol * (0.5 + 1.0 * t)) && t > 0.45;
}
function shapeNotched(
theta: number,
petals: number,
tol: number,
notchDepth: number = 0.25,
): boolean {
const sector = (2 * Math.PI) / petals;
const local = (theta % sector) / sector;
const notch = Math.abs(local - 0.5);
return shapeRose(theta, petals, tol) && notch > notchDepth;
}
function shapeHollow(
theta: number,
petals: number,
tol: number,
t: number,
inner: number = 0.28,
outer: number = 0.9,
): boolean {
return shapeRose(theta, petals, tol) && t > inner && t < outer;
}
function sectorGate(theta: number, petals: number, mask: boolean[]): boolean {
const sector = (2 * Math.PI) / petals;
const k = Math.floor(theta / sector);
return mask[k % mask.length];
}
export const cachedRepoAvatar = cached(
async (key: string) => renderRepoAvatar(key),
(key: string) => key,
{ max: 2000 },
);
export const cachedUserAvatar = cached(
async (key: string) => renderUserAvatar(key),
(key: string) => key,
{ max: 2000 },
);
function renderRepoAvatar(key: string): string {
if (typeof window === "undefined") {
return "";
}
{
// Color logic:
// A: square = Color1
// B: square = Color1, circle = Color2
// C: square = Color2, circle = Color3
// D: square = Color3
// Letters: solid-only (A or D). Background: other three atoms.
// Single initial: 2x2 expansion (10x14). Two initials: 5x7 each.
const GRID = REPO_CONFIG.GRID_SIZE;
const CELL = REPO_CONFIG.CELL_SIZE;
const W = GRID * CELL;
const H = GRID * CELL;
const { canvas, ctx } = createOffscreenCanvas(
W,
H,
REPO_CONFIG.PIXEL_DENSITY,
);
const atoms = createAtomRenderer({
cellSize: CELL,
circleB: CELL * REPO_CONFIG.ATOMS.CIRCLE_B_RATIO,
circleC: CELL * REPO_CONFIG.ATOMS.CIRCLE_C_RATIO,
useEllipse: false,
});
function renderInitialsAvatar(nameKey: string = "color bright") {
const initials = getInitials(nameKey);
const seed = hash32(nameKey.toLowerCase());
const rnd = mulberry32(seed);
// Select three distinct colors deterministically (Color1, Color2, Color3)
const [c1, c2, c3] = chooseK(PALETTE, 3, rnd);
// Choose letter solid atom: 'A' or 'D' (deterministic)
const letterSolidAtom: AtomType = ((seed >>> 7) & 1) === 0 ? "A" : "D";
const bgAtoms: AtomType[] = (["A", "B", "C", "D"] as AtomType[]).filter(
a => a !== letterSolidAtom,
);
function pickBgAtom(gx: number, gy: number): AtomType {
const k = (gy * 131 + gx * 197 + seed) >>> 0;
return bgAtoms[k % bgAtoms.length];
}
// 1) Background: fill with bgAtoms using strict color mapping
for (let gy = 0; gy < GRID; gy++) {
for (let gx = 0; gx < GRID; gx++) {
const atom = pickBgAtom(gx, gy);
atoms.drawAtom(ctx, atom, gx, gy, c1, c2, c3);
}
}
// 2) Letters: solid-only atom across glyph pixels, strict mapping (A uses c1, D uses c3)
const glyphW = REPO_CONFIG.GLYPH.WIDTH;
const glyphH = REPO_CONFIG.GLYPH.HEIGHT;
function placeSolidLetter(
glyph: number[][],
startX: number,
startY: number,
scale2x: boolean,
) {
if (scale2x) {
// 2x2 expansion → 10x14
for (let r = 0; r < glyphH; r++) {
for (let c = 0; c < glyphW; c++) {
if (!glyph[r][c]) continue;
const gx = startX + c * 2;
const gy = startY + r * 2;
if (letterSolidAtom === "A") {
atoms.drawAtomA(ctx, gx, gy, c1);
atoms.drawAtomA(ctx, gx + 1, gy, c1);
atoms.drawAtomA(ctx, gx, gy + 1, c1);
atoms.drawAtomA(ctx, gx + 1, gy + 1, c1);
} else {
atoms.drawAtomD(ctx, gx, gy, c3);
atoms.drawAtomD(ctx, gx + 1, gy, c3);
atoms.drawAtomD(ctx, gx, gy + 1, c3);
atoms.drawAtomD(ctx, gx + 1, gy + 1, c3);
}
}
}
} else {
// 1x scale
for (let r = 0; r < glyphH; r++) {
for (let c = 0; c < glyphW; c++) {
if (!glyph[r][c]) continue;
const gx = startX + c;
const gy = startY + r;
if (letterSolidAtom === "A") {
atoms.drawAtomA(ctx, gx, gy, c1);
} else {
atoms.drawAtomD(ctx, gx, gy, c3);
}
}
}
}
}
if (initials.length === 1) {
// Single initial: 2x2 expansion (10x14), centered
const glyph = LETTER_5X7[initials[0]] || LETTER_5X7["?"];
const startX = Math.floor(
(GRID - REPO_CONFIG.GLYPH.SCALE_2X_WIDTH) / 2,
);
const startY = Math.floor(
(GRID - REPO_CONFIG.GLYPH.SCALE_2X_HEIGHT) / 2,
);
placeSolidLetter(glyph, startX, startY, true);
} else {
// Two initials: 5x7 each, side-by-side
const leftGlyph = LETTER_5X7[initials[0]] || LETTER_5X7["?"];
const rightGlyph = LETTER_5X7[initials[1]] || LETTER_5X7["?"];
const spacing = REPO_CONFIG.GLYPH.SPACING;
const totalW = REPO_CONFIG.GLYPH.WIDTH * 2 + spacing;
const totalH = REPO_CONFIG.GLYPH.HEIGHT;
const startX = Math.floor((GRID - totalW) / 2);
const startY = Math.floor((GRID - totalH) / 2);
placeSolidLetter(leftGlyph, startX, startY, false);
placeSolidLetter(
rightGlyph,
startX + REPO_CONFIG.GLYPH.WIDTH + spacing,
startY,
false,
);
}
return canvas.toDataURL();
}
return renderInitialsAvatar(key);
}
}
function renderUserAvatar(key: string): string {
if (typeof window === "undefined") {
return "";
}
{
const TILE = USER_CONFIG.TILE_SIZE;
const DEFAULT_GRID = USER_CONFIG.DEFAULT_GRID;
const atoms = createAtomRenderer({
cellSize: TILE,
circleB: USER_CONFIG.ATOMS.ELLIPSE_B_SIZE,
circleC: USER_CONFIG.ATOMS.ELLIPSE_C_SIZE,
useEllipse: true,
});
// Edge-to-edge integer placement
function drawAt(
fn: (x: number, y: number) => void,
gx: number,
gy: number,
): void {
fn(gx * TILE, gy * TILE);
}
// Strict 4-way symmetry (quadrant mirroring)
function drawQuad(
fn: (x: number, y: number) => void,
gx: number,
gy: number,
grid: number,
): void {
const N = grid - 1;
drawAt(fn, gx, gy);
drawAt(fn, N - gx, gy);
drawAt(fn, gx, N - gy);
drawAt(fn, N - gx, N - gy);
}
// Draw by atom type (converts pixel coordinates to grid coordinates)
function drawAtomByType(
ctx: CanvasRenderingContext2D,
type: AtomType,
x: number,
y: number,
c1: string,
c2: string,
c3: string,
): void {
const gx = x / TILE;
const gy = y / TILE;
atoms.drawAtom(ctx, type, gx, gy, c1, c2, c3);
}
// Make assigner among active petal atoms (exclude background atom)
function makeAssigner(
mode: AtomMode,
activeAtoms: AtomType[],
): (rCell: number, theta?: number, sectorIdx?: number) => AtomType {
if (mode === "bands-ABC")
return (rCell: number) => activeAtoms[rCell % 3];
if (mode === "angle-stripes")
return (_rCell: number, _theta?: number, sectorIdx?: number) =>
activeAtoms[(sectorIdx || 0) % activeAtoms.length];
if (mode === "parity-ACB")
return (rCell: number) => activeAtoms[rCell % 2 ? 1 : 0];
if (mode === "balanced-rand")
return (rCell: number, theta?: number, sectorIdx?: number) => {
const v =
(Math.sin(
(theta || 0) * 13.37 + rCell * 2.17 + (sectorIdx || 0) * 0.73,
) +
1) /
2;
if (v < 0.33) return activeAtoms[0];
if (v < 0.66) return activeAtoms[1];
return activeAtoms[2];
};
return (rCell: number) => activeAtoms[rCell % 3];
}
function generateFlower(
ctx: CanvasRenderingContext2D,
canvas: HTMLCanvasElement,
key: string,
grid: number = DEFAULT_GRID,
): void {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const rng = makeRNG(key);
const picked = PALETTE.slice().sort(() => rng() - 0.5);
const [c1, c2, c3] = picked.slice(0, 3);
const allAtoms: AtomType[] = ["A", "B", "C", "D"];
const bgAtom: AtomType = pick(rng, allAtoms);
const petalAtoms: AtomType[] = allAtoms
.filter(a => a !== bgAtom)
.sort(() => rng() - 0.5);
const cx = Math.floor(grid / 2),
cy = Math.floor(grid / 2);
const maxR = Math.min(cx, cy);
const petals = pick(rng, [5, 6, 7, 8, 9, 10]);
const petalDepth = Math.max(5, Math.floor(maxR * (0.6 + 0.35 * rng())));
const radialThickness = pick(rng, [1, 2, 2, 3, 3]);
const shapeModel: ShapeModel = pick(rng, [
"rose",
"starburst",
"ringed",
"tip",
"notched",
"hollow",
]);
const atomMode: AtomMode = pick(rng, [
"bands-ABC",
"angle-stripes",
"parity-ACB",
"balanced-rand",
]);
const assignAtom = makeAssigner(atomMode, petalAtoms);
// Angle tolerances
const angleTolNear =
USER_CONFIG.TOLERANCE.ANGLE_NEAR_BASE *
(USER_CONFIG.TOLERANCE.NEAR_RANGE.MIN +
rng() *
(USER_CONFIG.TOLERANCE.NEAR_RANGE.MAX -
USER_CONFIG.TOLERANCE.NEAR_RANGE.MIN));
const angleTolFar =
USER_CONFIG.TOLERANCE.ANGLE_FAR_BASE *
(USER_CONFIG.TOLERANCE.FAR_RANGE.MIN +
rng() *
(USER_CONFIG.TOLERANCE.FAR_RANGE.MAX -
USER_CONFIG.TOLERANCE.FAR_RANGE.MIN));
const softness =
USER_CONFIG.SOFTNESS.MIN +
rng() * (USER_CONFIG.SOFTNESS.MAX - USER_CONFIG.SOFTNESS.MIN);
const ringPhase =
USER_CONFIG.RING_PHASE.MIN +
rng() * (USER_CONFIG.RING_PHASE.MAX - USER_CONFIG.RING_PHASE.MIN);
// Sector gating mask (~70% sectors on), ensures bold shapes
const sectorMask = Array.from({ length: petals }, () => rng() > 0.3);
if (sectorMask.every(v => !v)) sectorMask[Math.floor(petals / 2)] = true;
const drawBgAtom = (gx: number, gy: number) => {
drawAt(
(x: number, y: number) =>
drawAtomByType(ctx, bgAtom, x, y, c1, c2, c3),
gx,
gy,
);
};
// 0) Base pass: paint every tile once with background atom to avoid gaps
function renderBasePass() {
for (let gy = 0; gy < grid; gy++) {
for (let gx = 0; gx < grid; gx++) {
drawBgAtom(gx, gy);
}
}
}
// 1) Outer edge circumference (1–2 tiles thick) reinforced in background atom
function renderEdgeReinforcement() {
const edgeThickness = pick(rng, [1, 1, 2]);
for (let t = 0; t < edgeThickness; t++) {
for (let i = 0; i < grid; i++) {
drawBgAtom(i, t);
drawBgAtom(i, grid - 1 - t);
drawBgAtom(t, i);
drawBgAtom(grid - 1 - t, i);
}
}
}
// 2) Background atom structural accents inside (deterministic)
// - mid ring (optional) and a few gated spokes to help define silhouette
function renderStructuralAccents() {
// Mid ring
if (rng() < 0.7) {
const midR = Math.floor(petalDepth * (0.5 + 0.2 * rng()));
for (let i = 0; i < grid; i++) {
const coords: [number, number][] = [
[cx - midR, i],
[cx + midR, i],
[i, cy - midR],
[i, cy + midR],
];
coords.forEach(([gx, gy]) => {
if (gx >= 0 && gy >= 0 && gx < grid && gy < grid) {
drawQuad(
(x: number, y: number) =>
drawAtomByType(ctx, bgAtom, x, y, c1, c2, c3),
gx,
gy,
grid,
);
}
});
}
}
// Gated spokes
if (rng() < 0.6) {
const gateEvery = pick(rng, [2, 3, 4]);
for (let s = 0; s < petals; s++) {
if (s % gateEvery !== 0) continue;
const theta = s * ((2 * Math.PI) / petals);
for (let r = 1; r <= petalDepth; r++) {
const gx = Math.round(cx + r * Math.cos(theta));
const gy = Math.round(cy + r * Math.sin(theta));
if (gx >= 0 && gy >= 0 && gx < grid && gy < grid) {
drawQuad(
(x: number, y: number) =>
drawAtomByType(ctx, bgAtom, x, y, c1, c2, c3),
gx,
gy,
grid,
);
}
}
}
}
}
// 3) Strong center cluster (guarantee coverage; seed form)
function renderCenterCluster() {
const centerCluster: [number, number, AtomType][] = [
[0, 0, "D"],
[0, -1, petalAtoms[0]],
[1, 0, petalAtoms[1]],
[0, 1, petalAtoms[2]],
[-1, 0, petalAtoms[0]],
];
centerCluster.forEach(([dx, dy, t]: [number, number, AtomType]) => {
const gx = cx + dx,
gy = cy + dy;
if (gx < 0 || gy < 0 || gx >= grid || gy >= grid) return;
drawQuad(
(x: number, y: number) => drawAtomByType(ctx, t, x, y, c1, c2, c3),
gx,
gy,
grid,
);
});
}
// 4) Petals (TL quadrant → mirror)
function renderPetals() {
const half = Math.ceil(grid / 2);
for (let gy = 0; gy < half; gy++) {
for (let gx = 0; gx < half; gx++) {
const { r, a } = polarFromCell(gx, gy, cx, cy);
const rCell = Math.floor(r);
if (rCell === 0 || rCell > petalDepth) continue;
const t = rCell / petalDepth;
const tol = angleTolNear * (1 - t) + angleTolFar * t;
const sectorIdx = Math.floor(a / ((2 * Math.PI) / petals));
if (!sectorGate(a, petals, sectorMask)) continue;
let inside = false;
if (shapeModel === "rose") inside = shapeRose(a, petals, tol);
else if (shapeModel === "starburst")
inside = shapeStarburst(a, petals, softness);
else if (shapeModel === "ringed")
inside = shapeRinged(a, petals, ringPhase, tol * 0.7);
else if (shapeModel === "tip") inside = shapeTip(a, petals, tol, t);
else if (shapeModel === "notched")
inside = shapeNotched(a, petals, tol, 0.24);
else inside = shapeHollow(a, petals, tol, t, 0.28, 0.92);
if (!inside) continue;
const type = assignAtom(rCell, a, sectorIdx);
// Draw with radial thickness and 4-way mirroring
for (let dr = 0; dr < radialThickness; dr++) {
const x1 = gx + dr,
y1 = gy + dr;
const coords: [number, number][] = [
[x1, y1],
[grid - 1 - x1, y1],
[x1, grid - 1 - y1],
[grid - 1 - x1, grid - 1 - y1],
];
coords.forEach(([ix, iy]: [number, number]) => {
if (ix < 0 || iy < 0 || ix >= grid || iy >= grid) return;
drawAt(
(x: number, y: number) =>
drawAtomByType(ctx, type, x, y, c1, c2, c3),
ix,
iy,
);
});
}
}
}
}
// 5) Ensure all three petal atoms appear at least once
function renderAccents() {
const accents: [number, number, AtomType][] = [
[cx, cy - 2, petalAtoms[0]],
[cx + 1, cy, petalAtoms[1]],
[cx, cy + 2, petalAtoms[2]],
];
accents.forEach(([gx, gy, t]: [number, number, AtomType]) => {
if (gx < 0 || gy < 0 || gx >= grid || gy >= grid) return;
drawQuad(
(x: number, y: number) => drawAtomByType(ctx, t, x, y, c1, c2, c3),
gx,
gy,
grid,
);
});
}
// 6) Final safety: ensure center 6×6 px is covered via 2×2 cluster
function renderFinalCluster() {
const cluster: [number, number, AtomType][] = [
[0, 0, "D"],
[1, 0, petalAtoms[1]],
[0, 1, petalAtoms[2]],
[1, 1, petalAtoms[0]],
];
cluster.forEach(([dx, dy, t]: [number, number, AtomType]) => {
const gx = cx + dx,
gy = cy + dy;
if (gx < 0 || gy < 0 || gx >= grid || gy >= grid) return;
drawQuad(
(x: number, y: number) => drawAtomByType(ctx, t, x, y, c1, c2, c3),
gx,
gy,
grid,
);
});
}
renderBasePass();
renderEdgeReinforcement();
renderStructuralAccents();
renderCenterCluster();
renderPetals();
renderAccents();
renderFinalCluster();
}
function drawFlowerForKey(
key: string,
grid: number = DEFAULT_GRID,
): string {
const canvasPx = grid * TILE;
const { canvas, ctx } = createOffscreenCanvas(
canvasPx,
canvasPx,
USER_CONFIG.PIXEL_DENSITY,
);
generateFlower(ctx, canvas, key, grid);
return canvas.toDataURL();
}
return drawFlowerForKey(key, DEFAULT_GRID);
}
}