Sierpiński curve animation JavaScript
/**
* Originally written on ObservableHQ, later translated to Vanilla JS
* https://observablehq.com/@forresto/sierpinski-curve-animation
*
* Inspired by
* - This 1972 computer animation from Nelson Max's Topology Films Project - https://youtu.be/2e8QJBkCwvo?t=1028
* - Nadieh Bremer and piratefsh's versions (the curve drawing code is forked from them) - https://observablehq.com/@nbremer/sierpinski-curve
* - Eddie Woo's lecture on how the curve is constructed by subdividing a square into isosceles right triangles - https://youtu.be/Ps9itT9KcdM
*
* Post: https://www.forresto.com/2020s/sierpinski.html
* Copyright ©️ 2020 Forrest O. Interactive
*/
// Constants
const MIN = 0;
const MAX = 11;
const DELTA = MAX - MIN;
const SIZE = 720;
const PADDING = 8;
const SECONDS_PER = 3;
const TIME_PER = SECONDS_PER * 1000;
const TIME_TOTAL = TIME_PER * DELTA;
// Utility functions
function midpoint(a, b) {
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
}
function line(points) {
return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p[0]},${p[1]}`).join(" ");
}
function subdivideTriangle(triangle, iterations = 1) {
const [p1, p2, p3] = triangle;
const points = [];
const triangles = [];
if (iterations === 0) {
// Find center of current triangle
const centroid = [(p1[0] + p2[0] + p3[0]) / 3, (p1[1] + p2[1] + p3[1]) / 3];
points.push(centroid);
triangles.push(triangle);
} else {
const mid = midpoint(p1, p3);
const sub1 = [p1, mid, p2];
const sub1d = subdivideTriangle(sub1, iterations - 1);
points.push(...sub1d.points);
triangles.push(...sub1d.triangles);
const sub2 = [p2, mid, p3];
const sub2d = subdivideTriangle(sub2, iterations - 1);
points.push(...sub2d.points);
triangles.push(...sub2d.triangles);
}
return { points, triangles };
}
function sierpinskiCurve(len, iterations) {
const triangle_1 = [
[0, len],
[0, 0],
[len, 0],
];
const triangle_2 = [
[len, 0],
[len, len],
[0, len],
];
const half1 = subdivideTriangle(triangle_1, iterations);
const half2 = subdivideTriangle(triangle_2, iterations);
const points = [...half1.points, ...half2.points];
const triangles = [...half1.triangles, ...half2.triangles];
return { points, triangles };
}
function doubleArray(arr) {
return arr.flatMap((item) => [item, item]);
}
function interpolatePoints(pointsA, pointsB, t) {
return pointsA.map((a, i) => [
a[0] + (pointsB[i][0] - a[0]) * t,
a[1] + (pointsB[i][1] - a[1]) * t,
]);
}
// Setup SVG
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute(
"viewBox",
`${-PADDING} ${-PADDING} ${SIZE + PADDING * 2} ${SIZE + PADDING * 2}`,
);
svg.setAttribute("width", SIZE);
svg.setAttribute("height", SIZE);
svg.style.backgroundColor = "rgb(135, 0, 35)";
const gridPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
gridPath.setAttribute("fill", "none");
gridPath.setAttribute("stroke", "rgb(215, 88, 134)");
gridPath.setAttribute("stroke-width", "1");
const curvePath = document.createElementNS("http://www.w3.org/2000/svg", "path");
curvePath.setAttribute("fill", "none");
curvePath.setAttribute("stroke", "rgb(251, 139, 192)");
curvePath.setAttribute("stroke-width", "4");
svg.appendChild(gridPath);
svg.appendChild(curvePath);
document.body.prepend(svg);
// Animation
let startTime = Date.now();
function animate() {
const elapsed = Date.now() - startTime;
const playhead = Math.abs(((elapsed + TIME_TOTAL) % (TIME_TOTAL * 2)) - TIME_TOTAL);
const interp = DELTA * (playhead / TIME_TOTAL);
const interpIndex = Math.floor(interp);
const interpRemainder = interp % 1;
const smoothRemainder = Math.sin(interpRemainder * Math.PI + Math.PI / 2) / -2 + 0.5;
if (interpIndex < DELTA) {
const fromPoints = doubleArray(sierpinskiCurve(SIZE, interpIndex + MIN).points);
const toPoints = sierpinskiCurve(SIZE, interpIndex + MIN + 1).points;
const currentPoints = interpolatePoints(fromPoints, toPoints, smoothRemainder);
curvePath.setAttribute("d", line(currentPoints) + "Z");
// Update grid
const triangles = sierpinskiCurve(SIZE, Math.ceil(interp) + MIN).triangles;
const gridPathData = triangles
.map(
(tri) =>
`M ${tri[0][0]},${tri[0][1]} L ${tri[1][0]},${tri[1][1]} L ${tri[2][0]},${tri[2][1]} Z `,
)
.join("");
gridPath.setAttribute("d", gridPathData);
}
requestAnimationFrame(animate);
}
animate();
// Make code viewable
const scriptEl = document.getElementById("sierpinski-script");
const codeEl = document.getElementById("sierpinski-code");
codeEl.textContent = scriptEl.textContent;