arrangeCircles
Place circles so the more two are linked (links as [a, b, weight]) the closer they sit, none overlapping (gap
apart), inside within if it's given, or else spread aspect times as wide as tall: each one's x and y are set.
export function arrangeCircles( items: { r: number; x: number; y: number }[], links: [number, number, number][], o: { within?: { x: number; y: number; r: number }; gap?: number; steps?: number; aspect?: number; } = {}, ): void { const n = items.length; const gap = o.gap ?? 10; const steps = o.steps ?? 600; const aspect = o.within ? 1 : (o.aspect ?? 1); const cx = o.within?.x ?? 0; const cy = o.within?.y ?? 0; // (a sunflower's spiral to start from, so the same circles always land the same way) items.forEach((it, j) => { const a = j * 2.399963; const d = o.within ? o.within.r * 0.5 * Math.sqrt((j + 0.5) / n) : 40 * Math.sqrt(j + 1); it.x = cx + Math.cos(a) * d; it.y = cy + Math.sin(a) * d; }); const most = Math.max(1, ...links.map((l) => l[2])); for (let s = 0; s < steps; s++) { const heat = 1 - s / steps; const fx = new Float64Array(n); const fy = new Float64Array(n); for (const [a, b, w] of links) { const A = items[a]!; const B = items[b]!; const dx = B.x - A.x; const dy = B.y - A.y; const d = Math.hypot(dx, dy) || 1; const f = (d - (A.r + B.r + gap)) * 0.02 * Math.sqrt(w / most); fx[a]! += (dx / d) * f; fy[a]! += (dy / d) * f; fx[b]! -= (dx / d) * f; fy[b]! -= (dy / d) * f; } for (let a = 0; a < n; a++) { for (let b = a + 1; b < n; b++) { const A = items[a]!; const B = items[b]!; const dx = B.x - A.x; const dy = B.y - A.y; const d = Math.hypot(dx, dy) || 0.01; const min = A.r + B.r + gap; if (d >= min) continue; const f = (min - d) * 0.5; fx[a]! -= (dx / d) * f; fy[a]! -= (dy / d) * f; fx[b]! += (dx / d) * f; fy[b]! += (dy / d) * f; } } for (let a = 0; a < n; a++) { const A = items[a]!; // (held to the middle more from above and below than from the sides: a wide picture, as screens are) A.x += (fx[a]! + ((cx - A.x) * 0.006) / aspect) * (0.3 + heat); A.y += (fy[a]! + (cy - A.y) * 0.006 * aspect) * (0.3 + heat); if (!o.within) continue; const dx = A.x - cx; const dy = A.y - cy; const d = Math.hypot(dx, dy); const max = Math.max(0, o.within.r - A.r - 4); if (d > max && d > 0) { A.x = cx + (dx / d) * max; A.y = cy + (dy / d) * max; } } } }