Public
Edited
Jan 8, 2024
1 fork
18 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function draw(c0, c1, c2, c3, opts = {}) {
let canvas_width, canvas_height, d3Canvas, context;

// Create the canvas and and set the bounds automatically
// when called the first time
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";
}
// Redraw to the canvas when called later.
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();
}
Insert cell
Insert cell
function gasket(c0, c1, c2, c3, rect) {
// A lot like Mike's function
let B = (800 * (-c0[2]) ** 0.3) / (rect[1][0] - rect[0][0]);
return [
c0,
c1,
c2,
c3,
...circles(c1, c2, c3, c0, B, rect),
...circles(c2, c3, c0, c1, B, rect),
...circles(c3, c0, c1, c2, B, rect),
...circles(c0, c1, c2, c3, B, rect)
];
}
Insert cell
function* circles(c0, c1, c2, c3, B, rect) {
// This function has substantial additions
const c = circle(c0, c1, c2, c3);
// If the curvature of c is too large,
// then we can stop.
if (!(Math.abs(c[2]) < B)) {
return;
}

// We now determine whether our viewing rectangle
// intersects the region in which c lies. If not
// we can stop.
let yield_more = false;
if (c0[2] > 0 && c1[2] > 0 && c2[2] > 0) {
let T = inner_triangle(c0, c1, c2);
if (intersect(T, rect)) {
yield_more = true;
}
} else {
if (c0[2] < 0) {
let A = c1;
let B = c2;
if (some_sameSide(A, B, c, rect)) {
yield_more = true;
}
} else if (c1[2] < 0) {
let A = c0;
let B = c2;
if (some_sameSide(A, B, c, rect)) {
yield_more = true;
}
} else if (c2[2] < 0) {
let A = c0;
let B = c1;
if (some_sameSide(A, B, c, rect)) {
yield_more = true;
}
}
}
if (yield_more) {
yield c;
yield* circles(c0, c1, c, c2, B, rect);
yield* circles(c1, c, c2, c0, B, rect);
yield* circles(c2, c, c0, c1, B, rect);
}
}
Insert cell
function circle([x0, y0, b0], [x1, y1, b1], [x2, y2, b2], [x3, y3, b3]) {
// Exactly Mike's function. Looks like it's based on Descartes' theorem
const b = 2 * b0 + 2 * b1 + 2 * b2 - b3;
return [
(2 * b0 * x0 + 2 * b1 * x1 + 2 * b2 * x2 - b3 * x3) / b,
(2 * b0 * y0 + 2 * b1 * y1 + 2 * b2 * y2 - b3 * y3) / b,
b
];
}
Insert cell
Insert cell
Insert cell
Insert cell
function tangency(c1, c2) {
let [x1, y1, k1] = c1;
let [x2, y2, k2] = c2;
let r1 = 1 / k1;
let r2 = 1 / k2;
let d = r1 + r2;
let x = (x1 * r2 + x2 * r1) / d;
let y = (y1 * r2 + y2 * r1) / d;
return [x, y];
}
Insert cell
function inner_triangle(c1, c2, c3) {
let p = tangency(c1, c2);
let q = tangency(c2, c3);
let r = tangency(c3, c1);
return [p, q, r];
}
Insert cell
function intersect(Tverts, Rverts) {
let T = new Flatten.Polygon(Tverts);
let R = new Flatten.Polygon(Rverts);

// Edges might intersect
if (T.intersect(R).length > 0) {
return true;
}

// R might fully contain T
let Tvs = T.vertices;
for (let i = 0; i < Tvs.length; i++) {
if (R.contains(Tvs[i])) {
return true;
}
}

// T might fully contain R
let Rvs = R.vertices;
for (let i = 0; i < Rvs.length; i++) {
if (T.contains(Rvs[i])) {
return true;
}
}

// Or they might not intersect at all
return false;
}
Insert cell
function some_sameSide([x0, y0], [x1, y1], [x, y], rect) {
for (let i = 0; i < rect.length; i++) {
if (sameSide([x0, y0], [x1, y1], [x, y], rect[i])) {
return true;
}
}
return false;
}
Insert cell
function sameSide([x0, y0], [x1, y1], [a, b], [c, d]) {
let ab = leftOf([x0, y0], [x1, y1], [a, b]);
let cd = leftOf([x0, y0], [x1, y1], [c, d]);
if ((ab && cd) || (!ab && !cd)) {
return true;
} else {
return false;
}
}
Insert cell
function leftOf([x0, y0], [x1, y1], [x, y]) {
return x1 * (y - y0) + x * (y0 - y1) + x0 * (y1 - y) > 0;
}
Insert cell
Insert cell
aps = [
[[0, 0, -1], [-1 / 2, 0, 2], [1 / 2, 0, 2], [0, -2 / 3, 3]],
[[0, 0, -2], [-1 / 6, 0, 3], [1 / 3, 0, 6], [3 / 14, -2 / 7, 7]],
[[0, 0, -3], [-2 / 15, 0, 5], [1 / 6, -1 / 8, 8], [1 / 6, 1 / 8, 8]],
[[0, 0, -6], [-5 / 66, 0, 11], [8 / 105, -2 / 35, 14], [3 / 50, 4 / 50, 15]],
[
[0, 0, -13],
[-10 / 299, 0, 23],
[14 / 325, -2 / 300, 30],
[22 / 1235, 9 / 190, 38]
],
[
[0, 0, -76],
[-1 / 5852, 0, 77],
[1 / 77, 0, 5852],
[2000 / 154053, -2 / 5853, 5853]
]
]
Insert cell
Insert cell
Flatten = require('@flatten-js/core')
Insert cell
import { slider } from "@jashkenas/inputs"
Insert cell
d3 = require('d3@5')
Insert cell
// html`<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/pure-min.css">
// <link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/grids-responsive-min.css">`
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more