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;
};
},
};
}