// ─── Data ────────────────────────────────────────────────────────────────────── const S = { seed:0, tex:'solid', col:0, mat:0, deform:0, dist:0.30, freq:1.50, speed:0.60, size:1.00, rough:0.55, emit:0.10, opac:1.00 }; const SEEDS = [ { name:'Forma 1', build(geo){ const p=geo.attributes.position; for(let i=0;i0?1+z*0.14:1-(-z)*0.06; const rT=1-Math.pow(Math.max(0,(y-0.3)/0.7),2)*0.3; p.setXYZ(i,x*1.55*rT,y*1.0,z*1.18*bly);} }}, ]; // ─── Tipos (seed identity colours) ──────────────────────────────────────────── // Each tipo has a primary colour. Multiple active tipos blend into a mixed palette. const TIPOS = [ {name:'Ecossistema', col:'#54FBC2'}, {name:'Território', col:'#FFAE4B'}, {name:'Sociedade', col:'#F05D87'}, {name:'Governança', col:'#77C7E8'}, ]; // Institutional colours — single-select, replaces tipo system when active const INST_COLS = [ {hex:'#FF753F', dark:false, name:'Laranja'}, {hex:'#F9F4EE', dark:false, name:'Creme'}, {hex:'#1E160B', dark:true, name:'Escuro'}, {hex:'#FFFFFF', dark:false, name:'Branco'}, ]; let colMode = 'tipo'; // 'tipo' | 'inst' let activeInst = null; // index of selected inst colour, or null // Active tipos: Set of indices. At least one must be active. let activeTipos = new Set(); // no tipo selected — starts as base colour // Compute blended palette from active tipos const BASE_COL = '#F9F4EE'; function blendTipos(){ // Institutional mode: single solid colour if(colMode === 'inst' && activeInst !== null){ const c = INST_COLS[activeInst].hex; const r = hexToRgb(c); return {s:false, a:c, b:c, e:c, hueA:0, hueB:0, satA:1, solid:true, solidR:r.r/255, solidG:r.g/255, solidB:r.b/255}; } const active = [...activeTipos].map(i => TIPOS[i]); // No tipos selected — pure base colour, neutral hue if(active.length === 0){ return makePal(BASE_COL, BASE_COL, BASE_COL); } // 1 tipo — cor sólida do tipo, sem mistura com base no hue if(active.length === 1){ const c = active[0].col; // Both hueA and hueB = same tipo hue → uniform colour across the surface return makePal(c, c, c); } // Multiple tipos — spread colours across the surface // A = first tipo, B = average of all, E = last tipo let r=0,g=0,b=0; active.forEach(t => { const c=hexToRgb(t.col); r+=c.r; g+=c.g; b+=c.b; }); const n=active.length; const avg = rgbToHex(r/n, g/n, b/n); const a = active[0].col; const e = active[active.length-1].col; return makePal(a, avg, e); } // Linear blend between two hex colours at factor t (0=a, 1=b) function blendHex(hexA, hexB, t){ const a=hexToRgb(hexA), b=hexToRgb(hexB); return rgbToHex(a.r+(b.r-a.r)*t, a.g+(b.g-a.g)*t, a.b+(b.b-a.b)*t); } function hexToRgb(hex){ const r=parseInt(hex.slice(1,3),16); const g=parseInt(hex.slice(3,5),16); const b=parseInt(hex.slice(5,7),16); return {r,g,b}; } function rgbToHex(r,g,b){ return '#'+[r,g,b].map(v=>Math.round(v).toString(16).padStart(2,'0')).join(''); } function colToHue(hex){ // Convert hex colour to HSL hue (0-1) const {r,g,b} = hexToRgb(hex); const rf=r/255,gf=g/255,bf=b/255; const max=Math.max(rf,gf,bf),min=Math.min(rf,gf,bf),d=max-min; if(d===0) return 0; let h; if(max===rf) h=(gf-bf)/d%6; else if(max===gf) h=(bf-rf)/d+2; else h=(rf-gf)/d+4; return ((h/6)%1+1)%1; } function makePal(a,b,e){ // saturation: 0 when no tipos (base colour), scales with tipo count const nT=activeTipos.size; const satA=nT===0?0:nT===1?1.0:Math.min(0.92,0.7+nT*0.08); return {s:false, a, b, e, hueA:colToHue(a), hueB:colToHue(e||b), satA, solid:false, solidR:1,solidG:1,solidB:1}; } // Legacy PALS alias — build() reads currentPal let currentPal = blendTipos(); const PALS = [currentPal]; // single entry, updated on tipo change const MATS = [ {name:'Espectral', type:'spectral', rough:0.1, emit:0.10}, {name:'Iridesc.', type:'iridescent', rough:0.1, emit:0.10}, {name:'Neon Glow', type:'glow', rough:0.5, emit:0.85}, {name:'Chrome', type:'chrome', rough:0.05, emit:0.0}, {name:'Pincel', type:'nebula', rough:0.85, emit:0.0}, {name:'Hologram', type:'hologram', rough:0.0, emit:0.50}, {name:'Dark Neb.', type:'darkneb', rough:0.3, emit:0.20}, {name:'Vidro', type:'glass', rough:0.0, emit:0.0}, ]; const BGS = ['#1E160B','#F9F4EE','#FF753F']; const DEFORM_DEF = [ {dist:0.30,freq:1.50,speed:0.60}, // fBm Orgânico {dist:0.28,freq:1.20,speed:0.80}, // Ondas Radiais {dist:0.45,freq:1.00,speed:0.30}, // Celular / Lobos {dist:0.40,freq:1.80,speed:1.20}, // Pulse / Batida {dist:0.32,freq:1.60,speed:0.45}, // Sulcos / Veias {dist:0.38,freq:1.40,speed:0.70}, // Pinçamento {dist:0.45,freq:1.80,speed:0.35}, // Celular / Voronoi {dist:0.35,freq:1.20,speed:0.45}, // Torsão ]; // ─── GLSL ────────────────────────────────────────────────────────────────────── const NOISE_GLSL = ` vec3 _m3(vec3 x){return x-floor(x*(1./289.))*289.;} vec4 _m4(vec4 x){return x-floor(x*(1./289.))*289.;} vec4 _pm(vec4 x){return _m4(((x*34.)+1.)*x);} vec4 _ti(vec4 r){return 1.79284291400159-0.85373472095314*r;} float snoise(vec3 v){ const vec2 C=vec2(1./6.,1./3.);const vec4 D=vec4(0.,.5,1.,2.); vec3 i=floor(v+dot(v,C.yyy));vec3 x0=v-i+dot(i,C.xxx); vec3 g=step(x0.yzx,x0.xyz);vec3 l=1.-g; vec3 i1=min(g.xyz,l.zxy);vec3 i2=max(g.xyz,l.zxy); vec3 x1=x0-i1+C.xxx;vec3 x2=x0-i2+C.yyy;vec3 x3=x0-D.yyy; i=_m3(i); vec4 p=_pm(_pm(_pm(i.z+vec4(0.,i1.z,i2.z,1.))+i.y+vec4(0.,i1.y,i2.y,1.))+i.x+vec4(0.,i1.x,i2.x,1.)); float n_=.142857142857;vec3 ns=n_*D.wyz-D.xzx; vec4 j=p-49.*floor(p*ns.z*ns.z); vec4 x_=floor(j*ns.z);vec4 y_=floor(j-7.*x_); vec4 xv=x_*ns.x+ns.yyyy;vec4 yv=y_*ns.x+ns.yyyy; vec4 h=1.-abs(xv)-abs(yv); vec4 b0=vec4(xv.xy,yv.xy);vec4 b1=vec4(xv.zw,yv.zw); vec4 s0=floor(b0)*2.+1.;vec4 s1=floor(b1)*2.+1.; vec4 sh=-step(h,vec4(0.)); vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;vec4 a1=b1.xzyw+s1.xzyw*sh.zzww; vec3 p0=vec3(a0.xy,h.x);vec3 p1=vec3(a0.zw,h.y); vec3 p2=vec3(a1.xy,h.z);vec3 p3=vec3(a1.zw,h.w); vec4 nm=_ti(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3))); p0*=nm.x;p1*=nm.y;p2*=nm.z;p3*=nm.w; vec4 m=max(.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.); m=m*m;return 42.*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3))); } float fbm(vec3 p,float f,float t){ float v=0.,a=.5; v+=a*snoise(p*f+vec3(t*.38,t*.31,t*.27));a*=.5;f*=2.08; v+=a*snoise(p*f+vec3(t*.22,t*.41,t*.33));a*=.5;f*=2.12; v+=a*snoise(p*f+vec3(t*.17,t*.28,t*.45));a*=.5;f*=2.05; v+=a*snoise(p*f+vec3(t*.35,t*.19,t*.24)); return v; } float wfbm(vec3 p,float f,float t){ // Cellular lobes — 12 rounded bumps on the sphere surface // Creates morula/blastocyst look: cells separated by grooves float ts=t*0.12; vec3 pn=normalize(p); float n=0.; // 12 lobe directions (roughly icosahedral + face centres) // Each is a smooth cosine-power bump that slowly rotates for(int i=0;i<12;i++){ float fi=float(i); // Distribute directions via golden angle on sphere float phi=acos(1.-2.*(fi+.5)/12.); float theta=3.88322*(fi+.5); // golden angle * i vec3 d=vec3(sin(phi)*cos(theta), cos(phi), sin(phi)*sin(theta)); // Slow independent orbit per lobe float ph=fi*0.5236+ts*(0.25+fi*0.035); float ca=cos(ph*.6),sa=sin(ph*.6); vec3 rd=normalize(vec3(d.x*ca-d.z*sa, d.y+sin(ph*.4)*.12, d.x*sa+d.z*ca)); float dt=max(0.,dot(pn,rd)); // Power 5 gives compact rounded lobe, size varies slightly per lobe float sz=0.88+sin(fi*1.37)*.06; n+=pow(dt,5.)*sz; } n=n/3.2-0.5; // Micro-texture: fine noise for skin-like pores in the grooves n+=snoise(p*f*2.5+vec3(ts*.5,ts*.4,ts*.6))*.07; return n; } float rfbm(vec3 p,float f,float t){ float v=0.,a=.5,prev=1.; for(int i=0;i<4;i++){float n=1.-abs(snoise(p*f+vec3(t*.3,t*.25,t*.35)));v+=n*n*a*prev;prev=n;a*=.5;f*=2.1;} return v; } float vor(vec3 p){ vec3 ip=floor(p),fp=fract(p);float md=8.; vec3 nb,h,rp;float d; #define VC(ox,oy,oz) nb=vec3(float(ox),float(oy),float(oz));h=_m3(ip+nb+vec3(127.1,311.7,74.7));rp=fract(sin(h*vec3(127.1,311.7,74.7))*43758.5453);d=length(nb+rp-fp);md=min(md,d); VC(-1,-1,-1)VC(0,-1,-1)VC(1,-1,-1)VC(-1,0,-1)VC(0,0,-1)VC(1,0,-1)VC(-1,1,-1)VC(0,1,-1)VC(1,1,-1) VC(-1,-1,0)VC(0,-1,0)VC(1,-1,0)VC(-1,0,0)VC(0,0,0)VC(1,0,0)VC(-1,1,0)VC(0,1,0)VC(1,1,0) VC(-1,-1,1)VC(0,-1,1)VC(1,-1,1)VC(-1,0,1)VC(0,0,1)VC(1,0,1)VC(-1,1,1)VC(0,1,1)VC(1,1,1) #undef VC return md; } float deformN(vec3 p,float fr,float t,float mode){ float n=0.; if(mode<.5) n=fbm(p,fr,t); else if(mode<1.5){float r=length(p);n=sin(r*fr*5.5-t*3.+atan(p.z,p.x)*1.5)*.5+.5;n+=snoise(p*fr*.5+vec3(t*.2))*.3;n=n*2.-1.;} else if(mode<2.5) n=wfbm(p,fr,t); else if(mode<3.5){float s=sin(t*2.8)*.5+.5;n=sin(p.y*fr*4.+t*2.)*.4+sin(p.x*fr*3.2+t*1.7)*.3+sin(p.z*fr*5.+t*2.5)*.2+snoise(p*fr+vec3(t*.5))*.3;n*=.8+s*.4;} else if(mode<4.5){n=rfbm(p,fr,t)*2.-1.;n+=snoise(p*fr*3.+vec3(t*.15))*.15;} else if(mode<5.5){float lat=asin(clamp(p.y/max(length(p),.001),-1.,1.));n=fbm(p,fr,t)*.5+cos(lat*2.5)*sin(t*1.2)*.5+snoise(p*fr*2.+vec3(t*.4,0.,t*.3))*.25;} else if(mode<6.5){float v=vor(p*fr+vec3(t*.25,t*.18,t*.22));float v2=vor(p*fr*2.1+vec3(t*.35,t*.28,t*.15));n=(1.-v)*.7+(1.-v2)*.3;n=n*2.-1.;} else { // Torsão: helical twist — rotates surface around Y axis proportional to height // Creates elegant twisted/spiral organic forms float twist=p.y*fr*1.8+t*0.4; float ca=cos(twist),sa=sin(twist); vec3 tp=vec3(p.x*ca-p.z*sa, p.y, p.x*sa+p.z*ca); n=fbm(tp,fr*1.2,t*0.5)*0.9; // Add secondary wave for more organic feel n+=sin(p.y*fr*3.+t*1.2)*0.25; n+=snoise(tp*fr*2.+vec3(t*.2,t*.15,t*.18))*0.15; } return n; } `; // Main vertex shader (solid/glass materials) const VERT = NOISE_GLSL + ` uniform float uTime,uFreq,uAmp,uSpeed,uMode; varying vec3 vN,vP,vW; varying float vNoise,vY; void main(){ vec3 p=position; float t=uTime*uSpeed; float n=deformN(p,uFreq,t,uMode); vNoise=n; vY=p.y; vec3 d=p+normal*n*uAmp; vN=normalMatrix*normalize(d); vW=normalize(mat3(modelMatrix)*normalize(d)); vP=(modelMatrix*vec4(d,1.)).xyz; gl_Position=projectionMatrix*modelViewMatrix*vec4(d,1.); }`; // Static vertex (no noise deformation — used for cluster spheres & glass) const VERT_STATIC = ` varying vec3 vN,vP,vW; varying float vNoise,vY; void main(){ vN=normalMatrix*normal; vW=normalize(mat3(modelMatrix)*normal); vP=(modelMatrix*vec4(position,1.)).xyz; vNoise=0.; vY=position.y; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.); }`; // Wireframe vertex (barycentric) const VERT_WIRE = NOISE_GLSL + ` uniform float uTime,uFreq,uAmp,uSpeed,uMode; attribute vec3 bary; varying vec3 vBary,vW; varying float vNoise; void main(){ vec3 p=position; float t=uTime*uSpeed; float n=deformN(p,uFreq,t,uMode); vNoise=n; vBary=bary; vec3 d=p+normal*n*uAmp; vW=normalize(mat3(modelMatrix)*normalize(d)); gl_Position=projectionMatrix*modelViewMatrix*vec4(d,1.); }`; // ─── Colour helpers (shared GLSL) ────────────────────────────────────────────── // NOTE: uHueA, uHueB, uSatA declared here — every frag shader using COL_GLSL // must NOT redeclare them. FRAG_WIRE/FRAG_FLAT declare them separately. const COL_GLSL = ` vec3 hsl(float h,float s,float l){ vec3 rgb=clamp(abs(mod(h*6.+vec3(0.,4.,2.),6.)-3.)-1.,0.,1.); return l+s*(rgb-.5)*(1.-abs(2.*l-1.)); } // Smooth shortest-path hue blend (avoids jumping across the colour wheel) float hueBlend(float a,float b,float t){ float d=b-a; d=d-floor(d+.5); return fract(a+d*t); } // Tipo colour uniforms — declared once here, shared by all COL_GLSL shaders uniform float uHueA,uHueB,uSatA; // nSpec: spectral colour tinted towards active tipos // uSatA=0 → near-white base, uSatA=1 → pure solid tipo colour // Institutional solid colour — bypasses spectral system entirely uniform float uSolid; // 0=tipo mode, 1=institutional solid uniform vec3 uSolidCol; // the RGB institutional colour vec3 nSpec(vec3 W,float off,float t){ // Institutional mode: return solid colour with subtle shading from normal if(uSolid > .5){ float shade=clamp(W.y*.18+.88,0.72,1.08); // very subtle top/bottom shading return uSolidCol*shade; } float baseH=fract(atan(W.z,W.x)/(6.28318)+.5+W.y*.25+off+t*.06); float tipoH=hueBlend(uHueB,uHueA,W.y*.5+.5); float h=hueBlend(baseH,tipoH,uSatA); float sat=mix(.10,1.,uSatA); float lit=mix(.93,.58,uSatA*uSatA); return hsl(fract(h),sat,lit); } // Institutional solid colour — call at top of main() when uSolid>0.5 // Returns the solid colour with proper 3D diffuse shading vec4 solidCol(vec3 N,vec3 V,float opa){ vec3 L0=normalize(vec3(2.2,3.5,2.5)); vec3 L1=normalize(vec3(-2.,-1.2,1.8)); vec3 L2=normalize(vec3(-.5,1.2,-2.8)); float d=clamp(max(0.,dot(N,L0))*.75+.25+max(0.,dot(N,L1))*.25+max(0.,dot(N,L2))*.1,0.15,1.2); float spec=pow(max(0.,dot(normalize(V+L0),N)),32.)*.22; float rim=pow(1.-max(0.,dot(N,V)),4.)*.1; return vec4(clamp(uSolidCol*d+vec3(spec)+uSolidCol*rim,0.,1.),opa); } float fr(vec3 V,vec3 N,float pw){return pow(1.-max(0.,dot(V,N)),pw);} vec3 sky(vec3 R){ float up=clamp(R.y*.5+.5,0.,1.); return mix(vec3(.03,.03,.07),vec3(.45,.62,.9),up*up) +vec3(1.,.88,.65)*pow(max(0.,dot(R,normalize(vec3(1.5,2.,1.)))),60.)*2.; } float sp(vec3 N,vec3 V,float sh){ vec3 L0=normalize(vec3(3.,4.,3.));vec3 L1=normalize(vec3(-3.,-2.,3.)); return pow(max(0.,dot(reflect(-L0,N),V)),sh) +pow(max(0.,dot(reflect(-L1,N),V)),sh*.5)*.4; } `; // Fragment shaders // All COL_GLSL-based shaders share: hsl, hueBlend, nSpec, fr, sky, sp, uHueA, uHueB, uSatA // They additionally declare their own: uA, uB, uE, uRough, uOpac, uTime, uEmit + varyings const FRAG_SPECTRAL = COL_GLSL + ` uniform vec3 uA,uB,uE; uniform float uRough,uOpac,uTime,uEmit; varying vec3 vN,vP,vW; varying float vNoise,vY; void main(){ vec3 N=normalize(vN),V=normalize(cameraPosition-vP),WN=normalize(vW); if(uSolid>.5){gl_FragColor=solidCol(normalize(vN),normalize(cameraPosition-vP),uOpac);return;} vec3 c1=nSpec(WN,0.,uTime),c2=nSpec(WN,.33,uTime); vec3 col=mix(c1,c2,.4+vNoise*.3); col*=clamp(vNoise*.5+.75,.3,1.1); col+=vec3(sp(N,V,max(20.,160.-uRough*150.)))*.9; col+=nSpec(WN,.5,uTime)*fr(V,N,2.5)*.8; col=mix(col,col*uA*3.,uEmit*.3); gl_FragColor=vec4(clamp(col,0.,1.),uOpac); }`; const FRAG_IRIDESCENT = COL_GLSL + ` uniform vec3 uA,uB,uE; uniform float uRough,uOpac,uTime,uEmit; varying vec3 vN,vP,vW; varying float vNoise,vY; void main(){ vec3 N=normalize(vN),V=normalize(cameraPosition-vP),WN=normalize(vW); if(uSolid>.5){gl_FragColor=solidCol(normalize(vN),normalize(cameraPosition-vP),uOpac);return;} if(uSolid>.5){gl_FragColor=solidCol(normalize(vN),normalize(cameraPosition-vP),uOpac);return;} float cosNV=max(0.,dot(N,V)); float h1=cosNV*4.2+vNoise*1.8+uTime*.22+vY*.7; float h2=cosNV*6.5+vNoise*2.4-uTime*.15+vY*1.2; // Tint iridescent hues towards active tipos float ih1=hueBlend(hueBlend(uHueB,uHueA,cosNV),h1,1.-uSatA); float ih2=hueBlend(uHueA,h2,1.-uSatA); vec3 i1=hsl(fract(ih1),mix(.5,1.,uSatA),mix(.8,.55,uSatA)); vec3 i2=hsl(fract(ih2),mix(.4,.9,uSatA),.6)*.6; float f1=fr(V,N,2.),f2=fr(V,N,4.5); vec3 base=mix(uA,uB,clamp(vY*.5+.5,0.,1.)*.5+clamp(vNoise*.5+.5,0.,1.)*.5); vec3 col=base*.2+sky(reflect(-V,N))*(1.-uRough)*.35; col+=i1*(f1*.7+clamp(vNoise*.5+.5,0.,1.)*.25)+i2*f2*.5; col+=vec3(sp(N,V,max(30.,180.-uRough*170.))); col+=hsl(fract(ih1+.5),mix(.4,1.,uSatA),.7)*f2*.5+uE*uEmit*.7; gl_FragColor=vec4(clamp(col,0.,1.),uOpac); }`; const FRAG_GLOW = COL_GLSL + ` uniform vec3 uA,uB,uE; uniform float uRough,uOpac,uTime,uEmit; varying vec3 vN,vP,vW; varying float vNoise,vY; void main(){ vec3 N=normalize(vN),V=normalize(cameraPosition-vP),WN=normalize(vW); if(uSolid>.5){gl_FragColor=solidCol(normalize(vN),normalize(cameraPosition-vP),uOpac);return;} vec3 sc1=nSpec(WN,0.,uTime),sc2=nSpec(WN,.25,uTime); float pulse=(.7+.3*sin(uTime*1.9+vY*2.5+vNoise*1.8))*(.8+.2*sin(uTime*3.1-vNoise*3.2)); float rb=clamp(vNoise*2.+.3,0.,1.); vec3 emit=mix(sc1,sc2,.5)*uEmit*pulse*(1.5+rb*1.5); emit+=nSpec(WN,uTime*.05,0.)*uEmit*.4*pulse; float f1=fr(V,N,2.2),f2=fr(V,N,5.); vec3 col=sc1*.05+emit+sc2*f1*uEmit*1.4+vec3(f2)*.4; col+=vec3(sp(N,V,25.))*.4*mix(sc1,sc2,.5); gl_FragColor=vec4(clamp(col,0.,1.),uOpac); }`; const FRAG_CHROME = COL_GLSL + ` uniform vec3 uA,uB,uE; uniform float uRough,uOpac,uTime,uEmit; varying vec3 vN,vP,vW; varying float vNoise,vY; void main(){ vec3 N=normalize(vN+vec3(vNoise)*uRough*.25),V=normalize(cameraPosition-vP); if(uSolid>.5){gl_FragColor=solidCol(normalize(vN),normalize(cameraPosition-vP),uOpac);return;} vec3 WN=normalize(vW),R=reflect(-V,N); vec3 nc=nSpec(WN,uTime*.07,0.); vec3 col=sky(R)*(nc*.5+.5); col+=vec3(pow(max(0.,dot(reflect(-normalize(vec3(2.,3.,2.)),N),V)),max(4.,220.-uRough*210.))*1.5); col+=nc*fr(V,N,3.5)*.4; col=mix(col,nc*.3,uRough*.6)+uE*uEmit*.4; gl_FragColor=vec4(clamp(col,0.,1.),uOpac); }`; const FRAG_NEBULA = COL_GLSL + ` uniform vec3 uA,uB,uE; uniform float uRough,uOpac,uTime,uEmit; varying vec3 vN,vP,vW; varying float vNoise,vY; float h3(vec3 p){p=fract(p*vec3(443.8975,397.2973,491.1871));p+=dot(p,p.yzx+19.19);return fract((p.x+p.y)*p.z);} float vnoise(vec3 p){vec3 i=floor(p),f=fract(p);f=f*f*(3.-2.*f);return mix(mix(mix(h3(i),h3(i+vec3(1,0,0)),f.x),mix(h3(i+vec3(0,1,0)),h3(i+vec3(1,1,0)),f.x),f.y),mix(mix(h3(i+vec3(0,0,1)),h3(i+vec3(1,0,1)),f.x),mix(h3(i+vec3(0,1,1)),h3(i+vec3(1,1,1)),f.x),f.y),f.z)*2.-1.;} float brush(vec3 P,vec3 N,float freq){ vec3 up=abs(N.y)>.85?vec3(1.,0.,0.):vec3(0.,1.,0.); vec3 T=normalize(cross(N,up));vec3 B=cross(N,T); vec3 sp2=vec3(dot(P,T)*5.5*freq,dot(P,B)*1.1*freq,dot(P,N)*0.9*freq); float s=vnoise(sp2);s+=vnoise(sp2*vec3(1.,2.1,1.3)+vec3(3.7,1.2,5.1))*.5; s+=vnoise(sp2*vec3(1.,4.3,1.6)+vec3(7.3,2.8,1.4))*.25;return s*(1./1.75); } void main(){ vec3 N=normalize(vN),V=normalize(cameraPosition-vP); if(uSolid>.5){gl_FragColor=solidCol(normalize(vN),normalize(cameraPosition-vP),uOpac);return;} float tY=clamp(vY*.45+.5,0.,1.); vec3 base=nSpec(normalize(vW),0.,uTime); base=mix(base*1.05,base*.88,brush(vP,N,uRough*1.2+0.9)*.5+.5); vec3 L0=normalize(vec3(1.5,2.8,1.2)),L1=normalize(vec3(-1.8,-1.2,1.6)),L2=normalize(vec3(0.,-2.,-.8)); float d0=dot(N,L0)*.65+.35,d1=dot(N,L1)*.3+.25,d2=max(0.,dot(N,L2))*.15; vec3 col=base*(max(0.,d0)+max(0.,d1)+d2); col+=nSpec(normalize(vW),.5,uTime)*fr(V,N,4.)*.15; col+=uA*uEmit*.2; gl_FragColor=vec4(clamp(col,0.,1.),uOpac); }`; const FRAG_HOLOGRAM = COL_GLSL + ` uniform vec3 uA,uB,uE; uniform float uRough,uOpac,uTime,uEmit; varying vec3 vN,vP,vW; varying float vNoise,vY; void main(){ vec3 N=normalize(vN),V=normalize(cameraPosition-vP),WN=normalize(vW); if(uSolid>.5){gl_FragColor=solidCol(normalize(vN),normalize(cameraPosition-vP),uOpac);return;} float f1=fr(V,N,1.8),f2=fr(V,N,4.5); float scan=abs(sin((vY*14.+uTime*1.2)*3.14159))*.6+.4; scan*=abs(sin((vY*55.-uTime*.7)*3.14159))*.35+.65; vec3 band=nSpec(WN,uTime*.12,0.); float h2=hueBlend(uHueA,hueBlend(uHueB,uHueA,vY*.5+.5),uSatA*.5); vec3 band2=hsl(fract(h2+vY*.3+uTime*.2),mix(.5,1.,uSatA),mix(.8,.55,uSatA)); vec3 col=mix(band,band2,.5)*scan; col+=f1*(band*.6+vec3(.3,.7,1.)*.4)*1.4+f2*vec3(.9,.95,1.)*.7; col+=vec3(pow(max(0.,dot(reflect(-normalize(vec3(1.5,2.5,2.)),N),V)),60.))*1.2; col+=uE*uEmit*.5; float alpha=f1*.85+scan*.12+clamp(vNoise*.3,0.,.3); gl_FragColor=vec4(clamp(col,0.,1.),clamp(alpha,0.,1.)*uOpac); }`; const FRAG_DARKNEB = COL_GLSL + ` uniform vec3 uA,uB,uE; uniform float uRough,uOpac,uTime,uEmit; varying vec3 vN,vP,vW; varying float vNoise,vY; void main(){ vec3 N=normalize(vN),V=normalize(cameraPosition-vP),WN=normalize(vW); float cosNV=max(0.,dot(N,V)); float f1=fr(V,N,1.5),f2=fr(V,N,3.5),f3=fr(V,N,7.),f4=fr(V,N,12.); float fog=clamp(vNoise*.5+.35,0.,1.); vec3 interior=mix(vec3(.01,.01,.06),mix(vec3(.02,.03,.12)*fog,vec3(.04,.01,.08)*(1.-fog),.5),.7); float hueIn=cosNV*1.8+uTime*.08+vNoise*.3; // Tint inner glow towards tipo colours vec3 iriIn=mix(hsl(fract(hueBlend(uHueB,uHueA,f1)+.62),mix(.5,.95,uSatA),.55), hsl(fract(hueBlend(uHueA,uHueB,f1)+.78),mix(.5,.95,uSatA),.6),f1); vec3 golden=nSpec(WN,uTime*.04,0.); vec3 col=interior+iriIn*f1*1.1+nSpec(WN,.3,uTime)*f2*.7; col+=golden*(f3*2.2+f4*4.5); col+=vec3(sp(N,V,max(80.,200.-uRough*180.)))*.3+uE*uEmit*.4*golden; float alpha=mix(f2*uOpac,uOpac,clamp(f1*1.5,0.,1.)); gl_FragColor=vec4(clamp(col,0.,1.),clamp(alpha,0.,1.)); }`; const FRAG_GLASS = COL_GLSL + ` uniform vec3 uA,uB; uniform float uRough,uOpac,uTime; varying vec3 vN,vP,vW; varying float vNoise,vY; void main(){ vec3 N=normalize(vN),WN=normalize(vW),V=normalize(cameraPosition-vP),R=reflect(-V,N); if(uSolid>.5){gl_FragColor=solidCol(normalize(vN),normalize(cameraPosition-vP),uOpac);return;} float f1=fr(V,N,3.5),f2=fr(V,N,1.8); float cosA=max(0.,dot(V,N)); // Glass iridescence tinted by tipo vec3 iri=nSpec(WN,cosA*.8+uTime*.1,0.); float h2=hueBlend(uHueA,uHueB,cosA*2.8); vec3 iri2=hsl(fract(h2+uTime*.14),mix(.4,.9,uSatA),mix(.85,.6,uSatA)); float sh=max(8.,130.-uRough*120.); vec3 L0=normalize(vec3(1.8,2.5,2.)),L1=normalize(vec3(-2.,-1.5,1.5)); float s0=pow(max(0.,dot(reflect(-L0,N),V)),sh); float s1=pow(max(0.,dot(reflect(-L1,N),V)),sh*.6)*.5; float c0=pow(max(0.,dot(WN,normalize(vec3(.4,.7,.6)))),10.)*.9; float c1=pow(max(0.,dot(WN,normalize(vec3(-.5,.2,.8)))),14.)*.5; vec3 col=(iri*.7+vec3(.8,.9,1.)*.3)*c0+(iri2*.6+vec3(.7,.85,1.)*.4)*c1; col+=iri*f2*.6+sky(R)*f1*.4+vec3(s0+s1)+(iri2*.4+vec3(.9,.95,1.)*.6)*f1*.9; float alpha=f1*.88+f2*.2+(s0+s1)*.45+(c0+c1)*.2; gl_FragColor=vec4(clamp(col,0.,1.),clamp(alpha,0.,1.)*uOpac); }`; // Wire and Flat: standalone shaders — declare hsl, hueBlend, and tipo uniforms themselves // ─── Shared GLSL helpers (for standalone shaders that don't use COL_GLSL) ───── const SHARED_GLSL = ` vec3 hsl(float h,float s,float l){ vec3 rgb=clamp(abs(mod(h*6.+vec3(0.,4.,2.),6.)-3.)-1.,0.,1.); return l+s*(rgb-.5)*(1.-abs(2.*l-1.)); } float hueBlend(float a,float b,float t){float d=b-a;d=d-floor(d+.5);return fract(a+d*t);} uniform float uHueA,uHueB,uSatA,uSolid; uniform vec3 uSolidCol; `; const FRAG_WIRE = SHARED_GLSL + ` varying vec3 vBary,vW; varying float vNoise; uniform float uOpac,uTime; void main(){ float edge=min(vBary.x,min(vBary.y,vBary.z)); float alpha=1.-smoothstep(0.,.008,edge); if(alpha<.01) discard; vec3 WN=normalize(vW); vec3 wcol; if(uSolid>.5){ float shade=clamp(WN.y*.18+.88,0.72,1.08); wcol=uSolidCol*shade; } else { float baseH=fract(atan(WN.z,WN.x)/6.28318+.5+WN.y*.25+uTime*.07); float tipoH=hueBlend(uHueB,uHueA,WN.y*.5+.5); float hue=hueBlend(baseH,tipoH,uSatA); float sat=mix(.12,1.,uSatA); float lit=mix(.88,.58,uSatA*uSatA); wcol=hsl(hue,sat,lit); } gl_FragColor=vec4(wcol,alpha*uOpac); }`; const FRAG_FLAT = SHARED_GLSL + ` varying vec3 vN,vP,vW; varying float vNoise,vY; uniform float uOpac,uTime; void main(){ vec3 WN=normalize(vW); vec3 fcol; if(uSolid>.5){ float shade=clamp(WN.y*.18+.88,0.72,1.08); fcol=uSolidCol*shade; } else { float baseH=fract(atan(WN.z,WN.x)/6.28318+.5+WN.y*.25+uTime*.05); float tipoH=hueBlend(uHueB,uHueA,WN.y*.5+.5); float hue=hueBlend(baseH,tipoH,uSatA); float sat=mix(.10,1.,uSatA); float lit=mix(.92,.58,uSatA*uSatA); fcol=hsl(hue,sat,lit); } gl_FragColor=vec4(fcol,uOpac); }`; // ─── Points shaders (defined once, reused on every build) ───────────────────── const FRAG_PTS = SHARED_GLSL + ` uniform float uOpac,uTime; varying vec3 vW; varying float vNoise; void main(){ vec2 c=gl_PointCoord*2.-1.; if(dot(c,c)>1.) discard; vec3 WN=normalize(vW); vec3 pcol; if(uSolid>.5){ float shade=clamp(WN.y*.18+.88,0.72,1.08); pcol=uSolidCol*shade; } else { float baseH=fract(atan(WN.z,WN.x)/6.28318+.5+WN.y*.25+uTime*.06); float tipoH=hueBlend(uHueB,uHueA,WN.y*.5+.5); float h=hueBlend(baseH,tipoH,uSatA); float sat=mix(.10,1.,uSatA); float lit=mix(.93,.58,uSatA*uSatA); pcol=hsl(h,sat,lit); } gl_FragColor=vec4(pcol,uOpac); }`; const FRAG_PTS_M = SHARED_GLSL + ` uniform float uOpac,uTime; varying vec3 vW; varying float vNoise; void main(){ vec2 c=gl_PointCoord*2.-1.; if(dot(c,c)>1.) discard; vec3 WN=normalize(vW); vec3 pcol; if(uSolid>.5){ float shade=clamp(WN.y*.18+.88,0.72,1.08); pcol=uSolidCol*shade; } else { float baseH=fract(atan(WN.z,WN.x)/6.28318+.5+WN.y*.25+uTime*.06); float tipoH=hueBlend(uHueB,uHueA,WN.y*.5+.5); float h=hueBlend(baseH,tipoH,uSatA); float sat=mix(.10,1.,uSatA); float lit=mix(.93,.58,uSatA*uSatA); pcol=hsl(h,sat,lit); } gl_FragColor=vec4(pcol,uOpac); }`; // VERT_PTS: VERT with gl_PointSize injected (built once from VERT at runtime) const VERT_PTS = VERT.replace( 'gl_Position=projectionMatrix*modelViewMatrix*vec4(d,1.);', 'gl_PointSize=max(0.8,1.8-length((modelViewMatrix*vec4(d,1.)).xyz)*.2);\n gl_Position=projectionMatrix*modelViewMatrix*vec4(d,1.);' ); const VERT_PTS_M = VERT_STATIC.replace( 'gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);', 'gl_PointSize=max(0.8,1.6-length((modelViewMatrix*vec4(position,1.)).xyz)*.18);\n gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);' ); // ─── Three.js setup ──────────────────────────────────────────────────────────── const PW=()=>window.innerWidth-268, PH=()=>window.innerHeight; const renderer=new THREE.WebGLRenderer({antialias:true,powerPreference:'high-performance',preserveDrawingBuffer:true}); renderer.setPixelRatio(Math.min(devicePixelRatio,2)); renderer.setSize(PW(),PH()); renderer.toneMapping=THREE.ACESFilmicToneMapping; renderer.toneMappingExposure=1.3; document.body.appendChild(renderer.domElement); const scene=new THREE.Scene(); scene.background=new THREE.Color(BGS[0]); const camera=new THREE.PerspectiveCamera(48,PW()/PH(),.1,100); camera.position.z=4.2; const aL=new THREE.AmbientLight(0xffffff,.2); scene.add(aL); const pLs=[ new THREE.PointLight(0xd4a870,5,22),new THREE.PointLight(0x7090b0,3,22), new THREE.PointLight(0xc07040,2,22),new THREE.PointLight(0x90b070,1.5,22), ]; pLs.forEach(l=>scene.add(l)); // PMREMGenerator for env maps const pmrem=new THREE.PMREMGenerator(renderer); pmrem.compileEquirectangularShader(); let envTex=null; function makeEnv(cA,cB){ const sc=new THREE.Scene(); sc.add(new THREE.HemisphereLight(cA,cB,1.5)); const kl=new THREE.DirectionalLight(0xffffff,1.2); kl.position.set(3,5,3); sc.add(kl); const fl=new THREE.DirectionalLight(cB,0.6); fl.position.set(-4,-2,2); sc.add(fl); if(envTex) envTex.dispose(); envTex=pmrem.fromScene(sc).texture; return envTex; } let grp=new THREE.Group(); scene.add(grp); let uni=null, clusterUnis=[]; // ─── Geometry ────────────────────────────────────────────────────────────────── function buildGeo(idx){ const geo=new THREE.SphereGeometry(1,112,112); SEEDS[idx].build(geo); geo.computeVertexNormals(); return geo; } function addBary(geo){ const g=geo.index?geo.toNonIndexed():geo.clone(); const n=g.attributes.position.count; const b=new Float32Array(n*3); for(let i=0;i{ if(m.map) m.map.dispose(); m.dispose(); }); } } // ─── Material factory (used by all sub-builders) ─────────────────────────────── // isStatic: use VERT_STATIC (no noise deform). opa: opacity override. function makeMat(isStatic, opa, pal, mat, cA, cB, cE){ if(S.tex==='wire'){ return new THREE.MeshBasicMaterial({wireframe:true,vertexColors:true,transparent:true,opacity:opa}); } if(S.tex==='points'){ const gu={uTime:{value:0},uOpac:{value:opa}, uHueA:{value:pal.hueA||0},uHueB:{value:pal.hueB||0},uSatA:{value:pal.satA||0}, uSolid:{value:pal.solid?1:0}, uSolidCol:{value:new THREE.Color(pal.solidR||1,pal.solidG||1,pal.solidB||1)}}; clusterUnis.push(gu); return new THREE.ShaderMaterial({ vertexShader:VERT_PTS_M,fragmentShader:FRAG_PTS_M, uniforms:gu,transparent:true,depthWrite:false}); } if(S.tex==='flat'){ const gu={uTime:{value:0},uFreq:{value:S.freq},uAmp:{value:isStatic?0:S.dist}, uSpeed:{value:S.speed},uMode:{value:S.deform},uOpac:{value:opa}, uHueA:{value:pal.hueA||0},uHueB:{value:pal.hueB||0},uSatA:{value:pal.satA||0}, uSolid:{value:pal.solid?1:0}, uSolidCol:{value:new THREE.Color(pal.solidR||1,pal.solidG||1,pal.solidB||1)}}; clusterUnis.push(gu); return new THREE.ShaderMaterial({ vertexShader:isStatic?VERT_STATIC:VERT,fragmentShader:FRAG_FLAT, uniforms:gu,transparent:opa<1,side:THREE.DoubleSide}); } // Solid materials const FRAG_MAP={spectral:FRAG_SPECTRAL,iridescent:FRAG_IRIDESCENT,glow:FRAG_GLOW, chrome:FRAG_CHROME,nebula:FRAG_NEBULA,hologram:FRAG_HOLOGRAM,darkneb:FRAG_DARKNEB}; if(mat.type==='glass'){ const gu={uA:{value:cA.clone()},uB:{value:cB.clone()}, uRough:{value:S.rough},uOpac:{value:opa},uTime:{value:0}, uHueA:{value:pal.hueA||0},uHueB:{value:pal.hueB||0},uSatA:{value:pal.satA||0}, uSolid:{value:pal.solid?1:0}, uSolidCol:{value:new THREE.Color(pal.solidR||1,pal.solidG||1,pal.solidB||1)}}; clusterUnis.push(gu); return new THREE.ShaderMaterial({ vertexShader:VERT_STATIC,fragmentShader:FRAG_GLASS, uniforms:gu,transparent:true,side:THREE.DoubleSide,depthWrite:false}); } const frag=FRAG_MAP[mat.type]||FRAG_SPECTRAL; const isTr=['hologram','darkneb'].includes(mat.type)||opa<1; const gu={uTime:{value:0},uFreq:{value:S.freq},uAmp:{value:isStatic?0:S.dist}, uSpeed:{value:S.speed},uMode:{value:S.deform}, uA:{value:cA.clone()},uB:{value:cB.clone()},uE:{value:cE.clone()}, uEmit:{value:S.emit},uOpac:{value:opa},uRough:{value:S.rough}, uHueA:{value:pal.hueA||0},uHueB:{value:pal.hueB||0},uSatA:{value:pal.satA||0}, uSolid:{value:pal.solid?1:0}, uSolidCol:{value:new THREE.Color(pal.solidR||1,pal.solidG||1,pal.solidB||1)}}; if(!uni) uni=gu; clusterUnis.push(gu); return new THREE.ShaderMaterial({ vertexShader:isStatic?VERT_STATIC:VERT,fragmentShader:frag, uniforms:gu,transparent:isTr,side:THREE.DoubleSide,depthWrite:!isTr}); } // ─── Sub-builders (each handles one surface mode) ────────────────────────────── function buildCluster(geo, pal, mat, cA, cB, cE){ const defs=[ [0,.1,0,.42,0,0,0],[.32,.1,.05,.28,0,0,0],[-.22,.18,-.08,.22,0,0,0],[.12,-.15,.1,.18,0,0,0], [-.55,-.2,.1,.30,.35,.30,0],[.55,.45,-.05,.38,.28,.20,1.05], [-.30,-.55,.2,.12,.55,.15,2.09],[.10,.60,0,.10,.45,.25,3.14], [-.60,.35,.1,.055,.70,.18,.52],[.45,-.5,.05,.045,.65,.22,1.57],[-.15,-.7,.08,.038,.80,.12,4.19], ]; const cms=[]; const FRAG_MAP={spectral:FRAG_SPECTRAL,iridescent:FRAG_IRIDESCENT,glow:FRAG_GLOW, chrome:FRAG_CHROME,nebula:FRAG_NEBULA,hologram:FRAG_HOLOGRAM,darkneb:FRAG_DARKNEB}; defs.forEach(([bx,by,bz,r,os,or_,op])=>{ const g=buildGeo(S.seed); const p=g.attributes.position; for(let v=0;v.15?S.opac:S.opac*.75; const m=makeMat(false,opa,pal,mat,cA,cB,cE); const mesh=(S.tex==='points')?new THREE.Points(g,m):new THREE.Mesh(g,m); mesh.position.set(bx,by,bz); grp.add(mesh); if(mat.type==='glass'){ const bk={uA:{value:cA.clone()},uB:{value:cB.clone()}, uRough:{value:S.rough},uOpac:{value:opa*.45},uTime:{value:0}}; clusterUnis.push(bk); const backMesh=new THREE.Mesh(g.clone(),new THREE.ShaderMaterial({ vertexShader:VERT_STATIC,fragmentShader:FRAG_GLASS, uniforms:bk,transparent:true,side:THREE.BackSide,depthWrite:false})); backMesh.position.set(bx,by,bz); grp.add(backMesh); } cms.push({mesh,bx,by,bz,os,or:or_,op}); }); grp.userData.cms=cms; grp.scale.setScalar(S.size*1.8); } function buildWire(geo, pal){ const wg=addBary(geo); uni={uTime:{value:0},uFreq:{value:S.freq},uAmp:{value:S.dist}, uSpeed:{value:S.speed},uMode:{value:S.deform},uOpac:{value:S.opac}, uHueA:{value:pal.hueA||0},uHueB:{value:pal.hueB||0},uSatA:{value:pal.satA||0}, uSolid:{value:pal.solid?1:0}, uSolidCol:{value:new THREE.Color(pal.solidR||1,pal.solidG||1,pal.solidB||1)}}; grp.add(new THREE.Mesh(wg,new THREE.ShaderMaterial({ vertexShader:VERT_WIRE,fragmentShader:FRAG_WIRE, uniforms:uni,transparent:true,depthWrite:false,side:THREE.DoubleSide}))); grp.scale.setScalar(S.size); } function buildPoints(geo, pal, cA, cB, cE){ uni={uTime:{value:0},uFreq:{value:S.freq},uAmp:{value:S.dist}, uSpeed:{value:S.speed},uMode:{value:S.deform},uOpac:{value:S.opac}, uHueA:{value:pal.hueA||0},uHueB:{value:pal.hueB||0},uSatA:{value:pal.satA||0}, uSolid:{value:pal.solid?1:0}, uSolidCol:{value:new THREE.Color(pal.solidR||1,pal.solidG||1,pal.solidB||1)}, uA:{value:cA.clone()},uB:{value:cB.clone()},uE:{value:cE.clone()},uEmit:{value:S.emit}}; grp.add(new THREE.Points(geo,new THREE.ShaderMaterial({ vertexShader:VERT_PTS,fragmentShader:FRAG_PTS, uniforms:uni,transparent:true,depthWrite:false}))); grp.scale.setScalar(S.size); } function buildSolid(geo, pal, mat, cA, cB, cE){ if(mat.type==='glass'){ const gu={uA:{value:cA.clone()},uB:{value:cB.clone()},uRough:{value:S.rough},uOpac:{value:S.opac},uTime:{value:0}}; const bk={uA:{value:cA.clone()},uB:{value:cB.clone()},uRough:{value:S.rough},uOpac:{value:S.opac*.45},uTime:{value:0}}; grp.add(new THREE.Mesh(geo.clone(),new THREE.ShaderMaterial({vertexShader:VERT_STATIC,fragmentShader:FRAG_GLASS,uniforms:bk,transparent:true,side:THREE.BackSide,depthWrite:false}))); grp.add(new THREE.Mesh(geo,new THREE.ShaderMaterial({vertexShader:VERT_STATIC,fragmentShader:FRAG_GLASS,uniforms:gu,transparent:true,side:THREE.DoubleSide,depthWrite:false}))); grp.userData.glassUni=gu; grp.userData.glassBack=bk; } else { clusterUnis=[]; const m=makeMat(false,S.opac,pal,mat,cA,cB,cE); grp.add(new THREE.Mesh(geo,m)); if(mat.type==='glow'){ const hg=buildGeo(S.seed); grp.add(new THREE.Mesh(hg,new THREE.MeshBasicMaterial({color:cB,transparent:true,opacity:.06,side:THREE.BackSide}))); } } grp.scale.setScalar(S.size); } function build(){ // Dispose GPU resources before clearing grp.children.forEach(disposeObj); while(grp.children.length) grp.remove(grp.children[0]); grp.userData={}; uni=null; clusterUnis=[]; PALS[0]=blendTipos(); currentPal=PALS[0]; const pal=PALS[0]; const mat=MATS[S.mat]; const cA=new THREE.Color(pal.a); const cB=new THREE.Color(pal.b); const cE=new THREE.Color(pal.e); const env=makeEnv(cA.clone(),cB.clone()); scene.environment=env; pLs[0].color.copy(cA); pLs[1].color.copy(cB); pLs[2].color.set(pal.e); if(S.tex==='cluster'){ buildCluster(null,pal,mat,cA,cB,cE); return; } const geo=buildGeo(S.seed); if(S.tex==='wire') { buildWire(geo,pal); return; } if(S.tex==='points') { buildPoints(geo,pal,cA,cB,cE); return; } buildSolid(geo,pal,mat,cA,cB,cE); } function syncU(){ if(grp.userData.cms){build();return;} PALS[0] = blendTipos(); currentPal = PALS[0]; const pal=PALS[0]; const cA=new THREE.Color(pal.a),cB=new THREE.Color(pal.b),cE=new THREE.Color(pal.e); pLs[0].color.copy(cA);pLs[1].color.copy(cB);pLs[2].color.set(pal.e); // Sync all tracked uniforms (covers both uni and clusterUnis) const all=[uni,...clusterUnis].filter(Boolean); all.forEach(u=>{ if(!u) return; if(u.uA) u.uA.value.copy(cA); if(u.uB) u.uB.value.copy(cB); if(u.uE) u.uE.value.copy(cE); if(u.uEmit!==undefined) u.uEmit.value=S.emit; if(u.uHueA!==undefined) u.uHueA.value=pal.hueA||0; if(u.uHueB!==undefined) u.uHueB.value=pal.hueB||0; if(u.uSatA!==undefined) u.uSatA.value=pal.satA||0; if(u.uSolid!==undefined) u.uSolid.value=pal.solid?1:0; if(u.uSolidCol!==undefined) u.uSolidCol.value.setRGB(pal.solidR||1,pal.solidG||1,pal.solidB||1); if(u.uOpac!==undefined) u.uOpac.value=S.opac; if(u.uRough!==undefined) u.uRough.value=S.rough; if(u.uFreq!==undefined) u.uFreq.value=S.freq; if(u.uAmp!==undefined) u.uAmp.value=S.dist; if(u.uSpeed!==undefined) u.uSpeed.value=S.speed; if(u.uMode!==undefined) u.uMode.value=S.deform; }); if(grp.userData.glassUni){ const g=grp.userData.glassUni,b=grp.userData.glassBack; g.uA.value.copy(cA);g.uB.value.copy(cB);g.uRough.value=S.rough;g.uOpac.value=S.opac; if(b){b.uA.value.copy(cA);b.uB.value.copy(cB);b.uRough.value=S.rough;b.uOpac.value=S.opac*.45;} } } // ─── Orbit ───────────────────────────────────────────────────────────────────── let drag=false,prev={x:0,y:0},rot={x:0,y:0},tgt={x:0,y:0}; renderer.domElement.addEventListener('mousedown',e=>{drag=true;prev={x:e.clientX,y:e.clientY}}); renderer.domElement.addEventListener('touchstart',e=>{drag=true;prev={x:e.touches[0].clientX,y:e.touches[0].clientY}}); window.addEventListener('mousemove',e=>{if(!drag)return;tgt.y+=(e.clientX-prev.x)*.005;tgt.x+=(e.clientY-prev.y)*.005;prev={x:e.clientX,y:e.clientY}}); window.addEventListener('touchmove',e=>{if(!drag)return;tgt.y+=(e.touches[0].clientX-prev.x)*.005;tgt.x+=(e.touches[0].clientY-prev.y)*.005;prev={x:e.touches[0].clientX,y:e.touches[0].clientY}}); window.addEventListener('mouseup',()=>drag=false); window.addEventListener('touchend',()=>drag=false); renderer.domElement.addEventListener('wheel',e=>{camera.position.z=Math.max(1.5,Math.min(9,camera.position.z+e.deltaY*.01));},{passive:true}); // ─── Animate ─────────────────────────────────────────────────────────────────── const clock=new THREE.Clock(); function animate(){ requestAnimationFrame(animate); const t=clock.getElapsedTime(); if(uni) uni.uTime.value=t; if(grp.userData.glassUni) grp.userData.glassUni.uTime.value=t; if(grp.userData.glassBack) grp.userData.glassBack.uTime.value=t; clusterUnis.forEach(u=>{if(u&&u.uTime)u.uTime.value=t;}); rot.x+=(tgt.x-rot.x)*.07; rot.y+=(tgt.y-rot.y)*.07; if(!drag) tgt.y+=.0022; grp.rotation.set(rot.x,rot.y,0); grp.scale.setScalar((grp.scale.x>0?grp.scale.x:S.size)*(1+Math.sin(t*.55)*.008)/(1+Math.sin((t-.016)*.55)*.008)); // Scale breathing const sc=S.size*(1+Math.sin(t*.55)*.008); grp.scale.setScalar(sc); // Cluster float if(grp.userData.cms){ grp.userData.cms.forEach(({mesh,bx,by,bz,os,or:orr,op})=>{ const a=t*os+op; mesh.position.set(bx+Math.cos(a)*orr,by+Math.sin(t*.4+op)*.05+Math.sin(t*os*.7)*orr*.4,bz+Math.sin(a)*orr*.6); mesh.rotation.y=t*.3+op;mesh.rotation.x=t*.18+op*.5; }); } // Orbit lights pLs.forEach((l,i)=>{const a=t*.2+i*1.57;l.position.set(Math.cos(a)*6,Math.sin(a*.65)*5,Math.sin(a)*4);}); renderer.render(scene,camera); } // ─── UI ──────────────────────────────────────────────────────────────────────── // Seeds const seedGrid=document.getElementById('seedGrid'); const ICONS=[ ``, ``, ``, ``, ``, ]; SEEDS.forEach((sd,i)=>{ const d=document.createElement('div'); d.className='sb'+(i===0?' active':''); d.innerHTML=`${ICONS[i]}${sd.name}`; d.onclick=()=>{document.querySelectorAll('.sb').forEach(b=>b.classList.remove('active'));d.classList.add('active');S.seed=i;build();}; seedGrid.appendChild(d); }); // Materials const matGrid=document.getElementById('matGrid'); MATS.forEach((m,i)=>{ const pal=PALS[S.col]; const wrap=document.createElement('div');wrap.className='ms'; const circ=document.createElement('div');circ.className='mc'+(i===0?' active':''); if(i===0) circ.style.background='conic-gradient(red,orange,yellow,lime,cyan,blue,violet,red)'; else circ.style.background=`radial-gradient(circle at 33% 33%,${pal.b},${pal.a} 55%,#050005)`; circ.onclick=()=>{ document.querySelectorAll('.mc').forEach(c=>c.classList.remove('active')); circ.classList.add('active'); S.mat=i;S.rough=m.rough;S.emit=m.emit||0.1; document.getElementById('sRough').value=S.rough;document.getElementById('vRough').textContent=S.rough.toFixed(2); document.getElementById('sEmit').value=S.emit;document.getElementById('vEmit').textContent=S.emit.toFixed(2); build(); }; const lbl=document.createElement('p');lbl.className='mn';lbl.textContent=m.name; wrap.appendChild(circ);wrap.appendChild(lbl);matGrid.appendChild(wrap); }); // Colour mode switcher window.setColMode = function(mode){ colMode = mode; document.getElementById('tabTipo').classList.toggle('active', mode==='tipo'); document.getElementById('tabInst').classList.toggle('active', mode==='inst'); document.getElementById('paneTipo').style.display = mode==='tipo' ? '' : 'none'; document.getElementById('paneInst').style.display = mode==='inst' ? '' : 'none'; build(); }; // Institutional swatches const instGrid = document.getElementById('instGrid'); INST_COLS.forEach((ic, i) => { const sw = document.createElement('div'); sw.className = 'inst-swatch'; if(ic.dark) sw.dataset.instDark = '1'; sw.style.background = ic.hex; sw.style.border = ic.hex === '#FFFFFF' ? '2px solid rgba(0,0,0,0.12)' : '2px solid transparent'; sw.title = ic.name; sw.onclick = () => { if(activeInst === i){ // Deselect → back to base colour activeInst = null; sw.classList.remove('active'); } else { activeInst = i; document.querySelectorAll('.inst-swatch').forEach(s => s.classList.remove('active')); sw.classList.add('active'); } build(); }; instGrid.appendChild(sw); }); // Tipos const tipoGrid=document.getElementById('tipoGrid'); const mixBar=document.getElementById('mixBar'); function updateMixBar(){ const active=[...activeTipos].map(i=>TIPOS[i].col); if(active.length===0){ mixBar.style.background=BASE_COL; mixBar.style.opacity='0.35'; } else if(active.length===1){ mixBar.style.background=`linear-gradient(90deg,${BASE_COL},${active[0]})`; mixBar.style.opacity='0.75'; } else { const allCols=[BASE_COL,...active]; const stops=allCols.map((c,i)=>`${c} ${Math.round(i/(allCols.length-1)*100)}%`).join(','); mixBar.style.background=`linear-gradient(90deg,${stops})`; mixBar.style.opacity='0.75'; } } TIPOS.forEach((tp,i)=>{ const btn=document.createElement('div'); btn.className='tipo-btn'; // no tipo active on load btn.style.setProperty('--tipo-col',tp.col); btn.innerHTML=`
${tp.name}`; btn.onclick=()=>{ if(activeTipos.has(i)){ // Allow full deselection — base colour state activeTipos.delete(i); btn.classList.remove('active'); } else { activeTipos.add(i); btn.classList.add('active'); } updateMixBar(); build(); }; tipoGrid.appendChild(btn); }); updateMixBar(); // Backgrounds const bgRow=document.getElementById('bgRow'); BGS.forEach((bg,i)=>{ const d=document.createElement('div');d.className='bs'+(i===0?' active':''); d.style.background=bg; d.onclick=()=>{document.querySelectorAll('.bs').forEach(b=>b.classList.remove('active'));d.classList.add('active');scene.background=new THREE.Color(bg);}; bgRow.appendChild(d); }); // Texture window.setTex=(t,btn)=>{ document.querySelectorAll('.tb').forEach(b=>b.classList.remove('active')); btn.classList.add('active');S.tex=t;build(); }; // Deform window.setDeform=(idx,btn)=>{ document.querySelectorAll('.db').forEach(b=>b.classList.remove('active')); btn.classList.add('active');S.deform=idx; document.getElementById('shapeSliders').style.display=''; const d=DEFORM_DEF[idx]; S.dist=d.dist;S.freq=d.freq;S.speed=d.speed; document.getElementById('sDist').value=d.dist;document.getElementById('vDist').textContent=d.dist.toFixed(2); document.getElementById('sFreq').value=d.freq;document.getElementById('vFreq').textContent=d.freq.toFixed(2); document.getElementById('sSpeed').value=d.speed;document.getElementById('vSpeed').textContent=d.speed.toFixed(2); syncU(); }; // Sliders function sl(id,vid,key,rb){ const el=document.getElementById(id),vo=document.getElementById(vid); el.addEventListener('input',()=>{const v=parseFloat(parseFloat(el.value).toFixed(2));vo.textContent=v;S[key]=v;rb?build():syncU();}); } sl('sDist','vDist','dist');sl('sFreq','vFreq','freq');sl('sSpeed','vSpeed','speed'); sl('sSize','vSize','size',true);sl('sRough','vRough','rough');sl('sEmit','vEmit','emit');sl('sOpac','vOpac','opac'); window.addEventListener('resize',()=>{camera.aspect=PW()/PH();camera.updateProjectionMatrix();renderer.setSize(PW(),PH());}); // ─── Capture ─────────────────────────────────────────────────────────────────── let seqOpen=false,seqRunning=false; function flash(){const f=document.getElementById('flash');f.classList.add('on');setTimeout(()=>f.classList.remove('on'),120);} // Render at 2K (2× pixel ratio) and restore afterwards function renderHiRes(fn){ const origRatio = renderer.getPixelRatio(); // 2K target: enough to reach ~2560px on a 1280px viewport const hiRatio = Math.max(origRatio, 2560 / Math.max(PW(), PH())); renderer.setPixelRatio(hiRatio); renderer.setSize(PW(), PH()); renderer.render(scene, camera); fn(); // Restore original ratio renderer.setPixelRatio(origRatio); renderer.setSize(PW(), PH()); renderer.render(scene, camera); } window.capFrame=()=>{ flash(); renderHiRes(()=>{ const a=document.createElement('a'); a.href=renderer.domElement.toDataURL('image/png'); a.download='seed-'+new Date().toISOString().slice(0,19).replace(/[:T]/g,'-')+'.png'; a.click(); }); }; window.toggleSeq=e=>{e.preventDefault();seqOpen=!seqOpen;document.getElementById('seqP').classList.toggle('open',seqOpen);}; document.addEventListener('click',e=>{if(seqOpen&&!e.target.closest('#seqP')&&!e.target.closest('#capBtn')){seqOpen=false;document.getElementById('seqP').classList.remove('open');}}); window.startSeq=async()=>{ if(seqRunning)return;seqRunning=true; const btn=document.getElementById('seqBtn'),bw=document.getElementById('seqBar-wrap'),bar=document.getElementById('seqBar'); const frames=parseInt(document.getElementById('sFrames').value); const delay=parseInt(document.getElementById('sDelay').value); btn.disabled=true;btn.textContent='⏳ Capturando...';bw.style.display='block';bar.style.width='0%'; const ts=new Date().toISOString().slice(0,19).replace(/[:T]/g,'-'); for(let i=0;isetTimeout(r,delay)); renderHiRes(()=>{ const a=document.createElement('a');a.href=renderer.domElement.toDataURL('image/png'); a.download='seed-'+ts+'-f'+String(i+1).padStart(3,'0')+'.png';a.click(); }); await new Promise(r=>setTimeout(r,80)); } bar.style.width='100%';flash(); setTimeout(()=>{btn.disabled=false;btn.textContent='▶ Capturar sequência';bw.style.display='none';bar.style.width='0%';seqRunning=false;},600); }; // ─── Randomize ──────────────────────────────────────────────────────────────── function pick(arr){ return arr[Math.floor(Math.random()*arr.length)]; } function rnd(min,max,step=0.01){ const steps=Math.round((max-min)/step); return Math.round((min+Math.floor(Math.random()*(steps+1))*step)*100)/100; } function randomize(){ const btn=document.getElementById('rndBtn'); btn.classList.add('spinning'); setTimeout(()=>btn.classList.remove('spinning'),500); // ── Pick random params ─────────────────────────────────────────────────── // Seed (form shape) const newSeed=Math.floor(Math.random()*SEEDS.length); S.seed=newSeed; document.querySelectorAll('.sb').forEach((b,i)=>b.classList.toggle('active',i===newSeed)); // Surface — exclude cluster (it's a special mode, not truly random) const texOptions=['solid','wire','flat','points']; const newTex=pick(texOptions); S.tex=newTex; document.querySelectorAll('.tb').forEach(b=>{ const isActive=b.onclick?.toString().includes(`'${newTex}'`)||b.getAttribute('onclick')?.includes(`'${newTex}'`); b.classList.toggle('active',isActive); }); // Material const newMat=Math.floor(Math.random()*MATS.length); S.mat=newMat; S.rough=MATS[newMat].rough; S.emit=MATS[newMat].emit||0.1; document.querySelectorAll('.mc').forEach((c,i)=>c.classList.toggle('active',i===newMat)); document.getElementById('sRough').value=S.rough; document.getElementById('vRough').textContent=S.rough.toFixed(2); document.getElementById('sEmit').value=S.emit; document.getElementById('vEmit').textContent=S.emit.toFixed(2); // Tipos — pick 1–3 randomly (at least 1) const nTipos=Math.random()<.3?1:Math.random()<.6?2:Math.random()<.85?3:4; const shuffled=[0,1,2,3].sort(()=>Math.random()-.5); const chosen=new Set(shuffled.slice(0,nTipos)); activeTipos=chosen; document.querySelectorAll('.tipo-btn').forEach((btn,i)=>{ btn.classList.toggle('active',chosen.has(i)); }); updateMixBar(); // Deform mode (skip cluster=7) const newDeform=Math.floor(Math.random()*7); S.deform=newDeform; const def=DEFORM_DEF[newDeform]; S.dist=def.dist; S.freq=def.freq; S.speed=def.speed; document.querySelectorAll('.db').forEach((b,i)=>b.classList.toggle('active',i===newDeform)); document.getElementById('sDist').value=S.dist; document.getElementById('vDist').textContent=S.dist.toFixed(2); document.getElementById('sFreq').value=S.freq; document.getElementById('vFreq').textContent=S.freq.toFixed(2); document.getElementById('sSpeed').value=S.speed; document.getElementById('vSpeed').textContent=S.speed.toFixed(2); document.getElementById('shapeSliders').style.display=''; // Randomize within reasonable ranges S.dist=rnd(Math.max(0,def.dist-.2), Math.min(1.2,def.dist+.25)); S.freq=rnd(Math.max(.3,def.freq-.5), Math.min(4,def.freq+.6),.05); S.speed=rnd(Math.max(0,def.speed-.3), Math.min(2,def.speed+.4),.05); S.size=rnd(.65,1.6,.05); S.opac=Math.random()<.15?rnd(.55,.9):1.0; // Update size slider document.getElementById('sSize').value=S.size; document.getElementById('vSize').textContent=S.size.toFixed(2); document.getElementById('sOpac').value=S.opac; document.getElementById('vOpac').textContent=S.opac.toFixed(2); // Re-update dist/freq/speed display after jitter document.getElementById('sDist').value=S.dist; document.getElementById('vDist').textContent=S.dist.toFixed(2); document.getElementById('sFreq').value=S.freq; document.getElementById('vFreq').textContent=S.freq.toFixed(2); document.getElementById('sSpeed').value=S.speed; document.getElementById('vSpeed').textContent=S.speed.toFixed(2); // Rebuild build(); } // ─── GIF Recording ───────────────────────────────────────────────────────────── const REC_MAX_MS = 3000; // 3 s max duration const REC_FPS = 15; // frames per second (15 = smooth + reasonable size) const REC_SCALE = 0.75; // canvas scale for GIF (0.75 = 75% of screen res) let recState = 'idle'; // 'idle' | 'recording' | 'processing' let recFrames = []; // raw ImageData frames let recRafId = null; let recStart = 0; let recInterval = null; let gifLib = null; // gif.js instance (lazy-loaded) // Lazy-load gif.js worker from CDN function loadGifJs(){ return new Promise((resolve, reject) => { if(window.GIF){ resolve(); return; } const s = document.createElement('script'); s.src = 'https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js'; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); } function toggleRecord(){ if(recState === 'idle') startRecord(); else if(recState === 'recording') stopRecord(); } async function startRecord(){ // Load gif.js if not already available try { await loadGifJs(); } catch(e) { alert('Não foi possível carregar gif.js. Verifique sua conexão.'); return; } recState = 'recording'; recFrames = []; recStart = performance.now(); // UI const btn = document.getElementById('recBtn'); const lbl = document.getElementById('recLabel'); const arc = document.getElementById('recArc'); btn.classList.add('recording'); document.getElementById('recRing').classList.add('visible'); lbl.textContent = '3s'; const CIRC = 138; // Capture a frame every (1000/FPS) ms const frameIntervalMs = 1000 / REC_FPS; function captureFrame(){ // Draw current Three.js frame to a temp canvas at reduced scale const cw = Math.round(renderer.domElement.width * REC_SCALE); const ch = Math.round(renderer.domElement.height * REC_SCALE); const tmp = document.createElement('canvas'); tmp.width = cw; tmp.height = ch; const ctx = tmp.getContext('2d'); ctx.drawImage(renderer.domElement, 0, 0, cw, ch); recFrames.push({ data: ctx.getImageData(0, 0, cw, ch), w: cw, h: ch }); } // Tick: update progress ring + countdown, capture frames function tick(){ const elapsed = performance.now() - recStart; const progress = Math.min(elapsed / REC_MAX_MS, 1); arc.style.strokeDashoffset = CIRC * (1 - progress); lbl.textContent = Math.ceil((REC_MAX_MS - elapsed) / 1000) || '…'; if(elapsed < REC_MAX_MS){ recRafId = requestAnimationFrame(tick); } else { stopRecord(); } } // Capture frames at fixed interval (independent of RAF to keep timing accurate) recInterval = setInterval(captureFrame, frameIntervalMs); recRafId = requestAnimationFrame(tick); } function stopRecord(){ if(recState !== 'recording') return; recState = 'processing'; clearInterval(recInterval); cancelAnimationFrame(recRafId); document.getElementById('recLabel').textContent = '⏳'; buildGif(); } function buildGif(){ if(recFrames.length === 0){ resetRecUI(); return; } const { w, h } = recFrames[0]; const gif = new window.GIF({ workers: 2, quality: 6, // 1=best, 30=worst — 6 is excellent/fast width: w, height: h, workerScript: 'https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.worker.js', dither: 'FloydSteinberg', // best colour accuracy for gradients repeat: 0, // 0 = loop forever }); const delayMs = Math.round(1000 / REC_FPS); recFrames.forEach(({ data }) => gif.addFrame(data, { delay: delayMs, copy: true })); gif.on('progress', p => { const pct = Math.round(p * 100); document.getElementById('recLabel').textContent = `${pct}%`; document.getElementById('recArc').style.strokeDashoffset = 138 * (1 - p); }); gif.on('finished', blob => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); const ts = new Date().toISOString().slice(0,19).replace(/[:T]/g,'-'); a.href = url; a.download = `seed-loop-${ts}.gif`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 8000); const f = document.getElementById('flash'); f.classList.add('on'); setTimeout(() => f.classList.remove('on'), 140); resetRecUI(); }); gif.render(); } function resetRecUI(){ recState = 'idle'; recFrames = []; const btn = document.getElementById('recBtn'); const arc = document.getElementById('recArc'); btn.classList.remove('recording'); document.getElementById('recRing').classList.remove('visible'); arc.style.strokeDashoffset = '138'; document.getElementById('recLabel').textContent = 'Gravar GIF'; } // ─── WebM Recording ──────────────────────────────────────────────────────────── const WEBM_MAX_MS = 5000; // 5 seconds let webmState = 'idle'; // 'idle' | 'recording' let webmRecorder = null; let webmChunks = []; let webmRafId = null; let webmStart = 0; function toggleWebm(){ if(webmState === 'idle') startWebm(); else if(webmState === 'recording') stopWebm(true); } function startWebm(){ if(!renderer.domElement.captureStream){ alert('Seu browser não suporta gravação de canvas (tente Chrome/Edge).'); return; } webmState = 'recording'; webmChunks = []; webmStart = performance.now(); // ── Boost to 2K resolution before starting stream ───────────────────────── const origRatio = renderer.getPixelRatio(); const hiRatio = Math.max(origRatio, 2560 / Math.max(PW(), PH())); renderer.setPixelRatio(hiRatio); renderer.setSize(PW(), PH()); renderer.render(scene, camera); // first frame at 2K // Pick best supported codec const mime = ['video/webm;codecs=vp9','video/webm;codecs=vp8','video/webm'] .find(t => MediaRecorder.isTypeSupported(t)) || ''; // 60 fps stream from the now-2K canvas const stream = renderer.domElement.captureStream(60); webmRecorder = new MediaRecorder(stream, mime ? {mimeType:mime, videoBitsPerSecond:40_000_000} : {videoBitsPerSecond:40_000_000} ); webmRecorder.ondataavailable = e => { if(e.data.size > 0) webmChunks.push(e.data); }; webmRecorder.onstop = () => { // Restore resolution after recording ends renderer.setPixelRatio(origRatio); renderer.setSize(PW(), PH()); renderer.render(scene, camera); finalizeWebm(); }; webmRecorder.start(100); // UI const btn = document.getElementById('webmBtn'); const lbl = document.getElementById('webmLabel'); btn.classList.add('recording'); lbl.textContent = '5s'; function tickWebm(){ const elapsed = performance.now() - webmStart; const remaining = Math.ceil((WEBM_MAX_MS - elapsed) / 1000); lbl.textContent = remaining > 0 ? `${remaining}s` : '…'; if(elapsed < WEBM_MAX_MS){ webmRafId = requestAnimationFrame(tickWebm); } else { stopWebm(true); } } webmRafId = requestAnimationFrame(tickWebm); } function stopWebm(finalize){ if(webmState !== 'recording') return; webmState = 'idle'; cancelAnimationFrame(webmRafId); if(webmRecorder && webmRecorder.state !== 'inactive') webmRecorder.stop(); // finalizeWebm() fires via onstop if(!finalize) resetWebmUI(); } function finalizeWebm(){ const blob = new Blob(webmChunks, {type:'video/webm'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const ts = new Date().toISOString().slice(0,19).replace(/[:T]/g,'-'); a.href = url; a.download = `seed-loop-${ts}.webm`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 8000); const f = document.getElementById('flash'); f.classList.add('on'); setTimeout(() => f.classList.remove('on'), 140); resetWebmUI(); } function resetWebmUI(){ webmState = 'idle'; webmChunks = []; webmRecorder = null; const btn = document.getElementById('webmBtn'); const lbl = document.getElementById('webmLabel'); btn.classList.remove('recording'); lbl.textContent = 'Gravar WebM'; } build(); animate();