function draw(c0, c1, c2, c3, opts={}) {
let {
bounds = 'auto',
interactive = true,
show_curvatures = true,
show_more = true,
show_regions = false,
n = 0
} = opts;
let { d3Canvas, context, canvas_width, canvas_height } = createCanvas(opts);
context.clearRect(0, 0, canvas_width, canvas_height);
let R = -1.1 / Number(c0[2]);
let xcenter = 0, ycenter = 0;
if (bounds == 'set') {
xcenter = opts.xcenter;
ycenter = opts.ycenter;
R = opts.R;
for (let circle of [c0, c1, c2, c3]) {
circle[0] -= xcenter;
circle[1] -= ycenter;
}
}
let x_scale = d3
.scaleLinear()
.domain([-R, R])
.range([0, canvas_width]);
let y_scale = d3
.scaleLinear()
.domain([R, -R])
.range([0, canvas_height]);
let r_scale = d3
.scaleLinear()
.domain([0, R])
.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 explanatory image on zooming
// later in this notebook.
if (show_regions) {
context.beginPath();
let r = -1 / Number(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 scaling_xy = (v) => [x_scale(v[0]), y_scale(v[1])]
let [x1, y1] = scaling_xy(v1);
let [x2, y2] = scaling_xy(v2);
let [x3, y3] = scaling_xy(v3);
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 = show_more ? gasket(c0, c1, c2, c3, R) : [c0, c1, c2, c3];
let divisor = 2n;
let to_text = (k) => {
if (!factor_out) return String(k);
let i = 0n;
while (k % divisor == 0n) { i++; k = k / divisor;}
if (i == 0n) return String(k);
let power = i == 1n ? '' : String(i).split('').map(d => '⁰¹²³⁴⁵⁶⁷⁸⁹'.split('')[d]).join('');
if (k == 1n) return String(divisor) + power;
return String(divisor) + power + '·' + k;
}
let to_textColor = (k) => {
if (isPrimeMemo(k)) {
return 'red';
} else if (factor_out) {
while (k % divisor == 0n) k = k / divisor;
if (isPrimeMemo(k)) return 'blue';
}
return 'black';
}
// Draw most of the circles
drawCircles(context, all_circles.slice(4), x_scale, y_scale, r_scale,
'transparent', show_curvatures, to_text, to_textColor);
// Draw and shade the first three interior circles
drawCircles(context, [c1, c2, c3], x_scale, y_scale, r_scale,
'#dddddd', show_curvatures, to_text, to_textColor);
// Draw the exterior circle
drawCircles(context, [c0], x_scale, y_scale, r_scale,
'transparent', false);
if (show_curvatures) {
let [x, y, k] = c0;
let r = 1 / Number(-k);
context.font = `50px Times New Roman`;
context.fillStyle = to_textColor(-k);
context.fillText(k, x_scale(x-0.85*r), y_scale(y+0.85*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();
// drawCircles(context, d3.event.transform.apply(all_circles), x_scale, y_scale, r_scale,
// 'transparent', false);
for (const [x, y, k] of all_circles) {
context.beginPath();
const r = Math.abs(1 / Number(k));
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, k] of [c1, c2, c3]) {
context.beginPath();
const r = Math.abs(1 / Number(k));
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);
new_ymax = new_ymin + new_xmax - new_xmin;
// Draw to the existing canvas using the new view window
draw(c0, c1, c2, c3, {
canvas: d3Canvas.node(),
xcenter: (new_xmax + new_xmin) / 2,
ycenter: (new_ymax + new_ymin) / 2,
R: (new_xmax - new_xmin) / 2,
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();
}