Public
Edited
Jan 8, 2024
1 fork
18 stars
A Julia set on the Riemann sphereThe Z-CurveBarnsley's fernA stochastic digraph IFS algorithmSelf-affine tilesThe TwindragonThe Eisenstein fractionsA self-affine tile with holesSelf-affine tiles via polygon mergeGolden rectangle fractalsBifurcation diagram with critical curvesThe tame twindragonIllustrations for the proof of Green's theoremNon-orientability of a Mobius stripExamples of parametric surfacesPenrose tilingThe extended unit circlePenrose three coloringNewtons's method on the Riemann sphereConic sectionsDivisor graphsThe dance of Earth and VenusIterating multiples of the sine functionBorderline fractalsSelf-similar intersectionsBox-counting dimension examplesMandelbrot by dimensionInverse iteration for quadratic Julia sets
Integer Apollonian Packings
Illustrations of two-dimensonal heat flowThe logistic bifurcation locusThe eleven unfoldings of the cubeA unimodal function with fractal level curvesGreen's theorem and polygonal areaThe geometry and numerics of first order ODEsThe xxx^xxx-spindleAnimated beatsRauzy FractalsHilbert's coordinate functionsPluckNot PiDrum strikeThe Koch snowflakeFractalized squareA Taylor series about π/4\pi/4π/4PlotX3D HyperboloidA PlotX3D animationModular arithmetic in 5th grade artSimple S-I-R ModelThe Poisson KernelPoly-gasketsClassification of 2D linear systems via trace and determinantJulia sets and the Mandelbrot setWater wavesFourier SeriesDisks for a solid of revolutionOrbit detection for the Mandelbrot setTracing a path on a spherePlot for mathematiciansFunctions of two variablesPartial derivativesDijkstra's algorithm on an RGGGradient ascentUnfolding polyhedraTangent plane to a level surfaceA strange discontinuityExamples of level surfacesMcMullen carpetsHills and valleysThe definition of ⇒Double and iterated integralsMST in an RGGTrees are bipartiteFractal typesettingd3.hierarchy and d3.treeK23 is PlanarPolar CoordinatesParametric region generatorParametric Plot 2DContour plotsGreedy graph coloringGraph6A few hundred interesting graphsThe Kings ProblemFirst order, autonomous systems of ODEsRunge-Kutta for systems of ODEs
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