The Fifth Root

An imaginary root that still insists on growing

Log in to post a comment.

// The Fifth Root
// An imaginary root that still insists on growing.
// Ported from https://codepen.io/ge1doot/pen/aRXbYK/ec522e6900a5db94438859fe159c7d8f
// ge1doot 1st March, 2026

Canvas.setpenopacity(0.75);
const turtle = new Turtle();
turtle.penup();
const DEG = Math.PI / 180;

const fillMin = 10;
const hatchAngle = 0.35;
const hatchStep = 0.25;

function line(x0, y0, x1, y1) {
	turtle.penup();
	turtle.goto(x0, y0);
	turtle.pendown();
	turtle.goto(x1, y1);
}

// RNG (Park-Miller)
const random = function RNG() {
	let seed = 1 + Math.floor(Math.random() * 2147483646);
	function next() {
		seed = (seed * 16807) % 2147483647;
		return (seed - 1) / 2147483646;
	}
	return next;
}();

// Mat2D stack (Canvas2D-like)
const mat = function Mat2D() {
	const stack = new Float32Array(128 * 6);
	let sp = 0;
	const m = new Float32Array(6);

	function set(a, b, c, d, e, f) {
		m[0] = a; m[1] = b; m[2] = c; m[3] = d; m[4] = e; m[5] = f;
	}
	function push() {
		stack[sp + 0] = m[0];
		stack[sp + 1] = m[1];
		stack[sp + 2] = m[2];
		stack[sp + 3] = m[3];
		stack[sp + 4] = m[4];
		stack[sp + 5] = m[5];
		sp += 6;
	}
	function pop() {
		sp -= 6;
		m[0] = stack[sp + 0];
		m[1] = stack[sp + 1];
		m[2] = stack[sp + 2];
		m[3] = stack[sp + 3];
		m[4] = stack[sp + 4];
		m[5] = stack[sp + 5];
	}
	function translate(x, y) {
		m[4] += x * m[0] + y * m[2];
		m[5] += x * m[1] + y * m[3];
	}
	function rotate(deg) {
		const r = deg * DEG;
		const cos = Math.cos(r);
		const sin = Math.sin(r);
		const a = m[0], b = m[1], c = m[2], d = m[3];
		m[0] = a * cos + c * sin;
		m[1] = b * cos + d * sin;
		m[2] = c * cos - a * sin;
		m[3] = d * cos - b * sin;
	}
	return { current: m, set, push, pop, translate, rotate };
}();

// Quad buffer
const quad = function Quad(mat) {
	const STRIDE = 9;
	const data = [];
	const m = mat.current;
	let sFit = 1, txFit = 0, tyFit = 0;

	function emit(size) {
		const x0 = m[4], y0 = m[5];
		const x1 = m[0] * size + m[4], y1 = m[1] * size + m[5];
		const x2 = m[0] * size + m[2] * size + m[4], y2 = m[1] * size + m[3] * size + m[5];
		const x3 = m[2] * size + m[4], y3 = m[3] * size + m[5];
		// Enforce CCW
		const area =
			x0 * y1 - y0 * x1 +
			x1 * y2 - y1 * x2 +
			x2 * y3 - y2 * x3 +
			x3 * y0 - y3 * x0;
		if (area < 0) {
			const tx = x1, ty = y1;
			x1 = x3; y1 = y3;
			x3 = tx; y3 = ty;
		}
		data.push(x0, y0, x1, y1, x2, y2, x3, y3, size);
	}
	
	function emitWorld(x0, y0, x1, y1, x2, y2, x3, y3, size) {
    	const area =
    		x0 * y1 - y0 * x1 +
    		x1 * y2 - y1 * x2 +
    		x2 * y3 - y2 * x3 +
    		x3 * y0 - y3 * x0;
    
    	if (area < 0) {
    		const tx = x1, ty = y1;
    		x1 = x3; y1 = y3;
    		x3 = tx; y3 = ty;
    	}
    	data.push(x0, y0, x1, y1, x2, y2, x3, y3, size);
    }

	function fitToPlot(pad, yBase) {
		let minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9;
		for (let i = 0; i < data.length; i += STRIDE) {
			for (let k = 0; k < 8; k += 2) {
				const x = data[i + k];
				const y = data[i + k + 1];
				if (x < minX) minX = x;
				if (x > maxX) maxX = x;
				if (y < minY) minY = y;
				if (y > maxY) maxY = y;
			}
		}
		const w = maxX - minX;
		const h = maxY - minY;
		sFit = Math.min((200 - pad * 2) / w, (200 - pad * 2) / h);
		txFit = -0.5 * (minX + maxX) * sFit;
		tyFit = yBase - minY * sFit;
		for (let i = 0; i < data.length; i += STRIDE) {
			for (let k = 0; k < 8; k += 2) {
				data[i + k] = data[i + k] * sFit + txFit;
				data[i + k + 1] = data[i + k + 1] * sFit + tyFit;
			}
		}
	}
	
	function projectFitY(y) { 
	    return y * sFit + tyFit
	}

	function drawOutlines(line) {
		for (let i = 0; i < data.length; i += STRIDE) {
			const x0 = data[i + 0], y0 = data[i + 1];
			const x1 = data[i + 2], y1 = data[i + 3];
			const x2 = data[i + 4], y2 = data[i + 5];
			const x3 = data[i + 6], y3 = data[i + 7];
			line(x0, y0, x1, y1);
			line(x1, y1, x2, y2);
			line(x2, y2, x3, y3);
			line(x3, y3, x0, y0);
		}
	}

	function prepare(scanline, keep, out) {
		return scanline.prepare(data, STRIDE, keep, out);
	}
	return { emit, emitWorld, fitToPlot, projectFitY, drawOutlines, prepare };
}(mat);

