@3driseai/3drise-viewer 0.1.19 → 0.1.20

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 (30) hide show
  1. package/dist/components/CreatedObject.d.ts.map +1 -1
  2. package/dist/components/CreatedObject.js +18 -14
  3. package/dist/components/ModelMaterialPassContext.d.ts +7 -0
  4. package/dist/components/ModelMaterialPassContext.d.ts.map +1 -0
  5. package/dist/components/ModelMaterialPassContext.js +7 -0
  6. package/dist/components/ModelPrimitive.d.ts.map +1 -1
  7. package/dist/components/ModelPrimitive.js +7 -1
  8. package/dist/components/ParticlesGenerator.d.ts.map +1 -1
  9. package/dist/components/ParticlesGenerator.js +6 -0
  10. package/dist/components/particles/ChladniSandParticles.d.ts +15 -0
  11. package/dist/components/particles/ChladniSandParticles.d.ts.map +1 -0
  12. package/dist/components/particles/ChladniSandParticles.js +912 -0
  13. package/dist/components/particles/FaradayPowderParticles.d.ts +15 -0
  14. package/dist/components/particles/FaradayPowderParticles.d.ts.map +1 -0
  15. package/dist/components/particles/FaradayPowderParticles.js +783 -0
  16. package/dist/components/particles/NeonRainParticles.d.ts +15 -0
  17. package/dist/components/particles/NeonRainParticles.d.ts.map +1 -0
  18. package/dist/components/particles/NeonRainParticles.js +716 -0
  19. package/dist/components/particles/index.d.ts +6 -0
  20. package/dist/components/particles/index.d.ts.map +1 -1
  21. package/dist/components/particles/index.js +3 -0
  22. package/dist/hooks/index.d.ts +1 -0
  23. package/dist/hooks/index.d.ts.map +1 -1
  24. package/dist/hooks/index.js +1 -0
  25. package/dist/hooks/useMaterialApplication.d.ts.map +1 -1
  26. package/dist/hooks/useMaterialApplication.js +1 -0
  27. package/dist/hooks/useNodeOverrides.d.ts +13 -0
  28. package/dist/hooks/useNodeOverrides.d.ts.map +1 -0
  29. package/dist/hooks/useNodeOverrides.js +254 -0
  30. package/package.json +2 -2
