Forest of trees

Trees again. Now with leaves.
Increase the tree count to turn it into a veritable forest...

Log in to post a comment.

// LL 2021

const detail = 0.0;        /// min=0 max=1 step=0.1
const perspective = 1.6;   /// min=1 max=3 step=0.1
const camera_theta = 0.;   /// min=-1 max=1 step=0.05
const camera_phi = 0.;     /// min=-0.99 max=1 step=0.05
const camera_r = 15;       /// min=0.1 max=100 step=0.1
const look_at_z = 0;       /// min=-10 max=10 step=0.1
const inside_lines = 0.5;  /// 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
const leaf_detail = 8;     // min=0 max=10 step=1
const tree_count = 1;      // min=1 max=10 step=1
const line_thickness = 2;  // min=1 max=3 step=1

//const turtle = new Turtle();
const turtle = new Slowbro(); turtle.thickness = line_thickness;

const detail_max_length = Math.max(0.9, 0.9 + 5 * (1 - detail));

var silhouette = new Silhouette();

var noise = SimplexNoise(seed ? seed : Math.floor(Math.random() * 1000));

function walk(i, t) {
	if (i==0) {
		if (t == 0 || t == 1) initOnce();
		initFrame(t);
	}

	if (silhouette.faceCount() < 1) {
		console.log(`Slowbro: thickness: ${turtle.thickness} draw: ${turtle.slowpoke_draw}, skip: ${turtle.slowpoke_skip}`);
		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();
}

function initOnce(t) {
	seed_t = (t < 1 && seed == 0) ? (Math.random() * 100 | 0) : seed;
	rng = undefined;

	initScene();

	silhouette.processOnceModels();
}

function initScene() {
	const locations = [];
	for (var i=0; i<tree_count; i++) {
	    var x = 0, z=0;
	    for (var t=0; t<100; t++) {
	        var min_len = Number.MAX_VALUE;
	        locations.forEach(l => {
	            const dist = len3(sub3([x, 0, z], l));
	            min_len = Math.min(min_len, dist);
	        });
	        if (min_len > 10) break;
	        x = 0 - random() * 10;
	        z = (random() - 0.5) * 100;
	    }
	    locations.push([x, 0, z]);
		const matrix = new Matrix().translate(x, -30, z);
		makeTree(matrix);
	}
}

/////////////////////////////////////////////////////////////////////
// Prototype Silhouette utility code - Created by Lionel Lemarie 2021
// https://turtletoy.net/turtle/334500a2c5
function Silhouette(){const t=[],s=[];class e{constructor(){this.faces=[],this.edges=[],this.hide_overlap=!0,this.inside_lines=inside_lines}transform(t){this.faces.forEach(s=>{s.transform(t)})}processOnce(){detail&&style&&this.subdivideDetail(detail_max_length),this.updateEdgeList()}processFrame(){this.faces.forEach(t=>{t.projectAndAdd()}),this.findStaticOutlines(),this.findProjectedOutlines(),this.updateOutlineMasks()}updateOutlineMasks(){this.edges.forEach(t=>{t.faces.forEach(s=>{const e=s.points.length;if(e>1)for(var o=0;o<e;o++){const n=i(s.points[o]),h=i(s.points[(o+1)%e]);t.hash!=n+h&&t.hash!=h+n||(t.state?s.outline_mask|=1<<o:s.outline_mask&=~(1<<o))}s.outline_mask&=~s.hidden_line_mask})})}addFace(t){const s=new o(t);return this.faces.push(s),s}merge(t){t.faces.forEach(t=>{this.faces.push(t)})}subdivideDetail(t){for(var s=this.faces.length,e=!0;e;){const i=this.faces;this.faces=[],i.forEach(s=>{s.getSubdivided(t).forEach(t=>{this.faces.push(t)})}),e=this.faces.length!=s,s=this.faces.length}}subdivideCount(t){for(;t-- >0;){const t=this.faces;this.faces=[],t.forEach(t=>{t.getSubdivided(0).forEach(t=>{this.faces.push(t)})})}}updateEdgeList(){this.edges=[];const t={};this.faces.forEach(s=>{const e=s.points.length;if(e>1)for(var o=0;o<e;o++){const n=i(s.points[o]),h=i(s.points[(o+1)%e]);if(n+h in t){const e=t[n+h];this.addFaceToEdge(e,s)}else if(h+n in t){const e=t[h+n];this.addFaceToEdge(e,s)}else{const e=this.edges.length;t[n+h]=e,this.edges.push({faces:[s],hash:n+h,state:1})}}})}addFaceToEdge(t,s){var e=!0;this.edges[t].faces.forEach(t=>{t.matches(s)&&(e=!1,this.hide_overlap&&(t.overlap=!0),s.overlap=!0)}),e&&this.edges[t].faces.push(s)}findStaticOutlines(){this.edges.forEach(t=>{t.state=1;for(var s=0,e=t.faces.length;s<e&&t.state;s++){if(t.faces[s].overlap)continue;const o=t.faces[s].getStaticNormal();for(var i=s+1;i<e&&t.state;i++){if(t.faces[i].overlap)continue;const s=t.faces[i].getStaticNormal();len3(sub3(o,s))<2*this.inside_lines&&(t.state=0)}}})}findProjectedOutlines(){this.edges.forEach(t=>{var s=!1,e=!1;t.faces.forEach(t=>{if(!t.overlap){const i=t.getProjectedNormal(),o=.001;i[2]<o&&(e=!0),i[2]>=-o&&(s=!0)}}),s&&e&&(t.state=1)})}}function i(t){return`${Math.round(100*t[0])},${Math.round(100*t[1])},${Math.round(100*t[2])}`}class o{constructor(t){this.points=[...t],this.z=0,this.projected_points=[],this.outline_mask=-1,this.hidden_line_mask=0,this.overlap=!1}draw(){if(!(this.projected_points.length<2||this.overlap)){var t=!0;if(this.projected_points.forEach(s=>t&=Math.min(Math.abs(s[0]),Math.abs(s[1]))<100),t)if(0==style)for(var s=0;s<this.projected_points.length;s++)this.outline_mask&1<<s&&(turtle.jump(this.projected_points[s]),turtle.goto(this.projected_points[(s+1)%this.projected_points.length]));else{const t=polygons.create();if(t.addPoints(...this.projected_points),1==style)t.addOutline();else if(style>1){for(s=0;s<this.projected_points.length;s++)this.outline_mask&1<<s&&t.addSegments(t.cp[s],t.cp[(s+1)%this.projected_points.length]);if(style>2){const s=.25,e=s+(1.5-s)*this.getLight();t.addHatching(-Math.PI/4,e)}}polygons.draw(turtle,t,!0)}}}getStaticNormal(){return void 0===this.cached_static_normal&&(this.points.length<3?this.cached_static_normal=[0,1,0]:this.cached_static_normal=normalize3(cross3(sub3(this.points[1],this.points[0]),sub3(this.points[2],this.points[0])))),this.cached_static_normal}getProjectedNormal(){return this.projected_points.length<3?[0,1,0]:normalize3(cross3(sub3(this.projected_points[1],this.projected_points[0]),sub3(this.projected_points[2],this.projected_points[0])))}getLight(){return.5*this.getProjectedNormal()[0]+.5}transform(t){for(var s=0,e=this.points.length;s<e;s++)this.points[s]=t.transform(this.points[s])}projectAndAdd(){this.overlap||(this.projected_points=[],this.z=0,this.points.forEach(t=>{const s=project(t);void 0!==s&&(this.projected_points.push(s),this.z+=s[2])}),this.projected_points.length>0&&(this.z/=this.projected_points.length),silhouette.addFace(this))}getSubdivided(t){var s=-1;const e=this.points.length;for(var i=0;i<e;i++){const o=len3(sub3(this.points[(i+1)%e],this.points[i]));o>t&&(t=o,s=i)}if(s>=0){if(4==e){const t=[],s=new o([this.points[0],this.points[1],this.points[2]]);t.push(s),s.hidden_line_mask=3&this.hidden_line_mask;const e=new o([this.points[2],this.points[3],this.points[0]]);return t.push(e),e.hidden_line_mask=this.hidden_line_mask>>2&3,t}{const t=[],n=mulf(add3(this.points[(s+1)%e],this.points[s]),.5),h=1&this.hidden_line_mask,a=this.hidden_line_mask>>1&1,r=this.hidden_line_mask>>2&1,c=[[[],[a,0,h],[r,h,0]],[[h,a,0],[],[r,0,a]],[[h,0,r],[a,r,0],[]]];for(i=0;i<e;i++)if(i!=s){const h=new o([this.points[i],this.points[(i+1)%e],n]);h.hidden_line_mask=0,c[s][i].forEach((t,s)=>{h.hidden_line_mask|=t<<s}),t.push(h)}return t}}return[this]}matches(t){const s=t.points.length;if(s!=this.points.length)return!1;if(s<1)return!0;var e=void 0;{const n=i(this.points[0]);for(var o=0;o<s;o++){if(n==i(t.points[o])){e=o;break}}}if(void 0===e)return!1;for(o=1;o<s;o++){const h=(e-o+s)%s;if(i(this.points[o])!=i(t.points[h])){for(var n=1;n<s;n++){const o=(e+n)%s;if(i(this.points[n])!=i(t.points[o]))return!1}return!0}}return!0}}return{addModel:s=>{t.push(s)},newModel:()=>new e,processFrameModels:()=>{t.forEach(t=>{t.processFrame()})},processOnceModels:()=>{t.forEach(t=>{t.processOnce()})},modelCount:()=>t.length,sortFaces:()=>{s.sort(function(t,s){return t.z-s.z})},nextFace:()=>s.shift(),faceCount:()=>s.length,addFace:t=>{s.push(t)}}}
class Matrix{constructor(){this.identity(),this.stack=[]}identity(){return this.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],this}translate(t,i,r){const a=new Matrix;return a.matrix[12]=t,a.matrix[13]=i,a.matrix[14]=r,this.multiply(a)}scale(t,i,r){void 0===i&&(i=t),void 0===r&&(r=i);const a=new Matrix;return a.matrix[0]*=t,a.matrix[5]*=i,a.matrix[10]*=r,this.multiply(a)}rotateX(t){const i=new Matrix;return i.matrix[5]=Math.cos(t),i.matrix[9]=Math.sin(t),i.matrix[6]=-Math.sin(t),i.matrix[10]=Math.cos(t),this.multiply(i)}rotateY(t){const i=new Matrix;return i.matrix[0]=Math.cos(t),i.matrix[8]=-Math.sin(t),i.matrix[2]=Math.sin(t),i.matrix[10]=Math.cos(t),this.multiply(i)}rotateZ(t){const i=new Matrix;return i.matrix[0]=Math.cos(t),i.matrix[1]=-Math.sin(t),i.matrix[4]=Math.sin(t),i.matrix[5]=Math.cos(t),this.multiply(i)}multiply(t){const i=[...this.matrix],r=[...t.matrix];this.matrix=[];for(let t=0;16>t;t+=4)for(let a=0;4>a;a++)this.matrix[t+a]=i[t+0]*r[0+a]+i[t+1]*r[4+a]+i[t+2]*r[8+a]+i[t+3]*r[12+a];return this}transform(t){const i=[...t];for(let r=0;r<3;r++)i[r]=this.matrix[r]*t[0]+this.matrix[r+4]*t[1]+this.matrix[r+8]*t[2]+this.matrix[r+12];return i}push(){this.stack.push([...this.matrix])}pop(){this.matrix=this.stack.pop()}}

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

