Zoomable Serpent Ski

2026-10-20

As you wish

As a friend of mine pointed out, a fractal is more fun if you can zoom in on it! Rather than handle dragging & zooming myself, I’ll reach for Konva. A little longer this time, even with the help of Konva’s infinite canvas (edit: Apologies, I can’t seem to get pinch & zoom on mobile working, so this is only with the mouse wheel {for now?}):

const STAGE = new Konva.Stage({
	container: "chaos",
	width: window.innerWidth,
	height: window.innerHeight,
	draggable: true,
});
Konva.hitOnDragEnabled = true;
const LAYER = new Konva.Layer();
STAGE.add(LAYER);
const [XS, YS] = ((b) => {
	const s = 0.75 * window.innerWidth;
	const h = (s * Math.sqrt(3)) / 2;
	return [
		[b + 0, b + s / 2, b + s],
		[b + h, b + 0, b + h],
	];
})(10);
let [X, Y, ON] = [0, 0, null];
const new_xy = () => {
	const [u, v] = [Math.sqrt(Math.random()), Math.random()];
	X = (1 - u) * XS[0] + u * (1 - v) * XS[1] + u * v * XS[2];
	Y = (1 - u) * YS[0] + u * (1 - v) * YS[1] + u * v * YS[2];
};
const draw = (x, y, radius = 2) => {
	LAYER.add(
		new Konva.Circle({
			x,
			y,
			radius,
			fill: "#027f8d",
		}),
	);
};
XS.forEach((x, i) => draw(x, YS[i], 4));
document.getElementById("reset").addEventListener("click", () => {
	ON = clearInterval(ON);
	LAYER.destroyChildren();
	STAGE.scaleX(1);
	STAGE.scaleY(1);
	STAGE.position({ x: 0, y: 0 });
	XS.forEach((x, i) => draw(x, YS[i], 4));
});
document.getElementById("start_stop").addEventListener("click", () => {
	if (!ON) {
		new_xy();
		ON = setInterval(() => {
			const i = Math.floor(XS.length * Math.random());
			X += 0.5 * (XS[i] - X);
			Y += 0.5 * (YS[i] - Y);
			draw(X, Y);
		});
	} else {
		ON = clearInterval(ON);
	}
});
STAGE.on("wheel", (e) => {
	e.evt.preventDefault();
	const dir = (e.evt.deltaY > 0 ? -1 : 1) * (e.evt.ctrlKey ? -1 : 1);
	const old = STAGE.scaleX();
	const scale = Math.max(0.1, Math.min(10, dir > 0 ? old * 1.05 : old / 1.05));
	STAGE.scale({ x: scale, y: scale });
	const p = STAGE.getPointerPosition();
	STAGE.position({
		x: p.x - scale * ((p.x - STAGE.x()) / old),
		y: p.y - scale * ((p.y - STAGE.y()) / old),
	});
});