@wave3d/core 0.6.0 → 0.7.0

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.
@@ -157,6 +157,107 @@ vec3 applyColorGrade(vec3 c){
157
157
  return hueShift(c, radians(uHueShift));
158
158
  }
159
159
  `;
160
+ const waveShapeChunk = `
161
+ // expStep: a falloff from 1 (at x=0) toward 0, sharpness set by n. The
162
+ // max() guards pow(0, n) (= Infinity → NaN) so negative n is safe — negative n
163
+ // just concentrates the twist toward the OTHER end instead.
164
+ float expStep(float x, float n){ return exp2(-exp2(n) * pow(max(x, 1.0e-3), n)); }
165
+
166
+ // rotationMatrix (mat4), used row-vector style: pos = (vec4(pos,1) * R).xyz
167
+ mat4 rotationMatrix(vec3 axis, float angle){
168
+ axis = normalize(axis);
169
+ float s = sin(angle), c = cos(angle), oc = 1.0 - c;
170
+ return mat4(
171
+ oc*axis.x*axis.x + c, oc*axis.x*axis.y - axis.z*s, oc*axis.z*axis.x + axis.y*s, 0.0,
172
+ oc*axis.x*axis.y + axis.z*s, oc*axis.y*axis.y + c, oc*axis.y*axis.z - axis.x*s, 0.0,
173
+ oc*axis.z*axis.x - axis.y*s, oc*axis.y*axis.z + axis.x*s, oc*axis.z*axis.z + c, 0.0,
174
+ 0.0, 0.0, 0.0, 1.0
175
+ );
176
+ }
177
+
178
+ // The wave SHAPE deform, shared by the wave vertex shader (below) and the particle SHED emitter
179
+ // (particleVertexShader): a base hairpin position + its uv → the displaced / helixed / twisted /
180
+ // fanned LOCAL position, plus the three twist matrices (the pointer field reads them). Every branch
181
+ // sits behind the SAME #ifdef gates as the code it replaces, so a given compiled program is
182
+ // byte-identical to the former inline version. t / loopOff are the linear / orbit time the
183
+ // caller computed; only the one selected by LOOP_MOTION is read (the other is a dead argument).
184
+ struct WaveShape { vec3 pos; mat4 rotA; mat4 rotB; mat4 rotC; };
185
+ WaveShape waveShape(vec3 position, vec2 uv, float t, vec2 loopOff){
186
+ // Displacement lifts Y by simplex noise of the (x,z) position.
187
+ vec3 pos = position;
188
+ #ifdef LOOP_MOTION
189
+ pos.y += uDispAmount * simplexNoise(vec2(pos.x * uDispFreqX, pos.z * uDispFreqZ) + loopOff);
190
+ #else
191
+ pos.y += uDispAmount * simplexNoise(vec2(pos.x * uDispFreqX + t, pos.z * uDispFreqZ + t));
192
+ #endif
193
+ #ifdef DETAIL_OCTAVE
194
+ // A second, finer octave riding on the broad swell (loop-orbit shared so it stays periodic).
195
+ #ifdef LOOP_MOTION
196
+ pos.y += uDetailAmount * simplexNoise(vec2(pos.x * uDetailFreq, pos.z * uDetailFreq) + loopOff);
197
+ #else
198
+ pos.y += uDetailAmount * simplexNoise(vec2(pos.x * uDetailFreq + t, pos.z * uDetailFreq + t));
199
+ #endif
200
+ #endif
201
+
202
+ #ifdef HELIX
203
+ // Helix — the periodic sweep the three twists (monotone falloffs) can't reach. Runs AFTER the
204
+ // displacement (so the noise still samples undeformed pos) and BEFORE the twist (so they compose).
205
+ float hAng = 6.28318530718 * uHelixTurns * uv.y + radians(uHelixPhase);
206
+ // Roll about the ribbon's width centre, not the origin — see RIBBON_Z_CENTER in WaveGeometry.
207
+ float rollA = hAng * uHelixRoll;
208
+ float rollC = cos(rollA), rollS = sin(rollA);
209
+ vec2 rel = vec2(pos.y, pos.z - ${(-8).toFixed(1)});
210
+ pos.y = rel.x * rollC - rel.y * rollS;
211
+ pos.z = ${(-8).toFixed(1)} + rel.x * rollS + rel.y * rollC;
212
+ pos.y += uHelixRadius * cos(hAng);
213
+ pos.z += uHelixRadius * sin(hAng);
214
+ #endif
215
+
216
+ // The X-twist frequency feeding rotB; the TWIST_MOTION variant modulates it with simplex noise
217
+ // indexed along the ribbon (uv.y) so the twist breathes over time.
218
+ float twistXFreq = uTwFreqX;
219
+ #ifdef TWIST_MOTION
220
+ #ifdef LOOP_MOTION
221
+ float twistXNoise = simplexNoise(vec2(uv.y * 2.0, 0.0) + loopOff);
222
+ #else
223
+ float twistXNoise = simplexNoise(vec2(uv.y * 2.0, t));
224
+ #endif
225
+ twistXFreq = uTwFreqX - twistXNoise * 0.1;
226
+ #endif
227
+
228
+ // Three-axis twist (see the falloff-axis note: rotA keys off uv.x/WIDTH, rotB/rotC off uv.y/LENGTH).
229
+ mat4 rotA = rotationMatrix(vec3(0.5, 0.0, 0.5), uTwFreqY * expStep(uv.x, uTwPowY));
230
+ mat4 rotB = rotationMatrix(vec3(0.0, 0.5, 0.5), twistXFreq * expStep(uv.y, uTwPowX));
231
+ mat4 rotC = rotationMatrix(vec3(0.5, 0.0, 0.5), uTwFreqZ * expStep(uv.y, uTwPowZ));
232
+ pos = (vec4(pos, 1.0) * rotA).xyz;
233
+ pos = (vec4(pos, 1.0) * rotB).xyz;
234
+ pos = (vec4(pos, 1.0) * rotC).xyz;
235
+
236
+ #ifdef RADIAL
237
+ // Radial fan: remap the ribbon to polar around the LOCAL origin so its LENGTH fans into a plume.
238
+ // uv.x (folded WIDTH) → fan ANGLE across uRadialArc; uv.y (LENGTH) → RADIUS, so a constant-uv.x
239
+ // combed fiber becomes a constant-angle radial spoke. mix(pos, fanned, 0) is identity → off is
240
+ // byte-identical. (Placement is the wave's position transform — the fan has no separate pivot.)
241
+ {
242
+ float rAng = radians(uRadialCenter) + (clamp(uv.x, 0.0, 1.0) - 0.5) * radians(uRadialArc);
243
+ float rRho = uRadialRadius + uv.y * 400.0 * uRadialSpread; // 400 = native ribbon length
244
+ vec3 rEr = vec3(cos(rAng), sin(rAng), 0.0); // radial dir, in local X–Y (screen plane)
245
+ vec3 rEt = vec3(-sin(rAng), cos(rAng), 0.0); // tangential
246
+ vec3 fanned = rEr * rRho
247
+ + rEt * (pos.z - ${(-8).toFixed(1)}) * 0.5
248
+ + vec3(0.0, 0.0, pos.y);
249
+ pos = mix(pos, fanned, clamp(uRadialAmount, 0.0, 1.0));
250
+ }
251
+ #endif
252
+
253
+ WaveShape s;
254
+ s.pos = pos;
255
+ s.rotA = rotA;
256
+ s.rotB = rotB;
257
+ s.rotC = rotC;
258
+ return s;
259
+ }
260
+ `;
160
261
  const vertexShader = `
