Cathedrals of Static
The signal remembers its ruins.
Log in to post a comment.
// "Cathedrals of Static"
// The signal remembers its ruins.
// ge1doot - November 2026
//
Canvas.setpenopacity(0.75);
const turtle = new Turtle();
turtle.pendown();
//////////////////////////////////////////////////////
// CFDG — Procedural scene generation
//
const CFDG = () => {
let rule = {};
let setup = {};
let rules = {};
let matrices = [];
let seed = (Math.random() * 1e7) | 0;
let primitiveCount = 0;
let minSize = 0;
const DEG = Math.PI / 180;
const shapes = [];
const stack = [];
const frame = [
-.45, 0, -.45, .1, .8, .1,
.45, 0, -.45, .1, .8, .1,
-.45, 0, .45, .1, .8, .1,
.45, 0, .45, .1, .8, .1,
0, -.45, -.45, 1, .1, .1,
0, .45, -.45, 1, .1, .1,
0, -.45, .45, 1, .1, .1,
0, .45, .45, 1, .1, .1,
-.45, -.45, 0, .1, .1, .8,
-.45, .45, 0, .1, .1, .8,
.45, -.45, 0, .1, .1, .8,
.45, .45, 0, .1, .1, .8
];
const randomize = (s) => {
seed = s | 0;
};
const snap = (x) =>
Math.abs(x) < 1e-12
? 0
: Math.abs(x - 1) < 1e-12
? 1
: Math.abs(x + 1) < 1e-12
? -1
: x;
const random = () => {
const s = seed;
seed = (Math.imul(s, 747796405) + 2891336453) | 0;
const word = Math.imul((s >>> ((s >>> 28) + 4)) ^ s, 277803737);
return (((word >>> 22) ^ word) >>> 0) / 4294967296;
};
// Local affine transformations used by the grammar
const transforms = {
x(m, v) {
m[12] += m[0] * v;
m[13] += m[1] * v;
m[14] += m[2] * v;
},
y(m, v) {
m[12] += m[4] * v;
m[13] += m[5] * v;
m[14] += m[6] * v;
},
z(m, v) {
m[12] += m[8] * v;
m[13] += m[9] * v;
m[14] += m[10] * v;
},
s(m, v) {
const a = Array.isArray(v);
const x = a ? v[0] : v;
const y = a ? v[1] : x;
const z = a ? v[2] : x;
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;
},
rx(m, v) {
const s = snap(Math.sin(v * DEG));
const c = snap(Math.cos(v * DEG));
const a10 = m[4];
const a11 = m[5];
const a12 = m[6];
const a20 = m[8];
const a21 = m[9];
const a22 = m[10];
m[4] = a10 * c + a20 * s;
m[5] = a11 * c + a21 * s;
m[6] = a12 * c + a22 * s;
m[8] = a10 * -s + a20 * c;
m[9] = a11 * -s + a21 * c;
m[10] = a12 * -s + a22 * c;
},
ry(m, v) {
const s = snap(Math.sin(v * DEG));
const c = snap(Math.cos(v * DEG));
const a00 = m[0];
const a01 = m[1];
const a02 = m[2];
const a20 = m[8];
const a21 = m[9];
const a22 = m[10];
m[0] = a00 * c + a20 * -s;
m[1] = a01 * c + a21 * -s;
m[2] = a02 * c + a22 * -s;
m[8] = a00 * s + a20 * c;
m[9] = a01 * s + a21 * c;
m[10] = a02 * s + a22 * c;
},
rz(m, v) {
const s = snap(Math.sin(v * DEG));
const c = snap(Math.cos(v * DEG));
const a00 = m[0];
const a01 = m[1];
const a02 = m[2];
const a10 = m[4];
const a11 = m[5];
const a12 = m[6];
m[0] = a00 * c + a10 * s;
m[1] = a01 * c + a11 * s;
m[2] = a02 * c + a12 * s;
m[4] = a00 * -s + a10 * c;
m[5] = a01 * -s + a11 * c;
m[6] = a02 * -s + a12 * c;
},
l(m, v) {
m[16] = v;
},
b(m, v) {
if (v > 0) m[16] += v * (1 - m[16]);
else m[16] += v * m[16];
},
};
const SHAPE = (m, t, p, pc) => {
const s = m.slice(0);
for (const c in t) transforms[c](s, t[c]);
s[19] = p;
primitiveCount += pc;
matrices.push(s);
};
const transform = (s, p) => {
const m = s.slice(0);
m[17]++;
for (const c in p) transforms[c](m, p[c]);
if (
minSize > 0 &&
Math.max(
Math.hypot(m[0], m[1], m[2]),
Math.hypot(m[4], m[5], m[6]),
Math.hypot(m[8], m[9], m[10]),
) < minSize
) return null;
return m;
};
// Rule dispatch and recursive growth.
const singlerule = (i) => {
return (s, t) => {
s = transform(s, t);
if (!s) return;
s[18] = i;
stack.push(s);
};
};
const randomrule = (totalWeight, weight, index, len) => {
return (s, t) => {
s = transform(s, t);
if (!s) return;
let w = 0;
const r = random() * totalWeight;
for (let i = 0; i < len; i++) {
w += weight[i];
if (r <= w) {
s[18] = index[i];
stack.push(s);
return;
}
}
};
};
// Reject primitives outside the camera view before building geometry
const cull = (scene, camera) => {
const planes = camera.frustum();
const visible = [];
for (let i = 0; i < scene.length; i++) {
const m = scene[i];
let rejected = false;
for (let j = 0; j < planes.length; j++) {
const p = planes[j];
const d = p[0] * m[12] + p[1] * m[13] + p[2] * m[14] + p[3];
const r = 0.5 * (
Math.abs(p[0] * m[0] + p[1] * m[1] + p[2] * m[2]) +
Math.abs(p[0] * m[4] + p[1] * m[5] + p[2] * m[6]) +
Math.abs(p[0] * m[8] + p[1] * m[9] + p[2] * m[10])
);
if (j === 0 ? d - r <= 0 : d + r < 0) {
rejected = true;
break;
}
}
if (!rejected) visible.push(m);
}
return visible;
};
// Expand logical primitives into world-space cubes
const buildMesh = (scene) => {
const mesh = [];
const emit = (m, x, y, z, sx, sy, sz) => {
const cx = m[0] * x + m[4] * y + m[8] * z + m[12];
const cy = m[1] * x + m[5] * y + m[9] * z + m[13];
const cz = m[2] * x + m[6] * y + m[10] * z + m[14];
const ax = m[0] * sx * 0.5;
const ay = m[1] * sx * 0.5;
const az = m[2] * sx * 0.5;
const bx = m[4] * sy * 0.5;
const by = m[5] * sy * 0.5;
const bz = m[6] * sy * 0.5;
const dx = m[8] * sz * 0.5;
const dy = m[9] * sz * 0.5;
const dz = m[10] * sz * 0.5;
const cube = new Array(8);
for (let i = 0; i < 8; i++) {
const u = i === 1 || i === 2 || i === 5 || i === 6 ? 1 : -1;
const v = i === 2 || i === 3 || i === 6 || i === 7 ? 1 : -1;
const w = i >= 4 ? 1 : -1;
cube[i] = [
cx + u * ax + v * bx + w * dx,
cy + u * ay + v * by + w * dy,
cz + u * az + v * bz + w * dz,
];
}
mesh.push(cube);
};
for (let i = 0; i < scene.length; i++) {
const m = scene[i];
if (m[19] === 1) {
for (let j = 0; j < frame.length; j += 6) {
emit(
m,
frame[j],
frame[j + 1],
frame[j + 2],
frame[j + 3],
frame[j + 4],
frame[j + 5],
);
}
} else {
emit(m, 0, 0, 0, 1, 1, 1);
}
}
return mesh;
};
return {
init(cfg, rules) {
setup = cfg;
if (setup.seed) randomize(setup.seed);
console.log("Seed: ", seed);
shapes.length = 0;
for (const namerule in rules) {
const r = rules[namerule];
if (Array.isArray(r)) {
let totalWeight = 0;
const weight = [];
const index = [];
for (let i = 0; i < r.length; i += 2) {
totalWeight += r[i];
shapes.push(r[i + 1]);
weight.push(r[i]);
index.push(shapes.length - 1);
}
rule[namerule] = randomrule(totalWeight, weight, index, index.length);
} else {
shapes.push(r);
rule[namerule] = singlerule(shapes.length - 1);
}
}
},
run() {
const maxDepth = setup.maxDepth ?? 100;
const maxPrimitives = setup.maxPrimitives ?? 1e9;
minSize = setup.minSize ?? 0;
primitiveCount = 0;
matrices.length = 0;
stack.length = 0;
// identity
rule[setup.start](
[
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
1, // 16 brightness
0, // 17 depth
0, // 18 fn index
0, // 19 primitive (0 = CUBE, 1 = FRAME)
],
setup.transform ?? {},
);
// run rules
let head = 0;
while (head < stack.length && primitiveCount < maxPrimitives) {
const s = stack[head++];
if (s !== undefined && s[17] <= maxDepth + 1) shapes[s[18]](s);
}
stack.length = 0;
const scene = matrices;
matrices = [];
return scene;
},
randomize,
random,
rule,
CUBE: (m, t) => SHAPE(m, t, 0, 1),
FRAME: (m, t) => SHAPE(m, t, 1, 12),
DEPTH: (s) => s[17],
cull,
buildMesh,
};
};
//////////////////////////////////////////////////////
// Camera3D — Perspective projection and viewing frustum
//
const Camera3D = () => {
const DEG = Math.PI / 180;
let scale, tanHalfFov;
let frustum = [];
let ex, ey, ez;
let fx, fy, fz;
let rx, ry, rz;
let ux, uy, uz;
const setup = ({
target = [0, 0, 0],
yaw = 0,
pitch = 25,
roll = 0,
radius = 50,
fov = 90,
screenScale = 200,
} = {}) => {
const a = yaw * DEG;
const b = pitch * DEG;
const c = roll * DEG;
const sa = Math.sin(a), ca = Math.cos(a);
const sb = Math.sin(b), cb = Math.cos(b);
const sc = Math.sin(c), cc = Math.cos(c);
ex = target[0] + radius * sa * cb;
ey = target[1] + radius * sb;
ez = target[2] - radius * ca * cb;
fx = -sa * cb;
fy = -sb;
fz = ca * cb;
const r0x = -ca;
const r0z = -sa;
const u0x = -sa * sb;
const u0y = cb;
const u0z = ca * sb;
rx = r0x * cc + u0x * sc;
ry = u0y * sc;
rz = r0z * cc + u0z * sc;
ux = u0x * cc - r0x * sc;
uy = u0y * cc;
uz = u0z * cc - r0z * sc;
scale = screenScale;
tanHalfFov = Math.tan(fov * DEG * 0.5);
const k = (100 * tanHalfFov) / scale;
const plane = (nx, ny, nz, near = 0) => [
nx,
ny,
nz,
-(nx * ex + ny * ey + nz * ez) - near,
];
frustum = [
plane(fx, fy, fz, 1e-2), // Near
plane(fx * k + rx, fy * k + ry, fz * k + rz), // Left
plane(fx * k - rx, fy * k - ry, fz * k - rz), // Right
plane(fx * k + ux, fy * k + uy, fz * k + uz), // Bottom
plane(fx * k - ux, fy * k - uy, fz * k - uz), // Top
];
};
const project = (x, y, z, out) => {
const dx = x - ex;
const dy = y - ey;
const dz = z - ez;
const cx = dx * rx + dy * ry + dz * rz;
const cy = dx * ux + dy * uy + dz * uz;
const cz = dx * fx + dy * fy + dz * fz;
if (cz <= 1e-2) return null;
const k = scale / (cz * tanHalfFov);
out[0] = cx * k;
out[1] = -cy * k;
out[2] = cz;
return out;
};
return { setup, project, frustum: () => frustum };
};
//////////////////////////////////////////////////////
// Projection3D — Project cubes and collect front-facing surfaces
//
const Projection3D = () => {
const vertices = [
[-.5, -.5, -.5],
[ .5, -.5, -.5],
[ .5, .5, -.5],
[-.5, .5, -.5],
[-.5, -.5, .5],
[ .5, -.5, .5],
[ .5, .5, .5],
[-.5, .5, .5],
];
const faces = [
[0, 3, 2, 1], // -Z
[4, 5, 6, 7], // +Z
[0, 1, 5, 4], // -Y
[3, 7, 6, 2], // +Y
[0, 4, 7, 3], // -X
[1, 2, 6, 5], // +X
];
const edges = [
[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 area2 = (a, b, c) =>
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
const determinant = (m) =>
m[0] * (m[5] * m[10] - m[6] * m[9]) +
m[1] * (m[6] * m[8] - m[4] * m[10]) +
m[2] * (m[4] * m[9] - m[5] * m[8]);
const transform = (v, m) => {
const x = v[0], y = v[1], z = v[2];
return [
m[0] * x + m[4] * y + m[8] * z + m[12],
m[1] * x + m[5] * y + m[9] * z + m[13],
m[2] * x + m[6] * y + m[10] * z + m[14],
];
};
const build = (mesh, camera) => {
const cubes = [];
const faceList = [];
for (let cubeId = 0; cubeId < mesh.length; cubeId++) {
const world = mesh[cubeId];
const screen = new Array(8);
let valid = true;
for (let i = 0; i < 8; i++) {
const p = world[i];
const q = camera.project(p[0], p[1], p[2], [0, 0, 0]);
if (!q) {
valid = false;
break;
}
screen[i] = q;
}
if (!valid) continue;
const id = cubes.length;
const p = world[0];
const a = world[1];
const b = world[3];
const c = world[4];
const ax = a[0] - p[0], ay = a[1] - p[1], az = a[2] - p[2];
const bx = b[0] - p[0], by = b[1] - p[1], bz = b[2] - p[2];
const cx = c[0] - p[0], cy = c[1] - p[1], cz = c[2] - p[2];
const det =
ax * (by * cz - bz * cy) +
ay * (bz * cx - bx * cz) +
az * (bx * cy - by * cx);
const sign = Math.sign(det) || 1;
const visibleFaces = [];
for (let faceId = 0; faceId < faces.length; faceId++) {
const f = faces[faceId];
const p0 = screen[f[0]];
const p1 = screen[f[1]];
const p2 = screen[f[2]];
if (area2(p0, p1, p2) * sign < 0) {
visibleFaces.push(faceId);
const poly = [];
let zsum = 0;
let xmin = Infinity, ymin = Infinity;
let xmax = -Infinity, ymax = -Infinity;
for (let i = 0; i < 4; i++) {
const sp = screen[f[i]];
poly.push(sp[0], sp[1]);
zsum += sp[2];
if (sp[0] < xmin) xmin = sp[0];
if (sp[1] < ymin) ymin = sp[1];
if (sp[0] > xmax) xmax = sp[0];
if (sp[1] > ymax) ymax = sp[1];
}
faceList.push({
cubeId: id,
faceId,
indices: f,
points: poly,
zAvg: zsum / 4,
bounds: [xmin, ymin, xmax, ymax],
});
}
}
cubes.push({
cubeId: id,
world,
screen,
visibleFaces,
});
}
return { cubes, faces: faceList };
};
return {
build,
};
};
//////////////////////////////////////////////////////
// Visibility3D — Screen-space occlusion and polygon clipping
//
const Visibility3D = () => {
const depth = [0, 0, 0];
let cells = [];
let faces = [];
let resolution = 32;
let xmin = -100, ymin = -100;
let xmax = 100, ymax = 100;
let tests = 0;
let marks = [];
let stamp = 0;
const cellX = (x) =>
Math.max(
0,
Math.min(
resolution - 1,
Math.floor(((x - xmin) / (xmax - xmin)) * resolution),
),
);
const cellY = (y) =>
Math.max(
0,
Math.min(
resolution - 1,
Math.floor(((y - ymin) / (ymax - ymin)) * resolution),
),
);
const inside = (p, x, y) => {
let sign = 0;
for (let i = 0; i < 8; i += 2) {
const j = (i + 2) % 8;
const cross = (p[j] - p[i]) * (y - p[i + 1]) - (p[j + 1] - p[i + 1]) * (x - p[i]);
if (Math.abs(cross) < 1e-9) continue;
const s = Math.sign(cross);
if (sign && sign !== s) return false;
sign = s;
}
return true;
};
// Fast point visibility test for subpixel texels
const build = (
projected,
{ grid = 32, bounds = [-100, -100, 100, 100] } = {},
) => {
resolution = grid;
[xmin, ymin, xmax, ymax] = bounds;
cells = Array.from({ length: grid * grid }, () => []);
faces = [];
tests = 0;
const cubes = projected.cubes;
for (const face of projected.faces) {
const cube = cubes[face.cubeId];
const [i0, i1, i2] = face.indices;
const p = cube.screen[i0];
const q = cube.screen[i1];
const r = cube.screen[i2];
if (!depthPlane(p, q, r, depth)) continue;
const id = faces.length;
faces.push({
points: face.points,
bounds: face.bounds,
a: depth[0],
b: depth[1],
c: depth[2],
});
const [x0, y0, x1, y1] = face.bounds;
if (x1 < xmin || x0 > xmax || y1 < ymin || y0 > ymax) continue;
for (let iy = cellY(y0); iy <= cellY(y1); iy++) {
for (let ix = cellX(x0); ix <= cellX(x1); ix++) {
cells[iy * grid + ix].push(id);
}
}
}
marks = new Array(faces.length).fill(0);
stamp = 0;
};
// Fast point visibility test for subpixel texels
const visible = (x, y, z) => {
if (x < xmin || x > xmax || y < ymin || y > ymax) return false;
const candidates = cells[cellY(y) * resolution + cellX(x)];
const tolerance = 1e-5 * Math.max(1, z);
for (const id of candidates) {
const f = faces[id];
const [x0, y0, x1, y1] = f.bounds;
tests++;
if (x < x0 || x > x1 || y < y0 || y > y1) continue;
if (!inside(f.points, x, y)) continue;
const inverseZ = f.a * x + f.b * y + f.c;
if (inverseZ <= 0) continue;
if (1 / inverseZ < z - tolerance) return false;
}
return true;
};
// Perspective-correct inverse-depth interpolation
const depthPlane = (p, q, r, out) => {
const dx1 = q[0] - p[0];
const dy1 = q[1] - p[1];
const dx2 = r[0] - p[0];
const dy2 = r[1] - p[1];
const det = dx1 * dy2 - dx2 * dy1;
if (Math.abs(det) < 1e-12) return null;
const z0 = 1 / p[2];
const dz1 = 1 / q[2] - z0;
const dz2 = 1 / r[2] - z0;
out[0] = (dz1 * dy2 - dz2 * dy1) / det;
out[1] = (dx1 * dz2 - dx2 * dz1) / det;
out[2] = z0 - out[0] * p[0] - out[1] * p[1];
return out;
};
// Split a tagged polygon along a line
const split = (poly, a, b, c) => {
const inside = [];
const outside = [];
const n = poly.length;
let p = poly[0];
let dp = a * p[0] + b * p[1] + c;
for (let i = 0; i < n; i++) {
const q = poly[(i + 1) % n];
const dq = a * q[0] + b * q[1] + c;
const pi = dp >= 0, qi = dq >= 0;
const po = dp <= 0, qo = dq <= 0;
if (pi) inside.push([p[0], p[1], p[2]]);
if (po) outside.push([p[0], p[1], p[2]]);
if (pi !== qi || po !== qo) {
const t = dp / (dp - dq);
const x = p[0] + (q[0] - p[0]) * t;
const y = p[1] + (q[1] - p[1]) * t;
if (pi !== qi)
inside.push([x, y, pi ? 7 : p[2]]);
if (po !== qo)
outside.push([x, y, po ? 7 : p[2]]);
}
p = q;
dp = dq;
}
return [inside, outside];
};
const area = poly => {
let a = 0;
for (let i = 0; i < poly.length; i++) {
const p = poly[i];
const q = poly[(i + 1) % poly.length];
a += p[0] * q[1] - q[0] * p[1];
}
return a * 0.5;
};
// Remove a convex occluder from a polygon
const subtract = (poly, mask) => {
const result = [];
const sign = Math.sign(area(mask));
if (!sign) return [poly];
let remainder = poly;
for (let i = 0; i < mask.length; i++) {
if (remainder.length < 3) break;
const a = mask[i];
const b = mask[(i + 1) % mask.length];
const dx = b[0] - a[0];
const dy = b[1] - a[1];
const A = -sign * dy;
const B = sign * dx;
const C = sign * (dy * a[0] - dx * a[1]);
const [inside, outside] = split(remainder, A, B, C);
if (outside.length >= 3 && Math.abs(area(outside)) > 1e-10)
result.push(outside);
remainder = inside;
}
return result;
};
// Subtract all overlapping foreground surfaces from a texel
const clipTexel = (points, tags) => {
const p0 = points[0];
const p1 = points[1];
const p2 = points[2];
const p3 = points[3];
const x0 = Math.min(p0[0], p1[0], p2[0], p3[0]);
const y0 = Math.min(p0[1], p1[1], p2[1], p3[1]);
const x1 = Math.max(p0[0], p1[0], p2[0], p3[0]);
const y1 = Math.max(p0[1], p1[1], p2[1], p3[1]);
if (x1 < xmin || x0 > xmax || y1 < ymin || y0 > ymax) return [];
const dx1 = p1[0] - p0[0];
const dy1 = p1[1] - p0[1];
const dx2 = p2[0] - p0[0];
const dy2 = p2[1] - p0[1];
const det = dx1 * dy2 - dx2 * dy1;
if (Math.abs(det) < 1e-12) return [];
const z0 = 1 / p0[2];
const dz1 = 1 / p1[2] - z0;
const dz2 = 1 / p2[2] - z0;
const ta = (dz1 * dy2 - dz2 * dy1) / det;
const tb = (dx1 * dz2 - dx2 * dz1) / det;
const tc = z0 - ta * p0[0] - tb * p0[1];
const tile = [
[p0[0], p0[1], tags[0]],
[p1[0], p1[1], tags[1]],
[p2[0], p2[1], tags[2]],
[p3[0], p3[1], tags[3]],
];
const current = ++stamp;
let fragments = [tile];
const ix0 = cellX(x0);
const iy0 = cellY(y0);
const ix1 = cellX(x1);
const iy1 = cellY(y1);
for (let iy = iy0; iy <= iy1; iy++) {
for (let ix = ix0; ix <= ix1; ix++) {
const cell = cells[iy * resolution + ix];
for (let k = 0; k < cell.length; k++) {
const id = cell[k];
if (marks[id] === current) continue;
marks[id] = current;
const f = faces[id];
const b = f.bounds;
if (b[2] < x0 || b[0] > x1 || b[3] < y0 || b[1] > y1) continue;
const da = f.a - ta;
const db = f.b - tb;
const dc = f.c - tc - 1e-7;
if (
da * tile[0][0] + db * tile[0][1] + dc <= 0 &&
da * tile[1][0] + db * tile[1][1] + dc <= 0 &&
da * tile[2][0] + db * tile[2][1] + dc <= 0 &&
da * tile[3][0] + db * tile[3][1] + dc <= 0
) continue;
const mask = [
[f.points[0], f.points[1], 7],
[f.points[2], f.points[3], 7],
[f.points[4], f.points[5], 7],
[f.points[6], f.points[7], 7],
];
const [front] = split(mask, da, db, dc);
if (front.length < 3 || Math.abs(area(front)) < 1e-10) continue;
const next = [];
for (const fragment of fragments)
next.push(...subtract(fragment, front));
fragments = next;
if (!fragments.length) return [];
}
}
}
return fragments;
};
// Skip faces fully hidden by a single foreground surface
const occludedFace = (face, cube) => {
const p = face.points;
const b = face.bounds;
const cx = (p[0] + p[2] + p[4] + p[6]) * 0.25;
const cy = (p[1] + p[3] + p[5] + p[7]) * 0.25;
const candidates = cells[cellY(cy) * resolution + cellX(cx)];
const i = face.indices;
if (
!depthPlane(
cube.screen[i[0]],
cube.screen[i[1]],
cube.screen[i[2]],
depth,
)
) return false;
const a = depth[0];
const d = depth[1];
const c = depth[2];
for (let k = 0; k < candidates.length; k++) {
const f = faces[candidates[k]];
const q = f.bounds;
if (f.points === p) continue;
if (q[0] > b[0] || q[1] > b[1] || q[2] < b[2] || q[3] < b[3]) continue;
if (
!inside(f.points, p[0], p[1]) ||
!inside(f.points, p[2], p[3]) ||
!inside(f.points, p[4], p[5]) ||
!inside(f.points, p[6], p[7])
) continue;
const da = f.a - a;
const db = f.b - d;
const dc = f.c - c - 1e-7;
if (
da * p[0] + db * p[1] + dc <= 0 ||
da * p[2] + db * p[3] + dc <= 0 ||
da * p[4] + db * p[5] + dc <= 0 ||
da * p[6] + db * p[7] + dc <= 0
) continue;
return true;
}
return false;
};
return { build, visible, clipTexel, occludedFace };
};
//////////////////////////////////////////////////////
// Texels — Adaptive surface hatching
//
const Texels = (camera, visibility) => {
const axisOf = (v) => {
const x = Math.abs(v[0]);
const y = Math.abs(v[1]);
const z = Math.abs(v[2]);
return x >= y && x >= z ? 0 : y >= z ? 1 : 2;
};
const p = [0, 0, 0];
const q = [0, 0, 0];
const projectedSize = (axis, length, center, size, screenStep) => {
if (length === 0) return 0;
const a = camera.project(center[0], center[1], center[2], p);
const b = camera.project(
center[0] + (axis[0] * size) / length,
center[1] + (axis[1] * size) / length,
center[2] + (axis[2] * size) / length,
q,
);
return a && b ? Math.hypot(q[0] - p[0], q[1] - p[1]) : screenStep;
};
// Tessellate visible faces into hatchable quads, yielding one texel at a time
function* generate(
projected,
{ size = 0.1, correction = 0.5, screenStep = 1, minProjectedSize = 0 } = {},
) {
const point = (a, u, v, x, y, out) =>
camera.project(
a[0] + u[0] * x + v[0] * y,
a[1] + u[1] * x + v[1] * y,
a[2] + u[2] * x + v[2] * y,
out,
);
const tags = [0, 0, 0, 0];
const ppt = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
];
const cubes = projected.cubes;
for (const face of projected.faces) {
const cube = cubes[face.cubeId];
const [i0, i1, , i3] = face.indices;
const a = cube.world[i0];
const b = cube.world[i1];
const d = cube.world[i3];
const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
const v = [d[0] - a[0], d[1] - a[1], d[2] - a[2]];
const tagU = axisOf(u) * 2 + 1;
const tagV = axisOf(v) * 2 + 1;
const lenU = Math.hypot(...u);
const lenV = Math.hypot(...v);
const hatchU = lenU <= lenV;
const center = [
a[0] + (u[0] + v[0]) * 0.5,
a[1] + (u[1] + v[1]) * 0.5,
a[2] + (u[2] + v[2]) * 0.5,
];
const stepU =
size *
Math.pow(
screenStep / Math.max(1e-6, projectedSize(u, lenU, center, size)),
correction,
);
const stepV =
size *
Math.pow(
screenStep / Math.max(1e-6, projectedSize(v, lenV, center, size)),
correction,
);
const nu = Math.max(1, Math.ceil(lenU / stepU));
const nv = Math.max(1, Math.ceil(lenV / stepV));
if (visibility.occludedFace(face, cube)) continue;
for (let j = 0; j < nv; j++) {
for (let i = 0; i < nu; i++) {
const x0 = i / nu;
const x1 = (i + 1) / nu;
const y0 = j / nv;
const y1 = (j + 1) / nv;
const p0 = point(a, u, v, x0, y0, ppt[0]);
const p1 = point(a, u, v, x1, y0, ppt[1]);
const p2 = point(a, u, v, x1, y1, ppt[2]);
const p3 = point(a, u, v, x0, y1, ppt[3]);
// Edge tags: 0 = hidden, 1–6 = drawable, 7 = clipping boundary
if (!p0 || !p1 || !p2 || !p3) continue;
tags[0] = j === 0 ? tagU : hatchU ? tagU + 1 : 0;
tags[1] = i === nu - 1 ? tagV : 0;
tags[2] = j === nv - 1 ? tagU : 0;
tags[3] = i === 0 ? tagV : !hatchU ? tagV + 1 : 0;
if (minProjectedSize > 0) {
const w =
Math.max(p0[0], p1[0], p2[0], p3[0]) -
Math.min(p0[0], p1[0], p2[0], p3[0]);
const h =
Math.max(p0[1], p1[1], p2[1], p3[1]) -
Math.min(p0[1], p1[1], p2[1], p3[1]);
if (Math.max(w, h) < minProjectedSize) {
const mx = (p0[0] + p2[0]) * 0.5;
const my = (p0[1] + p2[1]) * 0.5;
const mz = (2 * p0[2] * p2[2]) / (p0[2] + p2[2]);
if (visibility.visible(mx, my, mz)) {
yield [
[
[p0[0], p0[1], tags[0]],
[p1[0], p1[1], tags[1]],
[p2[0], p2[1], tags[2]],
[p3[0], p3[1], tags[3]],
],
];
} else yield [];
continue;
}
}
const fragments = visibility.clipTexel([p0, p1, p2, p3], tags);
yield fragments;
}
}
}
}
return { generate };
};
//////////////////////////////////////////////////////
// Cathedral — Generative grammar
//
const Cathedral = (cfdg) => {
const { rule, CUBE, FRAME, random } = cfdg;
const rules = {
start(s) {
rule.R0(s, {
ry: -30 + random() * 60,
rx: -30 + random() * 60
});
},
R0 (s) {
rule.R1(s, {x: 1.1});
rule.R1(s, {ry: 180, x: 1.1});
},
R1: [
1,
(s) => {
if (random() > 0.25) FRAME(s);
else CUBE(s);
rule.R1(s, { x: 1.1 });
},
0.04,
(s) => {
CUBE(s);
rule.R1(s, { x: 1.1, ry: 90 });
rule.R1(s, { x: 1.1, rz: -90, s: 0.5 });
},
0.04,
(s) => {
CUBE(s);
rule.R1(s, { x: 1.1, rx: 90 });
rule.R1(s, { x: 1.1, rz: -90, s: 3 });
},
],
};
return {
run(setup) {
cfdg.init(setup, rules);
const scene = cfdg.run();
return scene;
},
};
};
//////////////////////////////////////////////////////
// Scene setup and incremental SVG drawing
//
const cfdg = CFDG();
const cathedral = Cathedral(cfdg);
const camera = Camera3D();
camera.setup({
yaw: 0,
pitch: 0,
roll: 0,
radius: 12,
fov: 120,
});
const scene = cathedral.run({
start: "start",
seed: 0, // 0 = random
transform: { s: 0.2 },
maxPrimitives: 20000,
minSize: 0.025
});
const visible = cfdg.cull(scene, camera);
const mesh = cfdg.buildMesh(visible);
const renderer = Projection3D();
const projected = renderer.build(mesh, camera);
const visibility = Visibility3D();
const texels = Texels(camera, visibility);
visibility.build(projected, {
grid: 32,
});
const generator = texels.generate(projected, {
size: 0.01,
correction: 0.35,
screenStep: 10,
minProjectedSize: 0.5,
});
function walk() {
const next = generator.next();
if (next.done) {
return false;
}
for (const poly of next.value) {
for (let i = 0; i < poly.length; i++) {
const a = poly[i];
const b = poly[(i + 1) % poly.length];
const tag = a[2];
if (tag === 0 || tag === 7) continue;
turtle.jump([a[0], a[1]]);
turtle.goto([b[0], b[1]]);
}
}
return true;
}