@brandonlukas/luminar 0.2.2 โ†’ 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,15 +18,15 @@ A particle-flow visualization inspired by the bloom-heavy aesthetic of [lumap](h
18
18
 
19
19
  ## Quick Start
20
20
 
21
- Visualize a CSV vector field with zero install:
21
+ **Drag and drop** CSV files into the browser window:
22
22
 
23
23
  ```sh
24
- npx @brandonlukas/luminar path/to/field.csv
24
+ npx @brandonlukas/luminar
25
25
  ```
26
26
 
27
- Optional flags: `--port 5173`, `--host 0.0.0.0`, `--preview` (uses production build)
27
+ Then drag your CSV files onto the left or right side of the canvas to load Field A or Field B.
28
28
 
29
- **Or drag and drop:** Just run `npx @brandonlukas/luminar` and drag CSV files into the browser window!
29
+ Optional flags: `--port 5173`, `--host 0.0.0.0`, `--preview` (uses production build)
30
30
 
31
31
  ## CSV Format
32
32
 
package/bin/luminar.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, writeFileSync, existsSync } from 'node:fs'
3
2
  import { resolve, dirname } from 'node:path'
4
3
  import { fileURLToPath } from 'node:url'
5
4
  import { spawn } from 'node:child_process'
@@ -34,7 +33,7 @@ const projectRoot = resolve(__dirname, '..')
34
33
 
35
34
  function parseArgs() {
36
35
  const args = process.argv.slice(2)
37
- const out = { file: null, port: 5173, host: '0.0.0.0', preview: false }
36
+ const out = { port: 5173, host: '0.0.0.0', preview: false }
38
37
 
39
38
  for (let i = 0; i < args.length; i += 1) {
40
39
  const arg = args[i]
@@ -44,20 +43,11 @@ function parseArgs() {
44
43
  out.host = args[++i] || out.host
45
44
  } else if (arg === '--preview') {
46
45
  out.preview = true
47
- } else if (!arg.startsWith('-') && !out.file) {
48
- // First positional argument is the file
49
- out.file = arg
50
46
  }
51
47
  }
52
48
  return out
53
49
  }
54
50
 
