Published
Edited
May 11, 2019
10 stars
Insert cell
Insert cell
{
const w = 600, h = 600;
const maxDepth = 9, speed = .003;
// End points of the starting triangle's hypothenuse.
const p1 = [w/3, h/2], p2 = [w-(w/3), h/2];
const sd = depth => depth/maxDepth;
//const color = d => `hsla(0,0%,${sd(d)*100}%,.8)`;
// Takes a t offset, returns a function that scales t by depth.
const scaleT = t => d => t + (1-sd(d))*.4;
const ctx = DOM.context2d(w, h);
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, w, h);
// Anything other than "round" will cause spikes at narrow angles.
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
ctx.lineWidth = 2;
ctx.strokeStyle = 'hsla(0,0%,100%,.5)';
const [x1, y1] = p1, [x2, y2] = p2;
let t = .21;
while(true) {
const layers = Array.from({length: maxDepth + 1}).map(() => []);
for(let tri of split(x1, y1, x2, y2, maxDepth, scaleT(t))) {
layers[tri.depth].push(tri);
}
for(let tri of split(x1, y1, x2, y2, maxDepth, scaleT(t+1))) {
layers[tri.depth].push(tri);
}
ctx.fillStyle = 'hsla(0,0%,0%,1)';
ctx.fillRect(0, 0, w, h);
layers
.filter((l, d) => d < 3 || d > 5)
//.reverse()
//.slice(-5)
.forEach((l, d) => {
//if((d+1)%2) return;
ctx.beginPath();
for(let i = 0; i < l.length; i++) {
ctx.moveTo(l[i].x1, l[i].y1);
ctx.lineTo(l[i].x3, l[i].y3);
ctx.lineTo(l[i].x2, l[i].y2);
ctx.lineTo(l[i].x1, l[i].y1);
}
// Some paths overlap, causing clipping/flickering.
// But filling each triangle separately drags performance
// down considerably.
//ctx.fillStyle = color(d);
//ctx.fill();
ctx.stroke();
});
yield ctx.canvas;
t = (t + speed) % 1;
}
}
Insert cell
function* split(x1, y1, x2, y2, depth, tFn) {
const tri = Object.assign(triangle(x1, y1, x2, y2, depth, tFn), {depth});
yield tri;
if(depth > 0) {
yield* split(tri.x1, tri.y1, tri.x3, tri.y3, depth - 1, tFn);
yield* split(tri.x3, tri.y3, tri.x2, tri.y2, depth - 1, tFn);
}
}
Insert cell
function triangle(x1, y1, x2, y2, depth, tFn) {
const cx = (x2 - x1) / 2;
const cy = (y2 - y1) / 2;
const r = (cx ** 2 + cy ** 2) ** .5;
const a = Math.PI * (1 + tFn(depth)) + Math.atan2(cy, cx);
// Why an object? Because other metrics like angle or radius
// could be added and used during rendering.
return {
x1, y1, x2, y2,
x3: x1 + cx + Math.cos(a) * r,
y3: y1 + cy + Math.sin(a) * r,
}
}
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