Particle detector 📡

Witness a storm of particles through a detector with the sensor units firing!

When exporting as an animated GIF enable 'Draw different frames.' to see the animation of the storm in action. Using the 4D simplex from Simplex Port ⛴️ a looped animation will be continuous: you won't notice the start and end of a single cycle when looping.

Log in to post a comment.

const turbulence = 1; //min=.01 max=5 step=.01
const stormDirection = 0; //min=0 max=360 step=1
const rings = 6; //min=1 max=10 step=1
const segmentsPerRing = 16; //min=2 max=40 step=2
const segmentMargin = 2; //min=0 max=5 step=.1
const squaresPerSegment = 8; //min=1 max=15 step=1

Canvas.setpenopacity(.8);

/*
// Seedable random number generator by David Bau: http://davidbau.com/archives/2010/01/30/random_seeds_coded_hints_and_quintillions.html
!function(a,b,c,d,e,f,g,h,i){function j(a){var b,c=a.length,e=this,f=0,g=e.i=e.j=0,h=e.S=[];for(c||(a=[c++]);d>f;)h[f]=f++;for(f=0;d>f;f++)h[f]=h[g=s&g+a[f%c]+(b=h[f])],h[g]=b;(e.g=function(a){for(var b,c=0,f=e.i,g=e.j,h=e.S;a--;)b=h[f=s&f+1],c=c*d+h[s&(h[f]=h[g=s&g+b])+(h[g]=b)];return e.i=f,e.j=g,c})(d)}function k(a,b){var c,d=[],e=typeof a;if(b&&"object"==e)for(c in a)try{d.push(k(a[c],b-1))}catch(f){}return d.length?d:"string"==e?a:a+"\0"}function l(a,b){for(var c,d=a+"",e=0;e<d.length;)b[s&e]=s&(c^=19*b[s&e])+d.charCodeAt(e++);return n(b)}function m(c){try{return o?n(o.randomBytes(d)):(a.crypto.getRandomValues(c=new Uint8Array(d)),n(c))}catch(e){return[+new Date,a,(c=a.navigator)&&c.plugins,a.screen,n(b)]}}function n(a){return String.fromCharCode.apply(0,a)}var o,p=c.pow(d,e),q=c.pow(2,f),r=2*q,s=d-1,t=c["seed"+i]=function(a,f,g){var h=[];f=1==f?{entropy:!0}:f||{};var o=l(k(f.entropy?[a,n(b)]:null==a?m():a,3),h),s=new j(h);return l(n(s.S),b),(f.pass||g||function(a,b,d){return d?(c[i]=a,b):a})(function(){for(var a=s.g(e),b=p,c=0;q>a;)a=(a+c)*d,b*=d,c=s.g(1);for(;a>=r;)a/=2,b/=2,c>>>=1;return(a+c)/b},o,"global"in f?f.global:this==c)};if(l(c[i](),b),g&&g.exports){g.exports=t;try{o=require("crypto")}catch(u){}}else h&&h.amd&&h(function(){return t})}(this,[],Math,256,6,52,"object"==typeof module&&module,"function"==typeof define&&define,"random");
Math.seedrandom(new Date().toDateString());
*/

init();
const turtle = new Turtle();
const sn = SquareTiledSimplex2D(Simplex().createNoise4D(), 200, turbulence);//new SimplexNoise((Math.random() * 100) | 0);
const sdm = V.rot2d(stormDirection * 2 * Math.PI / 360);