class Branch {
	constructor(xyz, r, dir, length, factor, is_leaf) {
		this.xyz = xyz;
		this.r = r;
		this.dir = dir;
		this.length = length;
		this.factor = factor;
		this.is_leaf = is_leaf;
		this.children = [];
	}

	addModels(matrix) {
		if (this.is_leaf && leaf_detail) {
			const model = makeLeaves(1);
			const matrix2 = new Matrix();
			matrix2.scale(this.r, this.r / 2, this.r);
			matrix2.multiply(matrix);
			matrix2.translate(this.xyz[0], this.xyz[1], this.xyz[2]);
			model.transform(matrix2);
			silhouette.addModel(model);
		}

		if (!this.is_leaf) {
			const xyzt = add3(this.xyz, mulf(this.dir, this.length));
			const rt = this.r * this.factor;
			const rf = 1;
			const sides = 8;
			const model2 = makeCylinderP(xyzt, this.xyz, rt * rf, this.r * rf, sides);
			model2.transform(matrix);
			silhouette.addModel(model2);
		}

		this.children.forEach(child => { child.addModels(matrix); });
	}

	getTop(factor) {
		return add3(this.xyz, mulf(this.dir, this.length * factor));
	}

	addBranches(max, depth, leaf_r) {
		if (depth < 1) {
			this.is_leaf = true;
			this.r = leaf_r * (1 + random());
			return;
		}

		const max_splits = 30;
		const max_children = Math.min(max, 3);
		const splits = 2 + (random() * max_splits >> 0);

		const r = this.r * this.factor;
		const length = this.length * (this.factor ** 1);
		const factor = Math.max(0.7, this.factor * this.factor);
		const is_leaf = false;

		const p = [...this.dir]; if (this.dir[0] == 0 && this.dir[2] == 0) p[0]++; else p[1]++;
		const q  = normalize3(cross3(p, this.dir));
		const q2 = normalize3(cross3(this.dir, q));

		for (var i=0; i < splits; i++) {
			if (i < splits-1) if (random() > (1 / splits) ** 0.5) continue;
			var n = [0.1, -1, 0];
			for (var j=0; j<5; j++) {
				const a = Math.PI * (random() + 2 / splits * i);
				const c = Math.cos(a), s = Math.sin(a);
				var n2 = [ c * q2[0] + s * q[0], c * q2[1] + s * q[1], c * q2[2] + s * q[2] ];
				n2 = normalize3(n2);
				if (n[1] < n2[1]) n = n2;
			}
			const l = (max == 1) ? 0 : (length / 10);
			const dir = normalize3(add3(this.dir, mulf(n, l)));

			const tf = (this.children.length < 1) ? 1 : (random());
			const xyz = this.getTop(tf);
			
			const new_branch = new Branch(xyz, r, dir, length, factor, is_leaf);
			this.children.push(new_branch);
			new_branch.addBranches(max_splits, depth-1, leaf_r);
			if (new_branch.is_leaf) break;
			if (this.children.length >= max_children) break;
			if ((this.r < .5 || this.length < 2) && this.children.length > 0) break;
		}

		if (this.children.length < 1) {
			this.is_leaf = true;
			this.r = leaf_r;
		}
	}
}

