Prototype utility for 3D silhouettes.
It features:
- Combination of sharp edges and contour for the outline.
- Subdivision for more accurate depth sorting.
- Merging simple transformed primitives into complex shapes.
- Optimized to deal with non-trivial scenes. (Further optimization needed - starting with the subdivision.)
- Precomputes the scene once to improve GIF export times.
The principle is: a sphere looks like a disc from all angles, but a cylinder or cube has "inner outlines". The outer and inner outlines are calculated separately and combined to achieve this look.
Log in to post a comment.
// LL 2021
const turtle = new Turtle();
const detail = 0.5; // min=0 max=1 step=0.1
const perspective = 1.6; // min=1 max=3 step=0.1
const camera_theta = 0.3; // min=-1 max=1 step=0.01
const camera_phi = 0.3; // min=-0.99 max=1 step=0.01
const camera_r = 20; // min=0.1 max=40 step=0.1
const look_at_z = 0; // min=-100 max=100 step=0.1
const inside_lines = 0.33; // min=0 max=1 step=0.01
const style = 2; // min=0 max=3 step=1 (Preview,All outlines,Silhouette,Hatched)
const seed = 0; // min=0 max=100 step=1
Canvas.setpenopacity((style==0) ? 0.25 : 1);
const detail_max_length = Math.max(0.9, 0.9 + 5 * (1 - detail));
var silhouette = new Silhouette();
function walk(i, t) {
if (i==0) {
timer_start('total');
if (t == 0 || t == 1) initOnce();
initFrame(t);
}
if (silhouette.faceCount() < 1) {
timer_end('total');
return false;
}
silhouette.nextFace().draw();
return true;
}
function initFrame(t) {
const cameraOffset = [
camera_r * perspective ** 3 * Math.cos((camera_theta+t*2) * Math.PI) * Math.sin((camera_phi/2+0.5) * Math.PI),
camera_r * perspective ** 3 * Math.sin((camera_theta+t*2) * Math.PI) * Math.sin((camera_phi/2+0.5) * Math.PI),
-camera_r * perspective ** 3 * Math.cos((camera_phi/2+0.5) * Math.PI)
];
const cameraLookAt = [0, 0, look_at_z];
viewProjectionMatrix = setupCamera(add3(cameraOffset, cameraLookAt), cameraLookAt);
polygons = new Polygons();
silhouette.processFrameModels();
silhouette.sortFaces();
console.log(`Models :${silhouette.modelCount()}. Faces: ${silhouette.faceCount()}.`);
}
function initOnce(t) {
seed_t = (t < 1 && seed == 0) ? (Math.random() * 100 | 0) : seed;
rng = undefined;
initScene();
timer_start('processOnce');
silhouette.processOnceModels();
timer_end('processOnce');
}
function initScene() {
timer_start('initScene');
const count = 20;
const r = 0.5;
for (var vz = -1; vz <= 1; vz++) {
for (var vx = -1; vx <= 1; vx++) {
const model = silhouette.newModel();
switch (((vx+1) + (vz+1) * 3) % 9)
{
case 0:
case 1:
case 2: {
const mdl = makeVoxelCube(r, count);
var matrix = new Matrix();
matrix.translate(0, count * r / 2, 0);
mdl.transform(matrix);
model.merge(mdl);
}
break;
case 3: {
const mdl = makeTower(count * r * 2, count * r * 0.25);
model.merge(mdl);
}
break;
case 4: {
const mdl = makeCylinder(count * r * 1.3, 0, count * r * 0.75, 50);
var matrix = new Matrix();
matrix.translate(0, count * r * 1.3 / 2, 0);
mdl.transform(matrix);
model.merge(mdl);
}
break;
case 5: {
{
const mdl = makeBox(2 * r, count * r, 2 * r);
var matrix = new Matrix();
matrix.translate(0, count * r / 2, 0);
mdl.transform(matrix);
model.merge(mdl);
}
{
const mdl = makeBox(count * r, 2 * r, 2 * r);
var matrix = new Matrix();
matrix.translate(0, count * r / 2, 0);
mdl.transform(matrix);
model.merge(mdl);
}
}
break;
case 6: {
const radius = count * r * (0.5 + 0.25 * random());
const mdl = makeSphere(radius, 7);
var matrix = new Matrix();
matrix.translate(0, radius, 0);
mdl.transform(matrix);
model.merge(mdl);
}
break;
case 7: {
const mdl = makeCylinder(count * r, count * r * 0.5, count * r * 0.5, 50);
var matrix = new Matrix();
matrix.translate(0, count * r / 2, 0);
mdl.transform(matrix);
model.merge(mdl);
}
break;
case 8: {
const mdl = makeBox(count * r, count * r, count * r);
var matrix = new Matrix();
matrix.translate(0, count * r / 2, 0);
mdl.transform(matrix);
model.merge(mdl);
}
break;
}
{
var matrix = new Matrix();
matrix.multiply(matrix.rotateY(random() * Math.PI / 2));
matrix.translate(vx * count * r * 2, 0, vz * count * r * 2);
model.transform(matrix);
}
silhouette.addModel(model);
}
}
{
var matrix = new Matrix();
matrix.translate(0, -1.51, 0);
const mdl = makeBox(60, 1, 60);
mdl.transform(matrix);
silhouette.addModel(mdl);
}
timer_end('initScene');
}
/////////////////////////////////////////////////////////////////////
// Prototype Silhouette utility code - Created by Lionel Lemarie 2021
// https://turtletoy.net/turtle/334500a2c5
function Silhouette() {
const models = [];
const all_faces = [];
class Model {
constructor() {
this.faces = [];
this.edges = [];
}
transform(matrix) {
this.faces.forEach(face => {
face.transform(matrix);
});
}
processOnce() {
if (detail && style) this.subdivideDetail(detail_max_length);
this.updateEdgeList();
}
processFrame() {
this.faces.forEach(face => { face.projectAndAdd(); });
this.findStaticOutlines();
this.findProjectedOutlines();
this.updateOutlineMasks();
}
updateOutlineMasks() {
this.edges.forEach(edge => {
edge.faces.forEach(face => {
const pc = face.points.length;
if (pc > 1) {
for (var i=0; i<pc; i++) {
const hash0 = getPointHash(face.points[i]);
const hash1 = getPointHash(face.points[(i+1)%pc]);
if (edge.hash == hash0 + hash1 || edge.hash == hash1 + hash0) {
if (edge.state) {
face.outline_mask |= 1 << i;
} else {
face.outline_mask &= ~(1 << i);
}
}
}
}
});
});
}
addFace(points) {
const face = new Face(points);
this.faces.push(face);
}
merge(model) {
model.faces.forEach(face => { this.faces.push(face); })
}
subdivideDetail(max_length) {
var nfaces = this.faces.length;
var loop = true;
while (loop) {
const old_faces = this.faces;
this.faces = [];
old_faces.forEach(face => {
const new_faces = face.getSubdivided(max_length);
this.faces = [...this.faces, ...new_faces];
});
loop = this.faces.length != nfaces;
nfaces = this.faces.length;
}
}
subdivideCount(count) {
while (count-- > 0) {
const old_faces = this.faces;
this.faces = [];
old_faces.forEach(face => {
const new_faces = face.getSubdivided(0);
this.faces = [...this.faces, ...new_faces];
});
}
}
updateEdgeList() {
this.edges = [];
const edge_lookup = {};
this.faces.forEach(face => {
const pc = face.points.length;
if (pc > 1) {
for (var i=0; i<pc; i++) {
const hash0 = getPointHash(face.points[i]);
const hash1 = getPointHash(face.points[(i+1)%pc]);
if ((hash0 + hash1) in edge_lookup) {
const edge_id = edge_lookup[hash0 + hash1];
this.addFaceToEdge(edge_id, face);
} else if ((hash1 + hash0) in edge_lookup) {
const edge_id = edge_lookup[hash1 + hash0];
this.addFaceToEdge(edge_id, face);
} else {
const edge_id = this.edges.length;
edge_lookup[hash0 + hash1] = edge_id;
this.edges.push({ faces: [face], hash: hash0 + hash1, state: 1 });
}
}
}
});
}
addFaceToEdge(edge_id, new_face) {
var good = true;
this.edges[edge_id].faces.forEach(face => {
if (face.matches(new_face)) {
good = false;
face.overlap = true;
new_face.overlap = true;
}
});
if (good) this.edges[edge_id].faces.push(new_face);
}
findStaticOutlines() {
this.edges.forEach(edge => {
edge.state = 1;
for (var i=0, fl=edge.faces.length; i<fl && edge.state; i++) {
if (edge.faces[i].overlap) continue;
const nfi = edge.faces[i].getStaticNormal();
for (var j=i+1; j<fl && edge.state; j++) {
if (edge.faces[j].overlap) continue;
const nfj = edge.faces[j].getStaticNormal();
const d = len3(sub3(nfi, nfj));
if (d < inside_lines * 2) { edge.state = 0; }
}
}
});
}
findProjectedOutlines() {
this.edges.forEach(edge => {
var found_pos = false, found_neg = false;
edge.faces.forEach(face => {
if (!face.overlap) {
const nf = face.getProjectedNormal();
if (nf[2] < 0) found_neg = true;
if (nf[2] >= 0) found_pos = true;
}
});
if (found_pos && found_neg) edge.state = 1;
});
}
}
function getPointHash(point) {
const mult = 100;
const x0 = Math.round(point[0] * mult), y0 = Math.round(point[1] * mult), z0 = Math.round(point[2] * mult);
return `${x0},${y0},${z0}`;
}
class Face {
constructor(points) {
this.points = [...points];
this.z = 0;
this.projected_points = [];
this.outline_mask = -1;
this.overlap = false;
}
draw() {
if (this.projected_points.length < 2 || this.overlap) return;
var good = true;
this.projected_points.forEach(p => good &= Math.min(Math.abs(p[0]), Math.abs(p[1])) < 100 );
if (!good) return;
if (style == 0) {
// turtle.jump(this.projected_points[this.projected_points.length-1]);
// this.projected_points.forEach(p=>turtle.goto(p));
for (var i=0; i<this.projected_points.length; i++) {
if (this.outline_mask & (1 << i)) {
turtle.jump(this.projected_points[i]);
turtle.goto(this.projected_points[(i+1) % this.projected_points.length]);
}
}
} else {
const p1 = polygons.create();
p1.addPoints(...this.projected_points);
if (style == 1) {
p1.addOutline();
} else if (style > 1) {
for (var i=0; i<this.projected_points.length; i++) {
if (this.outline_mask & (1 << i)) {
p1.addSegments(p1.cp[i], p1.cp[(i+1) % this.projected_points.length]);
}
}
if (style > 2) {
const hmin = 0.15, hmax = 0.9;
const hatching = hmin + (hmax - hmin) * this.getLight();
p1.addHatching(-Math.PI/4, hatching);
}
}
polygons.draw(turtle, p1, true);
}
}
getStaticNormal() {
if (this.cached_static_normal === undefined) {
if (this.points.length < 3) this.cached_static_normal = [0, 1, 0];
else this.cached_static_normal = normalize3(cross3(sub3(this.points[1], this.points[0]), sub3(this.points[2], this.points[0])));
}
return this.cached_static_normal;
}
getProjectedNormal() {
if (this.projected_points.length < 3) return [0, 1, 0];
return normalize3(cross3(sub3(this.projected_points[1], this.projected_points[0]), sub3(this.projected_points[2], this.projected_points[0])));
}
getLight() {
const n = this.getProjectedNormal();
return n[0] * 0.5 + 0.5;
}
transform(matrix) {
for (var i=0, c=this.points.length; i<c; i++) {
this.points[i] = matrix.transform(this.points[i]);
}
}
projectAndAdd() {
if (this.overlap) return;
this.projected_points = [];
this.z = 0;
this.points.forEach(point => {
const pp = project(point);
if (pp === undefined) return;
this.projected_points.push(pp);
this.z += pp[2];
})
if (this.projected_points.length > 0) this.z /= this.projected_points.length;
silhouette.addFace(this);
}
getSubdivided(max_length) {
var long_index = -1;
const pc = this.points.length;
for (var i=0; i<pc; i++) {
const len = len3(sub3(this.points[(i+1)%pc],this.points[i]));
if (len > max_length) {
max_length = len;
long_index = i;
}
}
if (long_index >= 0) {
if (pc == 4) {
const new_faces = [];
new_faces.push(new Face([this.points[0], this.points[1], this.points[2]]));
new_faces.push(new Face([this.points[2], this.points[3], this.points[0]]));
return new_faces;
} else {
const new_faces = [];
const point = mulf(add3(this.points[(long_index+1)%pc],this.points[long_index]), 0.5);
for (var i=0; i<pc; i++) {
if (i != long_index) {
new_faces.push(new Face([this.points[i], this.points[(i+1)%pc], point]));
}
}
return new_faces;
}
}
return [ this ];
}
matches(face) {
const fl = face.points.length;
if (fl != this.points.length) return false;
if (fl < 1) return true;
var first_id = undefined;
{
const hash0 = getPointHash(this.points[0]);
for (var i=0; i<fl; i++) {
const hash1 = getPointHash(face.points[i]);
if (hash0 == hash1) { first_id = i; break; }
}
}
if (first_id === undefined) return false;
for (var i=1; i<fl; i++) {
const j = (first_id - i + fl) % fl;
const hash0 = getPointHash(this.points[i]);
const hash1 = getPointHash(face.points[j]);
if (hash0 != hash1) {
for (var i2=1; i2<fl; i2++) {
const j2 = (first_id + i2) % fl;
const hash0 = getPointHash(this.points[i2]);
const hash1 = getPointHash(face.points[j2]);
if (hash0 != hash1) return false;
}
return true;
}
}
return true;
}
}
return {
addModel: (model) => { models.push(model); },
newModel: () => { return new Model(); },
processFrameModels: () => { models.forEach(model => { model.processFrame(); }); },
processOnceModels: () => { models.forEach(model => { model.processOnce(); }); },
modelCount: () => { return models.length; },
sortFaces: () => { all_faces.sort(function(a, b) { return a.z - b.z; }); },
nextFace: () => { return all_faces.shift(); },
faceCount: () => { return all_faces.length; },
addFace: (face) => { all_faces.push(face); },
};
}
class Matrix {
constructor() { this.identity(); }
identity() {
this.matrix = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ];
return this;
}
translate(tx, ty, tz) {
const m2 = new Matrix();
m2.matrix[12] = tx;
m2.matrix[13] = ty;
m2.matrix[14] = tz;
return this.multiply(m2);
}
scale(sx, sy, sz) {
this.matrix[0] *= sx;
this.matrix[5] *= sy;
this.matrix[10] *= sz;
return this;
}
rotateX(a) {
const m2 = new Matrix();
m2.matrix[5] = Math.cos(a);
m2.matrix[9] = Math.sin(a);
m2.matrix[6] = -Math.sin(a);
m2.matrix[10] = Math.cos(a);
return this.multiply(m2);
}
rotateY(a) {
const m2 = new Matrix();
m2.matrix[0] = Math.cos(a);
m2.matrix[8] = -Math.sin(a);
m2.matrix[2] = Math.sin(a);
m2.matrix[10] = Math.cos(a);
return this.multiply(m2);
}
rotateZ(a) {
const m2 = new Matrix();
m2.matrix[0] = Math.cos(a);
m2.matrix[1] = -Math.sin(a);
m2.matrix[4] = Math.sin(a);
m2.matrix[5] = Math.cos(a);
return this.multiply(m2);
}
multiply(rhs) {
const m1 = [...this.matrix];
const m2 = [...rhs.matrix];
this.matrix = [];
for(let n=0; 16>n; n+=4) {
for(let o=0; 4>o; o++) {
this.matrix[n+o] = m1[n+0] * m2[0+o] + m1[n+1] * m2[4+o] + m1[n+2] * m2[8+o] + m1[n+3] * m2[12+o];
}
}
return this;
}
transform(point) {
const p = [...point];
for (let i=0; i<3; i++) {
p[i] = this.matrix[i] * point[0] + this.matrix[i+4] * point[1] + this.matrix[i+8] * point[2] + this.matrix[i+12];
}
return p;
}
}
const cube_points = [];
for (var z=-1; z<=1; z+=2) { for (var y=-1; y<=1; y+=2) { for (var x=-1; x<=1; x+=2) { cube_points.push([x, y, z]); } } }
const cube_faces = [ [0, 1, 2, 3], [7, 3, 5, 1], [6, 2, 7, 3], [0, 4, 1, 5], [4, 0, 6, 2], [6, 7, 4, 5] ];
function makeBox(w, h, d) {
const model = silhouette.newModel();
cube_faces.forEach(f => {
const quad = [ [...cube_points[f[0]]], [...cube_points[f[1]]], [...cube_points[f[3]]], [...cube_points[f[2]]] ];
quad.forEach(point => { point[0] *= w/2; point[1] *= h/2; point[2] *= d/2; });
model.addFace(quad);
});
return model;
}
function makeCylinder(height, radius_top, radius_bottom, sides) {
const model = silhouette.newModel();
const y = height / 2;
const astep = Math.PI * 2 / sides;
for (var i=0; i<sides; i++) {
const a0 = i * astep, a1 = a0 + astep;
const xt0 = radius_top * Math.cos(a0), zt0 = radius_top * Math.sin(a0);
const xt1 = radius_top * Math.cos(a1), zt1 = radius_top * Math.sin(a1);
const xb0 = radius_bottom * Math.cos(a0), zb0 = radius_bottom * Math.sin(a0);
const xb1 = radius_bottom * Math.cos(a1), zb1 = radius_bottom * Math.sin(a1);
if (radius_bottom > 0) model.addFace([ [xb1, -y, zb1], [xt0, y, zt0], [xb0, -y, zb0] ]);
if (radius_top > 0) model.addFace([ [xt1, y, zt1], [xt0, y, zt0], [xb1, -y, zb1] ]);
if (radius_top > 0) model.addFace([ [xt0, y, zt0], [xt1, y, zt1], [0, y, 0] ]);
if (radius_bottom > 0) model.addFace([ [xb1, -y, zb1], [xb0, -y, zb0], [0, -y, 0] ]);
}
return model;
}
function makeSphere(radius, subdiv) {
const model = makeBox(1, 1, 1);
model.subdivideCount(subdiv);
model.faces.forEach(face => { for (var i=0; i<face.points.length; i++) face.points[i] = mulf(normalize3(face.points[i]), radius); });
return model;
}
function makeTower(height, radius) {
const model = silhouette.newModel();
var oy = 0;
const steps = 5;
for (var j=0; j<steps*2+1; j++) {
var matrix = new Matrix();
const h = ((j&1) ? 1/steps/1.1 : 0.03) * height;
const r = ((j&1) ? 1 : 1.1) * radius;
matrix.translate(0, oy+h/2, 0);
oy += h;
const mdl = makeCylinder(h, r, r, 12);
mdl.transform(matrix);
model.merge(mdl);
}
{
var matrix = new Matrix();
const h = height / 5;
matrix.translate(0, oy + h / 2, 0);
const mdl = makeCylinder(h, 0, radius, 12);
mdl.transform(matrix);
model.merge(mdl);
}
{
var matrix = new Matrix();
matrix.translate(0, oy + radius * 1.5, 0);
const mdl = makeSphere(radius * 0.5, 4);
mdl.transform(matrix);
model.merge(mdl);
}
{
var matrix = new Matrix();
const h = height / 2
matrix.translate(0, oy + h / 2, 0);
const mdl = makeCylinder(h, 0, radius * 0.25, 12);
mdl.transform(matrix);
model.merge(mdl);
}
return model;
}
function makeVoxelCube(r, count) {
const model = silhouette.newModel();
const grid = {};
const o = (-(count-1)/2) * r;
for (var c=0; c<count * 20; c++) {
const size = random() * count / 4 | 0;
const xo = random() * count | 0;
const yo = random() * count | 0;
const zo = random() * count | 0;
for (var z=0; z<size; z++) {
for (var y=0; y<size; y++) {
for (var x=0; x<size; x++) {
const key = (x+xo) + (y+yo) * count + (z+zo) * count * count;
grid[key] = true;
}
}
}
}
Object.keys(grid).forEach(key => {
if (grid[key]) {
const x = key % count;
const y = (key / count | 0) % count;
const z = key / count / count| 0;
var matrix = new Matrix();
matrix.translate(x * r + o, y * r + o, z * r + o);
const rs = 1;
const mdl = makeBox(r * rs, r * rs, r * rs);
mdl.transform(matrix);
model.merge(mdl);
}
});
return model;
}
function project(op) {
const p = transform4([op[0], op[2], op[1], 1], viewProjectionMatrix);
const s = 5 * perspective **3;
if (p[2] < 0) return undefined;
return [ p[0]/p[3]*s, -p[1]/p[3]*s, p[2] ];
}
////////////////////////////////////////////////////////////////
// Polygon Clipping utility code - Created by Reinder Nijhoff 2019
// (Polygon binning by Lionel Lemarie 2021)
// https://turtletoy.net/turtle/a5befa1f8d
////////////////////////////////////////////////////////////////
function Polygons(){const t=[],s=50,e=Array.from({length:s**2},t=>[]),n=class{constructor(){this.cp=[],this.dp=[],this.aabb=[]}addPoints(...t){let s=1e5,e=-1e5,n=1e5,h=-1e5;(this.cp=[...this.cp,...t]).forEach(t=>{s=Math.min(s,t[0]),e=Math.max(e,t[0]),n=Math.min(n,t[1]),h=Math.max(h,t[1])}),this.aabb=[s,n,e,h]}addSegments(...t){t.forEach(t=>this.dp.push(t))}addOutline(){for(let t=0,s=this.cp.length;t<s;t++)this.dp.push(this.cp[t],this.cp[(t+1)%s])}draw(t){for(let s=0,e=this.dp.length;s<e;s+=2)t.jump(this.dp[s]),t.goto(this.dp[s+1])}addHatching(t,s){const e=new n;e.cp.push([-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);const h=Math.sin(t)*s,o=Math.cos(t)*s,a=200*Math.sin(t),i=200*Math.cos(t);for(let t=.5;t<150/s;t++)e.dp.push([h*t+i,o*t-a],[h*t-i,o*t+a]),e.dp.push([-h*t+i,-o*t-a],[-h*t-i,-o*t+a]);e.boolean(this,!1),this.dp=[...this.dp,...e.dp]}inside(t){let s=0;for(let e=0,n=this.cp.length;e<n;e++)this.segment_intersect(t,[.1,-1e3],this.cp[e],this.cp[(e+1)%n])&&s++;return 1&s}boolean(t,s=!0){const e=[];for(let n=0,h=this.dp.length;n<h;n+=2){const h=this.dp[n],o=this.dp[n+1],a=[];for(let s=0,e=t.cp.length;s<e;s++){const n=this.segment_intersect(h,o,t.cp[s],t.cp[(s+1)%e]);!1!==n&&a.push(n)}if(0===a.length)s===!t.inside(h)&&e.push(h,o);else{a.push(h,o);const n=o[0]-h[0],i=o[1]-h[1];a.sort((t,s)=>(t[0]-h[0])*n+(t[1]-h[1])*i-(s[0]-h[0])*n-(s[1]-h[1])*i);for(let n=0;n<a.length-1;n++)(a[n][0]-a[n+1][0])**2+(a[n][1]-a[n+1][1])**2>=.001&&s===!t.inside([(a[n][0]+a[n+1][0])/2,(a[n][1]+a[n+1][1])/2])&&e.push(a[n],a[n+1])}}return(this.dp=e).length>0}segment_intersect(t,s,e,n){const h=(n[1]-e[1])*(s[0]-t[0])-(n[0]-e[0])*(s[1]-t[1]);if(0===h)return!1;const o=((n[0]-e[0])*(t[1]-e[1])-(n[1]-e[1])*(t[0]-e[0]))/h,a=((s[0]-t[0])*(t[1]-e[1])-(s[1]-t[1])*(t[0]-e[0]))/h;return o>=0&&o<=1&&a>=0&&a<=1&&[t[0]+o*(s[0]-t[0]),t[1]+o*(s[1]-t[1])]}};return{list:()=>t,create:()=>new n,draw:(n,h,o=!0)=>{reducedPolygonList=function(n){const h={},o=200/s;for(var a=0;a<s;a++){const c=a*o-100,r=[0,c,200,c+o];if(!(n[3]<r[1]||n[1]>r[3]))for(var i=0;i<s;i++){const c=i*o-100;r[0]=c,r[2]=c+o,n[0]>r[2]||n[2]<r[0]||e[i+a*s].forEach(s=>{const e=t[s];n[3]<e.aabb[1]||n[1]>e.aabb[3]||n[0]>e.aabb[2]||n[2]<e.aabb[0]||(h[s]=1)})}}return Array.from(Object.keys(h),s=>t[s])}(h.aabb);for(let t=0;t<reducedPolygonList.length&&h.boolean(reducedPolygonList[t]);t++);h.draw(n),o&&function(n){t.push(n);const h=t.length-1,o=200/s;e.forEach((t,e)=>{const a=e%s*o-100,i=(e/s|0)*o-100,c=[a,i,a+o,i+o];c[3]<n.aabb[1]||c[1]>n.aabb[3]||c[0]>n.aabb[2]||c[2]<n.aabb[0]||t.push(h)})}(h)}}}
// Random with seed
var rng;
function random() { if (rng === undefined) rng = new RNG(seed_t); return rng.nextFloat(); }
function RNG(t){return new class{constructor(t){this.m=2147483648,this.a=1103515245,this.c=12345,this.state=t||Math.floor(Math.random()*(this.m-1))}nextFloat(){return this.state=(this.a*this.state+this.c)%this.m,this.state/(this.m-1)}}(t)}
////////////////////////////////////////////////////////////////
// Projection from reinder's https://turtletoy.net/turtle/b3acf08303
let viewProjectionMatrix;
function setupCamera(t,e){const m=lookAt4m(t,e,[0,0,1]),n=perspective4m(.25,1);return multiply4m(n,m)}
function lookAt4m(o,n,r){const s=new Float32Array(16);n=normalize3(sub3(o,n)),r=normalize3(cross3(r,n));const t=normalize3(cross3(n,r));return s[0]=r[0],s[1]=t[0],s[2]=n[0],s[3]=0,s[4]=r[1],s[5]=t[1],s[6]=n[1],s[7]=0,s[8]=r[2],s[9]=t[2],s[10]=n[2],s[11]=0,s[12]=-(r[0]*o[0]+r[1]*o[1]+r[2]*o[2]),s[13]=-(t[0]*o[0]+t[1]*o[1]+t[2]*o[2]),s[14]=-(n[0]*o[0]+n[1]*o[1]+n[2]*o[2]),s[15]=1,s}
function perspective4m(t,n){const e=new Float32Array(16).fill(0,0);return e[5]=1/Math.tan(t/2),e[0]=e[5]/n,e[10]=e[11]=-1,e}
function multiply4m(t,r){const l=new Float32Array(16);for(let n=0;16>n;n+=4)for(let o=0;4>o;o++)l[n+o]=r[n+0]*t[0+o]+r[n+1]*t[4+o]+r[n+2]*t[8+o]+r[n+3]*t[12+o];return l}
function transform4(r,n){const t=new Float32Array(4);for(let o=0;4>o;o++)t[o]=n[o]*r[0]+n[o+4]*r[1]+n[o+8]*r[2]+n[o+12];return t}
function normalize3(a) { const len = len3(a); return scale3(a,len3<0.0001?1:1/len); }
function scale3(a,b) { return [a[0]*b,a[1]*b,a[2]*b]; }
function len3(a) { return Math.sqrt(dot3(a,a)); }
function add3(a,b) { return [a[0]+b[0],a[1]+b[1],a[2]+b[2]]; }
function sub3(a,b) { return [a[0]-b[0],a[1]-b[1],a[2]-b[2]]; }
function dot3(a,b) { return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]; }
function cross3(a,b) { return [a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]]; }
function mulf(v, f) { return [v[0]*f,v[1]*f,v[2]*f]; }
// Timer
const timers = {};
function timer_start(label) { timers[label] = Date.now(); }
function timer_end(label) {
var elapsed = (Date.now() - timers[label]) / 1000;
console.log(`${label}: ${elapsed.toFixed(1)} s`);
}