Fork: Initial contact II

I woke up inside a geometry I didn’t remember drawing,
yet it recognized me as if I had lived there forever...

Log in to post a comment.

// Initial Contact – ge1doot
// ----------------------------------------------------
//
//

Canvas.setpenopacity(0.8);
const turtle = new Turtle();
turtle.pendown();

// Basic 3D vector helpers

function vec3(x, y, z) { return { x:x, y:y, z:z }; }
function add(a, b) { return vec3(a.x + b.x, a.y + b.y, a.z + b.z); }
function sub(a, b) { return vec3(a.x - b.x, a.y - b.y, a.z - b.z); }
function mul(a, s) { return vec3(a.x * s, a.y * s, a.z * s); }
function dot(a, b) { return a.x*b.x + a.y*b.y + a.z*b.z; }
function length(a) { return Math.hypot(a.x, a.y, a.z); }
function cross(a, b) {
    return vec3(
        a.y*b.z - a.z*b.y,
        a.z*b.x - a.x*b.z,
        a.x*b.y - a.y*b.x
    );
}
function normalize(a) {
    const l = length(a) || 1;
    return vec3(a.x / l, a.y / l, a.z / l);
}

// Camera pinhole

const camera = {
    eye:    vec3(0, 80, -260),
    target: vec3(0,  0,   40),
    up:     vec3(0,  1,    0),
    fov: 155 * Math.PI / 180,
    roll: (-8 * Math.random() * 16) * Math.PI / 180,
    fisheyeK: 0,
    setup() {
        const f = normalize(sub(this.target, this.eye));
        let r = normalize(cross(f, this.up));
        let u = cross(r, f);
        if (this.roll !== 0) {
            const c = Math.cos(this.roll);
            const s = Math.sin(this.roll);
            const r2 = vec3(
                r.x * c + u.x * s,
                r.y * c + u.y * s,
                r.z * c + u.z * s
            );
            const u2 = vec3(
                u.x * c - r.x * s,
                u.y * c - r.y * s,
                u.z * c - r.z * s
            );
            r = r2; u = u2;
        }
        this.f = f;
        this.r = r;
        this.u = u;
        this.tanHalfFov = Math.tan(this.fov * 0.5);
        this.screenScale = 230;
    },

    project(p) {
        const d = sub(p, this.eye);
        const x = dot(d, this.r);
        const y = dot(d, this.u);
        const z = dot(d, this.f);
        if (z <= 1e-2) return null;
        let xn = x / (z * this.tanHalfFov);
        let yn = -y / (z * this.tanHalfFov);
        if (this.fisheyeK !== 0) {
            const r2 = xn*xn + yn*yn;
            const factor = 1 + this.fisheyeK * r2;
            xn *= factor;
            yn *= factor;
        }
        return {
            x: xn * this.screenScale,
            y: yn * this.screenScale,
            z: z
        };
    }
};

camera.setup();

// Box primitive (axis-aligned) + face list

class Box {
    constructor(cx, cy, cz, sx, sy, sz) {
        this.c = vec3(cx, cy, cz);
        this.s = vec3(sx, sy, sz);
        this.verts = null;
        this.depth = 0;
    }
    vertices() {
        if (this.verts) return this.verts;
        const c = this.c, s = this.s;
        this.verts = [
            vec3(c.x-s.x, c.y-s.y, c.z-s.z), // 0
            vec3(c.x+s.x, c.y-s.y, c.z-s.z), // 1
            vec3(c.x+s.x, c.y+s.y, c.z-s.z), // 2
            vec3(c.x-s.x, c.y+s.y, c.z-s.z), // 3
            vec3(c.x-s.x, c.y-s.y, c.z+s.z), // 4
            vec3(c.x+s.x, c.y-s.y, c.z+s.z), // 5
            vec3(c.x+s.x, c.y+s.y, c.z+s.z), // 6
            vec3(c.x-s.x, c.y+s.y, c.z+s.z), // 7
        ];
        return this.verts;
    }
}