function makeTree(matrix) {

	const root_r = 1;
	const root_l = 10;

	{
		const model = makeTuft(30, root_r * 3, root_r / 2, root_l / 3);
		//const model = makeTuft(50, root_r * 10, root_r / 2, root_l / 3);
		model.transform(matrix);
		silhouette.addModel(model);
	}

	const factor = 0.95;
	const x = 0, y = 0.1, z = 0;
	const n = normalize3([x, y, z]);
	const xyz = mulf(n, 0);

	const root = new Branch(xyz, root_r, n, root_l, factor, false);
	root.addBranches(1, 10, 4);
	root.addModels(matrix);
}

function makeTuft(count, s, r, h) {
	const model = silhouette.newModel();

	for (var i=0; i<count; i++) {
		const a = (random() - 0.5) * 2 * Math.PI * 2;
		var xyz = [0, 2, 0]; while (len3(xyz) > 1) xyz = [(random() - 0.5) * 2, 0, (random() - 0.5) * 2]; xyz = mulf(xyz, s);
		var top = [ xyz[0], h * (2 + random()), xyz[2] ];
		//var top = [Math.cos(xyz[0] * .4), h, Math.cos(xyz[2] * .4)];
		top = add3(xyz, mulf(normalize3(top), h * ((random() - 0.5) * 0.2 + 1)));
		const sides = 6;
		for (var k=0; k<sides; k++) {
			const a0 = Math.PI * 2 * k / sides, a1 = a0 + Math.PI * 2 / sides;
			var b0 = add3(xyz, [ Math.cos(a0) * r, 0, Math.sin(a0) * r ]);
			var b1 = add3(xyz, [ Math.cos(a1) * r, 0, Math.sin(a1) * r ]);
			model.addFace([b0, top, b1]);
		}
	}

	return model;
}

