spb/phi-conseil
Public
1/**2 * Géométrie de la spirale de Fibonacci (pure, sans React) — partagée par le3 * composant <GoldenSpiral /> et le script de génération des icônes.4 */5type Square = { x: number; y: number; s: number };6type Dir = "right" | "down" | "left" | "up";7const DIRS: Dir[] = ["right", "down", "left", "up"];89export function buildSpiral(levels = 8) {10 const fib = [1, 1];11 while (fib.length < levels) fib.push(fib[fib.length - 1] + fib[fib.length - 2]);1213 const squares: Square[] = [{ x: 0, y: 0, s: 1 }];14 const dirs: Dir[] = ["right"]; // direction "virtuelle" du premier carré15 let bbox = { x: 0, y: 0, w: 1, h: 1 };1617 for (let i = 1; i < fib.length; i++) {18 const s = fib[i];19 const d = DIRS[(i - 1) % 4];20 let sq: Square;21 if (d === "right") sq = { x: bbox.x + bbox.w, y: bbox.y, s };22 else if (d === "down") sq = { x: bbox.x, y: bbox.y + bbox.h, s };23 else if (d === "left") sq = { x: bbox.x - s, y: bbox.y, s };24 else sq = { x: bbox.x, y: bbox.y - s, s };25 squares.push(sq);26 dirs.push(d);27 const x0 = Math.min(bbox.x, sq.x);28 const y0 = Math.min(bbox.y, sq.y);29 const x1 = Math.max(bbox.x + bbox.w, sq.x + sq.s);30 const y1 = Math.max(bbox.y + bbox.h, sq.y + sq.s);31 bbox = { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };32 }3334 // Centre de l'arc d'un carré = coin situé du côté du carré précédent35 // (opposé à sa direction) et du côté de la direction suivante.36 const opposite: Record<Dir, Dir> = {37 right: "left",38 left: "right",39 up: "down",40 down: "up",41 };42 const corner = (sq: Square, a: Dir, b: Dir) => {43 const x = a === "left" || b === "left" ? sq.x : sq.x + sq.s;44 const y = a === "up" || b === "up" ? sq.y : sq.y + sq.s;45 return { x, y };46 };4748 const centers = squares.map((sq, i) => {49 if (i === 0) return { x: sq.x, y: sq.y }; // coin haut-gauche du 1er carré50 const next = i + 1 < dirs.length ? dirs[i + 1] : DIRS[i % 4];51 return corner(sq, opposite[dirs[i]], next);52 });5354 let d = "";55 let prev: { x: number; y: number } | null = null;56 squares.forEach((sq, i) => {57 const c = centers[i];58 // Les deux extrémités possibles de l'arc : à distance s sur chaque axe.59 const dx = c.x === sq.x ? sq.x + sq.s : sq.x - 0; // extrémité horizontale60 const dy = c.y === sq.y ? sq.y + sq.s : sq.y - 0;61 const pH = { x: c.x === sq.x ? dx : sq.x, y: c.y };62 const pV = { x: c.x, y: c.y === sq.y ? dy : sq.y };63 let from = pV;64 let to = pH;65 if (prev) {66 const same = (p: { x: number; y: number }) =>67 Math.abs(p.x - prev!.x) < 1e-9 && Math.abs(p.y - prev!.y) < 1e-9;68 if (same(pH)) {69 from = pH;70 to = pV;71 }72 }73 const cross = (from.x - c.x) * (to.y - c.y) - (from.y - c.y) * (to.x - c.x);74 const sweep = cross > 0 ? 1 : 0;75 if (!prev) d += `M ${from.x} ${from.y} `;76 d += `A ${sq.s} ${sq.s} 0 0 ${sweep} ${to.x} ${to.y} `;77 prev = to;78 });7980 return { squares, path: d.trim(), bbox };81}82