const BOX_FACES = [
    { idx:[0,1,2,3], hatch:1 }, // front
    { idx:[0,4,5,1], hatch:1 }, // bottom / side
    { idx:[3,2,6,7], hatch:0 }, // top
    { idx:[0,3,7,4], hatch:0 }, // left
    { idx:[1,5,6,2], hatch:1 }, // right
    { idx:[4,7,6,5], hatch:0 }  // back
];

// Scene generation – "Initial Contact"

const shapes   = [];

function randRange(a, b) { return a + Math.random() * (b - a); }
function addBuilding(x, y, z, w, h, p) {
    const cx = x;
    const cy = y;
    const cz = z + p * 0.5;
    shapes.push(new Box(cx, cy, cz, w, h, p * 0.5));
}
 
const r = (d0, d1) => Math.round(Math.random() * (d1 - d0) + d0);
let w = 100;
for (let z = 400; z > -400; z -= 10) {
	addBuilding(
		r(-w, w), r(-w, w), z,
		r(2, 30), r(2, 30), r(2, 180)
	);
	if (z > -150 && z < 150) {
		for (let i = 0; i < 4; i++) {
			addBuilding(
				r(-400, 400), r(-w, w), z,
				r(2, 80), r(2, 30), r(2, 30)
			);
            addBuilding(
				r(-w, w), r(-400, 400), z,
				r(2, 30), r(2, 80), r(2, 30)
			);
		}
	}
}

// Depth = minimum camera-space z among the 8 vertices

for (const b of shapes) {
    const verts = b.vertices();
    let zmin = 1e9;
    for (let i = 0; i < verts.length; i++) {
        const d = sub(verts[i], camera.eye);
        const z = dot(d, camera.f);
        if (z < zmin) zmin = z;
    }
    b.depth = zmin;
}

// Sort so that closest boxes are drawn first

shapes.sort((a, b) => a.depth - b.depth);

// create clipper

const clipper = PolyClip(turtle);

// Project box faces 

function drawBoxFaces(box) {
    const verts = box.vertices();
    for (let f = 0; f < BOX_FACES.length; f++) {
        const face = BOX_FACES[f];
        const ids  = face.idx;
        const pts2D = [];
        const proj3 = [];
        let skip = false;
        for (let i = 0; i < ids.length; i++) {
            const proj = camera.project(verts[ids[i]]);
            if (!proj) { skip = true; break; }
            proj3.push(proj);
            pts2D.push(proj.x, proj.y);
        }
        if (skip) continue;
        const p0 = proj3[0], p1 = proj3[1], p2 = proj3[2], p3 = proj3[3];
        const area = (p1.x - p0.x)*(p2.y - p0.y) - (p1.y - p0.y)*(p2.x - p0.x);
        if (area <= 0) continue;
        let hatch = null;
        if (face.hatch) {
            const dx = p1.x - p0.x;
            const dy = p1.y - p0.y;
            const a  = Math.atan2(dy, dx);
            hatch = { angle: a, pitch: 0.1 + Math.random() };
        }
    	clipper.drawPolygon(
    		pts2D,
    		{
    			outline: true,
    			hatch
    		}
    	);
    }
}

// Draw all boxes

for (const b of shapes) {
    drawBoxFaces(b);
}