// Scanline XOR engine :)
const scanline = function Scanline() {
	const QSTRIDE = 10;
	const eps = 1e-12;
	let hx = 1, hy = 0, nx = 0, ny = 1;
	let step = 0.25;
	const ua = [];
	const ub = [];
	const segA = [];
	const segB = [];
	const segOut = [];

	function setup(angle, hatchStep) {
		hx = Math.cos(angle);
		hy = Math.sin(angle);
		nx = -hy;
		ny = hx;
		step = hatchStep;
	}

	function pushU(xa, ya, xb, yb, s, out) {
		const da = xa * nx + ya * ny - s;
		const db = xb * nx + yb * ny - s;
		if (da * da < eps && db * db < eps) return;
		if (da * da < eps) { out.push(xa * hx + ya * hy); return; }
		if (db * db < eps) { out.push(xb * hx + yb * hy); return; }
		if (da > 0 && db > 0) return;
		if (da < 0 && db < 0) return;
		const t = da / (da - db);
		const x = xa + (xb - xa) * t;
		const y = ya + (yb - ya) * t;
		out.push(x * hx + y * hy);
	}

	function buildSegments(uArr, out) {
		uArr.sort((a, b) => a - b);
		out.length = 0;
		for (let i = 0; i + 1 < uArr.length; i += 2) {
			const a = uArr[i];
			const b = uArr[i + 1];
			if (b - a > 1e-6) out.push(a, b);
		}
	}

	function subtractSegments(aSeg, bSeg, out) {
		out.length = 0;
		let j = 0;
		for (let i = 0; i < aSeg.length; i += 2) {
			let a0 = aSeg[i];
			const a1 = aSeg[i + 1];
			while (j < bSeg.length && bSeg[j + 1] <= a0) j += 2;
			let k = j;
			while (k < bSeg.length) {
				const b0 = bSeg[k];
				const b1 = bSeg[k + 1];
				if (b0 >= a1) break;
				if (b0 > a0) out.push(a0, Math.min(b0, a1));
				a0 = Math.max(a0, b1);
				if (a0 >= a1) break;
				k += 2;
			}
			if (a0 < a1) out.push(a0, a1);
		}
	}

	function prepare(shapes, stride, keep, out) {
		let sMin = 1e9, sMax = -1e9;
		for (let i = 0; i < shapes.length; i += stride) {
			const size = shapes[i + 8];
			if (!keep(size)) continue
			const x0 = shapes[i + 0], y0 = shapes[i + 1];
			const x1 = shapes[i + 2], y1 = shapes[i + 3];
			const x2 = shapes[i + 4], y2 = shapes[i + 5];
			const x3 = shapes[i + 6], y3 = shapes[i + 7];
			let mn = 1e9, mx = -1e9;
			const s0 = x0 * nx + y0 * ny; if (s0 < mn) mn = s0; if (s0 > mx) mx = s0;
			const s1 = x1 * nx + y1 * ny; if (s1 < mn) mn = s1; if (s1 > mx) mx = s1;
			const s2 = x2 * nx + y2 * ny; if (s2 < mn) mn = s2; if (s2 > mx) mx = s2;
			const s3 = x3 * nx + y3 * ny; if (s3 < mn) mn = s3; if (s3 > mx) mx = s3;
			out.push(x0, y0, x1, y1, x2, y2, x3, y3, mn, mx);
			if (mn < sMin) sMin = mn;
			if (mx > sMax) sMax = mx;
		}
		return [sMin, sMax];
	}

	function gatherU(list, s, outU) {
		outU.length = 0;
		for (let i = 0; i < list.length; i += QSTRIDE) {
			const mn = list[i + 8];
			const mx = list[i + 9];
			if (s < mn || s > mx) continue;
			const x0 = list[i + 0], y0 = list[i + 1];
			const x1 = list[i + 2], y1 = list[i + 3];
			const x2 = list[i + 4], y2 = list[i + 5];
			const x3 = list[i + 6], y3 = list[i + 7];
			pushU(x0, y0, x1, y1, s, outU);
			pushU(x1, y1, x2, y2, s, outU);
			pushU(x2, y2, x3, y3, s, outU);
			pushU(x3, y3, x0, y0, s, outU);
		}
	}

	function xorFill(line, fills, cutters, sMin, sMax) {
		for (let s = sMin - step; s <= sMax + step; s += step) {
			gatherU(fills, s, ua);
			if (ua.length < 2) continue;
			buildSegments(ua, segA);
			gatherU(cutters, s, ub);
			if (ub.length >= 2) {
				buildSegments(ub, segB);
				subtractSegments(segA, segB, segOut);
			} else {
				segOut.length = segA.length;
				for (let i = 0; i < segA.length; i++) segOut[i] = segA[i];
			}
			for (let i = 0; i < segOut.length; i += 2) {
				const u0 = segOut[i];
				const u1 = segOut[i + 1];
				line(hx * u0 + nx * s, hy * u0 + ny * s, hx * u1 + nx * s, hy * u1 + ny * s);
			}
		}
	}
	return { setup, prepare, xorFill };
}();

