block by renecnielsen 690bedde3f2b60a47895

Apollonius’ Problem II

Full Screen

Apollonius’ problem is to compute the circle that is tangent to three given circles. There are up to eight such circles.

Drag the circles to see the tangent circles change.

forked from mbostock‘s block: Apollonius’ Problem II

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.circle {
  fill-opacity: .5;
}

.ring {
  fill: none;
  stroke: #000;
  pointer-events: none;
}

.ring-inner {
  stroke-width: 5px;
  stroke-opacity: .25;
}

</style>
<svg width="960" height="500"></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script>

function apolloniusCircle(x1, y1, r1, x2, y2, r2, x3, y3, r3) {

  // The quadratic equation (1):
  //
  //          0 = (x - x1)² + (y - y1)² - (r ± r1)²
  //          0 = (x - x2)² + (y - y2)² - (r ± r2)²
  //          0 = (x - x3)² + (y - y3)² - (r ± r3)²
  //
  // Use a negative radius to choose a different circle.
  // We must rewrite this in standard form Ar² + Br + C = 0 to solve for r.
  // Per //mathworld.wolfram.com/ApolloniusProblem.html

  var a2 = 2 * (x1 - x2),
      b2 = 2 * (y1 - y2),
      c2 = 2 * (r2 - r1),
      d2 = x1 * x1 + y1 * y1 - r1 * r1 - x2 * x2 - y2 * y2 + r2 * r2,
      a3 = 2 * (x1 - x3),
      b3 = 2 * (y1 - y3),
      c3 = 2 * (r3 - r1),
      d3 = x1 * x1 + y1 * y1 - r1 * r1 - x3 * x3 - y3 * y3 + r3 * r3;

  // Giving:
  //
  //          x = (b2 * d3 - b3 * d2 + (b3 * c2 - b2 * c3) * r) / (a3 * b2 - a2 * b3)
  //          y = (a3 * d2 - a2 * d3 + (a2 * c3 - a3 * c2) * r) / (a3 * b2 - a2 * b3)
  //
  // Expand x - x1, substituting definition of x in terms of r.
  //
  //     x - x1 = (b2 * d3 - b3 * d2 + (b3 * c2 - b2 * c3) * r) / (a3 * b2 - a2 * b3) - x1
  //            = (b2 * d3 - b3 * d2) / (a3 * b2 - a2 * b3) + (b3 * c2 - b2 * c3) / (a3 * b2 - a2 * b3) * r - x1
  //            = bd / ab + bc / ab * r - x1
  //            = xa + xb * r
  //
  // Where:

  var ab = a3 * b2 - a2 * b3,
      xa = (b2 * d3 - b3 * d2) / ab - x1,
      xb = (b3 * c2 - b2 * c3) / ab;

  // Likewise expand y - y1, substituting definition of y in terms of r.
  //
  //     y - y1 = (a3 * d2 - a2 * d3 + (a2 * c3 - a3 * c2) * r) / (a3 * b2 - a2 * b3) - y1
  //            = (a3 * d2 - a2 * d3) / (a3 * b2 - a2 * b3) + (a2 * c3 - a3 * c2) / (a3 * b2 - a2 * b3) * r - y1
  //            = ad / ab + ac / ab * r - y1
  //            = ya + yb * r
  //
  // Where:

  var ya = (a3 * d2 - a2 * d3) / ab - y1,
      yb = (a2 * c3 - a3 * c2) / ab;

  // Expand (x - x1)², (y - y1)² and (r - r1)²:
  //
  //  (x - x1)² = xb * xb * r² + 2 * xa * xb * r + xa * xa
  //  (y - y1)² = yb * yb * r² + 2 * ya * yb * r + ya * ya
  //  (r - r1)² = r² - 2 * r1 * r + r1 * r1.
  //
  // Substitute back into quadratic equation (1):
  //
  //          0 = xb * xb * r² + yb * yb * r² - r²
  //              + 2 * xa * xb * r + 2 * ya * yb * r + 2 * r1 * r
  //              + xa * xa + ya * ya - r1 * r1
  //
  // Rewrite in standard form Ar² + Br + C = 0,
  // then plug into the quadratic formula to solve for r, x and y.

  var A = xb * xb + yb * yb - 1,
      B = 2 * (xa * xb + ya * yb + r1),
      C = xa * xa + ya * ya - r1 * r1,
      r = (-B - Math.sqrt(B * B - 4 * A * C)) / (2 * A);
  return isNaN(r) ? null : {x: xa + xb * r + x1, y: ya + yb * r + y1, r: r < 0 ? -r : r};
}

var c1 = {x: 380, y: 250, r: 80},
    c2 = {x: 600, y: 100, r: 20},
    c3 = {x: 600, y: 300, r: 120};

var color = d3.scale.category10();

var svg = d3.select("svg");

var circle = svg.selectAll(".circle")
    .data([c1, c2, c3])
  .enter().append("g")
    .attr("class", "circle")
    .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
    .call(d3.behavior.drag()
        .origin(function(d) { return d; })
        .on("dragstart", dragstarted)
        .on("drag", dragged));

circle.append("circle")
    .attr("r", function(d) { return d.r; });

var ring = svg.selectAll(".ring")
    .data([
      function() { return apolloniusCircle(c1.x, c1.y, +c1.r, c2.x, c2.y, +c2.r, c3.x, c3.y, +c3.r); },
      function() { return apolloniusCircle(c1.x, c1.y, +c1.r, c2.x, c2.y, +c2.r, c3.x, c3.y, -c3.r); },
      function() { return apolloniusCircle(c1.x, c1.y, +c1.r, c2.x, c2.y, -c2.r, c3.x, c3.y, +c3.r); },
      function() { return apolloniusCircle(c1.x, c1.y, +c1.r, c2.x, c2.y, -c2.r, c3.x, c3.y, -c3.r); },
      function() { return apolloniusCircle(c1.x, c1.y, -c1.r, c2.x, c2.y, +c2.r, c3.x, c3.y, +c3.r); },
      function() { return apolloniusCircle(c1.x, c1.y, -c1.r, c2.x, c2.y, +c2.r, c3.x, c3.y, -c3.r); },
      function() { return apolloniusCircle(c1.x, c1.y, -c1.r, c2.x, c2.y, -c2.r, c3.x, c3.y, +c3.r); },
      function() { return apolloniusCircle(c1.x, c1.y, -c1.r, c2.x, c2.y, -c2.r, c3.x, c3.y, -c3.r); }
    ])
  .enter().insert("g", ".circle")
    .attr("class", "ring")
    .style("stroke", function(d, i) { return color(i); });

ring.append("circle")
    .attr("class", "ring-outer");

ring.append("circle")
    .attr("class", "ring-inner");

update();

function dragstarted(d) {
  this.parentNode.appendChild(this);
}

function dragged(d) {
  d3.select(this).attr("transform", "translate(" + (d.x = d3.event.x) + "," + (d.y = d3.event.y) + ")");
  update();
}

function update() {
  ring.each(function(f) {
    var c = f();
    if (c) {
      var ring = d3.select(this).style("display", null).attr("transform", "translate(" + c.x + "," + c.y + ")");
      ring.select(".ring-inner").attr("r", c.r - 3);
      ring.select(".ring-outer").attr("r", c.r);
    } else {
      d3.select(this).style("display", "none");
    }
  });
}

</script>