// The walk function will be called until it returns false.
function walk(i, t) {
    function samplePoint(pt) {
        return N.clamp(.1 + .9*(sn.sample(...V.rotateClamp(
            V.add(V.scale([200,0], t), V.trans(sdm, pt)),
            [-100, -100],
            [100, 100]
        )) / 2 + .5), 0, 1);
    }
    
    const ring = i / segmentsPerRing | 0;
    const segment = i % segmentsPerRing;
    
    const innerR = 17 + ring * squaresPerSegment * 3;
    const outerR = 17 + (ring + 1) * squaresPerSegment * 3 - segmentMargin;
    
    let useSegmentsPerRing = segmentsPerRing;
    while(innerR * 2 * Math.PI / useSegmentsPerRing < 10) {
        useSegmentsPerRing /= 2;
    }
    
    const [innerThetaPerSegment, outerThetaPerSegment, innerStartTheta, outerStartTheta] = getThetaBounds(segment, useSegmentsPerRing, innerR, outerR);

    function getThetaBounds(segment, segmentsPerRing, innerR, outerR) {
        const innerThetaMargin = segmentMargin / innerR;
        const outerThetaMargin = segmentMargin / outerR;
        const innerThetaPerSegment = 2 * Math.PI / segmentsPerRing - innerThetaMargin;
        const outerThetaPerSegment = 2 * Math.PI / segmentsPerRing - outerThetaMargin;
        return [
            innerThetaPerSegment,
            outerThetaPerSegment,
            innerThetaMargin / 2 + (innerThetaPerSegment + innerThetaMargin) * segment,
            outerThetaMargin / 2 + (outerThetaPerSegment + outerThetaMargin) * segment
        ];
    }

    const innerPts = PT.arc(innerR, innerThetaPerSegment, innerStartTheta, null, true);
    const outerPts = PT.arc(outerR, outerThetaPerSegment, outerStartTheta, null, true).reverse();
    PT.drawTour(turtle, innerPts.concat(outerPts));
    
    const rowR = (outerR - innerR) / squaresPerSegment;
    for(let row = 0; row < squaresPerSegment; row++) {
        const rInnerR = innerR + row * rowR;
        const rOuterR = innerR + (row+1) * rowR;

        const [cInnerThetaPerSegment, cOuterThetaPerSegment, cInnerStartTheta, cOuterStartTheta] = getThetaBounds(segment, useSegmentsPerRing, rInnerR, rOuterR);
        
        for(let isle = 0; isle < squaresPerSegment; isle++) {
            const midR = (rOuterR + rInnerR) / 2;
            const midTheta = (cOuterStartTheta + cInnerStartTheta)/2 + ((cOuterThetaPerSegment + cInnerThetaPerSegment) / 2)/squaresPerSegment * (isle + .5);
            const midPoint = [
                midR * Math.cos(midTheta),
                midR * Math.sin(midTheta)
            ];
            
            //PT.drawPoint(turtle, midPoint);
            
            const cv = samplePoint(midPoint)
            const rDiff = (rOuterR - rInnerR) * (1-cv) / 2;
            const srInnerR = rInnerR + rDiff;
            const srOuterR = rOuterR - rDiff;

            const [scInnerThetaPerSegment, scOuterThetaPerSegment, scInnerStartTheta, scOuterStartTheta] = getThetaBounds(segment, useSegmentsPerRing, srInnerR, srOuterR);

            const innerThetaDiff = scInnerThetaPerSegment / squaresPerSegment * (1-cv) / 2;
            const outerThetaDiff = scOuterThetaPerSegment / squaresPerSegment * (1-cv) / 2;

            const innerReach = scInnerThetaPerSegment / squaresPerSegment * cv;
            const outerReach = scOuterThetaPerSegment / squaresPerSegment * cv;
            const cellInnerPts = PT.arc(srInnerR, innerReach, Math.max(0, cInnerStartTheta + isle * cInnerThetaPerSegment / squaresPerSegment + innerThetaDiff), Math.max(3, innerThetaDiff * 2 * srInnerR * 10 | 0), true);
            const cellOuterPts = PT.arc(srOuterR, outerReach, Math.max(0, cOuterStartTheta + isle * cOuterThetaPerSegment / squaresPerSegment + outerThetaDiff), Math.max(3, outerThetaDiff * 2 * srOuterR * 10 | 0), true).reverse();
            PT.drawTour(turtle, cellInnerPts.concat(cellOuterPts));
        }
    }
    
    const SQRT3 = 3**.5;
    const SQRT3over2 = SQRT3/2;

    const hexR = 3.6;
    const baseHex = PT.circular(hexR, 6);
    for(let x = -5; x <= 5; x++) {
        for(let y = -10; y <= 10; y++) {
            const l = [
                (Math.abs(y) % 2 == 1? hexR * Math.SQRT2: 0) + hexR * x * 3,
                hexR * SQRT3over2 * y
            ];
            if(V.lenSq(l) > ((17-hexR)**2)*.9) continue;
            
            const v = samplePoint(l);
            PT.drawTour(turtle, baseHex.map(pt => V.add(V.scale(pt, .8), l)));
            PT.drawTour(turtle, baseHex.map(pt => V.add(V.scale(pt, .8 * v), l)));
            PT.drawTour(turtle, baseHex.map(pt => V.add(V.scale(pt, .4 * v), l)));
        }
    }
    
    return i < rings * segmentsPerRing - 1;
}