function makeCylinderP(p_top, p_bottom, radius_top, radius_bottom, sides) {
	const model = silhouette.newModel();

	const n = sub3(p_top, p_bottom);
	const p = [...n]; if (n[0] == 0 && n[2] == 0) p[0]++; else p[1]++;
	const q  = normalize3(cross3(p, n));
	const q2 = normalize3(cross3(n, q));

	const astep = Math.PI * 2 / sides;
	for (var i=0; i<sides; i++) {
		const a0 = i * astep, a1 = a0 + astep;

		const c0 = Math.cos(a0), s0 = Math.sin(a0), c1 = Math.cos(a1), s1 = Math.sin(a1);
		var n0 = [ c0 * q2[0] + s0 * q[0], c0 * q2[1] + s0 * q[1], c0 * q2[2] + s0 * q[2] ];
		var n1 = [ c1 * q2[0] + s1 * q[0], c1 * q2[1] + s1 * q[1], c1 * q2[2] + s1 * q[2] ];
		n0 = normalize3(n0); n1 = normalize3(n1);

		const t0 = add3(p_top,    mulf(n0, radius_top   ));
		const t1 = add3(p_top,    mulf(n1, radius_top   ));
		const b0 = add3(p_bottom, mulf(n0, radius_bottom));
		const b1 = add3(p_bottom, mulf(n1, radius_bottom));

		const face = model.addFace([ t0, t1, b1, b0 ]);
		face.hidden_line_mask = 5;
	}

	return model;
}