@@ -0,0 +1,716 @@
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
+ * Neon Rain — Night rain lit by neon: Marshall–Palmer drops at terminal velocity, gusting sheets, splash crowns and rim drips, each drop lit like a tiny lens; the cursor is an umbrella, a gust or a headlight. Use for city, noir and cinematic scenes.
13
+ */
14
+ // Neon Rain — rain at night lit by off-screen neon and a streetlamp, filmed at high speed.
15
+ // The object is the rain alone: drops, splash crowns and drips. The lights are not drawn; they only
16
+ // exist as the light the drops scatter.
17
+ // Movement: raindrops with Marshall–Palmer sizes (N(D) ~ exp(-Lambda D), Lambda = 4.1 R^-0.21 /mm)
18
+ // falling at their Atlas terminal velocity v(D) = 9.65 - 10.3 exp(-0.6 D) m/s. Each drop
19
+ // relaxes to the local air velocity with its own drag time tau = v_t / g, so small drops
20
+ // follow the gusts and big ones cut through. The wind is a steady breeze plus travelling
21
+ // gust fronts that sweep sheets of rain across. On the ground plane the splash
22
+ // parameter K = We^1/2 Re^1/4 decides: K > 57.7 throws a crown of secondary droplets
23
+ // (Mundo's threshold), smaller drops just deposit.
24
+ // Colour: drops are tiny water lenses lit by two neon lights and a sodium streetlamp: forward
25
+ // refraction (Henyey–Greenstein lobe), Fresnel reflection, and the primary rainbow caustic
26
+ // at 138 degrees with its spectral split. Out-of-focus drops become bokeh discs (thin lens).
27
+ // Mouse (pointerMode):
28
+ // umbrella — an invisible umbrella: drops hitting the canopy splash off it (outlining its dome),
29
+ // water runs to the rim and drips in heavy streams, leaving a dry column below.
30
+ // gust — the cursor is a gust of wind: a moving-body flow with a wake that bends and swirls
31
+ // the rain sheets (small drops follow, big drops cut through).
32
+ // headlight — an oncoming headlight at the back of the street, placed by the cursor and aimed at
33
+ // the viewer: drops in the beam flare in their forward-scattering lobe.
34
+ // Trails (trailMode): streak = shutter streaks, comet = wet threads, echo = splash afterglow.
35
+ //
36
+ // Generated from the approved lab module (particle lab, batch 3). Sim units = object-local / size.
37
+ // ---------------------------------------------------------------------------------------------
38
+ // Engine features
39
+ // ---------------------------------------------------------------------------------------------
40
+ const QUALITY_SCALE = { low: 0.5, medium: 1, high: 1.6 };
41
+ /** mulberry32 — seeded PRNG, returns a function giving floats in [0,1). */
42
+ function mulberry32(seed) {
43
+ let a = seed >>> 0;
44
+ return function () {
45
+ a = (a + 0x6d2b79f5) >>> 0;
46
+ let t = a;
47
+ t = Math.imul(t ^ (t >>> 15), t | 1);
48
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
49
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
50
+ };
51
+ }
52
+ /** Standard normal from a uniform PRNG (Box–Muller, one value per call). */
53
+ function gauss(rng) {
54
+ const u = Math.max(rng(), 1e-9);
55
+ const v = rng();
56
+ return Math.sqrt(-2 * Math.log(u)) * Math.cos(6.283185307 * v);
57
+ }
58
+ /** Viewport-height hook so point sizes are pixel-correct at any canvas size. */
59
+ function attachViewH(obj, u) {
60
+ const scratch = new THREE.Vector2();
61
+ obj.onBeforeRender = (renderer) => {
62
+ renderer.getDrawingBufferSize(scratch);
63
+ u.uViewH.value = scratch.y;
64
+ };
65
+ }
66
+ function colorUniform(hex) {
67
+ return { value: new THREE.Color(hex) };
68
+ }
69
+ function qualityScale(q) { return QUALITY_SCALE[q] || 1; }
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
+ function createFixedStep(H, maxSteps) {
76
+ let acc = 0;
77
+ return {
78
+ H,
79
+ run(d, step) {
80
+ acc += Math.max(d, 0);
81
+ let k = 0;
82
+ while (acc >= H && k < maxSteps) {
83
+ step(H);
84
+ acc -= H;
85
+ k++;
86
+ }
87
+ if (k === maxSteps)
88
+ acc = Math.min(acc, H);
89
+ return acc / H;
90
+ },
91
+ reset() { acc = 0; },
92
+ };
93
+ }
94
+ function createPickProxy(center, radius) {
95
+ const geo = new THREE.SphereGeometry(1, 16, 10);
96
+ const mat = new THREE.MeshBasicMaterial({ visible: false });
97
+ const mesh = new THREE.Mesh(geo, mat);
98
+ mesh.name = 'pickProxy';
99
+ mesh.position.set(center[0], center[1], center[2]);
100
+ mesh.scale.setScalar(radius);
101
+ return { mesh, dispose() { geo.dispose(); mat.dispose(); } };
102
+ }
103
+ const _frustum = new THREE.Frustum();
104
+ const _pm = new THREE.Matrix4();
105
+ const _sphere = new THREE.Sphere();
106
+ function inView(object, camera, center, radius, size) {
107
+ if (!camera || !camera.projectionMatrix)
108
+ return true;
109
+ object.updateWorldMatrix(true, false);
110
+ _sphere.center.set(center[0], center[1], center[2]).multiplyScalar(size).applyMatrix4(object.matrixWorld);
111
+ _sphere.radius = radius * size * object.matrixWorld.getMaxScaleOnAxis();
112
+ _pm.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
113
+ _frustum.setFromProjectionMatrix(_pm);
114
+ return _frustum.intersectsSphere(_sphere);
115
+ }
116
+ const POINTER_MODES = ['umbrella', 'gust', 'headlight'];
117
+ // ---- world (sim units = metres), laid out round the object origin ----
118
+ const GROUND = -1.0; // road / puddle height
119
+ const TOP = 1.0; // rain enters here
120
+ const XH = 1.3, ZH = 0.85; // half extents of the rain volume
121
+ const ZS = -1.05; // plane of the (off-screen) neon lights, behind the rain
122
+ const SLOWMO = 0.3; // physics seconds per shown second (a high-speed camera)
123
+ const H_STEP = 1 / 60;
124
+ const GRAV = 9.81;
125
+ const TR_MODE = { streak: 0.04, comet: 0.06, echo: 0.08 }; // physics seconds per trail unit
126
+ const TRAIL_GAIN = { streak: 3, comet: 3.5, echo: 4.5 };
127
+ const BOUNDS = { center: [0, 0, 0], radius: 1.75 };
128
+ // neon light sources behind the rain (never drawn): segments [x0, y0, x1, y1, colour index], and a ring [cx, cy, r, colour]
129
+ const SEGS = [[-0.9, -0.35, -0.9, 0.65, 0]];
130
+ const RING = [0.78, 0.2, 0.3, 1];
131
+ const LAMP = [-0.45, 1.35, -0.55];
132
+ // point-light samples of the tubes (for lighting the drops): x, y, colour index, power
133
+ function tubeLights() {
134
+ const L = [];
135
+ for (const s of SEGS) {
136
+ const k = Math.max(2, Math.round(Math.hypot(s[2] - s[0], s[3] - s[1]) / 0.33));
137
+ for (let i = 0; i < k; i++) {
138
+ const t = (i + 0.5) / k;
139
+ L.push([s[0] + (s[2] - s[0]) * t, s[1] + (s[3] - s[1]) * t, s[4], 1 / k * Math.hypot(s[2] - s[0], s[3] - s[1])]);
140
+ }
141
+ }
142
+ for (let i = 0; i < 5; i++) {
143
+ const a = (i / 5) * 6.2831853 + 0.3;
144
+ L.push([RING[0] + RING[2] * Math.cos(a), RING[1] + RING[2] * Math.sin(a), RING[3], 1.0 / 5]); // both lights carry the same total power
145
+ }
146
+ return L;
147
+ }
148
+ const LIGHTS = tubeLights();
149
+ function createUniforms() {
150
+ return {
151
+ uTime: { value: 0 },
152
+ uPhysT: { value: 0 },
153
+ uSize: { value: 1 },
154
+ uViewH: { value: 800 },
155
+ uIntensity: { value: 1 },
156
+ uSpeed: { value: 1 },
157
+ uOpacity: { value: 0.9 },
158
+ uColor: colorUniform('#ffffff'),
159
+ uParticleCount: { value: 0 },
160
+ uRainRate: { value: 18 },
161
+ uWindSpeed: { value: 1.6 },
162
+ uGustiness: { value: 0.6 },
163
+ uNeonColor2: colorUniform('#ffffff'),
164
+ uLampColor: colorUniform('#ffffff'),
165
+ uNeonLight: { value: 1 },
166
+ uAperture: { value: 0.5 },
167
+ uDropSize: { value: 1 },
168
+ uPointerMode: { value: 0 },
169
+ uPointerStrength: { value: 1 },
170
+ uPointerRadius: { value: 1 },
171
+ uPointerColor: colorUniform('#ffffff'),
172
+ uFocus: { value: 3.3 },
173
+ uCamLocal: { value: new THREE.Vector3(0, 0, 3) },
174
+ uHeadPos: { value: new THREE.Vector3() },
175
+ uHeadDir: { value: new THREE.Vector3(0, 0, -1) },
176
+ uHead: { value: 0 }, // headlight power (0 = off)
177
+ uTrailGain: { value: 1 },
178
+ ...createPointerUniforms(),
179
+ ...createTrailUniforms(),
180
+ };
181
+ }
182
+ function applyConfig(u, c, size) {
183
+ u.uSize.value = size;
184
+ u.uIntensity.value = c.intensity;
185
+ u.uSpeed.value = c.speed;
186
+ u.uOpacity.value = c.opacity;
187
+ u.uColor.value.set(c.color);
188
+ u.uParticleCount.value = c.particleCount;
189
+ u.uRainRate.value = c.rainRate;
190
+ u.uWindSpeed.value = c.windSpeed;
191
+ u.uGustiness.value = c.gustiness;
192
+ u.uNeonColor2.value.set(c.neonColor2);
193
+ u.uLampColor.value.set(c.lampColor);
194
+ u.uNeonLight.value = c.neonLight;
195
+ u.uAperture.value = c.aperture;
196
+ u.uDropSize.value = c.dropSize;
197
+ u.uPointerMode.value = POINTER_MODES.indexOf(c.pointerMode); // -1 = 'none' (off)
198
+ u.uPointerStrength.value = c.pointerStrength;
199
+ u.uPointerRadius.value = c.pointerRadius;
200
+ u.uPointerColor.value.set(c.pointerColor);
201
+ applyTrailConfig(u, c);
202
+ }
203
+ const BASE_U = 'uniform float uSize, uViewH, uIntensity, uOpacity;';
204
+ const f3 = (v) => v.toFixed(4);
205
+ // ---- light on a water drop at p seen from the eye: refraction lobe + Fresnel + rainbow ----
206
+ const DROP_LIGHT_GLSL = /* glsl */ `
207
+ uniform vec3 uColor, uNeonColor2, uLampColor, uPointerColor;
208
+ uniform float uNeonLight, uHead;
209
+ uniform vec3 uHeadPos, uHeadDir;
210
+ uniform vec3 uCamLocal;
211
+ // scattering by a water sphere; cosT = cos(scattering angle), 1 = straight through
212
+ vec3 dropPhase(float cosT) {
213
+ float g = 0.75;
214
+ float hg = (1.0 - g * g) / pow(max(1.0 + g * g - 2.0 * g * cosT, 1e-4), 1.5) * 0.08; // refraction lobe
215
+ float refl = 0.06; // external reflection
216
+ // primary rainbow caustic near 138 deg, red outside, violet inside (Airy-ish widths)
217
+ float th = degrees(acos(clamp(cosT, -1.0, 1.0)));
218
+ vec3 bow = vec3(exp(-pow((th - 137.8) / 0.9, 2.0)), exp(-pow((th - 138.7) / 0.9, 2.0)), exp(-pow((th - 139.6) / 0.9, 2.0)));
219
+ bow += 0.25 * smoothstep(138.0, 150.0, th) * (1.0 - smoothstep(150.0, 170.0, th)); // brighter sky inside the bow
220
+ return vec3(hg + refl) + bow * 0.9;
221
+ }
222
+ vec3 pointLight(vec3 p, vec3 e, vec3 L, float pw) {
223
+ vec3 d = p - L;
224
+ float r2 = dot(d, d);
225
+ return vec3(pw / (r2 + 0.25)) * dropPhase(dot(normalize(d), e));
226
+ }
227
+ vec3 dropLight(vec3 p) {
228
+ vec3 e = normalize(uCamLocal - p);
229
+ vec3 acc = vec3(0.0);
230
+ ${LIGHTS.map((l) => ` acc += ${['uColor', 'uNeonColor2'][l[2]]} * pointLight(p, e, vec3(${f3(l[0])}, ${f3(l[1] - 0.25)}, ${f3(ZS)}), ${f3(l[3] * 7.0)}) * uNeonLight;`).join('\n')}
231
+ acc += uLampColor * pointLight(p, e, vec3(${LAMP.map(f3).join(', ')}), 3.0);
232
+ if (uHead > 0.001) {
233
+ vec3 d = p - uHeadPos;
234
+ float r2 = dot(d, d);
235
+ vec3 dn = d * inversesqrt(max(r2, 1e-6));
236
+ float cone = smoothstep(0.7, 0.93, dot(dn, uHeadDir));
237
+ acc += uPointerColor * (uHead * 5.0 * cone / (r2 + 0.05)) * dropPhase(dot(dn, e));
238
+ }
239
+ acc += vec3(0.05, 0.07, 0.12) * 0.8; // city sky glow
240
+ return acc;
241
+ }
242
+ `;
243
+ // ---- drops (rain, splash droplets, rim drips) ----
244
+ const dropVertex = /* glsl */ `
245
+ ${BASE_U}
246
+ ${DROP_LIGHT_GLSL}
247
+ uniform float uAperture, uFocus, uDropSize;
248
+ attribute float aD; // diameter in mm, 0 = not in flight
249
+ attribute float aKind; // 0 rain, 1 splash droplet, 2 rim drip, 3 canopy spray
250
+ varying vec3 vCol;
251
+ varying float vA;
252
+ varying float vBokeh;
253
+ void main() {
254
+ vec3 p = position;
255
+ vec4 mv = modelViewMatrix * vec4(p * uSize, 1.0);
256
+ float dist = -mv.z;
257
+ float alive = step(0.01, aD);
258
+ // fades: entering at the top, at the sides of the volume
259
+ float fade = smoothstep(${f3(TOP)}, ${f3(TOP - 0.2)}, p.y) * smoothstep(${f3(XH)}, ${f3(XH - 0.2)}, abs(p.x)) * smoothstep(${f3(ZH + 0.1)}, ${f3(ZH - 0.1)}, abs(p.z));
260
+ vec3 L = dropLight(p);
261
+ // a drop's light scales with its cross-section, gently (the sprites are much bigger than drops)
262
+ float s = sqrt(clamp(aD, 0.2, 5.0));
263
+ float lum = dot(L, vec3(0.3, 0.5, 0.2));
264
+ float basePx = uDropSize * uSize * uViewH * (0.0035 + 0.002 * s) * (1.0 + 0.6 * min(lum, 2.5)) / dist;
265
+ float coc = uAperture * uViewH * 0.05 * abs(dist - uFocus) / max(dist, 0.05);
266
+ float px = sqrt(basePx * basePx + coc * coc);
267
+ float spread = clamp((basePx * basePx) / (px * px), 0.02, 1.0);
268
+ vBokeh = smoothstep(4.0, 14.0, coc);
269
+ vCol = (L * (0.6 + 0.4 * s) + uPointerColor * (step(2.5, aKind) * 1.4 + step(1.5, aKind) * (1.0 - step(2.5, aKind)) * 0.5)) * uIntensity;
270
+ vA = uOpacity * alive * fade * mix(1.0, spread * 2.5, vBokeh);
271
+ gl_PointSize = clamp(px, 1.0, 72.0) * step(0.001, vA);
272
+ gl_Position = projectionMatrix * mv;
273
+ }
274
+ `;
275
+ const dropFragment = /* glsl */ `
276
+ varying vec3 vCol;
277
+ varying float vA;
278
+ varying float vBokeh;
279
+ void main() {
280
+ vec2 d = gl_PointCoord * 2.0 - 1.0;
281
+ float r2 = dot(d, d);
282
+ if (r2 > 1.0) discard;
283
+ float core = (exp(-r2 * 7.0) + 0.2 * exp(-r2 * 2.0)) * (1.0 - r2);
284
+ float r = sqrt(r2);
285
+ float disc = (1.0 - smoothstep(0.8, 1.0, r)) * (0.65 + 0.35 * smoothstep(0.5, 0.95, r));
286
+ float a = mix(core, disc * 0.45, vBokeh) * vA;
287
+ if (a < 0.002) discard;
288
+ gl_FragColor = vec4(vCol, a);
289
+ }
290
+ `;
291
+ function build(u, c, size) {
292
+ const q = qualityScale(c.quality);
293
+ const nr = Math.max(200, Math.round(c.particleCount * q));
294
+ const ns = Math.round(nr * 0.55);
295
+ const n = nr + ns;
296
+ const rng = mulberry32(seedMix(0x7a1f, c.seed));
297
+ const pos = new Float32Array(n * 3), vel = new Float32Array(n * 3);
298
+ const Dm = new Float32Array(n); // diameter (mm); 0 = not in flight
299
+ const vt = new Float32Array(n); // terminal velocity (m/s)
300
+ const kind = new Uint8Array(n); // 0 rain, 1 splash droplet, 2 rim drip
301
+ const K = new Float32Array(n); // trail brightness weight
302
+ const kindF = new Float32Array(n); // kind, as a vertex attribute
303
+ let trail = null;
304
+ const atlas = (D) => Math.max(0.3, 9.65 - 10.3 * Math.exp(-0.6 * D));
305
+ function sampleD() {
306
+ const R = Math.max(u.uRainRate.value, 0.5);
307
+ const lam = 4.1 * Math.pow(R, -0.21);
308
+ let D = 0;
309
+ do {
310
+ D = -Math.log(Math.max(rng(), 1e-9)) / lam;
311
+ } while (D < 0.3 || D > 5.5);
312
+ return D;
313
+ }
314
+ // wind: breeze + travelling gust fronts + two slow eddies (divergence-free in x-y)
315
+ let physT = 0;
316
+ const wind = new Float32Array(3);
317
+ function windAt(x, y, z) {
318
+ const U = u.uWindSpeed.value, G = u.uGustiness.value;
319
+ const xi = x - U * physT * 0.8;
320
+ const front = Math.sin(0.9 * xi + 1.3) * Math.sin(0.31 * physT + 0.7 * z) + 0.5 * Math.sin(2.1 * xi - 0.4 * physT + 1.9);
321
+ const ex = Math.sin(2.3 * y + 0.9 * physT) * Math.cos(1.7 * x), ey = -Math.cos(2.3 * y + 0.9 * physT) * Math.sin(1.7 * x) * (1.7 / 2.3);
322
+ wind[0] = U * (1 + 0.55 * G * front) + 0.6 * G * ex;
323
+ wind[1] = 0.6 * G * ey;
324
+ wind[2] = 0.35 * G * Math.sin(1.1 * xi + 0.5 * physT);
325
+ }
326
+ function spawnRain(i, anywhere) {
327
+ const j = i * 3;
328
+ const D = sampleD();
329
+ Dm[i] = D;
330
+ vt[i] = atlas(D);
331
+ kind[i] = 0;
332
+ kindF[i] = 0;
333
+ const U = u.uWindSpeed.value;
334
+ const fall = (TOP - GROUND) / vt[i];
335
+ // spawn upwind of the volume so the slanted rain fills it; rain sheets: accept by a travelling band
336
+ let x = 0, zz = 0;
337
+ for (let tries = 0; tries < 4; tries++) {
338
+ const drift = Math.max(-0.45, Math.min(0.45, U * fall * 0.6));
339
+ x = -XH + rng() * 2 * XH - drift * rng() * (anywhere ? 0 : 1);
340
+ zz = (rng() * 2 - 1) * ZH;
341
+ const band = 0.55 + 0.45 * Math.sin(1.4 * (x - U * physT * 0.8) + 0.9 * Math.sin(0.6 * zz + 0.2 * physT));
342
+ if (rng() < band)
343
+ break;
344
+ }
345
+ pos[j] = x;
346
+ pos[j + 2] = zz;
347
+ pos[j + 1] = anywhere ? GROUND + rng() * (TOP - GROUND) : TOP + 0.02 * rng();
348
+ vel[j] = U;
349
+ vel[j + 1] = -vt[i];
350
+ vel[j + 2] = 0;
351
+ K[i] = Math.sqrt(D);
352
+ if (trail)
353
+ trail.reset(i);
354
+ }
355
+ let sHead = 0;
356
+ function spawnSplash(x, y, z, vx, vy, vz, D, kd) {
357
+ const i = nr + (sHead % ns);
358
+ sHead++;
359
+ const j = i * 3;
360
+ pos[j] = x;
361
+ pos[j + 1] = y;
362
+ pos[j + 2] = z;
363
+ vel[j] = vx;
364
+ vel[j + 1] = vy;
365
+ vel[j + 2] = vz;
366
+ Dm[i] = D;
367
+ vt[i] = atlas(D);
368
+ kind[i] = kd;
369
+ kindF[i] = kd;
370
+ K[i] = Math.sqrt(D) * (kd === 1 || kd === 3 ? 1.4 : 1);
371
+ if (trail)
372
+ trail.reset(i);
373
+ }
374
+ for (let i = 0; i < nr; i++)
375
+ spawnRain(i, true);
376
+ const hp = new THREE.Vector3(), hv = new THREE.Vector3();
377
+ let emitAcc = 0, dripAcc = 0;
378
+ const hr = mulberry32(seedMix(0x5b1a, c.seed));
379
+ function step(hShown) {
380
+ const h = hShown * SLOWMO;
381
+ physT += h;
382
+ u.uPhysT.value = physT;
383
+ const mode = u.uPointerMode.value;
384
+ const S = u.uPointerStrength.value, act = u.uPointerActive.value, Rr = u.uPointerRadius.value;
385
+ hp.copy(u.uPointer.value);
386
+ hv.copy(u.uPointerVel.value);
387
+ const useUmb = mode === 0 && S > 0.01 && act > 0.02;
388
+ const useGust = mode === 1 && S > 0.01 && act > 0.01;
389
+ // umbrella: a dome of radius RU at the cursor (hand height), canopy apex a little above the rim
390
+ const RU = 0.42 * Rr * Math.min(Math.max(S, 0.3), 2);
391
+ const ux = hp.x, uy = Math.max(Math.min(hp.y, TOP - 0.1), GROUND + 0.4), uz = Math.max(Math.min(hp.z, ZH), -ZH);
392
+ // gust: a moving body in the air, velocity in physics time (a hand sweep is a strong gust)
393
+ const GS = 0.3 * Rr;
394
+ const AIR = 3.0 * S * act / SLOWMO;
395
+ const bvx = hv.x * AIR, bvy = hv.y * AIR, bvz = hv.z * AIR;
396
+ const bsp = Math.hypot(bvx, bvy, bvz), bn = bsp > 1e-4 ? 1 / bsp : 0;
397
+ const gustOn = useGust && bsp > 0.05;
398
+ const tauMin = 0.02;
399
+ // emission keeps the pool's live share tied to the rain rate
400
+ const liveShare = Math.min(1, 0.3 + u.uRainRate.value / 30);
401
+ emitAcc += (liveShare * nr * h) / ((TOP - GROUND) / 5.0);
402
+ let intercepted = 0;
403
+ for (let i = 0; i < n; i++) {
404
+ if (Dm[i] === 0) {
405
+ if (i < nr && emitAcc >= 1) {
406
+ emitAcc -= 1;
407
+ spawnRain(i, false);
408
+ }
409
+ continue;
410
+ }
411
+ const j = i * 3;
412
+ let x = pos[j], y = pos[j + 1], z = pos[j + 2];
413
+ let vx = vel[j], vy = vel[j + 1], vz = vel[j + 2];
414
+ windAt(x, y, z);
415
+ let ax = wind[0], ay = wind[1], az = wind[2];
416
+ if (gustOn) {
417
+ const qx = x - hp.x, qy = y - hp.y, qz = z - hp.z;
418
+ const r2 = qx * qx + qy * qy + qz * qz;
419
+ if (r2 < 49 * GS * GS) {
420
+ const r = Math.sqrt(r2) + 1e-6;
421
+ if (r > GS) {
422
+ const k3 = (GS * GS * GS) / (2 * r2 * r);
423
+ const vr = (bvx * qx + bvy * qy + bvz * qz) / r;
424
+ ax += k3 * (3 * vr * qx / r - bvx);
425
+ ay += k3 * (3 * vr * qy / r - bvy);
426
+ az += k3 * (3 * vr * qz / r - bvz);
427
+ }
428
+ else {
429
+ ax += bvx;
430
+ ay += bvy;
431
+ az += bvz;
432
+ }
433
+ const behind = -(qx * bvx + qy * bvy + qz * bvz) * bn;
434
+ if (behind > 0) {
435
+ const perp2 = r2 - behind * behind;
436
+ const wk = Math.exp(-perp2 / (GS * GS * 1.5) - behind / (6 * GS));
437
+ // the wake rolls up: a swirl about the gust direction
438
+ ax += bvx * wk * 0.7 + (qy * bvz - qz * bvy) * bn * wk * 2.0;
439
+ ay += bvy * wk * 0.7 + (qz * bvx - qx * bvz) * bn * wk * 2.0;
440
+ az += bvz * wk * 0.7 + (qx * bvy - qy * bvx) * bn * wk * 2.0;
441
+ }
442
+ }
443
+ }
444
+ // drag relaxation toward the air + terminal fall: tau = v_t / g (exact exponential update)
445
+ const tau = Math.max(vt[i] / GRAV, tauMin);
446
+ const dcy = Math.exp(-h / tau);
447
+ vx = ax + (vx - ax) * dcy;
448
+ vz = az + (vz - az) * dcy;
449
+ vy = (ay - vt[i]) + (vy - (ay - vt[i])) * dcy;
450
+ const py = y;
451
+ x += vx * h;
452
+ y += vy * h;
453
+ z += vz * h;
454
+ // umbrella canopy
455
+ if (useUmb && (kind[i] === 0 || kind[i] === 2) && py > uy) {
456
+ const dx = x - ux, dz = z - uz;
457
+ const rr2 = dx * dx + dz * dz;
458
+ if (rr2 < RU * RU) {
459
+ const cy = uy + 0.16 * RU * (1 - rr2 / (RU * RU));
460
+ if (y <= cy) {
461
+ const rr = Math.sqrt(rr2) + 1e-4;
462
+ intercepted += Dm[i] * Dm[i] * Dm[i];
463
+ // the drop shatters on the fabric: small droplets fly off along the dome's slope
464
+ const nSpray = Dm[i] > 0.6 ? 2 : 1;
465
+ for (let k = 0; k < nSpray; k++) {
466
+ const out = 0.6 + 1.4 * hr(), a = (hr() - 0.5) * 1.2;
467
+ const ox = dx / rr * Math.cos(a) - dz / rr * Math.sin(a), oz = dz / rr * Math.cos(a) + dx / rr * Math.sin(a);
468
+ spawnSplash(x, cy + 0.004, z, ox * out + vx * 0.2, 0.5 + 1.0 * hr(), oz * out + vz * 0.2, 0.5 + 0.5 * hr(), 3);
469
+ }
470
+ Dm[i] = 0;
471
+ pos[j] = x;
472
+ pos[j + 1] = cy;
473
+ pos[j + 2] = z;
474
+ vel[j] = vel[j + 1] = vel[j + 2] = 0;
475
+ continue;
476
+ }
477
+ }
478
+ }
479
+ // the road: splash (K = We^1/2 Re^1/4 > 57.7) or deposit
480
+ if (y <= GROUND) {
481
+ if (Math.abs(x) < XH + 0.05 && Math.abs(z) < ZH + 0.15 && (kind[i] === 0 || kind[i] === 2)) {
482
+ const v = Math.hypot(vx, vy, vz), D = Dm[i] * 1e-3;
483
+ const We = (1000 * v * v * D) / 0.072, Re = (v * D) / 1e-6;
484
+ const Ks = Math.sqrt(We) * Math.pow(Re, 0.25);
485
+ if (Ks > 57.7) {
486
+ const m = 2 + Math.min(5, Math.floor((Ks - 57.7) / 90));
487
+ const vs = 0.22 * v;
488
+ for (let k = 0; k < m; k++) {
489
+ if (hr() > 0.7)
490
+ continue;
491
+ const a = 6.2831853 * hr(), el = 0.6 + 0.5 * hr();
492
+ const sp = vs * (0.6 + 0.8 * hr());
493
+ spawnSplash(x, GROUND + 0.002, z, Math.cos(a) * Math.cos(el) * sp, Math.sin(el) * sp, Math.sin(a) * Math.cos(el) * sp, Dm[i] * (0.2 + 0.15 * hr()), 1);
494
+ }
495
+ // Worthington jet: one droplet straight up
496
+ if (Dm[i] > 2 && hr() < 0.5)
497
+ spawnSplash(x, GROUND + 0.003, z, vx * 0.05, 0.3 * v, vz * 0.05, Dm[i] * 0.45, 1);
498
+ }
499
+ }
500
+ Dm[i] = 0;
501
+ pos[j] = x;
502
+ pos[j + 1] = GROUND;
503
+ pos[j + 2] = z;
504
+ vel[j] = vel[j + 1] = vel[j + 2] = 0;
505
+ continue;
506
+ }
507
+ if (Math.abs(x) > XH + 0.45 || Math.abs(z) > ZH + 0.5 || y > TOP + 0.5) {
508
+ // left the street: parked on the road (hidden) until re-emitted
509
+ Dm[i] = 0;
510
+ vel[j] = vel[j + 1] = vel[j + 2] = 0;
511
+ pos[j] = 0;
512
+ pos[j + 1] = GROUND;
513
+ pos[j + 2] = 0;
514
+ continue;
515
+ }
516
+ pos[j] = x;
517
+ pos[j + 1] = y;
518
+ pos[j + 2] = z;
519
+ vel[j] = vx;
520
+ vel[j + 1] = vy;
521
+ vel[j + 2] = vz;
522
+ }
523
+ if (emitAcc > 50)
524
+ emitAcc = 50;
525
+ // rim drips: intercepted water runs to the rim and leaves as heavy drops
526
+ if (useUmb) {
527
+ dripAcc += intercepted / 8;
528
+ while (dripAcc >= 1) {
529
+ dripAcc -= 1;
530
+ const a = 6.2831853 * hr();
531
+ spawnSplash(ux + RU * 1.01 * Math.cos(a), uy - 0.005, uz + RU * 1.01 * Math.sin(a), 0, -0.2, 0, 3.2 + 1.2 * hr(), 2);
532
+ }
533
+ }
534
+ if (dripAcc > 20)
535
+ dripAcc = 20;
536
+ }
537
+ const stepper = createFixedStep(H_STEP, 4);
538
+ for (let s = 0; s < 90; s++)
539
+ step(H_STEP);
540
+ // ---- geometry ----
541
+ const geo = new THREE.BufferGeometry();
542
+ const posAttr = new THREE.BufferAttribute(pos, 3).setUsage(THREE.DynamicDrawUsage);
543
+ const dAttr = new THREE.BufferAttribute(Dm, 1).setUsage(THREE.DynamicDrawUsage);
544
+ geo.setAttribute('position', posAttr);
545
+ geo.setAttribute('aD', dAttr);
546
+ const kAttr = new THREE.BufferAttribute(kindF, 1).setUsage(THREE.DynamicDrawUsage);
547
+ geo.setAttribute('aKind', kAttr);
548
+ const mat = new THREE.ShaderMaterial({
549
+ uniforms: u, vertexShader: dropVertex, fragmentShader: dropFragment,
550
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
551
+ });
552
+ const drops = new THREE.Points(geo, mat);
553
+ drops.frustumCulled = false;
554
+ attachViewH(drops, u);
555
+ const object = new THREE.Group();
556
+ const proxy = createPickProxy(BOUNDS.center, BOUNDS.radius);
557
+ object.add(drops, proxy.mesh);
558
+ const anchor = new THREE.Vector3(0, 0, 0.25);
559
+ const anchorHead = new THREE.Vector3(0, 0, -0.55); // the headlight sits at the back of the street
560
+ const focusPt = new THREE.Vector3();
561
+ const camDir = new THREE.Vector3();
562
+ // ---- trails: every sample lit by the same lights as the drops ----
563
+ const trailGLSL = /* glsl */ `
564
+ ${DROP_LIGHT_GLSL}
565
+ uniform float uTrailGain;
566
+ vec3 trailShade(vec3 p, float f) {
567
+ float fade = smoothstep(${f3(TOP)}, ${f3(TOP - 0.2)}, p.y) * step(${f3(GROUND)}, p.y);
568
+ return dropLight(p) * (0.5 + 0.5 * TRAIL_K) * 0.55 * uTrailGain * fade;
569
+ }
570
+ vec3 trailView(vec3 mv, out float mag) { mag = 1.0; return mv; }
571
+ `;
572
+ const materials = [mat];
573
+ let trailKey = '', trailTR = TR_MODE.streak;
574
+ function buildTrails(mode, share) {
575
+ trailTR = TR_MODE[mode] || TR_MODE.streak;
576
+ const key = mode + ':' + share;
577
+ if (key === trailKey)
578
+ return;
579
+ trailKey = key;
580
+ if (trail) {
581
+ object.remove(trail.object);
582
+ trail.dispose();
583
+ trail = null;
584
+ }
585
+ materials.length = 1;
586
+ u.uTrailGain.value = TRAIL_GAIN[mode] || 1;
587
+ trail = createCpuTrail(mode, { u, n, pos, vel, k: K, glsl: trailGLSL, sizePx: 0.0055, spacing: 0.004, sub: 8 });
588
+ if (!trail)
589
+ return;
590
+ object.add(trail.object);
591
+ materials.push(...trail.materials);
592
+ }
593
+ buildTrails(c.trailMode, c.trailShare);
594
+ function upload() { posAttr.needsUpdate = true; dAttr.needsUpdate = true; kAttr.needsUpdate = true; }
595
+ return {
596
+ object,
597
+ materials,
598
+ apply(cfg) { if (cfg)
599
+ buildTrails(cfg.trailMode, cfg.trailShare); },
600
+ update(dt, camera, pointer) {
601
+ const rdt = Math.min(Math.max(dt, 0), 1 / 20);
602
+ if (camera && !inView(object, camera, BOUNDS.center, BOUNDS.radius, u.uSize.value))
603
+ return;
604
+ const d = rdt * u.uSpeed.value;
605
+ u.uTime.value += d;
606
+ if (camera) {
607
+ object.localToWorld(focusPt.set(0, 0, 0));
608
+ u.uFocus.value = camera.position.distanceTo(focusPt);
609
+ object.worldToLocal(u.uCamLocal.value.copy(camera.position)).divideScalar(u.uSize.value);
610
+ if (pointer)
611
+ stepPointerField(u, pointer, camera, object, u.uSize.value, rdt, u.uPointerMode.value === 2 ? anchorHead : anchor);
612
+ // headlight: an oncoming car at the back of the street, aimed at the viewer and tipped down,
613
+ // so the drops between it and the eye flare in the forward-scattering lobe
614
+ const on = u.uPointerMode.value === 2 ? u.uPointerStrength.value * u.uPointerActive.value : 0;
615
+ u.uHead.value = on;
616
+ if (on > 0) {
617
+ camDir.copy(u.uCamLocal.value).sub(u.uPointer.value).normalize();
618
+ camDir.y -= 0.12;
619
+ camDir.normalize();
620
+ u.uHeadDir.value.copy(camDir);
621
+ u.uHeadPos.value.copy(u.uPointer.value);
622
+ }
623
+ }
624
+ u.uVelScale.value = trailTR;
625
+ let simD = 0;
626
+ stepper.run(d, (hh) => { step(hh); simD += hh; });
627
+ if (simD > 0) {
628
+ upload();
629
+ if (trail)
630
+ trail.update((simD * SLOWMO) / trailTR);
631
+ }
632
+ },
633
+ seek(t) { for (let s = 0; s < Math.round(t / H_STEP); s++)
634
+ step(H_STEP); upload(); },
635
+ dispose() {
636
+ geo.dispose();
637
+ mat.dispose();
638
+ proxy.dispose();
639
+ if (trail)
640
+ trail.dispose();
641
+ },
642
+ _state: { pos, vel, Dm, kind },
643
+ };
644
+ }
645
+ /** Defaults for every knob, so an older saved config still renders (identical to the catalog entry). */
646
+ const DEFAULTS = {
647
+ intensity: 1,
648
+ speed: 1,
649
+ opacity: 0.9,
650
+ size: 1,
651
+ color: '#ff2f9a',
652
+ particleCount: 10000,
653
+ rainRate: 18,
654
+ windSpeed: 0.8,
655
+ gustiness: 0.6,
656
+ neonColor2: '#22dcff',
657
+ lampColor: '#ffae52',
658
+ neonLight: 1,
659
+ aperture: 0.5,
660
+ dropSize: 1,
661
+ seed: 1,
662
+ quality: 'medium',
663
+ pointerMode: 'umbrella',
664
+ pointerStrength: 1,
665
+ pointerRadius: 1,
666
+ pointerColor: '#eaf2ff',
667
+ trailMode: 'streak',
668
+ trailLength: 1.2,
669
+ trailOpacity: 0.7,
670
+ trailColor: '#9fb8ff',
671
+ trailShare: 0.6,
672
+ };
673
+ /** Knobs that rebuild the simulation: they change on commit, never through the live path. */
674
+ const STRUCTURAL = ['particleCount', 'seed', 'quality'];
675
+ /** Knobs an animation (or mouse move) may drive per frame — generated from applyConfig. */
676
+ const ANIMATABLE = new Set(GENERATIVE_PARTICLES_ANIMATABLE.neonRain.map((p) => p.value));
677
+ const NeonRainParticles = ({ id, size, config, animations, onObjectReady, onObjectDispose }) => {
678
+ const rootRef = useRef(null);
679
+ useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
680
+ const s = size > 0 ? size : 1;
681
+ const cfg = useMemo(() => ({ ...DEFAULTS, ...config }), [config]);
682
+ // uniforms outlive the objects, so keyframe animations survive a rebuild
683
+ const uniforms = useMemo(() => createUniforms(), []);
684
+ // rebuilt only when a structural knob changes (trails rebuild inside apply())
685
+ const instance = useMemo(() => build(uniforms, cfg, s), [uniforms, cfg.particleCount, cfg.seed, cfg.quality]);
686
+ useEffect(() => () => instance.dispose(), [instance]);
687
+ useLayoutEffect(() => {
688
+ applyConfig(uniforms, cfg, s);
689
+ instance.apply(cfg);
690
+ }, [instance, uniforms, cfg, s]);
691
+ // Slider drags and keyframe animations are both overrides on the committed config.
692
+ // This is the one place they are merged and pushed, so they compose instead of
693
+ // overwriting each other; the animation wins, because it is the active driver.
694
+ const keyframes = animations?.particles?.animations;
695
+ const animOverride = useRef({});
696
+ const animFrom = useRef({});
697
+ const animStart = useRef(0);
698
+ // Editing a keyframe restarts its cycle rather than dropping the user mid-way.
699
+ useEffect(() => { animStart.current = performance.now(); animFrom.current = {}; }, [keyframes]);
700
+ const push = useCallback((overrides) => {
701
+ const merged = { ...cfg, ...overrides, ...animOverride.current };
702
+ const z = merged.size;
703
+ applyConfig(uniforms, merged, z && z > 0 ? z : s);
704
+ }, [cfg, uniforms, s]);
705
+ const live = useConfigLiveHandle(rootRef, id, 'particles', cfg, STRUCTURAL, push);
706
+ const pointer = usePointerField();
707
+ useFrame((state, delta) => {
708
+ instance.update(Math.min(delta, 0.1), state.camera, pointer);
709
+ if (keyframes?.length) {
710
+ applyConfigKeyframeAnimations(keyframes, animFrom.current, cfg, animOverride.current, performance.now() - animStart.current, ANIMATABLE);
711
+ push(live.current);
712
+ }
713
+ });
714
+ return (_jsx("group", { ref: rootRef, children: _jsx("primitive", { object: instance.object }) }));
715
+ };
716
+ export default NeonRainParticles;