161
262
  ${simplex2d}
162
263
 
@@ -175,6 +276,16 @@ uniform float uHelixRoll; // cross-section roll, as a fraction of the turns (1
175
276
  uniform float uHelixPhase; // degrees
176
277
  #endif
177
278
 
279
+ // Radial fan (optional). Behind RADIAL so a wave without one compiles the exact same program (same
280
+ // byte-identity contract as HELIX / POINTER_FX above).
281
+ #ifdef RADIAL
282
+ uniform float uRadialAmount; // 0..1 blend (0 = identity)
283
+ uniform float uRadialArc; // fan spread, degrees
284
+ uniform float uRadialSpread; // length → radius scale
285
+ uniform float uRadialRadius; // source / inner radius
286
+ uniform float uRadialCenter; // base angle, degrees
287
+ #endif
288
+
178
289
  varying vec2 vUv;
179
290
  varying vec3 vWorldPos;
180
291
  varying vec3 vViewDir;
@@ -208,27 +319,13 @@ const float RIPPLE_MAX_R = 1.2; // reach where the crest has fully left th
208
319
  #endif
209
320
  #endif
210
321
 
211
- // expStep: a falloff from 1 (at x=0) toward 0, sharpness set by n. The
212
- // max() guards pow(0, n) (= Infinity → NaN) so negative n is safe — negative n
213
- // just concentrates the twist toward the OTHER end instead.
214
- float expStep(float x, float n){ return exp2(-exp2(n) * pow(max(x, 1.0e-3), n)); }
215
-
216
- // rotationMatrix (mat4), used row-vector style: pos = (vec4(pos,1) * R).xyz
217
- mat4 rotationMatrix(vec3 axis, float angle){
218
- axis = normalize(axis);
219
- float s = sin(angle), c = cos(angle), oc = 1.0 - c;
220
- return mat4(
221
- oc*axis.x*axis.x + c, oc*axis.x*axis.y - axis.z*s, oc*axis.z*axis.x + axis.y*s, 0.0,
222
- oc*axis.x*axis.y + axis.z*s, oc*axis.y*axis.y + c, oc*axis.y*axis.z - axis.x*s, 0.0,
223
- oc*axis.z*axis.x - axis.y*s, oc*axis.y*axis.z + axis.x*s, oc*axis.z*axis.z + c, 0.0,
224
- 0.0, 0.0, 0.0, 1.0
225
- );
226
- }
322
+ ${waveShapeChunk}
227
323
 
