@3driseai/3drise-viewer 0.1.17 → 0.1.19

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.
Files changed (52) hide show
  1. package/dist/components/AnimatedObject.d.ts.map +1 -1
  2. package/dist/components/AnimatedObject.js +3 -2
  3. package/dist/components/Galaxy.d.ts.map +1 -1
  4. package/dist/components/Galaxy.js +2 -0
  5. package/dist/components/ObjectNode.d.ts.map +1 -1
  6. package/dist/components/ObjectNode.js +17 -6
  7. package/dist/components/ParticlesGenerator.d.ts +10 -0
  8. package/dist/components/ParticlesGenerator.d.ts.map +1 -0
  9. package/dist/components/ParticlesGenerator.js +23 -0
  10. package/dist/components/effects/ShockwaveEffect.js +2 -2
  11. package/dist/components/index.d.ts +3 -0
  12. package/dist/components/index.d.ts.map +1 -1
  13. package/dist/components/index.js +2 -0
  14. package/dist/components/particles/BioluminescentWakeParticles.d.ts +15 -0
  15. package/dist/components/particles/BioluminescentWakeParticles.d.ts.map +1 -0
  16. package/dist/components/particles/BioluminescentWakeParticles.js +1145 -0
  17. package/dist/components/particles/BrownianDustParticles.d.ts +15 -0
  18. package/dist/components/particles/BrownianDustParticles.d.ts.map +1 -0
  19. package/dist/components/particles/BrownianDustParticles.js +779 -0
  20. package/dist/components/particles/ElectronOrbitalsParticles.d.ts +15 -0
  21. package/dist/components/particles/ElectronOrbitalsParticles.d.ts.map +1 -0
  22. package/dist/components/particles/ElectronOrbitalsParticles.js +966 -0
  23. package/dist/components/particles/GrinderSparksParticles.d.ts +15 -0
  24. package/dist/components/particles/GrinderSparksParticles.d.ts.map +1 -0
  25. package/dist/components/particles/GrinderSparksParticles.js +768 -0
  26. package/dist/components/particles/TidalStreamParticles.d.ts +15 -0
  27. package/dist/components/particles/TidalStreamParticles.d.ts.map +1 -0
  28. package/dist/components/particles/TidalStreamParticles.js +810 -0
  29. package/dist/components/particles/index.d.ts +11 -0
  30. package/dist/components/particles/index.d.ts.map +1 -0
  31. package/dist/components/particles/index.js +6 -0
  32. package/dist/constants.d.ts.map +1 -1
  33. package/dist/constants.js +2 -1
  34. package/dist/hooks/index.d.ts +2 -0
  35. package/dist/hooks/index.d.ts.map +1 -1
  36. package/dist/hooks/index.js +1 -0
  37. package/dist/hooks/useConfigLive.d.ts +54 -0
  38. package/dist/hooks/useConfigLive.d.ts.map +1 -0
  39. package/dist/hooks/useConfigLive.js +100 -0
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +1 -0
  43. package/dist/utils/configAnimations.d.ts +43 -0
  44. package/dist/utils/configAnimations.d.ts.map +1 -0
  45. package/dist/utils/configAnimations.js +73 -0
  46. package/dist/utils/particleTrails.d.ts +70 -0
  47. package/dist/utils/particleTrails.d.ts.map +1 -0
  48. package/dist/utils/particleTrails.js +378 -0
  49. package/dist/utils/pointerField.d.ts +28 -0
  50. package/dist/utils/pointerField.d.ts.map +1 -0
  51. package/dist/utils/pointerField.js +103 -0
  52. package/package.json +2 -2
