Log in to post a comment.

Canvas.setpenopacity(1);

/* Turtletoy build (ESM -> ESM.define/require) */

const ESM = (() => {
    'use strict';
	const modules = Object.create(null);

	function define(id, factory) {
		modules[id] = { factory, exports: null };
	}

	function require(id) {
		const m = modules[id];
		if (m.exports) return m.exports;
		m.exports = Object.create(null);
		m.factory(m.exports, require);
		return m.exports;
	}

	return { define, require };
})();

ESM.define("engine.js", (exports, require) => {
	'use strict';
	/* ========================= engine.js ========================= */
	const { zbInit, zbClear, zbTri, zbGet } = require("zbuffer.js");

	let turtle = null;
	let FOV = 0.5;
	let CAM_Z = 11;
	let OUT_SCALE = 22;
	let OUT_OX = 0;
	let OUT_OY = 0;
	let ZW = 1200;
	let ZH = 1200;
	let ZBUF = null;
	
	function drawLine (x0, y0, x1, y1) {
		const was = turtle.isdown();
		turtle.penup();
		turtle.goto(x0, y0);
		turtle.pendown();
		turtle.goto(x1, y1);
		if (!was) turtle.penup();
	};

	const cubeQueue = [];
	const cubePool = {
		state: [],
		top: 0,
		alloc() {
			return this.top ? this.state[--this.top] : new Float32Array(8 * 3);
		},
		free(a) {
			this.state[this.top++] = a;
		}
	};

	let ZB_OX = 100;
	let ZB_OY = 100;
	let ZB_SX = 1;
	let ZB_SY = 1;

	const EPS_Z = 1e-9;

	let seed = (Math.random() * 1000000) | 0;

	function randomSeed(s) {
		s = Number(s);
		if (seed === s) return;
		seed = s;
		noiseSeed(s);
	}
	
	function random01() {
		seed = (seed * 16807) % 2147483647;
		return (seed - 1) / 2147483646;
	}
	
	function random(a = 1, b) {
		const t = random01();
		return (b === undefined) ? t * a : a + (b - a) * t;
	}

	// ----- Perlin noise (Processing) 
	const PERLIN_YWRAPB = 4;
	const PERLIN_YWRAP = 1 << PERLIN_YWRAPB;
	const PERLIN_ZWRAPB = 8;
	const PERLIN_ZWRAP = 1 << PERLIN_ZWRAPB;
	const PERLIN_SIZE = 4095;

	const perlin = new Float32Array(PERLIN_SIZE + 1);
	let perlinOctaves = 4;
	let perlinAmpFalloff = 0.5;

	function noiseSeed(s) {
		const save = seed;
		seed = s;
		for (let i = 0; i <= PERLIN_SIZE; i++) perlin[i] = random01();
		seed = save;
	}

	noiseSeed(seed);

	function noiseFsc(t) {
		return 0.5 * (1 - Math.cos(Math.PI * t));
	}

	function noise(x, y = 0, z = 0) {
		if (x < 0) x = -x;
		if (y < 0) y = -y;
		if (z < 0) z = -z;

		let xi = x | 0, yi = y | 0, zi = z | 0;
		let xf = x - xi;
		let yf = y - yi;
		let zf = z - zi;

		let r = 0;
		let ampl = 0.5;

		for (let o = 0; o < perlinOctaves; o++) {
			let of = xi + (yi << PERLIN_YWRAPB) + (zi << PERLIN_ZWRAPB);

			const rxf = noiseFsc(xf);
			const ryf = noiseFsc(yf);

			let n1 = perlin[of & PERLIN_SIZE];
			n1 += rxf * (perlin[(of + 1) & PERLIN_SIZE] - n1);
			let n2 = perlin[(of + PERLIN_YWRAP) & PERLIN_SIZE];
			n2 += rxf * (perlin[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n2);
			n1 += ryf * (n2 - n1);

			of += PERLIN_ZWRAP;
			n2 = perlin[of & PERLIN_SIZE];
			n2 += rxf * (perlin[(of + 1) & PERLIN_SIZE] - n2);
			let n3 = perlin[(of + PERLIN_YWRAP) & PERLIN_SIZE];
			n3 += rxf * (perlin[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n3);
			n2 += ryf * (n3 - n2);

			n1 += noiseFsc(zf) * (n2 - n1);

			r += n1 * ampl;
			ampl *= perlinAmpFalloff;

			xi <<= 1; xf *= 2;
			yi <<= 1; yf *= 2;
			zi <<= 1; zf *= 2;

			if (xf >= 1) { xi++; xf--; }
			if (yf >= 1) { yi++; yf--; }
			if (zf >= 1) { zi++; zf--; }
		}

		return r;
	}

	function noiseDetail(lod = 4, falloff = 0.5) {
		if (lod > 0) perlinOctaves = lod | 0;
		if (falloff > 0) perlinAmpFalloff = falloff;
	}

	const matStack = [];
	const matPool = {
		state: [],
		top: 0,
		alloc() {
			return this.top ? this.state[--this.top] : new Float32Array(16);
		},
		free(m) {
			this.state[this.top++] = m;
		}
	};
	let sp = 0;

	const CUBE_V = new Float32Array([
		-0.5, -0.5, -0.5,
		0.5, -0.5, -0.5,
		0.5, 0.5, -0.5,
		-0.5, 0.5, -0.5,
		-0.5, -0.5, 0.5,
		0.5, -0.5, 0.5,
		0.5, 0.5, 0.5,
		-0.5, 0.5, 0.5,
	]);

	const FACE_Q = new Uint8Array([
		4, 5, 6, 7,
		1, 0, 3, 2,
		5, 1, 2, 6,
		0, 4, 7, 3,
		7, 6, 2, 3,
		0, 1, 5, 4
	]);

	const EDGE_V = new Uint8Array([
		0, 1, 1, 2, 2, 3, 3, 0,
		4, 5, 5, 6, 6, 7, 7, 4,
		0, 4, 1, 5, 2, 6, 3, 7
	]);

	const P = new Float32Array(8 * 3);
	const faceQueue = [];
    const c = 65536;
    let segX0 = new Float32Array(c);
    let segY0 = new Float32Array(c);
    let segX1 = new Float32Array(c);
    let segY1 = new Float32Array(c);
    let segN = 0;
    
    let segKx0 = new Int32Array(c);
    let segKy0 = new Int32Array(c);
    let segKx1 = new Int32Array(c);
    let segKy1 = new Int32Array(c);
    
    let segUx = new Float32Array(c);
    let segUy = new Float32Array(c);
    let segNx = new Float32Array(c);
    let segNy = new Float32Array(c);
    let segC  = new Float32Array(c);
    let segT0 = new Float32Array(c);
    let segT1 = new Float32Array(c);
    
    function segAuxEnsure(n) {
    	const cap = segKx0.length;
    	if (n <= cap) return;
    	let ncap = cap;
    	while (ncap < n) ncap <<= 1;
    
    	{
    		const a = new Int32Array(ncap); a.set(segKx0); segKx0 = a;
    		const b = new Int32Array(ncap); b.set(segKy0); segKy0 = b;
    		const c = new Int32Array(ncap); c.set(segKx1); segKx1 = c;
    		const d = new Int32Array(ncap); d.set(segKy1); segKy1 = d;
    	}
    	{
    		const a = new Float32Array(ncap); a.set(segUx); segUx = a;
    		const b = new Float32Array(ncap); b.set(segUy); segUy = b;
    		const c = new Float32Array(ncap); c.set(segNx); segNx = c;
    		const d = new Float32Array(ncap); d.set(segNy); segNy = d;
    		const e = new Float32Array(ncap); e.set(segC);  segC  = e;
    		const f = new Float32Array(ncap); f.set(segT0); segT0 = f;
    		const g = new Float32Array(ncap); g.set(segT1); segT1 = g;
    	}
    }
    
    function hash4i(a, b, c, d) {
    	let h = (a * 73856093) ^ (b * 19349663) ^ (c * 83492791) ^ (d * 2654435761);
    	return h >>> 0;
    }
    
    function hash3i(a, b, c) {
    	let h = (a * 73856093) ^ (b * 19349663) ^ (c * 83492791);
    	return h >>> 0;
    }
    
    function segReset() {
    	segN = 0;
    }
    
    function segEnsure(n) {
    	const cap = segX0.length;
    	if (n <= cap) return;
    	let ncap = cap;
    	while (ncap < n) ncap <<= 1;
    	const x0 = new Float32Array(ncap);
    	const y0 = new Float32Array(ncap);
    	const x1 = new Float32Array(ncap);
    	const y1 = new Float32Array(ncap);
    	x0.set(segX0); y0.set(segY0); x1.set(segX1); y1.set(segY1);
    	segX0 = x0; segY0 = y0; segX1 = x1; segY1 = y1;
    }
    
    function segPush(x0, y0, x1, y1) {
    	segEnsure(segN + 1);
    	segX0[segN] = x0; segY0[segN] = y0;
    	segX1[segN] = x1; segY1[segN] = y1;
    	segN++;
    }

    function segFlush() {
    	const n0 = segN;
    	if (!n0) return;
    
    	segAuxEnsure(n0);
    
    	const Q = 10000;
    	const seen = new Map();
    	let n = 0;
    
    	for (let i = 0; i < n0; i++) {
    		let x0 = segX0[i], y0 = segY0[i], x1 = segX1[i], y1 = segY1[i];
    		if (x0 > x1 || (x0 === x1 && y0 > y1)) {
    			const tx = x0; x0 = x1; x1 = tx;
    			const ty = y0; y0 = y1; y1 = ty;
    		}
    
    		const kx0 = Math.round(x0 * Q) | 0;
    		const ky0 = Math.round(y0 * Q) | 0;
    		const kx1 = Math.round(x1 * Q) | 0;
    		const ky1 = Math.round(y1 * Q) | 0;
    
    		const h = hash4i(kx0, ky0, kx1, ky1);
    		let list = seen.get(h);
    		let dup = 0;
    		if (list) {
    			for (let j = 0; j < list.length; j++) {
    				const k = list[j];
    				if (segKx0[k] === kx0 && segKy0[k] === ky0 && segKx1[k] === kx1 && segKy1[k] === ky1) {
    					dup = 1;
    					break;
    				}
    			}
    		} else {
    			list = [];
    			seen.set(h, list);
    		}
    		if (dup) continue;
    
    		segX0[n] = x0; segY0[n] = y0;
    		segX1[n] = x1; segY1[n] = y1;
    		segKx0[n] = kx0; segKy0[n] = ky0;
    		segKx1[n] = kx1; segKy1[n] = ky1;
    		list.push(n);
    		n++;
    	}
    
    	if (!n) return;
    
    	const qDir = 1e-3;
    	const qC = 1e-3;
    	const buckets = new Map();
    	const MERGE_GAP = 5e-4;
    
    	for (let i = 0; i < n; i++) {
    		const x0 = segX0[i], y0 = segY0[i], x1 = segX1[i], y1 = segY1[i];
    		const dx = x1 - x0;
    		const dy = y1 - y0;
    		const len = Math.hypot(dx, dy);
    		if (len < 1e-8) continue;
    
    		let ux = dx / len;
    		let uy = dy / len;
    		if (ux < 0 || (ux === 0 && uy < 0)) { ux = -ux; uy = -uy; }
    
    		const nx = -uy;
    		const ny = ux;
    		const c = nx * x0 + ny * y0;
    		const t0 = ux * x0 + uy * y0;
    		const t1 = ux * x1 + uy * y1;
    
    		segUx[i] = ux; segUy[i] = uy;
    		segNx[i] = nx; segNy[i] = ny;
    		segC[i] = c;
    		segT0[i] = t0;
    		segT1[i] = t1;
    
    		const k0 = Math.round(ux / qDir) | 0;
    		const k1 = Math.round(uy / qDir) | 0;
    		const k2 = Math.round(c / qC) | 0;
    		const h = hash3i(k0, k1, k2);
    
    		let b = buckets.get(h);
    		if (!b) {
    			b = { k0, k1, k2, idx: [] };
    			buckets.set(h, b);
    		} else if (b.k0 !== k0 || b.k1 !== k1 || b.k2 !== k2) {
    			// rare hash collision, linear probe via small chain
    			let chain = b.chain;
    			if (!chain) { chain = []; b.chain = chain; }
    			let hit = null;
    			for (let j = 0; j < chain.length; j++) {
    				const bb = chain[j];
    				if (bb.k0 === k0 && bb.k1 === k1 && bb.k2 === k2) { hit = bb; break; }
    			}
    			if (!hit) { hit = { k0, k1, k2, idx: [] }; chain.push(hit); }
    			b = hit;
    		}
    
    		b.idx.push(i);
    	}
    
    	for (const b of buckets.values()) {
    		const first = b;
    		const list0 = first.idx;
    		if (list0.length) {
    			list0.sort((ia, ib) => segT0[ia] - segT0[ib]);
    			mergeBucket(list0);
    		}
    		const chain = first.chain;
    		if (chain) {
    			for (let ci = 0; ci < chain.length; ci++) {
    				const list = chain[ci].idx;
    				if (!list.length) continue;
    				list.sort((ia, ib) => segT0[ia] - segT0[ib]);
    				mergeBucket(list);
    			}
    		}
    	}
    
    	function mergeBucket(arr) {
    		let i0 = arr[0];
    		let t0 = segT0[i0];
    		let t1 = segT1[i0];
    		const ux = segUx[i0], uy = segUy[i0];
    		const nx = segNx[i0], ny = segNy[i0];
    		const c = segC[i0];
    
    		for (let j = 1; j < arr.length; j++) {
    			const i = arr[j];
    			const s0 = segT0[i];
    			const s1 = segT1[i];
    			if (s0 <= t1 + MERGE_GAP) {
    				if (s1 > t1) t1 = s1;
    				continue;
    			}
    			emitMerged(ux, uy, nx, ny, c, t0, t1);
    			t0 = s0;
    			t1 = s1;
    		}
    		emitMerged(ux, uy, nx, ny, c, t0, t1);
    	}
    
    	function emitMerged(ux, uy, nx, ny, c, t0, t1) {
    		const ax = ux * t0 + nx * c;
    		const ay = uy * t0 + ny * c;
    		const bx = ux * t1 + nx * c;
    		const by = uy * t1 + ny * c;
    		drawLine(ax, ay, bx, by);
    	}
    }

	function mat4Identity(m) {
		m[0] = 1; m[1] = 0; m[2] = 0; m[3] = 0;
		m[4] = 0; m[5] = 1; m[6] = 0; m[7] = 0;
		m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0;
		m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1;
		return m;
	}
	
	function mat4Copy(out, a) { out.set(a); return out; }
	
	function mat4Translate(m, x, y, z) {
		m[12] += m[0] * x + m[4] * y + m[8] * z;
		m[13] += m[1] * x + m[5] * y + m[9] * z;
		m[14] += m[2] * x + m[6] * y + m[10] * z;
		return m;
	}
	
	function mat4Scale(m, x, y, z) {
		m[0] *= x; m[1] *= x; m[2] *= x;
		m[4] *= y; m[5] *= y; m[6] *= y;
		m[8] *= z; m[9] *= z; m[10] *= z;
		return m;
	}
	
	function mat4RotateX(m, a) {
		const s = Math.sin(a), c = Math.cos(a);
		const a4 = m[4], a5 = m[5], a6 = m[6];
		const a8 = m[8], a9 = m[9], a10 = m[10];
		m[4] = a4 * c + a8 * s;
		m[5] = a5 * c + a9 * s;
		m[6] = a6 * c + a10 * s;
		m[8] = a8 * c - a4 * s;
		m[9] = a9 * c - a5 * s;
		m[10] = a10 * c - a6 * s;
		return m;
	}
	
	function mat4RotateY(m, a) {
		const s = Math.sin(a), c = Math.cos(a);
		const a0 = m[0], a1 = m[1], a2 = m[2];
		const a8 = m[8], a9 = m[9], a10 = m[10];
		m[0] = a0 * c - a8 * s;
		m[1] = a1 * c - a9 * s;
		m[2] = a2 * c - a10 * s;
		m[8] = a0 * s + a8 * c;
		m[9] = a1 * s + a9 * c;
		m[10] = a2 * s + a10 * c;
		return m;
	}
	
	function mat4RotateZ(m, a) {
		const s = Math.sin(a), c = Math.cos(a);
		const a0 = m[0], a1 = m[1], a2 = m[2];
		const a4 = m[4], a5 = m[5], a6 = m[6];
		m[0] = a0 * c + a4 * s;
		m[1] = a1 * c + a5 * s;
		m[2] = a2 * c + a6 * s;
		m[4] = a4 * c - a0 * s;
		m[5] = a5 * c - a1 * s;
		m[6] = a6 * c - a2 * s;
		return m;
	}

	function pushMatrix() {
		const m = matPool.alloc();
		if (sp) mat4Copy(m, matStack[sp - 1]);
		else mat4Identity(m);
		matStack[sp++] = m;
	}
	function popMatrix() {
		if (sp <= 1) return;
		matPool.free(matStack[--sp]);
	}
	
	function currentMatrix() { return matStack[sp - 1]; }

	function transformPoint(out3, m, x, y, z) {
		out3[0] = m[0] * x + m[4] * y + m[8] * z + m[12];
		out3[1] = m[1] * x + m[5] * y + m[9] * z + m[13];
		out3[2] = m[2] * x + m[6] * y + m[10] * z + m[14];
	}

	function buildCubeAndRaster(m) {
		const cube = cubePool.alloc();

		for (let i = 0; i < 8; i++) {
			const ix = i * 3;
			transformPoint(
				P.subarray(ix, ix + 3),
				m,
				CUBE_V[ix + 0],
				CUBE_V[ix + 1],
				CUBE_V[ix + 2]
			);
		}

		for (let i = 0; i < 8; i++) {
			const ix = i * 3;
			const x = P[ix + 0], y = P[ix + 1], z = P[ix + 2];

			const denom = CAM_Z - z;
			const o = i * 3;

			if (denom <= 0.01) {
				cube[o + 2] = 0;
				continue;
			}

			const invW = 1 / denom;
			const k = (1 / Math.tan(FOV * 0.5)) * invW;

			cube[o + 0] = (x * k) * OUT_SCALE + OUT_OX;
			cube[o + 1] = (y * k) * OUT_SCALE + OUT_OY;
			cube[o + 2] = invW;
		}

		rasterCubeFaces(cube);
		cubeQueue.push(cube);
	}

	function rasterCubeFaces(cube) {
		for (let f = 0; f < 6; f++) {
			const ia = FACE_Q[f * 4 + 0];
			const ib = FACE_Q[f * 4 + 1];
			const ic = FACE_Q[f * 4 + 2];
			const id = FACE_Q[f * 4 + 3];

			const a = ia * 3, b = ib * 3, c = ic * 3, d = id * 3;

			const ax = cube[a + 0], ay = cube[a + 1], az = cube[a + 2];
			const bx = cube[b + 0], by = cube[b + 1], bz = cube[b + 2];
			const cx = cube[c + 0], cy = cube[c + 1], cz = cube[c + 2];
			const dx = cube[d + 0], dy = cube[d + 1], dz = cube[d + 2];

			if (!az || !bz || !cz || !dz) continue;

			zbTri(ax, ay, az, bx, by, bz, cx, cy, cz);
			zbTri(ax, ay, az, cx, cy, cz, dx, dy, dz);
		}
	}

	function emitCubeVisibleEdges(cube) {
		for (let e = 0; e < EDGE_V.length; e += 2) {
			const a = EDGE_V[e + 0] * 3;
			const b = EDGE_V[e + 1] * 3;

			const ax = cube[a + 0], ay = cube[a + 1], az = cube[a + 2];
			const bx = cube[b + 0], by = cube[b + 1], bz = cube[b + 2];

			if (!az || !bz) continue;

			emitVisibleEdge(ax, ay, az, bx, by, bz);
		}
	}

	function emitVisibleEdge(ax, ay, az, bx, by, bz) {
		const pax = (ax + ZB_OX) * ZB_SX;
		const pay = (ay + ZB_OY) * ZB_SY;
		const pbx = (bx + ZB_OX) * ZB_SX;
		const pby = (by + ZB_OY) * ZB_SY;

		let x0 = pax, y0 = pay;
		let x1 = pbx, y1 = pby;

		let dx = x1 - x0;
		let dy = y1 - y0;

		const adx = Math.abs(dx);
		const ady = Math.abs(dy);

		let steps = Math.ceil(Math.max(adx, ady));
		if (steps < 1) steps = 1;

		const invSteps = 1 / steps;
		const GAP_MAX = 1;

		let run = 0;
		let t0 = 0;
		let gap = 0;

		for (let i = 0; i <= steps; i++) {
			const t = i * invSteps;

			const px = x0 + dx * t;
			const py = y0 + dy * t;

			let ix = Math.floor(px);
			let iy = Math.floor(py);

			if (ix < 0) ix = 0;
			else if (ix >= ZW) ix = ZW - 1;

			if (iy < 0) iy = 0;
			else if (iy >= ZH) iy = ZH - 1;

			const z = az + (bz - az) * t;

			let vis = 0;

			let base = iy * ZW + ix;
			if (z >= ZBUF[base] - EPS_Z) vis = 1;

			if (!vis) {
				const fx = px - ix;
				if (fx < 0.001 && ix > 0) {
					if (z >= ZBUF[base - 1] - EPS_Z) vis = 1;
				} else if (fx > 0.999 && ix + 1 < ZW) {
					if (z >= ZBUF[base + 1] - EPS_Z) vis = 1;
				}
			}

			if (!vis) {
				const fy = py - iy;
				if (fy < 0.001 && iy > 0) {
					if (z >= ZBUF[base - ZW] - EPS_Z) vis = 1;
				} else if (fy > 0.999 && iy + 1 < ZH) {
					if (z >= ZBUF[base + ZW] - EPS_Z) vis = 1;
				}
			}

			if (vis) {
				if (!run) { run = 1; t0 = t; }
				gap = 0;
			} else if (run) {
				gap++;
				if (gap > GAP_MAX) {
					const t1 = t - gap * invSteps;
					if (t1 > t0) emitEdgeSegment(ax, ay, bx, by, t0, t1);
					run = 0;
					gap = 0;
				}
			}
		}

		if (run) {
			const t1 = 1 - gap * invSteps;
			if (t1 > t0) emitEdgeSegment(ax, ay, bx, by, t0, t1);
		}
	}

	function emitEdgeSegment(ax, ay, bx, by, t0, t1) {
		const x0 = ax + (bx - ax) * t0;
		const y0 = ay + (by - ay) * t0;
		const x1 = ax + (bx - ax) * t1;
		const y1 = ay + (by - ay) * t1;

		const dx = x1 - x0;
		const dy = y1 - y0;
		if (dx * dx + dy * dy < 1e-6) return;

		segPush(x0, y0, x1, y1);
	}

	function radians(d) { return d * Math.PI / 180; }
	function rotateXDeg(d) { mat4RotateX(currentMatrix(), radians(d)); return api; }
	function rotateYDeg(d) { mat4RotateY(currentMatrix(), radians(d)); return api; }
	function rotateZDeg(d) { mat4RotateZ(currentMatrix(), radians(d)); return api; }

	const api = {
		setOutputTransform(scale, ox, oy) { OUT_SCALE = scale; OUT_OX = ox; OUT_OY = oy; return this; },
		perspective(fov) { FOV = fov; return this; },
		cameraZ(z) { CAM_Z = z; return this; },
		beginFrame() {
			zbClear();
			cubeQueue.length = 0;
			segReset();
			sp = 0;
			pushMatrix();
			return this;
		},
		endFrame() {
			for (let i = 0; i < cubeQueue.length; i++) {
				emitCubeVisibleEdges(cubeQueue[i]);
				cubePool.free(cubeQueue[i]);
			}
			cubeQueue.length = 0;
			segFlush();
			return this;
		},
		pushMatrix() { pushMatrix(); return this; },
		popMatrix() { popMatrix(); return this; },
		translate(x, y, z = 0) { mat4Translate(currentMatrix(), x, y, z); return this; },
		scale(x, y = x, z = x) { mat4Scale(currentMatrix(), x, y, z); return this; },
		rotateX(a) { mat4RotateX(currentMatrix(), a); return this; },
		rotateY(a) { mat4RotateY(currentMatrix(), a); return this; },
		rotateZ(a) { mat4RotateZ(currentMatrix(), a); return this; },
		rotateXDeg,
		rotateYDeg,
		rotateZDeg,
		radians,
		randomSeed(s) { randomSeed(s); return this; },
		noiseSeed(s) { noiseSeed(s); return this; },
		random,
		lerp(a, b, t) { return a + (b - a) * t; },
		constrain(x, a, b) { return Math.max(a, Math.min(b, x)); },
		noiseDetail(lod = 4, ampFalloff = 0.5) { noiseDetail(lod, ampFalloff); return this; },
		noise,
		box(w, h, d) {
			const m = matPool.alloc();
			mat4Copy(m, currentMatrix());
			mat4Scale(m, w, h, d);
			buildCubeAndRaster(m);
			matPool.free(m);
			return this;
		}
	};

	function initEngine(t, cfg = {}) {
		turtle = t;
		if (cfg.fov != null) FOV = cfg.fov;
		if (cfg.camZ != null) CAM_Z = cfg.camZ;
		if (cfg.outScale != null) OUT_SCALE = cfg.outScale;
		if (cfg.outOx != null) OUT_OX = cfg.outOx;
		if (cfg.outOy != null) OUT_OY = cfg.outOy;

		zbInit({ w: ZW, h: ZH, x0: -100, y0: -100, x1: 100, y1: 100 });
		ZBUF = zbGet();
		ZB_SX = ZW / 200;
		ZB_SY = ZH / 200;

		return api;
	}

	exports.initEngine = initEngine;
});

ESM.define("zbuffer.js", (exports, require) => {
	'use strict';
	/* ========================= zbuffer.js ========================= */
	let w = 0, h = 0;
	let depth = null;

	let x0 = -100, y0 = -100;
	let sx = 1, sy = 1;

	function zbInit(cfg = {}) {
		w = cfg.w | 0;
		h = cfg.h | 0;
		if (w <= 0) w = 200;
		if (h <= 0) h = 200;

		x0 = (cfg.x0 != null) ? cfg.x0 : -100;
		y0 = (cfg.y0 != null) ? cfg.y0 : -100;

		const x1 = (cfg.x1 != null) ? cfg.x1 : 100;
		const y1 = (cfg.y1 != null) ? cfg.y1 : 100;

		sx = w / (x1 - x0);
		sy = h / (y1 - y0);

		depth = new Float32Array(w * h);
		zbClear();
	}

	function zbClear() {
		const d = depth;
		for (let i = 0; i < d.length; i++) d[i] = -Infinity;
	}

	function toPxX(x) { return (x - x0) * sx; }
	function toPxY(y) { return (y - y0) * sy; }

	function zbTri(ax, ay, az, bx, by, bz, cx, cy, cz) {
		ax = toPxX(ax); ay = toPxY(ay);
		bx = toPxX(bx); by = toPxY(by);
		cx = toPxX(cx); cy = toPxY(cy);

		const abx = bx - ax, aby = by - ay;
		const acx = cx - ax, acy = cy - ay;
		const area2 = abx * acy - aby * acx;
		if (area2 <= 0) return;

		let minX = ax; if (bx < minX) minX = bx; if (cx < minX) minX = cx;
		let maxX = ax; if (bx > maxX) maxX = bx; if (cx > maxX) maxX = cx;
		let minY = ay; if (by < minY) minY = by; if (cy < minY) minY = cy;
		let maxY = ay; if (by > maxY) maxY = by; if (cy > maxY) maxY = cy;

		let ix0 = Math.floor(minX);
		let ix1 = Math.ceil(maxX);
		let iy0 = Math.floor(minY);
		let iy1 = Math.ceil(maxY);

		if (ix1 < 0 || iy1 < 0 || ix0 >= w || iy0 >= h) return;

		if (ix0 < 0) ix0 = 0;
		if (iy0 < 0) iy0 = 0;
		if (ix1 > w - 1) ix1 = w - 1;
		if (iy1 > h - 1) iy1 = h - 1;

		const invArea = 1 / area2;
		const d = depth;

		const A = (az * (by - cy) + bz * (cy - ay) + cz * (ay - by)) * invArea;
		const B = (az * (cx - bx) + bz * (ax - cx) + cz * (bx - ax)) * invArea;
		const C = (az * (bx * cy - cx * by) + bz * (cx * ay - ax * cy) + cz * (ax * by - bx * ay)) * invArea;

		const bias = 0.5 * (Math.abs(A) + Math.abs(B)) + 1e-8;

		for (let y = iy0; y <= iy1; y++) {
			const py = y + 0.5;
			for (let x = ix0; x <= ix1; x++) {
				const px = x + 0.5;

				const e0 = (bx - ax) * (py - ay) - (by - ay) * (px - ax);
				const e1 = (cx - bx) * (py - by) - (cy - by) * (px - bx);
				const e2 = (ax - cx) * (py - cy) - (ay - cy) * (px - cx);

				if (e0 < 0 || e1 < 0 || e2 < 0) continue;

				const z = A * px + B * py + C;
				const zb = z - bias;

				const id = y * w + x;
				if (zb > d[id]) d[id] = zb;
			}
		}
	}

	function zbGet() {
		return depth;
	}
	exports.zbInit = zbInit;
	exports.zbClear = zbClear;
	exports.zbTri = zbTri;
	exports.zbGet = zbGet;

});

ESM.define("sketch.js", (exports, require) => {
	'use strict';
	/* processing sketch port (match original PDE as closely as possible)
	
	float time;
	
	float rnoise(float x) {
	  float t1 = time * 4;
	  float t2 = (time - 1) * 4;
	  float n1 = noise(x, t1);
	  float n2 = noise(x, t2);
	  float n = lerp(n1, n2, time);
	  return constrain(n * 5 - 0.2, 0, 1);
	}
	
	void setup() {
	  size(500, 500, P3D);
	  colorMode(RGB, 1);
	  noiseDetail(1);
	}
	
	void draw() {
	  int totalFrames = 144 * 6;
	  time = (float)frameCount / totalFrames;
	  
	  randomSeed(132);
	  
	  background(1);
	  
	  perspective(0.5, 1, 0.01, 100);
	  
	  camera(
	  0, 0, 11,
	  0, 0, 0,
	  0, 1, 0
	  );
	
	  rotateX(-0.5 + sin(PI * 4 * time) * 0.1);
	  rotateY(-0.5 + sin(PI * 2 * time) * 0.2);
	  
	  drawSurface(0, 0);
	  drawSurface(90, 0);
	  drawSurface(180, 0);
	  drawSurface(-90, 0);
	  drawSurface(90, 90);
	  drawSurface(90, -90);
	
	}
	
	void drawSurface(float rx, float ry) {
	  int columns = 2;
	
	  pushMatrix();
	  
	  rotateY(radians(ry));
	  rotateX(radians(rx));
	
	  float offs = 0.5 * columns - 0.5;
	  translate(-offs, -0.5 * columns, -offs);
	  
	  for (int ix = 0; ix < columns; ix++) {
	  for (int iy = 0; iy < columns; iy++) {
		pushMatrix();
		translate(ix, 0, iy);
	    
		float w = random(0.95, 1.0);
		float h = 0.2;
		drawUnit(w, h, 1, (int)random(4, 6));
	    
		popMatrix();
	  }
	  }
	  
	  popMatrix();
	}
	
	void drawUnit(float w, float h, int lv, int maxLv) {
	  if (lv >= maxLv) return;
	  
	  translate(0, -0.5 * h, 0);
	  box(w, h, w);
	  translate(0, -0.5 * h, 0);
	
	  for (int i = 0; i < 4; i++) {
	  float w2 = w * 0.5 * random(0.9, 1);
	  float h2 = h * random(0.1, 1.75);
	    
	  h2 *= rnoise(random(0, 10));
	    
	  float dx = w * 0.5 * ((i / 2) - 0.5);
	  float dy = w * 0.5 * ((i & 1) - 0.5);
	    
	  int maxLv2 = maxLv + (int)random(-1, 1.1);
	    
	  pushMatrix();
	  translate(dx, 0, dy);
	  drawUnit(w2, h2, lv + 1, maxLv2);
	  popMatrix();
	  }
	}*/

	let p = null;
	let seed = 1;
	let time = 1;
	
	function setup(eng, cfg = {}) {
		p = eng;
		seed = cfg.seed ? cfg.seed : (Math.random() * 10000) | 0;
		p.randomSeed(seed);
		p.noiseDetail(1, 0.5);
	}

	function rnoise(x) {
		const t1 = time * 4;
		const t2 = (time - 1) * 4;
		const n1 = p.noise(x, t1, 0);
		const n2 = p.noise(x, t2, 0);
		const n = p.lerp(n1, n2, time);
		return p.constrain(n * 5 - 0.2, 0, 1);
	}

	function drawSurface(rxDeg, ryDeg) {
		const columns = 2;

		p.pushMatrix();
		p.rotateYDeg(ryDeg);
		p.rotateXDeg(rxDeg);

		const offs = 0.5 * columns - 0.5;
		p.translate(-offs, -0.5 * columns, -offs);

		for (let ix = 0; ix < columns; ix++) {
			for (let iy = 0; iy < columns; iy++) {
				p.pushMatrix();
				p.translate(ix, 0, iy);
				const w = p.random(0.95, 1.0);
				const h = 0.2;
				drawUnit(w, h, 1, Math.floor(p.random(4, 7)));
				p.popMatrix();
			}
		}

		p.popMatrix();
	}

	function drawUnit(w, h, lv, maxLv) {
		if (lv >= maxLv) return;

		p.translate(0, -0.5 * h, 0);
		p.box(w, h, w);
		p.translate(0, -0.5 * h, 0);

		for (let i = 0; i < 4; i++) {
			const w2 = w * 0.5 * p.random(0.9, 1.0);
			let h2 = h * p.random(0.2, 2);
			h2 *= rnoise(p.random(0, 10));
			const dx = w * 0.5 * (((i / 2) | 0) - 0.5);
			const dz = w * 0.5 * ((i & 1) - 0.5);
			let maxLv2 = maxLv + Math.floor(p.random(-1, 1.1));
            if (maxLv2 < lv + 1) maxLv2 = lv + 1;
			p.pushMatrix();
			p.translate(dx, 0, dz);
			drawUnit(w2, h2, lv + 1, maxLv2);
			p.popMatrix();
		}
	}

	function draw() {
		p.randomSeed(seed);
		p.beginFrame();
        p.rotateX(p.random(-Math.PI, Math.PI));
        p.rotateY(p.random(-Math.PI, Math.PI));
		drawSurface(0, 0);
		drawSurface(90, 0);
		drawSurface(180, 0);
		drawSurface(-90, 0);
		drawSurface(90, 90);
		drawSurface(90, -90);
		p.endFrame();
	};
	exports.setup = setup;
	exports.draw = draw;

});

// ----- boot -----
const { initEngine } = ESM.require("engine.js");
const sketch = ESM.require("sketch.js");
const seed = ""; // type=string, Enter your seed! (Empty = random)

const turtle = new Turtle();
turtle.penup();

const p = initEngine(turtle, {
	fov: 0.5,
	camZ: 11,
	outScale: 100,
});

sketch.setup(p, { seed });
sketch.draw(0);