///////////////////////////////////////////////////////
// Port by Jurgen Westerhof 2024
// from https://www.skypack.dev/view/simplex-noise
//
// Usage:
// const noise = Simplex().createNoise2D(randomFunction); // or
//             = Simplex().createNoise3D(randomFunction); // or
//             = Simplex().createNoise4D(randomFunction);
// const simplexSample = noise(x, y[, z[, t]]);
///////////////////////////////////////////////////////
function Simplex(){const t=.5*(Math.sqrt(3)-1),e=(3-Math.sqrt(3))/6,n=1/6,r=(Math.sqrt(5)-1)/4,o=(5-Math.sqrt(5))/20,a=t=>0|Math.floor(t),l=new Float64Array([1,1,-1,1,1,-1,-1,-1,1,0,-1,0,1,0,-1,0,0,1,0,-1,0,1,0,-1]),s=new Float64Array([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]),c=new Float64Array([0,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,1,0,1,1,1,0,1,-1,1,0,-1,1,1,0,-1,-1,-1,0,1,1,-1,0,1,-1,-1,0,-1,1,-1,0,-1,-1,1,1,0,1,1,1,0,-1,1,-1,0,1,1,-1,0,-1,-1,1,0,1,-1,1,0,-1,-1,-1,0,1,-1,-1,0,-1,1,1,1,0,1,1,-1,0,1,-1,1,0,1,-1,-1,0,-1,1,1,0,-1,1,-1,0,-1,-1,1,0,-1,-1,-1,0]);function i(t){const e=512,n=new Uint8Array(e);for(let t=0;t<256;t++)n[t]=t;for(let e=0;e<255;e++){const r=e+~~(t()*(256-e)),o=n[e];n[e]=n[r],n[r]=o}for(let t=256;t<e;t++)n[t]=n[t-256];return n}return new class{buildPermutationTable(t){return i(t)}createNoise2D(n){return function(n=Math.random){const r=i(n),o=new Float64Array(r).map((t=>l[t%12*2])),s=new Float64Array(r).map((t=>l[t%12*2+1]));return function(n,l){let c=0,i=0,f=0;const u=(n+l)*t,m=a(n+u),w=a(l+u),y=(m+w)*e,A=n-(m-y),F=l-(w-y);let p,h;A>F?(p=1,h=0):(p=0,h=1);const M=A-p+e,d=F-h+e,q=A-1+2*e,D=F-1+2*e,N=255&m,b=255&w;let x=.5-A*A-F*F;if(x>=0){const t=N+r[b];x*=x,c=x*x*(o[t]*A+s[t]*F)}let P=.5-M*M-d*d;if(P>=0){const t=N+p+r[b+h];P*=P,i=P*P*(o[t]*M+s[t]*d)}let S=.5-q*q-D*D;if(S>=0){const t=N+1+r[b+1];S*=S,f=S*S*(o[t]*q+s[t]*D)}return 70*(c+i+f)}}(n)}createNoise3D(t){return function(t=Math.random){const e=i(t),r=new Float64Array(e).map((t=>s[t%12*3])),o=new Float64Array(e).map((t=>s[t%12*3+1])),l=new Float64Array(e).map((t=>s[t%12*3+2]));return function(t,s,c){let i,f,u,m;const w=.3333333333333333*(t+s+c),y=a(t+w),A=a(s+w),F=a(c+w),p=(y+A+F)*n,h=t-(y-p),M=s-(A-p),d=c-(F-p);let q,D,N,b,x,P;h>=M?M>=d?(q=1,D=0,N=0,b=1,x=1,P=0):h>=d?(q=1,D=0,N=0,b=1,x=0,P=1):(q=0,D=0,N=1,b=1,x=0,P=1):M<d?(q=0,D=0,N=1,b=0,x=1,P=1):h<d?(q=0,D=1,N=0,b=0,x=1,P=1):(q=0,D=1,N=0,b=1,x=1,P=0);const S=h-q+n,T=M-D+n,U=d-N+n,g=h-b+2*n,j=M-x+2*n,k=d-P+2*n,v=h-1+.5,z=M-1+.5,B=d-1+.5,C=255&y,E=255&A,G=255&F;let H=.6-h*h-M*M-d*d;if(H<0)i=0;else{const t=C+e[E+e[G]];H*=H,i=H*H*(r[t]*h+o[t]*M+l[t]*d)}let I=.6-S*S-T*T-U*U;if(I<0)f=0;else{const t=C+q+e[E+D+e[G+N]];I*=I,f=I*I*(r[t]*S+o[t]*T+l[t]*U)}let J=.6-g*g-j*j-k*k;if(J<0)u=0;else{const t=C+b+e[E+x+e[G+P]];J*=J,u=J*J*(r[t]*g+o[t]*j+l[t]*k)}let K=.6-v*v-z*z-B*B;if(K<0)m=0;else{const t=C+1+e[E+1+e[G+1]];K*=K,m=K*K*(r[t]*v+o[t]*z+l[t]*B)}return 32*(i+f+u+m)}}(t)}createNoise4D(t){return function(t=Math.random){const e=i(t),n=new Float64Array(e).map((t=>c[t%32*4])),l=new Float64Array(e).map((t=>c[t%32*4+1])),s=new Float64Array(e).map((t=>c[t%32*4+2])),f=new Float64Array(e).map((t=>c[t%32*4+3]));return function(t,c,i,u){let m,w,y,A,F;const p=(t+c+i+u)*r,h=a(t+p),M=a(c+p),d=a(i+p),q=a(u+p),D=(h+M+d+q)*o,N=t-(h-D),b=c-(M-D),x=i-(d-D),P=u-(q-D);let S=0,T=0,U=0,g=0;N>b?S++:T++,N>x?S++:U++,N>P?S++:g++,b>x?T++:U++,b>P?T++:g++,x>P?U++:g++;const j=S>=3?1:0,k=T>=3?1:0,v=U>=3?1:0,z=g>=3?1:0,B=S>=2?1:0,C=T>=2?1:0,E=U>=2?1:0,G=g>=2?1:0,H=S>=1?1:0,I=T>=1?1:0,J=U>=1?1:0,K=g>=1?1:0,L=N-j+o,O=b-k+o,Q=x-v+o,R=P-z+o,V=N-B+2*o,W=b-C+2*o,X=x-E+2*o,Y=P-G+2*o,Z=N-H+3*o,$=b-I+3*o,_=x-J+3*o,tt=P-K+3*o,et=N-1+4*o,nt=b-1+4*o,rt=x-1+4*o,ot=P-1+4*o,at=255&h,lt=255&M,st=255&d,ct=255&q;let it=.6-N*N-b*b-x*x-P*P;if(it<0)m=0;else{const t=at+e[lt+e[st+e[ct]]];it*=it,m=it*it*(n[t]*N+l[t]*b+s[t]*x+f[t]*P)}let ft=.6-L*L-O*O-Q*Q-R*R;if(ft<0)w=0;else{const t=at+j+e[lt+k+e[st+v+e[ct+z]]];ft*=ft,w=ft*ft*(n[t]*L+l[t]*O+s[t]*Q+f[t]*R)}let ut=.6-V*V-W*W-X*X-Y*Y;if(ut<0)y=0;else{const t=at+B+e[lt+C+e[st+E+e[ct+G]]];ut*=ut,y=ut*ut*(n[t]*V+l[t]*W+s[t]*X+f[t]*Y)}let mt=.6-Z*Z-$*$-_*_-tt*tt;if(mt<0)A=0;else{const t=at+H+e[lt+I+e[st+J+e[ct+K]]];mt*=mt,A=mt*mt*(n[t]*Z+l[t]*$+s[t]*_+f[t]*tt)}let wt=.6-et*et-nt*nt-rt*rt-ot*ot;if(wt<0)F=0;else{const t=at+1+e[lt+1+e[st+1+e[ct+1]]];wt*=wt,F=wt*wt*(n[t]*et+l[t]*nt+s[t]*rt+f[t]*ot)}return 27*(m+w+y+A+F)}}(t)}}}