function makeSphere(radius, subdiv=0) {
	if (radius < 0.0001) return silhouette.newModel();

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

// Input: 3D point in -1,1 range
function heightField3D(xyz) {
	const factor = 1.8;
	let x = xyz[0] * factor, y = xyz[1] * factor, z = xyz[2] * factor;

	let height = noise.noise3D([x,y,z]);
	height += .55 * noise.noise3D([x*2+3.4, y*2-56.1, z*2-4.5]);
	height += .35 * Math.abs(1-noise.noise3D([x*4+21.2, y*4+.5, z*4-8.7]));
	height += .25 * Math.abs(1-noise.noise3D([x*8+421.12, y*8+21.3, z*8-45.3]));
	height -= 0.5;
	height *= -0.4;
	height = Math.max(height, 0);
	const max = 0.5;
	height = Math.min(height, max);
	height *= 0.15 / max;
	height++;
	return height;
}

function makeLeaves(radius) {
	noise = SimplexNoise(Math.floor(Math.random() * 1000));
	const model = makeBox(1, 1, 1);
	model.inside_lines = .65;
	model.subdivideCount(leaf_detail);
	model.faces.forEach(face => {
		for (var i=0; i<face.points.length; i++) {
			face.points[i] = normalize3(face.points[i]);
			const h = ((heightField3D(face.points[i]) ** 2) - 1) * (leaf_detail ** 0.3);
			face.points[i] = mulf(face.points[i], radius * (1 + h));
		}
	});
	
	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=25,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)}}}

