// 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 = 10;       /// min=0.1 max=100 step=0.1
const look_at_z = -4;      /// min=-10 max=10 step=0.1
const inside_lines = 0.85; /// 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 hair_count = 100;    // min=1 max=1000 step=1
const hair_subdiv = 20;    // min=1 max=30 step=1
const physics_steps = 18;  // min=0 max=100 step=1
const thickness = 1;       //// min=1 max=3 step=1

const turtle = new Slowbro(); turtle.thickness = thickness;

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

var silhouette = new Silhouette();

function walk(i) {
	if (i==0) {
		initFrame(1);
	}

	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) {
	seed_t = (t < 1 && seed == 0) ? (Math.random() * 100 | 0) : seed;
	rng = undefined;

    physics_steps_t = physics_steps * t;

	initScene();

	silhouette.processOnceModels();

	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 initScene() {
	const matrix = new Matrix().translate(0, 0, 0);
	makeStrands(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;
}

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

function makeStrands(matrix) {
	const len = 20;
	const sides = 4;

	const strands = [];

	const sphere_radius = 5;
	const radius = 0.25;

	for (var i=0; i<hair_count; i++) {
		const dir = normalize3([ random() - 0.5, (random() - 0.5) * 0.5, random() - 0.5 ]);
		const xyz = mulf(dir, sphere_radius);
		strands.push(new Strand(xyz, dir, radius, len, hair_subdiv));
	}

	for (var i=0; i<physics_steps_t; i++) {
		strands.forEach(strand => {
			strand.update();
		});
	}

	{
		const model = makeSphere(sphere_radius, 7);
		model.transform(matrix);
		silhouette.addModel(model);
	}

	strands.forEach(strand => {
		const model = strand.getModel(sides);
		model.transform(matrix);
		silhouette.addModel(model);
	});
}

class Strand {
	constructor(xyz, dir, radius, len, count, sphere_radius) {
		this.length = len;
		this.count = count;
		this.slices = [];
		this.oxyz = xyz;
		this.odir = dir;
		this.radius = radius;
		this.sphere_radius = sphere_radius;
		this.init();
	}

	init() {
		this.slices = [];
		this.links = []
		var xyz = this.oxyz;
		var radius = this.radius;

		const diff = mulf(this.odir, this.length/this.count);

		for (var i=0; i<this.count; i++) {
			const r = radius * (this.count-1 - i) / (this.count-1);
			this.slices.push(new Slice(xyz, r, i <= 2));
			xyz = add3(xyz, diff);
		}

		for (var i=0; i<this.slices.length-1; i++) {
			const s0 = this.slices[i];
			const s1 = this.slices[i+1];
			const len = len3(sub3(s1.xyz, s0.xyz));
			this.links.push(new Link(i, i+1, len));
			if (i < this.slices.length-2) {
				const s2 = this.slices[i+2];
				const len2 = len3(sub3(s2.xyz, s0.xyz));
				this.links.push(new Link(i, i+2, len2));
			}
		}

		//console.log(this.links);
	}

	update() {
		for (var i=0; i < this.slices.length; i++) {
			const s = this.slices[i];
			if (s.locked) continue;

			var v = sub3(s.xyz, s.pxyz);
			s.pxyz = [...s.xyz];

			const gravity = -0.1, friction = 0.9999;
			v[1] += gravity;
			v = mulf(v, friction);

			//const l = len3(v);
			//if (l > 1) v = mulf(v, 1/l);

			s.xyz = add3(s.xyz, v);
		}

		const loops = 50;
		for (var j=0; j < loops; j++) {
			for (var i=0; i < this.links.length; i++) {
				const s0 = this.slices[this.links[i].i0];
				const s1 = this.slices[this.links[i].i1];
				if (s0.locked && s1.locked) continue;
				var dir = sub3(s1.xyz, s0.xyz);
				const len = len3(dir);
				if (len < 0.01) continue;
				dir = mulf(dir, 1 / len);
				dir = mulf(dir, len - this.links[i].len);
				dir = mulf(dir, 0.5);
				if (!s0.locked) s0.xyz = add3(s0.xyz, dir);
				if (!s1.locked) s1.xyz = sub3(s1.xyz, dir);
			}
		}

		for (var i=0; i < this.slices.length; i++) {
			const s = this.slices[i];
			if (s.locked) continue;
			const sphere_dist = len3(s.xyz) - s.r - this.sphere_radius;
			if (sphere_dist < 0) {
				s.xyz = mulf(normalize3(s.xyz), this.sphere_radius + s.r);
			}
		}
	}

	getModel(sides) {
		const model = silhouette.newModel();

		for (var i=1; i<this.slices.length; i++) {
			const s0 = this.slices[i-1];
			const s1 = this.slices[i];
			const s2 = (i < this.slices.length-1) ? this.slices[i+1] : undefined;

			const dir0 = sub3(s1.xyz, s0.xyz);
			const dir1 = (s2 === undefined) ? dir0 : sub3(s2.xyz, s1.xyz);

			const disc0 = getDisc(s0.xyz, dir0, s0.radius, sides);
			const disc1 = getDisc(s1.xyz, dir1, s1.radius, sides);

			var jfirst = 0, min_jdist = Number.MAX_VALUE;
		//	for (var j=0; j<disc0.length; j++) {
		//		const len = len3(disc0[j]);
		//		if (len < min_jdist) { min_jdist = len; jfirst = j; }
		//	}
			var kfirst = 0, min_kdist = Number.MAX_VALUE;
			const kt = add3(sub3(disc0[jfirst], s0.xyz), s1.xyz);
			for (var k=0; k<disc1.length; k++) {
				const len = len3(sub3(disc1[k], kt));
				if (len < min_kdist) { min_kdist = len; kfirst = k; }
			}

			for (var j=0; j<disc0.length; j++) {
				const j0 = (j + jfirst) % disc0.length;
				const j1 = (j0 + 1) % disc0.length;
				const k0 = (j + kfirst) % disc1.length;
				const k1 = (k0 + 1) % disc1.length;
				const t0 = disc0[j0], t1 = disc0[j1];
				const b0 = disc1[k0], b1 = disc1[k1];
				const face = [t0, t1, b1, b0];
				model.addFace(face);
			}
		}

		return model;
	}
}

class Link {
	constructor(i0, i1, len) {
		this.i0 = i0;
		this.i1 = i1;
		this.len = len;
	}
}

class Slice {
	constructor(xyz, radius, locked) {
		this.xyz = [...xyz];
		this.pxyz = [...xyz];
		this.radius = radius;
		this.locked = locked;
	}
}

function getDisc(xyz, dir, radius, sides) {
	const points = [];
	const n = dir;
	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;
		const c0 = Math.cos(a0), s0 = Math.sin(a0);
		const n0 = normalize3([ c0 * q2[0] + s0 * q[0], c0 * q2[1] + s0 * q[1], c0 * q2[2] + s0 * q[2] ]);
		const t0 = add3(xyz, mulf(n0, radius));
		points.push(t0);
	}

	return points;
}

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

// 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
// Based on 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);
}