// Build tree
function branch(size, angleDeg) {
	quad.emit(size);
	if (size < 3) return;
	const rad = angleDeg * DEG;
	const v1 = size * Math.cos(rad);
	const v2 = size * Math.sin(rad);
	
	mat.push();
	mat.translate(size, 0);
	mat.rotate(angleDeg);
	mat.translate(-v1, -v1);
	branch(v1, angleDeg + (random() - random()) * 15);
	mat.pop();

	mat.push();
	mat.rotate(angleDeg - 90);
	mat.translate(0, -v2);
	branch(v2, angleDeg + (random() - random()) * 15);
	mat.pop();
}
/////////// start ///////////
const rootSize = 70;
mat.set(1, 0, 0, 1, -0.5 * rootSize, rootSize);
branch(rootSize, 15 + random() * 60);
// Fit + outlines
quad.fitToPlot(5, -92);
// ground
const ground = quad.projectFitY(rootSize * 2);
quad.emitWorld(-100, ground + 0.25, 100, ground + 0.25, 100, 100, -100, 100, 1000);
// draw outlines
quad.drawOutlines(line);
// Scanline fill
scanline.setup(hatchAngle, hatchStep);
const fills = [];
const smalls = [];
const r0 = quad.prepare(scanline, s => s >= fillMin, fills);
const r1 = quad.prepare(scanline, s => s < fillMin, smalls);
const sMin = Math.min(r0[0], r1[0]);
const sMax = Math.max(r0[1], r1[1]);
scanline.xorFill(line, fills, smalls, sMin, sMax);
// end