@@ -0,0 +1,1145 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
3
+ import * as THREE from 'three';
4
+ import { useFrame } from '@react-three/fiber';
5
+ import { GENERATIVE_PARTICLES_ANIMATABLE } from '@3driseai/3drise-core';
6
+ import { applyConfigKeyframeAnimations } from '../../utils/configAnimations.js';
7
+ import { createPointerUniforms, stepPointerField, usePointerField } from '../../utils/pointerField.js';
8
+ import { createTrailUniforms, applyTrailConfig, createCpuTrail } from '../../utils/particleTrails.js';
9
+ import { useObjectReady } from '../../hooks/useObjectReady.js';
10
+ import { useConfigLiveHandle } from '../../hooks/useConfigLive.js';
11
+ /**
12
+ * Sea Sparkle — Bioluminescent plankton that flash when the water shears: swell orbits, excitable cells that tire and recover, moon glitter; the cursor is a hand, falling pebbles or a hunting fish. Use for night sea and fantasy scenes.
13
+ */
14
+ // Sea Sparkle — bioluminescent plankton in a moonlit sea.
15
+ // Movement: plankton are passive tracers. Linear deep-water swell (two Gerstner waves, deep-water
16
+ // dispersion w = sqrt(g k)) moves each cell on its closed orbital ellipse, amplitude
17
+ // A e^{k y}; the cloud drifts with a slow current. Noctiluca are buoyant, so cells sit in an
18
+ // exponential layer under the surface.
19
+ // Colour: mechanically stimulated luminescence. Every cell is an excitable unit: when the local
20
+ // shear rate of the water exceeds its threshold (lognormal across cells) it flashes
21
+ // (luciferin blue, ~0.2 s decay), spends part of its reserve and needs `recovery` seconds
22
+ // to recharge — so wakes glow bright once and fade as the cells tire. Light leaving deeper
23
+ // cells is absorbed (exp(-c depth)), shifted toward `deepColor` and widened by forward
24
+ // scattering. The surface is a moon-glitter layer: Cox–Munk style micro-facets on the
25
+ // same swell, glinting where a facet's normal bisects moon and eye.
26
+ // Mouse (pointerMode; the cursor lives on the water plane):
27
+ // hand — a swimmer's hand: moving-sphere potential flow (dipole) + wake; shear ~ 3 U R^3 / r^4
28
+ // lights a sheath around the stroke and a glowing wake behind it. A still hand is dark.
29
+ // pebble — pebbles dropped along the cursor path: a splash burst, a sinking pebble trailing a wake,
30
+ // and capillary–gravity ripple packets whose crests light expanding rings.
31
+ // fish — a fish that hunts the cursor (cruises in a circle when the cursor rests): the shear in
32
+ // its boundary layer outlines the body in light and its tail sheds a reverse Karman street
33
+ // of vortices that stir a glowing, swirling wake.
34
+ // Trails (trailMode): streak = long exposure, comet = glow threads, echo = afterglow — only
35
+ // glowing cells leave light (TRAIL_K = emission).
36
+ //
37
+ // Generated from the approved lab module (particle lab, batch 2). Sim units = object-local / size.
38
+ /** mulberry32 — seeded PRNG, returns a function giving floats in [0,1). */
39
+ function mulberry32(seed) {
40
+ let a = seed >>> 0;
41
+ return function () {
42
+ a = (a + 0x6d2b79f5) >>> 0;
43
+ let t = a;
44
+ t = Math.imul(t ^ (t >>> 15), t | 1);
45
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
46
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
47
+ };
48
+ }
49
+ /** Standard normal from a uniform PRNG (Box–Muller, one value per call). */
50
+ function gauss(rng) {
51
+ const u = Math.max(rng(), 1e-9);
52
+ const v = rng();
53
+ return Math.sqrt(-2 * Math.log(u)) * Math.cos(6.283185307 * v);
54
+ }
55
+ /** Viewport-height hook so point sizes are pixel-correct at any canvas size. */
56
+ function attachViewH(obj, u) {
57
+ const scratch = new THREE.Vector2();
58
+ obj.onBeforeRender = (renderer) => {
59
+ renderer.getDrawingBufferSize(scratch);
60
+ u.uViewH.value = scratch.y;
61
+ };
62
+ }
63
+ function colorUniform(hex) {
64
+ return { value: new THREE.Color(hex) };
65
+ }
66
+ /** Quality levels scale counts, raymarch samples and trail caps. Structural. */
67
+ const QUALITY_SCALE = { low: 0.5, medium: 1, high: 1.6 };
68
+ function qualityScale(q) { return QUALITY_SCALE[q] || 1; }
69
+ /** Mix the object's own base seed with the `seed` knob into a mulberry32 seed. */
70
+ function seedMix(base, seed) {
71
+ let h = (base ^ Math.imul((Math.round(seed) | 0) + 0x9e37, 0x85ebca6b)) >>> 0;
72
+ h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35) >>> 0;
73
+ return (h ^ (h >>> 16)) >>> 0;
74
+ }
75
+ /** Fixed-step clock: the same motion at 30, 60 or 144 Hz. `run(d, step)` advances d sim-seconds
76
+ * in steps of exactly H (at most maxSteps per call; a longer backlog is dropped, not caught up). */
77
+ function createFixedStep(H, maxSteps) {
78
+ let acc = 0;
79
+ return {
80
+ H,
81
+ run(d, step) {
82
+ acc += Math.max(d, 0);
83
+ let k = 0;
84
+ while (acc >= H && k < maxSteps) {
85
+ step(H);
86
+ acc -= H;
87
+ k++;
88
+ }
89
+ if (k === maxSteps)
90
+ acc = Math.min(acc, H);
91
+ return acc / H; // leftover fraction, for render interpolation
92
+ },
93
+ reset() { acc = 0; },
94
+ };
95
+ }
96
+ /** Selection bounds: an invisible sphere the editor can pick, frame and size (never rendered,
97
+ * still raycastable and counted by Box3.setFromObject). */
98
+ function createPickProxy(center, radius) {
99
+ const geo = new THREE.SphereGeometry(1, 16, 10);
100
+ const mat = new THREE.MeshBasicMaterial({ visible: false });
101
+ const mesh = new THREE.Mesh(geo, mat);
102
+ mesh.name = 'pickProxy';
103
+ mesh.position.set(center[0], center[1], center[2]);
104
+ mesh.scale.setScalar(radius);
105
+ return { mesh, dispose() { geo.dispose(); mat.dispose(); } };
106
+ }
107
+ const _frustum = new THREE.Frustum();
108
+ const _pm = new THREE.Matrix4();
109
+ const _sphere = new THREE.Sphere();
110
+ /** Off-screen test: false when the object's bounding sphere is outside the camera frustum. */
111
+ function inView(object, camera, center, radius, size) {
112
+ if (!camera || !camera.projectionMatrix)
113
+ return true;
114
+ object.updateWorldMatrix(true, false);
115
+ _sphere.center.set(center[0], center[1], center[2]).multiplyScalar(size).applyMatrix4(object.matrixWorld);
116
+ _sphere.radius = radius * size * object.matrixWorld.getMaxScaleOnAxis();
117
+ _pm.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
118
+ _frustum.setFromProjectionMatrix(_pm);
119
+ return _frustum.intersectsSphere(_sphere);
120
+ }
121
+ const POINTER_MODES = ['hand', 'pebble', 'fish'];
122
+ const HX = 1.0; // half width of the sea patch (sim units)
123
+ const D = 0.3; // depth of the plankton layer
124
+ const LAYER = 0.06; // e-folding depth of the buoyant cell layer
125
+ const G = 0.981; // sim gravity: 1 sim unit ~ 10 m of sea, so the swell is slow and calm
126
+ const H_STEP = 1 / 30;
127
+ const TH0 = 1.5; // median shear threshold (1/s) at sensitivity 1
128
+ const MAX_PEBBLES = 6;
129
+ const MAX_VORT = 16;
130
+ const RIP_C = 0.3; // ripple packet speed
131
+ const RIP_L = 0.05; // ripple crest spacing
132
+ const ANCHOR = [0, -0.035, 0];
133
+ // seconds of water motion per trail second, per trail type (cells drift a few cm/s; a flash lasts 0.2 s)
134
+ const TRAIL_SCALE = { streak: 3.5, comet: 2.5, echo: 1 };
135
+ const TRAIL_GAIN = { streak: 0.55, comet: 1, echo: 3.5 }; // even out the three looks
136
+ const BOUNDS = { center: [0, -0.1, 0], radius: 1.5 };
137
+ function createUniforms() {
138
+ const pebbles = [];
139
+ for (let i = 0; i < MAX_PEBBLES; i++)
140
+ pebbles.push(new THREE.Vector4(0, 0, 99, 0));
141
+ return {
142
+ uTime: { value: 0 },
143
+ uSize: { value: 1 },
144
+ uViewH: { value: 800 },
145
+ uIntensity: { value: 1 },
146
+ uSpeed: { value: 1 },
147
+ uOpacity: { value: 0.9 },
148
+ uColor: colorUniform('#ffffff'),
149
+ uParticleCount: { value: 0 },
150
+ uSwellHeight: { value: 1 },
151
+ uCurrent: { value: 0.3 },
152
+ uSensitivity: { value: 1 },
153
+ uRecovery: { value: 6 },
154
+ uGlowTime: { value: 1 },
155
+ uWaterClarity: { value: 1 },
156
+ uDeepColor: colorUniform('#ffffff'),
157
+ uMoonlight: { value: 0.6 },
158
+ uPointerMode: { value: 0 },
159
+ uPointerStrength: { value: 1 },
160
+ uPointerRadius: { value: 1 },
161
+ uPointerColor: colorUniform('#ffffff'),
162
+ uWave0: { value: new THREE.Vector4() }, // dir.x, dir.z, k, A
163
+ uWave1: { value: new THREE.Vector4() },
164
+ uWavePh: { value: new THREE.Vector2() }, // current phase of each wave (w t - phi)
165
+ uCurOff: { value: new THREE.Vector2() }, // current offset (x, z)
166
+ uPebbles: { value: pebbles }, // x, z, age, strength
167
+ uCamLocal: { value: new THREE.Vector3(0, 1, 2) },
168
+ uTrailGain: { value: 1 },
169
+ ...createPointerUniforms(),
170
+ ...createTrailUniforms(),
171
+ };
172
+ }
173
+ function applyConfig(u, c, size) {
174
+ u.uSize.value = size;
175
+ u.uIntensity.value = c.intensity;
176
+ u.uSpeed.value = c.speed;
177
+ u.uOpacity.value = c.opacity;
178
+ u.uColor.value.set(c.color);
179
+ u.uParticleCount.value = c.particleCount;
180
+ u.uSwellHeight.value = c.swellHeight;
181
+ u.uCurrent.value = c.current;
182
+ u.uSensitivity.value = c.sensitivity;
183
+ u.uRecovery.value = c.recovery;
184
+ u.uGlowTime.value = c.glowTime;
185
+ u.uWaterClarity.value = c.waterClarity;
186
+ u.uDeepColor.value.set(c.deepColor);
187
+ u.uMoonlight.value = c.moonlight;
188
+ u.uPointerMode.value = POINTER_MODES.indexOf(c.pointerMode); // -1 = 'none' (off)
189
+ u.uPointerStrength.value = c.pointerStrength;
190
+ u.uPointerRadius.value = c.pointerRadius;
191
+ u.uPointerColor.value.set(c.pointerColor);
192
+ applyTrailConfig(u, c);
193
+ }
194
+ const BASE_U = 'uniform float uSize, uViewH, uIntensity, uOpacity;';
195
+ // Shared water optics: absorption, depth tint, edge fade.
196
+ const WATER_GLSL = /* glsl */ `
197
+ uniform vec3 uColor, uDeepColor;
198
+ uniform float uWaterClarity;
199
+ float edgeFade(vec3 p) {
200
+ vec2 e = ${HX.toFixed(2)} - abs(p.xz);
201
+ return smoothstep(0.0, 0.18, min(e.x, e.y));
202
+ }
203
+ float absorb(float depth) { return exp(-depth * 7.0 / max(uWaterClarity, 0.05)); }
204
+ vec3 depthTint(float depth) { return mix(uColor, uDeepColor, 1.0 - exp(-depth * 6.0 / max(uWaterClarity, 0.05))); }
205
+ `;
206
+ const cellVertex = /* glsl */ `
207
+ ${BASE_U}
208
+ ${WATER_GLSL}
209
+ uniform float uMoonlight;
210
+ attribute float aL; // emitted light (0..~1.4)
211
+ attribute float aSeed;
212
+ varying vec3 vCol;
213
+ varying float vA;
214
+ void main() {
215
+ float depth = max(-position.y, 0.0);
216
+ float L = aL * edgeFade(position);
217
+ float att = absorb(depth);
218
+ // forward scattering in the water widens deeper flashes into soft discs (energy conserved)
219
+ float blur = 1.0 + depth * 10.0;
220
+ vec4 mv = modelViewMatrix * vec4(position * uSize, 1.0);
221
+ float Lc = min(L, 1.4);
222
+ float px = uSize * uViewH * (0.0035 + 0.013 * sqrt(Lc)) * (0.8 + 0.4 * aSeed) * blur / -mv.z;
223
+ vec3 glow = depthTint(depth) * L * att * 4.0;
224
+ // unlit cells are specks catching a little moonlight near the surface
225
+ vec3 speck = vec3(0.45, 0.55, 0.7) * 0.07 * uMoonlight * exp(-depth * 18.0) * edgeFade(position);
226
+ vCol = (glow + speck) * uIntensity;
227
+ vA = uOpacity / (blur * blur);
228
+ gl_PointSize = clamp(px, 1.0, 64.0);
229
+ gl_Position = projectionMatrix * mv;
230
+ }
231
+ `;
232
+ const cellFragment = /* glsl */ `
233
+ varying vec3 vCol;
234
+ varying float vA;
235
+ void main() {
236
+ vec2 d = gl_PointCoord * 2.0 - 1.0;
237
+ float r2 = dot(d, d);
238
+ if (r2 > 1.0) discard;
239
+ float a = (exp(-r2 * 10.0) + 0.22 * exp(-r2 * 2.5)) * (1.0 - r2) * vA;
240
+ vec3 c = vCol * a;
241
+ if (max(c.r, max(c.g, c.b)) < 0.0015) discard;
242
+ gl_FragColor = vec4(vCol, a);
243
+ }
244
+ `;
245
+ // In-water scattering: every lit cell also lights the water around it — a wide, faint disc
246
+ // (radius ~ a few scattering lengths). Where many cells flash together (a fish, a wake) the
247
+ // discs merge into a continuous glow; lone flashes stay points. Cells below a small emission
248
+ // are collapsed to zero size so the pass costs almost nothing when the sea is dark.
249
+ const haloVertex = /* glsl */ `
250
+ ${BASE_U}
251
+ ${WATER_GLSL}
252
+ attribute float aL;
253
+ varying vec3 vCol;
254
+ varying float vA;
255
+ void main() {
256
+ float depth = max(-position.y, 0.0);
257
+ float L = aL * edgeFade(position);
258
+ vec4 mv = modelViewMatrix * vec4(position * uSize, 1.0);
259
+ float on = step(0.12, L);
260
+ gl_PointSize = on * clamp(uSize * uViewH * (0.065 + 0.2 * depth) / -mv.z, 0.0, 180.0);
261
+ vCol = depthTint(depth + 0.05) * uIntensity;
262
+ vA = on * min(L, 1.4) * absorb(depth) * 0.12 * uOpacity;
263
+ gl_Position = projectionMatrix * mv;
264
+ }
265
+ `;
266
+ const haloFragment = /* glsl */ `
267
+ varying vec3 vCol;
268
+ varying float vA;
269
+ void main() {
270
+ vec2 d = gl_PointCoord * 2.0 - 1.0;
271
+ float r2 = dot(d, d);
272
+ if (r2 > 1.0) discard;
273
+ float a = exp(-r2 * 3.0) * (1.0 - r2) * vA;
274
+ gl_FragColor = vec4(vCol, a);
275
+ }
276
+ `;
277
+ // Moon glitter: facets on the swell (same waves as the plankton) plus pebble ripples. Each facet
278
+ // rocks slowly around the mean normal (Cox–Munk slope spread), so the glitter path twinkles calmly.
279
+ const surfVertex = /* glsl */ `
280
+ ${BASE_U}
281
+ uniform float uTime, uMoonlight, uSwellHeight;
282
+ uniform vec4 uWave0, uWave1;
283
+ uniform vec2 uWavePh, uCurOff;
284
+ uniform vec4 uPebbles[${MAX_PEBBLES}];
285
+ uniform vec3 uCamLocal;
286
+ attribute vec2 aRnd;
287
+ varying vec3 vCol;
288
+ varying float vA;
289
+ void wave(vec4 w, float ph, vec2 xw, inout vec3 P, inout vec2 slope) {
290
+ float th = w.z * dot(w.xy, xw) - ph;
291
+ float A = w.w * uSwellHeight;
292
+ P.xz -= w.xy * A * sin(th);
293
+ P.y += A * cos(th);
294
+ slope -= w.xy * (w.z * A * sin(th));
295
+ }
296
+ void main() {
297
+ vec3 P = position;
298
+ vec2 xw = position.xz - uCurOff;
299
+ vec2 slope = vec2(0.0);
300
+ wave(uWave0, uWavePh.x, xw, P, slope);
301
+ wave(uWave1, uWavePh.y, xw, P, slope);
302
+ for (int i = 0; i < ${MAX_PEBBLES}; i++) {
303
+ vec4 pb = uPebbles[i];
304
+ if (pb.w <= 0.0 || pb.z > 4.5) continue;
305
+ vec2 dd = position.xz - pb.xy;
306
+ float rho = length(dd) + 1e-4;
307
+ float rf = ${RIP_C.toFixed(3)} * pb.z;
308
+ float w = 0.03 + 0.016 * pb.z;
309
+ float x = rho - rf;
310
+ float env = pb.w * 0.004 * exp(-pb.z / 1.6) * exp(-(x * x) / (w * w)) / sqrt(1.0 + rf / 0.08);
311
+ float kk = 6.2831853 / ${RIP_L.toFixed(3)};
312
+ P.y += env * cos(kk * x);
313
+ slope -= (dd / rho) * env * kk * sin(kk * x);
314
+ }
315
+ vec2 rock = vec2(sin(aRnd.x * 6.2831 + uTime * (0.35 + 0.5 * aRnd.y)), cos(aRnd.y * 6.2831 + uTime * (0.3 + 0.45 * aRnd.x)));
316
+ vec3 n = normalize(vec3(-slope.x + 0.09 * rock.x, 1.0, -slope.y + 0.09 * rock.y));
317
+ vec3 V = normalize(uCamLocal - P);
318
+ vec3 Lm = normalize(vec3(0.0, 0.42, -0.9)); // low moon behind the patch
319
+ vec3 Hh = normalize(Lm + V);
320
+ float ndv = max(dot(n, V), 0.0);
321
+ float F = 0.02 + 0.98 * pow(1.0 - ndv, 5.0);
322
+ float spec = pow(max(dot(n, Hh), 0.0), 1400.0) * 90.0 * F;
323
+ vec2 e = ${HX.toFixed(2)} - abs(position.xz);
324
+ float edge = smoothstep(0.0, 0.3, min(e.x, e.y));
325
+ vec3 sky = vec3(0.10, 0.14, 0.22);
326
+ vCol = (vec3(0.80, 0.86, 1.0) * spec + sky * F * 0.5) * uMoonlight * uIntensity;
327
+ vA = edge * uOpacity;
328
+ vec4 mv = modelViewMatrix * vec4(P * uSize, 1.0);
329
+ gl_PointSize = clamp(uSize * uViewH * 0.0022 * (1.0 + min(spec, 3.0) * 0.8) / -mv.z, 1.0, 16.0);
330
+ gl_Position = projectionMatrix * mv;
331
+ }
332
+ `;
333
+ const surfFragment = /* glsl */ `
334
+ varying vec3 vCol;
335
+ varying float vA;
336
+ void main() {
337
+ vec2 d = gl_PointCoord * 2.0 - 1.0;
338
+ float r2 = dot(d, d);
339
+ if (r2 > 1.0) discard;
340
+ float a = (exp(-r2 * 6.0) + 0.2 * exp(-r2 * 2.0)) * (1.0 - r2) * vA;
341
+ if (max(vCol.r, max(vCol.g, vCol.b)) * a < 0.002) discard;
342
+ gl_FragColor = vec4(vCol, a);
343
+ }
344
+ `;
345
+ // Splash glyphs for the pebble mode: a short white burst where each pebble enters.
346
+ const splashVertex = /* glsl */ `
347
+ uniform float uSize, uViewH, uPointerRadius;
348
+ attribute vec2 aPb; // age, strength
349
+ varying float vA;
350
+ void main() {
351
+ vec4 mv = modelViewMatrix * vec4(position * uSize, 1.0);
352
+ float age = aPb.x;
353
+ vA = aPb.y * exp(-age / 0.12) * step(age, 0.6);
354
+ gl_PointSize = clamp(uSize * uViewH * 0.05 * uPointerRadius * (0.4 + age * 3.0) / -mv.z, 0.0, 200.0) * step(0.01, vA);
355
+ gl_Position = projectionMatrix * mv;
356
+ }
357
+ `;
358
+ const splashFragment = /* glsl */ `
359
+ uniform vec3 uPointerColor;
360
+ uniform float uOpacity, uIntensity;
361
+ varying float vA;
362
+ void main() {
363
+ vec2 d = gl_PointCoord * 2.0 - 1.0;
364
+ float r2 = dot(d, d);
365
+ if (r2 > 1.0) discard;
366
+ float ring = exp(-pow((sqrt(r2) - 0.6) / 0.18, 2.0));
367
+ float a = (0.6 * exp(-r2 * 8.0) + 0.5 * ring) * (1.0 - r2) * vA * uOpacity;
368
+ if (a < 0.003) discard;
369
+ gl_FragColor = vec4(uPointerColor * uIntensity, a);
370
+ }
371
+ `;
372
+ function build(u, c, size) {
373
+ const q = qualityScale(c.quality);
374
+ const n = Math.max(1, Math.round(c.particleCount * q));
375
+ const rng = mulberry32(seedMix(0x5e45, c.seed));
376
+ // ---- swell: two waves, deep-water dispersion ----
377
+ const waves = [
378
+ { ang: 0.35 + (rng() - 0.5) * 0.3, lambda: 0.85, amp: 0.013, phi: rng() * 6.283, dx: 0, dz: 0, k: 0, om: 0 },
379
+ { ang: -0.55 + (rng() - 0.5) * 0.3, lambda: 0.5, amp: 0.006, phi: rng() * 6.283, dx: 0, dz: 0, k: 0, om: 0 },
380
+ ];
381
+ for (const w of waves) {
382
+ w.dx = Math.sin(w.ang);
383
+ w.dz = -Math.cos(w.ang); // travelling mostly toward -z, away from the camera
384
+ w.k = (2 * Math.PI) / w.lambda;
385
+ w.om = Math.sqrt(G * w.k);
386
+ }
387
+ u.uWave0.value.set(waves[0].dx, waves[0].dz, waves[0].k, waves[0].amp);
388
+ u.uWave1.value.set(waves[1].dx, waves[1].dz, waves[1].k, waves[1].amp);
389
+ // ---- cells: water-frame rest positions, cached wave phases, excitable state ----
390
+ const rx = new Float32Array(n), ry = new Float32Array(n), rz = new Float32Array(n);
391
+ const pc = new Float32Array(n * 2), ps = new Float32Array(n * 2), ek = new Float32Array(n * 2);
392
+ const thr = new Float32Array(n), res = new Float32Array(n), cd = new Float32Array(n);
393
+ const L = new Float32Array(n);
394
+ // exposure memory: the brightest light a cell gave during the camera's exposure window (trails use it)
395
+ const EXP = new Float32Array(n);
396
+ const pos = new Float32Array(n * 3), vel = new Float32Array(n * 3);
397
+ let trail = null, trailKey = '', trailScale = 1;
398
+ const seedA = new Float32Array(n);
399
+ const dirty = new Uint8Array(n);
400
+ for (let i = 0; i < n; i++) {
401
+ rx[i] = (rng() * 2 - 1) * HX;
402
+ rz[i] = (rng() * 2 - 1) * HX;
403
+ ry[i] = -Math.min(-Math.log(1 - rng() * 0.995) * LAYER, D);
404
+ thr[i] = TH0 * Math.exp(0.55 * gauss(rng));
405
+ res[i] = 1;
406
+ seedA[i] = rng();
407
+ dirty[i] = 1;
408
+ }
409
+ function refresh(i) {
410
+ for (let w = 0; w < 2; w++) {
411
+ const W = waves[w];
412
+ const th = W.k * (W.dx * rx[i] + W.dz * rz[i]) + W.phi;
413
+ pc[i * 2 + w] = Math.cos(th);
414
+ ps[i * 2 + w] = Math.sin(th);
415
+ ek[i * 2 + w] = Math.exp(W.k * ry[i]);
416
+ }
417
+ dirty[i] = 0;
418
+ }
419
+ // ---- pointer agents ----
420
+ const PB = new Float64Array(MAX_PEBBLES * 4); // x, z, age, strength
421
+ for (let k = 0; k < MAX_PEBBLES; k++)
422
+ PB[k * 4 + 2] = 99;
423
+ let pebHead = 0, dropClock = 0, lastDropX = 1e9, lastDropZ = 1e9;
424
+ const PK = new Float64Array(MAX_PEBBLES * 12);
425
+ const fish = { x: 0.5, z: 0, y: -0.06, psi: Math.PI / 2, v: 0.28, tail: 0, lastSin: 0, wander: rng() * 6.28 };
426
+ const VX = new Float64Array(MAX_VORT * 5); // x, z, gamma, age, alive
427
+ let vHead = 0;
428
+ // uniform grid over the patch (world coordinates) so cursor agents visit only nearby cells
429
+ const GN = 24, GS = (2 * HX) / GN;
430
+ const gStart = new Int32Array(GN * GN + 1), gCell = new Int32Array(n), gOrder = new Int32Array(n);
431
+ const stamp = new Uint32Array(n), touch = new Int32Array(n);
432
+ const SX = new Float32Array(n), UX = new Float32Array(n), UY = new Float32Array(n), UZ = new Float32Array(n);
433
+ let stampId = 0, nTouch = 0, gridAge = 0;
434
+ const PAD = 0.05;
435
+ function buildGrid() {
436
+ gStart.fill(0);
437
+ for (let i = 0; i < n; i++) {
438
+ let gx = Math.floor((rx[i] + curX + HX) / GS), gz = Math.floor((rz[i] + curZ + HX) / GS);
439
+ gx = gx < 0 ? 0 : gx >= GN ? GN - 1 : gx;
440
+ gz = gz < 0 ? 0 : gz >= GN ? GN - 1 : gz;
441
+ const g = gz * GN + gx;
442
+ gCell[i] = g;
443
+ gStart[g + 1]++;
444
+ }
445
+ for (let g = 0; g < GN * GN; g++)
446
+ gStart[g + 1] += gStart[g];
447
+ const fill = gStart.slice(0, GN * GN);
448
+ for (let i = 0; i < n; i++)
449
+ gOrder[fill[gCell[i]]++] = i;
450
+ }
451
+ // mark every cell in the buckets overlapping a square (or an annulus r0..r1 when ring = 1)
452
+ function addBox(cx, cz, r1in, r0in, ring) {
453
+ const r1 = r1in + PAD, r0 = Math.max(r0in - PAD, 0);
454
+ const gx0 = Math.max(0, Math.floor((cx - r1 + HX) / GS)), gx1 = Math.min(GN - 1, Math.floor((cx + r1 + HX) / GS));
455
+ const gz0 = Math.max(0, Math.floor((cz - r1 + HX) / GS)), gz1 = Math.min(GN - 1, Math.floor((cz + r1 + HX) / GS));
456
+ for (let gz = gz0; gz <= gz1; gz++)
457
+ for (let gx = gx0; gx <= gx1; gx++) {
458
+ if (ring) {
459
+ const bx0 = -HX + gx * GS, bz0 = -HX + gz * GS;
460
+ const nx = Math.max(bx0 - cx, 0, cx - bx0 - GS), nz = Math.max(bz0 - cz, 0, cz - bz0 - GS);
461
+ const fx = Math.max(Math.abs(bx0 - cx), Math.abs(bx0 + GS - cx)), fz = Math.max(Math.abs(bz0 - cz), Math.abs(bz0 + GS - cz));
462
+ if (nx * nx + nz * nz > r1 * r1 || fx * fx + fz * fz < r0 * r0)
463
+ continue;
464
+ }
465
+ const g = gz * GN + gx;
466
+ for (let q = gStart[g]; q < gStart[g + 1]; q++) {
467
+ const i = gOrder[q];
468
+ if (stamp[i] !== stampId) {
469
+ stamp[i] = stampId;
470
+ touch[nTouch++] = i;
471
+ }
472
+ }
473
+ }
474
+ }
475
+ let time = 0;
476
+ let curX = 0, curZ = 0;
477
+ const hp = new THREE.Vector3(), hv = new THREE.Vector3();
478
+ function step(h) {
479
+ time += h;
480
+ const sw = Math.max(u.uSwellHeight.value, 0);
481
+ const sens = Math.max(u.uSensitivity.value, 0.05);
482
+ const tauL = 0.22 * Math.max(u.uGlowTime.value, 0.05);
483
+ const decL = Math.exp(-h / tauL);
484
+ const rec = h / Math.max(u.uRecovery.value, 0.1);
485
+ // current: slow drift toward +x, slightly across
486
+ const cur = 0.035 * u.uCurrent.value;
487
+ curX += cur * 0.92 * h;
488
+ curZ += cur * 0.38 * h;
489
+ u.uCurOff.value.set(curX, curZ);
490
+ // wave clocks
491
+ const W0 = waves[0], W1 = waves[1];
492
+ const C0 = Math.cos(W0.om * time), S0 = Math.sin(W0.om * time);
493
+ const C1 = Math.cos(W1.om * time), S1 = Math.sin(W1.om * time);
494
+ const Aw0 = W0.amp * sw, Aw1 = W1.amp * sw, om0 = W0.om, om1 = W1.om;
495
+ const sk0 = W0.om * W0.k, sk1 = W1.om * W1.k; // shear per unit amplitude
496
+ const dx0 = W0.dx, dz0 = W0.dz, dx1 = W1.dx, dz1 = W1.dz;
497
+ u.uWavePh.value.set(waves[0].om * time - waves[0].phi, waves[1].om * time - waves[1].phi);
498
+ // pointer
499
+ const mode = u.uPointerMode.value;
500
+ const Sp = u.uPointerStrength.value;
501
+ const act = u.uPointerActive.value;
502
+ const Rr = u.uPointerRadius.value;
503
+ hp.copy(u.uPointer.value);
504
+ hv.copy(u.uPointerVel.value);
505
+ hp.x -= curX;
506
+ hp.z -= curZ; // into the water frame
507
+ const useHand = mode === 0 && Sp * act > 0.01;
508
+ const usePeb = mode === 1 && Sp > 0.01;
509
+ const useFish = mode === 2 && Sp > 0.01;
510
+ const RH = 0.055 * Rr;
511
+ const hs = Sp * act;
512
+ const hSpeed = Math.hypot(hv.x, hv.z);
513
+ const hY = Math.min(Math.max(hp.y, -D * 0.6), -0.012);
514
+ // pebbles: one every 0.4 s while the cursor is active, or after 0.25 of travel
515
+ if (usePeb) {
516
+ dropClock += h;
517
+ const moved = Math.hypot(hp.x - lastDropX, hp.z - lastDropZ);
518
+ if (act > 0.6 && (dropClock > 0.55 || moved > 0.25 * Rr) && dropClock > 0.15) {
519
+ const k = pebHead % MAX_PEBBLES;
520
+ pebHead++;
521
+ PB[k * 4] = hp.x;
522
+ PB[k * 4 + 1] = hp.z;
523
+ PB[k * 4 + 2] = 0;
524
+ PB[k * 4 + 3] = Sp * Rr;
525
+ dropClock = 0;
526
+ lastDropX = hp.x;
527
+ lastDropZ = hp.z;
528
+ }
529
+ }
530
+ // per-pebble constants for this step (splash, sinking wake, ripple band) so the cell loop is cheap
531
+ let nPeb = 0;
532
+ for (let k = 0; k < MAX_PEBBLES; k++) {
533
+ PB[k * 4 + 2] += h;
534
+ const age = PB[k * 4 + 2], st = PB[k * 4 + 3];
535
+ const pbv = u.uPebbles.value[k];
536
+ pbv.set(PB[k * 4] + curX, PB[k * 4 + 1] + curZ, age, age < 4.5 ? st : 0);
537
+ if (age >= 3.5)
538
+ continue;
539
+ const o = nPeb * 12;
540
+ const rf = RIP_C * age, w = 0.03 + 0.016 * age;
541
+ const r0 = Math.max(rf - 2.3 * w, 0), r1 = rf + 2.3 * w;
542
+ PK[o] = PB[k * 4];
543
+ PK[o + 1] = PB[k * 4 + 1];
544
+ PK[o + 2] = age;
545
+ PK[o + 3] = st;
546
+ PK[o + 4] = rf;
547
+ PK[o + 5] = w;
548
+ PK[o + 6] = r0 * r0;
549
+ PK[o + 7] = r1 * r1;
550
+ PK[o + 8] = st * 18 * Math.exp(-age / 1.6) / Math.sqrt(1 + rf / 0.08);
551
+ if (PK[o + 8] * sens < 0.3 * TH0) {
552
+ PK[o + 6] = 1e9;
553
+ PK[o + 7] = -1;
554
+ } // too weak to light anything
555
+ PK[o + 9] = age < 0.3 ? 60 * Math.exp(-age / 0.08) : 0; // splash amplitude
556
+ PK[o + 10] = -0.35 * age; // pebble depth
557
+ PK[o + 11] = 0.015 * st + 0.004; // pebble radius
558
+ nPeb++;
559
+ }
560
+ // fish: steer toward the cursor (or cruise a slow circle), beat the tail, shed vortices
561
+ let fcos = 0, fsin = 0, fa = 0, fb = 0, fc = 0;
562
+ if (useFish) {
563
+ const F = fish;
564
+ let tx, tz;
565
+ // targets are chosen in the world (inside the patch), then taken into the water frame
566
+ if (act > 0.3) {
567
+ tx = hp.x + curX;
568
+ tz = hp.z + curZ;
569
+ }
570
+ else {
571
+ F.wander += h * 0.16;
572
+ tx = 0.62 * Math.cos(F.wander);
573
+ tz = 0.5 * Math.sin(F.wander);
574
+ }
575
+ tx = Math.max(-0.85, Math.min(0.85, tx)) - curX;
576
+ tz = Math.max(-0.85, Math.min(0.85, tz)) - curZ;
577
+ const dx = tx - F.x, dz = tz - F.z;
578
+ const dist = Math.hypot(dx, dz);
579
+ const want = Math.atan2(dz, dx);
580
+ let dpsi = want - F.psi;
581
+ while (dpsi > Math.PI)
582
+ dpsi -= 2 * Math.PI;
583
+ while (dpsi < -Math.PI)
584
+ dpsi += 2 * Math.PI;
585
+ const turn = 2.6 * Math.min(Sp, 2);
586
+ F.psi += Math.max(-turn * h, Math.min(turn * h, dpsi));
587
+ const vDes = (act > 0.3 ? Math.min(0.3 + dist * 1.5, 1.1) : 0.34) * Math.min(Sp, 2.5);
588
+ F.v += (vDes - F.v) * Math.min(1, h * 1.8);
589
+ fcos = Math.cos(F.psi);
590
+ fsin = Math.sin(F.psi);
591
+ F.x += fcos * F.v * h;
592
+ F.z += fsin * F.v * h;
593
+ F.y = -0.04 + 0.008 * Math.sin(time * 0.7);
594
+ const freq = 1.3 + 3.2 * F.v / Math.max(Rr, 0.3);
595
+ F.tail += 6.2831853 * freq * h;
596
+ fa = 0.15 * Rr;
597
+ fb = 0.032 * Rr;
598
+ fc = 0.025 * Rr;
599
+ const sn = Math.sin(F.tail);
600
+ if ((sn >= 0) !== (F.lastSin >= 0)) {
601
+ // tail tip at the end of a stroke sheds a vortex, alternating sign: a reverse Karman street
602
+ const k = vHead % MAX_VORT;
603
+ vHead++;
604
+ const lat = 0.9 * fb * Math.cos(F.tail);
605
+ const tx2 = F.x - fcos * fa * 1.15 - fsin * lat, tz2 = F.z - fsin * fa * 1.15 + fcos * lat;
606
+ VX[k * 5] = tx2;
607
+ VX[k * 5 + 1] = tz2;
608
+ VX[k * 5 + 2] = (sn >= 0 ? 1 : -1) * 1.1 * F.v * fa;
609
+ VX[k * 5 + 3] = 0;
610
+ VX[k * 5 + 4] = 1;
611
+ }
612
+ F.lastSin = sn;
613
+ }
614
+ // vortices: decay, grow, and their bounding box
615
+ let vMinX = 1e9, vMaxX = -1e9, vMinZ = 1e9, vMaxZ = -1e9;
616
+ const liveV = [];
617
+ for (let k = 0; k < MAX_VORT; k++) {
618
+ if (!VX[k * 5 + 4])
619
+ continue;
620
+ VX[k * 5 + 3] += h;
621
+ VX[k * 5 + 2] *= Math.exp(-h / 2.4);
622
+ if (VX[k * 5 + 3] > 7 || !useFish && VX[k * 5 + 3] > 3) {
623
+ VX[k * 5 + 4] = 0;
624
+ continue;
625
+ }
626
+ const a = 0.028 + 0.018 * VX[k * 5 + 3];
627
+ liveV.push(k);
628
+ vMinX = Math.min(vMinX, VX[k * 5] - 3 * a);
629
+ vMaxX = Math.max(vMaxX, VX[k * 5] + 3 * a);
630
+ vMinZ = Math.min(vMinZ, VX[k * 5 + 1] - 3 * a);
631
+ vMaxZ = Math.max(vMaxZ, VX[k * 5 + 1] + 3 * a);
632
+ }
633
+ // vortices advect each other (2D Biot–Savart) so the street spreads and pairs
634
+ for (const k of liveV) {
635
+ let ux = 0, uz = 0;
636
+ for (const m of liveV) {
637
+ if (m === k)
638
+ continue;
639
+ const dx = VX[k * 5] - VX[m * 5], dz = VX[k * 5 + 1] - VX[m * 5 + 1];
640
+ const a = 0.028 + 0.018 * VX[m * 5 + 3];
641
+ const f = VX[m * 5 + 2] / (6.2831853 * (dx * dx + dz * dz + a * a));
642
+ ux += -dz * f;
643
+ uz += dx * f;
644
+ }
645
+ VX[k * 5] += ux * h;
646
+ VX[k * 5 + 1] += uz * h;
647
+ }
648
+ const nV = liveV.length;
649
+ const F = fish;
650
+ const hvx = hv.x, hvy = 0, hvz = hv.z;
651
+ const hvn = hSpeed > 1e-4 ? 1 / hSpeed : 0;
652
+ // ---- cursor agents: only cells inside an agent's reach are visited (grid buckets) ----
653
+ const anyAgent = useHand || nPeb > 0 || useFish || nV > 0;
654
+ if (!anyAgent)
655
+ gridAge = 0;
656
+ stampId++;
657
+ if (anyAgent) {
658
+ // cells drift slowly, so the grid is rebuilt every 6 steps and agent boxes are padded
659
+ if (gridAge <= 0) {
660
+ buildGrid();
661
+ gridAge = 6;
662
+ }
663
+ gridAge--;
664
+ nTouch = 0;
665
+ if (useHand)
666
+ addBox(hp.x + curX, hp.z + curZ, 7 * RH, 0, 0);
667
+ for (let q = 0, o = 0; q < nPeb; q++, o += 12) {
668
+ const cx = PK[o] + curX, cz = PK[o + 1] + curZ;
669
+ addBox(cx, cz, 0.12, 0, 0); // splash + sinking pebble
670
+ if (PK[o + 7] > 0)
671
+ addBox(cx, cz, Math.sqrt(PK[o + 7]), Math.sqrt(PK[o + 6]), 1); // ripple band
672
+ }
673
+ if (useFish)
674
+ addBox(F.x + curX, F.z + curZ, 2 * fa, 0, 0);
675
+ for (let q = 0; q < nV; q++) {
676
+ const k = liveV[q];
677
+ addBox(VX[k * 5] + curX, VX[k * 5 + 1] + curZ, 3 * (0.028 + 0.018 * VX[k * 5 + 3]), 0, 0);
678
+ }
679
+ for (let t = 0; t < nTouch; t++) {
680
+ const i = touch[t];
681
+ const x = rx[i], y = ry[i], z = rz[i];
682
+ let s = 0;
683
+ let ux = 0, uy = 0, uz = 0;
684
+ if (useHand) {
685
+ const qx = x - hp.x, qy = y - hY, qz = z - hp.z;
686
+ const r2 = qx * qx + qy * qy + qz * qz;
687
+ if (r2 < 49 * RH * RH) {
688
+ const r = Math.sqrt(r2) + 1e-6;
689
+ if (r > RH) {
690
+ const k3 = (RH * RH * RH) / (2 * r2 * r);
691
+ const vr = (hvx * qx + hvy * qy + hvz * qz) / r;
692
+ ux += hs * k3 * (3 * vr * qx / r - hvx);
693
+ uy += hs * k3 * (3 * vr * qy / r - hvy);
694
+ uz += hs * k3 * (3 * vr * qz / r - hvz);
695
+ s += hs * 3 * hSpeed * RH * RH * RH / (r2 * r2);
696
+ }
697
+ else {
698
+ ux += hs * hvx;
699
+ uz += hs * hvz;
700
+ s += hs * hSpeed / RH;
701
+ }
702
+ const behind = -(qx * hvx + qz * hvz) * hvn;
703
+ if (behind > 0) {
704
+ const perp2 = r2 - behind * behind;
705
+ const wk = hs * Math.exp(-perp2 / (RH * RH) - behind / (4 * RH));
706
+ ux += hvx * wk * 0.6;
707
+ uz += hvz * wk * 0.6;
708
+ s += wk * hSpeed / RH * 0.9;
709
+ }
710
+ }
711
+ }
712
+ if (nPeb && y > -D - 0.01) {
713
+ for (let q = 0, o = 0; q < nPeb; q++, o += 12) {
714
+ const dx = x - PK[o], dz = z - PK[o + 1];
715
+ const rho2 = dx * dx + dz * dz;
716
+ const st = PK[o + 3];
717
+ if (PK[o + 9] > 0 && rho2 < 0.01 * st * st + 0.0016 && y > -0.12) { // splash
718
+ // compact bump kernels (~gaussian in rho, ~e^{30 y} in depth) — no transcendentals
719
+ const b = 1 - rho2 / (0.01 * st * st + 0.0016);
720
+ const dz4 = 1 + y * 7.5;
721
+ const e = dz4 > 0 ? b * b * b * dz4 * dz4 * dz4 * dz4 : 0;
722
+ s += PK[o + 9] * e;
723
+ const rho = Math.sqrt(rho2) + 1e-4;
724
+ const push = e * PK[o + 9] / 120;
725
+ ux += dx / rho * push;
726
+ uz += dz / rho * push;
727
+ }
728
+ // sinking pebble and its short wake
729
+ if (rho2 < 0.01 && PK[o + 10] > -D - 0.05) {
730
+ const dy = y - PK[o + 10];
731
+ if (dy > -0.03 && dy < 0.14) {
732
+ const Rp = PK[o + 11];
733
+ s += (0.35 / Rp) * Math.exp(-rho2 / (Rp * Rp * 2.2) - Math.max(dy, 0) / 0.07);
734
+ }
735
+ }
736
+ // ripple packet: crests light a ring near the surface
737
+ if (y > -0.09 && rho2 > PK[o + 6] && rho2 < PK[o + 7]) {
738
+ const xr = Math.sqrt(rho2) - PK[o + 4];
739
+ const w = PK[o + 5];
740
+ const cr = Math.cos(6.2831853 * xr / RIP_L);
741
+ const b = 1 - (xr * xr) / (5.3 * w * w); // bump with the band's support (2.3 w)
742
+ const dz4 = 1 + y * 7; // ~e^{28 y}
743
+ if (b > 0 && dz4 > 0)
744
+ s += PK[o + 8] * b * b * b * dz4 * dz4 * dz4 * dz4 * cr * cr;
745
+ }
746
+ }
747
+ }
748
+ if (useFish) {
749
+ const dx = x - F.x, dz = z - F.z, dy = y - F.y;
750
+ if (dx * dx + dz * dz < 4 * fa * fa && dy * dy < 0.01) {
751
+ const ua = dx * fcos + dz * fsin, va = -dx * fsin + dz * fcos;
752
+ const tailf = Math.max(0, (fa - ua) / (2 * fa));
753
+ const wig = 0.9 * fb * tailf * tailf * Math.sin(F.tail - 5 * tailf);
754
+ const e2 = (ua / fa) * (ua / fa) + ((va - wig) / fb) * ((va - wig) / fb) + (dy / fc) * (dy / fc);
755
+ if (e2 < 5) {
756
+ const de = Math.sqrt(e2);
757
+ const ds = (de - 1) * fb; // ~distance to the body surface
758
+ // shear in the boundary layer and the displaced water around the body
759
+ const delta = 0.028 * Rr + 0.006;
760
+ if (ds > -delta)
761
+ s += (F.v / delta) * 0.5 * Math.exp(-(ds * ds) / (delta * delta));
762
+ if (de < 1.2) { // no penetration: water is pushed off the body
763
+ const push = F.v * 2.2 * (1.2 - de);
764
+ const lat = (va - wig) >= 0 ? 1 : -1;
765
+ ux += -fsin * lat * push;
766
+ uz += fcos * lat * push;
767
+ uy += (dy >= 0 ? 1 : -1) * push * 0.3;
768
+ }
769
+ }
770
+ }
771
+ }
772
+ if (nV && x > vMinX && x < vMaxX && z > vMinZ && z < vMaxZ) {
773
+ const dfac = Math.exp(-((y - F.y) * (y - F.y)) / 0.0049);
774
+ if (dfac > 0.02) {
775
+ for (let q = 0; q < nV; q++) {
776
+ const k = liveV[q];
777
+ const dx = x - VX[k * 5], dz = z - VX[k * 5 + 1];
778
+ const a = 0.028 + 0.018 * VX[k * 5 + 3];
779
+ const r2 = dx * dx + dz * dz;
780
+ if (r2 > 9 * a * a)
781
+ continue;
782
+ const G2 = VX[k * 5 + 2];
783
+ const f = (G2 / (6.2831853 * (r2 + a * a))) * dfac;
784
+ ux += -dz * f;
785
+ uz += dx * f;
786
+ s += Math.abs(G2) / (3.14159 * a * a) * Math.exp(-r2 / (a * a)) * dfac * 0.7;
787
+ }
788
+ }
789
+ }
790
+ SX[i] = s;
791
+ UX[i] = ux;
792
+ UY[i] = uy;
793
+ UZ[i] = uz;
794
+ }
795
+ }
796
+ const decE = Math.exp(-h / Math.max(0.6 * u.uTrailLength.value, 0.1));
797
+ cellLoop(h, curX, curZ, stampId, cur, sens, decL, rec, decE, C0, S0, C1, S1, Aw0, Aw1, om0, om1, sk0, sk1, dx0, dz0, dx1, dz1);
798
+ }
799
+ // the per-cell hot loop, kept in its own small function so V8 keeps its scalars in registers
800
+ function cellLoop(h, cX, cZ, sid, cur, sens, decL, rec, decE, C0, S0, C1, S1, Aw0, Aw1, om0, om1, sk0, sk1, dx0, dz0, dx1, dz1) {
801
+ const k0 = waves[0].k, k1 = waves[1].k;
802
+ const kx0 = k0 * dx0, kz0 = k0 * dz0, kx1 = k1 * dx1, kz1 = k1 * dz1;
803
+ for (let i = 0; i < n; i++) {
804
+ let x = rx[i], y = ry[i], z = rz[i];
805
+ let s = 0; // shear rate felt by the cell (1/s)
806
+ let ux = 0, uy = 0, uz = 0; // extra flow from the cursor agents
807
+ if (stamp[i] === sid) {
808
+ s = SX[i];
809
+ ux = UX[i];
810
+ uy = UY[i];
811
+ uz = UZ[i];
812
+ }
813
+ if (ux !== 0 || uy !== 0 || uz !== 0) {
814
+ const ddx = ux * h, ddz = uz * h;
815
+ let ny = y + uy * h;
816
+ if (ny > -0.003)
817
+ ny = -0.003;
818
+ else if (ny < -D)
819
+ ny = -D;
820
+ const ddy = ny - y;
821
+ x += ddx;
822
+ y = ny;
823
+ z += ddz;
824
+ rx[i] = x;
825
+ ry[i] = y;
826
+ rz[i] = z;
827
+ // small moves: rotate the cached wave phases and rescale e^{ky} instead of recomputing them
828
+ const i2 = i * 2;
829
+ const a0 = kx0 * ddx + kz0 * ddz, a1 = kx1 * ddx + kz1 * ddz;
830
+ if (a0 * a0 + a1 * a1 < 0.01) {
831
+ const q0 = 1 - 0.5 * a0 * a0, q1 = 1 - 0.5 * a1 * a1;
832
+ const p0 = pc[i2], s0 = ps[i2], p1 = pc[i2 + 1], s1 = ps[i2 + 1];
833
+ pc[i2] = p0 * q0 - s0 * a0;
834
+ ps[i2] = s0 * q0 + p0 * a0;
835
+ pc[i2 + 1] = p1 * q1 - s1 * a1;
836
+ ps[i2 + 1] = s1 * q1 + p1 * a1;
837
+ const e0 = k0 * ddy, e1 = k1 * ddy;
838
+ ek[i2] *= 1 + e0 + 0.5 * e0 * e0;
839
+ ek[i2 + 1] *= 1 + e1 + 0.5 * e1 * e1;
840
+ }
841
+ else
842
+ dirty[i] = 1;
843
+ }
844
+ // periodic patch: keep world = water frame + current inside [-HX, HX]
845
+ let wrapped = false;
846
+ if (x + cX > HX) {
847
+ x -= 2 * HX;
848
+ rx[i] = x;
849
+ dirty[i] = 1;
850
+ wrapped = true;
851
+ }
852
+ else if (x + cX < -HX) {
853
+ x += 2 * HX;
854
+ rx[i] = x;
855
+ dirty[i] = 1;
856
+ wrapped = true;
857
+ }
858
+ if (z + cZ > HX) {
859
+ z -= 2 * HX;
860
+ rz[i] = z;
861
+ dirty[i] = 1;
862
+ wrapped = true;
863
+ }
864
+ else if (z + cZ < -HX) {
865
+ z += 2 * HX;
866
+ rz[i] = z;
867
+ dirty[i] = 1;
868
+ wrapped = true;
869
+ }
870
+ if (dirty[i])
871
+ refresh(i);
872
+ // swell: orbital displacement and shear (strongest at crests, where the swell steepens)
873
+ // cos/sin(th - w t) from the cached phase by rotation (two waves, unrolled)
874
+ const i2 = i * 2;
875
+ const ct0 = pc[i2] * C0 + ps[i2] * S0, st0 = ps[i2] * C0 - pc[i2] * S0;
876
+ const ct1 = pc[i2 + 1] * C1 + ps[i2 + 1] * S1, st1 = ps[i2 + 1] * C1 - pc[i2 + 1] * S1;
877
+ const A0 = Aw0 * ek[i2], A1 = Aw1 * ek[i2 + 1];
878
+ const ox = -(dx0 * A0 * st0 + dx1 * A1 * st1), oz = -(dz0 * A0 * st0 + dz1 * A1 * st1);
879
+ const oy = A0 * ct0 + A1 * ct1;
880
+ const vx = dx0 * A0 * om0 * ct0 + dx1 * A1 * om1 * ct1;
881
+ const vz = dz0 * A0 * om0 * ct0 + dz1 * A1 * om1 * ct1;
882
+ const vy = A0 * om0 * st0 + A1 * om1 * st1;
883
+ // crest factor max(cos, 0)^6, branch-free (a data-dependent branch here costs more than the maths)
884
+ const m0 = 0.5 * (ct0 + Math.abs(ct0)), m1 = 0.5 * (ct1 + Math.abs(ct1));
885
+ const c0 = m0 * m0 * m0, c1 = m1 * m1 * m1;
886
+ s += A0 * sk0 * (1 + 3.2 * c0 * c0) + A1 * sk1 * (1 + 3.2 * c1 * c1);
887
+ // world position (current offset, periodic patch)
888
+ const wx = x + cX + ox, wz = z + cZ + oz;
889
+ const wy = Math.min(y + oy, -0.001);
890
+ const j = i * 3;
891
+ vel[j] = vx + ux + cur * 0.92;
892
+ vel[j + 1] = vy + uy;
893
+ vel[j + 2] = vz + uz + cur * 0.38;
894
+ pos[j] = wx;
895
+ pos[j + 1] = wy;
896
+ pos[j + 2] = wz;
897
+ if (wrapped && trail)
898
+ trail.reset(i);
899
+ // excitable cell: threshold, flash, fatigue, recovery
900
+ let l = L[i] * decL;
901
+ let r = res[i];
902
+ const cdi = cd[i] - h;
903
+ cd[i] = cdi;
904
+ if (cdi <= 0 && r > 0.04 && s * sens > thr[i]) {
905
+ let over = (s * sens) / thr[i] - 1;
906
+ if (over > 1.5)
907
+ over = 1.5;
908
+ const fl = r * (0.75 + 0.45 * over);
909
+ if (fl > l)
910
+ l = fl;
911
+ r *= 0.62;
912
+ cd[i] = 0.16 + 0.12 * seedA[i];
913
+ }
914
+ res[i] = r + (1 - r) * rec;
915
+ // near the patch edge, light fades (cells wrap there)
916
+ const ex = HX - (wx < 0 ? -wx : wx), ez = HX - (wz < 0 ? -wz : wz);
917
+ const em = ex < ez ? ex : ez;
918
+ const li = em < 0.12 ? l * (em > 0 ? em / 0.12 : 0) : l;
919
+ L[i] = li;
920
+ const e = EXP[i] * decE;
921
+ EXP[i] = li > e ? li : e;
922
+ }
923
+ }
924
+ const stepper = createFixedStep(H_STEP, 4);
925
+ for (let i = 0; i < n; i++)
926
+ refresh(i);
927
+ // warm up: settle recovery state and fill the swell history
928
+ for (let s = 0; s < 60; s++)
929
+ step(H_STEP);
930
+ // ---- geometry ----
931
+ const geo = new THREE.BufferGeometry();
932
+ const posAttr = new THREE.BufferAttribute(pos, 3).setUsage(THREE.DynamicDrawUsage);
933
+ const lAttr = new THREE.BufferAttribute(L, 1).setUsage(THREE.DynamicDrawUsage);
934
+ geo.setAttribute('position', posAttr);
935
+ geo.setAttribute('aL', lAttr);
936
+ geo.setAttribute('aSeed', new THREE.BufferAttribute(seedA, 1));
937
+ const mat = new THREE.ShaderMaterial({
938
+ uniforms: u, vertexShader: cellVertex, fragmentShader: cellFragment,
939
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
940
+ });
941
+ const cells = new THREE.Points(geo, mat);
942
+ cells.frustumCulled = false;
943
+ attachViewH(cells, u);
944
+ const hmat = new THREE.ShaderMaterial({
945
+ uniforms: u, vertexShader: haloVertex, fragmentShader: haloFragment,
946
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
947
+ });
948
+ const halos = new THREE.Points(geo, hmat);
949
+ halos.frustumCulled = false;
950
+ const NS = Math.round(170 * Math.sqrt(q));
951
+ const sp = new Float32Array(NS * NS * 3), sr = new Float32Array(NS * NS * 2);
952
+ const srng = mulberry32(seedMix(0x6a17, c.seed));
953
+ for (let a = 0, p = 0; a < NS; a++)
954
+ for (let b = 0; b < NS; b++, p++) {
955
+ sp[p * 3] = -HX + (2 * HX * (a + srng())) / NS;
956
+ sp[p * 3 + 2] = -HX + (2 * HX * (b + srng())) / NS;
957
+ sr[p * 2] = srng();
958
+ sr[p * 2 + 1] = srng();
959
+ }
960
+ const sgeo = new THREE.BufferGeometry();
961
+ sgeo.setAttribute('position', new THREE.BufferAttribute(sp, 3));
962
+ sgeo.setAttribute('aRnd', new THREE.BufferAttribute(sr, 2));
963
+ const smat = new THREE.ShaderMaterial({
964
+ uniforms: u, vertexShader: surfVertex, fragmentShader: surfFragment,
965
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
966
+ });
967
+ const surface = new THREE.Points(sgeo, smat);
968
+ surface.frustumCulled = false;
969
+ const pgeo = new THREE.BufferGeometry();
970
+ const pp = new Float32Array(MAX_PEBBLES * 3), pa = new Float32Array(MAX_PEBBLES * 2);
971
+ const ppAttr = new THREE.BufferAttribute(pp, 3).setUsage(THREE.DynamicDrawUsage);
972
+ const paAttr = new THREE.BufferAttribute(pa, 2).setUsage(THREE.DynamicDrawUsage);
973
+ pgeo.setAttribute('position', ppAttr);
974
+ pgeo.setAttribute('aPb', paAttr);
975
+ const pmat = new THREE.ShaderMaterial({
976
+ uniforms: u, vertexShader: splashVertex, fragmentShader: splashFragment,
977
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
978
+ });
979
+ const splashes = new THREE.Points(pgeo, pmat);
980
+ splashes.frustumCulled = false;
981
+ const object = new THREE.Group();
982
+ const proxy = createPickProxy(BOUNDS.center, BOUNDS.radius);
983
+ object.add(surface, halos, cells, splashes, proxy.mesh);
984
+ const anchor = new THREE.Vector3(...ANCHOR);
985
+ const up = new THREE.Vector3(0, 1, 0);
986
+ // ---- trails: only glowing cells leave light ----
987
+ const trailGLSL = /* glsl */ `
988
+ ${WATER_GLSL}
989
+ uniform float uTrailGain;
990
+ vec3 trailShade(vec3 p, float f) {
991
+ float depth = max(-p.y, 0.0);
992
+ return depthTint(depth) * TRAIL_K * absorb(depth) * edgeFade(p) * 3.0 * uTrailGain;
993
+ }
994
+ vec3 trailView(vec3 mv, out float mag) { mag = 1.0; return mv; }
995
+ `;
996
+ const materials = [mat, hmat, smat, pmat];
997
+ trailKey = '';
998
+ function buildTrails(mode, share) {
999
+ const key = mode + ':' + share;
1000
+ if (key === trailKey)
1001
+ return;
1002
+ trailKey = key;
1003
+ if (trail) {
1004
+ object.remove(trail.object);
1005
+ trail.dispose();
1006
+ trail = null;
1007
+ }
1008
+ materials.length = 4;
1009
+ trailScale = TRAIL_SCALE[mode] || 1;
1010
+ u.uTrailGain.value = TRAIL_GAIN[mode] || 1;
1011
+ trail = createCpuTrail(mode, { u, n, pos, vel, k: EXP, glsl: trailGLSL, sizePx: 0.0075, spacing: 0.0015, sub: 6 });
1012
+ if (!trail)
1013
+ return;
1014
+ object.add(trail.object);
1015
+ materials.push(...trail.materials);
1016
+ }
1017
+ buildTrails(c.trailMode, c.trailShare);
1018
+ function upload() {
1019
+ posAttr.needsUpdate = true;
1020
+ lAttr.needsUpdate = true;
1021
+ for (let k = 0; k < MAX_PEBBLES; k++) {
1022
+ const v = u.uPebbles.value[k];
1023
+ pp[k * 3] = v.x;
1024
+ pp[k * 3 + 1] = 0.004;
1025
+ pp[k * 3 + 2] = v.y;
1026
+ pa[k * 2] = v.z;
1027
+ pa[k * 2 + 1] = v.w > 0 ? Math.min(v.w, 2) : 0;
1028
+ }
1029
+ ppAttr.needsUpdate = true;
1030
+ paAttr.needsUpdate = true;
1031
+ }
1032
+ return {
1033
+ object,
1034
+ materials,
1035
+ apply(cfg) { if (cfg)
1036
+ buildTrails(cfg.trailMode, cfg.trailShare); },
1037
+ update(dt, camera, pointer) {
1038
+ const rdt = Math.min(Math.max(dt, 0), 1 / 20);
1039
+ if (camera && !inView(object, camera, BOUNDS.center, BOUNDS.radius, u.uSize.value))
1040
+ return;
1041
+ const d = rdt * u.uSpeed.value;
1042
+ u.uTime.value += d;
1043
+ if (camera) {
1044
+ object.worldToLocal(u.uCamLocal.value.copy(camera.position)).divideScalar(u.uSize.value);
1045
+ if (pointer)
1046
+ stepPointerField(u, pointer, camera, object, u.uSize.value, rdt, anchor, up);
1047
+ }
1048
+ u.uVelScale.value = trailScale; // the sea moves slowly: exposures look back further
1049
+ let simD = 0;
1050
+ stepper.run(d, (hh) => { step(hh); simD += hh; });
1051
+ if (simD > 0) {
1052
+ upload();
1053
+ if (trail)
1054
+ trail.update(simD * trailScale);
1055
+ }
1056
+ },
1057
+ seek(t) { for (let s = 0; s < Math.round(t / H_STEP); s++)
1058
+ step(H_STEP); upload(); },
1059
+ dispose() {
1060
+ geo.dispose();
1061
+ mat.dispose();
1062
+ hmat.dispose();
1063
+ sgeo.dispose();
1064
+ smat.dispose();
1065
+ pgeo.dispose();
1066
+ pmat.dispose();
1067
+ proxy.dispose();
1068
+ if (trail)
1069
+ trail.dispose();
1070
+ },
1071
+ _state: { pos, vel, L, res, EXP },
1072
+ };
1073
+ }
1074
+ /** Defaults for every knob, so an older saved config still renders (identical to the catalog entry). */
1075
+ const DEFAULTS = {
1076
+ intensity: 1,
1077
+ speed: 1,
1078
+ opacity: 0.9,
1079
+ size: 1,
1080
+ color: '#3cc3ff',
1081
+ particleCount: 36000,
1082
+ swellHeight: 1,
1083
+ current: 0.3,
1084
+ sensitivity: 1,
1085
+ recovery: 6,
1086
+ glowTime: 1,
1087
+ waterClarity: 1,
1088
+ deepColor: '#1a8f8a',
1089
+ moonlight: 0.6,
1090
+ seed: 1,
1091
+ quality: 'medium',
1092
+ pointerMode: 'fish',
1093
+ pointerStrength: 1,
1094
+ pointerRadius: 1,
1095
+ pointerColor: '#dff4ff',
1096
+ trailMode: 'streak',
1097
+ trailLength: 1.5,
1098
+ trailOpacity: 0.6,
1099
+ trailColor: '#2f7dff',
1100
+ trailShare: 1,
1101
+ };
1102
+ /** Knobs that rebuild the simulation: they change on commit, never through the live path. */
1103
+ const STRUCTURAL = ['particleCount', 'seed', 'quality'];
1104
+ /** Knobs an animation (or mouse move) may drive per frame — generated from applyConfig. */
1105
+ const ANIMATABLE = new Set(GENERATIVE_PARTICLES_ANIMATABLE.bioluminescentWake.map((p) => p.value));
1106
+ const BioluminescentWakeParticles = ({ id, size, config, animations, onObjectReady, onObjectDispose }) => {
1107
+ const rootRef = useRef(null);
1108
+ useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
1109
+ const s = size > 0 ? size : 1;
1110
+ const cfg = useMemo(() => ({ ...DEFAULTS, ...config }), [config]);
1111
+ // uniforms outlive the objects, so keyframe animations survive a rebuild
1112
+ const uniforms = useMemo(() => createUniforms(), []);
1113
+ // rebuilt only when a structural knob changes (trails rebuild inside apply())
1114
+ const instance = useMemo(() => build(uniforms, cfg, s), [uniforms, cfg.particleCount, cfg.seed, cfg.quality]);
1115
+ useEffect(() => () => instance.dispose(), [instance]);
1116
+ useLayoutEffect(() => {
1117
+ applyConfig(uniforms, cfg, s);
1118
+ instance.apply(cfg);
1119
+ }, [instance, uniforms, cfg, s]);
1120
+ // Slider drags and keyframe animations are both overrides on the committed config.
1121
+ // This is the one place they are merged and pushed, so they compose instead of
1122
+ // overwriting each other; the animation wins, because it is the active driver.
1123
+ const keyframes = animations?.particles?.animations;
1124
+ const animOverride = useRef({});
1125
+ const animFrom = useRef({});
1126
+ const animStart = useRef(0);
1127
+ // Editing a keyframe restarts its cycle rather than dropping the user mid-way.
1128
+ useEffect(() => { animStart.current = performance.now(); animFrom.current = {}; }, [keyframes]);
1129
+ const push = useCallback((overrides) => {
1130
+ const merged = { ...cfg, ...overrides, ...animOverride.current };
1131
+ const z = merged.size;
1132
+ applyConfig(uniforms, merged, z && z > 0 ? z : s);
1133
+ }, [cfg, uniforms, s]);
1134
+ const live = useConfigLiveHandle(rootRef, id, 'particles', cfg, STRUCTURAL, push);
1135
+ const pointer = usePointerField();
1136
+ useFrame((state, delta) => {
1137
+ instance.update(Math.min(delta, 0.1), state.camera, pointer);
1138
+ if (keyframes?.length) {
1139
+ applyConfigKeyframeAnimations(keyframes, animFrom.current, cfg, animOverride.current, performance.now() - animStart.current, ANIMATABLE);
1140
+ push(live.current);
1141
+ }
1142
+ });
1143
+ return (_jsx("group", { ref: rootRef, children: _jsx("primitive", { object: instance.object }) }));
1144
+ };
1145
+ export default BioluminescentWakeParticles;