55
- function writeFieldJson(rows) {
56
- const target = resolve(projectRoot, 'public', 'vector-field.json')
57
- writeFileSync(target, JSON.stringify(rows, null, 2), 'utf8')
58
- console.log(`wrote ${rows.length} vectors to ${target}`)
59
- }
60
-
61
51
  function runServer({ port, host, preview }) {
62
52
  const cmd = 'npm'
63
53
  const args = preview
@@ -73,28 +63,11 @@ function runServer({ port, host, preview }) {
73
63
  }
74
64
 
75
65
  function main() {
76
- const { file, port, host, preview } = parseArgs()
66
+ const { port, host, preview } = parseArgs()
77
67
 
78
- if (file) {
79
- // User provided a CSV file - parse and load it
80
- const resolved = resolve(process.cwd(), file)
81
- if (!existsSync(resolved)) {
82
- console.error(`File not found: ${resolved}`)
83
- process.exit(1)
84
- }
85
- const text = readFileSync(resolved, 'utf8')
86
- const rows = parseCsv(text)
87
- if (rows.length === 0) {
88
- console.error('Parsed 0 rows; ensure CSV has x,y,dx,dy columns (header optional)')
89
- process.exit(1)
90
- }
91
- writeFieldJson(rows)
92
- console.log(`Loaded ${rows.length} vectors from ${resolved}`)
93
- } else {
94
- // No file provided - launch app with default empty state
95
- console.log('No CSV file provided - launching with default empty state')
96
- console.log('You can drag & drop CSV files in the webapp once it loads')
97
- }
68
+ console.log(`๐Ÿš€ Launching luminar on http://${host}:${port}`)
69
+ console.log('๐Ÿ“‚ Drag & drop CSV files into the browser to visualize')
70
+ console.log('')
98
71
 
99
72
  runServer({ port, host, preview })
100
73
  }
@@ -4178,4 +4178,4 @@ void main() {
4178
4178
 
4179
4179
  gl_FragColor = max(texelNew, texelOld);
4180
4180
 
4181
- }`},Cc=class extends dc{constructor(e=.96){super(),this.uniforms=Or.clone(Sc.uniforms),this.damp=e,this.compFsMaterial=new jr({uniforms:this.uniforms,vertexShader:Sc.vertexShader,fragmentShader:Sc.fragmentShader}),this.copyFsMaterial=new jr({uniforms:Or.clone(uc.uniforms),vertexShader:uc.vertexShader,fragmentShader:uc.fragmentShader,blending:0,depthTest:!1,depthWrite:!1}),this._textureComp=new xt(window.innerWidth,window.innerHeight,{magFilter:r,type:g}),this._textureOld=new xt(window.innerWidth,window.innerHeight,{magFilter:r,type:g}),this._compFsQuad=new mc(this.compFsMaterial),this._copyFsQuad=new mc(this.copyFsMaterial)}get damp(){return this.uniforms.damp.value}set damp(e){this.uniforms.damp.value=e}render(e,t,n){this.uniforms.tOld.value=this._textureOld.texture,this.uniforms.tNew.value=n.texture,e.setRenderTarget(this._textureComp),this._compFsQuad.render(e),this._copyFsQuad.material.uniforms.tDiffuse.value=this._textureComp.texture,this.renderToScreen?(e.setRenderTarget(null),this._copyFsQuad.render(e)):(e.setRenderTarget(t),this.clear&&e.clear(),this._copyFsQuad.render(e));let r=this._textureOld;this._textureOld=this._textureComp,this._textureComp=r}setSize(e,t){this._textureComp.setSize(e,t),this._textureOld.setSize(e,t)}dispose(){this._textureComp.dispose(),this._textureOld.dispose(),this.compFsMaterial.dispose(),this.copyFsMaterial.dispose(),this._compFsQuad.dispose(),this._copyFsQuad.dispose()}},wc=class{constructor(e=Math){this.grad3=[[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]],this.grad4=[[0,1,1,1],[0,1,1,-1],[0,1,-1,1],[0,1,-1,-1],[0,-1,1,1],[0,-1,1,-1],[0,-1,-1,1],[0,-1,-1,-1],[1,0,1,1],[1,0,1,-1],[1,0,-1,1],[1,0,-1,-1],[-1,0,1,1],[-1,0,1,-1],[-1,0,-1,1],[-1,0,-1,-1],[1,1,0,1],[1,1,0,-1],[1,-1,0,1],[1,-1,0,-1],[-1,1,0,1],[-1,1,0,-1],[-1,-1,0,1],[-1,-1,0,-1],[1,1,1,0],[1,1,-1,0],[1,-1,1,0],[1,-1,-1,0],[-1,1,1,0],[-1,1,-1,0],[-1,-1,1,0],[-1,-1,-1,0]],this.p=[];for(let t=0;t<256;t++)this.p[t]=Math.floor(e.random()*256);this.perm=[];for(let e=0;e<512;e++)this.perm[e]=this.p[e&255];this.simplex=[[0,1,2,3],[0,1,3,2],[0,0,0,0],[0,2,3,1],[0,0,0,0],[0,0,0,0],[0,0,0,0],[1,2,3,0],[0,2,1,3],[0,0,0,0],[0,3,1,2],[0,3,2,1],[0,0,0,0],[0,0,0,0],[0,0,0,0],[1,3,2,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[1,2,0,3],[0,0,0,0],[1,3,0,2],[0,0,0,0],[0,0,0,0],[0,0,0,0],[2,3,0,1],[2,3,1,0],[1,0,2,3],[1,0,3,2],[0,0,0,0],[0,0,0,0],[0,0,0,0],[2,0,3,1],[0,0,0,0],[2,1,3,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[2,0,1,3],[0,0,0,0],[0,0,0,0],[0,0,0,0],[3,0,1,2],[3,0,2,1],[0,0,0,0],[3,1,2,0],[2,1,0,3],[0,0,0,0],[0,0,0,0],[0,0,0,0],[3,1,0,2],[0,0,0,0],[3,2,0,1],[3,2,1,0]]}noise(e,t){let n,r,i,a=.5*(Math.sqrt(3)-1),o=(e+t)*a,s=Math.floor(e+o),c=Math.floor(t+o),l=(3-Math.sqrt(3))/6,u=(s+c)*l,d=s-u,f=c-u,p=e-d,m=t-f,h,g;p>m?(h=1,g=0):(h=0,g=1);let _=p-h+l,v=m-g+l,y=p-1+2*l,b=m-1+2*l,x=s&255,S=c&255,C=this.perm[x+this.perm[S]]%12,w=this.perm[x+h+this.perm[S+g]]%12,T=this.perm[x+1+this.perm[S+1]]%12,E=.5-p*p-m*m;E<0?n=0:(E*=E,n=E*E*this._dot(this.grad3[C],p,m));let D=.5-_*_-v*v;D<0?r=0:(D*=D,r=D*D*this._dot(this.grad3[w],_,v));let ee=.5-y*y-b*b;return ee<0?i=0:(ee*=ee,i=ee*ee*this._dot(this.grad3[T],y,b)),70*(n+r+i)}noise3d(e,t,n){let r,i,a,o,s=(e+t+n)*(1/3),c=Math.floor(e+s),l=Math.floor(t+s),u=Math.floor(n+s),d=1/6,f=(c+l+u)*d,p=c-f,m=l-f,h=u-f,g=e-p,_=t-m,v=n-h,y,b,x,S,C,w;g>=_?_>=v?(y=1,b=0,x=0,S=1,C=1,w=0):g>=v?(y=1,b=0,x=0,S=1,C=0,w=1):(y=0,b=0,x=1,S=1,C=0,w=1):_<v?(y=0,b=0,x=1,S=0,C=1,w=1):g<v?(y=0,b=1,x=0,S=0,C=1,w=1):(y=0,b=1,x=0,S=1,C=1,w=0);let T=g-y+d,E=_-b+d,D=v-x+d,ee=g-S+2*d,O=_-C+2*d,k=v-w+2*d,te=g-1+3*d,ne=_-1+3*d,A=v-1+3*d,j=c&255,M=l&255,N=u&255,re=this.perm[j+this.perm[M+this.perm[N]]]%12,ie=this.perm[j+y+this.perm[M+b+this.perm[N+x]]]%12,P=this.perm[j+S+this.perm[M+C+this.perm[N+w]]]%12,ae=this.perm[j+1+this.perm[M+1+this.perm[N+1]]]%12,oe=.6-g*g-_*_-v*v;oe<0?r=0:(oe*=oe,r=oe*oe*this._dot3(this.grad3[re],g,_,v));let F=.6-T*T-E*E-D*D;F<0?i=0:(F*=F,i=F*F*this._dot3(this.grad3[ie],T,E,D));let I=.6-ee*ee-O*O-k*k;I<0?a=0:(I*=I,a=I*I*this._dot3(this.grad3[P],ee,O,k));let L=.6-te*te-ne*ne-A*A;return L<0?o=0:(L*=L,o=L*L*this._dot3(this.grad3[ae],te,ne,A)),32*(r+i+a+o)}noise4d(e,t,n,r){let i=this.grad4,a=this.simplex,o=this.perm,s=(Math.sqrt(5)-1)/4,c=(5-Math.sqrt(5))/20,l,u,d,f,p,m=(e+t+n+r)*s,h=Math.floor(e+m),g=Math.floor(t+m),_=Math.floor(n+m),v=Math.floor(r+m),y=(h+g+_+v)*c,b=h-y,x=g-y,S=_-y,C=v-y,w=e-b,T=t-x,E=n-S,D=r-C,ee=w>T?32:0,O=w>E?16:0,k=T>E?8:0,te=w>D?4:0,ne=T>D?2:0,A=E>D?1:0,j=ee+O+k+te+ne+A,M=a[j][0]>=3?1:0,N=a[j][1]>=3?1:0,re=a[j][2]>=3?1:0,ie=a[j][3]>=3?1:0,P=a[j][0]>=2?1:0,ae=a[j][1]>=2?1:0,oe=a[j][2]>=2?1:0,F=a[j][3]>=2?1:0,I=a[j][0]>=1?1:0,L=a[j][1]>=1?1:0,se=a[j][2]>=1?1:0,ce=a[j][3]>=1?1:0,le=w-M+c,ue=T-N+c,R=E-re+c,de=D-ie+c,fe=w-P+2*c,pe=T-ae+2*c,me=E-oe+2*c,he=D-F+2*c,ge=w-I+3*c,_e=T-L+3*c,ve=E-se+3*c,ye=D-ce+3*c,be=w-1+4*c,xe=T-1+4*c,Se=E-1+4*c,Ce=D-1+4*c,we=h&255,z=g&255,Te=_&255,B=v&255,Ee=o[we+o[z+o[Te+o[B]]]]%32,V=o[we+M+o[z+N+o[Te+re+o[B+ie]]]]%32,De=o[we+P+o[z+ae+o[Te+oe+o[B+F]]]]%32,H=o[we+I+o[z+L+o[Te+se+o[B+ce]]]]%32,U=o[we+1+o[z+1+o[Te+1+o[B+1]]]]%32,Oe=.6-w*w-T*T-E*E-D*D;Oe<0?l=0:(Oe*=Oe,l=Oe*Oe*this._dot4(i[Ee],w,T,E,D));let ke=.6-le*le-ue*ue-R*R-de*de;ke<0?u=0:(ke*=ke,u=ke*ke*this._dot4(i[V],le,ue,R,de));let Ae=.6-fe*fe-pe*pe-me*me-he*he;Ae<0?d=0:(Ae*=Ae,d=Ae*Ae*this._dot4(i[De],fe,pe,me,he));let je=.6-ge*ge-_e*_e-ve*ve-ye*ye;je<0?f=0:(je*=je,f=je*je*this._dot4(i[H],ge,_e,ve,ye));let Me=.6-be*be-xe*xe-Se*Se-Ce*Ce;return Me<0?p=0:(Me*=Me,p=Me*Me*this._dot4(i[U],be,xe,Se,Ce)),27*(l+u+d+f+p)}_dot(e,t,n){return e[0]*t+e[1]*n}_dot3(e,t,n,r){return e[0]*t+e[1]*n+e[2]*r}_dot4(e,t,n,r,i){return e[0]*t+e[1]*n+e[2]*r+e[3]*i}};const Tc=1.25,Ec=[{key:`luminous-violet`,label:`Luminous violet`,rgb:[.6,.25,.9]},{key:`pure-white`,label:`Pure white`,rgb:[1,1,1]},{key:`neon-cyan`,label:`Neon cyan`,rgb:[.25,.95,1]},{key:`electric-lime`,label:`Electric lime`,rgb:[.75,1,.25]},{key:`solar-flare`,label:`Solar flare`,rgb:[1,.55,.15]},{key:`aurora-mint`,label:`Aurora mint`,rgb:[.4,1,.85]},{key:`sunrise-coral`,label:`Sunrise coral`,rgb:[1,.6,.5]},{key:`ember-gold`,label:`Ember gold`,rgb:[1,.8,.2]}],Dc=`luminous-violet`,Oc={size:2,bloomStrength:1.2,bloomRadius:.35,lifeMin:.5,lifeMax:1.4,fieldValidDistance:.05,speed:6,particleCount:5e3,colorPresetA:Dc,colorPresetB:Dc,noiseStrength:0,trailsEnabled:!1,trailDecay:.9};var kc=class{positions;colors;lifetimes;fieldData=null;fieldTransform={scale:1,offsetX:0,offsetY:0};grid=new Map;gridCellSize=.1;geometry;params;viewOffsetX;activePalette=null;noise;noiseScale=.9;noiseTimeScale=.15;constructor(e,t){this.geometry=e,this.params=t,this.viewOffsetX=0,this.positions=new Float32Array(t.particleCount*3),this.colors=new Float32Array(t.particleCount*3),this.lifetimes=new Float32Array(t.particleCount),this.noise=new wc,this.geometry.setAttribute(`position`,new Zn(this.positions,3)),this.geometry.setAttribute(`color`,new Zn(this.colors,3))}init(){for(let e=0;e<this.params.particleCount;e+=1)this.resetParticle(e);this.updateBuffers()}setFieldData(e,t){this.fieldData=e,this.fieldTransform=t,this.buildSpatialGrid()}hasFieldData(){return this.fieldData!==null&&this.fieldData.length>0}setViewOffset(e){if(this.viewOffsetX!==e){this.viewOffsetX=e;for(let e=0;e<this.params.particleCount;e+=1)this.resetParticle(e);this.updateBuffers()}}update(e,t){let n=performance.now()*.001;this.activePalette=t;let r=this.params.noiseStrength>0,i=.85*this.params.speed*e,a=.015*e;for(let o=0;o<this.params.particleCount;o+=1){let s=o*3,c=this.positions[s],l=this.positions[s+1],u=this.sampleField(c,l,n);if(!u){this.resetParticle(o);continue}r&&this.applyNoise(u,c,l,n),c+=u.x*i+this.randomRange(-a,a),l+=u.y*i+this.randomRange(-a,a);let d=Math.hypot(u.x,u.y),f=Math.min(1,d*2.6);this.applyColor(s,f,t,!1),this.lifetimes[o]-=e,this.shouldResetParticle(o,c,l)?this.resetParticle(o):(this.positions[s]=c,this.positions[s+1]=l)}this.updateBuffers()}resizeBuffers(e){this.params.particleCount=e,this.positions=new Float32Array(e*3),this.colors=new Float32Array(e*3),this.lifetimes=new Float32Array(e),this.geometry.setAttribute(`position`,new Zn(this.positions,3)),this.geometry.setAttribute(`color`,new Zn(this.colors,3)),this.init()}reseedLifetimes(){for(let e=0;e<this.params.particleCount;e+=1)this.lifetimes[e]=this.randomRange(this.params.lifeMin,this.params.lifeMax)}shouldResetParticle(e,t,n){return this.lifetimes[e]<=0||Math.abs(t-this.viewOffsetX)>1.25||Math.abs(n)>1.25}applyNoise(e,t,n,r){let i=this.noise.noise3d(t*this.noiseScale,n*this.noiseScale,r*this.noiseTimeScale),a=this.noise.noise3d((t+10)*this.noiseScale,(n+10)*this.noiseScale,r*this.noiseTimeScale);e.x+=i*this.params.noiseStrength,e.y+=a*this.params.noiseStrength}resetParticle(e){let t=e*3,n=this.activePalette??this.getActiveColorPreset();this.hasFieldData()?this.resetParticleWithinField(t):this.resetParticleRandomly(t),this.lifetimes[e]=this.randomRange(this.params.lifeMin,this.params.lifeMax);let r=.4+Math.random()*.2;this.applyColor(t,r,n,!0)}resetParticleWithinField(e){let t=this.fieldData[Math.floor(Math.random()*this.fieldData.length)],n=this.params.fieldValidDistance*.3,r=t.x+this.randomRange(-n,n)/this.fieldTransform.scale,i=t.y+this.randomRange(-n,n)/this.fieldTransform.scale;this.positions[e]=this.dataToWorldX(r),this.positions[e+1]=this.dataToWorldY(i),this.positions[e+2]=0}resetParticleRandomly(e){this.positions[e]=this.randomRange(-Tc,Tc)+this.viewOffsetX,this.positions[e+1]=this.randomRange(-Tc,Tc),this.positions[e+2]=0}dataToWorldX(e){return e*this.fieldTransform.scale+this.fieldTransform.offsetX+this.viewOffsetX}dataToWorldY(e){return e*this.fieldTransform.scale+this.fieldTransform.offsetY}worldToDataX(e){return(e-this.viewOffsetX-this.fieldTransform.offsetX)/this.fieldTransform.scale}worldToDataY(e){return(e-this.fieldTransform.offsetY)/this.fieldTransform.scale}buildSpatialGrid(){if(this.grid.clear(),!this.fieldData||this.fieldData.length===0)return;let e=this.fieldData[0].x,t=this.fieldData[0].x,n=this.fieldData[0].y,r=this.fieldData[0].y;for(let i of this.fieldData)i.x<e&&(e=i.x),i.x>t&&(t=i.x),i.y<n&&(n=i.y),i.y>r&&(r=i.y);let i=(t-e+(r-n))/2,a=Math.ceil(Math.sqrt(this.fieldData.length));this.gridCellSize=Math.max(.01,i/a);for(let e of this.fieldData){let t=this.getGridKey(e.x,e.y),n=this.grid.get(t);n?n.push(e):this.grid.set(t,[e])}}getGridKey(e,t){return`${Math.floor(e/this.gridCellSize)},${Math.floor(t/this.gridCellSize)}`}sampleField(e,t,n){if(!this.hasFieldData())return{x:1,y:0};let r=this.worldToDataX(e),i=this.worldToDataY(t),a=this.findNearestFieldPoint(r,i);return a?{x:a.dx*this.fieldTransform.scale,y:a.dy*this.fieldTransform.scale}:null}findNearestFieldPoint(e,t){let n=null,r=Number.MAX_VALUE,i=(this.params.fieldValidDistance/this.fieldTransform.scale)**2,a=Math.floor(e/this.gridCellSize),o=Math.floor(t/this.gridCellSize);for(let i=-1;i<=1;i+=1)for(let s=-1;s<=1;s+=1){let c=this.grid.get(`${a+i},${o+s}`);if(c)for(let i of c){let a=i.x-e,o=i.y-t,s=a*a+o*o;s<r&&(r=s,n=i)}}return r<=i?n:null}applyColor(e,t,n,r){let i=Math.min(1,Math.max(0,t));if(n.key===`luminous-violet`){r?(this.colors[e]=.6*i,this.colors[e+1]=.25*i,this.colors[e+2]=.9*i):(this.colors[e]=.35+i*.9,this.colors[e+1]=.18+i*.45,this.colors[e+2]=.6+i*.35);return}let a=r?i:.35+i*.65,[o,s,c]=n.rgb;this.colors[e]=o*a,this.colors[e+1]=s*a,this.colors[e+2]=c*a}getActiveColorPreset(){return Ec.find(e=>e.key===this.params.colorPresetA)??Ec[0]}randomRange(e,t){return e+Math.random()*(t-e)}updateBuffers(){this.geometry.attributes.position.needsUpdate=!0,this.geometry.attributes.color.needsUpdate=!0,this.geometry.computeBoundingSphere()}},Ac=class{fieldStatusEl=null;onFieldLoaded;constructor(e){this.onFieldLoaded=e}setStatusElement(e){this.fieldStatusEl=e}async load(){try{let e=await fetch(`/vector-field.json`,{cache:`no-store`});if(!e.ok){this.updateStatus(`default (built-in)`);return}let t=await e.json();if(Array.isArray(t)&&t.length>0){let{transform:e,bounds:n}=this.computeFieldTransform(t);this.onFieldLoaded(t,e),this.updateStatus(`loaded ${t.length} vectors (${n.width.toFixed(1)}ร—${n.height.toFixed(1)})`),console.log(`Field bounds:`,n,`scale:`,e.scale)}else this.updateStatus(`default (empty file)`)}catch(e){console.error(`Failed to load vector field`,e),this.updateStatus(`default (load error)`)}}computeFieldTransform(e){let t=e[0].x,n=e[0].x,r=e[0].y,i=e[0].y;for(let a of e)a.x<t&&(t=a.x),a.x>n&&(n=a.x),a.y<r&&(r=a.y),a.y>i&&(i=a.y);let a=n-t,o=i-r,s=Math.max(a,o);Tc*1.8;let c=s>0?2.25/s:1;return{transform:{scale:c,offsetX:-(t+n)*.5*c,offsetY:-(r+i)*.5*c},bounds:{minX:t,maxX:n,minY:r,maxY:i,width:a,height:o}}}updateStatus(e){this.fieldStatusEl&&(this.fieldStatusEl.textContent=e)}},jc=class{panel;controlHandles=new Map;selectHandles=new Map;trailToggle;container;params;material;bloomPass;callbacks;constructor(e,t,n,r,i){this.container=e,this.params=t,this.material=n,this.bloomPass=r,this.callbacks=i}create(){this.panel=document.createElement(`div`),this.panel.className=`controls`;let e=document.createElement(`div`);e.className=`controls__header`;let t=document.createElement(`div`);t.className=`controls__title`,t.textContent=`Controls`;let n=document.createElement(`button`);n.className=`controls__toggle`,n.textContent=`โˆ’`,n.type=`button`,n.addEventListener(`click`,()=>{this.panel.classList.toggle(`controls--collapsed`),n.textContent=this.panel.classList.contains(`controls--collapsed`)?`+`:`โˆ’`,window.dispatchEvent(new Event(`resize`))}),e.appendChild(t),e.appendChild(n),this.panel.appendChild(e);let r=this.addSelect(`Field A color`,Ec,this.params.colorPresetA,e=>{this.params.colorPresetA=e,this.callbacks.onColorChange()}),i=this.addSelect(`Field B color`,Ec,this.params.colorPresetB,e=>{this.params.colorPresetB=e,this.callbacks.onColorChange()});this.selectHandles.set(`colorA`,r),this.selectHandles.set(`colorB`,i);let a=this.addSlider(this.panel,`Speed`,.1,8,.1,this.params.speed,e=>{this.params.speed=e});this.controlHandles.set(`speed`,a);let o=document.createElement(`button`);o.type=`button`,o.className=`controls__button`,o.textContent=`Show advanced`;let s=document.createElement(`div`);s.className=`controls__advanced`,s.style.display=`none`;let c=this.addSlider(s,`Noise`,0,1,.01,this.params.noiseStrength,e=>{this.params.noiseStrength=e});this.controlHandles.set(`noiseStrength`,c);let l=this.addSlider(s,`Size`,.5,4,.1,this.params.size,e=>{this.params.size=e,this.material.size=e});this.controlHandles.set(`size`,l);let u=this.addSlider(s,`Particle count`,100,8e3,100,this.params.particleCount,e=>{this.params.particleCount=Math.round(e),this.callbacks.onParticleCountChange(this.params.particleCount)});this.controlHandles.set(`particleCount`,u);let d=this.addSlider(s,`Bloom strength`,.2,2.5,.05,this.params.bloomStrength,e=>{this.params.bloomStrength=e,this.updateBloom()});this.controlHandles.set(`bloomStrength`,d);let f=this.addSlider(s,`Bloom radius`,0,1.2,.02,this.params.bloomRadius,e=>{this.params.bloomRadius=e,this.updateBloom()});this.controlHandles.set(`bloomRadius`,f);let p=this.addSlider(s,`Life min (s)`,.1,2,.05,this.params.lifeMin,e=>{if(this.params.lifeMin=e,this.params.lifeMin>this.params.lifeMax){this.params.lifeMax=e;let t=this.controlHandles.get(`lifeMax`);t&&this.syncSlider(t,this.params.lifeMax)}this.callbacks.onLifetimeChange()});this.controlHandles.set(`lifeMin`,p);let m=this.addSlider(s,`Life max (s)`,.2,5,.05,this.params.lifeMax,e=>{if(this.params.lifeMax=e,this.params.lifeMax<this.params.lifeMin){this.params.lifeMin=e;let t=this.controlHandles.get(`lifeMin`);t&&this.syncSlider(t,this.params.lifeMin)}this.callbacks.onLifetimeChange()});this.controlHandles.set(`lifeMax`,m);let h=this.addSlider(s,`Field border`,.01,.1,.01,this.params.fieldValidDistance,e=>{this.params.fieldValidDistance=e});this.controlHandles.set(`fieldDist`,h),this.trailToggle=this.addToggle(s,`Trails`,this.params.trailsEnabled,e=>{this.params.trailsEnabled=e,this.callbacks.onTrailToggle(e)});let g=this.addSlider(s,`Trail decay`,.7,.99,.005,this.params.trailDecay,e=>{this.params.trailDecay=e,this.callbacks.onTrailDecayChange(e)});this.controlHandles.set(`trailDecay`,g),o.addEventListener(`click`,()=>{let e=s.style.display===`none`;s.style.display=e?`block`:`none`,o.textContent=e?`Hide advanced`:`Show advanced`}),this.panel.appendChild(o),this.panel.appendChild(s);let _=document.createElement(`button`);_.type=`button`,_.className=`controls__button`,_.textContent=`Reset to defaults`,_.addEventListener(`click`,()=>this.reset()),this.panel.appendChild(_);let v=document.createElement(`div`);v.className=`controls__section`;let y=document.createElement(`div`);y.className=`controls__subtitle`,y.textContent=`Fields`,v.appendChild(y);let b=document.createElement(`div`);b.className=`controls__row`;let x=document.createElement(`span`);x.textContent=`Clear Field A`;let S=document.createElement(`button`);S.type=`button`,S.className=`controls__button`,S.textContent=`Clear`,S.addEventListener(`click`,()=>this.callbacks.onClearFieldA()),b.appendChild(x),b.appendChild(S),v.appendChild(b);let C=document.createElement(`div`);C.className=`controls__row`;let w=document.createElement(`span`);w.textContent=`Clear Field B`;let T=document.createElement(`button`);T.type=`button`,T.className=`controls__button`,T.textContent=`Clear`,T.addEventListener(`click`,()=>this.callbacks.onClearFieldB()),C.appendChild(w),C.appendChild(T),v.appendChild(C),this.panel.appendChild(v),this.container.appendChild(this.panel)}syncFieldValidDistance(e){this.params.fieldValidDistance=e;let t=this.controlHandles.get(`fieldDist`);t&&this.syncSlider(t,e)}reset(){Object.assign(this.params,Oc),this.material.size=this.params.size,this.updateBloom(),this.callbacks.onLifetimeChange(),this.callbacks.onTrailToggle(this.params.trailsEnabled),this.callbacks.onTrailDecayChange(this.params.trailDecay);for(let[e,t]of this.controlHandles.entries()){let n=e;typeof this.params[n]==`number`&&this.syncSlider(t,this.params[n])}for(let[e,t]of this.selectHandles.entries())e===`colorA`&&(t.value=this.params.colorPresetA),e===`colorB`&&(t.value=this.params.colorPresetB);this.trailToggle&&(this.trailToggle.checked=this.params.trailsEnabled)}addToggle(e,t,n,r){let i=document.createElement(`label`);i.className=`controls__row`;let a=document.createElement(`span`);a.textContent=t;let o=document.createElement(`input`);return o.type=`checkbox`,o.checked=n,o.addEventListener(`change`,e=>{let t=e.target.checked;r(t)}),i.appendChild(a),i.appendChild(o),e.appendChild(i),o}addSlider(e,t,n,r,i,a,o){let s=document.createElement(`label`);s.className=`controls__row`;let c=document.createElement(`span`);c.textContent=t;let l=document.createElement(`input`);l.type=`range`,l.min=String(n),l.max=String(r),l.step=String(i),l.value=String(a);let u=document.createElement(`span`);return u.className=`controls__value`,u.textContent=this.formatValue(a,i),l.addEventListener(`input`,e=>{let t=parseFloat(e.target.value);u.textContent=this.formatValue(t,i),o(t)}),s.appendChild(c),s.appendChild(l),s.appendChild(u),e.appendChild(s),{input:l,valueTag:u}}addSelect(e,t,n,r){let i=document.createElement(`label`);i.className=`controls__row`;let a=document.createElement(`span`);a.textContent=e;let o=document.createElement(`select`);o.className=`controls__select`;for(let e of t){let t=document.createElement(`option`);t.value=e.key,t.textContent=e.label,o.appendChild(t)}return o.value=n,o.addEventListener(`change`,e=>{let t=e.target.value;r(t)}),i.appendChild(a),i.appendChild(o),this.panel.appendChild(i),o}updateBloom(){this.bloomPass.strength=this.params.bloomStrength,this.bloomPass.radius=this.params.bloomRadius}formatValue(e,t){return t>=1?e.toFixed(0):e.toFixed(2)}syncSlider(e,t){e.input.value=String(t),e.valueTag.textContent=this.formatValue(t,parseFloat(e.input.step))}},Mc=class{mediaRecorder=null;recordedChunks=[];isRecording=!1;recordingStartTime=0;recordingDuration=5;recordingResolution=`current`;originalCanvasSize=null;recordButton=null;recordStatus=null;renderer;composer;bloomPass;onResize;constructor(e,t,n,r){this.renderer=e,this.composer=t,this.bloomPass=n,this.onResize=r}createControls(e){let t=document.createElement(`div`);t.className=`controls__section`,t.innerHTML=`<div class="controls__subtitle">Export (WebM)</div>`;let n=document.createElement(`label`);n.className=`controls__row`;let r=document.createElement(`span`);r.textContent=`Resolution`;let i=document.createElement(`select`);i.className=`controls__select`,i.innerHTML=`<option value="current">Current window</option><option value="1080p">1080p (Full HD)</option><option value="1440p">1440p (2K)</option><option value="4k">4K (Ultra HD)</option>`,i.value=this.recordingResolution,i.addEventListener(`change`,e=>{this.recordingResolution=e.target.value}),n.appendChild(r),n.appendChild(i),t.appendChild(n);let a=document.createElement(`label`);a.className=`controls__row`;let o=document.createElement(`span`);o.textContent=`Duration`;let s=document.createElement(`select`);s.className=`controls__select`,s.innerHTML=`<option value="3">3 seconds</option><option value="5">5 seconds</option><option value="10">10 seconds</option><option value="15">15 seconds</option>`,s.value=String(this.recordingDuration),s.addEventListener(`change`,e=>{this.recordingDuration=parseInt(e.target.value)}),a.appendChild(o),a.appendChild(s),t.appendChild(a),this.recordButton=document.createElement(`button`),this.recordButton.type=`button`,this.recordButton.className=`controls__button controls__button--record`,this.recordButton.textContent=`โบ Start recording`,this.recordButton.addEventListener(`click`,()=>{this.isRecording?this.stop():this.start()}),t.appendChild(this.recordButton),this.recordStatus=document.createElement(`div`),this.recordStatus.className=`controls__status`,this.recordStatus.style.display=`none`,t.appendChild(this.recordStatus),e.appendChild(t)}update(){if(this.isRecording){let e=(performance.now()-this.recordingStartTime)/1e3;this.updateStatus(e),e>=this.recordingDuration&&this.stop()}}start(){if(!this.isRecording)try{let e=this.renderer.domElement,t,n,r=e.width/e.height;if(this.recordingResolution===`current`)t=e.width,n=e.height;else{switch(this.originalCanvasSize={width:e.width,height:e.height},this.recordingResolution){case`1080p`:n=1080,t=Math.round(n*r);break;case`1440p`:n=1440,t=Math.round(n*r);break;case`4k`:n=2160,t=Math.round(n*r);break}this.renderer.setSize(t,n,!1),this.composer.setSize(t,n),this.bloomPass.setSize(t,n)}let i=e.captureStream(60),a=t*n,o=Math.min(25e6,Math.max(8e6,a*4));this.recordedChunks=[],this.mediaRecorder=new MediaRecorder(i,{mimeType:`video/webm;codecs=vp9`,videoBitsPerSecond:o}),this.mediaRecorder.ondataavailable=e=>{e.data.size>0&&this.recordedChunks.push(e.data)},this.mediaRecorder.onstop=()=>{this.originalCanvasSize&&(this.renderer.setSize(this.originalCanvasSize.width,this.originalCanvasSize.height,!1),this.composer.setSize(this.originalCanvasSize.width,this.originalCanvasSize.height),this.bloomPass.setSize(this.originalCanvasSize.width,this.originalCanvasSize.height),this.originalCanvasSize=null,this.onResize());let e=new Blob(this.recordedChunks,{type:`video/webm`}),r=URL.createObjectURL(e),i=document.createElement(`a`);i.href=r,i.download=`luminar-${t}x${n}-${Date.now()}.webm`,i.click(),URL.revokeObjectURL(r),this.recordStatus&&(this.recordStatus.textContent=`Recording complete! Download started.`,setTimeout(()=>{this.recordStatus&&(this.recordStatus.style.display=`none`)},3e3))},this.mediaRecorder.start(),this.isRecording=!0,this.recordingStartTime=performance.now(),this.recordButton&&(this.recordButton.textContent=`โน Stop recording`,this.recordButton.style.opacity=`1`),this.recordStatus&&(this.recordStatus.style.display=`block`,this.recordStatus.textContent=`Recording at ${t}x${n} (${(o/1e6).toFixed(0)} Mbps): 0.0s / ${this.recordingDuration}s`)}catch(e){console.error(`Failed to start recording:`,e),this.originalCanvasSize&&(this.renderer.setSize(this.originalCanvasSize.width,this.originalCanvasSize.height,!1),this.composer.setSize(this.originalCanvasSize.width,this.originalCanvasSize.height),this.bloomPass.setSize(this.originalCanvasSize.width,this.originalCanvasSize.height),this.originalCanvasSize=null,this.onResize()),this.recordStatus&&(this.recordStatus.style.display=`block`,this.recordStatus.textContent=`Recording not supported in this browser.`)}}stop(){!this.isRecording||!this.mediaRecorder||(this.isRecording=!1,this.mediaRecorder.stop(),this.mediaRecorder=null,this.recordButton&&(this.recordButton.textContent=`โ–ถ Start recording`,this.recordButton.style.opacity=`1`))}updateStatus(e){if(this.recordStatus){let t=e.toFixed(1),n=this.renderer.domElement,r=Math.min(25e6,Math.max(8e6,n.width*n.height*4));this.recordStatus.textContent=`Recording at ${n.width}x${n.height} (${(r/1e6).toFixed(0)} Mbps): ${t}s / ${this.recordingDuration}s`}}};function Nc(e){let t=e.split(/\r?\n/).filter(Boolean),n=[],r=!1;for(let e of t){let t=e.split(/[,\s]+/).filter(Boolean);if(t.length<4)continue;let[i,a,o,s]=t.map(Number);if([i,a,o,s].some(e=>Number.isNaN(e))){!r&&n.length===0&&(r=!0,console.log(`skipping header line:`,e.substring(0,60)));continue}n.push({x:i,y:a,dx:o,dy:s})}return n}var Pc=document.querySelector(`#app`);if(!Pc)throw Error(`Missing #app container`);var Fc={...Oc},Ic=1.8,Lc=new lc({antialias:!1,alpha:!0});Lc.setPixelRatio(Math.min(window.devicePixelRatio,2)),Pc.appendChild(Lc.domElement);var Rc=new Gr;Rc.background=new Z(132106);var zc=new Ai(-1,1,1,-1,.1,10);zc.position.z=2;var Bc=new vc(Lc),Vc=new yc(Rc,zc),Hc=new xc(new q(1,1),Fc.bloomStrength,.82,Fc.bloomRadius),Uc=new Cc(Fc.trailDecay);Bc.addPass(Vc),Bc.addPass(Hc),Bc.addPass(Uc),Uc.enabled=Fc.trailsEnabled,Uc.uniforms.damp.value=Fc.trailDecay;var Wc=new cr,Gc=new cr,Kc=new ti({size:Fc.size,sizeAttenuation:!0,vertexColors:!0,transparent:!0,opacity:.9,blending:2,depthWrite:!1}),qc=new oi(Wc,Kc),Jc=new oi(Gc,Kc);Rc.add(qc),Rc.add(Jc);var Yc=new kc(Wc,Fc),Xc=new kc(Gc,Fc);Yc.init(),Xc.init();var Zc=!1,Qc=!1,$c=new Ac((e,t)=>{Yc.setFieldData(e,t),Zc=Yc.hasFieldData(),ll()}),el=new Ac((e,t)=>{Xc.setFieldData(e,t),Qc=Xc.hasFieldData(),ll()}),tl=new jc(Pc,Fc,Kc,Hc,{onParticleCountChange:e=>{Yc.resizeBuffers(e),Xc.resizeBuffers(e)},onLifetimeChange:()=>{Yc.reseedLifetimes(),Xc.reseedLifetimes()},onTrailToggle:e=>ul(e,Fc.trailDecay),onTrailDecayChange:e=>ul(Fc.trailsEnabled,e),onClearFieldA:()=>sl(`left`),onClearFieldB:()=>sl(`right`),onColorChange:()=>nl()});function nl(){fl=Ec.find(e=>e.key===Fc.colorPresetA)??Ec[0],pl=Ec.find(e=>e.key===Fc.colorPresetB)??Ec[0]}var rl=new Mc(Lc,Bc,Hc,cl);function il(){if(!Pc)return;let e=document.createElement(`div`);e.className=`hud`,e.innerHTML=`<div class="title">luminar</div><div class="subtitle">2D vector field bloom study</div><div class="status">Field A: <span id="field-status-a">default (built-in)</span> ยท Field B: <span id="field-status-b">default (built-in)</span></div>`,Pc.appendChild(e),$c.setStatusElement(document.getElementById(`field-status-a`)),el.setStatusElement(document.getElementById(`field-status-b`))}function al(){if(!Pc)return;let e=document.createElement(`div`);e.className=`drop-overlay drop-overlay--left`,e.textContent=`Drop to load Field A (left)`,e.style.display=`none`,Pc.appendChild(e);let t=document.createElement(`div`);t.className=`drop-overlay drop-overlay--right`,t.textContent=`Drop to load Field B (right)`,t.style.display=`none`,Pc.appendChild(t);let n=n=>{n===`left`?(e.style.display=`flex`,t.style.display=`none`):(e.style.display=`none`,t.style.display=`flex`)},r=()=>{e.style.display=`none`,t.style.display=`none`},i=async(e,t)=>{try{let n=Nc(await e.text());if(!n.length){t===`left`?$c.updateStatus(`CSV empty or invalid`):el.updateStatus(`CSV empty or invalid`),r();return}let i=t===`left`?$c:el,{transform:a,bounds:o}=i.computeFieldTransform(n);t===`left`?(Yc.setFieldData(n,a),Zc=Yc.hasFieldData()):(Xc.setFieldData(n,a),Qc=Xc.hasFieldData()),i.updateStatus(`loaded ${n.length} vectors (${o.width.toFixed(1)}ร—${o.height.toFixed(1)})`),ll()}catch(e){console.error(`Failed to load dropped CSV`,e),t===`left`?$c.updateStatus(`CSV load error`):el.updateStatus(`CSV load error`)}finally{r()}};window.addEventListener(`dragover`,e=>{e.preventDefault(),n(e.clientX<window.innerWidth*.5?`left`:`right`)}),window.addEventListener(`dragleave`,e=>{e.preventDefault(),r()}),window.addEventListener(`drop`,e=>{e.preventDefault();let t=e.dataTransfer;if(t&&t.files&&t.files[0]){let n=e.clientX<window.innerWidth*.5?`left`:`right`;i(t.files[0],n)}})}function ol(){let e=Ic*(window.innerWidth/window.innerHeight)*2,t=e*.45/2,n=(e-258/window.innerWidth*e-.5)/2,r=Math.min(1.4,n);return Math.max(1,Math.min(t,r))}function sl(e){let t={scale:1,offsetX:0,offsetY:0};e===`left`?(Yc.setFieldData(null,t),Yc.reseedLifetimes(),$c.updateStatus(`default (cleared)`),Zc=!1):(Xc.setFieldData(null,t),Xc.reseedLifetimes(),el.updateStatus(`default (cleared)`),Qc=!1),ll()}function cl(){let e=window.innerWidth,t=window.innerHeight,n=e/t;zc.left=-Ic*n,zc.right=Ic*n,zc.top=Ic,zc.bottom=-Ic,zc.updateProjectionMatrix(),Lc.setSize(e,t,!1),Bc.setSize(e,t),Hc.setSize(e,t),ll()}function ll(){let e=(Zc?1:0)+(Qc?1:0),t=ol();e===2?(qc.visible=!0,Jc.visible=!0,Yc.setViewOffset(-t),Xc.setViewOffset(t)):e===1?Zc?(qc.visible=!0,Jc.visible=!1,Yc.setViewOffset(0)):(qc.visible=!1,Jc.visible=!0,Xc.setViewOffset(0)):(qc.visible=!0,Jc.visible=!1,Yc.setViewOffset(0),Xc.setViewOffset(0))}function ul(e,t){Fc.trailsEnabled=e,Fc.trailDecay=t,Uc.enabled=e,Uc.uniforms.damp.value=t}var dl=performance.now(),fl=Ec.find(e=>e.key===Fc.colorPresetA)??Ec[0],pl=Ec.find(e=>e.key===Fc.colorPresetB)??Ec[0];function ml(e){let t=e||performance.now(),n=Math.min(.033,(t-dl)/1e3);dl=t,Yc.update(n,fl),Xc.update(n,pl),Bc.render(),rl.update(),requestAnimationFrame(ml)}il(),tl.create(),rl.createControls(Pc.querySelector(`.controls`)),cl(),window.addEventListener(`resize`,cl),$c.load(),el.load(),al(),ml(0);
4181
+ }`},Cc=class extends dc{constructor(e=.96){super(),this.uniforms=Or.clone(Sc.uniforms),this.damp=e,this.compFsMaterial=new jr({uniforms:this.uniforms,vertexShader:Sc.vertexShader,fragmentShader:Sc.fragmentShader}),this.copyFsMaterial=new jr({uniforms:Or.clone(uc.uniforms),vertexShader:uc.vertexShader,fragmentShader:uc.fragmentShader,blending:0,depthTest:!1,depthWrite:!1}),this._textureComp=new xt(window.innerWidth,window.innerHeight,{magFilter:r,type:g}),this._textureOld=new xt(window.innerWidth,window.innerHeight,{magFilter:r,type:g}),this._compFsQuad=new mc(this.compFsMaterial),this._copyFsQuad=new mc(this.copyFsMaterial)}get damp(){return this.uniforms.damp.value}set damp(e){this.uniforms.damp.value=e}render(e,t,n){this.uniforms.tOld.value=this._textureOld.texture,this.uniforms.tNew.value=n.texture,e.setRenderTarget(this._textureComp),this._compFsQuad.render(e),this._copyFsQuad.material.uniforms.tDiffuse.value=this._textureComp.texture,this.renderToScreen?(e.setRenderTarget(null),this._copyFsQuad.render(e)):(e.setRenderTarget(t),this.clear&&e.clear(),this._copyFsQuad.render(e));let r=this._textureOld;this._textureOld=this._textureComp,this._textureComp=r}setSize(e,t){this._textureComp.setSize(e,t),this._textureOld.setSize(e,t)}dispose(){this._textureComp.dispose(),this._textureOld.dispose(),this.compFsMaterial.dispose(),this.copyFsMaterial.dispose(),this._compFsQuad.dispose(),this._copyFsQuad.dispose()}},wc=class{constructor(e=Math){this.grad3=[[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]],this.grad4=[[0,1,1,1],[0,1,1,-1],[0,1,-1,1],[0,1,-1,-1],[0,-1,1,1],[0,-1,1,-1],[0,-1,-1,1],[0,-1,-1,-1],[1,0,1,1],[1,0,1,-1],[1,0,-1,1],[1,0,-1,-1],[-1,0,1,1],[-1,0,1,-1],[-1,0,-1,1],[-1,0,-1,-1],[1,1,0,1],[1,1,0,-1],[1,-1,0,1],[1,-1,0,-1],[-1,1,0,1],[-1,1,0,-1],[-1,-1,0,1],[-1,-1,0,-1],[1,1,1,0],[1,1,-1,0],[1,-1,1,0],[1,-1,-1,0],[-1,1,1,0],[-1,1,-1,0],[-1,-1,1,0],[-1,-1,-1,0]],this.p=[];for(let t=0;t<256;t++)this.p[t]=Math.floor(e.random()*256);this.perm=[];for(let e=0;e<512;e++)this.perm[e]=this.p[e&255];this.simplex=[[0,1,2,3],[0,1,3,2],[0,0,0,0],[0,2,3,1],[0,0,0,0],[0,0,0,0],[0,0,0,0],[1,2,3,0],[0,2,1,3],[0,0,0,0],[0,3,1,2],[0,3,2,1],[0,0,0,0],[0,0,0,0],[0,0,0,0],[1,3,2,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[1,2,0,3],[0,0,0,0],[1,3,0,2],[0,0,0,0],[0,0,0,0],[0,0,0,0],[2,3,0,1],[2,3,1,0],[1,0,2,3],[1,0,3,2],[0,0,0,0],[0,0,0,0],[0,0,0,0],[2,0,3,1],[0,0,0,0],[2,1,3,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[2,0,1,3],[0,0,0,0],[0,0,0,0],[0,0,0,0],[3,0,1,2],[3,0,2,1],[0,0,0,0],[3,1,2,0],[2,1,0,3],[0,0,0,0],[0,0,0,0],[0,0,0,0],[3,1,0,2],[0,0,0,0],[3,2,0,1],[3,2,1,0]]}noise(e,t){let n,r,i,a=.5*(Math.sqrt(3)-1),o=(e+t)*a,s=Math.floor(e+o),c=Math.floor(t+o),l=(3-Math.sqrt(3))/6,u=(s+c)*l,d=s-u,f=c-u,p=e-d,m=t-f,h,g;p>m?(h=1,g=0):(h=0,g=1);let _=p-h+l,v=m-g+l,y=p-1+2*l,b=m-1+2*l,x=s&255,S=c&255,C=this.perm[x+this.perm[S]]%12,w=this.perm[x+h+this.perm[S+g]]%12,T=this.perm[x+1+this.perm[S+1]]%12,E=.5-p*p-m*m;E<0?n=0:(E*=E,n=E*E*this._dot(this.grad3[C],p,m));let D=.5-_*_-v*v;D<0?r=0:(D*=D,r=D*D*this._dot(this.grad3[w],_,v));let ee=.5-y*y-b*b;return ee<0?i=0:(ee*=ee,i=ee*ee*this._dot(this.grad3[T],y,b)),70*(n+r+i)}noise3d(e,t,n){let r,i,a,o,s=(e+t+n)*(1/3),c=Math.floor(e+s),l=Math.floor(t+s),u=Math.floor(n+s),d=1/6,f=(c+l+u)*d,p=c-f,m=l-f,h=u-f,g=e-p,_=t-m,v=n-h,y,b,x,S,C,w;g>=_?_>=v?(y=1,b=0,x=0,S=1,C=1,w=0):g>=v?(y=1,b=0,x=0,S=1,C=0,w=1):(y=0,b=0,x=1,S=1,C=0,w=1):_<v?(y=0,b=0,x=1,S=0,C=1,w=1):g<v?(y=0,b=1,x=0,S=0,C=1,w=1):(y=0,b=1,x=0,S=1,C=1,w=0);let T=g-y+d,E=_-b+d,D=v-x+d,ee=g-S+2*d,O=_-C+2*d,k=v-w+2*d,te=g-1+3*d,ne=_-1+3*d,A=v-1+3*d,j=c&255,M=l&255,N=u&255,re=this.perm[j+this.perm[M+this.perm[N]]]%12,ie=this.perm[j+y+this.perm[M+b+this.perm[N+x]]]%12,P=this.perm[j+S+this.perm[M+C+this.perm[N+w]]]%12,ae=this.perm[j+1+this.perm[M+1+this.perm[N+1]]]%12,oe=.6-g*g-_*_-v*v;oe<0?r=0:(oe*=oe,r=oe*oe*this._dot3(this.grad3[re],g,_,v));let F=.6-T*T-E*E-D*D;F<0?i=0:(F*=F,i=F*F*this._dot3(this.grad3[ie],T,E,D));let I=.6-ee*ee-O*O-k*k;I<0?a=0:(I*=I,a=I*I*this._dot3(this.grad3[P],ee,O,k));let L=.6-te*te-ne*ne-A*A;return L<0?o=0:(L*=L,o=L*L*this._dot3(this.grad3[ae],te,ne,A)),32*(r+i+a+o)}noise4d(e,t,n,r){let i=this.grad4,a=this.simplex,o=this.perm,s=(Math.sqrt(5)-1)/4,c=(5-Math.sqrt(5))/20,l,u,d,f,p,m=(e+t+n+r)*s,h=Math.floor(e+m),g=Math.floor(t+m),_=Math.floor(n+m),v=Math.floor(r+m),y=(h+g+_+v)*c,b=h-y,x=g-y,S=_-y,C=v-y,w=e-b,T=t-x,E=n-S,D=r-C,ee=w>T?32:0,O=w>E?16:0,k=T>E?8:0,te=w>D?4:0,ne=T>D?2:0,A=E>D?1:0,j=ee+O+k+te+ne+A,M=a[j][0]>=3?1:0,N=a[j][1]>=3?1:0,re=a[j][2]>=3?1:0,ie=a[j][3]>=3?1:0,P=a[j][0]>=2?1:0,ae=a[j][1]>=2?1:0,oe=a[j][2]>=2?1:0,F=a[j][3]>=2?1:0,I=a[j][0]>=1?1:0,L=a[j][1]>=1?1:0,se=a[j][2]>=1?1:0,ce=a[j][3]>=1?1:0,le=w-M+c,ue=T-N+c,R=E-re+c,de=D-ie+c,fe=w-P+2*c,pe=T-ae+2*c,me=E-oe+2*c,he=D-F+2*c,ge=w-I+3*c,_e=T-L+3*c,ve=E-se+3*c,ye=D-ce+3*c,be=w-1+4*c,xe=T-1+4*c,Se=E-1+4*c,Ce=D-1+4*c,we=h&255,z=g&255,Te=_&255,B=v&255,Ee=o[we+o[z+o[Te+o[B]]]]%32,V=o[we+M+o[z+N+o[Te+re+o[B+ie]]]]%32,De=o[we+P+o[z+ae+o[Te+oe+o[B+F]]]]%32,H=o[we+I+o[z+L+o[Te+se+o[B+ce]]]]%32,U=o[we+1+o[z+1+o[Te+1+o[B+1]]]]%32,Oe=.6-w*w-T*T-E*E-D*D;Oe<0?l=0:(Oe*=Oe,l=Oe*Oe*this._dot4(i[Ee],w,T,E,D));let ke=.6-le*le-ue*ue-R*R-de*de;ke<0?u=0:(ke*=ke,u=ke*ke*this._dot4(i[V],le,ue,R,de));let Ae=.6-fe*fe-pe*pe-me*me-he*he;Ae<0?d=0:(Ae*=Ae,d=Ae*Ae*this._dot4(i[De],fe,pe,me,he));let je=.6-ge*ge-_e*_e-ve*ve-ye*ye;je<0?f=0:(je*=je,f=je*je*this._dot4(i[H],ge,_e,ve,ye));let Me=.6-be*be-xe*xe-Se*Se-Ce*Ce;return Me<0?p=0:(Me*=Me,p=Me*Me*this._dot4(i[U],be,xe,Se,Ce)),27*(l+u+d+f+p)}_dot(e,t,n){return e[0]*t+e[1]*n}_dot3(e,t,n,r){return e[0]*t+e[1]*n+e[2]*r}_dot4(e,t,n,r,i){return e[0]*t+e[1]*n+e[2]*r+e[3]*i}};const Tc=1.25,Ec=[{key:`luminous-violet`,label:`Luminous violet`,rgb:[.6,.25,.9]},{key:`pure-white`,label:`Pure white`,rgb:[1,1,1]},{key:`neon-cyan`,label:`Neon cyan`,rgb:[.25,.95,1]},{key:`electric-lime`,label:`Electric lime`,rgb:[.75,1,.25]},{key:`solar-flare`,label:`Solar flare`,rgb:[1,.55,.15]},{key:`aurora-mint`,label:`Aurora mint`,rgb:[.4,1,.85]},{key:`sunrise-coral`,label:`Sunrise coral`,rgb:[1,.6,.5]},{key:`ember-gold`,label:`Ember gold`,rgb:[1,.8,.2]}],Dc=`luminous-violet`,Oc={size:2,bloomStrength:1.2,bloomRadius:.35,lifeMin:.5,lifeMax:1.4,fieldValidDistance:.05,speed:6,particleCount:5e3,colorPresetA:Dc,colorPresetB:Dc,noiseStrength:0,trailsEnabled:!1,trailDecay:.9};var kc=class{positions;colors;lifetimes;fieldData=null;fieldTransform={scale:1,offsetX:0,offsetY:0};grid=new Map;gridCellSize=.1;geometry;params;viewOffsetX;activePalette=null;noise;noiseScale=.9;noiseTimeScale=.15;constructor(e,t){this.geometry=e,this.params=t,this.viewOffsetX=0,this.positions=new Float32Array(t.particleCount*3),this.colors=new Float32Array(t.particleCount*3),this.lifetimes=new Float32Array(t.particleCount),this.noise=new wc,this.geometry.setAttribute(`position`,new Zn(this.positions,3)),this.geometry.setAttribute(`color`,new Zn(this.colors,3))}init(){for(let e=0;e<this.params.particleCount;e+=1)this.resetParticle(e);this.updateBuffers()}setFieldData(e,t){this.fieldData=e,this.fieldTransform=t,this.buildSpatialGrid()}hasFieldData(){return this.fieldData!==null&&this.fieldData.length>0}setViewOffset(e){if(this.viewOffsetX!==e){this.viewOffsetX=e;for(let e=0;e<this.params.particleCount;e+=1)this.resetParticle(e);this.updateBuffers()}}update(e,t){let n=performance.now()*.001;this.activePalette=t;let r=this.params.noiseStrength>0,i=.85*this.params.speed*e,a=.015*e;for(let o=0;o<this.params.particleCount;o+=1){let s=o*3,c=this.positions[s],l=this.positions[s+1],u=this.sampleField(c,l,n);if(!u){this.resetParticle(o);continue}r&&this.applyNoise(u,c,l,n),c+=u.x*i+this.randomRange(-a,a),l+=u.y*i+this.randomRange(-a,a);let d=Math.hypot(u.x,u.y),f=Math.min(1,d*2.6);this.applyColor(s,f,t,!1),this.lifetimes[o]-=e,this.shouldResetParticle(o,c,l)?this.resetParticle(o):(this.positions[s]=c,this.positions[s+1]=l)}this.updateBuffers()}resizeBuffers(e){this.params.particleCount=e,this.positions=new Float32Array(e*3),this.colors=new Float32Array(e*3),this.lifetimes=new Float32Array(e),this.geometry.setAttribute(`position`,new Zn(this.positions,3)),this.geometry.setAttribute(`color`,new Zn(this.colors,3)),this.init()}reseedLifetimes(){for(let e=0;e<this.params.particleCount;e+=1)this.lifetimes[e]=this.randomRange(this.params.lifeMin,this.params.lifeMax)}shouldResetParticle(e,t,n){return this.lifetimes[e]<=0||Math.abs(t-this.viewOffsetX)>1.25||Math.abs(n)>1.25}applyNoise(e,t,n,r){let i=this.noise.noise3d(t*this.noiseScale,n*this.noiseScale,r*this.noiseTimeScale),a=this.noise.noise3d((t+10)*this.noiseScale,(n+10)*this.noiseScale,r*this.noiseTimeScale);e.x+=i*this.params.noiseStrength,e.y+=a*this.params.noiseStrength}resetParticle(e){let t=e*3,n=this.activePalette??this.getActiveColorPreset();this.hasFieldData()?this.resetParticleWithinField(t):this.resetParticleRandomly(t),this.lifetimes[e]=this.randomRange(this.params.lifeMin,this.params.lifeMax);let r=.4+Math.random()*.2;this.applyColor(t,r,n,!0)}resetParticleWithinField(e){let t=this.fieldData[Math.floor(Math.random()*this.fieldData.length)],n=this.params.fieldValidDistance*.3,r=t.x+this.randomRange(-n,n)/this.fieldTransform.scale,i=t.y+this.randomRange(-n,n)/this.fieldTransform.scale;this.positions[e]=this.dataToWorldX(r),this.positions[e+1]=this.dataToWorldY(i),this.positions[e+2]=0}resetParticleRandomly(e){this.positions[e]=this.randomRange(-Tc,Tc)+this.viewOffsetX,this.positions[e+1]=this.randomRange(-Tc,Tc),this.positions[e+2]=0}dataToWorldX(e){return e*this.fieldTransform.scale+this.fieldTransform.offsetX+this.viewOffsetX}dataToWorldY(e){return e*this.fieldTransform.scale+this.fieldTransform.offsetY}worldToDataX(e){return(e-this.viewOffsetX-this.fieldTransform.offsetX)/this.fieldTransform.scale}worldToDataY(e){return(e-this.fieldTransform.offsetY)/this.fieldTransform.scale}buildSpatialGrid(){if(this.grid.clear(),!this.fieldData||this.fieldData.length===0)return;let e=this.fieldData[0].x,t=this.fieldData[0].x,n=this.fieldData[0].y,r=this.fieldData[0].y;for(let i of this.fieldData)i.x<e&&(e=i.x),i.x>t&&(t=i.x),i.y<n&&(n=i.y),i.y>r&&(r=i.y);let i=(t-e+(r-n))/2,a=Math.ceil(Math.sqrt(this.fieldData.length));this.gridCellSize=Math.max(.01,i/a);for(let e of this.fieldData){let t=this.getGridKey(e.x,e.y),n=this.grid.get(t);n?n.push(e):this.grid.set(t,[e])}}getGridKey(e,t){return`${Math.floor(e/this.gridCellSize)},${Math.floor(t/this.gridCellSize)}`}sampleField(e,t,n){if(!this.hasFieldData())return{x:1,y:0};let r=this.worldToDataX(e),i=this.worldToDataY(t),a=this.findNearestFieldPoint(r,i);return a?{x:a.dx*this.fieldTransform.scale,y:a.dy*this.fieldTransform.scale}:null}findNearestFieldPoint(e,t){let n=null,r=Number.MAX_VALUE,i=(this.params.fieldValidDistance/this.fieldTransform.scale)**2,a=Math.floor(e/this.gridCellSize),o=Math.floor(t/this.gridCellSize);for(let i=-1;i<=1;i+=1)for(let s=-1;s<=1;s+=1){let c=this.grid.get(`${a+i},${o+s}`);if(c)for(let i of c){let a=i.x-e,o=i.y-t,s=a*a+o*o;s<r&&(r=s,n=i)}}return r<=i?n:null}applyColor(e,t,n,r){let i=Math.min(1,Math.max(0,t));if(n.key===`luminous-violet`){r?(this.colors[e]=.6*i,this.colors[e+1]=.25*i,this.colors[e+2]=.9*i):(this.colors[e]=.35+i*.9,this.colors[e+1]=.18+i*.45,this.colors[e+2]=.6+i*.35);return}let a=r?i:.35+i*.65,[o,s,c]=n.rgb;this.colors[e]=o*a,this.colors[e+1]=s*a,this.colors[e+2]=c*a}getActiveColorPreset(){return Ec.find(e=>e.key===this.params.colorPresetA)??Ec[0]}randomRange(e,t){return e+Math.random()*(t-e)}updateBuffers(){this.geometry.attributes.position.needsUpdate=!0,this.geometry.attributes.color.needsUpdate=!0,this.geometry.computeBoundingSphere()}},Ac=class{fieldStatusEl=null;onFieldLoaded;constructor(e){this.onFieldLoaded=e}setStatusElement(e){this.fieldStatusEl=e}async load(){try{let e=await fetch(`/vector-field.json`,{cache:`no-store`});if(!e.ok){this.updateStatus(`default (built-in)`);return}let t=await e.json();if(Array.isArray(t)&&t.length>0){let{transform:e,bounds:n}=this.computeFieldTransform(t);this.onFieldLoaded(t,e),this.updateStatus(`loaded ${t.length} vectors (${n.width.toFixed(1)}ร—${n.height.toFixed(1)})`),console.log(`Field bounds:`,n,`scale:`,e.scale)}else this.updateStatus(`default (empty file)`)}catch(e){console.error(`Failed to load vector field`,e),this.updateStatus(`default (load error)`)}}computeFieldTransform(e){let t=e[0].x,n=e[0].x,r=e[0].y,i=e[0].y;for(let a of e)a.x<t&&(t=a.x),a.x>n&&(n=a.x),a.y<r&&(r=a.y),a.y>i&&(i=a.y);let a=n-t,o=i-r,s=Math.max(a,o);Tc*1.8;let c=s>0?2.25/s:1;return{transform:{scale:c,offsetX:-(t+n)*.5*c,offsetY:-(r+i)*.5*c},bounds:{minX:t,maxX:n,minY:r,maxY:i,width:a,height:o}}}updateStatus(e){this.fieldStatusEl&&(this.fieldStatusEl.textContent=e)}},jc=class{panel;controlHandles=new Map;selectHandles=new Map;trailToggle;container;params;material;bloomPass;callbacks;constructor(e,t,n,r,i){this.container=e,this.params=t,this.material=n,this.bloomPass=r,this.callbacks=i}create(){this.panel=document.createElement(`div`),this.panel.className=`controls`;let e=document.createElement(`div`);e.className=`controls__header`;let t=document.createElement(`div`);t.className=`controls__title`,t.textContent=`Controls`;let n=document.createElement(`button`);n.className=`controls__toggle`,n.textContent=`โˆ’`,n.type=`button`,n.addEventListener(`click`,()=>{this.panel.classList.toggle(`controls--collapsed`),n.textContent=this.panel.classList.contains(`controls--collapsed`)?`+`:`โˆ’`,window.dispatchEvent(new Event(`resize`))}),e.appendChild(t),e.appendChild(n),this.panel.appendChild(e);let r=this.addSelect(`Field A color`,Ec,this.params.colorPresetA,e=>{this.params.colorPresetA=e,this.callbacks.onColorChange()}),i=this.addSelect(`Field B color`,Ec,this.params.colorPresetB,e=>{this.params.colorPresetB=e,this.callbacks.onColorChange()});this.selectHandles.set(`colorA`,r),this.selectHandles.set(`colorB`,i);let a=this.addSlider(this.panel,`Speed`,.1,8,.1,this.params.speed,e=>{this.params.speed=e});this.controlHandles.set(`speed`,a);let o=document.createElement(`button`);o.type=`button`,o.className=`controls__button`,o.textContent=`Show advanced`;let s=document.createElement(`div`);s.className=`controls__advanced`,s.style.display=`none`;let c=this.addSlider(s,`Noise`,0,1,.01,this.params.noiseStrength,e=>{this.params.noiseStrength=e});this.controlHandles.set(`noiseStrength`,c);let l=this.addSlider(s,`Size`,.5,4,.1,this.params.size,e=>{this.params.size=e,this.material.size=e});this.controlHandles.set(`size`,l);let u=this.addSlider(s,`Particle count`,100,8e3,100,this.params.particleCount,e=>{this.params.particleCount=Math.round(e),this.callbacks.onParticleCountChange(this.params.particleCount)});this.controlHandles.set(`particleCount`,u);let d=this.addSlider(s,`Bloom strength`,.2,2.5,.05,this.params.bloomStrength,e=>{this.params.bloomStrength=e,this.updateBloom()});this.controlHandles.set(`bloomStrength`,d);let f=this.addSlider(s,`Bloom radius`,0,1.2,.02,this.params.bloomRadius,e=>{this.params.bloomRadius=e,this.updateBloom()});this.controlHandles.set(`bloomRadius`,f);let p=this.addSlider(s,`Life min (s)`,.1,2,.05,this.params.lifeMin,e=>{if(this.params.lifeMin=e,this.params.lifeMin>this.params.lifeMax){this.params.lifeMax=e;let t=this.controlHandles.get(`lifeMax`);t&&this.syncSlider(t,this.params.lifeMax)}this.callbacks.onLifetimeChange()});this.controlHandles.set(`lifeMin`,p);let m=this.addSlider(s,`Life max (s)`,.2,5,.05,this.params.lifeMax,e=>{if(this.params.lifeMax=e,this.params.lifeMax<this.params.lifeMin){this.params.lifeMin=e;let t=this.controlHandles.get(`lifeMin`);t&&this.syncSlider(t,this.params.lifeMin)}this.callbacks.onLifetimeChange()});this.controlHandles.set(`lifeMax`,m);let h=this.addSlider(s,`Field border`,.01,.1,.01,this.params.fieldValidDistance,e=>{this.params.fieldValidDistance=e});this.controlHandles.set(`fieldDist`,h),this.trailToggle=this.addToggle(s,`Trails`,this.params.trailsEnabled,e=>{this.params.trailsEnabled=e,this.callbacks.onTrailToggle(e)});let g=this.addSlider(s,`Trail decay`,.7,.99,.005,this.params.trailDecay,e=>{this.params.trailDecay=e,this.callbacks.onTrailDecayChange(e)});this.controlHandles.set(`trailDecay`,g),o.addEventListener(`click`,()=>{let e=s.style.display===`none`;s.style.display=e?`block`:`none`,o.textContent=e?`Hide advanced`:`Show advanced`}),this.panel.appendChild(o),this.panel.appendChild(s);let _=document.createElement(`button`);_.type=`button`,_.className=`controls__button`,_.textContent=`Reset to defaults`,_.addEventListener(`click`,()=>this.reset()),this.panel.appendChild(_);let v=document.createElement(`div`);v.className=`controls__section`;let y=document.createElement(`div`);y.className=`controls__subtitle`,y.textContent=`Fields`,v.appendChild(y);let b=document.createElement(`div`);b.className=`controls__row`;let x=document.createElement(`span`);x.textContent=`Clear Field A`;let S=document.createElement(`button`);S.type=`button`,S.className=`controls__button`,S.textContent=`Clear`,S.addEventListener(`click`,()=>this.callbacks.onClearFieldA()),b.appendChild(x),b.appendChild(S),v.appendChild(b);let C=document.createElement(`div`);C.className=`controls__row`;let w=document.createElement(`span`);w.textContent=`Clear Field B`;let T=document.createElement(`button`);T.type=`button`,T.className=`controls__button`,T.textContent=`Clear`,T.addEventListener(`click`,()=>this.callbacks.onClearFieldB()),C.appendChild(w),C.appendChild(T),v.appendChild(C),this.panel.appendChild(v),this.container.appendChild(this.panel)}syncFieldValidDistance(e){this.params.fieldValidDistance=e;let t=this.controlHandles.get(`fieldDist`);t&&this.syncSlider(t,e)}reset(){Object.assign(this.params,Oc),this.material.size=this.params.size,this.updateBloom(),this.callbacks.onParticleCountChange(this.params.particleCount),this.callbacks.onLifetimeChange(),this.callbacks.onTrailToggle(this.params.trailsEnabled),this.callbacks.onTrailDecayChange(this.params.trailDecay),this.callbacks.onColorChange();for(let[e,t]of this.controlHandles.entries()){let n=e;typeof this.params[n]==`number`&&this.syncSlider(t,this.params[n])}for(let[e,t]of this.selectHandles.entries())e===`colorA`&&(t.value=this.params.colorPresetA),e===`colorB`&&(t.value=this.params.colorPresetB);this.trailToggle&&(this.trailToggle.checked=this.params.trailsEnabled)}addToggle(e,t,n,r){let i=document.createElement(`label`);i.className=`controls__row`;let a=document.createElement(`span`);a.textContent=t;let o=document.createElement(`input`);return o.type=`checkbox`,o.checked=n,o.addEventListener(`change`,e=>{let t=e.target.checked;r(t)}),i.appendChild(a),i.appendChild(o),e.appendChild(i),o}addSlider(e,t,n,r,i,a,o){let s=document.createElement(`label`);s.className=`controls__row`;let c=document.createElement(`span`);c.textContent=t;let l=document.createElement(`input`);l.type=`range`,l.min=String(n),l.max=String(r),l.step=String(i),l.value=String(a);let u=document.createElement(`span`);return u.className=`controls__value`,u.textContent=this.formatValue(a,i),l.addEventListener(`input`,e=>{let t=parseFloat(e.target.value);u.textContent=this.formatValue(t,i),o(t)}),s.appendChild(c),s.appendChild(l),s.appendChild(u),e.appendChild(s),{input:l,valueTag:u}}addSelect(e,t,n,r){let i=document.createElement(`label`);i.className=`controls__row`;let a=document.createElement(`span`);a.textContent=e;let o=document.createElement(`select`);o.className=`controls__select`;for(let e of t){let t=document.createElement(`option`);t.value=e.key,t.textContent=e.label,o.appendChild(t)}return o.value=n,o.addEventListener(`change`,e=>{let t=e.target.value;r(t)}),i.appendChild(a),i.appendChild(o),this.panel.appendChild(i),o}updateBloom(){this.bloomPass.strength=this.params.bloomStrength,this.bloomPass.radius=this.params.bloomRadius}formatValue(e,t){return t>=1?e.toFixed(0):e.toFixed(2)}syncSlider(e,t){e.input.value=String(t),e.valueTag.textContent=this.formatValue(t,parseFloat(e.input.step))}},Mc=class{mediaRecorder=null;recordedChunks=[];isRecording=!1;recordingStartTime=0;recordingDuration=5;recordingResolution=`current`;originalCanvasSize=null;recordButton=null;recordStatus=null;renderer;composer;bloomPass;onResize;constructor(e,t,n,r){this.renderer=e,this.composer=t,this.bloomPass=n,this.onResize=r}createControls(e){let t=document.createElement(`div`);t.className=`controls__section`,t.innerHTML=`<div class="controls__subtitle">Export (WebM)</div>`;let n=document.createElement(`label`);n.className=`controls__row`;let r=document.createElement(`span`);r.textContent=`Resolution`;let i=document.createElement(`select`);i.className=`controls__select`,i.innerHTML=`<option value="current">Current window</option><option value="1080p">1080p (Full HD)</option><option value="1440p">1440p (2K)</option><option value="4k">4K (Ultra HD)</option>`,i.value=this.recordingResolution,i.addEventListener(`change`,e=>{this.recordingResolution=e.target.value}),n.appendChild(r),n.appendChild(i),t.appendChild(n);let a=document.createElement(`label`);a.className=`controls__row`;let o=document.createElement(`span`);o.textContent=`Duration`;let s=document.createElement(`select`);s.className=`controls__select`,s.innerHTML=`<option value="3">3 seconds</option><option value="5">5 seconds</option><option value="10">10 seconds</option><option value="15">15 seconds</option>`,s.value=String(this.recordingDuration),s.addEventListener(`change`,e=>{this.recordingDuration=parseInt(e.target.value)}),a.appendChild(o),a.appendChild(s),t.appendChild(a),this.recordButton=document.createElement(`button`),this.recordButton.type=`button`,this.recordButton.className=`controls__button controls__button--record`,this.recordButton.textContent=`โบ Start recording`,this.recordButton.addEventListener(`click`,()=>{this.isRecording?this.stop():this.start()}),t.appendChild(this.recordButton),this.recordStatus=document.createElement(`div`),this.recordStatus.className=`controls__status`,this.recordStatus.style.display=`none`,t.appendChild(this.recordStatus),e.appendChild(t)}update(){if(this.isRecording){let e=(performance.now()-this.recordingStartTime)/1e3;this.updateStatus(e),e>=this.recordingDuration&&this.stop()}}start(){if(!this.isRecording)try{let e=this.renderer.domElement,t,n,r=e.width/e.height;if(this.recordingResolution===`current`)t=e.width,n=e.height;else{switch(this.originalCanvasSize={width:e.width,height:e.height},this.recordingResolution){case`1080p`:n=1080,t=Math.round(n*r);break;case`1440p`:n=1440,t=Math.round(n*r);break;case`4k`:n=2160,t=Math.round(n*r);break}this.renderer.setSize(t,n,!1),this.composer.setSize(t,n),this.bloomPass.setSize(t,n)}let i=e.captureStream(60),a=t*n,o=Math.min(25e6,Math.max(8e6,a*4));this.recordedChunks=[],this.mediaRecorder=new MediaRecorder(i,{mimeType:`video/webm;codecs=vp9`,videoBitsPerSecond:o}),this.mediaRecorder.ondataavailable=e=>{e.data.size>0&&this.recordedChunks.push(e.data)},this.mediaRecorder.onstop=()=>{this.originalCanvasSize&&(this.renderer.setSize(this.originalCanvasSize.width,this.originalCanvasSize.height,!1),this.composer.setSize(this.originalCanvasSize.width,this.originalCanvasSize.height),this.bloomPass.setSize(this.originalCanvasSize.width,this.originalCanvasSize.height),this.originalCanvasSize=null,this.onResize());let e=new Blob(this.recordedChunks,{type:`video/webm`}),r=URL.createObjectURL(e),i=document.createElement(`a`);i.href=r,i.download=`luminar-${t}x${n}-${Date.now()}.webm`,i.click(),URL.revokeObjectURL(r),this.recordStatus&&(this.recordStatus.textContent=`Recording complete! Download started.`,setTimeout(()=>{this.recordStatus&&(this.recordStatus.style.display=`none`)},3e3))},this.mediaRecorder.start(),this.isRecording=!0,this.recordingStartTime=performance.now(),this.recordButton&&(this.recordButton.textContent=`โน Stop recording`,this.recordButton.style.opacity=`1`),this.recordStatus&&(this.recordStatus.style.display=`block`,this.recordStatus.textContent=`Recording at ${t}x${n} (${(o/1e6).toFixed(0)} Mbps): 0.0s / ${this.recordingDuration}s`)}catch(e){console.error(`Failed to start recording:`,e),this.originalCanvasSize&&(this.renderer.setSize(this.originalCanvasSize.width,this.originalCanvasSize.height,!1),this.composer.setSize(this.originalCanvasSize.width,this.originalCanvasSize.height),this.bloomPass.setSize(this.originalCanvasSize.width,this.originalCanvasSize.height),this.originalCanvasSize=null,this.onResize()),this.recordStatus&&(this.recordStatus.style.display=`block`,this.recordStatus.textContent=`Recording not supported in this browser.`)}}stop(){!this.isRecording||!this.mediaRecorder||(this.isRecording=!1,this.mediaRecorder.stop(),this.mediaRecorder=null,this.recordButton&&(this.recordButton.textContent=`โ–ถ Start recording`,this.recordButton.style.opacity=`1`))}updateStatus(e){if(this.recordStatus){let t=e.toFixed(1),n=this.renderer.domElement,r=Math.min(25e6,Math.max(8e6,n.width*n.height*4));this.recordStatus.textContent=`Recording at ${n.width}x${n.height} (${(r/1e6).toFixed(0)} Mbps): ${t}s / ${this.recordingDuration}s`}}};function Nc(e){let t=e.split(/\r?\n/).filter(Boolean),n=[],r=!1;for(let e of t){let t=e.split(/[,\s]+/).filter(Boolean);if(t.length<4)continue;let[i,a,o,s]=t.map(Number);if([i,a,o,s].some(e=>Number.isNaN(e))){!r&&n.length===0&&(r=!0,console.log(`skipping header line:`,e.substring(0,60)));continue}n.push({x:i,y:a,dx:o,dy:s})}return n}var Pc=document.querySelector(`#app`);if(!Pc)throw Error(`Missing #app container`);var Fc={...Oc},Ic=1.8,Lc=new lc({antialias:!1,alpha:!0});Lc.setPixelRatio(Math.min(window.devicePixelRatio,2)),Pc.appendChild(Lc.domElement);var Rc=new Gr;Rc.background=new Z(132106);var zc=new Ai(-1,1,1,-1,.1,10);zc.position.z=2;var Bc=new vc(Lc),Vc=new yc(Rc,zc),Hc=new xc(new q(1,1),Fc.bloomStrength,.82,Fc.bloomRadius),Uc=new Cc(Fc.trailDecay);Bc.addPass(Vc),Bc.addPass(Hc),Bc.addPass(Uc),Uc.enabled=Fc.trailsEnabled,Uc.uniforms.damp.value=Fc.trailDecay;var Wc=new cr,Gc=new cr,Kc=new ti({size:Fc.size,sizeAttenuation:!0,vertexColors:!0,transparent:!0,opacity:.9,blending:2,depthWrite:!1}),qc=new oi(Wc,Kc),Jc=new oi(Gc,Kc);Rc.add(qc),Rc.add(Jc);var Yc=new kc(Wc,Fc),Xc=new kc(Gc,Fc);Yc.init(),Xc.init();var Zc=!1,Qc=!1,$c=new Ac((e,t)=>{Yc.setFieldData(e,t),Zc=Yc.hasFieldData(),ll()}),el=new Ac((e,t)=>{Xc.setFieldData(e,t),Qc=Xc.hasFieldData(),ll()}),tl=new jc(Pc,Fc,Kc,Hc,{onParticleCountChange:e=>{Yc.resizeBuffers(e),Xc.resizeBuffers(e)},onLifetimeChange:()=>{Yc.reseedLifetimes(),Xc.reseedLifetimes()},onTrailToggle:e=>ul(e,Fc.trailDecay),onTrailDecayChange:e=>ul(Fc.trailsEnabled,e),onClearFieldA:()=>sl(`left`),onClearFieldB:()=>sl(`right`),onColorChange:()=>nl()});function nl(){fl=Ec.find(e=>e.key===Fc.colorPresetA)??Ec[0],pl=Ec.find(e=>e.key===Fc.colorPresetB)??Ec[0]}var rl=new Mc(Lc,Bc,Hc,cl);function il(){if(!Pc)return;let e=document.createElement(`div`);e.className=`hud`,e.innerHTML=`<div class="title">luminar</div><div class="subtitle">2D vector field bloom study</div><div class="status">Field A: <span id="field-status-a">default (built-in)</span> ยท Field B: <span id="field-status-b">default (built-in)</span></div>`,Pc.appendChild(e),$c.setStatusElement(document.getElementById(`field-status-a`)),el.setStatusElement(document.getElementById(`field-status-b`))}function al(){if(!Pc)return;let e=document.createElement(`div`);e.className=`drop-overlay drop-overlay--left`,e.textContent=`Drop to load Field A (left)`,e.style.display=`none`,Pc.appendChild(e);let t=document.createElement(`div`);t.className=`drop-overlay drop-overlay--right`,t.textContent=`Drop to load Field B (right)`,t.style.display=`none`,Pc.appendChild(t);let n=n=>{n===`left`?(e.style.display=`flex`,t.style.display=`none`):(e.style.display=`none`,t.style.display=`flex`)},r=()=>{e.style.display=`none`,t.style.display=`none`},i=async(e,t)=>{try{let n=Nc(await e.text());if(!n.length){t===`left`?$c.updateStatus(`CSV empty or invalid`):el.updateStatus(`CSV empty or invalid`),r();return}let i=t===`left`?$c:el,{transform:a,bounds:o}=i.computeFieldTransform(n);t===`left`?(Yc.setFieldData(n,a),Zc=Yc.hasFieldData()):(Xc.setFieldData(n,a),Qc=Xc.hasFieldData()),i.updateStatus(`loaded ${n.length} vectors (${o.width.toFixed(1)}ร—${o.height.toFixed(1)})`),ll()}catch(e){console.error(`Failed to load dropped CSV`,e),t===`left`?$c.updateStatus(`CSV load error`):el.updateStatus(`CSV load error`)}finally{r()}};window.addEventListener(`dragover`,e=>{e.preventDefault(),n(e.clientX<window.innerWidth*.5?`left`:`right`)}),window.addEventListener(`dragleave`,e=>{e.preventDefault(),r()}),window.addEventListener(`drop`,e=>{e.preventDefault();let t=e.dataTransfer;if(t&&t.files&&t.files[0]){let n=e.clientX<window.innerWidth*.5?`left`:`right`;i(t.files[0],n)}})}function ol(){let e=Ic*(window.innerWidth/window.innerHeight)*2,t=e*.45/2,n=(e-258/window.innerWidth*e-.5)/2,r=Math.min(1.4,n);return Math.max(1,Math.min(t,r))}function sl(e){let t={scale:1,offsetX:0,offsetY:0};e===`left`?(Yc.setFieldData(null,t),Yc.reseedLifetimes(),$c.updateStatus(`default (cleared)`),Zc=!1):(Xc.setFieldData(null,t),Xc.reseedLifetimes(),el.updateStatus(`default (cleared)`),Qc=!1),ll()}function cl(){let e=window.innerWidth,t=window.innerHeight,n=e/t;zc.left=-Ic*n,zc.right=Ic*n,zc.top=Ic,zc.bottom=-Ic,zc.updateProjectionMatrix(),Lc.setSize(e,t,!1),Bc.setSize(e,t),Hc.setSize(e,t),ll()}function ll(){let e=(Zc?1:0)+(Qc?1:0),t=ol();e===2?(qc.visible=!0,Jc.visible=!0,Yc.setViewOffset(-t),Xc.setViewOffset(t)):e===1?Zc?(qc.visible=!0,Jc.visible=!1,Yc.setViewOffset(0)):(qc.visible=!1,Jc.visible=!0,Xc.setViewOffset(0)):(qc.visible=!0,Jc.visible=!1,Yc.setViewOffset(0),Xc.setViewOffset(0))}function ul(e,t){Fc.trailsEnabled=e,Fc.trailDecay=t,Uc.enabled=e,Uc.uniforms.damp.value=t}var dl=performance.now(),fl=Ec.find(e=>e.key===Fc.colorPresetA)??Ec[0],pl=Ec.find(e=>e.key===Fc.colorPresetB)??Ec[0];function ml(e){let t=e||performance.now(),n=Math.min(.033,(t-dl)/1e3);dl=t,Yc.update(n,fl),Xc.update(n,pl),Bc.render(),rl.update(),requestAnimationFrame(ml)}il(),tl.create(),rl.createControls(Pc.querySelector(`.controls`)),cl(),window.addEventListener(`resize`,cl),$c.load(),el.load(),al(),ml(0);
package/dist/index.html CHANGED
@@ -6,7 +6,7 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>luminar</title>
9
- <script type="module" crossorigin src="/assets/index-DqXax9_P.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-DtdKjtzr.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="/assets/index-BTv18fJQ.css">
11
11
  </head>
12
12