// PolyClip — analytic segment / polygon clipping
// Inspired by Reinder Nijhoff's Polygon Clipping Utility (2019, MIT)
// ge1doot — November 2026
function PolyClip(
    turtle,
	bounds = [-100, -100, 100, 100],
	stroke = (x0, y0, x1, y1) => {
		turtle.jump([x0, y0]);
		turtle.goto([x1, y1]);
	},
) {
	const epsilon = 1e-9,
		crossings = [],
		spans = [],
		layers = [],
		[xmin, ymin, xmax, ymax] = bounds,
		a = new Float64Array(2),
		b = new Float64Array(2),
		p0 = new Float64Array(2),
		p1 = new Float64Array(2),
		pieces = [],
		masks = [],
		occluders = [],
		intersections = [];
	let hx, hy, nx, ny;
	function prepare(points) {
		const flat = typeof points?.[0] === "number",
			count = flat ? points.length / 2 : points?.length,
			xy = new Float64Array(count * 2);
		let minx = Infinity,
			miny = Infinity,
			maxx = -Infinity,
			maxy = -Infinity;
		for (let i = 0; i < count; i++) {
			const x = flat ? points[2 * i] : points[i][0];
			const y = flat ? points[2 * i + 1] : points[i][1];
			xy[i * 2] = x;
			xy[i * 2 + 1] = y;
			if (x < minx) minx = x;
			if (x > maxx) maxx = x;
			if (y < miny) miny = y;
			if (y > maxy) maxy = y;
		}
		return { xy, minx, miny, maxx, maxy };
	}
	function subtractRanges(a, b, ranges) {
		const ax = a[0],
			ay = a[1],
			bx = b[0],
			by = b[1],
			dx = bx - ax,
			dy = by - ay,
			length = Math.hypot(dx, dy),
			output = (lo, hi) => {
    			if (hi - lo > tEps)
    				emitBounded(ax + dx * lo, ay + dy * lo, ax + dx * hi, ay + dy * hi);
    		};
		if (length <= epsilon) return;
		const tEps = epsilon / length;
		spans.length = 0;
		for (let i = 0; i < ranges.length; i += 2) {
			const lo = Math.max(0, Math.min(ranges[i], ranges[i + 1]));
			const hi = Math.min(1, Math.max(ranges[i], ranges[i + 1]));
			if (hi - lo > tEps) spans.push(lo, hi);
		}
		union(spans, tEps);
		let at = 0;
		for (let i = 0; i < spans.length; i += 2) {
			output(at, spans[i]);
			at = Math.max(at, spans[i + 1]);
		}
		output(at, 1);
	}
	function emitBounded(x0, y0, x1, y1) {
		const dx = x1 - x0,
			dy = y1 - y0;
		let lo = 0,
			hi = 1;
		function trim(p, q) {
			if (Math.abs(p) < 1e-12) return q >= 0;
			const t = q / p;
			if (p < 0) lo = Math.max(lo, t);
			else hi = Math.min(hi, t);
			return lo <= hi;
		}
		if (
			!trim(-dx, x0 - xmin) ||
			!trim(dx, xmax - x0) ||
			!trim(-dy, y0 - ymin) ||
			!trim(dy, ymax - y0)
		)
			return;
		if (hi - lo <= epsilon / Math.max(1, Math.hypot(dx, dy))) return;
		stroke(x0 + dx * lo, y0 + dy * lo, x0 + dx * hi, y0 + dy * hi);
	}
	function visible(x0, y0, x1, y1) {
		p0[0] = x0;
		p0[1] = y0;
		p1[0] = x1;
		p1[1] = y1;
		if (!occluders.length) {
			emitBounded(x0, y0, x1, y1);
			return;
		}
		const dx = x1 - x0,
			dy = y1 - y0,
			len2 = dx * dx + dy * dy;
		if (len2 <= epsilon * epsilon) return;
		const minx = Math.min(x0, x1),
			maxx = Math.max(x0, x1),
			miny = Math.min(y0, y1),
			maxy = Math.max(y0, y1);
		masks.length = 0;
		for (let j = 0; j < occluders.length; j++) {
			const q = occluders[j];
			if (!(q.maxx < minx || q.minx > maxx || q.maxy < miny || q.miny > maxy)) {
				intersections.length = 0;
				intersect(p0, p1, q, intersections);
				for (let k = 0; k < intersections.length; k += 4) {
					masks.push(
						((intersections[k] - x0) * dx + (intersections[k + 1] - y0) * dy) /
							len2,
						((intersections[k + 2] - x0) * dx +
							(intersections[k + 3] - y0) * dy) /
							len2,
					);
				}
			}
		}
		subtractRanges(p0, p1, masks);
	}
	function viewportLine(s) {
		let lo = -Infinity,
			hi = Infinity;
		const cx = s * nx,
			cy = s * ny;
		if (Math.abs(hx) < 1e-12) {
			if (cx < xmin || cx > xmax) return false;
		} else {
			const t0 = (xmin - cx) / hx,
				t1 = (xmax - cx) / hx;
			lo = Math.max(lo, Math.min(t0, t1));
			hi = Math.min(hi, Math.max(t0, t1));
		}
		if (Math.abs(hy) < 1e-12) {
			if (cy < ymin || cy > ymax) return false;
		} else {
			const t0 = (ymin - cy) / hy,
				t1 = (ymax - cy) / hy;
			lo = Math.max(lo, Math.min(t0, t1));
			hi = Math.min(hi, Math.max(t0, t1));
		}
		if (!(hi - lo > epsilon)) return false;
		a[0] = cx + lo * hx;
		a[1] = cy + lo * hy;
		b[0] = cx + hi * hx;
		b[1] = cy + hi * hy;
		return true;
	}
	function gather(ax, ay, bx, by, poly, length, out) {
		if (
			poly.maxx < Math.min(ax, bx) - epsilon ||
			poly.minx > Math.max(ax, bx) + epsilon ||
			poly.maxy < Math.min(ay, by) - epsilon ||
			poly.miny > Math.max(ay, by) + epsilon
		)
			return;
		const dx = bx - ax,
			dy = by - ay,
			invLength2 = 1 / (length * length),
			sideEps = epsilon * length,
			tEps = epsilon / length,
			xy = poly.xy;
		crossings.length = 0;
		let x0 = xy[xy.length - 2],
			y0 = xy[xy.length - 1],
			s0 = dx * (y0 - ay) - dy * (x0 - ax);
		if (Math.abs(s0) <= sideEps) s0 = 0;
		let t0 = ((x0 - ax) * dx + (y0 - ay) * dy) * invLength2;
		for (let i = 0; i < xy.length; i += 2) {
			const x1 = xy[i],
				y1 = xy[i + 1];
			let s1 = dx * (y1 - ay) - dy * (x1 - ax);
			if (Math.abs(s1) <= sideEps) s1 = 0;
			const t1 = ((x1 - ax) * dx + (y1 - ay) * dy) * invLength2;
			if (s0 === 0 && s1 === 0) {
				const lo = Math.max(0, Math.min(t0, t1)),
					hi = Math.min(1, Math.max(t0, t1));
				if (hi - lo > tEps) out.push(lo, hi);
			} else if (s0 > 0 !== s1 > 0)
				crossings.push(t0 + ((t1 - t0) * s0) / (s0 - s1));
			x0 = x1;
			y0 = y1;
			s0 = s1;
			t0 = t1;
		}
		crossings.sort((a, b) => a - b);
		for (let i = 0; i + 1 < crossings.length; i += 2) {
			const lo = Math.max(0, crossings[i]),
				hi = Math.min(1, crossings[i + 1]);
			if (hi - lo > tEps) out.push(lo, hi);
		}
	}
	function sortPairs(a, left, right) {
		while (left < right) {
			let i = left,
				j = right;
			const pivot = a[2 * ((left + right) >> 1)];
			while (i <= j) {
				while (a[2 * i] < pivot) i++;
				while (a[2 * j] > pivot) j--;
				if (i <= j) {
					const lo = a[2 * i],
						hi = a[2 * i + 1];
					a[2 * i] = a[2 * j];
					a[2 * i + 1] = a[2 * j + 1];
					a[2 * j] = lo;
					a[2 * j + 1] = hi;
					i++;
					j--;
				}
			}
			if (j - left < right - i) {
				if (left < j) sortPairs(a, left, j);
				left = i;
			} else {
				if (i < right) sortPairs(a, i, right);
				right = j;
			}
		}
	}
	function union(a, tEps) {
		if (!a.length) return;
		if (a.length > 2) sortPairs(a, 0, a.length / 2 - 1);
		let lo = a[0],
			hi = a[1],
			w = 0;
		for (let i = 2; i < a.length; i += 2) {
			if (a[i] <= hi + tEps) hi = Math.max(hi, a[i + 1]);
			else {
				a[w++] = lo;
				a[w++] = hi;
				lo = a[i];
				hi = a[i + 1];
			}
		}
		a[w++] = lo;
		a[w++] = hi;
		a.length = w;
	}
	function intersect(a, b, poly, out) {
		const ax = a[0],
			ay = a[1],
			bx = b[0],
			by = b[1],
			dx = bx - ax,
			dy = by - ay,
			length = Math.hypot(dx, dy);
		if (length <= epsilon) return;
		spans.length = 0;
		gather(ax, ay, bx, by, poly, length, spans);
		const tEps = epsilon / length;
		union(spans, tEps);
		for (let i = 0; i < spans.length; i += 2) {
			const lo = spans[i],
				hi = spans[i + 1];
			if (hi - lo > tEps) {
				out.push(ax + dx * lo, ay + dy * lo, ax + dx * hi, ay + dy * hi);
			}
		}
	}
	return {
		drawPolygon(
			points,
			{ outline = false, hatch = null, segments = null, occlude = true } = {},
		) {
			const poly =
					points && points.xy instanceof Float64Array
						? points
						: prepare(points),
				minx = Math.max(xmin, poly.minx),
				maxx = Math.min(xmax, poly.maxx),
				miny = Math.max(ymin, poly.miny),
				maxy = Math.min(ymax, poly.maxy),
				pitch = hatch ? (hatch.pitch ?? hatch.step ?? 0) : 0,
				angle = hatch ? (hatch.angle ?? 0) : 0;
			if (minx < maxx && miny < maxy && (outline || pitch || segments)) {
				occluders.length = 0;
				for (let j = 0; j < layers.length; j++) {
					const q = layers[j];
					if (!(
						q.maxx < minx ||
						q.minx > maxx ||
						q.maxy < miny ||
						q.miny > maxy
					))
						occluders.push(q);
				}
				if (outline) {
					const xy = poly.xy;
					for (let q = 0; q < xy.length; q += 2) {
						const n = (q + 2) % xy.length;
						visible(xy[q], xy[q + 1], xy[n], xy[n + 1]);
					}
				}
				if (segments) {
					if (typeof segments[0] === "number") {
						for (let q = 0; q < segments.length; q += 4)
							visible(
								segments[q],
								segments[q + 1],
								segments[q + 2],
								segments[q + 3]
							);
					} else {
						for (const [p, q] of segments)
							visible(p[0], p[1], q[0], q[1]);
					}
				}
				if (pitch > 0) {
					hx = Math.cos(angle);
					hy = Math.sin(angle);
					nx = -hy;
					ny = hx;
					const sMin =
						(nx > 0 ? nx * minx : nx * maxx) + (ny > 0 ? ny * miny : ny * maxy);
					const sMax =
						(nx > 0 ? nx * maxx : nx * minx) + (ny > 0 ? ny * maxy : ny * miny);
					for (
						let k = Math.ceil((sMin - 1e-10) / pitch);
						k <= Math.floor((sMax + 1e-10) / pitch);
						k++
					) {
						if (!viewportLine(k * pitch)) continue;
						pieces.length = 0;
						intersect(a, b, poly, pieces);
						for (let q = 0; q < pieces.length; q += 4)
							visible(
								pieces[q],
								pieces[q + 1],
								pieces[q + 2],
								pieces[q + 3]
							);
					}
				}
			}
			if (occlude) layers.push(poly);
		},
		walk(draw, intervalMs = 0) {
			let next = 0;
			return function () {
				const now = performance.now();
				if (now < next) return true;
				const more = draw();
				next = now + intervalMs;
				return more;
			};
		},
	};
}