function draw(c0, c1, c2, c3, opts = {}) {
let canvas_width, canvas_height, d3Canvas, context;
let {
canvas = "new",
bounds = "auto",
interactive = true,
show_curvatures = true,
show_more = true,
show_regions = false,
n = 0
} = opts;
if (canvas == "new") {
if (width <= 720) {
canvas_width = width;
canvas_height = width;
} else {
canvas_width = 0.7 * width;
canvas_height = 0.7 * width;
}
d3Canvas = d3
.create("canvas")
.attr("width", canvas_width)
.attr("height", canvas_height);
context = d3Canvas.node().getContext("2d");
context.textAlign = "center";
context.textBaseline = "middle";
}
else {
d3Canvas = d3.select(opts.canvas);
canvas_width = opts.canvas.width;
canvas_height = opts.canvas.height;
context = opts.canvas.getContext("2d");
}
// Set the size
context.clearRect(0, 0, canvas_width, canvas_height);
let r = -1 / c0[2];
let R = 1.1 * r;
// Set the view rectangle - either automatically
// or based on specified bounds
let xmin, xmax, ymin, ymax, rect;
if (bounds == "auto") {
xmin = -R;
xmax = R;
ymin = -R;
ymax = R;
rect = [
[xmin, ymin],
[xmax, ymin],
[xmax, ymax],
[xmin, ymax]
];
} else {
xmin = opts.xmin;
ymin = opts.ymin;
xmax = xmin + opts.dd;
ymax = ymin + opts.dd;
rect = [
[xmin, ymin],
[xmax, ymin],
[xmax, ymax],
[xmin, ymax]
];
}
// Set scale functions and path generator
let x_scale = d3.scaleLinear().domain([xmin, xmax]).range([0, canvas_width]);
let y_scale = d3.scaleLinear().domain([ymax, ymin]).range([0, canvas_height]);
let r_scale = d3
.scaleLinear()
.domain([0, (xmax - xmin) / 2])
.range([0, canvas_width / 2]);
let path = d3
.line()
.x((d) => x_scale(d[0]))
.y((d) => y_scale(d[1]));
// This option is used to generate an explantory image on zooming
// later in this notebook.
if (show_regions) {
context.beginPath();
let r = -1 / c0[2];
context.moveTo(x_scale(r), y_scale(0));
context.arc(x_scale(0), y_scale(0), r_scale(r), 0, 2 * Math.PI);
context.fillStyle = "#dddd00";
context.fill();
let [v1, v2, v3] = inner_triangle(c1, c2, c3);
let x1 = x_scale(v1[0]);
let y1 = y_scale(v1[1]);
let x2 = x_scale(v2[0]);
let y2 = y_scale(v2[1]);
let x3 = x_scale(v3[0]);
let y3 = y_scale(v3[1]);
context.beginPath();
context.moveTo(x1, y1);
context.lineTo(x2, y2);
context.lineTo(x3, y3);
context.closePath();
context.fillStyle = "#0000dd";
context.fill();
}
// Generate the gasket and store
let all_circles;
if (show_more) {
all_circles = gasket(c0, c1, c2, c3, rect);
} else {
all_circles = [c0, c1, c2, c3];
}
// Draw most of the circles
for (let [x, y, c] of all_circles.slice(4)) {
context.beginPath();
let r = 1 / c;
context.moveTo(x_scale(x + r), y_scale(y));
context.arc(x_scale(x), y_scale(y), r_scale(r), 0, 2 * Math.PI);
context.stroke();
if (show_curvatures) {
let s = r_scale(1 / (2 * c));
context.font = `${s}px sans-serif`;
context.fillText(c, x_scale(x), y_scale(y));
context.closePath();
}
}
// Draw and shade the first three interior circles
for (let [x, y, c] of [c1, c2, c3]) {
context.beginPath();
let r = 1 / c;
context.moveTo(x_scale(x + r), y_scale(y));
context.arc(x_scale(x), y_scale(y), r_scale(r), 0, 2 * Math.PI);
context.fillStyle = "#dddddd";
context.fill();
context.stroke();
if (show_curvatures) {
let s = r_scale(1 / (2 * c));
context.font = `${s}px sans-serif`;
context.fillStyle = "#000000";
context.fillText(c, x_scale(x), y_scale(y));
context.closePath();
}
}
// Draw the exterior circle
let [x, y, c] = c0;
context.beginPath();
r = Math.abs(1 / c);
context.lineWidth = 2;
context.moveTo(x_scale(x + r), y_scale(y));
context.arc(x_scale(x), y_scale(y), r_scale(r), 0, 2 * Math.PI);
context.stroke();
context.lineWidth = 1;
context.closePath();
if (show_curvatures) {
let s = 50;
context.font = `${s}px sans-serif`;
context.fillStyle = "#000000";
context.fillText(c, x_scale(-0.75 * R), y_scale(0.75 * R));
context.stroke();
context.closePath();
}
// Set up the zooming behavior
if (interactive) {
d3Canvas.call(
d3
.zoom()
.scaleExtent([0, 8])
// On zoom, we'll simply draw the circles that have already been generated
.on("zoom", function () {
context.save();
context.clearRect(0, 0, canvas_width, canvas_height);
context.beginPath();
for (const [x, y, c] of all_circles) {
context.beginPath();
const r = Math.abs(1 / c);
let [xxr, yyr] = d3.event.transform.apply([
x_scale(x + r),
y_scale(y)
]);
context.moveTo(xxr, yyr);
let [xx, yy] = d3.event.transform.apply([x_scale(x), y_scale(y)]);
context.arc(xx, yy, xxr - xx, 0, 2 * Math.PI);
context.stroke();
context.closePath();
}
for (let [x, y, c] of [c1, c2, c3]) {
context.beginPath();
const r = Math.abs(1 / c);
let [xxr, yyr] = d3.event.transform.apply([
x_scale(x + r),
y_scale(y)
]);
context.moveTo(xxr, yyr);
let [xx, yy] = d3.event.transform.apply([x_scale(x), y_scale(y)]);
context.arc(xx, yy, xxr - xx, 0, 2 * Math.PI);
context.fillStyle = "#dddddd";
context.fill();
context.stroke();
context.closePath();
}
context.restore();
})
// On zoom-end, we'll refine, regenerate, and redraw
.on("end", function () {
// Reset the window based on the transform.
let [new_xmin, new_ymin] = d3.event.transform.invert([
0,
canvas_height
]);
let [new_xmax, new_ymax] = d3.event.transform.invert([
canvas_width,
0
]);
new_xmin = x_scale.invert(new_xmin);
new_ymin = y_scale.invert(new_ymin);
new_xmax = x_scale.invert(new_xmax);
let dd = new_xmax - new_xmin;
// Draw to the existing canvas using the new view window
draw(c0, c1, c2, c3, {
canvas: d3Canvas.node(),
xmin: new_xmin,
ymin: new_ymin,
dd: dd,
bounds: "set"
});
// Reset the canvas transform since drawing is based on
// the window we just computed.
d3.zoom().transform(d3Canvas, d3.zoomIdentity);
})
);
}
return d3Canvas.node();
}