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