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