////////////////////////////////////////////////////////////////
// Simplex Noise utility code. Created by Reinder Nijhoff 2020
// https://turtletoy.net/turtle/6e4e06d42e
// Based on: http://webstaff.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf
////////////////////////////////////////////////////////////////
function SimplexNoise(t=1){const a=[[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]],o=new Uint8Array(512),r=(Math.sqrt(3)-1)/2,h=1/3,n=(3-Math.sqrt(3))/6,s=1/6,e=(t,a)=>t[0]*a[0]+t[1]*a[1],M=(t,a)=>[t[0]-a[0],t[1]-a[1]],l=(t,a)=>t[0]*a[0]+t[1]*a[1]+t[2]*a[2],c=(t,a)=>[t[0]-a[0],t[1]-a[1],t[2]-a[2]];return new class{constructor(t=1){for(let t=0;t<512;t++)o[t]=255&t;for(let a=0;a<255;a++){const r=(t=this.hash(a+t))%(256-a)+a,h=o[a];o[a+256]=o[a]=o[r],o[r+256]=o[r]=h}}noise2D(t){const h=e(t,[r,r]),s=[Math.floor(t[0]+h),Math.floor(t[1]+h)],l=255&s[0],c=255&s[1],f=e(s,[n,n]),m=M(t,M(s,[f,f])),x=m[0]>m[1]?[1,0]:[0,1],i=M(M(m,x),[-n,-n]),u=M(m,[1-2*n,1-2*n]);let q=Math.max(0,.5-e(m,m))**4*e(a[o[l+o[c]]%12],m);return q+=Math.max(0,.5-e(i,i))**4*e(a[o[l+x[0]+o[c+x[1]]]%12],i),70*(q+=Math.max(0,.5-e(u,u))**4*e(a[o[l+1+o[c+1]]%12],u))}noise3D(t){const r=l(t,[h,h,h]),n=[Math.floor(t[0]+r),Math.floor(t[1]+r),Math.floor(t[2]+r)],e=255&n[0],M=255&n[1],f=255&n[2],m=l(n,[s,s,s]),x=c(t,c(n,[m,m,m])),[i,u]=x[0]>=x[1]?x[1]>=x[2]?[[1,0,0],[1,1,0]]:x[0]>=x[2]?[[1,0,0],[1,0,1]]:[[0,0,1],[1,0,1]]:x[1]<x[2]?[[0,0,1],[0,1,1]]:x[0]<x[2]?[[0,1,0],[0,1,1]]:[[0,1,0],[1,1,0]],q=c(c(x,i),[-s,-s,-s]),w=c(c(x,u),[-2*s,-2*s,-2*s]),D=c(x,[1-3*s,1-3*s,1-3*s]);let p=Math.max(0,.6-l(x,x))**4*l(a[o[e+o[M+o[f]]]%12],x);return p+=Math.max(0,.6-l(q,q))**4*l(a[o[e+i[0]+o[M+i[1]+o[f+i[2]]]]%12],q),p+=Math.max(0,.6-l(w,w))**4*l(a[o[e+u[0]+o[M+u[1]+o[f+u[2]]]]%12],w),32*(p+=Math.max(0,.6-l(D,D))**4*l(a[o[e+1+o[M+1+o[f+1]]]%12],D))}hash(t){const a=1103515245*((t=1103515245*(t>>1^t))^t>>3);return a^a>>16}}(t)}


// 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]; }

////////////////////////////////////////////////////////////////
// Slowbro utility code. Created by Lionel Lemarie 2021
// Built on Reinder's Slowpoke, which removes most duplicate lines
// Slowbro adds optional thickness to the lines
////////////////////////////////////////////////////////////////
function Slowbro(x, y) {
    const linesDrawn = {};
    class Slowbro extends Turtle {
        constructor(x, y) {
        	super(x, y);
        	this.thickness = 1; this.offset = 0.2;
			this.slowpoke_skip = this.slowpoke_draw = 0;
        }
        goto(x, y) {
            if (Array.isArray(x)) { y = x[1]; x = x[0]; }
            const ox = this.x(), oy = this.y();
            if (this.isdown()) {
                const p = [x, y], o = [ox, oy];
                const h1 = o[0].toFixed(2) + '_' + p[0].toFixed(2) + o[1].toFixed(2) + '_' + p[1].toFixed(2);
                const h2 = p[0].toFixed(2) + '_' + o[0].toFixed(2) + p[1].toFixed(2) + '_' + o[1].toFixed(2);
                if (linesDrawn[h1] || linesDrawn[h2]) {
                    super.up(); super.goto(p); super.down();
                    this.slowpoke_skip++;
                    return;
                }
                linesDrawn[h1] = linesDrawn[h2] = true;
                this.slowpoke_draw++;

            	for (var dx = this.thickness-1; dx >=0 ; dx--) {
            		for (var dy = this.thickness-1; dy >= 0; dy--) {
            			if (!dx && !dy) continue;
            			super.goto( x + dx * this.offset,  y + dy * this.offset);
            			super.goto(ox + dx * this.offset, oy + dy * this.offset);
            		}
            	}
            } 
            super.goto(x, y);
        }
    }
    return new Slowbro(x, y);
}