diff --git a/README.md b/README.md index e30be72..27c7279 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Embrace the full potential of your creative coding projects with p5.brush.js, wh ## Installation +Important note: p5.brush requires p5.js 1.11 or higher + ### Local Installation To set up your project, add `p5.min.js` `p5.brush.js` to your HTML file. You can download the last version of the p5.brush.js library in the [dist](/dist) folder. @@ -41,7 +43,7 @@ Alternatively, you can link to a `p5.brush.js` file hosted online. All versions ```html - + ``` ### Install with NPM and other modular-based apps diff --git a/dist/p5.brush.js b/dist/p5.brush.js index f3dd895..333aec4 100644 --- a/dist/p5.brush.js +++ b/dist/p5.brush.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).brush={})}(this,(function(t){"use strict";function e(t,e){let s=new i(t),n=()=>s.next();return n.double=()=>n()+11102230246251565e-32*(2097152*n()|0),n.int32=()=>4294967296*s.next()|0,n.quick=n,function(t,e,i){let s=i&&i.state;s&&("object"==typeof s&&e.copy(s,e),t.state=()=>e.copy(e,{}))}(n,s,e),n}class i{constructor(t){null==t&&(t=+new Date);let e=4022871197;function i(t){t=String(t);for(let i=0;i>>0,s-=e,s*=e,e=s>>>0,s-=e,e+=4294967296*s}return 2.3283064365386963e-10*(e>>>0)}this.c=1,this.s0=i(" "),this.s1=i(" "),this.s2=i(" "),this.s0-=i(t),this.s0<0&&(this.s0+=1),this.s1-=i(t),this.s1<0&&(this.s1+=1),this.s2-=i(t),this.s2<0&&(this.s2+=1)}next(){let{c:t,s0:e,s1:i,s2:s}=this,n=2091639*e+2.3283064365386963e-10*t;return this.s0=i,this.s1=s,this.s2=n-(this.c=0|n)}copy(t,e){return e.c=t.c,e.s0=t.s0,e.s1=t.s1,e.s2=t.s2,e}}let s,n=!1,o=!1,r=!1,a=!1;function h(t=!1){let e=!(!r||!t)&&a;n&&l(!1),!t&&r&&(t=a),s=t||window.self,y.load(e),o=!0}function l(t=!0){n&&(y.masks[0].remove(),y.masks[0]=null,y.masks[1].remove(),y.masks[1]=null,y.masks[2].remove(),y.masks[2]=null,t&&brush.load())}function c(){n||(o||h(),z.create(),A(s.width/250),n=!0)}let m=new e(Math.random());const d={random:(t=0,e=1)=>t+m()*(e-t),randInt(t,e){return Math.floor(this.random(t,e))},gaussian(t=0,e=1){const i=1-m(),s=m();return Math.sqrt(-2*Math.log(i))*Math.cos(2*Math.PI*s)*e+t},weightedRand(t){let e,i,s=[];for(e in t)for(i=0;i<10*t[e];i++)s.push(e);return s[Math.floor(m()*s.length)]},map(t,e,i,s,n,o=!1){let r=s+(t-e)/(i-e)*(n-s);return o?sMath.max(Math.min(t,i),e),cos(t){return this.c[Math.floor((t%360+360)%360*4)]},sin(t){return this.s[Math.floor((t%360+360)%360*4)]},isPrecalculationDone:!1,preCalculation(){if(this.isPrecalculationDone)return;const t=1440,e=2*Math.PI/t;this.c=new Float64Array(t),this.s=new Float64Array(t);for(let i=0;i!isNaN(t),toDegrees:t=>(("radians"===s.angleMode()?180*t/Math.PI:t)%360+360)%360,dist:(t,e,i,s)=>Math.hypot(i-t,s-e)};function u(t,e,i,s,n=!1){let o=t.x,r=t.y,a=e.x,h=e.y,l=i.x,c=i.y,m=s.x,d=s.y;if(o===a&&r===h||l===m&&c===d)return!1;let u=a-o,p=h-r,f=m-l,v=d-c,g=v*u-f*p;if(0===g)return!1;let x=(f*(r-c)-v*(o-l))/g,y=(u*(r-c)-p*(o-l))/g;return!(!n&&(y<0||y>1))&&{x:o+x*u,y:r+x*p}}function p(t,e,i,s){return(Math.atan2(-(s-e),i-t)*(180/Math.PI)%360+360)%360}d.preCalculation();const f={field:{},stroke:{},hatch:{},fill:{},others:{}};const v={translation:[0,0],rotation:0,trans(){return this.translation=[s._renderer.uModelMatrix.mat4[12],s._renderer.uModelMatrix.mat4[13]],this.translation}};let g=1;function x(t){g*=t}const y={loaded:!1,isBlending:!1,isCaching:!0,currentColor:new Float32Array(3),load(t){this.type=r&&!t?0:t?2:1,this.masks=[];for(let e=0;e<3;e++)switch(this.type){case 0:this.masks[e]=s.createGraphics(s.width,s.height,1==e?s.WEBGL:s.P2D);break;case 1:this.masks[e]=createGraphics(s.width,s.height,1==e?WEBGL:P2D);break;case 2:this.masks[e]=t.createGraphics(t.width,t.height,1==e?t.WEBGL:t.P2D)}for(let t of this.masks)t.pixelDensity(s.pixelDensity()),t.clear(),t.angleMode(s.DEGREES),t.noSmooth();this.shader=s.createShader(this.vert,this.frag),y.loaded=!0},getPigment(t){let e=t.levels,i=new Float32Array(3);return i[0]=e[0]/255,i[1]=e[1]/255,i[2]=e[2]/255,i},color1:new Float32Array(3),color2:new Float32Array(3),blending1:!1,blending2:!1,blend(t=!1,e=!1,i=!1){if(c(),this.isBlending=i?this.blending1:this.blending2,this.currentColor=i?this.color1:this.color2,!this.isBlending)if(t)this.currentColor=this.getPigment(t),i?(this.blending1=!0,this.color1=this.currentColor):(this.blending2=!0,this.color2=this.currentColor);else if(e)return void(i||w());if((t?this.getPigment(t):this.currentColor).toString()!==this.currentColor.toString()||e||!this.isCaching){if(w(),this.isBlending){s.push(),s.translate(-v.trans()[0],-v.trans()[1]),s.shader(this.shader),this.shader.setUniform("addColor",this.currentColor),this.shader.setUniform("source",s._renderer),this.shader.setUniform("active",y.watercolor),this.shader.setUniform("random",[d.random(),d.random(),d.random()]);let t=i?this.masks[1]:this.masks[0];this.shader.setUniform("mask",t),s.fill(0,0,0,0),s.noStroke(),s.rect(-s.width/2,-s.height/2,s.width,s.height),s.pop(),t.clear()}e||(this.currentColor=this.getPigment(t),i?this.color1=this.currentColor:this.color2=this.currentColor)}e&&(this.isBlending=!1,i?this.blending1=this.isBlending:this.blending2=this.isBlending)},vert:"precision highp float;attribute vec3 aPosition;attribute vec2 aTexCoord;uniform mat4 uModelViewMatrix,uProjectionMatrix;varying vec2 vVertTexCoord;void main(){gl_Position=uProjectionMatrix*uModelViewMatrix*vec4(aPosition,1);vVertTexCoord=aTexCoord;}",frag:"precision highp float;varying vec2 vVertTexCoord;uniform sampler2D source,mask;uniform vec4 addColor;uniform vec3 random;uniform bool active;\n #ifndef SPECTRAL\n #define SPECTRAL\n float x(float v){return v<.04045?v/12.92:pow((v+.055)/1.055,2.4);}float v(float v){return v<.0031308?v*12.92:1.055*pow(v,1./2.4)-.055;}vec3 m(vec3 v){return vec3(x(v[0]),x(v[1]),x(v[2]));}vec3 f(vec3 f){return clamp(vec3(v(f[0]),v(f[1]),v(f[2])),0.,1.);}void f(vec3 v,out float m,out float f,out float x,out float y,out float z,out float i,out float r){m=min(v.x,min(v.y,v.z));v-=m;f=min(v.y,v.z);x=min(v.x,v.z);y=min(v.x,v.y);z=min(max(0.,v.x-v.z),max(0.,v.x-v.y));i=min(max(0.,v.y-v.z),max(0.,v.y-v.x));r=min(max(0.,v.z-v.y),max(0.,v.z-v.x));}void f(vec3 v,inout float i[38]){float x,y,d,z,o,m,e;f(v,x,y,d,z,o,m,e);i[0]=max(1e-4,x+y*.96853629+d*.51567122+z*.02055257+o*.03147571+m*.49108579+e*.97901834);i[1]=max(1e-4,x+y*.96855103+d*.5401552+z*.02059936+o*.03146636+m*.46944057+e*.97901649);i[2]=max(1e-4,x+y*.96859338+d*.62645502+z*.02062723+o*.03140624+m*.4016578+e*.97901118);i[3]=max(1e-4,x+y*.96877345+d*.75595012+z*.02073387+o*.03119611+m*.2449042+e*.97892146);i[4]=max(1e-4,x+y*.96942204+d*.92826996+z*.02114202+o*.03053888+m*.0682688+e*.97858555);i[5]=max(1e-4,x+y*.97143709+d*.97223624+z*.02233154+o*.02856855+m*.02732883+e*.97743705);i[6]=max(1e-4,x+y*.97541862+d*.98616174+z*.02556857+o*.02459485+m*.013606+e*.97428075);i[7]=max(1e-4,x+y*.98074186+d*.98955255+z*.03330189+o*.0192952+m*.01000187+e*.96663223);i[8]=max(1e-4,x+y*.98580992+d*.98676237+z*.05185294+o*.01423112+m*.01284127+e*.94822893);i[9]=max(1e-4,x+y*.98971194+d*.97312575+z*.10087639+o*.01033111+m*.02636635+e*.89937713);i[10]=max(1e-4,x+y*.99238027+d*.91944277+z*.24000413+o*.00765876+m*.07058713+e*.76070164);i[11]=max(1e-4,x+y*.99409844+d*.32564851+z*.53589066+o*.00593693+m*.70421692+e*.4642044);i[12]=max(1e-4,x+y*.995172+d*.13820628+z*.79874659+o*.00485616+m*.85473994+e*.20123039);i[13]=max(1e-4,x+y*.99576545+d*.05015143+z*.91186529+o*.00426186+m*.95081565+e*.08808402);i[14]=max(1e-4,x+y*.99593552+d*.02912336+z*.95399623+o*.00409039+m*.9717037+e*.04592894);i[15]=max(1e-4,x+y*.99564041+d*.02421691+z*.97137099+o*.00438375+m*.97651888+e*.02860373);i[16]=max(1e-4,x+y*.99464769+d*.02660696+z*.97939505+o*.00537525+m*.97429245+e*.02060067);i[17]=max(1e-4,x+y*.99229579+d*.03407586+z*.98345207+o*.00772962+m*.97012917+e*.01656701);i[18]=max(1e-4,x+y*.98638762+d*.04835936+z*.98553736+o*.0136612+m*.9425863+e*.01451549);i[19]=max(1e-4,x+y*.96829712+d*.0001172+z*.98648905+o*.03181352+m*.99989207+e*.01357964);i[20]=max(1e-4,x+y*.89228016+d*8.554e-5+z*.98674535+o*.10791525+m*.99989891+e*.01331243);i[21]=max(1e-4,x+y*.53740239+d*.85267882+z*.98657555+o*.46249516+m*.13823139+e*.01347661);i[22]=max(1e-4,x+y*.15360445+d*.93188793+z*.98611877+o*.84604333+m*.06968113+e*.01387181);i[23]=max(1e-4,x+y*.05705719+d*.94810268+z*.98559942+o*.94275572+m*.05628787+e*.01435472);i[24]=max(1e-4,x+y*.03126539+d*.94200977+z*.98507063+o*.96860996+m*.06111561+e*.01479836);i[25]=max(1e-4,x+y*.02205445+d*.91478045+z*.98460039+o*.97783966+m*.08987709+e*.0151525);i[26]=max(1e-4,x+y*.01802271+d*.87065445+z*.98425301+o*.98187757+m*.13656016+e*.01540513);i[27]=max(1e-4,x+y*.0161346+d*.78827548+z*.98403909+o*.98377315+m*.22169624+e*.01557233);i[28]=max(1e-4,x+y*.01520947+d*.65738359+z*.98388535+o*.98470202+m*.32176956+e*.0156571);i[29]=max(1e-4,x+y*.01475977+d*.59909403+z*.98376116+o*.98515481+m*.36157329+e*.01571025);i[30]=max(1e-4,x+y*.01454263+d*.56817268+z*.98368246+o*.98537114+m*.4836192+e*.01571916);i[31]=max(1e-4,x+y*.01444459+d*.54031997+z*.98365023+o*.98546685+m*.46488579+e*.01572133);i[32]=max(1e-4,x+y*.01439897+d*.52110241+z*.98361309+o*.98550011+m*.47440306+e*.01572502);i[33]=max(1e-4,x+y*.0143762+d*.51041094+z*.98357259+o*.98551031+m*.4857699+e*.01571717);i[34]=max(1e-4,x+y*.01436343+d*.50526577+z*.98353856+o*.98550741+m*.49267971+e*.01571905);i[35]=max(1e-4,x+y*.01435687+d*.5025508+z*.98351247+o*.98551323+m*.49625685+e*.01571059);i[36]=max(1e-4,x+y*.0143537+d*.50126452+z*.98350101+o*.98551563+m*.49807754+e*.01569728);i[37]=max(1e-4,x+y*.01435408+d*.50083021+z*.98350852+o*.98551547+m*.49889859+e*.0157002);}vec3 t(vec3 x){mat3 i;i[0]=vec3(3.24306333,-1.53837619,-.49893282);i[1]=vec3(-.96896309,1.87542451,.04154303);i[2]=vec3(.05568392,-.20417438,1.05799454);float v=dot(i[0],x),y=dot(i[1],x),o=dot(i[2],x);return f(vec3(v,y,o));}vec3 d(float m[38]){vec3 i=vec3(0);i+=m[0]*vec3(6.469e-5,1.84e-6,.00030502);i+=m[1]*vec3(.00021941,6.21e-6,.00103681);i+=m[2]*vec3(.00112057,3.101e-5,.00531314);i+=m[3]*vec3(.00376661,.00010475,.01795439);i+=m[4]*vec3(.01188055,.00035364,.05707758);i+=m[5]*vec3(.02328644,.00095147,.11365162);i+=m[6]*vec3(.03455942,.00228226,.17335873);i+=m[7]*vec3(.03722379,.00420733,.19620658);i+=m[8]*vec3(.03241838,.0066888,.18608237);i+=m[9]*vec3(.02123321,.0098884,.13995048);i+=m[10]*vec3(.01049099,.01524945,.08917453);i+=m[11]*vec3(.00329584,.02141831,.04789621);i+=m[12]*vec3(.00050704,.03342293,.02814563);i+=m[13]*vec3(.00094867,.05131001,.01613766);i+=m[14]*vec3(.00627372,.07040208,.0077591);i+=m[15]*vec3(.01686462,.08783871,.00429615);i+=m[16]*vec3(.02868965,.09424905,.00200551);i+=m[17]*vec3(.04267481,.09795667,.00086147);i+=m[18]*vec3(.05625475,.09415219,.00036904);i+=m[19]*vec3(.0694704,.08678102,.00019143);i+=m[20]*vec3(.08305315,.07885653,.00014956);i+=m[21]*vec3(.0861261,.0635267,9.231e-5);i+=m[22]*vec3(.09046614,.05374142,6.813e-5);i+=m[23]*vec3(.08500387,.04264606,2.883e-5);i+=m[24]*vec3(.07090667,.03161735,1.577e-5);i+=m[25]*vec3(.05062889,.02088521,3.94e-6);i+=m[26]*vec3(.03547396,.01386011,1.58e-6);i+=m[27]*vec3(.02146821,.00810264,0);i+=m[28]*vec3(.01251646,.0046301,0);i+=m[29]*vec3(.00680458,.00249138,0);i+=m[30]*vec3(.00346457,.0012593,0);i+=m[31]*vec3(.00149761,.00054165,0);i+=m[32]*vec3(.0007697,.00027795,0);i+=m[33]*vec3(.00040737,.00014711,0);i+=m[34]*vec3(.00016901,6.103e-5,0);i+=m[35]*vec3(9.522e-5,3.439e-5,0);i+=m[36]*vec3(4.903e-5,1.771e-5,0);i+=m[37]*vec3(2e-5,7.22e-6,0);return i;}float d(float y,float m,float v){float z=m*pow(v,2.);return z/(y*pow(1.-v,2.)+z);}vec3 f(vec3 v,vec3 y,float z){vec3 x=m(v),o=m(y);float i[38],a[38];f(x,i);f(o,a);float r=d(i)[1],e=d(a)[1];z=d(r,e,z);float s[38];for(int u=0;u<38;u++){float p=(1.-z)*(pow(1.-i[u],2.)/(2.*i[u]))+z*(pow(1.-a[u],2.)/(2.*a[u]));s[u]=1.+p-sqrt(pow(p,2.)+2.*p);}return t(d(s));}vec4 f(vec4 v,vec4 x,float y){return vec4(f(v.xyz,x.xyz,y),mix(v.w,x.w,y));}\n #endif\n float d(vec2 m,vec2 v,float y,out vec2 i){vec2 f=vec2(m.x+m.y*.5,m.y),x=floor(f),o=fract(f);float z=step(o.y,o.x);vec2 d=vec2(z,1.-z),r=x+d,e=x+1.,a=vec2(x.x-x.y*.5,x.y),p=vec2(a.x+d.x-d.y*.5,a.y+d.y),s=vec2(a.x+.5,a.y+1.),w=m-a,g=m-p,k=m-s;vec3 u,c,t,A;if(any(greaterThan(v,vec2(0)))){t=vec3(a.x,p.x,s);A=vec3(a.y,p.y,s.y);if(v.x>0.)t=mod(vec3(a.x,p.x,s),v.x);if(v.y>0.)A=mod(vec3(a.y,p.y,s.y),v.y);u=floor(t+.5*A+.5);c=floor(A+.5);}else u=vec3(x.x,r.x,e),c=vec3(x.y,r.y,e.y);vec3 S=mod(u,289.);S=mod((S*51.+2.)*S+c,289.);S=mod((S*34.+10.)*S,289.);vec3 b=S*.07482+y,C=cos(b),D=sin(b);vec2 h=vec2(C.x,D),B=vec2(C.y,D.y),E=vec2(C.z,D.z);vec3 F=.8-vec3(dot(w,w),dot(g,g),dot(k,k));F=max(F,0.);vec3 G=F*F,H=G*G,I=vec3(dot(h,w),dot(B,g),dot(E,k)),J=G*F,K=-8.*J*I;i=10.9*(H.x*h+K.x*w+(H.y*B+K.y*g)+(H.z*E+K.z*k));return 10.9*dot(H,I);}vec4 d(vec3 v,float x){return vec4(mix(v,vec3(dot(vec3(.299,.587,.114),v)),x),1);}float f(vec2 v,float x,float y,float f){return fract(sin(dot(v,vec2(x,y)))*f);}void main(){vec4 v=texture2D(mask,vVertTexCoord);if(v.x>0.){vec2 x=vec2(12.9898,78.233),o=vec2(7.9898,58.233),m=vec2(17.9898,3.233);float y=f(vVertTexCoord,x.x,x.y,43358.5453)*2.-1.,z=f(vVertTexCoord,o.x,o.y,43213.5453)*2.-1.,e=f(vVertTexCoord,m.x,m.y,33358.5453)*2.-1.;const vec2 i=vec2(0);vec2 s;vec4 r;if(active){float a=d(vVertTexCoord*5.,i,10.*random.x,s),p=d(vVertTexCoord*5.,i,10.*random.y,s),g=d(vVertTexCoord*5.,i,10.*random.z,s),k=.25+.25*d(vVertTexCoord*4.,i,3.*random.x,s);r=vec4(d(addColor.xyz,k).xyz+vec3(a,p,g)*.03*abs(addColor.x-addColor.y-addColor.z),1);}else r=vec4(addColor.xyz,1);if(v.w>.7){float a=.5*(v.w-.7);r=r*(1.-a)-vec4(.5)*a;}vec3 a=f(texture2D(source,vVertTexCoord).xyz,r.xyz,.9*v.w);gl_FragColor=vec4(a+.01*vec3(y,z,e),1);}}"};function w(){s.push(),s.translate(-v.trans()[0],-v.trans()[1]),s.image(y.masks[2],-s.width/2,-s.height/2),y.masks[2].clear(),s.pop()}function k(t){t.registerMethod("afterSetup",(()=>y.blend(!1,!0))),t.registerMethod("afterSetup",(()=>y.blend(!1,!0,!0))),t.registerMethod("post",(()=>y.blend(!1,!0))),t.registerMethod("post",(()=>y.blend(!1,!0,!0)))}function _(t,e){z.list.set(t,{gen:e}),z.current=t,z.refresh()}"undefined"!=typeof p5&&k(p5.prototype);const z={isActive:!1,list:new Map,current:"",step_length:()=>Math.min(s.width,s.height)/1e3,create(){this.R=.01*s.width,this.left_x=-1*s.width,this.top_y=-1*s.height,this.num_columns=Math.round(2*s.width/this.R),this.num_rows=Math.round(2*s.height/this.R),this.addStandard()},flow_field(){return this.list.get(this.current).field},refresh(t=0){this.list.get(this.current).field=this.list.get(this.current).gen(t,this.genField())},genField(){let t=new Array(this.num_columns);for(let e=0;e=0&&this.row_index>=0&&this.column_index=-t-v.trans()[0]&&this.x<=t-v.trans()[0]&&this.y>=-e-v.trans()[1]&&this.y<=e-v.trans()[1]}angle(){return this.isIn()&&z.isActive?z.flow_field()[this.column_index][this.row_index]:0}moveTo(t,e,i=C.spacing(),s=!0){if(this.isIn()){let n,o;s||(n=d.cos(-e),o=d.sin(-e));for(let r=0;r=C.cr[0]&&this.position.x<=C.cr[2]&&this.position.y>=C.cr[1]&&this.position.y<=C.cr[3];{let t=.55*s.width,e=.55*s.height;return this.position.x>=-t-v.trans()[0]&&this.position.x<=t-v.trans()[0]&&this.position.y>=-e-v.trans()[1]&&this.position.y<=e-v.trans()[1]}},drawSpray(t){let e=this.w*this.p.vibration*t+this.w*d.gaussian()*this.p.vibration/3,i=this.p.weight*d.random(.9,1.1);const s=this.p.quality/t;for(let t=0;t.4&&this.mask.circle(this.position.x+.7*e*d.random(-1,1),this.position.y+e*d.random(-1,1),t*this.p.weight*d.random(.85,1.15))},adjustSizeAndRotation(t,e){if(this.mask.scale(t),"image"===this.p.type&&(this.p.blend?this.mask.tint(255,0,0,e/2):this.mask.tint(this.mask.red(this.c),this.mask.green(this.c),this.mask.blue(this.c),e)),"random"===this.p.rotate)this.mask.rotate(d.randInt(0,360));else if("natural"===this.p.rotate){let t=(this.plot?-this.plot.angle(this.position.plotted):-this.dir)+(this.flow?this.position.angle():0);this.mask.rotate(t)}},markerTip(){if(this.isInsideClippingArea()){let t=this.calculatePressure(),e=this.calculateAlpha(t);if(this.mask.fill(255,0,0,e/1.5),"marker"===C.p.type)for(let e=1;e<5;e++)this.drawMarker(t*e/5,!1);else if("custom"===C.p.type||"image"===C.p.type)for(let i=1;i<5;i++)this.drawCustomOrImage(t*i/5,e,!1)}}};function S(t,e){const i="marker"===e.type||"custom"===e.type||"image"===e.type;i||"spray"===e.type||(e.type="default"),"image"===e.type&&(B.add(e.image.src),e.tip=()=>C.mask.image(B.tips.get(C.p.image.src),-C.p.weight/2,-C.p.weight/2,C.p.weight,C.p.weight)),e.blend=!!(i&&!1!==e.blend||e.blend),C.list.set(t,{param:e,colors:[],buffers:[]})}function P(t,e,i=1){I(t),C.c=e,C.w=i,C.isActive=!0}function I(t){C.name=t}function D(t,e,i,s){c();let n=d.dist(t,e,i,s);if(0==n)return;C.initializeDrawingState(t,e,n,!1,!1);let o=p(t,e,i,s);C.draw(o,!1)}function T(t,e,i,s){c(),C.initializeDrawingState(e,i,t.length,!0,t),C.draw(s,!0)}const B={tips:new Map,add(t){this.tips.set(t,!1)},imageToWhite(t){t.loadPixels();for(let e=0;e<4*t.width*t.height;e+=4){let i=(t.pixels[e]+t.pixels[e+1]+t.pixels[e+2])/3;t.pixels[e]=t.pixels[e+1]=t.pixels[e+2]=255,t.pixels[e+3]=255-i}t.updatePixels()},load(){for(let t of this.tips.keys()){let e=(r?a:window.self).loadImage(t,(()=>B.imageToWhite(e)));this.tips.set(t,e)}}};function E(t=5,e=45,i={rand:!1,continuous:!1,gradient:!1}){F.isActive=!0,F.hatchingParams=[t,e,i]}const F={isActive:!1,hatchingParams:[5,45,{}],hatchingBrush:!1,hatch(t){let e=F.hatchingParams[0],i=F.hatchingParams[1],s=F.hatchingParams[2],n=C.c,o=C.name,r=C.w,a=C.isActive;F.hatchingBrush&&P(F.hatchingBrush[0],F.hatchingBrush[1],F.hatchingBrush[2]),i=d.toDegrees(i)%180;let h=1/0,l=-1/0,c=1/0,m=-1/0,u=t=>{for(let e of t.a)h=e[0]l?e[0]:l,c=e[1]m?e[1]:m};Array.isArray(t)||(t=[t]);for(let e of t)u(e);let p=new R([[h,c],[l,c],[l,m],[h,m]]),f=i<=90&&i>=0?c:m,v=s.gradient?d.map(s.gradient,0,1,1,1.1,!0):1,g=[],x=0,y=e,w=t=>({point1:{x:h+y*t*d.cos(90-i),y:f+y*t*d.sin(90-i)},point2:{x:h+y*t*d.cos(90-i)+d.cos(-i),y:f+y*t*d.sin(90-i)+d.sin(-i)}});for(;p.intersect(w(x)).length>0;){let e=[];for(let i of t)e.push(i.intersect(w(x)));g[x]=e.flat().sort(((t,e)=>t.x===e.x?t.y-e.y:t.x-e.x)),y*=v,x++}let k=[];for(let t of g)void 0!==t[0]&&k.push(t);let _=s.rand?s.rand:0;for(let t=0;t0&&s.continuous;for(let s=0;s({x:t[0],y:t[1]}))),e&&(this.vertices=t),this.sides=this.vertices.map(((t,e,i)=>[t,i[(e+1)%i.length]]))}intersect(t){let e=`${t.point1.x},${t.point1.y}-${t.point2.x},${t.point2.y}`;if(this._intersectionCache&&this._intersectionCache[e])return this._intersectionCache[e];let i=[];for(let e of this.sides){let s=u(t.point1,t.point2,e[0],e[1]);!1!==s&&i.push(s)}return this._intersectionCache||(this._intersectionCache={}),this._intersectionCache[e]=i,i}draw(t=!1,e,i){let s=C.isActive;if(t&&P(t,e,i),C.isActive){c();for(let t of this.sides)D(t[0].x,t[0].y,t[1].x,t[1].y)}C.isActive=s}fill(t=!1,e,i,s,n,o){let r=J.isActive;t&&(U(t,e),K(i,o),$(s,n)),J.isActive&&(c(),J.fill(this)),J.isActive=r}hatch(t=!1,e,i){let s=F.isActive;t&&E(t,e,i),F.isActive&&(c(),F.hatch(this)),F.isActive=s}erase(t=!1,e=j.a){if(j.isActive||t){y.masks[2].push(),y.masks[2].noStroke();let i=s.color(t||j.c);i.setAlpha(e),y.masks[2].fill(i),y.masks[2].beginShape();for(let t of this.vertices)y.masks[2].vertex(t.x,t.y);y.masks[2].endShape(s.CLOSE),y.masks[2].pop()}}show(){this.fill(),this.hatch(),this.draw(),this.erase()}}class G{constructor(t){this.segments=[],this.angles=[],this.pres=[],this.type=t,this.dir=0,this.calcIndex(0),this.pol=!1}addSegment(t=0,e=0,i=1,s=!1){this.angles.length>0&&this.angles.splice(-1),t=s?(t%360+360)%360:d.toDegrees(t),this.angles.push(t),this.pres.push(i),this.segments.push(e),this.length=this.segments.reduce(((t,e)=>t+e),0),this.angles.push(t)}endPlot(t=0,e=1,i=!1){t=i?(t%360+360)%360:d.toDegrees(t),this.angles.splice(-1),this.angles.push(t),this.pres.push(e)}rotate(t){this.dir=d.toDegrees(t)}pressure(t){return t>this.length?this.pres[this.pres.length-1]:this.curving(this.pres,t)}angle(t){return t>this.length?this.angles[this.angles.length-1]:(this.calcIndex(t),"curve"===this.type?this.curving(this.angles,t)+this.dir:this.angles[this.index]+this.dir)}curving(t,e){let i=t[this.index],s=t[this.index+1];return void 0===s&&(s=i),Math.abs(s-i)>180&&(s>i?s=-(360-s):i=-(360-i)),d.map(e-this.suma,0,this.segments[this.index],i,s,!0)}calcIndex(t){this.index=-1,this.suma=0;let e=0;for(;e<=t;)this.suma=e,e+=this.segments[this.index+1],this.index++;return this.index}genPol(t,e,i=1,s=!1){c();const n=.5,o=[],r=Math.round(this.length/n),a=new b(t,e);let h=s?.15:3*J.bleed_strength,l=0,m=0;for(let t=0;t=this.segments[t]*h*d.random(.7,1.3)||t>=m)&&a.x&&(o.push([a.x,a.y]),l=0,t>=m&&m++)}return new R(o)}draw(t,e,i){C.isActive&&(c(),this.origin&&(t=this.origin[0],e=this.origin[1],i=1),T(this,t,e,i))}fill(t,e,i){J.isActive&&(c(),this.origin&&(t=this.origin[0],e=this.origin[1],i=1),this.pol=this.genPol(t,e,i),this.pol.fill())}hatch(t,e,i){F.isActive&&(c(),this.origin&&(t=this.origin[0],e=this.origin[1],i=1),this.pol=this.genPol(t,e,i,!0),this.pol.hatch())}erase(t,e,i){if(j.isActive){this.origin&&(t=this.origin[0],e=this.origin[1],i=1),this.pol=this.genPol(t,e,i,!0),y.masks[2].push(),y.masks[2].noStroke();let n=s.color(j.c);n.setAlpha(j.a),y.masks[2].fill(n),y.masks[2].beginShape();for(let t of this.pol.vertices)y.masks[2].vertex(t.x,t.y);y.masks[2].endShape(s.CLOSE),y.masks[2].pop()}}show(t,e,i=1){this.draw(t,e,i),this.fill(t,e,i),this.hatch(t,e,i),this.erase(t,e,i)}}let L,H=!1;function O(t=0){L=d.constrain(t,0,1),H=[]}function q(t,e,i){H.push([t,e,i])}function N(t){c(),t===s.CLOSE&&(H.push(H[0]),H.push(H[1])),(0!=L||z.isActive?W(H,L,t===s.CLOSE):new R(H)).show(),H=!1}function W(t,e=.5,i=!1){let s=new G(0===e?"segments":"curve");if(t&&t.length>0){let n,o,r,a=0;for(let h=0;h0&&h{let s=d.random(.8,1.2)*this.bleed_strength;return ithis.size&&(this.size=e)}if(n)for(let t=0;t(e.x-t.x)*(i.y-t.y)-(e.y-t.y)*(i.x-t.x)>.01;let a=0;for(let t of J.polygon.intersect(o))r(e,i,t)&&a++;this.dir[t]=a%2==0}}trim(t){let e=[...this.v],i=[...this.m],s=[...this.dir];if(this.v.length>10&&t>=.2){let n=~~((1-t)*this.v.length),o=~~this.v.length/2-~~n/2;e.splice(o,n),i.splice(o,n),s.splice(o,n)}return{v:e,m:i,dir:s}}grow(t,e=!1){const i=[],s=[],n=[];let o=this.trim(t);const r=e?-.5:1,a=t=>t+.1*(d.gaussian(.5,.1)-.5);for(let e=0;es?d.random(-1,1):0;n.addSegment(0+r+a(),o+a(),1,!0),n.addSegment(-90+r+a(),o+a(),1,!0),n.addSegment(-180+r+a(),o+a(),1,!0),n.addSegment(-270+r+a(),o+a(),1,!0);let h=s?d.randInt(-5,5):0;s&&n.addSegment(0+r,h*(Math.PI/180)*i,!0),n.endPlot(h+r,1,!0);let l=[t-i*d.sin(r),e-i*d.cos(-r)];n.show(l[0],l[1],1)},t.clip=function(t){C.cr=t},t.colorCache=function(t=!0){y.isCaching=t},t.endShape=N,t.endStroke=function(t,e){H.endPlot(t,e),H.draw(L[0],L[1],1),H=!1},t.erase=function(t="white",e=255){j.isActive=!0,j.c=t,j.a=e},t.field=function(t){c(),z.isActive=!0,z.current=t},t.fill=U,t.fillAnimatedMode=function(t){J.isAnimated=t},t.fillTexture=$,t.flowLine=function(t,e,i,s){c(),C.initializeDrawingState(t,e,i,!0,!1),C.draw(d.toDegrees(s),!1)},t.gravity=function(t,e){c(),J.light_source={x:t,y:e}},t.hatch=E,t.hatchArray=V,t.instance=function(t){r=!0,a=t,s=t,k(t)},t.line=D,t.listFields=function(){return Array.from(z.list.keys())},t.load=h,t.noClip=function(){C.cr=null},t.noErase=function(){j.isActive=!1},t.noField=function(){c(),z.isActive=!1},t.noFill=function(){J.isActive=!1},t.noGravity=function(){J.light_source=!1},t.noHatch=function(){F.isActive=!1,F.hatchingBrush=!1},t.noStroke=function(){C.isActive=!1},t.pick=I,t.plot=T,t.polygon=function(t){new R(t).show()},t.pop=function(){z.isActive=f.field.isActive,z.current=f.field.current,C.isActive=f.stroke.isActive,C.name=f.stroke.name,C.c=f.stroke.color,C.w=f.stroke.weight,C.cr=f.stroke.clip,F.isActive=f.hatch.isActive,F.hatchingParams=f.hatch.hatchingParams,F.hatchingBrush=f.hatch.hatchingBrush,J.isActive=f.fill.isActive,J.color=f.fill.color,J.opacity=f.fill.opacity,J.bleed_strength=f.fill.bleed_strength,J.texture_strength=f.fill.texture_strength,J.border_strength=f.fill.border_strength,v.rotation=f.others.rotate},t.preload=function(){B.load()},t.push=function(){f.field.isActive=z.isActive,f.field.current=z.current,f.stroke.isActive=C.isActive,f.stroke.name=C.name,f.stroke.color=C.c,f.stroke.weight=C.w,f.stroke.clip=C.cr,f.hatch.isActive=F.isActive,f.hatch.hatchingParams=F.hatchingParams,f.hatch.hatchingBrush=F.hatchingBrush,f.fill.isActive=J.isActive,f.fill.color=J.color,f.fill.opacity=J.opacity,f.fill.bleed_strength=J.bleed_strength,f.fill.texture_strength=J.texture_strength,f.fill.border_strength=J.border_strength,f.others.rotate=v.rotation},t.reBlend=function(){y.blend(!1,!0),y.blend(!1,!0,!0)},t.reDraw=w,t.rect=function(t,e,i,n,o=s.CORNER){if(o==s.CENTER&&(t-=i/2,e-=n/2),z.isActive)O(0),q(t,e),q(t+i,e),q(t+i,e+n),q(t,e+n),N(s.CLOSE);else{new R([[t,e],[t+i,e],[t+i,e+n],[t,e+n]]).show()}},t.refreshField=function(t){z.refresh(t)},t.remove=l,t.rotate=function(t=0){v.rotation=d.toDegrees(t)},t.scale=x,t.scaleBrushes=A,t.seed=function(t){m=new e(t)},t.segment=function(t,e,i){H.addSegment(t,e,i)},t.set=P,t.setHatch=function(t,e="black",i=1){F.hatchingBrush=[t,e,i]},t.spline=function(t,e=.5){W(t,e).draw()},t.stroke=function(t,e,i){arguments.length>0&&(C.c=arguments.length<2?t:[t,e,i]),C.isActive=!0},t.strokeWeight=function(t){C.w=t},t.vertex=q})); +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).brush={})}(this,(function(t){"use strict";function e(t,e){let s=new i(t),n=()=>s.next();return n.double=()=>n()+11102230246251565e-32*(2097152*n()|0),n.int32=()=>4294967296*s.next()|0,n.quick=n,function(t,e,i){let s=i&&i.state;s&&("object"==typeof s&&e.copy(s,e),t.state=()=>e.copy(e,{}))}(n,s,e),n}class i{constructor(t){null==t&&(t=+new Date);let e=4022871197;function i(t){t=String(t);for(let i=0;i>>0,s-=e,s*=e,e=s>>>0,s-=e,e+=4294967296*s}return 2.3283064365386963e-10*(e>>>0)}this.c=1,this.s0=i(" "),this.s1=i(" "),this.s2=i(" "),this.s0-=i(t),this.s0<0&&(this.s0+=1),this.s1-=i(t),this.s1<0&&(this.s1+=1),this.s2-=i(t),this.s2<0&&(this.s2+=1)}next(){let{c:t,s0:e,s1:i,s2:s}=this,n=2091639*e+2.3283064365386963e-10*t;return this.s0=i,this.s1=s,this.s2=n-(this.c=0|n)}copy(t,e){return e.c=t.c,e.s0=t.s0,e.s1=t.s1,e.s2=t.s2,e}}let s,n=!1,o=!1,r=!1,a=!1;function h(t=!1){let e=!(!r||!t)&&a;n&&l(!1),!t&&r&&(t=a),s=t||window.self,y.load(e),o=!0}function l(t=!0){n&&(y.masks[0].remove(),y.masks[0]=null,y.masks[1].remove(),y.masks[1]=null,y.masks[2].remove(),y.masks[2]=null,t&&brush.load())}function c(){n||(o||h(),z.create(),A(s.width/250),n=!0)}let m=new e(Math.random());const d={random:(t=0,e=1)=>t+m()*(e-t),randInt(t,e){return Math.floor(this.random(t,e))},gaussian(t=0,e=1){const i=1-m(),s=m();return Math.sqrt(-2*Math.log(i))*Math.cos(2*Math.PI*s)*e+t},weightedRand(t){let e,i,s=[];for(e in t)for(i=0;i<10*t[e];i++)s.push(e);return s[Math.floor(m()*s.length)]},map(t,e,i,s,n,o=!1){let r=s+(t-e)/(i-e)*(n-s);return o?sMath.max(Math.min(t,i),e),cos(t){return this.c[Math.floor((t%360+360)%360*4)]},sin(t){return this.s[Math.floor((t%360+360)%360*4)]},isPrecalculationDone:!1,preCalculation(){if(this.isPrecalculationDone)return;const t=1440,e=2*Math.PI/t;this.c=new Float64Array(t),this.s=new Float64Array(t);for(let i=0;i!isNaN(t),toDegrees:t=>(("radians"===s.angleMode()?180*t/Math.PI:t)%360+360)%360,dist:(t,e,i,s)=>Math.hypot(i-t,s-e)};function u(t,e,i,s,n=!1){let o=t.x,r=t.y,a=e.x,h=e.y,l=i.x,c=i.y,m=s.x,d=s.y;if(o===a&&r===h||l===m&&c===d)return!1;let u=a-o,p=h-r,f=m-l,v=d-c,g=v*u-f*p;if(0===g)return!1;let x=(f*(r-c)-v*(o-l))/g,y=(u*(r-c)-p*(o-l))/g;return!(!n&&(y<0||y>1))&&{x:o+x*u,y:r+x*p}}function p(t,e,i,s){return(Math.atan2(-(s-e),i-t)*(180/Math.PI)%360+360)%360}d.preCalculation();const f={field:{},stroke:{},hatch:{},fill:{},others:{}};const v={translation:[0,0],rotation:0,trans(){return this.translation=[s._renderer.uModelMatrix.mat4[12],s._renderer.uModelMatrix.mat4[13]],this.translation}};let g=1;function x(t){g*=t}const y={loaded:!1,isBlending:!1,isCaching:!0,currentColor:new Float32Array(3),load(t){this.type=r&&!t?0:t?2:1,this.masks=[];for(let e=0;e<3;e++)switch(this.type){case 0:this.masks[e]=s.createGraphics(s.width,s.height,1==e?s.WEBGL:s.P2D);break;case 1:this.masks[e]=createGraphics(s.width,s.height,1==e?WEBGL:P2D);break;case 2:this.masks[e]=t.createGraphics(t.width,t.height,1==e?t.WEBGL:t.P2D)}for(let t of this.masks)t.pixelDensity(s.pixelDensity()),t.clear(),t.angleMode(s.DEGREES),t.noSmooth();this.shader=s.createShader(this.vert,this.frag),y.loaded=!0},getPigment(t){let e=t.levels,i=new Float32Array(3);return i[0]=e[0]/255,i[1]=e[1]/255,i[2]=e[2]/255,i},color1:new Float32Array(3),color2:new Float32Array(3),blending1:!1,blending2:!1,blend(t=!1,e=!1,i=!1){if(c(),this.isBlending=i?this.blending1:this.blending2,this.currentColor=i?this.color1:this.color2,!this.isBlending)if(t)this.currentColor=this.getPigment(t),i?(this.blending1=!0,this.color1=this.currentColor):(this.blending2=!0,this.color2=this.currentColor);else if(e)return void(i||w());if((t?this.getPigment(t):this.currentColor).toString()!==this.currentColor.toString()||e||!this.isCaching){if(w(),this.isBlending){s.push(),s.translate(-v.trans()[0],-v.trans()[1]),s.shader(this.shader),this.shader.setUniform("addColor",this.currentColor),this.shader.setUniform("source",s._renderer),this.shader.setUniform("active",y.watercolor),this.shader.setUniform("random",[d.random(),d.random(),d.random()]);let t=i?this.masks[1]:this.masks[0];this.shader.setUniform("mask",t),s.fill(0,0,0,0),s.noStroke(),s.rect(-s.width/2,-s.height/2,s.width,s.height),s.pop(),t.clear()}e||(this.currentColor=this.getPigment(t),i?this.color1=this.currentColor:this.color2=this.currentColor)}e&&(this.isBlending=!1,i?this.blending1=this.isBlending:this.blending2=this.isBlending)},vert:"precision highp float;attribute vec3 aPosition;attribute vec2 aTexCoord;uniform mat4 uModelViewMatrix,uProjectionMatrix;varying vec2 vVertTexCoord;void main(){gl_Position=uProjectionMatrix*uModelViewMatrix*vec4(aPosition,1);vVertTexCoord=aTexCoord;}",frag:"precision highp float;varying vec2 vVertTexCoord;uniform sampler2D source,mask;uniform vec4 addColor;uniform vec3 random;uniform bool active;\n #ifndef SPECTRAL\n #define SPECTRAL\n float x(float v){return v<.04045?v/12.92:pow((v+.055)/1.055,2.4);}float v(float v){return v<.0031308?v*12.92:1.055*pow(v,1./2.4)-.055;}vec3 m(vec3 v){return vec3(x(v[0]),x(v[1]),x(v[2]));}vec3 f(vec3 f){return clamp(vec3(v(f[0]),v(f[1]),v(f[2])),0.,1.);}void f(vec3 v,out float m,out float f,out float x,out float y,out float z,out float i,out float r){m=min(v.x,min(v.y,v.z));v-=m;f=min(v.y,v.z);x=min(v.x,v.z);y=min(v.x,v.y);z=min(max(0.,v.x-v.z),max(0.,v.x-v.y));i=min(max(0.,v.y-v.z),max(0.,v.y-v.x));r=min(max(0.,v.z-v.y),max(0.,v.z-v.x));}void f(vec3 v,inout float i[38]){float x,y,d,z,o,m,e;f(v,x,y,d,z,o,m,e);i[0]=max(1e-4,x+y*.96853629+d*.51567122+z*.02055257+o*.03147571+m*.49108579+e*.97901834);i[1]=max(1e-4,x+y*.96855103+d*.5401552+z*.02059936+o*.03146636+m*.46944057+e*.97901649);i[2]=max(1e-4,x+y*.96859338+d*.62645502+z*.02062723+o*.03140624+m*.4016578+e*.97901118);i[3]=max(1e-4,x+y*.96877345+d*.75595012+z*.02073387+o*.03119611+m*.2449042+e*.97892146);i[4]=max(1e-4,x+y*.96942204+d*.92826996+z*.02114202+o*.03053888+m*.0682688+e*.97858555);i[5]=max(1e-4,x+y*.97143709+d*.97223624+z*.02233154+o*.02856855+m*.02732883+e*.97743705);i[6]=max(1e-4,x+y*.97541862+d*.98616174+z*.02556857+o*.02459485+m*.013606+e*.97428075);i[7]=max(1e-4,x+y*.98074186+d*.98955255+z*.03330189+o*.0192952+m*.01000187+e*.96663223);i[8]=max(1e-4,x+y*.98580992+d*.98676237+z*.05185294+o*.01423112+m*.01284127+e*.94822893);i[9]=max(1e-4,x+y*.98971194+d*.97312575+z*.10087639+o*.01033111+m*.02636635+e*.89937713);i[10]=max(1e-4,x+y*.99238027+d*.91944277+z*.24000413+o*.00765876+m*.07058713+e*.76070164);i[11]=max(1e-4,x+y*.99409844+d*.32564851+z*.53589066+o*.00593693+m*.70421692+e*.4642044);i[12]=max(1e-4,x+y*.995172+d*.13820628+z*.79874659+o*.00485616+m*.85473994+e*.20123039);i[13]=max(1e-4,x+y*.99576545+d*.05015143+z*.91186529+o*.00426186+m*.95081565+e*.08808402);i[14]=max(1e-4,x+y*.99593552+d*.02912336+z*.95399623+o*.00409039+m*.9717037+e*.04592894);i[15]=max(1e-4,x+y*.99564041+d*.02421691+z*.97137099+o*.00438375+m*.97651888+e*.02860373);i[16]=max(1e-4,x+y*.99464769+d*.02660696+z*.97939505+o*.00537525+m*.97429245+e*.02060067);i[17]=max(1e-4,x+y*.99229579+d*.03407586+z*.98345207+o*.00772962+m*.97012917+e*.01656701);i[18]=max(1e-4,x+y*.98638762+d*.04835936+z*.98553736+o*.0136612+m*.9425863+e*.01451549);i[19]=max(1e-4,x+y*.96829712+d*.0001172+z*.98648905+o*.03181352+m*.99989207+e*.01357964);i[20]=max(1e-4,x+y*.89228016+d*8.554e-5+z*.98674535+o*.10791525+m*.99989891+e*.01331243);i[21]=max(1e-4,x+y*.53740239+d*.85267882+z*.98657555+o*.46249516+m*.13823139+e*.01347661);i[22]=max(1e-4,x+y*.15360445+d*.93188793+z*.98611877+o*.84604333+m*.06968113+e*.01387181);i[23]=max(1e-4,x+y*.05705719+d*.94810268+z*.98559942+o*.94275572+m*.05628787+e*.01435472);i[24]=max(1e-4,x+y*.03126539+d*.94200977+z*.98507063+o*.96860996+m*.06111561+e*.01479836);i[25]=max(1e-4,x+y*.02205445+d*.91478045+z*.98460039+o*.97783966+m*.08987709+e*.0151525);i[26]=max(1e-4,x+y*.01802271+d*.87065445+z*.98425301+o*.98187757+m*.13656016+e*.01540513);i[27]=max(1e-4,x+y*.0161346+d*.78827548+z*.98403909+o*.98377315+m*.22169624+e*.01557233);i[28]=max(1e-4,x+y*.01520947+d*.65738359+z*.98388535+o*.98470202+m*.32176956+e*.0156571);i[29]=max(1e-4,x+y*.01475977+d*.59909403+z*.98376116+o*.98515481+m*.36157329+e*.01571025);i[30]=max(1e-4,x+y*.01454263+d*.56817268+z*.98368246+o*.98537114+m*.4836192+e*.01571916);i[31]=max(1e-4,x+y*.01444459+d*.54031997+z*.98365023+o*.98546685+m*.46488579+e*.01572133);i[32]=max(1e-4,x+y*.01439897+d*.52110241+z*.98361309+o*.98550011+m*.47440306+e*.01572502);i[33]=max(1e-4,x+y*.0143762+d*.51041094+z*.98357259+o*.98551031+m*.4857699+e*.01571717);i[34]=max(1e-4,x+y*.01436343+d*.50526577+z*.98353856+o*.98550741+m*.49267971+e*.01571905);i[35]=max(1e-4,x+y*.01435687+d*.5025508+z*.98351247+o*.98551323+m*.49625685+e*.01571059);i[36]=max(1e-4,x+y*.0143537+d*.50126452+z*.98350101+o*.98551563+m*.49807754+e*.01569728);i[37]=max(1e-4,x+y*.01435408+d*.50083021+z*.98350852+o*.98551547+m*.49889859+e*.0157002);}vec3 t(vec3 x){mat3 i;i[0]=vec3(3.24306333,-1.53837619,-.49893282);i[1]=vec3(-.96896309,1.87542451,.04154303);i[2]=vec3(.05568392,-.20417438,1.05799454);float v=dot(i[0],x),y=dot(i[1],x),o=dot(i[2],x);return f(vec3(v,y,o));}vec3 d(float m[38]){vec3 i=vec3(0);i+=m[0]*vec3(6.469e-5,1.84e-6,.00030502);i+=m[1]*vec3(.00021941,6.21e-6,.00103681);i+=m[2]*vec3(.00112057,3.101e-5,.00531314);i+=m[3]*vec3(.00376661,.00010475,.01795439);i+=m[4]*vec3(.01188055,.00035364,.05707758);i+=m[5]*vec3(.02328644,.00095147,.11365162);i+=m[6]*vec3(.03455942,.00228226,.17335873);i+=m[7]*vec3(.03722379,.00420733,.19620658);i+=m[8]*vec3(.03241838,.0066888,.18608237);i+=m[9]*vec3(.02123321,.0098884,.13995048);i+=m[10]*vec3(.01049099,.01524945,.08917453);i+=m[11]*vec3(.00329584,.02141831,.04789621);i+=m[12]*vec3(.00050704,.03342293,.02814563);i+=m[13]*vec3(.00094867,.05131001,.01613766);i+=m[14]*vec3(.00627372,.07040208,.0077591);i+=m[15]*vec3(.01686462,.08783871,.00429615);i+=m[16]*vec3(.02868965,.09424905,.00200551);i+=m[17]*vec3(.04267481,.09795667,.00086147);i+=m[18]*vec3(.05625475,.09415219,.00036904);i+=m[19]*vec3(.0694704,.08678102,.00019143);i+=m[20]*vec3(.08305315,.07885653,.00014956);i+=m[21]*vec3(.0861261,.0635267,9.231e-5);i+=m[22]*vec3(.09046614,.05374142,6.813e-5);i+=m[23]*vec3(.08500387,.04264606,2.883e-5);i+=m[24]*vec3(.07090667,.03161735,1.577e-5);i+=m[25]*vec3(.05062889,.02088521,3.94e-6);i+=m[26]*vec3(.03547396,.01386011,1.58e-6);i+=m[27]*vec3(.02146821,.00810264,0);i+=m[28]*vec3(.01251646,.0046301,0);i+=m[29]*vec3(.00680458,.00249138,0);i+=m[30]*vec3(.00346457,.0012593,0);i+=m[31]*vec3(.00149761,.00054165,0);i+=m[32]*vec3(.0007697,.00027795,0);i+=m[33]*vec3(.00040737,.00014711,0);i+=m[34]*vec3(.00016901,6.103e-5,0);i+=m[35]*vec3(9.522e-5,3.439e-5,0);i+=m[36]*vec3(4.903e-5,1.771e-5,0);i+=m[37]*vec3(2e-5,7.22e-6,0);return i;}float d(float y,float m,float v){float z=m*pow(v,2.);return z/(y*pow(1.-v,2.)+z);}vec3 f(vec3 v,vec3 y,float z){vec3 x=m(v),o=m(y);float i[38],a[38];f(x,i);f(o,a);float r=d(i)[1],e=d(a)[1];z=d(r,e,z);float s[38];for(int u=0;u<38;u++){float p=(1.-z)*(pow(1.-i[u],2.)/(2.*i[u]))+z*(pow(1.-a[u],2.)/(2.*a[u]));s[u]=1.+p-sqrt(pow(p,2.)+2.*p);}return t(d(s));}vec4 f(vec4 v,vec4 x,float y){return vec4(f(v.xyz,x.xyz,y),mix(v.w,x.w,y));}\n #endif\n float d(vec2 m,vec2 v,float y,out vec2 i){vec2 f=vec2(m.x+m.y*.5,m.y),x=floor(f),o=fract(f);float z=step(o.y,o.x);vec2 d=vec2(z,1.-z),r=x+d,e=x+1.,a=vec2(x.x-x.y*.5,x.y),p=vec2(a.x+d.x-d.y*.5,a.y+d.y),s=vec2(a.x+.5,a.y+1.),w=m-a,g=m-p,k=m-s;vec3 u,c,t,A;if(any(greaterThan(v,vec2(0)))){t=vec3(a.x,p.x,s);A=vec3(a.y,p.y,s.y);if(v.x>0.)t=mod(vec3(a.x,p.x,s),v.x);if(v.y>0.)A=mod(vec3(a.y,p.y,s.y),v.y);u=floor(t+.5*A+.5);c=floor(A+.5);}else u=vec3(x.x,r.x,e),c=vec3(x.y,r.y,e.y);vec3 S=mod(u,289.);S=mod((S*51.+2.)*S+c,289.);S=mod((S*34.+10.)*S,289.);vec3 b=S*.07482+y,C=cos(b),D=sin(b);vec2 h=vec2(C.x,D),B=vec2(C.y,D.y),E=vec2(C.z,D.z);vec3 F=.8-vec3(dot(w,w),dot(g,g),dot(k,k));F=max(F,0.);vec3 G=F*F,H=G*G,I=vec3(dot(h,w),dot(B,g),dot(E,k)),J=G*F,K=-8.*J*I;i=10.9*(H.x*h+K.x*w+(H.y*B+K.y*g)+(H.z*E+K.z*k));return 10.9*dot(H,I);}vec4 d(vec3 v,float x){return vec4(mix(v,vec3(dot(vec3(.299,.587,.114),v)),x),1);}float f(vec2 v,float x,float y,float f){return fract(sin(dot(v,vec2(x,y)))*f);}void main(){vec4 v=texture2D(mask,vVertTexCoord);if(v.x>0.){vec2 x=vec2(12.9898,78.233),o=vec2(7.9898,58.233),m=vec2(17.9898,3.233);float y=f(vVertTexCoord,x.x,x.y,43358.5453)*2.-1.,z=f(vVertTexCoord,o.x,o.y,43213.5453)*2.-1.,e=f(vVertTexCoord,m.x,m.y,33358.5453)*2.-1.;const vec2 i=vec2(0);vec2 s;vec4 r;if(active){float a=d(vVertTexCoord*5.,i,10.*random.x,s),p=d(vVertTexCoord*5.,i,10.*random.y,s),g=d(vVertTexCoord*5.,i,10.*random.z,s),k=.25+.25*d(vVertTexCoord*4.,i,3.*random.x,s);r=vec4(d(addColor.xyz,k).xyz+vec3(a,p,g)*.03*abs(addColor.x-addColor.y-addColor.z),1);}else r=vec4(addColor.xyz,1);if(v.w>.7){float a=.5*(v.w-.7);r=r*(1.-a)-vec4(.5)*a;}vec3 a=f(texture2D(source,vVertTexCoord).xyz,r.xyz,.9*v.w);gl_FragColor=vec4(a+.01*vec3(y,z,e),1);}}"};function w(){s.push(),s.translate(-v.trans()[0],-v.trans()[1]),s.image(y.masks[2],-s.width/2,-s.height/2),y.masks[2].clear(),s.pop()}function k(t){t.registerMethod("afterSetup",(()=>y.blend(!1,!0))),t.registerMethod("afterSetup",(()=>y.blend(!1,!0,!0))),t.registerMethod("post",(()=>y.blend(!1,!0))),t.registerMethod("post",(()=>y.blend(!1,!0,!0)))}function _(t,e){z.list.set(t,{gen:e}),z.current=t,z.refresh()}"undefined"!=typeof p5&&k(p5.prototype);const z={isActive:!1,list:new Map,current:"",step_length:()=>Math.min(s.width,s.height)/1e3,create(){this.R=.01*s.width,this.left_x=-1*s.width,this.top_y=-1*s.height,this.num_columns=Math.round(2*s.width/this.R),this.num_rows=Math.round(2*s.height/this.R),this.addStandard()},flow_field(){return this.list.get(this.current).field},refresh(t=0){this.list.get(this.current).field=this.list.get(this.current).gen(t,this.genField())},genField(){let t=new Array(this.num_columns);for(let e=0;e=0&&this.row_index>=0&&this.column_index=-t-v.trans()[0]&&this.x<=t-v.trans()[0]&&this.y>=-e-v.trans()[1]&&this.y<=e-v.trans()[1]}angle(){return this.isIn()&&z.isActive?z.flow_field()[this.column_index][this.row_index]:0}moveTo(t,e,i=C.spacing(),s=!0){if(this.isIn()){let n,o;s||(n=d.cos(-e),o=d.sin(-e));for(let r=0;r=C.cr[0]&&this.position.x<=C.cr[2]&&this.position.y>=C.cr[1]&&this.position.y<=C.cr[3];{let t=.55*s.width,e=.55*s.height;return this.position.x>=-t-v.trans()[0]&&this.position.x<=t-v.trans()[0]&&this.position.y>=-e-v.trans()[1]&&this.position.y<=e-v.trans()[1]}},drawSpray(t){let e=this.w*this.p.vibration*t+this.w*d.gaussian()*this.p.vibration/3,i=this.p.weight*d.random(.9,1.1);const s=this.p.quality/t;for(let t=0;t.4&&this.mask.circle(this.position.x+.7*e*d.random(-1,1),this.position.y+e*d.random(-1,1),t*this.p.weight*d.random(.85,1.15))},adjustSizeAndRotation(t,e){if(this.mask.scale(t),"image"===this.p.type&&(this.p.blend?this.mask.tint(255,0,0,e/2):this.mask.tint(this.mask.red(this.c),this.mask.green(this.c),this.mask.blue(this.c),e)),"random"===this.p.rotate)this.mask.rotate(d.randInt(0,360));else if("natural"===this.p.rotate){let t=(this.plot?-this.plot.angle(this.position.plotted):-this.dir)+(this.flow?this.position.angle():0);this.mask.rotate(t)}},markerTip(){if(this.isInsideClippingArea()){let t=this.calculatePressure(),e=this.calculateAlpha(t);if(this.mask.fill(255,0,0,e/1.5),"marker"===C.p.type)for(let e=1;e<5;e++)this.drawMarker(t*e/5,!1);else if("custom"===C.p.type||"image"===C.p.type)for(let i=1;i<5;i++)this.drawCustomOrImage(t*i/5,e,!1)}}};function S(t,e){const i="marker"===e.type||"custom"===e.type||"image"===e.type;i||"spray"===e.type||(e.type="default"),"image"===e.type&&(B.add(e.image.src),e.tip=()=>C.mask.image(B.tips.get(C.p.image.src),-C.p.weight/2,-C.p.weight/2,C.p.weight,C.p.weight)),e.blend=!!(i&&!1!==e.blend||e.blend),C.list.set(t,{param:e,colors:[],buffers:[]})}function P(t,e,i=1){I(t),C.c=e,C.w=i,C.isActive=!0}function I(t){C.name=t}function D(t,e,i,s){c();let n=d.dist(t,e,i,s);if(0==n)return;C.initializeDrawingState(t,e,n,!1,!1);let o=p(t,e,i,s);C.draw(o,!1)}function T(t,e,i,s){c(),C.initializeDrawingState(e,i,t.length,!0,t),C.draw(s,!0)}const B={tips:new Map,add(t){this.tips.set(t,!1)},imageToWhite(t){t.loadPixels();for(let e=0;e<4*t.width*t.height;e+=4){let i=(t.pixels[e]+t.pixels[e+1]+t.pixels[e+2])/3;t.pixels[e]=t.pixels[e+1]=t.pixels[e+2]=255,t.pixels[e+3]=255-i}t.updatePixels()},load(){for(let t of this.tips.keys()){let e=(r?a:window.self).loadImage(t,(()=>B.imageToWhite(e)));this.tips.set(t,e)}}};function E(t=5,e=45,i={rand:!1,continuous:!1,gradient:!1}){F.isActive=!0,F.hatchingParams=[t,e,i]}const F={isActive:!1,hatchingParams:[5,45,{}],hatchingBrush:!1,hatch(t){let e=F.hatchingParams[0],i=F.hatchingParams[1],s=F.hatchingParams[2],n=C.c,o=C.name,r=C.w,a=C.isActive;F.hatchingBrush&&P(F.hatchingBrush[0],F.hatchingBrush[1],F.hatchingBrush[2]),i=d.toDegrees(i)%180;let h=1/0,l=-1/0,c=1/0,m=-1/0,u=t=>{for(let e of t.a)h=e[0]l?e[0]:l,c=e[1]m?e[1]:m};Array.isArray(t)||(t=[t]);for(let e of t)u(e);let p=new V([[h,c],[l,c],[l,m],[h,m]]),f=i<=90&&i>=0?c:m,v=s.gradient?d.map(s.gradient,0,1,1,1.1,!0):1,g=[],x=0,y=e,w=t=>({point1:{x:h+y*t*d.cos(90-i),y:f+y*t*d.sin(90-i)},point2:{x:h+y*t*d.cos(90-i)+d.cos(-i),y:f+y*t*d.sin(90-i)+d.sin(-i)}});for(;p.intersect(w(x)).length>0;){let e=[];for(let i of t)e.push(i.intersect(w(x)));g[x]=e.flat().sort(((t,e)=>t.x===e.x?t.y-e.y:t.x-e.x)),y*=v,x++}let k=[];for(let t of g)void 0!==t[0]&&k.push(t);let _=s.rand?s.rand:0;for(let t=0;t0&&s.continuous;for(let s=0;s({x:t[0],y:t[1]}))),e&&(this.vertices=t),this.sides=this.vertices.map(((t,e,i)=>[t,i[(e+1)%i.length]]))}intersect(t){let e=`${t.point1.x},${t.point1.y}-${t.point2.x},${t.point2.y}`;if(this._intersectionCache&&this._intersectionCache[e])return this._intersectionCache[e];let i=[];for(let e of this.sides){let s=u(t.point1,t.point2,e[0],e[1]);!1!==s&&i.push(s)}return this._intersectionCache||(this._intersectionCache={}),this._intersectionCache[e]=i,i}draw(t=!1,e,i){let s=C.isActive;if(t&&P(t,e,i),C.isActive){c();for(let t of this.sides)D(t[0].x,t[0].y,t[1].x,t[1].y)}C.isActive=s}fill(t=!1,e,i,s,n,o){let r=J.isActive;t&&(U(t,e),K(i,o),$(s,n)),J.isActive&&(c(),J.fill(this)),J.isActive=r}hatch(t=!1,e,i){let s=F.isActive;t&&E(t,e,i),F.isActive&&(c(),F.hatch(this)),F.isActive=s}erase(t=!1,e=j.a){if(j.isActive||t){y.masks[2].push(),y.masks[2].noStroke();let i=s.color(t||j.c);i.setAlpha(e),y.masks[2].fill(i),y.masks[2].beginShape();for(let t of this.vertices)y.masks[2].vertex(t.x,t.y);y.masks[2].endShape(s.CLOSE),y.masks[2].pop()}}show(){this.fill(),this.hatch(),this.draw(),this.erase()}}class G{constructor(t){this.segments=[],this.angles=[],this.pres=[],this.type=t,this.dir=0,this.calcIndex(0),this.pol=!1}addSegment(t=0,e=0,i=1,s=!1){this.angles.length>0&&this.angles.splice(-1),t=s?(t%360+360)%360:d.toDegrees(t),this.angles.push(t),this.pres.push(i),this.segments.push(e),this.length=this.segments.reduce(((t,e)=>t+e),0),this.angles.push(t)}endPlot(t=0,e=1,i=!1){t=i?(t%360+360)%360:d.toDegrees(t),this.angles.splice(-1),this.angles.push(t),this.pres.push(e)}rotate(t){this.dir=d.toDegrees(t)}pressure(t){return t>this.length?this.pres[this.pres.length-1]:this.curving(this.pres,t)}angle(t){return t>this.length?this.angles[this.angles.length-1]:(this.calcIndex(t),"curve"===this.type?this.curving(this.angles,t)+this.dir:this.angles[this.index]+this.dir)}curving(t,e){let i=t[this.index],s=t[this.index+1];return void 0===s&&(s=i),Math.abs(s-i)>180&&(s>i?s=-(360-s):i=-(360-i)),d.map(e-this.suma,0,this.segments[this.index],i,s,!0)}calcIndex(t){this.index=-1,this.suma=0;let e=0;for(;e<=t;)this.suma=e,e+=this.segments[this.index+1],this.index++;return this.index}genPol(t,e,i=1,s=!1){c();const n=.5,o=[],r=Math.round(this.length/n),a=new b(t,e);let h=s?.15:3*J.bleed_strength,l=0,m=0;for(let t=0;t=this.segments[t]*h*d.random(.7,1.3)||t>=m)&&a.x&&(o.push([a.x,a.y]),l=0,t>=m&&m++)}return new V(o)}draw(t,e,i){C.isActive&&(c(),this.origin&&(t=this.origin[0],e=this.origin[1],i=1),T(this,t,e,i))}fill(t,e,i){J.isActive&&(c(),this.origin&&(t=this.origin[0],e=this.origin[1],i=1),this.pol=this.genPol(t,e,i),this.pol.fill())}hatch(t,e,i){F.isActive&&(c(),this.origin&&(t=this.origin[0],e=this.origin[1],i=1),this.pol=this.genPol(t,e,i,!0),this.pol.hatch())}erase(t,e,i){if(j.isActive){this.origin&&(t=this.origin[0],e=this.origin[1],i=1),this.pol=this.genPol(t,e,i,!0),y.masks[2].push(),y.masks[2].noStroke();let n=s.color(j.c);n.setAlpha(j.a),y.masks[2].fill(n),y.masks[2].beginShape();for(let t of this.pol.vertices)y.masks[2].vertex(t.x,t.y);y.masks[2].endShape(s.CLOSE),y.masks[2].pop()}}show(t,e,i=1){this.draw(t,e,i),this.fill(t,e,i),this.hatch(t,e,i),this.erase(t,e,i)}}let L,H=!1;function O(t=0){L=d.constrain(t,0,1),H=[]}function q(t,e,i){H.push([t,e,i])}function N(t){c(),t===s.CLOSE&&(H.push(H[0]),H.push(H[1])),(0!=L||z.isActive?W(H,L,t===s.CLOSE):new V(H)).show(),H=!1}function W(t,e=.5,i=!1){let s=new G(0===e?"segments":"curve");if(t&&t.length>0){let n,o,r,a=0;for(let h=0;h0&&h{let s=d.random(.8,1.2)*this.bleed_strength;return ithis.size&&(this.size=e)}if(n)for(let t=0;t(e.x-t.x)*(i.y-t.y)-(e.y-t.y)*(i.x-t.x)>.01;let a=0;for(let t of J.polygon.intersect(o))r(e,i,t)&&a++;this.dir[t]=a%2==0}}trim(t){let e=[...this.v],i=[...this.m],s=[...this.dir];if(this.v.length>10&&t>=.2){let n=~~((1-t)*this.v.length),o=~~this.v.length/2-~~n/2;e.splice(o,n),i.splice(o,n),s.splice(o,n)}return{v:e,m:i,dir:s}}grow(t,e=!1){const i=[],s=[],n=[];let o=this.trim(t);const r=e?-.5:1,a=t=>t+.1*(d.gaussian(.5,.1)-.5);for(let e=0;es?d.random(-1,1):0;n.addSegment(0+r+a(),o+a(),1,!0),n.addSegment(-90+r+a(),o+a(),1,!0),n.addSegment(-180+r+a(),o+a(),1,!0),n.addSegment(-270+r+a(),o+a(),1,!0);let h=s?d.randInt(-5,5):0;s&&n.addSegment(0+r,h*(Math.PI/180)*i,!0),n.endPlot(h+r,1,!0);let l=[t-i*d.sin(r),e-i*d.cos(-r)];n.show(l[0],l[1],1)},t.clip=function(t){C.cr=t},t.colorCache=function(t=!0){y.isCaching=t},t.endShape=N,t.endStroke=function(t,e){H.endPlot(t,e),H.draw(L[0],L[1],1),H=!1},t.erase=function(t="white",e=255){j.isActive=!0,j.c=t,j.a=e},t.field=function(t){c(),z.isActive=!0,z.current=t},t.fill=U,t.fillAnimatedMode=function(t){J.isAnimated=t},t.fillTexture=$,t.flowLine=function(t,e,i,s){c(),C.initializeDrawingState(t,e,i,!0,!1),C.draw(d.toDegrees(s),!1)},t.gravity=function(t,e){c(),J.light_source={x:t,y:e}},t.hatch=E,t.hatchArray=R,t.instance=function(t){r=!0,a=t,s=t,k(t)},t.line=D,t.listFields=function(){return Array.from(z.list.keys())},t.load=h,t.noClip=function(){C.cr=null},t.noErase=function(){j.isActive=!1},t.noField=function(){c(),z.isActive=!1},t.noFill=function(){J.isActive=!1},t.noGravity=function(){J.light_source=!1},t.noHatch=function(){F.isActive=!1,F.hatchingBrush=!1},t.noStroke=function(){C.isActive=!1},t.pick=I,t.plot=T,t.polygon=function(t){new V(t).show()},t.pop=function(){z.isActive=f.field.isActive,z.current=f.field.current,C.isActive=f.stroke.isActive,C.name=f.stroke.name,C.c=f.stroke.color,C.w=f.stroke.weight,C.cr=f.stroke.clip,F.isActive=f.hatch.isActive,F.hatchingParams=f.hatch.hatchingParams,F.hatchingBrush=f.hatch.hatchingBrush,J.isActive=f.fill.isActive,J.color=f.fill.color,J.opacity=f.fill.opacity,J.bleed_strength=f.fill.bleed_strength,J.texture_strength=f.fill.texture_strength,J.border_strength=f.fill.border_strength,v.rotation=f.others.rotate},t.preload=function(){B.load()},t.push=function(){f.field.isActive=z.isActive,f.field.current=z.current,f.stroke.isActive=C.isActive,f.stroke.name=C.name,f.stroke.color=C.c,f.stroke.weight=C.w,f.stroke.clip=C.cr,f.hatch.isActive=F.isActive,f.hatch.hatchingParams=F.hatchingParams,f.hatch.hatchingBrush=F.hatchingBrush,f.fill.isActive=J.isActive,f.fill.color=J.color,f.fill.opacity=J.opacity,f.fill.bleed_strength=J.bleed_strength,f.fill.texture_strength=J.texture_strength,f.fill.border_strength=J.border_strength,f.others.rotate=v.rotation},t.reBlend=function(){y.blend(!1,!0),y.blend(!1,!0,!0)},t.reDraw=w,t.rect=function(t,e,i,n,o=s.CORNER){if(o==s.CENTER&&(t-=i/2,e-=n/2),z.isActive)O(0),q(t,e),q(t+i,e),q(t+i,e+n),q(t,e+n),N(s.CLOSE);else{new V([[t,e],[t+i,e],[t+i,e+n],[t,e+n]]).show()}},t.refreshField=function(t){z.refresh(t)},t.remove=l,t.rotate=function(t=0){v.rotation=d.toDegrees(t)},t.scale=x,t.scaleBrushes=A,t.seed=function(t){m=new e(t)},t.segment=function(t,e,i){H.addSegment(t,e,i)},t.set=P,t.setHatch=function(t,e="black",i=1){F.hatchingBrush=[t,e,i]},t.spline=function(t,e=.5){W(t,e).draw()},t.stroke=function(t,e,i){arguments.length>0&&(C.c=arguments.length<2?t:[t,e,i]),C.isActive=!0},t.strokeWeight=function(t){C.w=t},t.vertex=q})); diff --git a/example/index.html b/example/index.html index 51297d6..20cb0b3 100644 --- a/example/index.html +++ b/example/index.html @@ -1,12 +1,12 @@ - + p5.brush.js Example - - - + + + - \ No newline at end of file + diff --git a/example/p5.brush.js b/example/p5.brush.js new file mode 100644 index 0000000..333aec4 --- /dev/null +++ b/example/p5.brush.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).brush={})}(this,(function(t){"use strict";function e(t,e){let s=new i(t),n=()=>s.next();return n.double=()=>n()+11102230246251565e-32*(2097152*n()|0),n.int32=()=>4294967296*s.next()|0,n.quick=n,function(t,e,i){let s=i&&i.state;s&&("object"==typeof s&&e.copy(s,e),t.state=()=>e.copy(e,{}))}(n,s,e),n}class i{constructor(t){null==t&&(t=+new Date);let e=4022871197;function i(t){t=String(t);for(let i=0;i>>0,s-=e,s*=e,e=s>>>0,s-=e,e+=4294967296*s}return 2.3283064365386963e-10*(e>>>0)}this.c=1,this.s0=i(" "),this.s1=i(" "),this.s2=i(" "),this.s0-=i(t),this.s0<0&&(this.s0+=1),this.s1-=i(t),this.s1<0&&(this.s1+=1),this.s2-=i(t),this.s2<0&&(this.s2+=1)}next(){let{c:t,s0:e,s1:i,s2:s}=this,n=2091639*e+2.3283064365386963e-10*t;return this.s0=i,this.s1=s,this.s2=n-(this.c=0|n)}copy(t,e){return e.c=t.c,e.s0=t.s0,e.s1=t.s1,e.s2=t.s2,e}}let s,n=!1,o=!1,r=!1,a=!1;function h(t=!1){let e=!(!r||!t)&&a;n&&l(!1),!t&&r&&(t=a),s=t||window.self,y.load(e),o=!0}function l(t=!0){n&&(y.masks[0].remove(),y.masks[0]=null,y.masks[1].remove(),y.masks[1]=null,y.masks[2].remove(),y.masks[2]=null,t&&brush.load())}function c(){n||(o||h(),z.create(),A(s.width/250),n=!0)}let m=new e(Math.random());const d={random:(t=0,e=1)=>t+m()*(e-t),randInt(t,e){return Math.floor(this.random(t,e))},gaussian(t=0,e=1){const i=1-m(),s=m();return Math.sqrt(-2*Math.log(i))*Math.cos(2*Math.PI*s)*e+t},weightedRand(t){let e,i,s=[];for(e in t)for(i=0;i<10*t[e];i++)s.push(e);return s[Math.floor(m()*s.length)]},map(t,e,i,s,n,o=!1){let r=s+(t-e)/(i-e)*(n-s);return o?sMath.max(Math.min(t,i),e),cos(t){return this.c[Math.floor((t%360+360)%360*4)]},sin(t){return this.s[Math.floor((t%360+360)%360*4)]},isPrecalculationDone:!1,preCalculation(){if(this.isPrecalculationDone)return;const t=1440,e=2*Math.PI/t;this.c=new Float64Array(t),this.s=new Float64Array(t);for(let i=0;i!isNaN(t),toDegrees:t=>(("radians"===s.angleMode()?180*t/Math.PI:t)%360+360)%360,dist:(t,e,i,s)=>Math.hypot(i-t,s-e)};function u(t,e,i,s,n=!1){let o=t.x,r=t.y,a=e.x,h=e.y,l=i.x,c=i.y,m=s.x,d=s.y;if(o===a&&r===h||l===m&&c===d)return!1;let u=a-o,p=h-r,f=m-l,v=d-c,g=v*u-f*p;if(0===g)return!1;let x=(f*(r-c)-v*(o-l))/g,y=(u*(r-c)-p*(o-l))/g;return!(!n&&(y<0||y>1))&&{x:o+x*u,y:r+x*p}}function p(t,e,i,s){return(Math.atan2(-(s-e),i-t)*(180/Math.PI)%360+360)%360}d.preCalculation();const f={field:{},stroke:{},hatch:{},fill:{},others:{}};const v={translation:[0,0],rotation:0,trans(){return this.translation=[s._renderer.uModelMatrix.mat4[12],s._renderer.uModelMatrix.mat4[13]],this.translation}};let g=1;function x(t){g*=t}const y={loaded:!1,isBlending:!1,isCaching:!0,currentColor:new Float32Array(3),load(t){this.type=r&&!t?0:t?2:1,this.masks=[];for(let e=0;e<3;e++)switch(this.type){case 0:this.masks[e]=s.createGraphics(s.width,s.height,1==e?s.WEBGL:s.P2D);break;case 1:this.masks[e]=createGraphics(s.width,s.height,1==e?WEBGL:P2D);break;case 2:this.masks[e]=t.createGraphics(t.width,t.height,1==e?t.WEBGL:t.P2D)}for(let t of this.masks)t.pixelDensity(s.pixelDensity()),t.clear(),t.angleMode(s.DEGREES),t.noSmooth();this.shader=s.createShader(this.vert,this.frag),y.loaded=!0},getPigment(t){let e=t.levels,i=new Float32Array(3);return i[0]=e[0]/255,i[1]=e[1]/255,i[2]=e[2]/255,i},color1:new Float32Array(3),color2:new Float32Array(3),blending1:!1,blending2:!1,blend(t=!1,e=!1,i=!1){if(c(),this.isBlending=i?this.blending1:this.blending2,this.currentColor=i?this.color1:this.color2,!this.isBlending)if(t)this.currentColor=this.getPigment(t),i?(this.blending1=!0,this.color1=this.currentColor):(this.blending2=!0,this.color2=this.currentColor);else if(e)return void(i||w());if((t?this.getPigment(t):this.currentColor).toString()!==this.currentColor.toString()||e||!this.isCaching){if(w(),this.isBlending){s.push(),s.translate(-v.trans()[0],-v.trans()[1]),s.shader(this.shader),this.shader.setUniform("addColor",this.currentColor),this.shader.setUniform("source",s._renderer),this.shader.setUniform("active",y.watercolor),this.shader.setUniform("random",[d.random(),d.random(),d.random()]);let t=i?this.masks[1]:this.masks[0];this.shader.setUniform("mask",t),s.fill(0,0,0,0),s.noStroke(),s.rect(-s.width/2,-s.height/2,s.width,s.height),s.pop(),t.clear()}e||(this.currentColor=this.getPigment(t),i?this.color1=this.currentColor:this.color2=this.currentColor)}e&&(this.isBlending=!1,i?this.blending1=this.isBlending:this.blending2=this.isBlending)},vert:"precision highp float;attribute vec3 aPosition;attribute vec2 aTexCoord;uniform mat4 uModelViewMatrix,uProjectionMatrix;varying vec2 vVertTexCoord;void main(){gl_Position=uProjectionMatrix*uModelViewMatrix*vec4(aPosition,1);vVertTexCoord=aTexCoord;}",frag:"precision highp float;varying vec2 vVertTexCoord;uniform sampler2D source,mask;uniform vec4 addColor;uniform vec3 random;uniform bool active;\n #ifndef SPECTRAL\n #define SPECTRAL\n float x(float v){return v<.04045?v/12.92:pow((v+.055)/1.055,2.4);}float v(float v){return v<.0031308?v*12.92:1.055*pow(v,1./2.4)-.055;}vec3 m(vec3 v){return vec3(x(v[0]),x(v[1]),x(v[2]));}vec3 f(vec3 f){return clamp(vec3(v(f[0]),v(f[1]),v(f[2])),0.,1.);}void f(vec3 v,out float m,out float f,out float x,out float y,out float z,out float i,out float r){m=min(v.x,min(v.y,v.z));v-=m;f=min(v.y,v.z);x=min(v.x,v.z);y=min(v.x,v.y);z=min(max(0.,v.x-v.z),max(0.,v.x-v.y));i=min(max(0.,v.y-v.z),max(0.,v.y-v.x));r=min(max(0.,v.z-v.y),max(0.,v.z-v.x));}void f(vec3 v,inout float i[38]){float x,y,d,z,o,m,e;f(v,x,y,d,z,o,m,e);i[0]=max(1e-4,x+y*.96853629+d*.51567122+z*.02055257+o*.03147571+m*.49108579+e*.97901834);i[1]=max(1e-4,x+y*.96855103+d*.5401552+z*.02059936+o*.03146636+m*.46944057+e*.97901649);i[2]=max(1e-4,x+y*.96859338+d*.62645502+z*.02062723+o*.03140624+m*.4016578+e*.97901118);i[3]=max(1e-4,x+y*.96877345+d*.75595012+z*.02073387+o*.03119611+m*.2449042+e*.97892146);i[4]=max(1e-4,x+y*.96942204+d*.92826996+z*.02114202+o*.03053888+m*.0682688+e*.97858555);i[5]=max(1e-4,x+y*.97143709+d*.97223624+z*.02233154+o*.02856855+m*.02732883+e*.97743705);i[6]=max(1e-4,x+y*.97541862+d*.98616174+z*.02556857+o*.02459485+m*.013606+e*.97428075);i[7]=max(1e-4,x+y*.98074186+d*.98955255+z*.03330189+o*.0192952+m*.01000187+e*.96663223);i[8]=max(1e-4,x+y*.98580992+d*.98676237+z*.05185294+o*.01423112+m*.01284127+e*.94822893);i[9]=max(1e-4,x+y*.98971194+d*.97312575+z*.10087639+o*.01033111+m*.02636635+e*.89937713);i[10]=max(1e-4,x+y*.99238027+d*.91944277+z*.24000413+o*.00765876+m*.07058713+e*.76070164);i[11]=max(1e-4,x+y*.99409844+d*.32564851+z*.53589066+o*.00593693+m*.70421692+e*.4642044);i[12]=max(1e-4,x+y*.995172+d*.13820628+z*.79874659+o*.00485616+m*.85473994+e*.20123039);i[13]=max(1e-4,x+y*.99576545+d*.05015143+z*.91186529+o*.00426186+m*.95081565+e*.08808402);i[14]=max(1e-4,x+y*.99593552+d*.02912336+z*.95399623+o*.00409039+m*.9717037+e*.04592894);i[15]=max(1e-4,x+y*.99564041+d*.02421691+z*.97137099+o*.00438375+m*.97651888+e*.02860373);i[16]=max(1e-4,x+y*.99464769+d*.02660696+z*.97939505+o*.00537525+m*.97429245+e*.02060067);i[17]=max(1e-4,x+y*.99229579+d*.03407586+z*.98345207+o*.00772962+m*.97012917+e*.01656701);i[18]=max(1e-4,x+y*.98638762+d*.04835936+z*.98553736+o*.0136612+m*.9425863+e*.01451549);i[19]=max(1e-4,x+y*.96829712+d*.0001172+z*.98648905+o*.03181352+m*.99989207+e*.01357964);i[20]=max(1e-4,x+y*.89228016+d*8.554e-5+z*.98674535+o*.10791525+m*.99989891+e*.01331243);i[21]=max(1e-4,x+y*.53740239+d*.85267882+z*.98657555+o*.46249516+m*.13823139+e*.01347661);i[22]=max(1e-4,x+y*.15360445+d*.93188793+z*.98611877+o*.84604333+m*.06968113+e*.01387181);i[23]=max(1e-4,x+y*.05705719+d*.94810268+z*.98559942+o*.94275572+m*.05628787+e*.01435472);i[24]=max(1e-4,x+y*.03126539+d*.94200977+z*.98507063+o*.96860996+m*.06111561+e*.01479836);i[25]=max(1e-4,x+y*.02205445+d*.91478045+z*.98460039+o*.97783966+m*.08987709+e*.0151525);i[26]=max(1e-4,x+y*.01802271+d*.87065445+z*.98425301+o*.98187757+m*.13656016+e*.01540513);i[27]=max(1e-4,x+y*.0161346+d*.78827548+z*.98403909+o*.98377315+m*.22169624+e*.01557233);i[28]=max(1e-4,x+y*.01520947+d*.65738359+z*.98388535+o*.98470202+m*.32176956+e*.0156571);i[29]=max(1e-4,x+y*.01475977+d*.59909403+z*.98376116+o*.98515481+m*.36157329+e*.01571025);i[30]=max(1e-4,x+y*.01454263+d*.56817268+z*.98368246+o*.98537114+m*.4836192+e*.01571916);i[31]=max(1e-4,x+y*.01444459+d*.54031997+z*.98365023+o*.98546685+m*.46488579+e*.01572133);i[32]=max(1e-4,x+y*.01439897+d*.52110241+z*.98361309+o*.98550011+m*.47440306+e*.01572502);i[33]=max(1e-4,x+y*.0143762+d*.51041094+z*.98357259+o*.98551031+m*.4857699+e*.01571717);i[34]=max(1e-4,x+y*.01436343+d*.50526577+z*.98353856+o*.98550741+m*.49267971+e*.01571905);i[35]=max(1e-4,x+y*.01435687+d*.5025508+z*.98351247+o*.98551323+m*.49625685+e*.01571059);i[36]=max(1e-4,x+y*.0143537+d*.50126452+z*.98350101+o*.98551563+m*.49807754+e*.01569728);i[37]=max(1e-4,x+y*.01435408+d*.50083021+z*.98350852+o*.98551547+m*.49889859+e*.0157002);}vec3 t(vec3 x){mat3 i;i[0]=vec3(3.24306333,-1.53837619,-.49893282);i[1]=vec3(-.96896309,1.87542451,.04154303);i[2]=vec3(.05568392,-.20417438,1.05799454);float v=dot(i[0],x),y=dot(i[1],x),o=dot(i[2],x);return f(vec3(v,y,o));}vec3 d(float m[38]){vec3 i=vec3(0);i+=m[0]*vec3(6.469e-5,1.84e-6,.00030502);i+=m[1]*vec3(.00021941,6.21e-6,.00103681);i+=m[2]*vec3(.00112057,3.101e-5,.00531314);i+=m[3]*vec3(.00376661,.00010475,.01795439);i+=m[4]*vec3(.01188055,.00035364,.05707758);i+=m[5]*vec3(.02328644,.00095147,.11365162);i+=m[6]*vec3(.03455942,.00228226,.17335873);i+=m[7]*vec3(.03722379,.00420733,.19620658);i+=m[8]*vec3(.03241838,.0066888,.18608237);i+=m[9]*vec3(.02123321,.0098884,.13995048);i+=m[10]*vec3(.01049099,.01524945,.08917453);i+=m[11]*vec3(.00329584,.02141831,.04789621);i+=m[12]*vec3(.00050704,.03342293,.02814563);i+=m[13]*vec3(.00094867,.05131001,.01613766);i+=m[14]*vec3(.00627372,.07040208,.0077591);i+=m[15]*vec3(.01686462,.08783871,.00429615);i+=m[16]*vec3(.02868965,.09424905,.00200551);i+=m[17]*vec3(.04267481,.09795667,.00086147);i+=m[18]*vec3(.05625475,.09415219,.00036904);i+=m[19]*vec3(.0694704,.08678102,.00019143);i+=m[20]*vec3(.08305315,.07885653,.00014956);i+=m[21]*vec3(.0861261,.0635267,9.231e-5);i+=m[22]*vec3(.09046614,.05374142,6.813e-5);i+=m[23]*vec3(.08500387,.04264606,2.883e-5);i+=m[24]*vec3(.07090667,.03161735,1.577e-5);i+=m[25]*vec3(.05062889,.02088521,3.94e-6);i+=m[26]*vec3(.03547396,.01386011,1.58e-6);i+=m[27]*vec3(.02146821,.00810264,0);i+=m[28]*vec3(.01251646,.0046301,0);i+=m[29]*vec3(.00680458,.00249138,0);i+=m[30]*vec3(.00346457,.0012593,0);i+=m[31]*vec3(.00149761,.00054165,0);i+=m[32]*vec3(.0007697,.00027795,0);i+=m[33]*vec3(.00040737,.00014711,0);i+=m[34]*vec3(.00016901,6.103e-5,0);i+=m[35]*vec3(9.522e-5,3.439e-5,0);i+=m[36]*vec3(4.903e-5,1.771e-5,0);i+=m[37]*vec3(2e-5,7.22e-6,0);return i;}float d(float y,float m,float v){float z=m*pow(v,2.);return z/(y*pow(1.-v,2.)+z);}vec3 f(vec3 v,vec3 y,float z){vec3 x=m(v),o=m(y);float i[38],a[38];f(x,i);f(o,a);float r=d(i)[1],e=d(a)[1];z=d(r,e,z);float s[38];for(int u=0;u<38;u++){float p=(1.-z)*(pow(1.-i[u],2.)/(2.*i[u]))+z*(pow(1.-a[u],2.)/(2.*a[u]));s[u]=1.+p-sqrt(pow(p,2.)+2.*p);}return t(d(s));}vec4 f(vec4 v,vec4 x,float y){return vec4(f(v.xyz,x.xyz,y),mix(v.w,x.w,y));}\n #endif\n float d(vec2 m,vec2 v,float y,out vec2 i){vec2 f=vec2(m.x+m.y*.5,m.y),x=floor(f),o=fract(f);float z=step(o.y,o.x);vec2 d=vec2(z,1.-z),r=x+d,e=x+1.,a=vec2(x.x-x.y*.5,x.y),p=vec2(a.x+d.x-d.y*.5,a.y+d.y),s=vec2(a.x+.5,a.y+1.),w=m-a,g=m-p,k=m-s;vec3 u,c,t,A;if(any(greaterThan(v,vec2(0)))){t=vec3(a.x,p.x,s);A=vec3(a.y,p.y,s.y);if(v.x>0.)t=mod(vec3(a.x,p.x,s),v.x);if(v.y>0.)A=mod(vec3(a.y,p.y,s.y),v.y);u=floor(t+.5*A+.5);c=floor(A+.5);}else u=vec3(x.x,r.x,e),c=vec3(x.y,r.y,e.y);vec3 S=mod(u,289.);S=mod((S*51.+2.)*S+c,289.);S=mod((S*34.+10.)*S,289.);vec3 b=S*.07482+y,C=cos(b),D=sin(b);vec2 h=vec2(C.x,D),B=vec2(C.y,D.y),E=vec2(C.z,D.z);vec3 F=.8-vec3(dot(w,w),dot(g,g),dot(k,k));F=max(F,0.);vec3 G=F*F,H=G*G,I=vec3(dot(h,w),dot(B,g),dot(E,k)),J=G*F,K=-8.*J*I;i=10.9*(H.x*h+K.x*w+(H.y*B+K.y*g)+(H.z*E+K.z*k));return 10.9*dot(H,I);}vec4 d(vec3 v,float x){return vec4(mix(v,vec3(dot(vec3(.299,.587,.114),v)),x),1);}float f(vec2 v,float x,float y,float f){return fract(sin(dot(v,vec2(x,y)))*f);}void main(){vec4 v=texture2D(mask,vVertTexCoord);if(v.x>0.){vec2 x=vec2(12.9898,78.233),o=vec2(7.9898,58.233),m=vec2(17.9898,3.233);float y=f(vVertTexCoord,x.x,x.y,43358.5453)*2.-1.,z=f(vVertTexCoord,o.x,o.y,43213.5453)*2.-1.,e=f(vVertTexCoord,m.x,m.y,33358.5453)*2.-1.;const vec2 i=vec2(0);vec2 s;vec4 r;if(active){float a=d(vVertTexCoord*5.,i,10.*random.x,s),p=d(vVertTexCoord*5.,i,10.*random.y,s),g=d(vVertTexCoord*5.,i,10.*random.z,s),k=.25+.25*d(vVertTexCoord*4.,i,3.*random.x,s);r=vec4(d(addColor.xyz,k).xyz+vec3(a,p,g)*.03*abs(addColor.x-addColor.y-addColor.z),1);}else r=vec4(addColor.xyz,1);if(v.w>.7){float a=.5*(v.w-.7);r=r*(1.-a)-vec4(.5)*a;}vec3 a=f(texture2D(source,vVertTexCoord).xyz,r.xyz,.9*v.w);gl_FragColor=vec4(a+.01*vec3(y,z,e),1);}}"};function w(){s.push(),s.translate(-v.trans()[0],-v.trans()[1]),s.image(y.masks[2],-s.width/2,-s.height/2),y.masks[2].clear(),s.pop()}function k(t){t.registerMethod("afterSetup",(()=>y.blend(!1,!0))),t.registerMethod("afterSetup",(()=>y.blend(!1,!0,!0))),t.registerMethod("post",(()=>y.blend(!1,!0))),t.registerMethod("post",(()=>y.blend(!1,!0,!0)))}function _(t,e){z.list.set(t,{gen:e}),z.current=t,z.refresh()}"undefined"!=typeof p5&&k(p5.prototype);const z={isActive:!1,list:new Map,current:"",step_length:()=>Math.min(s.width,s.height)/1e3,create(){this.R=.01*s.width,this.left_x=-1*s.width,this.top_y=-1*s.height,this.num_columns=Math.round(2*s.width/this.R),this.num_rows=Math.round(2*s.height/this.R),this.addStandard()},flow_field(){return this.list.get(this.current).field},refresh(t=0){this.list.get(this.current).field=this.list.get(this.current).gen(t,this.genField())},genField(){let t=new Array(this.num_columns);for(let e=0;e=0&&this.row_index>=0&&this.column_index=-t-v.trans()[0]&&this.x<=t-v.trans()[0]&&this.y>=-e-v.trans()[1]&&this.y<=e-v.trans()[1]}angle(){return this.isIn()&&z.isActive?z.flow_field()[this.column_index][this.row_index]:0}moveTo(t,e,i=C.spacing(),s=!0){if(this.isIn()){let n,o;s||(n=d.cos(-e),o=d.sin(-e));for(let r=0;r=C.cr[0]&&this.position.x<=C.cr[2]&&this.position.y>=C.cr[1]&&this.position.y<=C.cr[3];{let t=.55*s.width,e=.55*s.height;return this.position.x>=-t-v.trans()[0]&&this.position.x<=t-v.trans()[0]&&this.position.y>=-e-v.trans()[1]&&this.position.y<=e-v.trans()[1]}},drawSpray(t){let e=this.w*this.p.vibration*t+this.w*d.gaussian()*this.p.vibration/3,i=this.p.weight*d.random(.9,1.1);const s=this.p.quality/t;for(let t=0;t.4&&this.mask.circle(this.position.x+.7*e*d.random(-1,1),this.position.y+e*d.random(-1,1),t*this.p.weight*d.random(.85,1.15))},adjustSizeAndRotation(t,e){if(this.mask.scale(t),"image"===this.p.type&&(this.p.blend?this.mask.tint(255,0,0,e/2):this.mask.tint(this.mask.red(this.c),this.mask.green(this.c),this.mask.blue(this.c),e)),"random"===this.p.rotate)this.mask.rotate(d.randInt(0,360));else if("natural"===this.p.rotate){let t=(this.plot?-this.plot.angle(this.position.plotted):-this.dir)+(this.flow?this.position.angle():0);this.mask.rotate(t)}},markerTip(){if(this.isInsideClippingArea()){let t=this.calculatePressure(),e=this.calculateAlpha(t);if(this.mask.fill(255,0,0,e/1.5),"marker"===C.p.type)for(let e=1;e<5;e++)this.drawMarker(t*e/5,!1);else if("custom"===C.p.type||"image"===C.p.type)for(let i=1;i<5;i++)this.drawCustomOrImage(t*i/5,e,!1)}}};function S(t,e){const i="marker"===e.type||"custom"===e.type||"image"===e.type;i||"spray"===e.type||(e.type="default"),"image"===e.type&&(B.add(e.image.src),e.tip=()=>C.mask.image(B.tips.get(C.p.image.src),-C.p.weight/2,-C.p.weight/2,C.p.weight,C.p.weight)),e.blend=!!(i&&!1!==e.blend||e.blend),C.list.set(t,{param:e,colors:[],buffers:[]})}function P(t,e,i=1){I(t),C.c=e,C.w=i,C.isActive=!0}function I(t){C.name=t}function D(t,e,i,s){c();let n=d.dist(t,e,i,s);if(0==n)return;C.initializeDrawingState(t,e,n,!1,!1);let o=p(t,e,i,s);C.draw(o,!1)}function T(t,e,i,s){c(),C.initializeDrawingState(e,i,t.length,!0,t),C.draw(s,!0)}const B={tips:new Map,add(t){this.tips.set(t,!1)},imageToWhite(t){t.loadPixels();for(let e=0;e<4*t.width*t.height;e+=4){let i=(t.pixels[e]+t.pixels[e+1]+t.pixels[e+2])/3;t.pixels[e]=t.pixels[e+1]=t.pixels[e+2]=255,t.pixels[e+3]=255-i}t.updatePixels()},load(){for(let t of this.tips.keys()){let e=(r?a:window.self).loadImage(t,(()=>B.imageToWhite(e)));this.tips.set(t,e)}}};function E(t=5,e=45,i={rand:!1,continuous:!1,gradient:!1}){F.isActive=!0,F.hatchingParams=[t,e,i]}const F={isActive:!1,hatchingParams:[5,45,{}],hatchingBrush:!1,hatch(t){let e=F.hatchingParams[0],i=F.hatchingParams[1],s=F.hatchingParams[2],n=C.c,o=C.name,r=C.w,a=C.isActive;F.hatchingBrush&&P(F.hatchingBrush[0],F.hatchingBrush[1],F.hatchingBrush[2]),i=d.toDegrees(i)%180;let h=1/0,l=-1/0,c=1/0,m=-1/0,u=t=>{for(let e of t.a)h=e[0]l?e[0]:l,c=e[1]m?e[1]:m};Array.isArray(t)||(t=[t]);for(let e of t)u(e);let p=new V([[h,c],[l,c],[l,m],[h,m]]),f=i<=90&&i>=0?c:m,v=s.gradient?d.map(s.gradient,0,1,1,1.1,!0):1,g=[],x=0,y=e,w=t=>({point1:{x:h+y*t*d.cos(90-i),y:f+y*t*d.sin(90-i)},point2:{x:h+y*t*d.cos(90-i)+d.cos(-i),y:f+y*t*d.sin(90-i)+d.sin(-i)}});for(;p.intersect(w(x)).length>0;){let e=[];for(let i of t)e.push(i.intersect(w(x)));g[x]=e.flat().sort(((t,e)=>t.x===e.x?t.y-e.y:t.x-e.x)),y*=v,x++}let k=[];for(let t of g)void 0!==t[0]&&k.push(t);let _=s.rand?s.rand:0;for(let t=0;t0&&s.continuous;for(let s=0;s({x:t[0],y:t[1]}))),e&&(this.vertices=t),this.sides=this.vertices.map(((t,e,i)=>[t,i[(e+1)%i.length]]))}intersect(t){let e=`${t.point1.x},${t.point1.y}-${t.point2.x},${t.point2.y}`;if(this._intersectionCache&&this._intersectionCache[e])return this._intersectionCache[e];let i=[];for(let e of this.sides){let s=u(t.point1,t.point2,e[0],e[1]);!1!==s&&i.push(s)}return this._intersectionCache||(this._intersectionCache={}),this._intersectionCache[e]=i,i}draw(t=!1,e,i){let s=C.isActive;if(t&&P(t,e,i),C.isActive){c();for(let t of this.sides)D(t[0].x,t[0].y,t[1].x,t[1].y)}C.isActive=s}fill(t=!1,e,i,s,n,o){let r=J.isActive;t&&(U(t,e),K(i,o),$(s,n)),J.isActive&&(c(),J.fill(this)),J.isActive=r}hatch(t=!1,e,i){let s=F.isActive;t&&E(t,e,i),F.isActive&&(c(),F.hatch(this)),F.isActive=s}erase(t=!1,e=j.a){if(j.isActive||t){y.masks[2].push(),y.masks[2].noStroke();let i=s.color(t||j.c);i.setAlpha(e),y.masks[2].fill(i),y.masks[2].beginShape();for(let t of this.vertices)y.masks[2].vertex(t.x,t.y);y.masks[2].endShape(s.CLOSE),y.masks[2].pop()}}show(){this.fill(),this.hatch(),this.draw(),this.erase()}}class G{constructor(t){this.segments=[],this.angles=[],this.pres=[],this.type=t,this.dir=0,this.calcIndex(0),this.pol=!1}addSegment(t=0,e=0,i=1,s=!1){this.angles.length>0&&this.angles.splice(-1),t=s?(t%360+360)%360:d.toDegrees(t),this.angles.push(t),this.pres.push(i),this.segments.push(e),this.length=this.segments.reduce(((t,e)=>t+e),0),this.angles.push(t)}endPlot(t=0,e=1,i=!1){t=i?(t%360+360)%360:d.toDegrees(t),this.angles.splice(-1),this.angles.push(t),this.pres.push(e)}rotate(t){this.dir=d.toDegrees(t)}pressure(t){return t>this.length?this.pres[this.pres.length-1]:this.curving(this.pres,t)}angle(t){return t>this.length?this.angles[this.angles.length-1]:(this.calcIndex(t),"curve"===this.type?this.curving(this.angles,t)+this.dir:this.angles[this.index]+this.dir)}curving(t,e){let i=t[this.index],s=t[this.index+1];return void 0===s&&(s=i),Math.abs(s-i)>180&&(s>i?s=-(360-s):i=-(360-i)),d.map(e-this.suma,0,this.segments[this.index],i,s,!0)}calcIndex(t){this.index=-1,this.suma=0;let e=0;for(;e<=t;)this.suma=e,e+=this.segments[this.index+1],this.index++;return this.index}genPol(t,e,i=1,s=!1){c();const n=.5,o=[],r=Math.round(this.length/n),a=new b(t,e);let h=s?.15:3*J.bleed_strength,l=0,m=0;for(let t=0;t=this.segments[t]*h*d.random(.7,1.3)||t>=m)&&a.x&&(o.push([a.x,a.y]),l=0,t>=m&&m++)}return new V(o)}draw(t,e,i){C.isActive&&(c(),this.origin&&(t=this.origin[0],e=this.origin[1],i=1),T(this,t,e,i))}fill(t,e,i){J.isActive&&(c(),this.origin&&(t=this.origin[0],e=this.origin[1],i=1),this.pol=this.genPol(t,e,i),this.pol.fill())}hatch(t,e,i){F.isActive&&(c(),this.origin&&(t=this.origin[0],e=this.origin[1],i=1),this.pol=this.genPol(t,e,i,!0),this.pol.hatch())}erase(t,e,i){if(j.isActive){this.origin&&(t=this.origin[0],e=this.origin[1],i=1),this.pol=this.genPol(t,e,i,!0),y.masks[2].push(),y.masks[2].noStroke();let n=s.color(j.c);n.setAlpha(j.a),y.masks[2].fill(n),y.masks[2].beginShape();for(let t of this.pol.vertices)y.masks[2].vertex(t.x,t.y);y.masks[2].endShape(s.CLOSE),y.masks[2].pop()}}show(t,e,i=1){this.draw(t,e,i),this.fill(t,e,i),this.hatch(t,e,i),this.erase(t,e,i)}}let L,H=!1;function O(t=0){L=d.constrain(t,0,1),H=[]}function q(t,e,i){H.push([t,e,i])}function N(t){c(),t===s.CLOSE&&(H.push(H[0]),H.push(H[1])),(0!=L||z.isActive?W(H,L,t===s.CLOSE):new V(H)).show(),H=!1}function W(t,e=.5,i=!1){let s=new G(0===e?"segments":"curve");if(t&&t.length>0){let n,o,r,a=0;for(let h=0;h0&&h{let s=d.random(.8,1.2)*this.bleed_strength;return ithis.size&&(this.size=e)}if(n)for(let t=0;t(e.x-t.x)*(i.y-t.y)-(e.y-t.y)*(i.x-t.x)>.01;let a=0;for(let t of J.polygon.intersect(o))r(e,i,t)&&a++;this.dir[t]=a%2==0}}trim(t){let e=[...this.v],i=[...this.m],s=[...this.dir];if(this.v.length>10&&t>=.2){let n=~~((1-t)*this.v.length),o=~~this.v.length/2-~~n/2;e.splice(o,n),i.splice(o,n),s.splice(o,n)}return{v:e,m:i,dir:s}}grow(t,e=!1){const i=[],s=[],n=[];let o=this.trim(t);const r=e?-.5:1,a=t=>t+.1*(d.gaussian(.5,.1)-.5);for(let e=0;es?d.random(-1,1):0;n.addSegment(0+r+a(),o+a(),1,!0),n.addSegment(-90+r+a(),o+a(),1,!0),n.addSegment(-180+r+a(),o+a(),1,!0),n.addSegment(-270+r+a(),o+a(),1,!0);let h=s?d.randInt(-5,5):0;s&&n.addSegment(0+r,h*(Math.PI/180)*i,!0),n.endPlot(h+r,1,!0);let l=[t-i*d.sin(r),e-i*d.cos(-r)];n.show(l[0],l[1],1)},t.clip=function(t){C.cr=t},t.colorCache=function(t=!0){y.isCaching=t},t.endShape=N,t.endStroke=function(t,e){H.endPlot(t,e),H.draw(L[0],L[1],1),H=!1},t.erase=function(t="white",e=255){j.isActive=!0,j.c=t,j.a=e},t.field=function(t){c(),z.isActive=!0,z.current=t},t.fill=U,t.fillAnimatedMode=function(t){J.isAnimated=t},t.fillTexture=$,t.flowLine=function(t,e,i,s){c(),C.initializeDrawingState(t,e,i,!0,!1),C.draw(d.toDegrees(s),!1)},t.gravity=function(t,e){c(),J.light_source={x:t,y:e}},t.hatch=E,t.hatchArray=R,t.instance=function(t){r=!0,a=t,s=t,k(t)},t.line=D,t.listFields=function(){return Array.from(z.list.keys())},t.load=h,t.noClip=function(){C.cr=null},t.noErase=function(){j.isActive=!1},t.noField=function(){c(),z.isActive=!1},t.noFill=function(){J.isActive=!1},t.noGravity=function(){J.light_source=!1},t.noHatch=function(){F.isActive=!1,F.hatchingBrush=!1},t.noStroke=function(){C.isActive=!1},t.pick=I,t.plot=T,t.polygon=function(t){new V(t).show()},t.pop=function(){z.isActive=f.field.isActive,z.current=f.field.current,C.isActive=f.stroke.isActive,C.name=f.stroke.name,C.c=f.stroke.color,C.w=f.stroke.weight,C.cr=f.stroke.clip,F.isActive=f.hatch.isActive,F.hatchingParams=f.hatch.hatchingParams,F.hatchingBrush=f.hatch.hatchingBrush,J.isActive=f.fill.isActive,J.color=f.fill.color,J.opacity=f.fill.opacity,J.bleed_strength=f.fill.bleed_strength,J.texture_strength=f.fill.texture_strength,J.border_strength=f.fill.border_strength,v.rotation=f.others.rotate},t.preload=function(){B.load()},t.push=function(){f.field.isActive=z.isActive,f.field.current=z.current,f.stroke.isActive=C.isActive,f.stroke.name=C.name,f.stroke.color=C.c,f.stroke.weight=C.w,f.stroke.clip=C.cr,f.hatch.isActive=F.isActive,f.hatch.hatchingParams=F.hatchingParams,f.hatch.hatchingBrush=F.hatchingBrush,f.fill.isActive=J.isActive,f.fill.color=J.color,f.fill.opacity=J.opacity,f.fill.bleed_strength=J.bleed_strength,f.fill.texture_strength=J.texture_strength,f.fill.border_strength=J.border_strength,f.others.rotate=v.rotation},t.reBlend=function(){y.blend(!1,!0),y.blend(!1,!0,!0)},t.reDraw=w,t.rect=function(t,e,i,n,o=s.CORNER){if(o==s.CENTER&&(t-=i/2,e-=n/2),z.isActive)O(0),q(t,e),q(t+i,e),q(t+i,e+n),q(t,e+n),N(s.CLOSE);else{new V([[t,e],[t+i,e],[t+i,e+n],[t,e+n]]).show()}},t.refreshField=function(t){z.refresh(t)},t.remove=l,t.rotate=function(t=0){v.rotation=d.toDegrees(t)},t.scale=x,t.scaleBrushes=A,t.seed=function(t){m=new e(t)},t.segment=function(t,e,i){H.addSegment(t,e,i)},t.set=P,t.setHatch=function(t,e="black",i=1){F.hatchingBrush=[t,e,i]},t.spline=function(t,e=.5){W(t,e).draw()},t.stroke=function(t,e,i){arguments.length>0&&(C.c=arguments.length<2?t:[t,e,i]),C.isActive=!0},t.strokeWeight=function(t){C.w=t},t.vertex=q})); diff --git a/package.json b/package.json index 4c33aa4..c0ca050 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "p5.brush", - "version": "1.1.3", + "version": "1.1.4", "description": "Unlock custom brushes, natural fill effects and intuitive hatching in p5.js", "main": "src/index.js", "module": "src/index.js", @@ -36,7 +36,7 @@ "rollup-plugin-cleanup": "^3.2.1" }, "peerDependencies": { - "p5": "^1.9.0" + "p5": "^1.11.0" }, "dependencies": { "esm-seedrandom": "^3.0.5" diff --git a/src/index.js b/src/index.js index 55b9c4e..a5e0a8d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,9 @@ /** * @fileoverview p5.brush - A comprehensive toolset for brush management in p5.js. - * @version 1.1.3 + * @version 1.1.4 * @license MIT * @author Alejandro Campos Uribe - * + *n * @description * p5.brush is a p5.js library dedicated to the creation and management of custom brushes. * It extends the drawing capabilities of p5.js by allowing users to simulate a wide range @@ -18,11 +18,11 @@ * brush.stroke(255, 0, 0); // Set brush color * brush.strokeWeight(10); // Set brush size * brush.line(25, 25, 75, 75); // Draw a line - * + * * // Add a new brush type: * brush.add('customBrush', { /* parameters for the brush *\/ }); * brush.pick('customBrush'); - * + * * // Use the custom brush for a vector-field line: * brush.field('field_name') // Pick a flowfield * brush.flowLine(50, 50, 100, PI / 4); // Draw a line within the vector-field @@ -32,22 +32,22 @@ * brush.fill('#FF0000', 90); // Set fill color to red and opacity to 90 * brush.bleed(0.3); // Set bleed effect for a watercolor-like appearance * brush.rect(100, 100, 50, 50); // Fill a rectangle with the bleed effect - * + * * @license * MIT License - * + * * Copyright (c) 2023-2024 Alejandro Campos Uribe - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -61,646 +61,680 @@ // Section: Configure and Initiate // ============================================================================= /** - * This section contains functions for setting up the drawing system. It allows + * This section contains functions for setting up the drawing system. It allows * for configuration with custom options, initialization of the system, preloading - * necessary assets, and a check to ensure the system is ready before any drawing + * necessary assets, and a check to ensure the system is ready before any drawing * operation is performed. */ - /** - * Reference to the renderer or canvas object. - * @type {Object} - */ - let _r; - - /** - * Flag to indicate if the system is ready for rendering. - * @type {boolean} - */ - let _isReady = false; - let _isLoaded = false; - - /** - * Flag to indicate if p5 is instanced or global mode. - * @type {boolean} - */ - let _isInstanced = false; - let _inst = false; - - /** - * Initializes the drawing system and sets up the environment. - * @param {string|boolean} [canvasID=false] - Optional ID of the canvas element to use. - * If false, it uses the current window as the rendering context. - */ - export function load (canvasID = false) { - let inst = (_isInstanced && canvasID) ? _inst : false; - if (_isReady) remove(false) - // Set the renderer to the specified canvas or to the window if no ID is given - if (!canvasID && _isInstanced) canvasID = _inst; - _r = (!canvasID) ? window.self : canvasID; - - // Load color blending - Mix.load(inst); - _isLoaded = true; - } +/** + * Reference to the renderer or canvas object. + * @type {Object} + */ +let _r; - /** - * Removes brush buffers - */ - export function remove (a = true) { - if (_isReady) { - Mix.masks[0].remove() - Mix.masks[0] = null; - Mix.masks[1].remove() - Mix.masks[1] = null; - Mix.masks[2].remove() - Mix.masks[2] = null; - if (a) brush.load() - } - } +/** + * Flag to indicate if the system is ready for rendering. + * @type {boolean} + */ +let _isReady = false; +let _isLoaded = false; - /** - * Preloads necessary assets or configurations. - * This function should be called before setup to ensure all assets are loaded. - */ - export function preload () { - // Load custom image tips - T.load(); - } +/** + * Flag to indicate if p5 is instanced or global mode. + * @type {boolean} + */ +let _isInstanced = false; +let _inst = false; - /** - * Ensures that the drawing system is ready before any drawing operation. - * Loads the system if it hasn't been loaded already. - */ - function _ensureReady () { - if (!_isReady) { - if (!_isLoaded) load(); - FF.create(); // Load flowfield system - scaleBrushes(_r.width / 250) // Adjust standard brushes to match canvas - _isReady = true; - } - } +/** + * Initializes the drawing system and sets up the environment. + * @param {string|boolean} [canvasID=false] - Optional ID of the canvas element to use. + * If false, it uses the current window as the rendering context. + */ +export function load(canvasID = false) { + let inst = _isInstanced && canvasID ? _inst : false; + if (_isReady) remove(false); + // Set the renderer to the specified canvas or to the window if no ID is given + if (!canvasID && _isInstanced) canvasID = _inst; + _r = !canvasID ? window.self : canvasID; + + // Load color blending + Mix.load(inst); + _isLoaded = true; +} + +/** + * Removes brush buffers + */ +export function remove(a = true) { + if (_isReady) { + Mix.masks[0].remove(); + Mix.masks[0] = null; + Mix.masks[1].remove(); + Mix.masks[1] = null; + Mix.masks[2].remove(); + Mix.masks[2] = null; + if (a) brush.load(); + } +} + +/** + * Preloads necessary assets or configurations. + * This function should be called before setup to ensure all assets are loaded. + */ +export function preload() { + // Load custom image tips + T.load(); +} + +/** + * Ensures that the drawing system is ready before any drawing operation. + * Loads the system if it hasn't been loaded already. + */ +function _ensureReady() { + if (!_isReady) { + if (!_isLoaded) load(); + FF.create(); // Load flowfield system + scaleBrushes(_r.width / 250); // Adjust standard brushes to match canvas + _isReady = true; + } +} // ============================================================================= // Section: Randomness and other auxiliary functions // ============================================================================= /** - * This section includes utility functions for randomness, mapping values, - * constraining numbers within a range, and precalculated trigonometric values - * to optimize performance. Additionally, it provides auxiliary functions for - * geometric calculations such as translation extraction, line intersection, + * This section includes utility functions for randomness, mapping values, + * constraining numbers within a range, and precalculated trigonometric values + * to optimize performance. Additionally, it provides auxiliary functions for + * geometric calculations such as translation extraction, line intersection, * and angle calculation. */ - import { prng_alea } from 'esm-seedrandom'; - - /** - * The basic source of randomness, can be seeded for determinism. - * @returns {number} A random number between 0 and 1. - */ - let rng = new prng_alea(Math.random()) - export function seed (s) { - rng = new prng_alea(s) - } - - /** - * Object for random number generation and related utility functions. - * @property {function} source - Function that returns a random number from the base random generator. - * @property {function} random - Function to generate a random number within a specified range. - * @property {function} randInt - Function to generate a random integer within a specified range. - * @property {function} weightedRand - Function to generate a random value based on weighted probabilities. - * @property {function} map - Function to remap a number from one range to another. - * @property {function} constrain - Function to constrain a number within a range. - * @property {function} cos - Function to get the cosine of an angle from precalculated values. - * @property {function} sin - Function to get the sine of an angle from precalculated values. - * @property {boolean} isPrecalculationDone - Flag to check if precalculation of trigonometric values is complete. - * @property {function} preCalculation - Function to precalculate trigonometric values. - */ - const R = { - - /** - * Generates a random number within a specified range. - * @param {number} [min=0] - The lower bound of the range. - * @param {number} [max=1] - The upper bound of the range. - * @returns {number} A random number within the specified range. - */ - random(e = 0, r = 1) { - return e + rng() * (r - e); - }, - - /** - * Generates a random integer within a specified range. - * @param {number} min - The lower bound of the range. - * @param {number} max - The upper bound of the range. - * @returns {number} A random integer within the specified range. - */ - randInt(e, r) { - return Math.floor(this.random(e,r)) - }, - - /** - * Generates a random gaussian. - * @param {number} mean - Mean. - * @param {number} stdev - Standard deviation. - * @returns {number} A random number following a normal distribution. - */ - gaussian(mean = 0, stdev = 1) { - const u = 1 - rng(); - const v = rng(); - const z = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v ); - return z * stdev + mean; - }, - - /** - * Generates a random value based on weighted probabilities. - * @param {Object} weights - An object containing values as keys and their probabilities as values. - * @returns {string} A key randomly chosen based on its weight. - */ - weightedRand(e) { - let r, a, n = []; - for (r in e) - for (a = 0; a < 10 * e[r]; a++) - n.push(r); - return n[Math.floor(rng() * n.length)] - }, - - /** - * Remaps a number from one range to another. - * @param {number} value - The number to remap. - * @param {number} a - The lower bound of the value's current range. - * @param {number} b- The upper bound of the value's current range. - * @param {number} c - The lower bound of the value's target range. - * @param {number} d - The upper bound of the value's target range. - * @param {boolean} [withinBounds=false] - Whether to constrain the value to the target range. - * @returns {number} The remapped number. - */ - map(value, a, b, c, d, withinBounds = false) { - let r = c + (value - a) / (b - a) * (d - c); - if (!withinBounds) return r; - if (c < d) {return this.constrain(r, c, d)} - else {return this.constrain(r, d, c)} - }, - - /** - * Constrains a number to be within a range. - * @param {number} n - The number to constrain. - * @param {number} low - The lower bound of the range. - * @param {number} high - The upper bound of the range. - * @returns {number} The constrained number. - */ - constrain (n, low, high) { - return Math.max(Math.min(n, high), low); - }, - - /** - * Calculates the cosine for a given angle using precalculated values. - * @param {number} angle - The angle in degrees. - * @returns {number} The cosine of the angle. - */ - cos(angle) { - return this.c[Math.floor(4 * ((angle % 360 + 360) % 360))]; - }, - - /** - * Calculates the sine for a given angle using precalculated values. - * @param {number} angle - The angle in degrees. - * @returns {number} The sine of the angle. - */ - sin(angle) { - return this.s[Math.floor(4 * ((angle % 360 + 360) % 360))]; - }, - // Flag to indicate if the trigonometric tables have been precalculated - isPrecalculationDone: false, - - /** - * Precalculates trigonometric values for improved performance. - * This function should be called before any trigonometric calculations are performed. - */ - preCalculation() { - if (this.isPrecalculationDone) return; - const totalDegrees = 1440; - const radiansPerIndex = 2 * Math.PI / totalDegrees; - this.c = new Float64Array(totalDegrees); - this.s = new Float64Array(totalDegrees); - for (let i = 0; i < totalDegrees; i++) { - const radians = i * radiansPerIndex; - R.c[i] = Math.cos(radians); - R.s[i] = Math.sin(radians); - } - this.isPrecalculationDone = true; - }, +import { prng_alea } from "esm-seedrandom"; - /** - * Checks if value is numeric - */ - isNumber: (a) => !isNaN(a), +/** + * The basic source of randomness, can be seeded for determinism. + * @returns {number} A random number between 0 and 1. + */ +let rng = new prng_alea(Math.random()); +export function seed(s) { + rng = new prng_alea(s); +} - /** - * Changes angles to degrees and between 0-360 - */ - toDegrees: (a) => (((_r.angleMode() === "radians") ? a * 180 / Math.PI : a) % 360 + 360) % 360, +/** + * Object for random number generation and related utility functions. + * @property {function} source - Function that returns a random number from the base random generator. + * @property {function} random - Function to generate a random number within a specified range. + * @property {function} randInt - Function to generate a random integer within a specified range. + * @property {function} weightedRand - Function to generate a random value based on weighted probabilities. + * @property {function} map - Function to remap a number from one range to another. + * @property {function} constrain - Function to constrain a number within a range. + * @property {function} cos - Function to get the cosine of an angle from precalculated values. + * @property {function} sin - Function to get the sine of an angle from precalculated values. + * @property {boolean} isPrecalculationDone - Flag to check if precalculation of trigonometric values is complete. + * @property {function} preCalculation - Function to precalculate trigonometric values. + */ +const R = { + /** + * Generates a random number within a specified range. + * @param {number} [min=0] - The lower bound of the range. + * @param {number} [max=1] - The upper bound of the range. + * @returns {number} A random number within the specified range. + */ + random(e = 0, r = 1) { + return e + rng() * (r - e); + }, + + /** + * Generates a random integer within a specified range. + * @param {number} min - The lower bound of the range. + * @param {number} max - The upper bound of the range. + * @returns {number} A random integer within the specified range. + */ + randInt(e, r) { + return Math.floor(this.random(e, r)); + }, + + /** + * Generates a random gaussian. + * @param {number} mean - Mean. + * @param {number} stdev - Standard deviation. + * @returns {number} A random number following a normal distribution. + */ + gaussian(mean = 0, stdev = 1) { + const u = 1 - rng(); + const v = rng(); + const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); + return z * stdev + mean; + }, + + /** + * Generates a random value based on weighted probabilities. + * @param {Object} weights - An object containing values as keys and their probabilities as values. + * @returns {string} A key randomly chosen based on its weight. + */ + weightedRand(e) { + let r, + a, + n = []; + for (r in e) for (a = 0; a < 10 * e[r]; a++) n.push(r); + return n[Math.floor(rng() * n.length)]; + }, + + /** + * Remaps a number from one range to another. + * @param {number} value - The number to remap. + * @param {number} a - The lower bound of the value's current range. + * @param {number} b- The upper bound of the value's current range. + * @param {number} c - The lower bound of the value's target range. + * @param {number} d - The upper bound of the value's target range. + * @param {boolean} [withinBounds=false] - Whether to constrain the value to the target range. + * @returns {number} The remapped number. + */ + map(value, a, b, c, d, withinBounds = false) { + let r = c + ((value - a) / (b - a)) * (d - c); + if (!withinBounds) return r; + if (c < d) { + return this.constrain(r, c, d); + } else { + return this.constrain(r, d, c); + } + }, + + /** + * Constrains a number to be within a range. + * @param {number} n - The number to constrain. + * @param {number} low - The lower bound of the range. + * @param {number} high - The upper bound of the range. + * @returns {number} The constrained number. + */ + constrain(n, low, high) { + return Math.max(Math.min(n, high), low); + }, + + /** + * Calculates the cosine for a given angle using precalculated values. + * @param {number} angle - The angle in degrees. + * @returns {number} The cosine of the angle. + */ + cos(angle) { + return this.c[Math.floor(4 * (((angle % 360) + 360) % 360))]; + }, + + /** + * Calculates the sine for a given angle using precalculated values. + * @param {number} angle - The angle in degrees. + * @returns {number} The sine of the angle. + */ + sin(angle) { + return this.s[Math.floor(4 * (((angle % 360) + 360) % 360))]; + }, + // Flag to indicate if the trigonometric tables have been precalculated + isPrecalculationDone: false, + + /** + * Precalculates trigonometric values for improved performance. + * This function should be called before any trigonometric calculations are performed. + */ + preCalculation() { + if (this.isPrecalculationDone) return; + const totalDegrees = 1440; + const radiansPerIndex = (2 * Math.PI) / totalDegrees; + this.c = new Float64Array(totalDegrees); + this.s = new Float64Array(totalDegrees); + for (let i = 0; i < totalDegrees; i++) { + const radians = i * radiansPerIndex; + R.c[i] = Math.cos(radians); + R.s[i] = Math.sin(radians); + } + this.isPrecalculationDone = true; + }, + + /** + * Checks if value is numeric + */ + isNumber: (a) => !isNaN(a), + + /** + * Changes angles to degrees and between 0-360 + */ + toDegrees: (a) => + (((_r.angleMode() === "radians" ? (a * 180) / Math.PI : a) % 360) + 360) % + 360, + + /** + * Calculates distance between two 2D points + */ + dist: (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1), +}; +// Perform the precalculation of trigonometric values for the R object +R.preCalculation(); - /** - * Calculates distance between two 2D points - */ - dist: (x1,y1,x2,y2) => Math.hypot(x2-x1, y2-y1) - } - // Perform the precalculation of trigonometric values for the R object - R.preCalculation(); - - /** - * Calculates the intersection point between two line segments if it exists. - * - * @param {Object} seg1Start - The start point of the first line segment. - * @param {Object} seg1End - The end point of the first line segment. - * @param {Object} seg2Start - The start point of the second line segment. - * @param {Object} seg2End - The end point of the second line segment. - * @param {boolean} [includeSegmentExtension=false] - Whether to include points of intersection not lying on the segments. - * @returns {Object|boolean} The intersection point as an object with 'x' and 'y' properties, or 'false' if there is no intersection. - */ - function _intersectLines(seg1Start, seg1End, seg2Start, seg2End, includeSegmentExtension = false) { - // Extract coordinates from points - let x1 = seg1Start.x, y1 = seg1Start.y; - let x2 = seg1End.x, y2 = seg1End.y; - let x3 = seg2Start.x, y3 = seg2Start.y; - let x4 = seg2End.x, y4 = seg2End.y; - // Early return if line segments are points or if the lines are parallel - if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) { - return false; // Segments are points - } - let deltaX1 = x2 - x1, deltaY1 = y2 - y1; - let deltaX2 = x4 - x3, deltaY2 = y4 - y3; - let denominator = (deltaY2 * deltaX1 - deltaX2 * deltaY1); - if (denominator === 0) { - return false; // Lines are parallel - } - // Calculate the intersection point - let ua = (deltaX2 * (y1 - y3) - deltaY2 * (x1 - x3)) / denominator; - let ub = (deltaX1 * (y1 - y3) - deltaY1 * (x1 - x3)) / denominator; - // Check if the intersection is within the bounds of the line segments - if (!includeSegmentExtension && (ub < 0 || ub > 1)) { - return false; - } - // Calculate the intersection coordinates - let x = x1 + ua * deltaX1; - let y = y1 + ua * deltaY1; - return { x: x, y: y }; - } +/** + * Calculates the intersection point between two line segments if it exists. + * + * @param {Object} seg1Start - The start point of the first line segment. + * @param {Object} seg1End - The end point of the first line segment. + * @param {Object} seg2Start - The start point of the second line segment. + * @param {Object} seg2End - The end point of the second line segment. + * @param {boolean} [includeSegmentExtension=false] - Whether to include points of intersection not lying on the segments. + * @returns {Object|boolean} The intersection point as an object with 'x' and 'y' properties, or 'false' if there is no intersection. + */ +function _intersectLines( + seg1Start, + seg1End, + seg2Start, + seg2End, + includeSegmentExtension = false +) { + // Extract coordinates from points + let x1 = seg1Start.x, + y1 = seg1Start.y; + let x2 = seg1End.x, + y2 = seg1End.y; + let x3 = seg2Start.x, + y3 = seg2Start.y; + let x4 = seg2End.x, + y4 = seg2End.y; + // Early return if line segments are points or if the lines are parallel + if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) { + return false; // Segments are points + } + let deltaX1 = x2 - x1, + deltaY1 = y2 - y1; + let deltaX2 = x4 - x3, + deltaY2 = y4 - y3; + let denominator = deltaY2 * deltaX1 - deltaX2 * deltaY1; + if (denominator === 0) { + return false; // Lines are parallel + } + // Calculate the intersection point + let ua = (deltaX2 * (y1 - y3) - deltaY2 * (x1 - x3)) / denominator; + let ub = (deltaX1 * (y1 - y3) - deltaY1 * (x1 - x3)) / denominator; + // Check if the intersection is within the bounds of the line segments + if (!includeSegmentExtension && (ub < 0 || ub > 1)) { + return false; + } + // Calculate the intersection coordinates + let x = x1 + ua * deltaX1; + let y = y1 + ua * deltaY1; + return { x: x, y: y }; +} - /** - * Calculates the angle in degrees between two points in 2D space. - * The angle is measured in a clockwise direction from the positive X-axis. - * - * @param {number} x1 - The x-coordinate of the first point. - * @param {number} y1 - The y-coordinate of the first point. - * @param {number} x2 - The x-coordinate of the second point. - * @param {number} y2 - The y-coordinate of the second point. - * @returns {number} The angle in degrees between the two points. - */ - function _calculateAngle(x1,y1,x2,y2) { - // Calculate the angle based on the quadrant in which the second point lies - let angleRadians = Math.atan2(-(y2 - y1), (x2 - x1)); - // Convert radians to degrees and normalize the angle between 0 and 360 - let angleDegrees = angleRadians * (180 / Math.PI); - return (angleDegrees % 360 + 360) % 360; - } +/** + * Calculates the angle in degrees between two points in 2D space. + * The angle is measured in a clockwise direction from the positive X-axis. + * + * @param {number} x1 - The x-coordinate of the first point. + * @param {number} y1 - The y-coordinate of the first point. + * @param {number} x2 - The x-coordinate of the second point. + * @param {number} y2 - The y-coordinate of the second point. + * @returns {number} The angle in degrees between the two points. + */ +function _calculateAngle(x1, y1, x2, y2) { + // Calculate the angle based on the quadrant in which the second point lies + let angleRadians = Math.atan2(-(y2 - y1), x2 - x1); + // Convert radians to degrees and normalize the angle between 0 and 360 + let angleDegrees = angleRadians * (180 / Math.PI); + return ((angleDegrees % 360) + 360) % 360; +} - /** - * Object that saves the current p5.brush state for push and pop operations - */ - const _saveState = { - field: {}, - stroke: {}, - hatch: {}, - fill: {}, - others: {}, - } +/** + * Object that saves the current p5.brush state for push and pop operations + */ +const _saveState = { + field: {}, + stroke: {}, + hatch: {}, + fill: {}, + others: {}, +}; - /** - * Saves current state to object - */ - export function push () { - // Field - _saveState.field.isActive = FF.isActive; - _saveState.field.current = FF.current; - - // Stroke - _saveState.stroke.isActive = B.isActive; - _saveState.stroke.name = B.name; - _saveState.stroke.color = B.c; - _saveState.stroke.weight = B.w; - _saveState.stroke.clip = B.cr; - - // Hatch - _saveState.hatch.isActive = H.isActive; - _saveState.hatch.hatchingParams = H.hatchingParams; - _saveState.hatch.hatchingBrush = H.hatchingBrush; - - // Fill - _saveState.fill.isActive = F.isActive; - _saveState.fill.color = F.color; - _saveState.fill.opacity = F.opacity; - _saveState.fill.bleed_strength = F.bleed_strength; - _saveState.fill.texture_strength = F.texture_strength; - _saveState.fill.border_strength = F.border_strength; - - // Rotate - _saveState.others.rotate = Matrix.rotation; - } +/** + * Saves current state to object + */ +export function push() { + // Field + _saveState.field.isActive = FF.isActive; + _saveState.field.current = FF.current; + + // Stroke + _saveState.stroke.isActive = B.isActive; + _saveState.stroke.name = B.name; + _saveState.stroke.color = B.c; + _saveState.stroke.weight = B.w; + _saveState.stroke.clip = B.cr; + + // Hatch + _saveState.hatch.isActive = H.isActive; + _saveState.hatch.hatchingParams = H.hatchingParams; + _saveState.hatch.hatchingBrush = H.hatchingBrush; + + // Fill + _saveState.fill.isActive = F.isActive; + _saveState.fill.color = F.color; + _saveState.fill.opacity = F.opacity; + _saveState.fill.bleed_strength = F.bleed_strength; + _saveState.fill.texture_strength = F.texture_strength; + _saveState.fill.border_strength = F.border_strength; + + // Rotate + _saveState.others.rotate = Matrix.rotation; +} - /** - * Restores previous state from object - */ - export function pop() { - // Field - FF.isActive = _saveState.field.isActive; - FF.current = _saveState.field.current; - - // Stroke - B.isActive = _saveState.stroke.isActive; - B.name = _saveState.stroke.name; - B.c = _saveState.stroke.color; - B.w = _saveState.stroke.weight; - B.cr = _saveState.stroke.clip; - - // Hatch - H.isActive = _saveState.hatch.isActive; - H.hatchingParams = _saveState.hatch.hatchingParams; - H.hatchingBrush = _saveState.hatch.hatchingBrush; - - // Fill - F.isActive = _saveState.fill.isActive; - F.color = _saveState.fill.color; - F.opacity = _saveState.fill.opacity; - F.bleed_strength = _saveState.fill.bleed_strength; - F.texture_strength = _saveState.fill.texture_strength; - F.border_strength = _saveState.fill.border_strength; - - // Rotate - Matrix.rotation = _saveState.others.rotate; - } +/** + * Restores previous state from object + */ +export function pop() { + // Field + FF.isActive = _saveState.field.isActive; + FF.current = _saveState.field.current; + + // Stroke + B.isActive = _saveState.stroke.isActive; + B.name = _saveState.stroke.name; + B.c = _saveState.stroke.color; + B.w = _saveState.stroke.weight; + B.cr = _saveState.stroke.clip; + + // Hatch + H.isActive = _saveState.hatch.isActive; + H.hatchingParams = _saveState.hatch.hatchingParams; + H.hatchingBrush = _saveState.hatch.hatchingBrush; + + // Fill + F.isActive = _saveState.fill.isActive; + F.color = _saveState.fill.color; + F.opacity = _saveState.fill.opacity; + F.bleed_strength = _saveState.fill.bleed_strength; + F.texture_strength = _saveState.fill.texture_strength; + F.border_strength = _saveState.fill.border_strength; + + // Rotate + Matrix.rotation = _saveState.others.rotate; +} - /** - * Object to perform matrix translation and rotation operations - */ - const Matrix = { - translation: [0, 0], - rotation: 0, - /** - * Captures the current translation values from the renderer's transformation matrix. - * - * Assumes that the renderer's transformation matrix (`uModelMatrix`) is a 4x4 matrix - * where the translation components are in the 13th (index 12) and 14th (index 13) positions. - * - * @returns {number[]} An array containing the x (horizontal) and y (vertical) translation values. - */ - trans () { - // Access the renderer's current model-view matrix and extract the translation components - this.translation = [_r._renderer.uModelMatrix.mat4[12],_r._renderer.uModelMatrix.mat4[13]]; - // Return the translation components as a two-element array - return this.translation - } - } +/** + * Object to perform matrix translation and rotation operations + */ +const Matrix = { + translation: [0, 0], + rotation: 0, + /** + * Captures the current translation values from the renderer's transformation matrix. + * + * Assumes that the renderer's transformation matrix (`uModelMatrix`) is a 4x4 matrix + * where the translation components are in the 13th (index 12) and 14th (index 13) positions. + * + * @returns {number[]} An array containing the x (horizontal) and y (vertical) translation values. + */ + trans() { + // Access the renderer's current model-view matrix and extract the translation components + this.translation = [ + _r._renderer.uModelMatrix.mat4[12], + _r._renderer.uModelMatrix.mat4[13], + ]; + // Return the translation components as a two-element array + return this.translation; + }, +}; - /** - * Captures the desired rotation. - */ - export function rotate (a = 0) { - Matrix.rotation = R.toDegrees(a) - } +/** + * Captures the desired rotation. + */ +export function rotate(a = 0) { + Matrix.rotation = R.toDegrees(a); +} - /** - * Object to perform scale operations - */ - let _curScale = 1; - export function scale (a) { - _curScale *= a - } - +/** + * Object to perform scale operations + */ +let _curScale = 1; +export function scale(a) { + _curScale *= a; +} // ============================================================================= // Section: Color Blending - Uses spectral.js as a module // ============================================================================= /** - * The Mix object is responsible for handling color blending operations within - * the rendering context. It utilizes WebGL shaders to apply advanced blending - * effects based on Kubelka-Munk theory. It depends on spectral.js for the + * The Mix object is responsible for handling color blending operations within + * the rendering context. It utilizes WebGL shaders to apply advanced blending + * effects based on Kubelka-Munk theory. It depends on spectral.js for the * blending logic incorporated into its fragment shader. */ - /** - * Enables/Disables color caching for WebGL Shaders. - * Color caching increases performance but might produce worse textures - * when using the same colour repeteadly. - * @param {bool} bool - * - */ - export function colorCache(bool = true) { - Mix.isCaching = bool; - } - - /** - * Object handling blending operations with WebGL shaders. - * @property {boolean} loaded - Flag indicating if the blend shaders have been loaded. - * @property {boolean} isBlending - Flag indicating if the blending has been initiated. - * @property {boolean} isCaching - Flag indicating if the color caching is active. - * @property {Float32Array} currentColor - Typed array to hold color values for shaders. - * @property {function} load - Loads resources and initializes blend operations. - * @property {function} blend - Applies blending effects using the initialized shader. - * @property {string} vert - Vertex shader source code. - * @property {string} frag - Fragment shader source code with blending logic. - */ - const Mix = { - loaded: false, - isBlending: false, - isCaching: true, - currentColor: new Float32Array(3), - - /** - * Loads necessary resources and prepares the mask buffer and shader for colour blending. - */ - load(inst) { - this.type = (_isInstanced && !inst) ? 0 : (!inst ? 1 : 2) - this.masks = [] - // Create a buffer to be used as a mask for blending - // WEBGL buffer for img brushes (image() is much quicker like this) - // Create a buffer for noBlend brushes - for (let i = 0; i < 3; i++) { - switch(this.type) { - case 0: - this.masks[i] = _r.createGraphics(_r.width,_r.height, i == 1 ? _r.WEBGL : _r.P2D) - break; - case 1: - this.masks[i] = createGraphics(_r.width,_r.height, i == 1 ? WEBGL : P2D) - break; - case 2: - this.masks[i] = inst.createGraphics(inst.width, inst.height, i == 1 ? inst.WEBGL : inst.P2D) - break; - } - } - - for (let mask of this.masks) { - mask.pixelDensity(_r.pixelDensity()); - mask.clear(); - mask.angleMode(_r.DEGREES); - mask.noSmooth(); - } - - // Create the shader program from the vertex and fragment shaders - this.shader = _r.createShader(this.vert, this.frag); - Mix.loaded = true; - }, - - /** - * Converts a color object with RGB levels to a Float32Array representation. - * The RGB levels are normalized to a range of 0.0 to 1.0. - * @param {object} _color - A p5 color object representing a color, containing an 'levels' property. - * @returns {Float32Array} A Float32Array with three elements, each representing the normalized levels of red, green, and blue. - */ - getPigment(_color) { - let currentLevels = _color.levels; - let colorArray = new Float32Array(3); - colorArray[0] = currentLevels[0] / 255.0; - colorArray[1] = currentLevels[1] / 255.0; - colorArray[2] = currentLevels[2] / 255.0; - return colorArray; - }, - - /** - * There are two parallel blender instances: one uses the 2D mask, the other the WEBGL mask - * 2D Canvas API mask: for basi geometry (circles, polygons, etc), which is much faster with the 2D API - * WEBGL mask: for image-type brushes. p5 image() is much faster in WEBGL mode - */ - color1: new Float32Array(3), - color2: new Float32Array(3), - blending1: false, - blending2: false, - - /** - * Applies the blend shader to the current rendering context. - * @param {string} _c - The color used for blending, as a p5.Color object. - * @param {boolean} _isLast - Indicates if this is the last blend after setup and draw. - * @param {boolean} _isLast - Indicates if this is the last blend after setup and draw. - */ - blend (_color = false, _isLast = false, webgl_mask = false) { - - _ensureReady(); - - // Select between the two options: - this.isBlending = webgl_mask ? this.blending1 : this.blending2; - this.currentColor = webgl_mask ? this.color1 : this.color2; - - // Check if blending is initialised - if(!this.isBlending) { - // If color has been provided, we initialise blending - if (_color) { - this.currentColor = this.getPigment(_color); - if (webgl_mask) this.blending1 = true, this.color1 = this.currentColor; - else this.blending2 = true, this.color2 = this.currentColor; - } else if (_isLast) { - if (!webgl_mask) reDraw() - return - } - } - - // Checks if newColor is the same than the cadhedColor - // If it is the same, we wait before applying the shader for color mixing - // If it's NOT the same, we apply the shader and cache the new color - let newColor = !_color ? this.currentColor : this.getPigment(_color); - - if (newColor.toString() !== this.currentColor.toString() || _isLast || !this.isCaching) { - // Paste info from noBlend buffer - reDraw() - - if (this.isBlending) { - _r.push(); - _r.translate(-Matrix.trans()[0],-Matrix.trans()[1]) - // Use the blend shader for rendering - _r.shader(this.shader); - // Set shader uniforms - // Color to blend - this.shader.setUniform('addColor', this.currentColor); - // Source canvas - this.shader.setUniform('source', _r._renderer); - // Bool to active watercolor blender vs marker blenderd - this.shader.setUniform('active', Mix.watercolor); - // Random values for watercolor blender - this.shader.setUniform('random', [R.random(),R.random(),R.random()]); - // We select and apply the correct mask here - let mask = webgl_mask ? this.masks[1] : this.masks[0]; - this.shader.setUniform('mask', mask); - // Draw a rectangle covering the whole canvas to apply the shader - _r.fill(0,0,0,0); - _r.noStroke(); - _r.rect(-_r.width/2, -_r.height/2, _r.width, _r.height); - _r.pop(); - // Clear the mask after drawing - mask.clear() - } - // We cache the new color here - if (!_isLast) { - this.currentColor = this.getPigment(_color); - if (webgl_mask) this.color1 = this.currentColor; - else this.color2 = this.currentColor; - } - } - - if (_isLast) { - this.isBlending = false; - if (webgl_mask) this.blending1 = this.isBlending - else this.blending2 = this.isBlending - } - }, +/** + * Enables/Disables color caching for WebGL Shaders. + * Color caching increases performance but might produce worse textures + * when using the same colour repeteadly. + * @param {bool} bool + * + */ +export function colorCache(bool = true) { + Mix.isCaching = bool; +} - // Vertex shader source code - vert: `precision highp float;attribute vec3 aPosition;attribute vec2 aTexCoord;uniform mat4 uModelViewMatrix,uProjectionMatrix;varying vec2 vVertTexCoord;void main(){gl_Position=uProjectionMatrix*uModelViewMatrix*vec4(aPosition,1);vVertTexCoord=aTexCoord;}`, - - // Fragment shader source code with blending operations - // For unminified shader see shader_unminified.frag - frag: `precision highp float;varying vec2 vVertTexCoord;uniform sampler2D source,mask;uniform vec4 addColor;uniform vec3 random;uniform bool active; +/** + * Object handling blending operations with WebGL shaders. + * @property {boolean} loaded - Flag indicating if the blend shaders have been loaded. + * @property {boolean} isBlending - Flag indicating if the blending has been initiated. + * @property {boolean} isCaching - Flag indicating if the color caching is active. + * @property {Float32Array} currentColor - Typed array to hold color values for shaders. + * @property {function} load - Loads resources and initializes blend operations. + * @property {function} blend - Applies blending effects using the initialized shader. + * @property {string} vert - Vertex shader source code. + * @property {string} frag - Fragment shader source code with blending logic. + */ +const Mix = { + loaded: false, + isBlending: false, + isCaching: true, + currentColor: new Float32Array(3), + + /** + * Loads necessary resources and prepares the mask buffer and shader for colour blending. + */ + load(inst) { + this.type = _isInstanced && !inst ? 0 : !inst ? 1 : 2; + this.masks = []; + // Create a buffer to be used as a mask for blending + // WEBGL buffer for img brushes (image() is much quicker like this) + // Create a buffer for noBlend brushes + for (let i = 0; i < 3; i++) { + switch (this.type) { + case 0: + this.masks[i] = _r.createGraphics( + _r.width, + _r.height, + i == 1 ? _r.WEBGL : _r.P2D + ); + break; + case 1: + this.masks[i] = createGraphics( + _r.width, + _r.height, + i == 1 ? WEBGL : P2D + ); + break; + case 2: + this.masks[i] = inst.createGraphics( + inst.width, + inst.height, + i == 1 ? inst.WEBGL : inst.P2D + ); + break; + } + } + + for (let mask of this.masks) { + mask.pixelDensity(_r.pixelDensity()); + mask.clear(); + mask.angleMode(_r.DEGREES); + mask.noSmooth(); + } + + // Create the shader program from the vertex and fragment shaders + this.shader = _r.createShader(this.vert, this.frag); + Mix.loaded = true; + }, + + /** + * Converts a color object with RGB levels to a Float32Array representation. + * The RGB levels are normalized to a range of 0.0 to 1.0. + * @param {object} _color - A p5 color object representing a color, containing an 'levels' property. + * @returns {Float32Array} A Float32Array with three elements, each representing the normalized levels of red, green, and blue. + */ + getPigment(_color) { + let currentLevels = _color.levels; + let colorArray = new Float32Array(3); + colorArray[0] = currentLevels[0] / 255.0; + colorArray[1] = currentLevels[1] / 255.0; + colorArray[2] = currentLevels[2] / 255.0; + return colorArray; + }, + + /** + * There are two parallel blender instances: one uses the 2D mask, the other the WEBGL mask + * 2D Canvas API mask: for basi geometry (circles, polygons, etc), which is much faster with the 2D API + * WEBGL mask: for image-type brushes. p5 image() is much faster in WEBGL mode + */ + color1: new Float32Array(3), + color2: new Float32Array(3), + blending1: false, + blending2: false, + + /** + * Applies the blend shader to the current rendering context. + * @param {string} _c - The color used for blending, as a p5.Color object. + * @param {boolean} _isLast - Indicates if this is the last blend after setup and draw. + * @param {boolean} _isLast - Indicates if this is the last blend after setup and draw. + */ + blend(_color = false, _isLast = false, webgl_mask = false) { + _ensureReady(); + + // Select between the two options: + this.isBlending = webgl_mask ? this.blending1 : this.blending2; + this.currentColor = webgl_mask ? this.color1 : this.color2; + + // Check if blending is initialised + if (!this.isBlending) { + // If color has been provided, we initialise blending + if (_color) { + this.currentColor = this.getPigment(_color); + if (webgl_mask) + (this.blending1 = true), (this.color1 = this.currentColor); + else (this.blending2 = true), (this.color2 = this.currentColor); + } else if (_isLast) { + if (!webgl_mask) reDraw(); + return; + } + } + + // Checks if newColor is the same than the cadhedColor + // If it is the same, we wait before applying the shader for color mixing + // If it's NOT the same, we apply the shader and cache the new color + let newColor = !_color ? this.currentColor : this.getPigment(_color); + + if ( + newColor.toString() !== this.currentColor.toString() || + _isLast || + !this.isCaching + ) { + // Paste info from noBlend buffer + reDraw(); + + if (this.isBlending) { + _r.push(); + _r.translate(-Matrix.trans()[0], -Matrix.trans()[1]); + // Use the blend shader for rendering + _r.shader(this.shader); + // Set shader uniforms + // Color to blend + this.shader.setUniform("addColor", this.currentColor); + // Source canvas + this.shader.setUniform("source", _r._renderer); + // Bool to active watercolor blender vs marker blenderd + this.shader.setUniform("active", Mix.watercolor); + // Random values for watercolor blender + this.shader.setUniform("random", [R.random(), R.random(), R.random()]); + // We select and apply the correct mask here + let mask = webgl_mask ? this.masks[1] : this.masks[0]; + this.shader.setUniform("mask", mask); + // Draw a rectangle covering the whole canvas to apply the shader + _r.fill(0, 0, 0, 0); + _r.noStroke(); + _r.rect(-_r.width / 2, -_r.height / 2, _r.width, _r.height); + _r.pop(); + // Clear the mask after drawing + mask.clear(); + } + // We cache the new color here + if (!_isLast) { + this.currentColor = this.getPigment(_color); + if (webgl_mask) this.color1 = this.currentColor; + else this.color2 = this.currentColor; + } + } + + if (_isLast) { + this.isBlending = false; + if (webgl_mask) this.blending1 = this.isBlending; + else this.blending2 = this.isBlending; + } + }, + + // Vertex shader source code + vert: `precision highp float;attribute vec3 aPosition;attribute vec2 aTexCoord;uniform mat4 uModelViewMatrix,uProjectionMatrix;varying vec2 vVertTexCoord;void main(){gl_Position=uProjectionMatrix*uModelViewMatrix*vec4(aPosition,1);vVertTexCoord=aTexCoord;}`, + + // Fragment shader source code with blending operations + // For unminified shader see shader_unminified.frag + frag: `precision highp float;varying vec2 vVertTexCoord;uniform sampler2D source,mask;uniform vec4 addColor;uniform vec3 random;uniform bool active; #ifndef SPECTRAL #define SPECTRAL float x(float v){return v<.04045?v/12.92:pow((v+.055)/1.055,2.4);}float v(float v){return v<.0031308?v*12.92:1.055*pow(v,1./2.4)-.055;}vec3 m(vec3 v){return vec3(x(v[0]),x(v[1]),x(v[2]));}vec3 f(vec3 f){return clamp(vec3(v(f[0]),v(f[1]),v(f[2])),0.,1.);}void f(vec3 v,out float m,out float f,out float x,out float y,out float z,out float i,out float r){m=min(v.x,min(v.y,v.z));v-=m;f=min(v.y,v.z);x=min(v.x,v.z);y=min(v.x,v.y);z=min(max(0.,v.x-v.z),max(0.,v.x-v.y));i=min(max(0.,v.y-v.z),max(0.,v.y-v.x));r=min(max(0.,v.z-v.y),max(0.,v.z-v.x));}void f(vec3 v,inout float i[38]){float x,y,d,z,o,m,e;f(v,x,y,d,z,o,m,e);i[0]=max(1e-4,x+y*.96853629+d*.51567122+z*.02055257+o*.03147571+m*.49108579+e*.97901834);i[1]=max(1e-4,x+y*.96855103+d*.5401552+z*.02059936+o*.03146636+m*.46944057+e*.97901649);i[2]=max(1e-4,x+y*.96859338+d*.62645502+z*.02062723+o*.03140624+m*.4016578+e*.97901118);i[3]=max(1e-4,x+y*.96877345+d*.75595012+z*.02073387+o*.03119611+m*.2449042+e*.97892146);i[4]=max(1e-4,x+y*.96942204+d*.92826996+z*.02114202+o*.03053888+m*.0682688+e*.97858555);i[5]=max(1e-4,x+y*.97143709+d*.97223624+z*.02233154+o*.02856855+m*.02732883+e*.97743705);i[6]=max(1e-4,x+y*.97541862+d*.98616174+z*.02556857+o*.02459485+m*.013606+e*.97428075);i[7]=max(1e-4,x+y*.98074186+d*.98955255+z*.03330189+o*.0192952+m*.01000187+e*.96663223);i[8]=max(1e-4,x+y*.98580992+d*.98676237+z*.05185294+o*.01423112+m*.01284127+e*.94822893);i[9]=max(1e-4,x+y*.98971194+d*.97312575+z*.10087639+o*.01033111+m*.02636635+e*.89937713);i[10]=max(1e-4,x+y*.99238027+d*.91944277+z*.24000413+o*.00765876+m*.07058713+e*.76070164);i[11]=max(1e-4,x+y*.99409844+d*.32564851+z*.53589066+o*.00593693+m*.70421692+e*.4642044);i[12]=max(1e-4,x+y*.995172+d*.13820628+z*.79874659+o*.00485616+m*.85473994+e*.20123039);i[13]=max(1e-4,x+y*.99576545+d*.05015143+z*.91186529+o*.00426186+m*.95081565+e*.08808402);i[14]=max(1e-4,x+y*.99593552+d*.02912336+z*.95399623+o*.00409039+m*.9717037+e*.04592894);i[15]=max(1e-4,x+y*.99564041+d*.02421691+z*.97137099+o*.00438375+m*.97651888+e*.02860373);i[16]=max(1e-4,x+y*.99464769+d*.02660696+z*.97939505+o*.00537525+m*.97429245+e*.02060067);i[17]=max(1e-4,x+y*.99229579+d*.03407586+z*.98345207+o*.00772962+m*.97012917+e*.01656701);i[18]=max(1e-4,x+y*.98638762+d*.04835936+z*.98553736+o*.0136612+m*.9425863+e*.01451549);i[19]=max(1e-4,x+y*.96829712+d*.0001172+z*.98648905+o*.03181352+m*.99989207+e*.01357964);i[20]=max(1e-4,x+y*.89228016+d*8.554e-5+z*.98674535+o*.10791525+m*.99989891+e*.01331243);i[21]=max(1e-4,x+y*.53740239+d*.85267882+z*.98657555+o*.46249516+m*.13823139+e*.01347661);i[22]=max(1e-4,x+y*.15360445+d*.93188793+z*.98611877+o*.84604333+m*.06968113+e*.01387181);i[23]=max(1e-4,x+y*.05705719+d*.94810268+z*.98559942+o*.94275572+m*.05628787+e*.01435472);i[24]=max(1e-4,x+y*.03126539+d*.94200977+z*.98507063+o*.96860996+m*.06111561+e*.01479836);i[25]=max(1e-4,x+y*.02205445+d*.91478045+z*.98460039+o*.97783966+m*.08987709+e*.0151525);i[26]=max(1e-4,x+y*.01802271+d*.87065445+z*.98425301+o*.98187757+m*.13656016+e*.01540513);i[27]=max(1e-4,x+y*.0161346+d*.78827548+z*.98403909+o*.98377315+m*.22169624+e*.01557233);i[28]=max(1e-4,x+y*.01520947+d*.65738359+z*.98388535+o*.98470202+m*.32176956+e*.0156571);i[29]=max(1e-4,x+y*.01475977+d*.59909403+z*.98376116+o*.98515481+m*.36157329+e*.01571025);i[30]=max(1e-4,x+y*.01454263+d*.56817268+z*.98368246+o*.98537114+m*.4836192+e*.01571916);i[31]=max(1e-4,x+y*.01444459+d*.54031997+z*.98365023+o*.98546685+m*.46488579+e*.01572133);i[32]=max(1e-4,x+y*.01439897+d*.52110241+z*.98361309+o*.98550011+m*.47440306+e*.01572502);i[33]=max(1e-4,x+y*.0143762+d*.51041094+z*.98357259+o*.98551031+m*.4857699+e*.01571717);i[34]=max(1e-4,x+y*.01436343+d*.50526577+z*.98353856+o*.98550741+m*.49267971+e*.01571905);i[35]=max(1e-4,x+y*.01435687+d*.5025508+z*.98351247+o*.98551323+m*.49625685+e*.01571059);i[36]=max(1e-4,x+y*.0143537+d*.50126452+z*.98350101+o*.98551563+m*.49807754+e*.01569728);i[37]=max(1e-4,x+y*.01435408+d*.50083021+z*.98350852+o*.98551547+m*.49889859+e*.0157002);}vec3 t(vec3 x){mat3 i;i[0]=vec3(3.24306333,-1.53837619,-.49893282);i[1]=vec3(-.96896309,1.87542451,.04154303);i[2]=vec3(.05568392,-.20417438,1.05799454);float v=dot(i[0],x),y=dot(i[1],x),o=dot(i[2],x);return f(vec3(v,y,o));}vec3 d(float m[38]){vec3 i=vec3(0);i+=m[0]*vec3(6.469e-5,1.84e-6,.00030502);i+=m[1]*vec3(.00021941,6.21e-6,.00103681);i+=m[2]*vec3(.00112057,3.101e-5,.00531314);i+=m[3]*vec3(.00376661,.00010475,.01795439);i+=m[4]*vec3(.01188055,.00035364,.05707758);i+=m[5]*vec3(.02328644,.00095147,.11365162);i+=m[6]*vec3(.03455942,.00228226,.17335873);i+=m[7]*vec3(.03722379,.00420733,.19620658);i+=m[8]*vec3(.03241838,.0066888,.18608237);i+=m[9]*vec3(.02123321,.0098884,.13995048);i+=m[10]*vec3(.01049099,.01524945,.08917453);i+=m[11]*vec3(.00329584,.02141831,.04789621);i+=m[12]*vec3(.00050704,.03342293,.02814563);i+=m[13]*vec3(.00094867,.05131001,.01613766);i+=m[14]*vec3(.00627372,.07040208,.0077591);i+=m[15]*vec3(.01686462,.08783871,.00429615);i+=m[16]*vec3(.02868965,.09424905,.00200551);i+=m[17]*vec3(.04267481,.09795667,.00086147);i+=m[18]*vec3(.05625475,.09415219,.00036904);i+=m[19]*vec3(.0694704,.08678102,.00019143);i+=m[20]*vec3(.08305315,.07885653,.00014956);i+=m[21]*vec3(.0861261,.0635267,9.231e-5);i+=m[22]*vec3(.09046614,.05374142,6.813e-5);i+=m[23]*vec3(.08500387,.04264606,2.883e-5);i+=m[24]*vec3(.07090667,.03161735,1.577e-5);i+=m[25]*vec3(.05062889,.02088521,3.94e-6);i+=m[26]*vec3(.03547396,.01386011,1.58e-6);i+=m[27]*vec3(.02146821,.00810264,0);i+=m[28]*vec3(.01251646,.0046301,0);i+=m[29]*vec3(.00680458,.00249138,0);i+=m[30]*vec3(.00346457,.0012593,0);i+=m[31]*vec3(.00149761,.00054165,0);i+=m[32]*vec3(.0007697,.00027795,0);i+=m[33]*vec3(.00040737,.00014711,0);i+=m[34]*vec3(.00016901,6.103e-5,0);i+=m[35]*vec3(9.522e-5,3.439e-5,0);i+=m[36]*vec3(4.903e-5,1.771e-5,0);i+=m[37]*vec3(2e-5,7.22e-6,0);return i;}float d(float y,float m,float v){float z=m*pow(v,2.);return z/(y*pow(1.-v,2.)+z);}vec3 f(vec3 v,vec3 y,float z){vec3 x=m(v),o=m(y);float i[38],a[38];f(x,i);f(o,a);float r=d(i)[1],e=d(a)[1];z=d(r,e,z);float s[38];for(int u=0;u<38;u++){float p=(1.-z)*(pow(1.-i[u],2.)/(2.*i[u]))+z*(pow(1.-a[u],2.)/(2.*a[u]));s[u]=1.+p-sqrt(pow(p,2.)+2.*p);}return t(d(s));}vec4 f(vec4 v,vec4 x,float y){return vec4(f(v.xyz,x.xyz,y),mix(v.w,x.w,y));} #endif - float d(vec2 m,vec2 v,float y,out vec2 i){vec2 f=vec2(m.x+m.y*.5,m.y),x=floor(f),o=fract(f);float z=step(o.y,o.x);vec2 d=vec2(z,1.-z),r=x+d,e=x+1.,a=vec2(x.x-x.y*.5,x.y),p=vec2(a.x+d.x-d.y*.5,a.y+d.y),s=vec2(a.x+.5,a.y+1.),w=m-a,g=m-p,k=m-s;vec3 u,c,t,A;if(any(greaterThan(v,vec2(0)))){t=vec3(a.x,p.x,s);A=vec3(a.y,p.y,s.y);if(v.x>0.)t=mod(vec3(a.x,p.x,s),v.x);if(v.y>0.)A=mod(vec3(a.y,p.y,s.y),v.y);u=floor(t+.5*A+.5);c=floor(A+.5);}else u=vec3(x.x,r.x,e),c=vec3(x.y,r.y,e.y);vec3 S=mod(u,289.);S=mod((S*51.+2.)*S+c,289.);S=mod((S*34.+10.)*S,289.);vec3 b=S*.07482+y,C=cos(b),D=sin(b);vec2 h=vec2(C.x,D),B=vec2(C.y,D.y),E=vec2(C.z,D.z);vec3 F=.8-vec3(dot(w,w),dot(g,g),dot(k,k));F=max(F,0.);vec3 G=F*F,H=G*G,I=vec3(dot(h,w),dot(B,g),dot(E,k)),J=G*F,K=-8.*J*I;i=10.9*(H.x*h+K.x*w+(H.y*B+K.y*g)+(H.z*E+K.z*k));return 10.9*dot(H,I);}vec4 d(vec3 v,float x){return vec4(mix(v,vec3(dot(vec3(.299,.587,.114),v)),x),1);}float f(vec2 v,float x,float y,float f){return fract(sin(dot(v,vec2(x,y)))*f);}void main(){vec4 v=texture2D(mask,vVertTexCoord);if(v.x>0.){vec2 x=vec2(12.9898,78.233),o=vec2(7.9898,58.233),m=vec2(17.9898,3.233);float y=f(vVertTexCoord,x.x,x.y,43358.5453)*2.-1.,z=f(vVertTexCoord,o.x,o.y,43213.5453)*2.-1.,e=f(vVertTexCoord,m.x,m.y,33358.5453)*2.-1.;const vec2 i=vec2(0);vec2 s;vec4 r;if(active){float a=d(vVertTexCoord*5.,i,10.*random.x,s),p=d(vVertTexCoord*5.,i,10.*random.y,s),g=d(vVertTexCoord*5.,i,10.*random.z,s),k=.25+.25*d(vVertTexCoord*4.,i,3.*random.x,s);r=vec4(d(addColor.xyz,k).xyz+vec3(a,p,g)*.03*abs(addColor.x-addColor.y-addColor.z),1);}else r=vec4(addColor.xyz,1);if(v.w>.7){float a=.5*(v.w-.7);r=r*(1.-a)-vec4(.5)*a;}vec3 a=f(texture2D(source,vVertTexCoord).xyz,r.xyz,.9*v.w);gl_FragColor=vec4(a+.01*vec3(y,z,e),1);}}` - } + float d(vec2 m,vec2 v,float y,out vec2 i){vec2 f=vec2(m.x+m.y*.5,m.y),x=floor(f),o=fract(f);float z=step(o.y,o.x);vec2 d=vec2(z,1.-z),r=x+d,e=x+1.,a=vec2(x.x-x.y*.5,x.y),p=vec2(a.x+d.x-d.y*.5,a.y+d.y),s=vec2(a.x+.5,a.y+1.),w=m-a,g=m-p,k=m-s;vec3 u,c,t,A;if(any(greaterThan(v,vec2(0)))){t=vec3(a.x,p.x,s);A=vec3(a.y,p.y,s.y);if(v.x>0.)t=mod(vec3(a.x,p.x,s),v.x);if(v.y>0.)A=mod(vec3(a.y,p.y,s.y),v.y);u=floor(t+.5*A+.5);c=floor(A+.5);}else u=vec3(x.x,r.x,e),c=vec3(x.y,r.y,e.y);vec3 S=mod(u,289.);S=mod((S*51.+2.)*S+c,289.);S=mod((S*34.+10.)*S,289.);vec3 b=S*.07482+y,C=cos(b),D=sin(b);vec2 h=vec2(C.x,D),B=vec2(C.y,D.y),E=vec2(C.z,D.z);vec3 F=.8-vec3(dot(w,w),dot(g,g),dot(k,k));F=max(F,0.);vec3 G=F*F,H=G*G,I=vec3(dot(h,w),dot(B,g),dot(E,k)),J=G*F,K=-8.*J*I;i=10.9*(H.x*h+K.x*w+(H.y*B+K.y*g)+(H.z*E+K.z*k));return 10.9*dot(H,I);}vec4 d(vec3 v,float x){return vec4(mix(v,vec3(dot(vec3(.299,.587,.114),v)),x),1);}float f(vec2 v,float x,float y,float f){return fract(sin(dot(v,vec2(x,y)))*f);}void main(){vec4 v=texture2D(mask,vVertTexCoord);if(v.x>0.){vec2 x=vec2(12.9898,78.233),o=vec2(7.9898,58.233),m=vec2(17.9898,3.233);float y=f(vVertTexCoord,x.x,x.y,43358.5453)*2.-1.,z=f(vVertTexCoord,o.x,o.y,43213.5453)*2.-1.,e=f(vVertTexCoord,m.x,m.y,33358.5453)*2.-1.;const vec2 i=vec2(0);vec2 s;vec4 r;if(active){float a=d(vVertTexCoord*5.,i,10.*random.x,s),p=d(vVertTexCoord*5.,i,10.*random.y,s),g=d(vVertTexCoord*5.,i,10.*random.z,s),k=.25+.25*d(vVertTexCoord*4.,i,3.*random.x,s);r=vec4(d(addColor.xyz,k).xyz+vec3(a,p,g)*.03*abs(addColor.x-addColor.y-addColor.z),1);}else r=vec4(addColor.xyz,1);if(v.w>.7){float a=.5*(v.w-.7);r=r*(1.-a)-vec4(.5)*a;}vec3 a=f(texture2D(source,vVertTexCoord).xyz,r.xyz,.9*v.w);gl_FragColor=vec4(a+.01*vec3(y,z,e),1);}}`, +}; - /** - * This function forces standard-brushes to be updated into the canvas - */ - export function reDraw() { - _r.push(); - _r.translate(-Matrix.trans()[0],-Matrix.trans()[1]) - _r.image(Mix.masks[2], -_r.width/2, -_r.height/2) - Mix.masks[2].clear() - _r.pop(); - } +/** + * This function forces standard-brushes to be updated into the canvas + */ +export function reDraw() { + _r.push(); + _r.translate(-Matrix.trans()[0], -Matrix.trans()[1]); + _r.image(Mix.masks[2], -_r.width / 2, -_r.height / 2); + Mix.masks[2].clear(); + _r.pop(); +} - /** - * This function forces marker-brushes and fills to be updated into the canvas - */ - export function reBlend() { - Mix.blend(false, true) - Mix.blend(false, true, true) - } +/** + * This function forces marker-brushes and fills to be updated into the canvas + */ +export function reBlend() { + Mix.blend(false, true); + Mix.blend(false, true, true); +} - /** - * Register methods after setup() and post draw() for belding last buffered color - */ - function _registerMethods (p5p) { - p5p.registerMethod('afterSetup', () => Mix.blend(false, true)); - p5p.registerMethod('afterSetup', () => Mix.blend(false, true, true)); - p5p.registerMethod('post', () => Mix.blend(false, true)); - p5p.registerMethod('post', () => Mix.blend(false, true, true)); - } - if (typeof p5 !== "undefined") _registerMethods(p5.prototype); - - export function instance (inst) { - _isInstanced = true; - _inst = inst; - _r = inst; - _registerMethods(inst) - } +/** + * Register methods after setup() and post draw() for belding last buffered color + */ +function _registerMethods(p5p) { + p5p.registerMethod("afterSetup", () => Mix.blend(false, true)); + p5p.registerMethod("afterSetup", () => Mix.blend(false, true, true)); + p5p.registerMethod("post", () => Mix.blend(false, true)); + p5p.registerMethod("post", () => Mix.blend(false, true, true)); +} +if (typeof p5 !== "undefined") _registerMethods(p5.prototype); + +export function instance(inst) { + _isInstanced = true; + _inst = inst; + _r = inst; + _registerMethods(inst); +} // ============================================================================= // Section: FlowField @@ -711,298 +745,328 @@ * dynamic visual patterns. */ - /** - * Activates a specific vector field by name, ensuring it's ready for use. - * @param {string} a - The name of the vector field to activate. - */ - export function field (a) { - _ensureReady(); - // Check if field exists - FF.isActive = true; // Mark the field framework as active - FF.current = a; // Update the current field - } - - /** - * Deactivates the current vector field. - */ - export function noField () { - _ensureReady(); - FF.isActive = false; - } - - /** - * Adds a new vector field to the field list with a unique name and a generator function. - * @param {string} name - The unique name for the new vector field. - * @param {Function} funct - The function that generates the field values. - */ - export function addField(name,funct) { - FF.list.set(name,{gen: funct}); // Map the field name to its generator function - FF.current = name; // Set the newly added field as the current one to be used - FF.refresh(); // Refresh the field values using the generator function - } - - /** - * Refreshes the current vector field based on the generator function, which can be time-dependent. - * @param {number} [t=0] - An optional time parameter that can affect field generation. - */ - export function refreshField(t) { - FF.refresh(t) - } - - /** - * Retrieves a list of all available vector field names. - * @returns {Iterator} An iterator that provides the names of all the fields. - */ - export function listFields() {return Array.from(FF.list.keys())} - - /** - * Represents a framework for managing vector fields used in dynamic simulations or visualizations. - * @property {boolean} isActive - Indicates whether any vector field is currently active. - * @property {Map} list - A map associating field names to their respective generator functions and current states. - * @property {Array} field - An array representing the current vector field grid with values. - */ - const FF = { - isActive: false, - list: new Map(), - current: '', - - /** - * Calculates a relative step length based on the renderer's dimensions, used in field grid calculations. - * @returns {number} The relative step length value. - */ - step_length() { - return Math.min(_r.width,_r.height) / 1000 - }, - - /** - * Initializes the field grid and sets up the vector field's structure based on the renderer's dimensions. - */ - create() { - this.R = _r.width * 0.01; // Determine the resolution of the field grid - this.left_x = -1 * _r.width; // Left boundary of the field - this.top_y = -1 * _r.height; // Top boundary of the field - this.num_columns = Math.round(2 * _r.width / this.R); // Number of columns in the grid - this.num_rows = Math.round(2 * _r.height / this.R); // Number of columns in the grid - this.addStandard(); // Add default vector fields - }, +/** + * Activates a specific vector field by name, ensuring it's ready for use. + * @param {string} a - The name of the vector field to activate. + */ +export function field(a) { + _ensureReady(); + // Check if field exists + FF.isActive = true; // Mark the field framework as active + FF.current = a; // Update the current field +} - /** - * Retrieves the field values for the current vector field. - * @returns {Float64Array[]} The current vector field grid. - */ - flow_field() { - return this.list.get(this.current).field - }, +/** + * Deactivates the current vector field. + */ +export function noField() { + _ensureReady(); + FF.isActive = false; +} - /** - * Regenerates the current vector field using its associated generator function. - * @param {number} [t=0] - An optional time parameter that can affect field generation. - */ - refresh(t = 0) { - this.list.get(this.current).field = this.list.get(this.current).gen(t,this.genField()) - }, +/** + * Adds a new vector field to the field list with a unique name and a generator function. + * @param {string} name - The unique name for the new vector field. + * @param {Function} funct - The function that generates the field values. + */ +export function addField(name, funct) { + FF.list.set(name, { gen: funct }); // Map the field name to its generator function + FF.current = name; // Set the newly added field as the current one to be used + FF.refresh(); // Refresh the field values using the generator function +} - /** - * Generates empty field array using its associated generator function. - * @returns {Float64Array[]} Empty vector field grid. - */ - genField() { - let grid = new Array(this.num_columns); // Initialize the field array - for (let i = 0; i < this.num_columns; i++) { - grid[i] = new Float64Array(this.num_rows); - } - return grid; - }, +/** + * Refreshes the current vector field based on the generator function, which can be time-dependent. + * @param {number} [t=0] - An optional time parameter that can affect field generation. + */ +export function refreshField(t) { + FF.refresh(t); +} - /** - * Adds standard predefined vector fields to the list with unique behaviors. - */ - addStandard() { - addField("curved", function(t,field) { - let angleRange = R.randInt(-25,-15); - if (R.randInt(0,100)%2 == 0) {angleRange = angleRange * -1} - for (let column=0;column} An iterator that provides the names of all the fields. + */ +export function listFields() { + return Array.from(FF.list.keys()); +} - /** - * The Position class represents a point within a two-dimensional space, which can interact with a vector field. - * It provides methods to update the position based on the field's flow and to check whether the position is - * within certain bounds (e.g., within the field or canvas). - */ - export class Position { - - /** - * Constructs a new Position instance. - * @param {number} x - The initial x-coordinate. - * @param {number} y - The initial y-coordinate. - */ - constructor (x,y) { - this.update(x, y); - this.plotted = 0; +/** + * Represents a framework for managing vector fields used in dynamic simulations or visualizations. + * @property {boolean} isActive - Indicates whether any vector field is currently active. + * @property {Map} list - A map associating field names to their respective generator functions and current states. + * @property {Array} field - An array representing the current vector field grid with values. + */ +const FF = { + isActive: false, + list: new Map(), + current: "", + + /** + * Calculates a relative step length based on the renderer's dimensions, used in field grid calculations. + * @returns {number} The relative step length value. + */ + step_length() { + return Math.min(_r.width, _r.height) / 1000; + }, + + /** + * Initializes the field grid and sets up the vector field's structure based on the renderer's dimensions. + */ + create() { + this.R = _r.width * 0.01; // Determine the resolution of the field grid + this.left_x = -1 * _r.width; // Left boundary of the field + this.top_y = -1 * _r.height; // Top boundary of the field + this.num_columns = Math.round((2 * _r.width) / this.R); // Number of columns in the grid + this.num_rows = Math.round((2 * _r.height) / this.R); // Number of columns in the grid + this.addStandard(); // Add default vector fields + }, + + /** + * Retrieves the field values for the current vector field. + * @returns {Float64Array[]} The current vector field grid. + */ + flow_field() { + return this.list.get(this.current).field; + }, + + /** + * Regenerates the current vector field using its associated generator function. + * @param {number} [t=0] - An optional time parameter that can affect field generation. + */ + refresh(t = 0) { + this.list.get(this.current).field = this.list + .get(this.current) + .gen(t, this.genField()); + }, + + /** + * Generates empty field array using its associated generator function. + * @returns {Float64Array[]} Empty vector field grid. + */ + genField() { + let grid = new Array(this.num_columns); // Initialize the field array + for (let i = 0; i < this.num_columns; i++) { + grid[i] = new Float64Array(this.num_rows); + } + return grid; + }, + + /** + * Adds standard predefined vector fields to the list with unique behaviors. + */ + addStandard() { + addField("curved", function (t, field) { + let angleRange = R.randInt(-25, -15); + if (R.randInt(0, 100) % 2 == 0) { + angleRange = angleRange * -1; + } + for (let column = 0; column < FF.num_columns; column++) { + for (let row = 0; row < FF.num_rows; row++) { + let noise_val = _r.noise( + column * 0.02 + t * 0.03, + row * 0.02 + t * 0.03 + ); + let angle = R.map(noise_val, 0.0, 1.0, -angleRange, angleRange); + field[column][row] = 3 * angle; } - - /** - * Updates the position's coordinates and calculates its offsets and indices within the flow field if active. - * @param {number} x - The new x-coordinate. - * @param {number} y - The new y-coordinate. - */ - update (x,y) { - this.x = x , this.y = y; - if (FF.isActive) { - this.x_offset = this.x - FF.left_x + Matrix.trans()[0]; - this.y_offset = this.y - FF.top_y + Matrix.trans()[1]; - this.column_index = Math.round(this.x_offset / FF.R); - this.row_index = Math.round(this.y_offset / FF.R); - } + } + return field; + }); + addField("truncated", function (t, field) { + let angleRange = R.randInt(-25, -15) + 5 * R.sin(t); + if (R.randInt(0, 100) % 2 == 0) { + angleRange = angleRange * -1; + } + let truncate = R.randInt(5, 10); + for (let column = 0; column < FF.num_columns; column++) { + for (let row = 0; row < FF.num_rows; row++) { + let noise_val = _r.noise(column * 0.02, row * 0.02); + let angle = + Math.round( + R.map(noise_val, 0.0, 1.0, -angleRange, angleRange) / truncate + ) * truncate; + field[column][row] = 4 * angle; } - - /** - * Resets the 'plotted' property to 0. - */ - reset() { - this.plotted = 0; + } + return field; + }); + addField("zigzag", function (t, field) { + let angleRange = R.randInt(-30, -15) + Math.abs(44 * R.sin(t)); + if (R.randInt(0, 100) % 2 == 0) { + angleRange = angleRange * -1; + } + let dif = angleRange; + let angle = 0; + for (let column = 0; column < FF.num_columns; column++) { + for (let row = 0; row < FF.num_rows; row++) { + field[column][row] = angle; + angle = angle + dif; + dif = -1 * dif; } - - /** - * Checks if the position is within the active flow field's bounds. - * @returns {boolean} - True if the position is within the flow field, false otherwise. - */ - isIn() { - return (FF.isActive) ? ((this.column_index >= 0 && this.row_index >= 0) && (this.column_index < FF.num_columns && this.row_index < FF.num_rows)) : this.isInCanvas() + angle = angle + dif; + dif = -1 * dif; + } + return field; + }); + addField("waves", function (t, field) { + let sinrange = R.randInt(10, 15) + 5 * R.sin(t); + let cosrange = R.randInt(3, 6) + 3 * R.cos(t); + let baseAngle = R.randInt(20, 35); + for (let column = 0; column < FF.num_columns; column++) { + for (let row = 0; row < FF.num_rows; row++) { + let angle = + R.sin(sinrange * column) * (baseAngle * R.cos(row * cosrange)) + + R.randInt(-3, 3); + field[column][row] = angle; } - - /** - * Checks if the position is within reasonable bounds (+ half canvas on each side). - * @returns {boolean} - True if the position is within bounds, false otherwise. - */ - isInCanvas() { - let w = _r.width, h = _r.height; - return (this.x >= -w - Matrix.trans()[0] && this.x <= w - Matrix.trans()[0]) && (this.y >= -h - Matrix.trans()[1] && this.y <= h - Matrix.trans()[1]) + } + return field; + }); + addField("seabed", function (t, field) { + let baseSize = R.random(0.4, 0.8); + let baseAngle = R.randInt(18, 26); + for (let column = 0; column < FF.num_columns; column++) { + for (let row = 0; row < FF.num_rows; row++) { + let addition = R.randInt(15, 20); + let angle = baseAngle * R.sin(baseSize * row * column + addition); + field[column][row] = 1.1 * angle * R.cos(t); } + } + return field; + }); + }, +}; - /** - * Calculates the angle of the flow field at the position's current coordinates. - * @returns {number} - The angle in radians, or 0 if the position is not in the flow field or if the flow field is not active. - */ - angle () { - return (this.isIn() && FF.isActive) ? FF.flow_field()[this.column_index][this.row_index] : 0 - } - - /** - * Moves the position along the flow field by a certain length. - * @param {number} _length - The length to move along the field. - * @param {number} _dir - The direction of movement. - * @param {number} _step_length - The length of each step. - * @param {boolean} isFlow - Whether to use the flow field for movement. - */ - moveTo (_length, _dir, _step_length = B.spacing(), isFlow = true) { - if (this.isIn()) { - let a, b; - if (!isFlow) { - a = R.cos(-_dir); - b = R.sin(-_dir); - } - for (let i = 0; i < _length / _step_length; i++) { - if (isFlow) { - let angle = this.angle(); - a = R.cos(angle - _dir); - b = R.sin(angle - _dir); - } - let x_step = (_step_length * a), y_step = (_step_length * b); - this.plotted += _step_length; - this.update(this.x + x_step, this.y + y_step); - } - } else { - this.plotted += _step_length; - } - } - - /** - * Plots a point to another position within the flow field, following a Plot object - * @param {Position} _plot - The Plot path object. - * @param {number} _length - The length to move towards the target position. - * @param {number} _step_length - The length of each step. - * @param {number} _scale - The scaling factor for the plotting path. - */ - plotTo (_plot, _length, _step_length, _scale) { - if (this.isIn()) { - const inverse_scale = 1 / _scale; - for (let i = 0; i < _length / _step_length; i++) { - let current_angle = this.angle(); - let plot_angle = _plot.angle(this.plotted); - let x_step = (_step_length * R.cos(current_angle - plot_angle)); - let y_step = (_step_length * R.sin(current_angle - plot_angle)); - this.plotted += _step_length * inverse_scale; - this.update(this.x + x_step, this.y + y_step); - } - } else { - this.plotted += _step_length / scale; - } +/** + * The Position class represents a point within a two-dimensional space, which can interact with a vector field. + * It provides methods to update the position based on the field's flow and to check whether the position is + * within certain bounds (e.g., within the field or canvas). + */ +export class Position { + /** + * Constructs a new Position instance. + * @param {number} x - The initial x-coordinate. + * @param {number} y - The initial y-coordinate. + */ + constructor(x, y) { + this.update(x, y); + this.plotted = 0; + } + + /** + * Updates the position's coordinates and calculates its offsets and indices within the flow field if active. + * @param {number} x - The new x-coordinate. + * @param {number} y - The new y-coordinate. + */ + update(x, y) { + (this.x = x), (this.y = y); + if (FF.isActive) { + this.x_offset = this.x - FF.left_x + Matrix.trans()[0]; + this.y_offset = this.y - FF.top_y + Matrix.trans()[1]; + this.column_index = Math.round(this.x_offset / FF.R); + this.row_index = Math.round(this.y_offset / FF.R); + } + } + + /** + * Resets the 'plotted' property to 0. + */ + reset() { + this.plotted = 0; + } + + /** + * Checks if the position is within the active flow field's bounds. + * @returns {boolean} - True if the position is within the flow field, false otherwise. + */ + isIn() { + return FF.isActive + ? this.column_index >= 0 && + this.row_index >= 0 && + this.column_index < FF.num_columns && + this.row_index < FF.num_rows + : this.isInCanvas(); + } + + /** + * Checks if the position is within reasonable bounds (+ half canvas on each side). + * @returns {boolean} - True if the position is within bounds, false otherwise. + */ + isInCanvas() { + let w = _r.width, + h = _r.height; + return ( + this.x >= -w - Matrix.trans()[0] && + this.x <= w - Matrix.trans()[0] && + this.y >= -h - Matrix.trans()[1] && + this.y <= h - Matrix.trans()[1] + ); + } + + /** + * Calculates the angle of the flow field at the position's current coordinates. + * @returns {number} - The angle in radians, or 0 if the position is not in the flow field or if the flow field is not active. + */ + angle() { + return this.isIn() && FF.isActive + ? FF.flow_field()[this.column_index][this.row_index] + : 0; + } + + /** + * Moves the position along the flow field by a certain length. + * @param {number} _length - The length to move along the field. + * @param {number} _dir - The direction of movement. + * @param {number} _step_length - The length of each step. + * @param {boolean} isFlow - Whether to use the flow field for movement. + */ + moveTo(_length, _dir, _step_length = B.spacing(), isFlow = true) { + if (this.isIn()) { + let a, b; + if (!isFlow) { + a = R.cos(-_dir); + b = R.sin(-_dir); + } + for (let i = 0; i < _length / _step_length; i++) { + if (isFlow) { + let angle = this.angle(); + a = R.cos(angle - _dir); + b = R.sin(angle - _dir); } - } - + let x_step = _step_length * a, + y_step = _step_length * b; + this.plotted += _step_length; + this.update(this.x + x_step, this.y + y_step); + } + } else { + this.plotted += _step_length; + } + } + + /** + * Plots a point to another position within the flow field, following a Plot object + * @param {Position} _plot - The Plot path object. + * @param {number} _length - The length to move towards the target position. + * @param {number} _step_length - The length of each step. + * @param {number} _scale - The scaling factor for the plotting path. + */ + plotTo(_plot, _length, _step_length, _scale) { + if (this.isIn()) { + const inverse_scale = 1 / _scale; + for (let i = 0; i < _length / _step_length; i++) { + let current_angle = this.angle(); + let plot_angle = _plot.angle(this.plotted); + let x_step = _step_length * R.cos(current_angle - plot_angle); + let y_step = _step_length * R.sin(current_angle - plot_angle); + this.plotted += _step_length * inverse_scale; + this.update(this.x + x_step, this.y + y_step); + } + } else { + this.plotted += _step_length / scale; + } + } +} // ============================================================================= // Section: Brushes @@ -1029,451 +1093,552 @@ * one can achieve a wide range of artistic styles and techniques. */ - /** - * Adjusts the global scale of brush parameters based on the provided scale factor. - * This affects the weight, vibration, and spacing of each standard brush. - * - * @param {number} _scale - The scaling factor to apply to the brush parameters. - */ - export function scaleBrushes(_scale) { - for (let s of _standard_brushes) { - let params = B.list.get(s[0]).param - params.weight *= _scale, params.vibration *= _scale, params.spacing *= _scale; - } - _gScale = _scale - } - let _gScale = 1; - - /** - * Disables the stroke for subsequent drawing operations. - * This function sets the brush's `isActive` property to false, indicating that no stroke - * should be applied to the shapes drawn after this method is called. - */ - export function noStroke() { - B.isActive = false; - } - - /** - * Retrieves a list of all available brush names from the brush manager. - * @returns {Array} An array containing the names of all brushes. - */ - export function box() { - return Array.from(B.list.keys()) - } - - /** - * The B object, representing a brush, contains properties and methods to manipulate - * the brush's appearance and behavior when drawing on the canvas. - * @type {Object} - */ - const B = { - isActive: true, // Indicates if the brush is active. - list: new Map(), // Stores brush definitions by name. - c: "#000000", // Current color of the brush. - w: 1, // Current weight (size) of the brush. - cr: null, // Clipping region for brush strokes. - name: "HB", // Name of the current brush. - - /** - * Calculates the tip spacing based on the current brush parameters. - * @returns {number} The calculated spacing value. - */ - spacing() { - this.p = this.list.get(this.name).param - if (this.p.type === "default" || this.p.type === "spray") return this.p.spacing / this.w; - return this.p.spacing; - }, - - /** - * Initializes the drawing state with the given parameters. - * @param {number} x - The x-coordinate of the starting point. - * @param {number} y - The y-coordinate of the starting point. - * @param {number} length - The length of the line to draw. - * @param {boolean} flow - Flag indicating if the line should follow the vector-field. - * @param {Object|boolean} plot - The shape object to be used for plotting, or false if not plotting a shape. - */ - initializeDrawingState(x, y, length, flow, plot) { - this.position = new Position(x, y); - this.length = length; - this.flow = flow; - this.plot = plot; - if (plot) plot.calcIndex(0); - }, - - /** - * Executes the drawing operation for lines or shapes. - * @param {number} angle_scale - The angle or scale to apply during drawing. - * @param {boolean} isPlot - Flag indicating if the operation is plotting a shape. - */ - draw(angle_scale, isPlot) { - if (!isPlot) this.dir = angle_scale; - this.pushState(); - const st = this.spacing(); - const total_steps = isPlot ? Math.round(this.length * angle_scale / st) : Math.round(this.length / st); - for (let steps = 0; steps < total_steps; steps++) { - this.tip(); - if (isPlot) { - this.position.plotTo(this.plot, st, st, angle_scale); - } else { - this.position.moveTo(st, angle_scale, st, this.flow); - } - } - this.popState(); - }, - - /** - * Executes the drawing operation for a single tip. - * @param {number} pressure - The desired pressure value. - */ - drawTip(pressure) { - this.pushState(true); - this.tip(pressure) - this.popState(true); - }, - - /** - * Sets up the environment for a brush stroke. - */ - pushState(isTip = false) { - this.p = this.list.get(this.name).param - // Pressure values for the stroke - if (!isTip) { - this.a = this.p.pressure.type !== "custom" ? R.random(-1, 1) : 0; - this.b = this.p.pressure.type !== "custom" ? R.random(1, 1.5) : 0; - this.cp = this.p.pressure.type !== "custom" ? R.random(3, 3.5) : R.random(-0.2, 0.2); - const [min, max] = this.p.pressure.min_max; - this.min = min; - this.max = max; - } - // Blend Mode - this.c = _r.color(this.c); - // Select mask buffer for blend mode - this.mask = this.p.blend ? ((this.p.type === "image") ? Mix.masks[1] : Mix.masks[0]) : Mix.masks[2]; - Matrix.trans() - // Set the blender - this.mask.push(); - this.mask.noStroke(); - (this.p.type === "image") ? this.mask.translate(Matrix.translation[0],Matrix.translation[1]) : this.mask.translate(Matrix.translation[0] + _r.width/2,Matrix.translation[1] + _r.height/2); - this.mask.rotate(-Matrix.rotation) - this.mask.scale(_curScale) - if (this.p.blend) { - Mix.watercolor = false; - if (this.p.type !== "image") Mix.blend(this.c); - else Mix.blend(this.c,false,true) - if (!isTip) this.markerTip() - } - this.alpha = this.calculateAlpha(); // Calcula Alpha - this.applyColor(this.alpha); // Apply Color - }, - - /** - * Restores the drawing state after a brush stroke is completed. - */ - popState(isTip = false) { - if (this.p.blend && !isTip) this.markerTip(); - this.mask.pop(); - }, - - /** - * Draws the tip of the brush based on the current pressure and position. - * @param {number} pressure - The desired pressure value. - */ - tip(custom_pressure = false) { - let pressure = custom_pressure ? custom_pressure : this.calculatePressure(); // Calculate Pressure - if (this.isInsideClippingArea()) { // Check if it's inside clipping area - switch (this.p.type) { // Draw different tip types - case "spray": - this.drawSpray(pressure); - break; - case "marker": - this.drawMarker(pressure); - break; - case "custom": - case "image": - this.drawCustomOrImage(pressure, this.alpha); - break; - default: - this.drawDefault(pressure); - break; - } - } - }, - - /** - * Calculates the pressure for the current position in the stroke. - * @returns {number} The calculated pressure value. - */ - calculatePressure() { - return this.plot - ? this.simPressure() * this.plot.pressure(this.position.plotted) - : this.simPressure(); - }, - - /** - * Simulates brush pressure based on the current position and brush parameters. - * @returns {number} The simulated pressure value. - */ - simPressure () { - if (this.p.pressure.type === "custom") { - return R.map(this.p.pressure.curve(this.position.plotted / this.length) + this.cp, 0, 1, this.min, this.max, true); - } - return this.gauss() - }, - - /** - * Generates a Gaussian distribution value for the pressure calculation. - * @param {number} a - Center of the Gaussian bell curve. - * @param {number} b - Width of the Gaussian bell curve. - * @param {number} c - Shape of the Gaussian bell curve. - * @param {number} min - Minimum pressure value. - * @param {number} max - Maximum pressure value. - * @returns {number} The calculated Gaussian value. - */ - gauss(a = 0.5 + B.p.pressure.curve[0] * B.a, b = 1 - B.p.pressure.curve[1] * B.b, c = B.cp, min = B.min, max = B.max) { - return R.map((1 / ( 1 + Math.pow(Math.abs( ( this.position.plotted - a * this.length ) / ( b * this.length / 2 ) ), 2 * c))), 0, 1, min, max); - }, - - /** - * Calculates the alpha (opacity) level for the brush stroke based on pressure. - * @param {number} pressure - The current pressure value. - * @returns {number} The calculated alpha value. - */ - calculateAlpha() { - let opacity = (this.p.type !== "default" && this.p.type !== "spray") ? this.p.opacity / this.w : this.p.opacity; - return opacity; - }, - - /** - * Applies the current color and alpha to the renderer. - * @param {number} alpha - The alpha (opacity) level to apply. - */ - applyColor(alpha) { - if (this.p.blend) { - this.mask.fill(255, 0, 0, alpha / 2); - } else { - this.c.setAlpha(alpha); - this.mask.fill(this.c); - } - }, - - /** - * Checks if the current brush position is inside the defined clipping area. - * @returns {boolean} True if the position is inside the clipping area, false otherwise. - */ - isInsideClippingArea() { - if (B.cr) return this.position.x >= B.cr[0] && this.position.x <= B.cr[2] && this.position.y >= B.cr[1] && this.position.y <= B.cr[3]; - else { - let w = 0.55 * _r.width, h = 0.55 * _r.height; - return (this.position.x >= -w - Matrix.trans()[0] && this.position.x <= w - Matrix.trans()[0]) && (this.position.y >= -h - Matrix.trans()[1] && this.position.y <= h - Matrix.trans()[1]) - } - }, - - /** - * Draws the spray tip of the brush. - * @param {number} pressure - The current pressure value. - */ - drawSpray(pressure) { - let vibration = (this.w * this.p.vibration * pressure) + this.w * R.gaussian() * this.p.vibration / 3; - let sw = this.p.weight * R.random(0.9,1.1); - const iterations = this.p.quality / pressure; - for (let j = 0; j < iterations; j++) { - let r = R.random(0.9,1.1); - let rX = r * vibration * R.random(-1,1); - let yRandomFactor = R.random(-1, 1); - let rVibrationSquared = Math.pow(r * vibration, 2); - let sqrtPart = Math.sqrt(rVibrationSquared - Math.pow(rX, 2)); - this.mask.circle(this.position.x + rX, this.position.y + yRandomFactor * sqrtPart, sw); - } - }, - - /** - * Draws the marker tip of the brush. - * @param {number} pressure - The current pressure value. - * @param {boolean} [vibrate=true] - Whether to apply vibration effect. - */ - drawMarker(pressure, vibrate = true) { - let vibration = vibrate ? this.w * this.p.vibration : 0; - let rx = vibrate ? vibration * R.random(-1,1) : 0; - let ry = vibrate ? vibration * R.random(-1,1) : 0; - this.mask.circle(this.position.x + rx, this.position.y + ry, this.w * this.p.weight * pressure) - }, - - /** - * Draws the custom or image tip of the brush. - * @param {number} pressure - The current pressure value. - * @param {number} alpha - The alpha (opacity) level to apply. - * @param {boolean} [vibrate=true] - Whether to apply vibration effect. - */ - drawCustomOrImage(pressure, alpha, vibrate = true) { - this.mask.push(); - let vibration = vibrate ? this.w * this.p.vibration : 0; - let rx = vibrate ? vibration * R.random(-1,1) : 0; - let ry = vibrate ? vibration * R.random(-1,1) : 0; - this.mask.translate(this.position.x + rx, this.position.y + ry); - this.adjustSizeAndRotation(this.w * pressure, alpha) - this.p.tip(this.mask); - this.mask.pop(); - }, - - /** - * Draws the default tip of the brush. - * @param {number} pressure - The current pressure value. - */ - drawDefault(pressure) { - let vibration = this.w * this.p.vibration * (this.p.definition + (1-this.p.definition) * R.gaussian() * this.gauss(0.5,0.9,5,0.2,1.2) / pressure); - if (R.random(0, this.p.quality * pressure) > 0.4) { - this.mask.circle(this.position.x + 0.7 * vibration * R.random(-1,1),this.position.y + vibration * R.random(-1,1), pressure * this.p.weight * R.random(0.85,1.15)); - } - }, +/** + * Adjusts the global scale of brush parameters based on the provided scale factor. + * This affects the weight, vibration, and spacing of each standard brush. + * + * @param {number} _scale - The scaling factor to apply to the brush parameters. + */ +export function scaleBrushes(_scale) { + for (let s of _standard_brushes) { + let params = B.list.get(s[0]).param; + (params.weight *= _scale), + (params.vibration *= _scale), + (params.spacing *= _scale); + } + _gScale = _scale; +} +let _gScale = 1; - /** - * Adjusts the size and rotation of the brush tip before drawing. - * @param {number} pressure - The current pressure value. - * @param {number} alpha - The alpha (opacity) level to apply. - */ - adjustSizeAndRotation(pressure, alpha) { - this.mask.scale(pressure); - if (this.p.type === "image") (this.p.blend) ? this.mask.tint(255, 0, 0, alpha / 2) : this.mask.tint(this.mask.red(this.c), this.mask.green(this.c), this.mask.blue(this.c), alpha); - if (this.p.rotate === "random") this.mask.rotate(R.randInt(0,360)); - else if (this.p.rotate === "natural") { - let angle = ((this.plot) ? - this.plot.angle(this.position.plotted) : - this.dir) + (this.flow ? this.position.angle() : 0) - this.mask.rotate(angle) - } - }, +/** + * Disables the stroke for subsequent drawing operations. + * This function sets the brush's `isActive` property to false, indicating that no stroke + * should be applied to the shapes drawn after this method is called. + */ +export function noStroke() { + B.isActive = false; +} - /** - * Draws the marker tip with a blend effect. - */ - markerTip() { - if (this.isInsideClippingArea()) { - let pressure = this.calculatePressure(); - let alpha = this.calculateAlpha(pressure); - this.mask.fill(255, 0, 0, alpha / 1.5); - if (B.p.type === "marker") { - for (let s = 1; s < 5; s++) { - this.drawMarker(pressure * s/5, false) - } - } else if (B.p.type === "custom" || B.p.type === "image") { - for (let s = 1; s < 5; s++) { - this.drawCustomOrImage(pressure * s/5, alpha, false) - } - } - } - }, - } +/** + * Retrieves a list of all available brush names from the brush manager. + * @returns {Array} An array containing the names of all brushes. + */ +export function box() { + return Array.from(B.list.keys()); +} - /** - * Adds a new brush with the specified parameters to the brush list. - * @param {string} name - The unique name for the new brush. - * @param {BrushParameters} params - The parameters defining the brush behavior and appearance. - */ - export function add (a, b) { - const isBlendableType = b.type === "marker" || b.type === "custom" || b.type === "image"; - if (!isBlendableType && b.type !== "spray") b.type = "default"; - if (b.type === "image") { - T.add(b.image.src); - b.tip = () => B.mask.image(T.tips.get(B.p.image.src), -B.p.weight / 2, -B.p.weight / 2, B.p.weight, B.p.weight); +/** + * The B object, representing a brush, contains properties and methods to manipulate + * the brush's appearance and behavior when drawing on the canvas. + * @type {Object} + */ +const B = { + isActive: true, // Indicates if the brush is active. + list: new Map(), // Stores brush definitions by name. + c: "#000000", // Current color of the brush. + w: 1, // Current weight (size) of the brush. + cr: null, // Clipping region for brush strokes. + name: "HB", // Name of the current brush. + + /** + * Calculates the tip spacing based on the current brush parameters. + * @returns {number} The calculated spacing value. + */ + spacing() { + this.p = this.list.get(this.name).param; + if (this.p.type === "default" || this.p.type === "spray") + return this.p.spacing / this.w; + return this.p.spacing; + }, + + /** + * Initializes the drawing state with the given parameters. + * @param {number} x - The x-coordinate of the starting point. + * @param {number} y - The y-coordinate of the starting point. + * @param {number} length - The length of the line to draw. + * @param {boolean} flow - Flag indicating if the line should follow the vector-field. + * @param {Object|boolean} plot - The shape object to be used for plotting, or false if not plotting a shape. + */ + initializeDrawingState(x, y, length, flow, plot) { + this.position = new Position(x, y); + this.length = length; + this.flow = flow; + this.plot = plot; + if (plot) plot.calcIndex(0); + }, + + /** + * Executes the drawing operation for lines or shapes. + * @param {number} angle_scale - The angle or scale to apply during drawing. + * @param {boolean} isPlot - Flag indicating if the operation is plotting a shape. + */ + draw(angle_scale, isPlot) { + if (!isPlot) this.dir = angle_scale; + this.pushState(); + const st = this.spacing(); + const total_steps = isPlot + ? Math.round((this.length * angle_scale) / st) + : Math.round(this.length / st); + for (let steps = 0; steps < total_steps; steps++) { + this.tip(); + if (isPlot) { + this.position.plotTo(this.plot, st, st, angle_scale); + } else { + this.position.moveTo(st, angle_scale, st, this.flow); + } + } + this.popState(); + }, + + /** + * Executes the drawing operation for a single tip. + * @param {number} pressure - The desired pressure value. + */ + drawTip(pressure) { + this.pushState(true); + this.tip(pressure); + this.popState(true); + }, + + /** + * Sets up the environment for a brush stroke. + */ + pushState(isTip = false) { + this.p = this.list.get(this.name).param; + // Pressure values for the stroke + if (!isTip) { + this.a = this.p.pressure.type !== "custom" ? R.random(-1, 1) : 0; + this.b = this.p.pressure.type !== "custom" ? R.random(1, 1.5) : 0; + this.cp = + this.p.pressure.type !== "custom" + ? R.random(3, 3.5) + : R.random(-0.2, 0.2); + const [min, max] = this.p.pressure.min_max; + this.min = min; + this.max = max; + } + // Blend Mode + this.c = _r.color(this.c); + // Select mask buffer for blend mode + this.mask = this.p.blend + ? this.p.type === "image" + ? Mix.masks[1] + : Mix.masks[0] + : Mix.masks[2]; + Matrix.trans(); + // Set the blender + this.mask.push(); + this.mask.noStroke(); + this.p.type === "image" + ? this.mask.translate(Matrix.translation[0], Matrix.translation[1]) + : this.mask.translate( + Matrix.translation[0] + _r.width / 2, + Matrix.translation[1] + _r.height / 2 + ); + this.mask.rotate(-Matrix.rotation); + this.mask.scale(_curScale); + if (this.p.blend) { + Mix.watercolor = false; + if (this.p.type !== "image") Mix.blend(this.c); + else Mix.blend(this.c, false, true); + if (!isTip) this.markerTip(); + } + this.alpha = this.calculateAlpha(); // Calcula Alpha + this.applyColor(this.alpha); // Apply Color + }, + + /** + * Restores the drawing state after a brush stroke is completed. + */ + popState(isTip = false) { + if (this.p.blend && !isTip) this.markerTip(); + this.mask.pop(); + }, + + /** + * Draws the tip of the brush based on the current pressure and position. + * @param {number} pressure - The desired pressure value. + */ + tip(custom_pressure = false) { + let pressure = custom_pressure ? custom_pressure : this.calculatePressure(); // Calculate Pressure + if (this.isInsideClippingArea()) { + // Check if it's inside clipping area + switch ( + this.p.type // Draw different tip types + ) { + case "spray": + this.drawSpray(pressure); + break; + case "marker": + this.drawMarker(pressure); + break; + case "custom": + case "image": + this.drawCustomOrImage(pressure, this.alpha); + break; + default: + this.drawDefault(pressure); + break; + } + } + }, + + /** + * Calculates the pressure for the current position in the stroke. + * @returns {number} The calculated pressure value. + */ + calculatePressure() { + return this.plot + ? this.simPressure() * this.plot.pressure(this.position.plotted) + : this.simPressure(); + }, + + /** + * Simulates brush pressure based on the current position and brush parameters. + * @returns {number} The simulated pressure value. + */ + simPressure() { + if (this.p.pressure.type === "custom") { + return R.map( + this.p.pressure.curve(this.position.plotted / this.length) + this.cp, + 0, + 1, + this.min, + this.max, + true + ); + } + return this.gauss(); + }, + + /** + * Generates a Gaussian distribution value for the pressure calculation. + * @param {number} a - Center of the Gaussian bell curve. + * @param {number} b - Width of the Gaussian bell curve. + * @param {number} c - Shape of the Gaussian bell curve. + * @param {number} min - Minimum pressure value. + * @param {number} max - Maximum pressure value. + * @returns {number} The calculated Gaussian value. + */ + gauss( + a = 0.5 + B.p.pressure.curve[0] * B.a, + b = 1 - B.p.pressure.curve[1] * B.b, + c = B.cp, + min = B.min, + max = B.max + ) { + return R.map( + 1 / + (1 + + Math.pow( + Math.abs( + (this.position.plotted - a * this.length) / + ((b * this.length) / 2) + ), + 2 * c + )), + 0, + 1, + min, + max + ); + }, + + /** + * Calculates the alpha (opacity) level for the brush stroke based on pressure. + * @param {number} pressure - The current pressure value. + * @returns {number} The calculated alpha value. + */ + calculateAlpha() { + let opacity = + this.p.type !== "default" && this.p.type !== "spray" + ? this.p.opacity / this.w + : this.p.opacity; + return opacity; + }, + + /** + * Applies the current color and alpha to the renderer. + * @param {number} alpha - The alpha (opacity) level to apply. + */ + applyColor(alpha) { + if (this.p.blend) { + this.mask.fill(255, 0, 0, alpha / 2); + } else { + this.c.setAlpha(alpha); + this.mask.fill(this.c); + } + }, + + /** + * Checks if the current brush position is inside the defined clipping area. + * @returns {boolean} True if the position is inside the clipping area, false otherwise. + */ + isInsideClippingArea() { + if (B.cr) + return ( + this.position.x >= B.cr[0] && + this.position.x <= B.cr[2] && + this.position.y >= B.cr[1] && + this.position.y <= B.cr[3] + ); + else { + let w = 0.55 * _r.width, + h = 0.55 * _r.height; + return ( + this.position.x >= -w - Matrix.trans()[0] && + this.position.x <= w - Matrix.trans()[0] && + this.position.y >= -h - Matrix.trans()[1] && + this.position.y <= h - Matrix.trans()[1] + ); + } + }, + + /** + * Draws the spray tip of the brush. + * @param {number} pressure - The current pressure value. + */ + drawSpray(pressure) { + let vibration = + this.w * this.p.vibration * pressure + + (this.w * R.gaussian() * this.p.vibration) / 3; + let sw = this.p.weight * R.random(0.9, 1.1); + const iterations = this.p.quality / pressure; + for (let j = 0; j < iterations; j++) { + let r = R.random(0.9, 1.1); + let rX = r * vibration * R.random(-1, 1); + let yRandomFactor = R.random(-1, 1); + let rVibrationSquared = Math.pow(r * vibration, 2); + let sqrtPart = Math.sqrt(rVibrationSquared - Math.pow(rX, 2)); + this.mask.circle( + this.position.x + rX, + this.position.y + yRandomFactor * sqrtPart, + sw + ); + } + }, + + /** + * Draws the marker tip of the brush. + * @param {number} pressure - The current pressure value. + * @param {boolean} [vibrate=true] - Whether to apply vibration effect. + */ + drawMarker(pressure, vibrate = true) { + let vibration = vibrate ? this.w * this.p.vibration : 0; + let rx = vibrate ? vibration * R.random(-1, 1) : 0; + let ry = vibrate ? vibration * R.random(-1, 1) : 0; + this.mask.circle( + this.position.x + rx, + this.position.y + ry, + this.w * this.p.weight * pressure + ); + }, + + /** + * Draws the custom or image tip of the brush. + * @param {number} pressure - The current pressure value. + * @param {number} alpha - The alpha (opacity) level to apply. + * @param {boolean} [vibrate=true] - Whether to apply vibration effect. + */ + drawCustomOrImage(pressure, alpha, vibrate = true) { + this.mask.push(); + let vibration = vibrate ? this.w * this.p.vibration : 0; + let rx = vibrate ? vibration * R.random(-1, 1) : 0; + let ry = vibrate ? vibration * R.random(-1, 1) : 0; + this.mask.translate(this.position.x + rx, this.position.y + ry); + this.adjustSizeAndRotation(this.w * pressure, alpha); + this.p.tip(this.mask); + this.mask.pop(); + }, + + /** + * Draws the default tip of the brush. + * @param {number} pressure - The current pressure value. + */ + drawDefault(pressure) { + let vibration = + this.w * + this.p.vibration * + (this.p.definition + + ((1 - this.p.definition) * + R.gaussian() * + this.gauss(0.5, 0.9, 5, 0.2, 1.2)) / + pressure); + if (R.random(0, this.p.quality * pressure) > 0.4) { + this.mask.circle( + this.position.x + 0.7 * vibration * R.random(-1, 1), + this.position.y + vibration * R.random(-1, 1), + pressure * this.p.weight * R.random(0.85, 1.15) + ); + } + }, + + /** + * Adjusts the size and rotation of the brush tip before drawing. + * @param {number} pressure - The current pressure value. + * @param {number} alpha - The alpha (opacity) level to apply. + */ + adjustSizeAndRotation(pressure, alpha) { + this.mask.scale(pressure); + if (this.p.type === "image") + this.p.blend + ? this.mask.tint(255, 0, 0, alpha / 2) + : this.mask.tint( + this.mask.red(this.c), + this.mask.green(this.c), + this.mask.blue(this.c), + alpha + ); + if (this.p.rotate === "random") this.mask.rotate(R.randInt(0, 360)); + else if (this.p.rotate === "natural") { + let angle = + (this.plot ? -this.plot.angle(this.position.plotted) : -this.dir) + + (this.flow ? this.position.angle() : 0); + this.mask.rotate(angle); + } + }, + + /** + * Draws the marker tip with a blend effect. + */ + markerTip() { + if (this.isInsideClippingArea()) { + let pressure = this.calculatePressure(); + let alpha = this.calculateAlpha(pressure); + this.mask.fill(255, 0, 0, alpha / 1.5); + if (B.p.type === "marker") { + for (let s = 1; s < 5; s++) { + this.drawMarker((pressure * s) / 5, false); + } + } else if (B.p.type === "custom" || B.p.type === "image") { + for (let s = 1; s < 5; s++) { + this.drawCustomOrImage((pressure * s) / 5, alpha, false); } - b.blend = ((isBlendableType && b.blend !== false) || b.blend) ? true : false; - B.list.set(a, { param: b, colors: [], buffers: [] }); + } } + }, +}; - /** - * Sets the current brush with the specified name, color, and weight. - * @param {string} brushName - The name of the brush to set as current. - * @param {string|p5.Color} color - The color to set for the brush. - * @param {number} weight - The weight (size) to set for the brush. - */ - export function set(brushName, color, weight = 1) { - pick(brushName); - B.c = color; - B.w = weight; - B.isActive = true; - } +/** + * Adds a new brush with the specified parameters to the brush list. + * @param {string} name - The unique name for the new brush. + * @param {BrushParameters} params - The parameters defining the brush behavior and appearance. + */ +export function add(a, b) { + const isBlendableType = + b.type === "marker" || b.type === "custom" || b.type === "image"; + if (!isBlendableType && b.type !== "spray") b.type = "default"; + if (b.type === "image") { + T.add(b.image.src); + b.tip = () => + B.mask.image( + T.tips.get(B.p.image.src), + -B.p.weight / 2, + -B.p.weight / 2, + B.p.weight, + B.p.weight + ); + } + b.blend = (isBlendableType && b.blend !== false) || b.blend ? true : false; + B.list.set(a, { param: b, colors: [], buffers: [] }); +} - /** - * Sets only the current brush type based on the given name. - * @param {string} brushName - The name of the brush to set as current. - */ - export function pick(brushName) { - B.name = brushName; - } +/** + * Sets the current brush with the specified name, color, and weight. + * @param {string} brushName - The name of the brush to set as current. + * @param {string|p5.Color} color - The color to set for the brush. + * @param {number} weight - The weight (size) to set for the brush. + */ +export function set(brushName, color, weight = 1) { + pick(brushName); + B.c = color; + B.w = weight; + B.isActive = true; +} - /** - * Sets the color of the current brush. - * @param {number|string|p5.Color} r - The red component of the color, a CSS color string, or a p5.Color object. - * @param {number} [g] - The green component of the color. - * @param {number} [b] - The blue component of the color. - */ - export function stroke(r,g,b) { - if (arguments.length > 0) B.c = (arguments.length < 2) ? r : [r,g,b]; - B.isActive = true; - } +/** + * Sets only the current brush type based on the given name. + * @param {string} brushName - The name of the brush to set as current. + */ +export function pick(brushName) { + B.name = brushName; +} - /** - * Sets the weight (size) of the current brush. - * @param {number} weight - The weight to set for the brush. - */ - export function strokeWeight(weight) { - B.w = weight; - } +/** + * Sets the color of the current brush. + * @param {number|string|p5.Color} r - The red component of the color, a CSS color string, or a p5.Color object. + * @param {number} [g] - The green component of the color. + * @param {number} [b] - The blue component of the color. + */ +export function stroke(r, g, b) { + if (arguments.length > 0) B.c = arguments.length < 2 ? r : [r, g, b]; + B.isActive = true; +} - /** - * Defines a clipping region for the brush strokes. - * @param {number[]} clippingRegion - An array defining the clipping region as [x1, y1, x2, y2]. - */ - export function clip(clippingRegion) { - B.cr = clippingRegion; - } +/** + * Sets the weight (size) of the current brush. + * @param {number} weight - The weight to set for the brush. + */ +export function strokeWeight(weight) { + B.w = weight; +} - /** - * Disables clipping region. - */ - export function noClip() { - B.cr = null; - } +/** + * Defines a clipping region for the brush strokes. + * @param {number[]} clippingRegion - An array defining the clipping region as [x1, y1, x2, y2]. + */ +export function clip(clippingRegion) { + B.cr = clippingRegion; +} - /** - * Draws a line using the current brush from (x1, y1) to (x2, y2). - * @param {number} x1 - The x-coordinate of the start point. - * @param {number} y1 - The y-coordinate of the start point. - * @param {number} x2 - The x-coordinate of the end point. - * @param {number} y2 - The y-coordinate of the end point. - */ - export function line(x1,y1,x2,y2) { - _ensureReady(); - let d = R.dist(x1,y1,x2,y2) - if (d == 0) return; - B.initializeDrawingState(x1, y1, d, false, false); - let angle = _calculateAngle(x1,y1,x2,y2); - B.draw(angle, false); - } +/** + * Disables clipping region. + */ +export function noClip() { + B.cr = null; +} - /** - * Draws a flow line with the current brush from a starting point in a specified direction. - * @param {number} x - The x-coordinate of the starting point. - * @param {number} y - The y-coordinate of the starting point. - * @param {number} length - The length of the line to draw. - * @param {number} dir - The direction in which to draw the line. Angles measured anticlockwise from the x-axis - */ - export function flowLine(x,y,length,dir) { - _ensureReady(); - B.initializeDrawingState(x, y, length, true, false); - B.draw(R.toDegrees(dir), false); - } +/** + * Draws a line using the current brush from (x1, y1) to (x2, y2). + * @param {number} x1 - The x-coordinate of the start point. + * @param {number} y1 - The y-coordinate of the start point. + * @param {number} x2 - The x-coordinate of the end point. + * @param {number} y2 - The y-coordinate of the end point. + */ +export function line(x1, y1, x2, y2) { + _ensureReady(); + let d = R.dist(x1, y1, x2, y2); + if (d == 0) return; + B.initializeDrawingState(x1, y1, d, false, false); + let angle = _calculateAngle(x1, y1, x2, y2); + B.draw(angle, false); +} - /** - * Draws a predefined shape/plot with a flowing brush stroke. - * @param {Object} p - An object representing the shape to draw. - * @param {number} x - The x-coordinate of the starting position to draw the shape. - * @param {number} y - The y-coordinate of the starting position to draw the shape. - * @param {number} scale - The scale at which to draw the shape. - */ - export function plot(p,x,y,scale) { - _ensureReady(); - B.initializeDrawingState(x, y, p.length, true, p); - B.draw(scale, true); - } +/** + * Draws a flow line with the current brush from a starting point in a specified direction. + * @param {number} x - The x-coordinate of the starting point. + * @param {number} y - The y-coordinate of the starting point. + * @param {number} length - The length of the line to draw. + * @param {number} dir - The direction in which to draw the line. Angles measured anticlockwise from the x-axis + */ +export function flowLine(x, y, length, dir) { + _ensureReady(); + B.initializeDrawingState(x, y, length, true, false); + B.draw(R.toDegrees(dir), false); +} + +/** + * Draws a predefined shape/plot with a flowing brush stroke. + * @param {Object} p - An object representing the shape to draw. + * @param {number} x - The x-coordinate of the starting position to draw the shape. + * @param {number} y - The y-coordinate of the starting position to draw the shape. + * @param {number} scale - The scale at which to draw the shape. + */ +export function plot(p, x, y, scale) { + _ensureReady(); + B.initializeDrawingState(x, y, p.length, true, p); + B.draw(scale, true); +} // ============================================================================= // Section: Loading Custom Image Tips @@ -1485,52 +1650,53 @@ * scheme, and integrate them into the p5.js graphics library. */ - /** - * A utility object for loading images, converting them to a red tint, and managing their states. - */ - const T = { - tips: new Map(), - - /** - * Adds an image to the tips Map and sets up loading and processing. - * - * @param {string} src - The source URL of the image to be added and processed. - */ - add (src) { - // Initially set the source as not processed - this.tips.set(src,false) - }, - - /** - * Converts the given image to a white tint by setting all color channels to white and adjusting the alpha channel. - * - * @param {Image} image - The image to be converted. - */ - imageToWhite (image) { - image.loadPixels() - // Modify the image data to create a white tint effect - for (let i = 0; i < 4 * image.width * image.height; i += 4) { - // Calculate the average for the grayscale value - let average = (image.pixels[i] + image.pixels[i + 1] + image.pixels[i + 2]) / 3; - // Set all color channels to white - image.pixels[i] = image.pixels[i + 1] = image.pixels[i + 2] = 255; - // Adjust the alpha channel to the inverse of the average, creating the white tint effect - image.pixels[i + 3] = 255 - average; - } - image.updatePixels() - }, - /** - * Loads all processed images into the p5.js environment. - * If no images are in the tips Map, logs a warning message. - */ - load() { - for (let key of this.tips.keys()){ - let _r = _isInstanced ? _inst : window.self; - let image = _r.loadImage(key, () => T.imageToWhite(image)) - this.tips.set(key, image) - } - } - } +/** + * A utility object for loading images, converting them to a red tint, and managing their states. + */ +const T = { + tips: new Map(), + + /** + * Adds an image to the tips Map and sets up loading and processing. + * + * @param {string} src - The source URL of the image to be added and processed. + */ + add(src) { + // Initially set the source as not processed + this.tips.set(src, false); + }, + + /** + * Converts the given image to a white tint by setting all color channels to white and adjusting the alpha channel. + * + * @param {Image} image - The image to be converted. + */ + imageToWhite(image) { + image.loadPixels(); + // Modify the image data to create a white tint effect + for (let i = 0; i < 4 * image.width * image.height; i += 4) { + // Calculate the average for the grayscale value + let average = + (image.pixels[i] + image.pixels[i + 1] + image.pixels[i + 2]) / 3; + // Set all color channels to white + image.pixels[i] = image.pixels[i + 1] = image.pixels[i + 2] = 255; + // Adjust the alpha channel to the inverse of the average, creating the white tint effect + image.pixels[i + 3] = 255 - average; + } + image.updatePixels(); + }, + /** + * Loads all processed images into the p5.js environment. + * If no images are in the tips Map, logs a warning message. + */ + load() { + for (let key of this.tips.keys()) { + let _r = _isInstanced ? _inst : window.self; + let image = _r.loadImage(key, () => T.imageToWhite(image)); + this.tips.set(key, image); + } + }, +}; // ============================================================================= // Section: Hatching @@ -1540,144 +1706,179 @@ * Hatching involves drawing closely spaced parallel lines. */ - /** - * Activates hatching for subsequent geometries, with the given params. - * @param {number} dist - The distance between hatching lines. - * @param {number} angle - The angle at which hatching lines are drawn. - * @param {Object} options - An object containing optional parameters to affect the hatching style: - * - rand: Introduces randomness to the line placement. - * - continuous: Connects the end of a line with the start of the next. - * - gradient: Changes the distance between lines to create a gradient effect. - * Defaults to {rand: false, continuous: false, gradient: false}. - */ - export function hatch(dist = 5, angle = 45, options = {rand: false, continuous: false, gradient: false}) { - H.isActive = true; - H.hatchingParams = [dist, angle, options] - } +/** + * Activates hatching for subsequent geometries, with the given params. + * @param {number} dist - The distance between hatching lines. + * @param {number} angle - The angle at which hatching lines are drawn. + * @param {Object} options - An object containing optional parameters to affect the hatching style: + * - rand: Introduces randomness to the line placement. + * - continuous: Connects the end of a line with the start of the next. + * - gradient: Changes the distance between lines to create a gradient effect. + * Defaults to {rand: false, continuous: false, gradient: false}. + */ +export function hatch( + dist = 5, + angle = 45, + options = { rand: false, continuous: false, gradient: false } +) { + H.isActive = true; + H.hatchingParams = [dist, angle, options]; +} - /** - * Sets the brush type, color, and weight for subsequent hatches. - * If this function is not called, hatches will use the parameters from stroke operations. - * @param {string} brushName - The name of the brush to set as current. - * @param {string|p5.Color} color - The color to set for the brush. - * @param {number} weight - The weight (size) to set for the brush. - */ - export function setHatch(brush, color = "black", weight = 1) { - H.hatchingBrush = [brush, color, weight] - } +/** + * Sets the brush type, color, and weight for subsequent hatches. + * If this function is not called, hatches will use the parameters from stroke operations. + * @param {string} brushName - The name of the brush to set as current. + * @param {string|p5.Color} color - The color to set for the brush. + * @param {number} weight - The weight (size) to set for the brush. + */ +export function setHatch(brush, color = "black", weight = 1) { + H.hatchingBrush = [brush, color, weight]; +} - /** - * Disables hatching for subsequent shapes - */ - export function noHatch() { - H.isActive = false; - H.hatchingBrush = false; - } +/** + * Disables hatching for subsequent shapes + */ +export function noHatch() { + H.isActive = false; + H.hatchingBrush = false; +} - /** - * Object to hold the current hatch state and to perform hatch calculation - */ - const H = { - isActive: false, - hatchingParams: [5,45,{}], - hatchingBrush: false, - - /** - * Creates a hatching pattern across the given polygons. - * - * @param {Array|Object} polygons - A single polygon or an array of polygons to apply the hatching. - */ - hatch(polygons) { - - let dist = H.hatchingParams[0]; - let angle = H.hatchingParams[1]; - let options = H.hatchingParams[2]; - - // Save current stroke state - let strokeColor = B.c, strokeBrush = B.name, strokeWeight = B.w, strokeActive = B.isActive; - // Change state if hatch has been set to different params than stroke - if (H.hatchingBrush) set(H.hatchingBrush[0],H.hatchingBrush[1],H.hatchingBrush[2]) - - // Transform to degrees and between 0-180 - angle = R.toDegrees(angle) % 180; - - // Calculate the bounding area of the provided polygons - let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; - let processPolygonPoints = (p) => { - for (let a of p.a) { - // (process points of a single polygon to find bounding area) - minX = a[0] < minX ? a[0] : minX; - maxX = a[0] > maxX ? a[0] : maxX; - minY = a[1] < minY ? a[1] : minY; - maxY = a[1] > maxY ? a[1] : maxY; - } - }; - - // Ensure polygons is an array and find overall bounding area - if (!Array.isArray(polygons)) {polygons = [polygons]} - for (let p of polygons) {processPolygonPoints(p);} - - // Create a bounding polygon - let ventana = new Polygon([[minX,minY],[maxX,minY],[maxX,maxY],[minX,maxY]]) - - // Set initial values for line generation - let startY = (angle <= 90 && angle >= 0) ? minY : maxY; - let gradient = options.gradient ? R.map(options.gradient,0,1,1,1.1,true) : 1 - let dots = []; - let i = 0; - let dist1 = dist; - let linea = (i) => { - return { - point1 : { x: minX + dist1 * i * R.cos(-angle+90), y: startY + dist1 * i * R.sin(-angle+90) }, - point2 : { x: minX + dist1 * i * R.cos(-angle+90) + R.cos(-angle), y: startY + dist1 * i * R.sin(-angle+90) + R.sin(-angle) } - } - } - - // Generate lines and calculate intersections with polygons - // Loop through the lines based on the distance and angle to calculate intersections with the polygons - // The loop continues until a line does not intersect with the bounding window polygon - // Each iteration accounts for the gradient effect by adjusting the distance between lines - while (ventana.intersect(linea(i)).length > 0) { - let tempArray = []; - for (let p of polygons) {tempArray.push(p.intersect(linea(i)))}; - dots[i] = tempArray.flat().sort((a, b) => (a.x === b.x) ? (a.y - b.y) : (a.x - b.x)); - dist1 *= gradient - i++ - } - - // Filter out empty arrays to avoid drawing unnecessary lines - let gdots = [] - for (let dd of dots) {if (typeof dd[0] !== "undefined") { gdots.push(dd)} } - - // Draw the hatching lines using the calculated intersections - // If the 'rand' option is enabled, add randomness to the start and end points of the lines - // If the 'continuous' option is set, connect the end of one line to the start of the next - let r = options.rand ? options.rand : 0; - for (let j = 0; j < gdots.length; j++) { - let dd = gdots[j] - let shouldDrawContinuousLine = j > 0 && options.continuous; - for (let i = 0; i < dd.length-1; i += 2) { - if (r !== 0) { - dd[i].x += r * dist * R.random(-10, 10); - dd[i].y += r * dist * R.random(-10, 10); - dd[i + 1].x += r * dist * R.random(-10, 10); - dd[i + 1].y += r * dist * R.random(-10, 10); - } - line(dd[i].x, dd[i].y, dd[i + 1].x, dd[i + 1].y); - if (shouldDrawContinuousLine) { - line(gdots[j - 1][1].x, gdots[j - 1][1].y, dd[i].x, dd[i].y); - } - } - } - - // Change state back to previous - set(strokeBrush, strokeColor, strokeWeight) - B.isActive = strokeActive; +/** + * Object to hold the current hatch state and to perform hatch calculation + */ +const H = { + isActive: false, + hatchingParams: [5, 45, {}], + hatchingBrush: false, + + /** + * Creates a hatching pattern across the given polygons. + * + * @param {Array|Object} polygons - A single polygon or an array of polygons to apply the hatching. + */ + hatch(polygons) { + let dist = H.hatchingParams[0]; + let angle = H.hatchingParams[1]; + let options = H.hatchingParams[2]; + + // Save current stroke state + let strokeColor = B.c, + strokeBrush = B.name, + strokeWeight = B.w, + strokeActive = B.isActive; + // Change state if hatch has been set to different params than stroke + if (H.hatchingBrush) + set(H.hatchingBrush[0], H.hatchingBrush[1], H.hatchingBrush[2]); + + // Transform to degrees and between 0-180 + angle = R.toDegrees(angle) % 180; + + // Calculate the bounding area of the provided polygons + let minX = Infinity, + maxX = -Infinity, + minY = Infinity, + maxY = -Infinity; + let processPolygonPoints = (p) => { + for (let a of p.a) { + // (process points of a single polygon to find bounding area) + minX = a[0] < minX ? a[0] : minX; + maxX = a[0] > maxX ? a[0] : maxX; + minY = a[1] < minY ? a[1] : minY; + maxY = a[1] > maxY ? a[1] : maxY; + } + }; + + // Ensure polygons is an array and find overall bounding area + if (!Array.isArray(polygons)) { + polygons = [polygons]; + } + for (let p of polygons) { + processPolygonPoints(p); + } + + // Create a bounding polygon + let ventana = new Polygon([ + [minX, minY], + [maxX, minY], + [maxX, maxY], + [minX, maxY], + ]); + + // Set initial values for line generation + let startY = angle <= 90 && angle >= 0 ? minY : maxY; + let gradient = options.gradient + ? R.map(options.gradient, 0, 1, 1, 1.1, true) + : 1; + let dots = []; + let i = 0; + let dist1 = dist; + let linea = (i) => { + return { + point1: { + x: minX + dist1 * i * R.cos(-angle + 90), + y: startY + dist1 * i * R.sin(-angle + 90), + }, + point2: { + x: minX + dist1 * i * R.cos(-angle + 90) + R.cos(-angle), + y: startY + dist1 * i * R.sin(-angle + 90) + R.sin(-angle), + }, + }; + }; + + // Generate lines and calculate intersections with polygons + // Loop through the lines based on the distance and angle to calculate intersections with the polygons + // The loop continues until a line does not intersect with the bounding window polygon + // Each iteration accounts for the gradient effect by adjusting the distance between lines + while (ventana.intersect(linea(i)).length > 0) { + let tempArray = []; + for (let p of polygons) { + tempArray.push(p.intersect(linea(i))); + } + dots[i] = tempArray + .flat() + .sort((a, b) => (a.x === b.x ? a.y - b.y : a.x - b.x)); + dist1 *= gradient; + i++; + } + + // Filter out empty arrays to avoid drawing unnecessary lines + let gdots = []; + for (let dd of dots) { + if (typeof dd[0] !== "undefined") { + gdots.push(dd); + } + } + + // Draw the hatching lines using the calculated intersections + // If the 'rand' option is enabled, add randomness to the start and end points of the lines + // If the 'continuous' option is set, connect the end of one line to the start of the next + let r = options.rand ? options.rand : 0; + for (let j = 0; j < gdots.length; j++) { + let dd = gdots[j]; + let shouldDrawContinuousLine = j > 0 && options.continuous; + for (let i = 0; i < dd.length - 1; i += 2) { + if (r !== 0) { + dd[i].x += r * dist * R.random(-10, 10); + dd[i].y += r * dist * R.random(-10, 10); + dd[i + 1].x += r * dist * R.random(-10, 10); + dd[i + 1].y += r * dist * R.random(-10, 10); + } + line(dd[i].x, dd[i].y, dd[i + 1].x, dd[i + 1].y); + if (shouldDrawContinuousLine) { + line(gdots[j - 1][1].x, gdots[j - 1][1].y, dd[i].x, dd[i].y); } + } } - export const hatchArray = H.hatch; - + // Change state back to previous + set(strokeBrush, strokeColor, strokeWeight); + B.isActive = strokeActive; + }, +}; + +export const hatchArray = H.hatch; + // ============================================================================= // Section: Polygon management. Basic geometries // ============================================================================= @@ -1688,146 +1889,156 @@ * to draw rectangles with options for randomness and different drawing modes. */ - /** - * Represents a polygon with a set of vertices. - */ - export class Polygon { - - /** - * Constructs the Polygon object from an array of points. - * - * @param {Array} pointsArray - An array of points, where each point is an array of two numbers [x, y]. - */ - constructor (array , bool = false) { - this.a = array; - this.vertices = array.map(a => ({ x: a[0], y: a[1] })); - if (bool) this.vertices = array; - this.sides = this.vertices.map((v, i, arr) => [v, arr[(i + 1) % arr.length]]); - } - /** - * Intersects a given line with the polygon, returning all intersection points. - * - * @param {Object} line - The line to intersect with the polygon, having two properties 'point1' and 'point2'. - * @returns {Array} An array of intersection points (each with 'x' and 'y' properties) or an empty array if no intersections. - */ - intersect (line) { - // Check if the result has been cached - let cacheKey = `${line.point1.x},${line.point1.y}-${line.point2.x},${line.point2.y}`; - if (this._intersectionCache && this._intersectionCache[cacheKey]) { - return this._intersectionCache[cacheKey]; - } - let points = [] - for (let s of this.sides) { - let intersection = _intersectLines(line.point1,line.point2,s[0],s[1]) - if (intersection !== false) {points.push(intersection)} - } - // Cache the result - if (!this._intersectionCache) this._intersectionCache = {}; - this._intersectionCache[cacheKey] = points; - - return points; - } - /** - * Draws the polygon by iterating over its sides and drawing lines between the vertices. - */ - draw (_brush = false, _color, _weight) { - let curState = B.isActive; - if (_brush) set(_brush, _color, _weight) - if (B.isActive) { - _ensureReady(); - for (let s of this.sides) {line(s[0].x,s[0].y,s[1].x,s[1].y)} - } - B.isActive = curState; - } - /** - * Fills the polygon using the current fill state. - */ - fill (_color = false, _opacity, _bleed, _texture, _border, _direction) { - let curState = F.isActive; - if (_color) { - fill(_color, _opacity) - bleed(_bleed, _direction) - fillTexture(_texture, _border) - } - if (F.isActive) { - _ensureReady(); - F.fill(this); - } - F.isActive = curState; - } - /** - * Creates hatch lines across the polygon based on a given distance and angle. - */ - hatch (_dist = false, _angle, _options) { - let curState = H.isActive; - if (_dist) hatch(_dist, _angle, _options) - if (H.isActive) { - _ensureReady(); - H.hatch(this) - } - H.isActive = curState; - } - - erase (c = false, a = E.a) { - if (E.isActive || c) { - Mix.masks[2].push() - Mix.masks[2].noStroke() - let ccc = _r.color(c ? c : E.c) - ccc.setAlpha(a) - Mix.masks[2].fill(ccc) - Mix.masks[2].beginShape() - for (let p of this.vertices) { - Mix.masks[2].vertex(p.x,p.y) - } - Mix.masks[2].endShape(_r.CLOSE) - Mix.masks[2].pop() - } - } - - show () { - this.fill(); - this.hatch(); - this.draw(); - this.erase(); - } - } - - /** - * Creates a Polygon from a given array of points and performs drawing and filling - * operations based on active states. - * - * @param {Array} pointsArray - An array of points where each point is an array of two numbers [x, y]. - */ - export function polygon(pointsArray) { - // Create a new Polygon instance - let polygon = new Polygon(pointsArray); - polygon.show() - } +/** + * Represents a polygon with a set of vertices. + */ +export class Polygon { + /** + * Constructs the Polygon object from an array of points. + * + * @param {Array} pointsArray - An array of points, where each point is an array of two numbers [x, y]. + */ + constructor(array, bool = false) { + this.a = array; + this.vertices = array.map((a) => ({ x: a[0], y: a[1] })); + if (bool) this.vertices = array; + this.sides = this.vertices.map((v, i, arr) => [ + v, + arr[(i + 1) % arr.length], + ]); + } + /** + * Intersects a given line with the polygon, returning all intersection points. + * + * @param {Object} line - The line to intersect with the polygon, having two properties 'point1' and 'point2'. + * @returns {Array} An array of intersection points (each with 'x' and 'y' properties) or an empty array if no intersections. + */ + intersect(line) { + // Check if the result has been cached + let cacheKey = `${line.point1.x},${line.point1.y}-${line.point2.x},${line.point2.y}`; + if (this._intersectionCache && this._intersectionCache[cacheKey]) { + return this._intersectionCache[cacheKey]; + } + let points = []; + for (let s of this.sides) { + let intersection = _intersectLines(line.point1, line.point2, s[0], s[1]); + if (intersection !== false) { + points.push(intersection); + } + } + // Cache the result + if (!this._intersectionCache) this._intersectionCache = {}; + this._intersectionCache[cacheKey] = points; + + return points; + } + /** + * Draws the polygon by iterating over its sides and drawing lines between the vertices. + */ + draw(_brush = false, _color, _weight) { + let curState = B.isActive; + if (_brush) set(_brush, _color, _weight); + if (B.isActive) { + _ensureReady(); + for (let s of this.sides) { + line(s[0].x, s[0].y, s[1].x, s[1].y); + } + } + B.isActive = curState; + } + /** + * Fills the polygon using the current fill state. + */ + fill(_color = false, _opacity, _bleed, _texture, _border, _direction) { + let curState = F.isActive; + if (_color) { + fill(_color, _opacity); + bleed(_bleed, _direction); + fillTexture(_texture, _border); + } + if (F.isActive) { + _ensureReady(); + F.fill(this); + } + F.isActive = curState; + } + /** + * Creates hatch lines across the polygon based on a given distance and angle. + */ + hatch(_dist = false, _angle, _options) { + let curState = H.isActive; + if (_dist) hatch(_dist, _angle, _options); + if (H.isActive) { + _ensureReady(); + H.hatch(this); + } + H.isActive = curState; + } + + erase(c = false, a = E.a) { + if (E.isActive || c) { + Mix.masks[2].push(); + Mix.masks[2].noStroke(); + let ccc = _r.color(c ? c : E.c); + ccc.setAlpha(a); + Mix.masks[2].fill(ccc); + Mix.masks[2].beginShape(); + for (let p of this.vertices) { + Mix.masks[2].vertex(p.x, p.y); + } + Mix.masks[2].endShape(_r.CLOSE); + Mix.masks[2].pop(); + } + } + + show() { + this.fill(); + this.hatch(); + this.draw(); + this.erase(); + } +} - /** - * Draws a rectangle on the canvas and fills it with the current fill color. - * - * @param {number} x - The x-coordinate of the rectangle. - * @param {number} y - The y-coordinate of the rectangle. - * @param {number} w - The width of the rectangle. - * @param {number} h - The height of the rectangle. - * @param {boolean} [mode=CORNER] - If CENTER, the rectangle is drawn centered at (x, y). - */ - export function rect(x,y,w,h,mode = _r.CORNER) { - if (mode == _r.CENTER) x = x - w / 2, y = y - h / 2; - if (FF.isActive) { - beginShape(0); - vertex(x,y); - vertex(x+w,y); - vertex(x+w,y+h); - vertex(x,y+h); - endShape(_r.CLOSE) - } else { - let p = new Polygon([[x,y],[x+w,y],[x+w,y+h],[x,y+h]]) - p.show() - } - } +/** + * Creates a Polygon from a given array of points and performs drawing and filling + * operations based on active states. + * + * @param {Array} pointsArray - An array of points where each point is an array of two numbers [x, y]. + */ +export function polygon(pointsArray) { + // Create a new Polygon instance + let polygon = new Polygon(pointsArray); + polygon.show(); +} +/** + * Draws a rectangle on the canvas and fills it with the current fill color. + * + * @param {number} x - The x-coordinate of the rectangle. + * @param {number} y - The y-coordinate of the rectangle. + * @param {number} w - The width of the rectangle. + * @param {number} h - The height of the rectangle. + * @param {boolean} [mode=CORNER] - If CENTER, the rectangle is drawn centered at (x, y). + */ +export function rect(x, y, w, h, mode = _r.CORNER) { + if (mode == _r.CENTER) (x = x - w / 2), (y = y - h / 2); + if (FF.isActive) { + beginShape(0); + vertex(x, y); + vertex(x + w, y); + vertex(x + w, y + h); + vertex(x, y + h); + endShape(_r.CLOSE); + } else { + let p = new Polygon([ + [x, y], + [x + w, y], + [x + w, y + h], + [x, y + h], + ]); + p.show(); + } +} // ============================================================================= // Section: Shape, Stroke, and Spline. Plot System @@ -1836,435 +2047,485 @@ * This section defines the functionality for creating and managing plots, which are used to draw complex shapes, * strokes, and splines on a canvas. It includes classes and functions to create plots of type "curve" or "segments", * manipulate them with operations like adding segments and applying rotations, and render them as visual elements - * like polygons or strokes. The spline functionality allows for smooth curve creation using control points with + * like polygons or strokes. The spline functionality allows for smooth curve creation using control points with * specified curvature, which can be rendered directly or used as part of more complex drawings. */ - /** - * The Plot class is central to the plot system, serving as a blueprint for creating and manipulating a variety - * of shapes and paths. It manages a collection of segments, each defined by an angle, length, and pressure, - * allowing for intricate designs such as curves and custom strokes. Plot instances can be transformed by rotation, - * and their visual representation can be controlled through pressure and angle calculations along their length. - */ - export class Plot { - - /** - * Creates a new Plot. - * @param {string} _type - The type of plot, "curve" or "segments" - */ - constructor (_type) { - this.segments = [], this.angles = [], this.pres = []; - this.type = _type; - this.dir = 0; - this.calcIndex(0); - this.pol = false; - } - - /** - * Adds a segment to the plot with specified angle, length, and pressure. - * @param {number} _a - The angle of the segment. - * @param {number} _length - The length of the segment. - * @param {number} _pres - The pressure of the segment. - * @param {boolean} _degrees - Whether the angle is in degrees. - */ - addSegment (_a = 0,_length = 0,_pres = 1,_degrees = false) { - // Remove the last angle if the angles array is not empty - if (this.angles.length > 0) { - this.angles.splice(-1) - } - // Convert to degrees and normalize between 0 and 360 degrees - _a = (_degrees) ? (_a % 360 + 360) % 360 : R.toDegrees(_a); - // Store the angle, pressure, and segment length - this.angles.push(_a); - this.pres.push(_pres); - this.segments.push(_length); - // Calculate the total length of the plot - this.length = this.segments.reduce((partialSum, a) => partialSum + a, 0); - // Push the angle again to prepare for the next segment - this.angles.push(_a) - } - - /** - * Finalizes the plot by setting the last angle and pressure. - * @param {number} _a - The final angle of the plot. - * @param {number} _pres - The final pressure of the plot. - * @param {boolean} _degrees - Whether the angle is in degrees. - */ - endPlot (_a = 0, _pres = 1, _degrees = false) { - // Convert angle to degrees if necessary - _a = (_degrees) ? (_a % 360 + 360) % 360 : R.toDegrees(_a); - // Replace the last angle with the final angle - this.angles.splice(-1) - this.angles.push(_a); - // Store the final pressure - this.pres.push(_pres); - } - - /** - * Rotates the entire plot by a given angle. - * @param {number} _a - The angle to rotate the plot. - */ - rotate (_a) { - this.dir = R.toDegrees(_a); - } - - /** - * Calculates the pressure at a given distance along the plot. - * @param {number} _d - The distance along the plot. - * @returns {number} - The calculated pressure. - */ - pressure (_d) { - // If the distance exceeds the plot length, return the last pressure - if (_d > this.length) return this.pres[this.pres.length-1]; - // Otherwise, calculate the pressure using the curving function - return this.curving(this.pres,_d); - } - - /** - * Calculates the angle at a given distance along the plot. - * @param {number} _d - The distance along the plot. - * @returns {number} - The calculated angle. - */ - angle (_d) { - // If the distance exceeds the plot length, return the last angle - if (_d > this.length) return this.angles[this.angles.length-1]; - // Calculate the index for the given distance - this.calcIndex(_d); - // Return the angle, adjusted for the plot type and direction - return (this.type === "curve") ? - this.curving(this.angles, _d) + this.dir : - this.angles[this.index] + this.dir; - } +/** + * The Plot class is central to the plot system, serving as a blueprint for creating and manipulating a variety + * of shapes and paths. It manages a collection of segments, each defined by an angle, length, and pressure, + * allowing for intricate designs such as curves and custom strokes. Plot instances can be transformed by rotation, + * and their visual representation can be controlled through pressure and angle calculations along their length. + */ +export class Plot { + /** + * Creates a new Plot. + * @param {string} _type - The type of plot, "curve" or "segments" + */ + constructor(_type) { + (this.segments = []), (this.angles = []), (this.pres = []); + this.type = _type; + this.dir = 0; + this.calcIndex(0); + this.pol = false; + } + + /** + * Adds a segment to the plot with specified angle, length, and pressure. + * @param {number} _a - The angle of the segment. + * @param {number} _length - The length of the segment. + * @param {number} _pres - The pressure of the segment. + * @param {boolean} _degrees - Whether the angle is in degrees. + */ + addSegment(_a = 0, _length = 0, _pres = 1, _degrees = false) { + // Remove the last angle if the angles array is not empty + if (this.angles.length > 0) { + this.angles.splice(-1); + } + // Convert to degrees and normalize between 0 and 360 degrees + _a = _degrees ? ((_a % 360) + 360) % 360 : R.toDegrees(_a); + // Store the angle, pressure, and segment length + this.angles.push(_a); + this.pres.push(_pres); + this.segments.push(_length); + // Calculate the total length of the plot + this.length = this.segments.reduce((partialSum, a) => partialSum + a, 0); + // Push the angle again to prepare for the next segment + this.angles.push(_a); + } + + /** + * Finalizes the plot by setting the last angle and pressure. + * @param {number} _a - The final angle of the plot. + * @param {number} _pres - The final pressure of the plot. + * @param {boolean} _degrees - Whether the angle is in degrees. + */ + endPlot(_a = 0, _pres = 1, _degrees = false) { + // Convert angle to degrees if necessary + _a = _degrees ? ((_a % 360) + 360) % 360 : R.toDegrees(_a); + // Replace the last angle with the final angle + this.angles.splice(-1); + this.angles.push(_a); + // Store the final pressure + this.pres.push(_pres); + } + + /** + * Rotates the entire plot by a given angle. + * @param {number} _a - The angle to rotate the plot. + */ + rotate(_a) { + this.dir = R.toDegrees(_a); + } + + /** + * Calculates the pressure at a given distance along the plot. + * @param {number} _d - The distance along the plot. + * @returns {number} - The calculated pressure. + */ + pressure(_d) { + // If the distance exceeds the plot length, return the last pressure + if (_d > this.length) return this.pres[this.pres.length - 1]; + // Otherwise, calculate the pressure using the curving function + return this.curving(this.pres, _d); + } + + /** + * Calculates the angle at a given distance along the plot. + * @param {number} _d - The distance along the plot. + * @returns {number} - The calculated angle. + */ + angle(_d) { + // If the distance exceeds the plot length, return the last angle + if (_d > this.length) return this.angles[this.angles.length - 1]; + // Calculate the index for the given distance + this.calcIndex(_d); + // Return the angle, adjusted for the plot type and direction + return this.type === "curve" + ? this.curving(this.angles, _d) + this.dir + : this.angles[this.index] + this.dir; + } + + /** + * Interpolates values between segments for smooth transitions. + * @param {Array} array - The array to interpolate within. + * @param {number} _d - The distance along the plot. + * @returns {number} - The interpolated value. + */ + curving(array, _d) { + let map0 = array[this.index]; + let map1 = array[this.index + 1]; + if (typeof map1 == "undefined") { + map1 = map0; + } + if (Math.abs(map1 - map0) > 180) { + if (map1 > map0) { + map1 = -(360 - map1); + } else { + map0 = -(360 - map0); + } + } + return R.map( + _d - this.suma, + 0, + this.segments[this.index], + map0, + map1, + true + ); + } + + /** + * Calculates the current index of the plot based on the distance. + * @param {number} _d - The distance along the plot. + */ + calcIndex(_d) { + (this.index = -1), (this.suma = 0); + let d = 0; + while (d <= _d) { + this.suma = d; + d += this.segments[this.index + 1]; + this.index++; + } + return this.index; + } + + /** + * Generates a polygon based on the plot. + * @param {number} _x - The x-coordinate for the starting point of the polygon. + * @param {number} _y - The y-coordinate for the starting point of the polygon. + * @returns {Polygon} - The generated polygon. + */ + genPol(_x, _y, _scale = 1, isHatch = false) { + _ensureReady(); // Ensure that the drawing environment is prepared + const step = 0.5; + const vertices = []; + const numSteps = Math.round(this.length / step); + const pos = new Position(_x, _y); + let side = isHatch ? 0.15 : F.bleed_strength * 3; + let pside = 0; + let prevIdx = 0; + for (let i = 0; i < numSteps; i++) { + pos.plotTo(this, step, step, 1); + let idx = this.calcIndex(pos.plotted); + pside += step; + if ( + (pside >= this.segments[idx] * side * R.random(0.7, 1.3) || + idx >= prevIdx) && + pos.x + ) { + vertices.push([pos.x, pos.y]); + pside = 0; + if (idx >= prevIdx) prevIdx++; + } + } + return new Polygon(vertices); + } + + /** + * Draws the plot on the canvas. + * @param {number} x - The x-coordinate to draw at. + * @param {number} y - The y-coordinate to draw at. + * @param {number} scale - The scale to draw with. + */ + draw(x, y, scale) { + if (B.isActive) { + _ensureReady(); // Ensure that the drawing environment is prepared + if (this.origin) (x = this.origin[0]), (y = this.origin[1]), (scale = 1); + plot(this, x, y, scale); + } + } + + /** + * Fill the plot on the canvas. + * @param {number} x - The x-coordinate to draw at. + * @param {number} y - The y-coordinate to draw at. + */ + fill(x, y, scale) { + if (F.isActive) { + _ensureReady(); // Ensure that the drawing environment is prepared + if (this.origin) (x = this.origin[0]), (y = this.origin[1]), (scale = 1); + this.pol = this.genPol(x, y, scale); + this.pol.fill(); + } + } + + /** + * Hatch the plot on the canvas. + * @param {number} x - The x-coordinate to draw at. + * @param {number} y - The y-coordinate to draw at. + */ + hatch(x, y, scale) { + if (H.isActive) { + _ensureReady(); // Ensure that the drawing environment is prepared + if (this.origin) (x = this.origin[0]), (y = this.origin[1]), (scale = 1); + this.pol = this.genPol(x, y, scale, true); + this.pol.hatch(); + } + } + + erase(x, y, scale) { + if (E.isActive) { + if (this.origin) (x = this.origin[0]), (y = this.origin[1]), (scale = 1); + this.pol = this.genPol(x, y, scale, true); + Mix.masks[2].push(); + Mix.masks[2].noStroke(); + let ccc = _r.color(E.c); + ccc.setAlpha(E.a); + Mix.masks[2].fill(ccc); + Mix.masks[2].beginShape(); + for (let p of this.pol.vertices) { + Mix.masks[2].vertex(p.x, p.y); + } + Mix.masks[2].endShape(_r.CLOSE); + Mix.masks[2].pop(); + } + } + + show(x, y, scale = 1) { + this.draw(x, y, scale); + this.fill(x, y, scale); + this.hatch(x, y, scale); + this.erase(x, y, scale); + } +} - /** - * Interpolates values between segments for smooth transitions. - * @param {Array} array - The array to interpolate within. - * @param {number} _d - The distance along the plot. - * @returns {number} - The interpolated value. - */ - curving (array,_d) { - let map0 = array[this.index]; - let map1 = array[this.index+1]; - if (typeof map1 == "undefined") { map1 = map0} - if (Math.abs(map1-map0) > 180) {if (map1 > map0) {map1 = - (360-map1);} else {map0 = - (360-map0);}} - return R.map(_d-this.suma,0,this.segments[this.index],map0,map1,true); - } +/** + * Draws a circle on the canvas and fills it with the current fill color. + * + * @param {number} x - The x-coordinate of the center of the circle. + * @param {number} y - The y-coordinate of the center of the circle. + * @param {number} radius - The radius of the circle. + * @param {boolean} [r=false] - If true, applies a random factor to the radius for each segment. + */ +export function circle(x, y, radius, r = false) { + _ensureReady(); + // Create a new Plot instance for a curve shape + let p = new Plot("curve"); + // Calculate the length of the arc for each quarter of the circle + let l = (Math.PI * radius) / 2; + // Initialize the angle for the drawing segments + let angle = R.random(0, 360); + // Define a function to optionally add randomness to the segment length + let rr = () => (r ? R.random(-1, 1) : 0); + // Add segments for each quarter of the circle with optional randomness + p.addSegment(0 + angle + rr(), l + rr(), 1, true); + p.addSegment(-90 + angle + rr(), l + rr(), 1, true); + p.addSegment(-180 + angle + rr(), l + rr(), 1, true); + p.addSegment(-270 + angle + rr(), l + rr(), 1, true); + // Optionally add a random final angle for the last segment + let angle2 = r ? R.randInt(-5, 5) : 0; + if (r) p.addSegment(0 + angle, angle2 * (Math.PI / 180) * radius, true); + // Finalize the plot + p.endPlot(angle2 + angle, 1, true); + // Fill / hatch / draw + let o = [x - radius * R.sin(angle), y - radius * R.cos(-angle)]; + p.show(o[0], o[1], 1); +} + +export function arc(x, y, radius, start, end) { + _ensureReady(); + // Create a new Plot instance for a curve shape + let p = new Plot("curve"); + // Calculate start angle and end angle + let a1 = 270 - R.toDegrees(start), + a2 = 270 - R.toDegrees(end); + // Calculate length arc + let arcAngle = R.toDegrees(end - start); + let l = (Math.PI * radius * arcAngle) / 180; + // Add segments to plot + p.addSegment(a1, l, 1, true); + p.endPlot(a2, 1, true); + // Draw from starting point + p.draw(x + radius * R.cos(-a1 - 90), y + radius * R.sin(-a1 - 90), 1); +} + +// Holds the array of vertices for the custom shape being defined. Each vertex includes position and optional pressure. +let _strokeArray = false; +// Holds options for the stroke, such as curvature, that can influence the shape's rendering. +let _strokeOption; - /** - * Calculates the current index of the plot based on the distance. - * @param {number} _d - The distance along the plot. - */ - calcIndex(_d) { - this.index = -1, this.suma = 0; - let d = 0; - while (d <= _d) {this.suma = d; d += this.segments[this.index+1]; this.index++;} - return this.index - } +/** + * Starts recording vertices for a custom shape. Optionally, a curvature can be defined. + * @param {number} [curvature] - From 0 to 1. Defines the curvature for the vertices being recorded (optional). + */ +export function beginShape(curvature = 0) { + _strokeOption = R.constrain(curvature, 0, 1); // Set the curvature option for the shape + _strokeArray = []; // Initialize the array to store vertices +} - /** - * Generates a polygon based on the plot. - * @param {number} _x - The x-coordinate for the starting point of the polygon. - * @param {number} _y - The y-coordinate for the starting point of the polygon. - * @returns {Polygon} - The generated polygon. - */ - genPol (_x,_y,_scale = 1, isHatch = false) { - _ensureReady(); // Ensure that the drawing environment is prepared - const step = 0.5; - const vertices = [] - const numSteps = Math.round(this.length/step); - const pos = new Position(_x,_y) - let side = isHatch ? 0.15 : F.bleed_strength * 3; - let pside = 0; - let prevIdx = 0 - for (let i = 0; i < numSteps; i++) { - pos.plotTo(this, step, step, 1) - let idx = this.calcIndex(pos.plotted) - pside += step; - if ((pside >= this.segments[idx] * side * R.random(0.7,1.3) || idx >= prevIdx) && pos.x) { - vertices.push([pos.x,pos.y]) - pside = 0; - if (idx >= prevIdx) prevIdx++ - } - } - return new Polygon(vertices); - } +/** + * Records a vertex in the custom shape being defined between beginShape and endShape. + * @param {number} x - The x-coordinate of the vertex. + * @param {number} y - The y-coordinate of the vertex. + * @param {number} [pressure] - The pressure at the vertex (optional). + */ +export function vertex(x, y, pressure) { + _strokeArray.push([x, y, pressure]); // Add the vertex to the array +} - /** - * Draws the plot on the canvas. - * @param {number} x - The x-coordinate to draw at. - * @param {number} y - The y-coordinate to draw at. - * @param {number} scale - The scale to draw with. - */ - draw (x, y, scale) { - if (B.isActive) { - _ensureReady(); // Ensure that the drawing environment is prepared - if (this.origin) x = this.origin[0], y = this.origin[1], scale = 1; - plot(this,x,y,scale) - } - } +/** + * Finishes recording vertices for a custom shape and either closes it or leaves it open. + * It also triggers the drawing of the shape with the active stroke(), fill() and hatch() states. + * @param {string} [a] - An optional argument to close the shape if set to _r.CLOSE. + */ +export function endShape(a) { + _ensureReady(); + if (a === _r.CLOSE) { + _strokeArray.push(_strokeArray[0]); // Close the shape by connecting the last vertex to the first + _strokeArray.push(_strokeArray[1]); + } + // Create a new Plot with the recorded vertices and curvature option + let plot = + _strokeOption == 0 && !FF.isActive + ? new Polygon(_strokeArray) + : _createSpline( + _strokeArray, + _strokeOption, + a === _r.CLOSE ? true : false + ); + plot.show(); + _strokeArray = false; // Clear the array after the shape has been drawn +} - /** - * Fill the plot on the canvas. - * @param {number} x - The x-coordinate to draw at. - * @param {number} y - The y-coordinate to draw at. - */ - fill (x, y, scale) { - if (F.isActive) { - _ensureReady(); // Ensure that the drawing environment is prepared - if (this.origin) x = this.origin[0], y = this.origin[1], scale = 1; - this.pol = this.genPol(x, y, scale) - this.pol.fill() - } - } +/** + * Begins a new stroke with a given type and starting position. This initializes + * a new Plot to record the stroke's path. + * @param {string} type - The type of the stroke, which defines the kind of Plot to create. + * @param {number} x - The x-coordinate of the starting point of the stroke. + * @param {number} y - The y-coordinate of the starting point of the stroke. + */ +export function beginStroke(type, x, y) { + _strokeOption = [x, y]; // Store the starting position for later use + _strokeArray = new Plot(type); // Initialize a new Plot with the specified type +} - /** - * Hatch the plot on the canvas. - * @param {number} x - The x-coordinate to draw at. - * @param {number} y - The y-coordinate to draw at. - */ - hatch (x, y, scale) { - if (H.isActive) { - _ensureReady(); // Ensure that the drawing environment is prepared - if (this.origin) x = this.origin[0], y = this.origin[1], scale = 1; - this.pol = this.genPol(x, y, scale, true) - this.pol.hatch() - } - } +/** + * Adds a segment to the stroke with a given angle, length, and pressure. This function + * is called between beginStroke and endStroke to define the stroke's path. + * @param {number} angle - The initial angle of the segment, relative to the canvas. + * @param {number} length - The length of the segment. + * @param {number} pressure - The pressure at the start of the segment, affecting properties like width. + */ +export function segment(angle, length, pressure) { + _strokeArray.addSegment(angle, length, pressure); // Add the new segment to the Plot +} - erase(x, y, scale) { - if (E.isActive) { - if (this.origin) x = this.origin[0], y = this.origin[1], scale = 1; - this.pol = this.genPol(x, y, scale, true) - Mix.masks[2].push() - Mix.masks[2].noStroke() - let ccc = _r.color(E.c) - ccc.setAlpha(E.a) - Mix.masks[2].fill(ccc) - Mix.masks[2].beginShape() - for (let p of this.pol.vertices) { - Mix.masks[2].vertex(p.x,p.y) - } - Mix.masks[2].endShape(_r.CLOSE) - Mix.masks[2].pop() - } - } +/** + * Completes the stroke path and triggers the rendering of the stroke. + * @param {number} angle - The angle of the curve at the last point of the stroke path. + * @param {number} pressure - The pressure at the end of the stroke. + */ +export function endStroke(angle, pressure) { + _strokeArray.endPlot(angle, pressure); // Finalize the Plot with the end angle and pressure + _strokeArray.draw(_strokeOption[0], _strokeOption[1], 1); // Draw the stroke using the stored starting position + _strokeArray = false; // Clear the _strokeArray to indicate the end of this stroke +} - show(x, y, scale = 1) { - this.draw(x, y, scale) - this.fill(x, y, scale) - this.hatch(x, y, scale) - this.erase(x, y, scale) +/** + * Creates a new Plot object. + * @param {Array>} array_points - An array of points defining the spline curve. + * @param {number} [curvature=0.5] - The curvature of the spline curve, beterrn 0 and 1. A curvature of 0 will create a series of straight segments. + */ +function _createSpline(array_points, curvature = 0.5, _close = false) { + // Initialize the plot type based on curvature + let plotType = curvature === 0 ? "segments" : "curve"; + let p = new Plot(plotType); + + // Proceed only if there are points provided + if (array_points && array_points.length > 0) { + // Add each segment to the plot + let done = 0; + let pep, tep, pep2; + for (let i = 0; i < array_points.length - 1; i++) { + if (curvature > 0 && i < array_points.length - 2) { + // Get the current and next points + let p1 = array_points[i], + p2 = array_points[i + 1], + p3 = array_points[i + 2]; + // Calculate distances and angles between points + let d1 = R.dist(p1[0], p1[1], p2[0], p2[1]), + d2 = R.dist(p2[0], p2[1], p3[0], p3[1]); + let a1 = _calculateAngle(p1[0], p1[1], p2[0], p2[1]), + a2 = _calculateAngle(p2[0], p2[1], p3[0], p3[1]); + // Calculate curvature based on the minimum distance + let dd = curvature * Math.min(Math.min(d1, d2), 0.5 * Math.min(d1, d2)), + dmax = Math.max(d1, d2); + let s1 = d1 - dd, + s2 = d2 - dd; + // If the angles are approximately the same, create a straight segment + if (Math.floor(a1) === Math.floor(a2)) { + let temp = _close ? (i === 0 ? 0 : d1 - done) : d1 - done; + let temp2 = _close ? (i === 0 ? 0 : d2 - pep2) : d2; + p.addSegment(a1, temp, p1[2], true); + if (i === array_points.length - 3) + p.addSegment(a2, temp2, p2[2], true); + done = 0; + if (i === 0) + (pep = d1), (pep2 = dd), (tep = array_points[1]), (done = 0); + } else { + // If the angles are not the same, create curves, etc (this is a too complex...) + let point1 = { + x: p2[0] - dd * R.cos(-a1), + y: p2[1] - dd * R.sin(-a1), + }; + let point2 = { + x: point1.x + dmax * R.cos(-a1 + 90), + y: point1.y + dmax * R.sin(-a1 + 90), + }; + let point3 = { + x: p2[0] + dd * R.cos(-a2), + y: p2[1] + dd * R.sin(-a2), + }; + let point4 = { + x: point3.x + dmax * R.cos(-a2 + 90), + y: point3.y + dmax * R.sin(-a2 + 90), + }; + let int = _intersectLines(point1, point2, point3, point4, true); + let radius = R.dist(point1.x, point1.y, int.x, int.y); + let disti = R.dist(point1.x, point1.y, point3.x, point3.y) / 2; + let a3 = 2 * Math.asin(disti / radius) * (180 / Math.PI); + let s3 = (2 * Math.PI * radius * a3) / 360; + let temp = _close ? (i === 0 ? 0 : s1 - done) : s1 - done; + let temp2 = + i === array_points.length - 3 ? (_close ? pep - dd : s2) : 0; + p.addSegment(a1, temp, p1[2], true); + p.addSegment(a1, isNaN(s3) ? 0 : s3, p1[2], true); + p.addSegment(a2, temp2, p2[2], true); + done = dd; + if (i === 0) (pep = s1), (pep2 = dd), (tep = [point1.x, point1.y]); } - } - - /** - * Draws a circle on the canvas and fills it with the current fill color. - * - * @param {number} x - The x-coordinate of the center of the circle. - * @param {number} y - The y-coordinate of the center of the circle. - * @param {number} radius - The radius of the circle. - * @param {boolean} [r=false] - If true, applies a random factor to the radius for each segment. - */ - export function circle(x,y,radius,r = false) { - _ensureReady(); - // Create a new Plot instance for a curve shape - let p = new Plot("curve") - // Calculate the length of the arc for each quarter of the circle - let l = Math.PI * radius / 2; - // Initialize the angle for the drawing segments - let angle = R.random(0,360) - // Define a function to optionally add randomness to the segment length - let rr = () => (r ? R.random(-1,1) : 0) - // Add segments for each quarter of the circle with optional randomness - p.addSegment(0 + angle + rr(), l + rr(), 1, true) - p.addSegment(-90 + angle + rr(), l + rr(), 1, true) - p.addSegment(-180 + angle + rr(), l + rr(), 1, true) - p.addSegment(-270 + angle + rr(), l + rr(), 1, true) - // Optionally add a random final angle for the last segment - let angle2 = r ? R.randInt(-5,5) : 0; - if (r) p.addSegment(0 + angle, angle2 * (Math.PI/180) * radius, true) - // Finalize the plot - p.endPlot(angle2 + angle,1, true) - // Fill / hatch / draw - let o = [x - radius * R.sin(angle),y - radius * R.cos(-angle)] - p.show(o[0],o[1],1) - } - - export function arc(x,y,radius,start,end) { - _ensureReady(); - // Create a new Plot instance for a curve shape - let p = new Plot("curve") - // Calculate start angle and end angle - let a1 = 270 - R.toDegrees(start), a2 = 270 - R.toDegrees(end); - // Calculate length arc - let arcAngle = R.toDegrees(end - start); - let l = Math.PI * radius * arcAngle / 180; - // Add segments to plot - p.addSegment(a1, l, 1, true) - p.endPlot(a2, 1, true) - // Draw from starting point - p.draw(x + radius * R.cos(- a1 - 90),y + radius * R.sin(- a1 - 90),1) - } - - // Holds the array of vertices for the custom shape being defined. Each vertex includes position and optional pressure. - let _strokeArray = false; - // Holds options for the stroke, such as curvature, that can influence the shape's rendering. - let _strokeOption; - - /** - * Starts recording vertices for a custom shape. Optionally, a curvature can be defined. - * @param {number} [curvature] - From 0 to 1. Defines the curvature for the vertices being recorded (optional). - */ - export function beginShape(curvature = 0) { - _strokeOption = R.constrain(curvature,0,1); // Set the curvature option for the shape - _strokeArray = []; // Initialize the array to store vertices - } - - /** - * Records a vertex in the custom shape being defined between beginShape and endShape. - * @param {number} x - The x-coordinate of the vertex. - * @param {number} y - The y-coordinate of the vertex. - * @param {number} [pressure] - The pressure at the vertex (optional). - */ - export function vertex(x, y, pressure) { - _strokeArray.push([x, y, pressure]); // Add the vertex to the array - } - - /** - * Finishes recording vertices for a custom shape and either closes it or leaves it open. - * It also triggers the drawing of the shape with the active stroke(), fill() and hatch() states. - * @param {string} [a] - An optional argument to close the shape if set to _r.CLOSE. - */ - export function endShape(a) { - _ensureReady(); - if (a === _r.CLOSE) { - _strokeArray.push(_strokeArray[0]); // Close the shape by connecting the last vertex to the first - _strokeArray.push(_strokeArray[1]); + if (i == array_points.length - 3) { + p.endPlot(a2, p2[2], true); } - // Create a new Plot with the recorded vertices and curvature option - let plot = (_strokeOption == 0 && !FF.isActive) ? new Polygon(_strokeArray) : _createSpline(_strokeArray, _strokeOption, a === _r.CLOSE ? true : false); - plot.show(); - _strokeArray = false; // Clear the array after the shape has been drawn - } - - /** - * Begins a new stroke with a given type and starting position. This initializes - * a new Plot to record the stroke's path. - * @param {string} type - The type of the stroke, which defines the kind of Plot to create. - * @param {number} x - The x-coordinate of the starting point of the stroke. - * @param {number} y - The y-coordinate of the starting point of the stroke. - */ - export function beginStroke(type, x, y) { - _strokeOption = [x, y]; // Store the starting position for later use - _strokeArray = new Plot(type); // Initialize a new Plot with the specified type - } - - /** - * Adds a segment to the stroke with a given angle, length, and pressure. This function - * is called between beginStroke and endStroke to define the stroke's path. - * @param {number} angle - The initial angle of the segment, relative to the canvas. - * @param {number} length - The length of the segment. - * @param {number} pressure - The pressure at the start of the segment, affecting properties like width. - */ - export function segment(angle, length, pressure) { - _strokeArray.addSegment(angle, length, pressure); // Add the new segment to the Plot - } - - /** - * Completes the stroke path and triggers the rendering of the stroke. - * @param {number} angle - The angle of the curve at the last point of the stroke path. - * @param {number} pressure - The pressure at the end of the stroke. - */ - export function endStroke(angle, pressure) { - _strokeArray.endPlot(angle, pressure); // Finalize the Plot with the end angle and pressure - _strokeArray.draw(_strokeOption[0], _strokeOption[1], 1); // Draw the stroke using the stored starting position - _strokeArray = false; // Clear the _strokeArray to indicate the end of this stroke - } - - /** - * Creates a new Plot object. - * @param {Array>} array_points - An array of points defining the spline curve. - * @param {number} [curvature=0.5] - The curvature of the spline curve, beterrn 0 and 1. A curvature of 0 will create a series of straight segments. - */ - function _createSpline (array_points, curvature = 0.5, _close = false) { - - // Initialize the plot type based on curvature - let plotType = (curvature === 0) ? "segments" : "curve"; - let p = new Plot(plotType); - - // Proceed only if there are points provided - if (array_points && array_points.length > 0) { - // Add each segment to the plot - let done = 0; - let pep, tep, pep2; - for (let i = 0; i < array_points.length - 1; i++) { - if (curvature > 0 && i < array_points.length - 2) { - // Get the current and next points - let p1 = array_points[i], p2 = array_points[i+1], p3 = array_points[i+2]; - // Calculate distances and angles between points - let d1 = R.dist(p1[0],p1[1],p2[0],p2[1]), d2 = R.dist(p2[0],p2[1],p3[0],p3[1]); - let a1 = _calculateAngle(p1[0],p1[1],p2[0],p2[1]), a2 = _calculateAngle(p2[0],p2[1],p3[0],p3[1]); - // Calculate curvature based on the minimum distance - let dd = curvature * Math.min(Math.min(d1,d2),0.5 * Math.min(d1,d2)), dmax = Math.max(d1,d2) - let s1 = d1 - dd, s2 = d2 - dd; - // If the angles are approximately the same, create a straight segment - if (Math.floor(a1) === Math.floor(a2)) { - let temp = _close ? (i === 0 ? 0 : d1 - done) : d1 - done; - let temp2 = _close ? (i === 0 ? 0 : d2 - pep2) : d2; - p.addSegment(a1,temp,p1[2],true) - if (i === array_points.length - 3) p.addSegment(a2,temp2,p2[2],true); - done = 0; - if (i === 0) pep = d1, pep2 = dd, tep = array_points[1], done = 0; - } else { - // If the angles are not the same, create curves, etc (this is a too complex...) - let point1 = {x: p2[0] - dd * R.cos(-a1), y: p2[1] - dd * R.sin(-a1)} - let point2 = {x: point1.x + dmax * R.cos(-a1+90), y: point1.y + dmax * R.sin(-a1+90)} - let point3 = {x: p2[0] + dd * R.cos(-a2), y: p2[1] + dd * R.sin(-a2)} - let point4 = {x: point3.x + dmax * R.cos(-a2+90), y: point3.y + dmax * R.sin(-a2+90)} - let int = _intersectLines(point1,point2,point3,point4,true) - let radius = R.dist(point1.x,point1.y,int.x,int.y) - let disti = R.dist(point1.x,point1.y,point3.x,point3.y)/2 - let a3 = 2 * Math.asin( disti/radius ) * (180 / Math.PI); - let s3 = 2 * Math.PI * radius * a3 / 360; - let temp = _close ? (i === 0 ? 0 : s1-done) : s1-done; - let temp2 = (i === array_points.length - 3) ? (_close ? pep - dd : s2) : 0; - p.addSegment(a1,temp, p1[2],true) - p.addSegment(a1,isNaN(s3) ? 0 : s3, p1[2],true) - p.addSegment(a2,temp2, p2[2],true) - done = dd; - if (i === 0) pep = s1, pep2 = dd, tep = [point1.x,point1.y]; - } - if (i == array_points.length - 3) { - p.endPlot(a2,p2[2],true) - } - } else if (curvature === 0) { - // If curvature is 0, simply create segments - if (i === 0 && _close) array_points.pop() - let p1 = array_points[i], p2 = array_points[i+1] - let d = R.dist(p1[0],p1[1],p2[0],p2[1]); - let a = _calculateAngle(p1[0],p1[1],p2[0],p2[1]); - p.addSegment(a,d,1,true) - if (i == array_points.length - 2) { - p.endPlot(a,1,true) - } - } - } - // Set the origin point from the first point in the array - p.origin = (_close && curvature !== 0) ? tep : array_points[0] + } else if (curvature === 0) { + // If curvature is 0, simply create segments + if (i === 0 && _close) array_points.pop(); + let p1 = array_points[i], + p2 = array_points[i + 1]; + let d = R.dist(p1[0], p1[1], p2[0], p2[1]); + let a = _calculateAngle(p1[0], p1[1], p2[0], p2[1]); + p.addSegment(a, d, 1, true); + if (i == array_points.length - 2) { + p.endPlot(a, 1, true); } - return p; - + } } + // Set the origin point from the first point in the array + p.origin = _close && curvature !== 0 ? tep : array_points[0]; + } + return p; +} - /** - * Creates and draws a spline curve with the given points and curvature. - * @param {Array>} array_points - An array of points defining the spline curve. - * @param {number} [curvature=0.5] - The curvature of the spline curve, between 0 and 1. A curvature of 0 will create a series of straight segments. - */ - export function spline(array_points, curvature = 0.5) { - let p = _createSpline(array_points, curvature); // Create a new Plot-spline instance - p.draw(); // Draw the Plot - } +/** + * Creates and draws a spline curve with the given points and curvature. + * @param {Array>} array_points - An array of points defining the spline curve. + * @param {number} [curvature=0.5] - The curvature of the spline curve, between 0 and 1. A curvature of 0 will create a series of straight segments. + */ +export function spline(array_points, curvature = 0.5) { + let p = _createSpline(array_points, curvature); // Create a new Plot-spline instance + p.draw(); // Draw the Plot +} // ============================================================================= // Section: Fill Management @@ -2280,396 +2541,508 @@ * techniques for simulating watercolor paints. */ - // No docs for now - const E = { } - export function erase(color = "white", alpha = 255) { - E.isActive = true - E.c = color; E.a = alpha; - } - export function noErase() { - E.isActive = false - } - - /** - * Sets the fill color and opacity for subsequent drawing operations. - * @param {number|p5.Color} a - The red component of the color or grayscale value, a CSS color string, or a p5.Color object. - * @param {number} [b] - The green component of the color or the grayscale opacity if two arguments. - * @param {number} [c] - The blue component of the color. - * @param {number} [d] - The opacity of the color. - */ - export function fill(a,b,c,d) { - _ensureReady() - F.opacity = (arguments.length < 4) ? ((arguments.length < 3) ? b : 1) : d; - F.color = (arguments.length < 3) ? _r.color(a) : _r.color(a,b,c); - F.isActive = true; - } - - /** - * Sets the bleed and texture levels for the fill operation, simulating a watercolor effect. - * @param {number} _i - The intensity of the bleed effect, capped at 0.5. - * @param {number} _texture - The texture of the watercolor effect, from 0 to 1. - */ - export function bleed(_i, _direction = "out") { - _ensureReady() - F.bleed_strength = R.constrain(_i,0,0.6); - F.direction = _direction - } - - export function fillTexture(_texture = 0.4, _border = 0.4) { - _ensureReady() - F.texture_strength = R.constrain(_texture, 0, 1); - F.border_strength = R.constrain(_border, 0, 1); - } - - export function gravity(x, y) { - _ensureReady() - F.light_source = {x: x, y: y} - } - export function noGravity() { - F.light_source = false; - } - - /** - * Disables the fill for subsequent drawing operations. - */ - export function noFill() { - F.isActive = false; - } - - /** - * Disables some operations in order to guarantee a consistent bleed efect for animations (at different bleed levels) - */ - export function fillAnimatedMode(bool) { - F.isAnimated = bool; - } - - /** - * Object representing the fill state and operations for drawing. - * @property {boolean} isActive - Indicates if the fill operation is active. - * @property {boolean} isAnimated - Enable or disable animation-mode - * @property {Array} v - Array of p5.Vector representing vertices of the polygon to fill. - * @property {Array} m - Array of multipliers for the bleed effect on each vertex. - * @property {p5.Color} color - Current fill color. - * @property {p5.Color} opacity - Current fill opacity. - * @property {number} bleed_strength - Base value for bleed effect. - * @property {number} texture_strength - Base value for texture strength. - * @property {number} border_strength - Base value for border strength. - * @property {function} fill - Method to fill a polygon with a watercolor effect. - * @property {function} calcCenter - Method to calculate the centroid of the polygon. - */ - const F = { - isActive: false, - isAnimated: false, - color: "#002185", - opacity: 80, - bleed_strength: 0.07, - texture_strength: 0.4, - border_strength: 0.4, - - /** - * Fills the given polygon with a watercolor effect. - * @param {Object} polygon - The polygon to fill. - */ - fill (polygon) { - // Store polygon - this.polygon = polygon; - // Map polygon vertices to p5.Vector objects - this.v = [...polygon.vertices]; - // Calculate fluidity once, outside the loop - const fluid = this.v.length * R.random(0.4); - // Map vertices to bleed multipliers with more intense effect on 'fluid' vertices - F.m = this.v.map((_, i) => { - let multiplier = R.random(0.8, 1.2) * this.bleed_strength; - return i < fluid ? R.constrain(multiplier * 2, 0, 0.9) : multiplier; - }); - // Shift vertices randomly to create a more natural watercolor edge - let shift = R.randInt(0, this.v.length); - // If light source, look for closest - if (F.light_source) { - for (let i = 0; i < this.v.length; i ++) { - if (R.dist(this.v[i].x,this.v[i].y,F.light_source.x,F.light_source.y) < R.dist(this.v[shift].x,this.v[shift].y,F.light_source.x,F.light_source.y)) shift = i - } - } - this.v = [...this.v.slice(shift), ...this.v.slice(0, shift)]; - // Create and fill the polygon with the calculated bleed effect - let pol = new FillPolygon (this.v, this.m, this.calcCenter(),[],true) - pol.fill(this.color, Math.floor(R.map(this.opacity,0,155,0,20,true)), this.texture_strength) - }, - - /** - * Calculates the center point of the polygon based on the vertices. - * @returns {p5.Vector} A vector representing the centroid of the polygon. - */ - calcCenter () { - let midx = 0, midy = 0; - for(let i = 0; i < this.v.length; ++i) { - midx += this.v[i].x; - midy += this.v[i].y; - } - midx /= this.v.length, midy /= this.v.length; - return {x: midx, y: midy} - } - } +// No docs for now +const E = {}; +export function erase(color = "white", alpha = 255) { + E.isActive = true; + E.c = color; + E.a = alpha; +} +export function noErase() { + E.isActive = false; +} - function _rotate(cx, cy, x, y, angle) { - let cos = R.cos(angle), sin = R.sin(angle), - nx = (cos * (x - cx)) + (sin * (y - cy)) + cx, - ny = (cos * (y - cy)) - (sin * (x - cx)) + cy; - return { x: nx, y: ny }; - } +/** + * Sets the fill color and opacity for subsequent drawing operations. + * @param {number|p5.Color} a - The red component of the color or grayscale value, a CSS color string, or a p5.Color object. + * @param {number} [b] - The green component of the color or the grayscale opacity if two arguments. + * @param {number} [c] - The blue component of the color. + * @param {number} [d] - The opacity of the color. + */ +export function fill(a, b, c, d) { + _ensureReady(); + F.opacity = arguments.length < 4 ? (arguments.length < 3 ? b : 1) : d; + F.color = arguments.length < 3 ? _r.color(a) : _r.color(a, b, c); + F.isActive = true; +} - /** - * The FillPolygon class is used to create and manage the properties of the polygons that produces - * the watercolor effect. It includes methods to grow (expand) the polygon and apply layers - * of color with varying intensity and erase parts to simulate a natural watercolor bleed. - * The implementation follows Tyler Hobbs' guide to simulating watercolor: - * https://tylerxhobbs.com/essays/2017/a-generative-approach-to-simulating-watercolor-paints - */ - class FillPolygon { - - /** - * The constructor initializes the polygon with a set of vertices, multipliers for the bleed effect, and a center point. - * @param {p5.Vector[]} _v - An array of p5.Vector objects representing the vertices of the polygon. - * @param {number[]} _m - An array of numbers representing the multipliers for the bleed effect at each vertex. - * @param {p5.Vector} _center - A p5.Vector representing the calculated center point of the polygon. - * @param {boolean[]} dir - An array of booleans representing the bleed direction. - * @param {boolean} isFirst - Boolean = true for initial fill polygon - */ - constructor (_v,_m,_center,dir,isFirst = false) { - this.pol = new Polygon(_v, true) - this.v = _v; - this.dir = dir; - this.m = _m; - this.midP = _center; - this.size = -Infinity; - for (let v of this.v) { - let temp_size = R.dist(this.midP.x,this.midP.y,v.x,v.y) - if (temp_size > this.size) this.size = temp_size; - } - // This calculates the bleed direction for the initial shape, for each of the vertices. - if (isFirst) { - for (let i = 0; i < this.v.length; i++) { - const v1 = this.v[i] - const v2 = this.v[(i + 1) % this.v.length]; - const side = { x: v2.x - v1.x, y: v2.y - v1.y } - const rt = _rotate(0,0,side.x,side.y,90) - let linea = { - point1 : {x: v1.x + side.x / 2, y: v1.y + side.y / 2}, - point2 : {x: v1.x + side.x / 2 + rt.x, y: v1.y + side.y / 2 + rt.y}, - } - const isLeft = (a, b, c) => { - return (b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x) > 0.01; - } - let d1 = 0; - for (let int of F.polygon.intersect(linea)) {if (isLeft(v1,v2,int)) d1++;} - this.dir[i] = (d1 % 2 === 0) ? true : false; - } - } - } +/** + * Sets the bleed and texture levels for the fill operation, simulating a watercolor effect. + * @param {number} _i - The intensity of the bleed effect, capped at 0.5. + * @param {number} _texture - The texture of the watercolor effect, from 0 to 1. + */ +export function bleed(_i, _direction = "out") { + _ensureReady(); + F.bleed_strength = R.constrain(_i, 0, 0.6); + F.direction = _direction; +} + +export function fillTexture(_texture = 0.4, _border = 0.4) { + _ensureReady(); + F.texture_strength = R.constrain(_texture, 0, 1); + F.border_strength = R.constrain(_border, 0, 1); +} + +export function gravity(x, y) { + _ensureReady(); + F.light_source = { x: x, y: y }; +} +export function noGravity() { + F.light_source = false; +} - trim (factor) { - let v = [...this.v], m = [...this.m], dir = [...this.dir]; - if (this.v.length > 10 && factor >= 0.2) { - let numTrim = ~~((1 - factor) * this.v.length); - let sp = ~~this.v.length/2 - ~~numTrim/2; - v.splice(sp, numTrim) - m.splice(sp, numTrim) - dir.splice(sp, numTrim) - } - return {v: v, m: m, dir: dir} - } +/** + * Disables the fill for subsequent drawing operations. + */ +export function noFill() { + F.isActive = false; +} - /** - * Grows the polygon's vertices outwards to simulate the spread of watercolor. - * Optionally, can also shrink (degrow) the polygon's vertices inward. - * @param {number} _a - The growth factor. - * @param {boolean} [degrow=false] - If true, vertices will move inwards. - * @returns {FillPolygon} A new `FillPolygon` object with adjusted vertices. - */ - grow (growthFactor, degrow = false) { - const newVerts = []; - const newMods = []; - const newDirs = []; - // Determine the length of vertices to process based on growth factor - let tr = this.trim(growthFactor) - // Pre-compute values that do not change within the loop - const modAdjustment = degrow ? -0.5 : 1; - // Inline changeModifier to reduce function calls - const changeModifier = (modifier) => { - const gaussianVariation = R.gaussian(0.5, 0.1); - return modifier + (gaussianVariation - 0.5) * 0.1; - }; - // Loop through each vertex to calculate the new position based on growth - for (let i = 0; i < tr.v.length; i++) { - const currentVertex = tr.v[i]; - const nextVertex = tr.v[(i + 1) % tr.v.length]; - // Determine the growth modifier - let mod = (growthFactor === 0.1) ? (F.bleed_strength <= 0.1 ? 0.25 : 0.75) : tr.m[i]; - mod *= modAdjustment; - // Add the current vertex and its modified value - newVerts.push(currentVertex); - newMods.push(changeModifier(mod)); - - // Calculate side - let side = {x: nextVertex.x - currentVertex.x, y: nextVertex.y - currentVertex.y} - - // Make sure that we always bleed in the selected direction - let dir = tr.dir[i]; - let bleed = (F.direction == "out") ? -90 : 90; - let rotationDegrees = ((dir) ? bleed : -bleed) + (R.gaussian(0,0.4)) * 45; - - // Calculate the middle vertex position - let lerp = R.constrain(R.gaussian(0.5,0.2),0.1,0.9) - let newVertex = {x: currentVertex.x + side.x * lerp, y: currentVertex.y + side.y * lerp} - - // Calculate the new vertex position - let mult = R.gaussian(0.5, 0.2) * R.random(0.6, 1.4) * mod; - let direction = _rotate(0,0,side.x,side.y,rotationDegrees) - newVertex.x += direction.x * mult - newVertex.y += direction.y * mult - - // Add the new vertex and its modifier - newVerts.push(newVertex); - newMods.push(changeModifier(mod)); - newDirs.push(dir,dir) - } - return new FillPolygon (newVerts, newMods, this.midP, newDirs); - } +/** + * Disables some operations in order to guarantee a consistent bleed efect for animations (at different bleed levels) + */ +export function fillAnimatedMode(bool) { + F.isAnimated = bool; +} - /** - * Fills the polygon with the specified color and intensity. - * It uses layered growth to simulate watercolor paper absorption and drying patterns. - * @param {p5.Color|string} color - The fill color. - * @param {number} intensity - The opacity of the color layers. - */ - fill (color, intensity, tex) { - let bleed = R.map(F.bleed_strength,0,0.15,0.6,1,true) - // Precalculate stuff - const numLayers = 24 * bleed; - const intensityThird = intensity / 5 + tex * intensity / 6; - const intensityQuarter = intensity / 4 + tex * intensity / 3; - const intensityFifth = intensity / 7 + tex * intensity / 3; - const intensityHalf = intensity / 5 ; - const texture = tex * 3; - - // Perform initial setup only once - Mix.watercolor = true; - Matrix.trans(); - Mix.blend(color,false,false,true) - Mix.masks[0].push(); - Mix.masks[0].noStroke(); - Mix.masks[0].translate(Matrix.translation[0] + _r.width/2, Matrix.translation[1] + _r.height/2); - Mix.masks[0].rotate(Matrix.rotation) - Mix.masks[0].scale(_curScale) - - // Set the different polygons for texture - let pol = this.grow() - let pol2 = pol.grow().grow(0.9); - let pol3 = pol2.grow(0.75); - let pol4 = this.grow(0.6) - - for (let i = 0; i < numLayers; i ++) { - if (i === Math.floor(numLayers / 4) || i === Math.floor(numLayers / 2) || i === Math.floor(3 * numLayers / 4)) { - // Grow the polygon objects once per fourth of the process - pol = pol.grow(); - // Grow the texture polygons if conditions are met - if (bleed === 1 || i === Math.floor(numLayers / 2)) { - pol2 = pol2.grow(0.75); - pol3 = pol3.grow(0.75); - pol4 = pol4.grow(0.1,true); - } - } - // Draw layers - pol.grow().layer(i, intensityHalf); - pol4.grow(0.1, true).grow(0.1).layer(i, intensityFifth, false); - pol2.grow(0.1).grow(0.1).layer(i, intensityQuarter, false); - pol3.grow(0.8).grow(0.1).layer(i, intensityThird, false); - // Erase after each set of layers is drawn - if (texture !== 0) pol.erase(texture, intensity); - } - Mix.masks[0].pop(); - } +/** + * Object representing the fill state and operations for drawing. + * @property {boolean} isActive - Indicates if the fill operation is active. + * @property {boolean} isAnimated - Enable or disable animation-mode + * @property {Array} v - Array of p5.Vector representing vertices of the polygon to fill. + * @property {Array} m - Array of multipliers for the bleed effect on each vertex. + * @property {p5.Color} color - Current fill color. + * @property {p5.Color} opacity - Current fill opacity. + * @property {number} bleed_strength - Base value for bleed effect. + * @property {number} texture_strength - Base value for texture strength. + * @property {number} border_strength - Base value for border strength. + * @property {function} fill - Method to fill a polygon with a watercolor effect. + * @property {function} calcCenter - Method to calculate the centroid of the polygon. + */ +const F = { + isActive: false, + isAnimated: false, + color: "#002185", + opacity: 80, + bleed_strength: 0.07, + texture_strength: 0.4, + border_strength: 0.4, + + /** + * Fills the given polygon with a watercolor effect. + * @param {Object} polygon - The polygon to fill. + */ + fill(polygon) { + // Store polygon + this.polygon = polygon; + // Map polygon vertices to p5.Vector objects + this.v = [...polygon.vertices]; + // Calculate fluidity once, outside the loop + const fluid = this.v.length * R.random(0.4); + // Map vertices to bleed multipliers with more intense effect on 'fluid' vertices + F.m = this.v.map((_, i) => { + let multiplier = R.random(0.8, 1.2) * this.bleed_strength; + return i < fluid ? R.constrain(multiplier * 2, 0, 0.9) : multiplier; + }); + // Shift vertices randomly to create a more natural watercolor edge + let shift = R.randInt(0, this.v.length); + // If light source, look for closest + if (F.light_source) { + for (let i = 0; i < this.v.length; i++) { + if ( + R.dist(this.v[i].x, this.v[i].y, F.light_source.x, F.light_source.y) < + R.dist( + this.v[shift].x, + this.v[shift].y, + F.light_source.x, + F.light_source.y + ) + ) + shift = i; + } + } + this.v = [...this.v.slice(shift), ...this.v.slice(0, shift)]; + // Create and fill the polygon with the calculated bleed effect + let pol = new FillPolygon(this.v, this.m, this.calcCenter(), [], true); + pol.fill( + this.color, + Math.floor(R.map(this.opacity, 0, 155, 0, 20, true)), + this.texture_strength + ); + }, + + /** + * Calculates the center point of the polygon based on the vertices. + * @returns {p5.Vector} A vector representing the centroid of the polygon. + */ + calcCenter() { + let midx = 0, + midy = 0; + for (let i = 0; i < this.v.length; ++i) { + midx += this.v[i].x; + midy += this.v[i].y; + } + (midx /= this.v.length), (midy /= this.v.length); + return { x: midx, y: midy }; + }, +}; + +function _rotate(cx, cy, x, y, angle) { + let cos = R.cos(angle), + sin = R.sin(angle), + nx = cos * (x - cx) + sin * (y - cy) + cx, + ny = cos * (y - cy) - sin * (x - cx) + cy; + return { x: nx, y: ny }; +} - /** - * Adds a layer of color to the polygon with specified opacity. - * It also sets a stroke to outline the layer edges. - * @param {number} _nr - The layer number, affecting the stroke and opacity mapping. - * @param {number} _alpha - The opacity of the layer. - * @param {boolean} [bool=true] - If true, adds a stroke to the layer. - */ - layer (_nr, _alpha, bool = true) { - // Set fill and stroke properties once - Mix.masks[0].fill(255, 0, 0, _alpha); - if (bool) { - Mix.masks[0].stroke(255, 0, 0, 0.5 + 1.5 * F.border_strength); - Mix.masks[0].strokeWeight(R.map(_nr, 0, 24, 6, 0.5)); - } else { - Mix.masks[0].noStroke(); - } - Mix.masks[0].beginShape(); - for(let v of this.v) { - Mix.masks[0].vertex(v.x, v.y); - } - Mix.masks[0].endShape(_r.CLOSE); +/** + * The FillPolygon class is used to create and manage the properties of the polygons that produces + * the watercolor effect. It includes methods to grow (expand) the polygon and apply layers + * of color with varying intensity and erase parts to simulate a natural watercolor bleed. + * The implementation follows Tyler Hobbs' guide to simulating watercolor: + * https://tylerxhobbs.com/essays/2017/a-generative-approach-to-simulating-watercolor-paints + */ +class FillPolygon { + /** + * The constructor initializes the polygon with a set of vertices, multipliers for the bleed effect, and a center point. + * @param {p5.Vector[]} _v - An array of p5.Vector objects representing the vertices of the polygon. + * @param {number[]} _m - An array of numbers representing the multipliers for the bleed effect at each vertex. + * @param {p5.Vector} _center - A p5.Vector representing the calculated center point of the polygon. + * @param {boolean[]} dir - An array of booleans representing the bleed direction. + * @param {boolean} isFirst - Boolean = true for initial fill polygon + */ + constructor(_v, _m, _center, dir, isFirst = false) { + this.pol = new Polygon(_v, true); + this.v = _v; + this.dir = dir; + this.m = _m; + this.midP = _center; + this.size = -Infinity; + for (let v of this.v) { + let temp_size = R.dist(this.midP.x, this.midP.y, v.x, v.y); + if (temp_size > this.size) this.size = temp_size; + } + // This calculates the bleed direction for the initial shape, for each of the vertices. + if (isFirst) { + for (let i = 0; i < this.v.length; i++) { + const v1 = this.v[i]; + const v2 = this.v[(i + 1) % this.v.length]; + const side = { x: v2.x - v1.x, y: v2.y - v1.y }; + const rt = _rotate(0, 0, side.x, side.y, 90); + let linea = { + point1: { x: v1.x + side.x / 2, y: v1.y + side.y / 2 }, + point2: { x: v1.x + side.x / 2 + rt.x, y: v1.y + side.y / 2 + rt.y }, + }; + const isLeft = (a, b, c) => { + return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) > 0.01; + }; + let d1 = 0; + for (let int of F.polygon.intersect(linea)) { + if (isLeft(v1, v2, int)) d1++; } - - /** - * Erases parts of the polygon to create a more natural, uneven watercolor texture. - * Uses random placement and sizing of circles to simulate texture. - */ - erase (texture, intensity) { - const numCircles = R.random(130, 200); - const halfSize = this.size / 2; - const minSizeFactor = 0.025 * this.size; - const maxSizeFactor = 0.19 * this.size; - Mix.masks[0].erase(3.5 * texture - R.map(intensity, 80, 120, 0.3, 1, true),0); - for (let i = 0; i < numCircles; i++) { - const x = this.midP.x + R.gaussian(0, halfSize); - const y = this.midP.y + R.gaussian(0, halfSize); - const size = R.random(minSizeFactor, maxSizeFactor); - Mix.masks[0].circle(x, y, size); - } - Mix.masks[0].noErase(); + this.dir[i] = d1 % 2 === 0 ? true : false; + } + } + } + + trim(factor) { + let v = [...this.v], + m = [...this.m], + dir = [...this.dir]; + if (this.v.length > 10 && factor >= 0.2) { + let numTrim = ~~((1 - factor) * this.v.length); + let sp = ~~this.v.length / 2 - ~~numTrim / 2; + v.splice(sp, numTrim); + m.splice(sp, numTrim); + dir.splice(sp, numTrim); + } + return { v: v, m: m, dir: dir }; + } + + /** + * Grows the polygon's vertices outwards to simulate the spread of watercolor. + * Optionally, can also shrink (degrow) the polygon's vertices inward. + * @param {number} _a - The growth factor. + * @param {boolean} [degrow=false] - If true, vertices will move inwards. + * @returns {FillPolygon} A new `FillPolygon` object with adjusted vertices. + */ + grow(growthFactor, degrow = false) { + const newVerts = []; + const newMods = []; + const newDirs = []; + // Determine the length of vertices to process based on growth factor + let tr = this.trim(growthFactor); + // Pre-compute values that do not change within the loop + const modAdjustment = degrow ? -0.5 : 1; + // Inline changeModifier to reduce function calls + const changeModifier = (modifier) => { + const gaussianVariation = R.gaussian(0.5, 0.1); + return modifier + (gaussianVariation - 0.5) * 0.1; + }; + // Loop through each vertex to calculate the new position based on growth + for (let i = 0; i < tr.v.length; i++) { + const currentVertex = tr.v[i]; + const nextVertex = tr.v[(i + 1) % tr.v.length]; + // Determine the growth modifier + let mod = + growthFactor === 0.1 + ? F.bleed_strength <= 0.1 + ? 0.25 + : 0.75 + : tr.m[i]; + mod *= modAdjustment; + // Add the current vertex and its modified value + newVerts.push(currentVertex); + newMods.push(changeModifier(mod)); + + // Calculate side + let side = { + x: nextVertex.x - currentVertex.x, + y: nextVertex.y - currentVertex.y, + }; + + // Make sure that we always bleed in the selected direction + let dir = tr.dir[i]; + let bleed = F.direction == "out" ? -90 : 90; + let rotationDegrees = (dir ? bleed : -bleed) + R.gaussian(0, 0.4) * 45; + + // Calculate the middle vertex position + let lerp = R.constrain(R.gaussian(0.5, 0.2), 0.1, 0.9); + let newVertex = { + x: currentVertex.x + side.x * lerp, + y: currentVertex.y + side.y * lerp, + }; + + // Calculate the new vertex position + let mult = R.gaussian(0.5, 0.2) * R.random(0.6, 1.4) * mod; + let direction = _rotate(0, 0, side.x, side.y, rotationDegrees); + newVertex.x += direction.x * mult; + newVertex.y += direction.y * mult; + + // Add the new vertex and its modifier + newVerts.push(newVertex); + newMods.push(changeModifier(mod)); + newDirs.push(dir, dir); + } + return new FillPolygon(newVerts, newMods, this.midP, newDirs); + } + + /** + * Fills the polygon with the specified color and intensity. + * It uses layered growth to simulate watercolor paper absorption and drying patterns. + * @param {p5.Color|string} color - The fill color. + * @param {number} intensity - The opacity of the color layers. + */ + fill(color, intensity, tex) { + let bleed = R.map(F.bleed_strength, 0, 0.15, 0.6, 1, true); + // Precalculate stuff + const numLayers = 24 * bleed; + const intensityThird = intensity / 5 + (tex * intensity) / 6; + const intensityQuarter = intensity / 4 + (tex * intensity) / 3; + const intensityFifth = intensity / 7 + (tex * intensity) / 3; + const intensityHalf = intensity / 5; + const texture = tex * 3; + + // Perform initial setup only once + Mix.watercolor = true; + Matrix.trans(); + Mix.blend(color, false, false, true); + Mix.masks[0].push(); + Mix.masks[0].noStroke(); + Mix.masks[0].translate( + Matrix.translation[0] + _r.width / 2, + Matrix.translation[1] + _r.height / 2 + ); + Mix.masks[0].rotate(Matrix.rotation); + Mix.masks[0].scale(_curScale); + + // Set the different polygons for texture + let pol = this.grow(); + let pol2 = pol.grow().grow(0.9); + let pol3 = pol2.grow(0.75); + let pol4 = this.grow(0.6); + + for (let i = 0; i < numLayers; i++) { + if ( + i === Math.floor(numLayers / 4) || + i === Math.floor(numLayers / 2) || + i === Math.floor((3 * numLayers) / 4) + ) { + // Grow the polygon objects once per fourth of the process + pol = pol.grow(); + // Grow the texture polygons if conditions are met + if (bleed === 1 || i === Math.floor(numLayers / 2)) { + pol2 = pol2.grow(0.75); + pol3 = pol3.grow(0.75); + pol4 = pol4.grow(0.1, true); } - } + } + // Draw layers + pol.grow().layer(i, intensityHalf); + pol4.grow(0.1, true).grow(0.1).layer(i, intensityFifth, false); + pol2.grow(0.1).grow(0.1).layer(i, intensityQuarter, false); + pol3.grow(0.8).grow(0.1).layer(i, intensityThird, false); + // Erase after each set of layers is drawn + if (texture !== 0) pol.erase(texture, intensity); + } + Mix.masks[0].pop(); + } + + /** + * Adds a layer of color to the polygon with specified opacity. + * It also sets a stroke to outline the layer edges. + * @param {number} _nr - The layer number, affecting the stroke and opacity mapping. + * @param {number} _alpha - The opacity of the layer. + * @param {boolean} [bool=true] - If true, adds a stroke to the layer. + */ + layer(_nr, _alpha, bool = true) { + // Set fill and stroke properties once + Mix.masks[0].fill(255, 0, 0, _alpha); + if (bool) { + Mix.masks[0].stroke(255, 0, 0, 0.5 + 1.5 * F.border_strength); + Mix.masks[0].strokeWeight(R.map(_nr, 0, 24, 6, 0.5)); + } else { + Mix.masks[0].noStroke(); + } + Mix.masks[0].beginShape(); + for (let v of this.v) { + Mix.masks[0].vertex(v.x, v.y); + } + Mix.masks[0].endShape(_r.CLOSE); + } + + /** + * Erases parts of the polygon to create a more natural, uneven watercolor texture. + * Uses random placement and sizing of circles to simulate texture. + */ + erase(texture, intensity) { + const numCircles = R.random(130, 200); + const halfSize = this.size / 2; + const minSizeFactor = 0.025 * this.size; + const maxSizeFactor = 0.19 * this.size; + Mix.masks[0].erase( + 3.5 * texture - R.map(intensity, 80, 120, 0.3, 1, true), + 0 + ); + for (let i = 0; i < numCircles; i++) { + const x = this.midP.x + R.gaussian(0, halfSize); + const y = this.midP.y + R.gaussian(0, halfSize); + const size = R.random(minSizeFactor, maxSizeFactor); + Mix.masks[0].circle(x, y, size); + } + Mix.masks[0].noErase(); + } +} // ============================================================================= // Section: Standard Brushes // ============================================================================= - - /** - * Defines a set of standard brushes with specific characteristics. Each brush is defined - * with properties such as weight, vibration, definition, quality, opacity, spacing, and - * pressure sensitivity. Some brushes have additional properties like type, tip, and rotate. - */ - const _vals = ["weight", "vibration", "definition", "quality", "opacity", "spacing", "pressure", "type", "tip", "rotate"] - const _standard_brushes = [ - // Define each brush with a name and a set of parameters - // For example, the "pen" brush has a weight of 0.35, a vibration of 0.12, etc. - // The "marker2" brush has a custom tip defined by a function that draws rectangles. - ["pen", [ 0.35, 0.12, 0.5, 8, 200, 0.3, {curve: [0.15,0.2], min_max: [1.4,0.9]} ] ], - ["rotring", [ 0.2, 0.05, 1, 3, 250, 0.15, {curve: [0.05,0.2], min_max: [1.7,0.8]} ]], - ["2B", [ 0.35, 0.5, 0.1, 8, 180, 0.2, {curve: [0.15,0.2], min_max: [1.3,1]} ]], - ["HB", [ 0.3, 0.5, 0.4, 4, 180, 0.25, {curve: [0.15,0.2], min_max: [1.2,0.9]} ]], - ["2H", [ 0.2, 0.4, 0.3, 2, 150, 0.2, {curve: [0.15,0.2], min_max: [1.2,0.9]} ]], - ["cpencil", [ 0.4, 0.6, 0.8, 7, 120, 0.15, {curve: [0.15,0.2], min_max: [0.95,1.2]} ]], - ["charcoal", [ 0.5, 2, 0.8, 300, 110, 0.06, {curve: [0.15,0.2], min_max: [1.3,0.8]} ]], - ["hatch_brush", [ 0.2, 0.4, 0.3, 2, 150, 0.15, {curve: [0.5,0.7], min_max: [1,1.5]} ]], - ["spray", [ 0.3, 12, 15, 40, 80, 0.65, {curve: [0,0.1], min_max: [0.15,1.2]}, "spray" ]], - ["marker", [ 2.5, 0.12, null, null, 25, 0.4, {curve: [0.35,0.25], min_max: [1.5,1]}, "marker" ]], - ["marker2", [ 2.5, 0.12, null, null, 25, 0.35, {curve: [0.35,0.25], min_max: [1.3,0.95]}, "custom", - function (t) { - let scale = _gScale; - t.rect(-1.5 * scale,-1.5 * scale,3 * scale,3 * scale); t.rect(1 * scale,1 * scale,1 * scale,1 * scale) - }, "natural" - ]], - ]; - /** - * Iterates through the list of standard brushes and adds each one to the brush manager. - * The brush manager is assumed to be a global object `B` that has an `add` method. - */ - for (let s of _standard_brushes) { - let obj = {} - for (let i = 0; i < s[1].length; i++) obj[_vals[i]] = s[1][i] - add(s[0],obj) - } + +/** + * Defines a set of standard brushes with specific characteristics. Each brush is defined + * with properties such as weight, vibration, definition, quality, opacity, spacing, and + * pressure sensitivity. Some brushes have additional properties like type, tip, and rotate. + */ +const _vals = [ + "weight", + "vibration", + "definition", + "quality", + "opacity", + "spacing", + "pressure", + "type", + "tip", + "rotate", +]; +const _standard_brushes = [ + // Define each brush with a name and a set of parameters + // For example, the "pen" brush has a weight of 0.35, a vibration of 0.12, etc. + // The "marker2" brush has a custom tip defined by a function that draws rectangles. + [ + "pen", + [0.35, 0.12, 0.5, 8, 200, 0.3, { curve: [0.15, 0.2], min_max: [1.4, 0.9] }], + ], + [ + "rotring", + [0.2, 0.05, 1, 3, 250, 0.15, { curve: [0.05, 0.2], min_max: [1.7, 0.8] }], + ], + [ + "2B", + [0.35, 0.5, 0.1, 8, 180, 0.2, { curve: [0.15, 0.2], min_max: [1.3, 1] }], + ], + [ + "HB", + [0.3, 0.5, 0.4, 4, 180, 0.25, { curve: [0.15, 0.2], min_max: [1.2, 0.9] }], + ], + [ + "2H", + [0.2, 0.4, 0.3, 2, 150, 0.2, { curve: [0.15, 0.2], min_max: [1.2, 0.9] }], + ], + [ + "cpencil", + [0.4, 0.6, 0.8, 7, 120, 0.15, { curve: [0.15, 0.2], min_max: [0.95, 1.2] }], + ], + [ + "charcoal", + [0.5, 2, 0.8, 300, 110, 0.06, { curve: [0.15, 0.2], min_max: [1.3, 0.8] }], + ], + [ + "hatch_brush", + [0.2, 0.4, 0.3, 2, 150, 0.15, { curve: [0.5, 0.7], min_max: [1, 1.5] }], + ], + [ + "spray", + [ + 0.3, + 12, + 15, + 40, + 80, + 0.65, + { curve: [0, 0.1], min_max: [0.15, 1.2] }, + "spray", + ], + ], + [ + "marker", + [ + 2.5, + 0.12, + null, + null, + 25, + 0.4, + { curve: [0.35, 0.25], min_max: [1.5, 1] }, + "marker", + ], + ], + [ + "marker2", + [ + 2.5, + 0.12, + null, + null, + 25, + 0.35, + { curve: [0.35, 0.25], min_max: [1.3, 0.95] }, + "custom", + function (t) { + let scale = _gScale; + t.rect(-1.5 * scale, -1.5 * scale, 3 * scale, 3 * scale); + t.rect(1 * scale, 1 * scale, 1 * scale, 1 * scale); + }, + "natural", + ], + ], +]; +/** + * Iterates through the list of standard brushes and adds each one to the brush manager. + * The brush manager is assumed to be a global object `B` that has an `add` method. + */ +for (let s of _standard_brushes) { + let obj = {}; + for (let i = 0; i < s[1].length; i++) obj[_vals[i]] = s[1][i]; + add(s[0], obj); +}