228
324
  void main(){
229
325
  vUv = uv;
230
326
  #ifndef LOOP_MOTION
231
327
  float t = uTime * uSpeed + uSeed;
328
+ vec2 loopOff = vec2(0.0); // unused under linear time; kept so waveShape's signature is uniform
232
329
  #endif
233
330
 
234
331
  #ifdef LOOP_MOTION
@@ -241,73 +338,14 @@ void main(){
241
338
  float loopTheta = uTime * (6.28318530718 / uLoopSeconds) + uSeed;
242
339
  float loopR = uSpeed * uLoopSeconds * 0.159154943092; // = uSpeed·uLoopSeconds / (2π)
243
340
  vec2 loopOff = loopR * vec2(cos(loopTheta), sin(loopTheta));
341
+ float t = 0.0; // unused under loop time
244
342
  #endif
245
343
 
246
- // The base geometry is already a baked hairpin fold. On top of it we deform the
247
- // vertices: a displacement lifts Y by simplex noise of the (x,z) position, then
248
- // three axis-rotations twist the strip.
249
- vec3 pos = position;
250
- #ifdef LOOP_MOTION
251
- pos.y += uDispAmount * simplexNoise(vec2(pos.x * uDispFreqX, pos.z * uDispFreqZ) + loopOff);
252
- #else
253
- pos.y += uDispAmount * simplexNoise(vec2(pos.x * uDispFreqX + t, pos.z * uDispFreqZ + t));
254
- #endif
255
- #ifdef DETAIL_OCTAVE
256
- // A second, finer octave riding on the broad swell — fine ripples on top of the big shape, a
257
- // shape vocabulary single-octave displacement can't reach. Shares the loop orbit so it stays
258
- // periodic when looping.
259
- #ifdef LOOP_MOTION
260
- pos.y += uDetailAmount * simplexNoise(vec2(pos.x * uDetailFreq, pos.z * uDetailFreq) + loopOff);
261
- #else
262
- pos.y += uDetailAmount * simplexNoise(vec2(pos.x * uDetailFreq + t, pos.z * uDetailFreq + t));
263
- #endif
264
- #endif
265
-
266
- #ifdef HELIX
267
- // Helix — the one shape the three twists below cannot reach. Their angle is freq * expStep(uv),
268
- // a MONOTONE falloff, so it can only ramp once; this one is periodic in uv.y (the length), so
269
- // uHelixTurns full turns land evenly from end to end. Runs AFTER the displacement so the noise
270
- // above still samples the undeformed pos.x/pos.z (byte-identical sampling), and BEFORE the twist
271
- // so the two compose.
272
- // roll rolls the ribbon's own cross-section about the axis in step with the sweep, swinging
273
- // its two long edges onto opposite sides — one wave becomes a ladder whose edges are
274
- // both strands (pair with the wireframe theme's rungs for the rungs between them).
275
- // radius carries the whole ribbon around the axis instead, orientation intact — a narrow ribbon
276
- // then reads as ONE strand, and a second wave at phase+180 is the other.
277
- float hAng = 6.28318530718 * uHelixTurns * uv.y + radians(uHelixPhase);
278
- // Roll about the ribbon's width centre, not the origin — see RIBBON_Z_CENTER in WaveGeometry.
279
- float rollA = hAng * uHelixRoll;
280
- float rollC = cos(rollA), rollS = sin(rollA);
281
- vec2 rel = vec2(pos.y, pos.z - ${(-8).toFixed(1)});
282
- pos.y = rel.x * rollC - rel.y * rollS;
283
- pos.z = ${(-8).toFixed(1)} + rel.x * rollS + rel.y * rollC;
284
- pos.y += uHelixRadius * cos(hAng);
285
- pos.z += uHelixRadius * sin(hAng);
286
- #endif
287
-
288
- // The X-twist frequency feeding rotB. Two modes: by default uTwFreqX is used
289
- // directly; the variant (used by the Wave 4 preset) modulates it with
290
- // simplex noise indexed along the ribbon (uv.y) so the twist breathes over time.
291
- // We gate the wobble with a #define so the compiled program is unchanged when off.
292
- float twistXFreq = uTwFreqX;
293
- #ifdef TWIST_MOTION
294
- #ifdef LOOP_MOTION
295
- float twistXNoise = simplexNoise(vec2(vUv.y * 2.0, 0.0) + loopOff);
296
- #else
297
- float twistXNoise = simplexNoise(vec2(vUv.y * 2.0, t));
298
- #endif
299
- twistXFreq = uTwFreqX - twistXNoise * 0.1;
300
- #endif
301
-
302
- // Three-axis twist: expStep falloff sets how
303
- // sharply each rotation concentrates toward an edge. rotA keys off uv.x, rotB/rotC
304
- // off uv.y; axes (0.5,0,0.5) and (0,0.5,0.5) are normalised inside rotationMatrix.
305
- mat4 rotA = rotationMatrix(vec3(0.5, 0.0, 0.5), uTwFreqY * expStep(uv.x, uTwPowY));
306
- mat4 rotB = rotationMatrix(vec3(0.0, 0.5, 0.5), twistXFreq * expStep(uv.y, uTwPowX));
307
- mat4 rotC = rotationMatrix(vec3(0.5, 0.0, 0.5), uTwFreqZ * expStep(uv.y, uTwPowZ));
308
- pos = (vec4(pos, 1.0) * rotA).xyz;
309
- pos = (vec4(pos, 1.0) * rotB).xyz;
310
- pos = (vec4(pos, 1.0) * rotC).xyz;
344
+ // Deform the baked hairpin via the shared waveShape chunk (displacement + helix + twist + radial),
345
+ // which also drives the particle shed emitter. It returns the deformed local pos + the twist
346
+ // matrices the pointer field reads below.
347
+ WaveShape ws = waveShape(position, uv, t, loopOff);
348
+ vec3 pos = ws.pos;
311
349
 
312
350
  #ifdef POINTER_FX
313
351
  // Pointer field: displace along the wave's own (post-twist) up-axis, weighted by a screen-space
@@ -330,7 +368,7 @@ void main(){
330
368
  // point-projection and no perspective divide. (A true per-pixel uv would need GPU picking — the
331
369
  // visible surface is shader-displaced, so a CPU raycast of the base geometry misses.)
332
370
  if (uShapeFlow > 0.0) {
333
- vec3 tangentLocal = (((vec4(1.0, 0.0, 0.0, 0.0) * rotA) * rotB) * rotC).xyz;
371
+ vec3 tangentLocal = (((vec4(1.0, 0.0, 0.0, 0.0) * ws.rotA) * ws.rotB) * ws.rotC).xyz;
334
372
  vec2 tang = (mvp * vec4(tangentLocal, 0.0)).xy * vec2(uPointerAspect, 1.0);
335
373
  float tl = length(tang);
336
374
  if (tl > 1.0e-6) {
@@ -343,7 +381,7 @@ void main(){
343
381
  vPointerFall = fall * uPointerActive;
344
382
  // Displacement axis = local +Y carried through the SAME three twist rotations as pos (row-vector
345
383
  // convention). Rotations are linear, so post-twist axis displacement equals pre-twist Y displacement.
346
- vec3 dispAxis = (((vec4(0.0, 1.0, 0.0, 0.0) * rotA) * rotB) * rotC).xyz;
384
+ vec3 dispAxis = (((vec4(0.0, 1.0, 0.0, 0.0) * ws.rotA) * ws.rotB) * ws.rotC).xyz;
347
385
  // Agitation: a fast churn octave near the cursor (additive — never rewrites base noise t, which
348
386
  // would force restructuring the shared path). Loop-safe under both time variants.
349
387
  #ifdef LOOP_MOTION
@@ -434,7 +472,7 @@ uniform vec3 uDepthTintColor;
434
472
  varying vec4 vClipPosition; // clip-space depth (written by the vertex shader for both programs)
435
473
  #endif
436
474
  #ifdef EDGE_FEATHER
437
- uniform float uEdgeFeather; // ribbon long-edge softness (only when it differs from the 0.1 default)
475
+ uniform float uEdgeFeather; // softness of the ribbon's two ENDS (only when it differs from 0.1)
438
476
  #endif
439
477
  #ifdef POINTER_FX
440
478
  uniform float uPointerThin; // 0..1 local translucency near the cursor
@@ -474,8 +512,10 @@ vec3 surfaceStreaks(vec2 uv, vec3 color, float crease){
474
512
  colorAtten = mix(colorAtten, prm.w, blend);
475
513
  paraPow = mix(paraPow, uNoiseBandParaPow[i], blend);
476
514
  }
477
- // The high frequency runs along uv.x (the ribbon's length) so the streaks read as
478
- // fine lengthwise fibers; end-weighted by 1 - parabola(uv.x).
515
+ // The high frequency runs along uv.x (the folded WIDTH see WaveGeometry's UV AXES note),
516
+ // packing many thin stripes across the cross-section while uv.y is barely scaled, so each
517
+ // one stretches out into a fine LENGTHWISE fiber. 1 - parabola(uv.x) then weights them
518
+ // toward the two long edges and away from the width centreline.
479
519
  float p = 1.0 - parabola(uv.x, paraPow);
480
520
  float n0 = simplexNoise(vec2(uv.x * 0.1, uv.y * 0.5));
481
521
  float n1 = simplexNoise(vec2(uv.x * (freq + freq * 0.5 * n0), uv.y * 4.0 * n0));
@@ -564,7 +604,8 @@ void main(){
564
604
 
565
605
  if (uTexture > 0.001) col *= 1.0 + (grainHash(vUv * 850.0) - 0.5) * uTexture * 0.25;
566
606
 
567
- // Soft long edges + optional viewport-edge fade. The edge softness is the hardcoded 0.1 by
607
+ // Soft ribbon ENDS (it fades on vUv.y, the length) + optional viewport-edge fade. The edge
608
+ // softness is the hardcoded 0.1 by
568
609
  // default (literal branch → byte-identical); EDGE_FEATHER swaps in the uEdgeFeather knob only
569
610
  // when it differs, so razor-crisp or vapor-soft edges are both reachable.
570
611
  #ifdef EDGE_FEATHER
@@ -638,7 +679,7 @@ void main(){
638
679
  color *= 1.0 + uPointerLighten * vPointerFall;
639
680
  #endif
640
681
 
641
- // Carve into fine vertical lines; thickness from the screen-space uv derivative.
682
+ // Carve into fine lengthwise strands; thickness from the screen-space uv derivative.
642
683
  vec2 dy = dFdy(vUv);
643
684
  float lineThickness = uLineThickness * pow(abs(dy.x * uMaxWidth), uLineDerivativePower);
644
685
  #ifdef POINTER_FX
@@ -897,7 +938,124 @@ void main(){
897
938
  gl_FragColor = vec4(mix(src.rgb, outc, clamp(uHalftoneCmyk, 0.0, 1.0)), src.a);
898
939
  }
899
940
  `;
941
+ const particleVertexShader = `
942
+ attribute float aSeed;
943
+ attribute vec4 aRnd;
944
+ attribute vec2 aUv; // where this particle spawns on the ribbon (x = flank, y = along length; edge-biased at build)
945
+
946
+ uniform float uTime, uLoopSeconds, uLife, uSize, uSizeJitter, uTwinkle, uPixelRatio;
947
+ uniform float uPartSpeed;
948
+ uniform vec3 uColor, uColor2, uCenter, uRight, uUp;
949
+ uniform float uDrift, uRise, uSwirl, uWander;
950
+
951
+ // The owning wave's shape, mirrored in configure() so the dust rides the SAME deform as the ribbon.
952
+ // The HELIX/RADIAL uniform blocks are declared only when the matching #define is set.
953
+ ${simplex2d}
954
+ uniform float uDispFreqX, uDispFreqZ, uDispAmount;
955
+ uniform float uDetailFreq, uDetailAmount;
956
+ uniform float uTwFreqX, uTwFreqY, uTwFreqZ, uTwPowX, uTwPowY, uTwPowZ;
957
+ #ifdef HELIX
958
+ uniform float uHelixTurns, uHelixRadius, uHelixRoll, uHelixPhase;
959
+ #endif
960
+ #ifdef RADIAL
961
+ uniform float uRadialAmount, uRadialArc, uRadialSpread, uRadialRadius, uRadialCenter;
962
+ #endif
963
+ uniform mat4 uShedModel; // the wave's matrixWorld (deformed LOCAL → world)
964
+ uniform float uShedSpeed, uShedSeed;
965
+ ${waveShapeChunk}
966
+
967
+ varying float vAlpha;
968
+ varying vec3 vColor;
969
+ varying vec2 vDir; // screen-space motion direction (for the streak sprite)
970
+
971
+ const float TAU = 6.28318530718;
972
+
973
+ void main(){
974
+ // Deterministic life: age 0..1 from uTime + a per-particle seed. Advances once per loop period when
975
+ // looping (so the whole field repeats seamlessly), else once per uLife seconds. uPartSpeed scales the
976
+ // cadence (motion speed); under a loop it snaps to a whole number of cycles so the seam stays seamless.
977
+ float cyc = max(1.0, floor(uPartSpeed + 0.5));
978
+ float rate = (uLoopSeconds > 0.0) ? (uTime / uLoopSeconds * cyc) : (uTime * uPartSpeed / max(uLife, 0.001));
979
+ float age = fract(rate + aSeed);
980
+ float fade = sin(3.14159265 * age); // 0 at birth/death, 1 mid-life
981
+
982
+ // Spawn on the owning wave's DEFORMED surface / edge at aUv (via the shared waveShape), then peel
983
+ // outward from the wave centre as the particle ages — silk dissolving into glitter.
984
+ float ts = uTime * uShedSpeed + uShedSeed;
985
+ vec2 loopOff = vec2(0.0);
986
+ #ifdef LOOP_MOTION
987
+ float loopTheta = uTime * (TAU / uLoopSeconds) + uShedSeed;
988
+ float loopR = uShedSpeed * uLoopSeconds * 0.159154943092;
989
+ loopOff = loopR * vec2(cos(loopTheta), sin(loopTheta));
990
+ ts = 0.0;
991
+ #endif
992
+ // Approximate the base hairpin point for this uv (length from uv.y; width centre), then deform it
993
+ // exactly as the wave does. Good enough for dust — the fan / displacement dominate.
994
+ vec3 base = vec3((aUv.y - 0.5) * 400.0, 0.0, ${(-8).toFixed(1)});
995
+ vec3 origin = (uShedModel * vec4(waveShape(base, aUv, ts, loopOff).pos, 1.0)).xyz;
996
+ vec3 outward = normalize(origin - uCenter + vec3(1e-4));
997
+ vec3 p = origin + outward * age * uDrift + (aRnd.xyz - 0.5) * age * uDrift * 0.35;
998
+
999
+ // Motion styles, each 0 = off, all riding age so they stay loop-safe (the age wrap is hidden by
1000
+ // fade→0 at birth/death). rise = screen-vertical buoyancy (embers up / snow down); swirl = orbit
1001
+ // around the wave centre in the screen plane; wander = curl-noise turbulence (fireflies / motes).
1002
+ p += uUp * age * uRise;
1003
+ if (uSwirl != 0.0) {
1004
+ vec3 nrm = cross(uRight, uUp);
1005
+ vec3 rel = p - uCenter;
1006
+ float rx = dot(rel, uRight), ry = dot(rel, uUp), rz = dot(rel, nrm);
1007
+ float a = age * uSwirl * TAU;
1008
+ float ca = cos(a), sa = sin(a);
1009
+ p = uCenter + uRight * (rx * ca - ry * sa) + uUp * (rx * sa + ry * ca) + nrm * rz;
1010
+ }
1011
+ if (uWander != 0.0) {
1012
+ vec2 wan = vec2(simplexNoise(vec2(aSeed * 17.0, age * 3.0)),
1013
+ simplexNoise(vec2(age * 3.0, aSeed * 23.0)));
1014
+ p += (uRight * wan.x + uUp * wan.y) * uWander;
1015
+ }
1016
+
1017
+ float tw = 0.5 + 0.5 * sin((age * 9.0 + aSeed) * TAU); // loop-safe flicker (rides age)
1018
+ vAlpha = fade * mix(1.0, tw, clamp(uTwinkle, 0.0, 1.0));
1019
+ vColor = mix(uColor, uColor2, aRnd.w); // two-tone dust: per-particle blend of the two colours
1020
+ vDir = normalize(vec2(dot(outward, uRight), dot(outward, uUp)) + vec2(1e-4)); // outward, in screen space
1021
+ gl_Position = projectionMatrix * viewMatrix * vec4(p, 1.0);
1022
+ // Orthographic camera → point size is constant in device pixels (no perspective depth divide).
1023
+ float jitter = 1.0 + uSizeJitter * (aSeed - 0.5) * 2.0;
1024
+ gl_PointSize = max(uSize * uPixelRatio * jitter * fade, 0.0);
1025
+ }
1026
+ `;
1027
+ const particleFragmentShader = `
1028
+ precision highp float;
1029
+ uniform float uShape; // 0 glitter · 1 soft · 2 ring · 3 star · 4 streak
1030
+ varying float vAlpha;
1031
+ varying vec3 vColor;
1032
+ varying vec2 vDir;
1033
+ void main(){
1034
+ vec2 pc = gl_PointCoord - 0.5;
1035
+ float d = length(pc);
1036
+ int s = int(uShape + 0.5);
1037
+ float a;
1038
+ if (s == 1) { // soft: a diffuse gaussian blob (motes / pollen)
1039
+ a = exp(-d * d * 7.0);
1040
+ } else if (s == 2) { // ring: a hollow band (bubbles)
1041
+ a = smoothstep(0.09, 0.0, abs(d - 0.34));
1042
+ } else if (s == 3) { // star: a 4-point sparkle
1043
+ float ang = atan(pc.y, pc.x);
1044
+ float spike = pow(abs(cos(ang * 2.0)), 6.0);
1045
+ a = smoothstep(1.0, 0.0, d / (0.14 + 0.5 * spike));
1046
+ } else if (s == 4) { // streak: an elongated comet along the motion direction
1047
+ float along = dot(pc, vDir);
1048
+ float perp = dot(pc, vec2(-vDir.y, vDir.x));
1049
+ a = smoothstep(0.5, 0.0, length(vec2(along * 0.42, perp * 2.2)));
1050
+ } else { // glitter (0): the soft round additive disc
1051
+ a = smoothstep(0.5, 0.0, d);
1052
+ }
1053
+ a *= vAlpha;
1054
+ if (a <= 0.0) discard;
1055
+ gl_FragColor = vec4(vColor, a); // AdditiveBlending (src = SrcAlpha) → adds vColor·a
1056
+ }
1057
+ `;
900
1058
  //#endregion
901
- export { ditherFragmentShader, fragmentShader, halftoneCmykFragmentShader, halftoneFragmentShader, heatmapFragmentShader, innerLightFragmentShader, lineFragmentShader, paperTextureFragmentShader, postFragmentShader, postVertexShader, vertexShader };
1059
+ export { ditherFragmentShader, fragmentShader, halftoneCmykFragmentShader, halftoneFragmentShader, heatmapFragmentShader, innerLightFragmentShader, lineFragmentShader, paperTextureFragmentShader, particleFragmentShader, particleVertexShader, postFragmentShader, postVertexShader, vertexShader };
902
1060
 
903
1061
  //# sourceMappingURL=shaders.js.map