arrangeCircles

tcmts.share.arrangeCircles · #47rlt7fj · function · runs anywhere

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;
      }
    }
  }
}

used by