function SquareTiledSimplex2D(noise4D, tileSize = 200, noiseZoom = 1) {
    class SquareTiledSimplex2D {
        constructor(noise4D, tileSize, noiseZoom) {
            this.noise4D = noise4D;
            this.tileSize = tileSize;
            this.noiseZoom = noiseZoom
        }
        sample(x, y) {
            const alpha = Math.PI * 2 / 200 * (200 / this.tileSize) * x;
            const beta = Math.PI * 2 / 200 * (200 / this.tileSize) * y;
    
            return this.noise4D(
                Math.cos(alpha) * this.noiseZoom,
                Math.sin(alpha) * this.noiseZoom,
                Math.cos(beta) * this.noiseZoom,
                Math.sin(beta) * this.noiseZoom
            );
        }
    }
    return new SquareTiledSimplex2D(noise4D, tileSize, noiseZoom);
}

function init() {
    ///////////////////////////////////////////////////////
    // Vector functions - Created by Jurgen Westerhof 2024
    // https://turtletoy.net/turtle/d068ad6040
    ///////////////////////////////////////////////////////
    class Vector {
        static add  (a,b) { return a.map((v,i)=>v+b[i]); }
        static sub  (a,b) { return a.map((v,i)=>v-b[i]); }
        static mul  (a,b) { return a.map((v,i)=>v*b[i]); }
        static div  (a,b) { return a.map((v,i)=>v/b[i]); }
        static scale(a,s) { return a.map(v=>v*s); }
    
        static det(m)                { return m.length == 1? m[0][0]: m.length == 2 ? m[0][0]*m[1][1]-m[0][1]*m[1][0]: m[0].reduce((r,e,i) => r+(-1)**(i+2)*e*this.det(m.slice(1).map(c => c.filter((_,j) => i != j))),0); }
        static angle(a)              { return Math.PI - Math.atan2(a[1], -a[0]); } //compatible with turtletoy heading
        static rot2d(angle)          { return [[Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]]; }
        static rot3d(yaw,pitch,roll) { return [[Math.cos(yaw)*Math.cos(pitch), Math.cos(yaw)*Math.sin(pitch)*Math.sin(roll)-Math.sin(yaw)*Math.cos(roll), Math.cos(yaw)*Math.sin(pitch)*Math.cos(roll)+Math.sin(yaw)*Math.sin(roll)],[Math.sin(yaw)*Math.cos(pitch), Math.sin(yaw)*Math.sin(pitch)*Math.sin(roll)+Math.cos(yaw)*Math.cos(roll), Math.sin(yaw)*Math.sin(pitch)*Math.cos(roll)-Math.cos(yaw)*Math.sin(roll)],[-Math.sin(pitch), Math.cos(pitch)*Math.sin(roll), Math.cos(pitch)*Math.cos(roll)]]; }
        static trans(matrix,a)       { return a.map((v,i) => a.reduce((acc, cur, ci) => acc + cur * matrix[ci][i], 0)); }
        //Mirror vector a in a ray through [0,0] with direction mirror
        static mirror2d(a,mirror)    { return [Math.atan2(...mirror)].map(angle => this.trans(this.rot2d(angle), this.mul([-1,1], this.trans(this.rot2d(-angle), a)))).pop(); }

        static equals(a,b)   { return !a.some((e, i) => e != b[i]); }
        static approx(a,b,p) { return this.len(this.sub(a,b)) < (p === undefined? .001: p); }
        static norm  (a)     { return this.scale(a,1/this.len(a)); }
        static len   (a)     { return Math.hypot(...a); }
        static lenSq (a)     { return a.reduce((a,c)=>a+c**2,0); }
        static lerp  (a,b,t) { return a.map((v, i) => v*(1-t) + b[i]*t); }
        static dist  (a,b)   { return Math.hypot(...this.sub(a,b)); }
        
        static dot  (a,b)   { return a.reduce((a,c,i) => a+c*b[i], 0); }
        static cross(...ab) { return ab[0].map((e, i) => ab.map(v => v.filter((ee, ii) => ii != i))).map((m,i) => (i%2==0?-1:1)*this.det(m)); }
        
        static clamp(a,min,max) { return a.map((e,i) => Math.min(Math.max(e, min[i]), max[i])) };
        static rotateClamp(a,min,max) { return a.map((e,i) => {
            const d = max[i]-min[i];
            if(d == 0) return min[i];
            while(e < min[i]) { e+=d; }
            while(e > max[i]) { e-=d; }
            return e;
        });
        }
    }
    this.V = Vector;
    
    class Intersection2D {
        //a-start, a-direction, b-start, b-direction
        //returns false on no intersection or [[intersection:x,y], scalar a-direction, scalar b-direction
        static info(as, ad, bs, bd) { const d = V.sub(bs, as), det = -V.det([bd, ad]); if(det === 0) return false; const res = [V.det([d, bd]) / det, V.det([d, ad]) / det]; return [V.add(as, V.scale(ad, res[0])), ...res]; }
        static ray(a, b, c, d) { return this.info(a, b, c, d); }
        static segment(a,b,c,d, inclusiveStart = true, inclusiveEnd = true) { const i = this.info(a, V.sub(b, a), c, V.sub(d, c)); return i === false? false: ( (inclusiveStart? 0<=i[1] && 0<=i[2]: 0<i[1] && 0<i[2]) && (inclusiveEnd?   i[1]<=1 && i[2]<=1: i[1]<1 && i[2]<1) )?i[0]:false;}
        static tour(tour, segmentStart, segmentDirection) { return tour.map((e, i, a) => [i, this.info(e, V.sub(a[(i+1)%a.length], e), segmentStart, segmentDirection)]).filter(e => e[1] !== false && 0 <= e[1][1] && e[1][1] <= 1).filter(e => 0 <= e[1][2]).map(e => ({position: e[1][0],tourIndex: e[0],tourSegmentPortion: e[1][1],segmentPortion: e[1][2],}));}
        static inside(tour, pt) { return tour.map((e,i,a) => this.segment(e, a[(i+1)%a.length], pt, [Number.MAX_SAFE_INTEGER, 0], true, false)).filter(e => e !== false).length % 2 == 1; }
        static circles(centerA, radiusA, centerB, radiusB) {const result = {intersect_count: 0,intersect_occurs: true,one_is_in_other: false,are_equal: false,point_1: [null, null],point_2: [null, null],};const dx = centerB[0] - centerA[0];const dy = centerB[1] - centerA[1];const dist = Math.hypot(dy, dx);if (dist > radiusA + radiusB) {result.intersect_occurs = false;}if (dist < Math.abs(radiusA - radiusB) && !N.approx(dist, Math.abs(radiusA - radiusB))) {result.intersect_occurs = false;result.one_is_in_other = true;}if (V.approx(centerA, centerB) && radiusA === radiusB) {result.are_equal = true;}if (result.intersect_occurs) {const centroid = (radiusA**2 - radiusB**2 + dist * dist) / (2.0 * dist);const x2 = centerA[0] + (dx * centroid) / dist;const y2 = centerA[1] + (dy * centroid) / dist;const prec = 10000;const h = (Math.round(radiusA**2 * prec)/prec - Math.round(centroid**2 * prec)/prec)**.5;const rx = -dy * (h / dist);const ry = dx * (h / dist);result.point_1 = [x2 + rx, y2 + ry];result.point_2 = [x2 - rx, y2 - ry];if (result.are_equal) {result.intersect_count = null;} else if (result.point_1.x === result.point_2.x && result.point_1.y === result.point_2.y) {result.intersect_count = 1;} else {result.intersect_count = 2;}}return result;}
    }
    this.Intersection = Intersection2D;
    
    class PathTools {
        static bezier(p1, cp1, cp2, p2, steps = null) {steps = (steps === null? Math.max(3, (V.len(V.sub(cp1, p1)) + V.len(V.sub(cp2, cp1)) + V.len(V.sub(p2, cp2))) | 0): steps) - 1; return Array.from({length: steps + 1}).map((v, i, a, f = i/steps) => [[V.lerp(p1, cp1, f),V.lerp(cp1, cp2, f),V.lerp(cp2, p2, f)]].map(v => V.lerp(V.lerp(v[0], v[1], f), V.lerp(v[1], v[2], f), f))[0]);}
        // https://stackoverflow.com/questions/18655135/divide-bezier-curve-into-two-equal-halves#18681336
        static splitBezier(p1, cp1, cp2, p2, t=.5) {const e = V.lerp(p1, cp1, t);const f = V.lerp(cp1, cp2, t);const g = V.lerp(cp2, p2, t);const h = V.lerp(e, f, t);const j = V.lerp(f, g, t);const k = V.lerp(h, j, t);return [[p1, e, h, k], [k, j, g, p2]];}
        static circular(radius,verticeCount,rotation=0) {return Array.from({length: verticeCount}).map((e,i,a,f=i*2*Math.PI/verticeCount+rotation) => [radius*Math.cos(f),radius*Math.sin(f)])}
        static circle(r){return this.circular(r,Math.max(12, r*2*Math.PI|0));}
        static arc(radius, extend = 2 * Math.PI, clockWiseStart = 0, steps = null, includeLast = false) { return [steps == null? (radius*extend+1)|0: steps].map(steps => Array.from({length: steps}).map((v, i, a) => [radius * Math.cos(clockWiseStart + extend*i/(a.length-(includeLast?1:0))), radius * Math.sin(clockWiseStart + extend*i/(a.length-(includeLast?1:0)))])).pop(); }
        static draw(turtle, path) {path.forEach((pt, i) => turtle[i==0?'jump':'goto'](pt));}
        static drawTour(turtle, path) {this.draw(turtle, path.concat([path[0]]));}
        static drawPoint(turtle, pt, r = .1) {this.drawTour(turtle, this.circle(r).map(e => V.add(e, pt)));}
        static drawArrow(turtle, s, d, width = 6, length = 3) {turtle.jump(s);const arrowHeadBase = V.add(s,d);turtle.goto(arrowHeadBase);turtle.goto(V.add(arrowHeadBase, V.trans(V.rot2d(-V.angle(d)), [-length, width/2])));turtle.jump(V.add(arrowHeadBase, V.trans(V.rot2d(-V.angle(d)), [-length, -width/2])));turtle.goto(arrowHeadBase);}
        static circlesTangents(c1_center, c1_radius, c2_center, c2_radius, internal = false) {let middle_circle = [V.scale(V.sub(c1_center, c2_center), .5)].map(hwp => [V.add(c2_center, hwp), V.len(hwp)]).pop();if(!internal && c1_radius == c2_radius) {let target = V.sub(c2_center, c1_center);let scaledTarget = V.scale(target, c1_radius/V.len(target));let partResult = [V.add(c1_center, V.trans(V.rot2d(Math.PI/2), scaledTarget)),V.add(c1_center, V.trans(V.rot2d(Math.PI/-2), scaledTarget))];return [partResult,partResult.map(pt => V.add(pt, target))]}let swap = !internal && c2_radius > c1_radius;if(swap) {let t = [[...c1_center], c1_radius];c1_center = c2_center;c1_radius = c2_radius;c2_center = t[0];c2_radius = t[1];}let internal_waypoints = intersectCircles2(c1_center, c1_radius + (internal?c2_radius:-c2_radius), ...middle_circle);if(internal_waypoints.length == 0) return [];const circlePointAtDirection2 = (circle_center, radius, direction) => V.add(circle_center, V.scale(direction, radius/V.len(direction)));const result = [[circlePointAtDirection2(c1_center, c1_radius, V.sub(internal_waypoints[0], c1_center)),circlePointAtDirection2(c1_center, c1_radius, V.sub(internal_waypoints[1], c1_center))],[circlePointAtDirection2(c2_center, c2_radius, internal? V.sub(c1_center, internal_waypoints[0]): V.sub(internal_waypoints[0], c1_center)),circlePointAtDirection2(c2_center, c2_radius, internal? V.sub(c1_center, internal_waypoints[1]): V.sub(internal_waypoints[1], c1_center))]];return swap? [[result[1][1],result[1][0]],[result[0][1],result[0][0]]]: result;}
    }
    
    this.PT = PathTools;
    
    class Complex {
        static add(a,b)     { return V.add(a,b); }
        static sub(a,b)     { return V.sub(a,b); }
        static scale(a,s)   { return V.scale(a,s); }
        static mult(a,b)    { return [a[0]*b[0]-a[1]*b[1],a[0]*b[1]+a[1]*b[0]]; }
        static sqrt(a)      { return [[Math.hypot(...a)**.5, Math.atan2(...a.reverse()) / 2]].map(ra => [ra[0]*Math.cos(ra[1]), ra[0]*Math.sin(ra[1])]).pop(); }
    }
    this.C = Complex;
    
    class Numbers {
        static approx(a,b,p)        { return Math.abs(a-b) < (p === undefined? .001: p); }
        static clamp(a, min, max)   { return Math.min(Math.max(a, min), max); }
        static rotateClamp(a, min, max) {
            if(min == max) return min;
            while (a < min) { a+=(max-min); }
            while (a > max) { a-=(max-min); }
            return a;
        }
    }
    this.N = Numbers;

    class Matrix {
        static bayer(order) { return [...Array(1<<order)].map((_,y,a) => { const g = (k=order,x)=>k--&&4*g(k,x)|2*(x>>k)+3*(y>>k&1)&3; return a.map(g); }); }
        static rotate(m) { return m[0].map((e, i) => m.map(r => r[i]).reverse()); }
        static rotateCCW(m) { return m[0].map((e, i) => m.map(r => r[r.length-1-i])); }
        static add(a,b) { return a.map((e, c) => e.map((e, r) => a[c][r] + b[c][r])); }
        static sub(a,b) { return a.map((e, c) => e.map((e, r) => a[c][r] - b[c][r])); }
        static multiply(a,b) { return Array.from({length: b.length}, (e,resCol) => Array.from({length: a[0].length}, (e,resRow) => b[resCol].reduce((acc, c, bRow) => acc + a[bRow][resRow] * b[resCol][bRow], 0)));}
        static scale(a,s) { return a.map((e, c) => e.map((e, r) => a[c][r] * s)); }
        static random(c,r,fillFn = Math.random) { return Array.from({length: c}, (e,i) => Array.from({length: r}, e => fillFn(c, r))); }
        static identity(d) { return Array.from({length: d}, (e,c) => Array.from({length: d}, (e, r) => c==r?1:0 )); }
        static log(m, name, logFn = console.log) { if(name != undefined) logFn(name); if(m === undefined || (typeof m == 'object' && (m[0] === undefined || m[0][0] === undefined))) { return logFn(`Failed to log matrix:`, m); } logFn(m[0].map((e,r) => m.map((e,c) => m[c][r]).join(', ')).join('\n')); }
        static invert(m) { let _A = m.map(col => col.map(cell => cell));/*clone matrix*/let temp;const N = _A.length;const E = Array.from({length: N}, (e,i) => Array.from({length: _A[0].length}, (e,j) => i==j?1:0));for (let k = 0; k < N; k++) {temp = _A[k][k];for (let j = 0; j < N; j++) {_A[k][j] /= temp;E[k][j] /= temp;}for (let i = k + 1; i < N; i++) {temp = _A[i][k];for (let j = 0; j < N; j++) {_A[i][j] -= _A[k][j] * temp;E[i][j] -= E[k][j] * temp;}}}for (let k = N - 1; k > 0; k--) {for (let i = k - 1; i >= 0; i--) {temp = _A[i][k];for (let j = 0; j < N; j++) {_A[i][j] -= _A[k][j] * temp;E[i][j] -= E[k][j] * temp;}}}return E; }
        static determinant(m) { return m.length == 1 ?m[0][0] :m.length == 2 ? m[0][0]*m[1][1]-m[0][1]*m[1][0] :m[0].reduce((r,e,i) => r+(-1)**(i+2)*e*this.determinant(m.slice(1).map(c => c.filter((_,j) => i != j))), 0)}
        static flip(m) { return Array.from({length: m[0].length}, (_, r) => Array.from({length: m.length}, (e, c) => m[c][r])); }
        static sum(m) { return m.reduce((a, c) => a + c.reduce((aa, cc) => aa + cc, 0), 0); }
    }
    this.M = Matrix;
    
    class Algorithms {
        static nthTriangular(n) { return ((n * n) + n) / 2; }
    }
    this.A = Algorithms;
}