@wave3d/core 0.8.0 → 0.10.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.
- package/README.md +3 -1
- package/dist/config/model.d.ts +48 -2
- package/dist/config/model.js +8 -1
- package/dist/config/model.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/renderer/WaveRenderer.d.ts +142 -10
- package/dist/renderer/WaveRenderer.js +209 -53
- package/dist/renderer/WaveRenderer.js.map +1 -1
- package/dist/renderer/WaveRendererGPU.js +291 -0
- package/dist/renderer/WaveRendererGPU.js.map +1 -0
- package/dist/renderer/gpu-loader.js +2 -0
- package/dist/renderer/index.d.ts +3 -2
- package/dist/renderer/interaction.d.ts +29 -0
- package/dist/renderer/interaction.js +77 -31
- package/dist/renderer/interaction.js.map +1 -1
- package/dist/renderer/interactionGates.js +59 -0
- package/dist/renderer/interactionGates.js.map +1 -0
- package/dist/renderer/particleField.d.ts +1 -59
- package/dist/renderer/particleField.js +49 -29
- package/dist/renderer/particleField.js.map +1 -1
- package/dist/renderer/particleFieldGPU.js +141 -0
- package/dist/renderer/particleFieldGPU.js.map +1 -0
- package/dist/renderer/tilt.d.ts +17 -0
- package/dist/renderer/tilt.js +124 -0
- package/dist/renderer/tilt.js.map +1 -0
- package/dist/renderer/tsl/color.js +129 -0
- package/dist/renderer/tsl/color.js.map +1 -0
- package/dist/renderer/tsl/noise.js +83 -0
- package/dist/renderer/tsl/noise.js.map +1 -0
- package/dist/renderer/tsl/packedArray.js +76 -0
- package/dist/renderer/tsl/packedArray.js.map +1 -0
- package/dist/renderer/tsl/particleMaterial.js +133 -0
- package/dist/renderer/tsl/particleMaterial.js.map +1 -0
- package/dist/renderer/tsl/particleUniforms.js +44 -0
- package/dist/renderer/tsl/particleUniforms.js.map +1 -0
- package/dist/renderer/tsl/pointerField.js +73 -0
- package/dist/renderer/tsl/pointerField.js.map +1 -0
- package/dist/renderer/tsl/post.js +63 -0
- package/dist/renderer/tsl/post.js.map +1 -0
- package/dist/renderer/tsl/postChain.js +56 -0
- package/dist/renderer/tsl/postChain.js.map +1 -0
- package/dist/renderer/tsl/postEffects.js +221 -0
- package/dist/renderer/tsl/postEffects.js.map +1 -0
- package/dist/renderer/tsl/types.js +28 -0
- package/dist/renderer/tsl/types.js.map +1 -0
- package/dist/renderer/tsl/uniforms.js +152 -0
- package/dist/renderer/tsl/uniforms.js.map +1 -0
- package/dist/renderer/tsl/waveMaterial.js +185 -0
- package/dist/renderer/tsl/waveMaterial.js.map +1 -0
- package/dist/renderer/tsl/waveShape.js +108 -0
- package/dist/renderer/tsl/waveShape.js.map +1 -0
- package/dist/shell/createWave.d.ts +29 -0
- package/dist/shell/createWave.js +39 -11
- package/dist/shell/createWave.js.map +1 -1
- package/dist/shell/probe.js +18 -1
- package/dist/shell/probe.js.map +1 -1
- package/dist/standalone/wave3d.standalone.js +1811 -1594
- package/dist/standalone/wave3d.standalone.webgpu.js +36647 -0
- package/dist/standalone.d.ts +10 -3
- package/dist/standalone.js +10 -3
- package/dist/standalone.js.map +1 -1
- package/dist/studio/StudioWaveRenderer.d.ts +4 -0
- package/dist/studio/StudioWaveRenderer.js +9 -0
- package/dist/studio/StudioWaveRenderer.js.map +1 -1
- package/dist/studio/StudioWaveRendererGPU.js +18 -0
- package/dist/studio/StudioWaveRendererGPU.js.map +1 -0
- package/dist/studio/index.d.ts +12 -2
- package/dist/studio/index.js +14 -1
- package/dist/studio/index.js.map +1 -0
- package/package.json +12 -3
- package/skills/wave3d/SKILL.md +29 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ensureStudioConfig } from "../config/model.js";
|
|
2
2
|
import { WaveGeometry } from "./WaveGeometry.js";
|
|
3
3
|
import { ditherFragmentShader, fragmentShader, halftoneCmykFragmentShader, halftoneFragmentShader, heatmapFragmentShader, innerLightFragmentShader, lineFragmentShader, paperTextureFragmentShader, postFragmentShader, postVertexShader, vertexShader } from "./shaders.js";
|
|
4
|
-
import {
|
|
4
|
+
import { anyPointerFxActive, interactionActive, wavePointerFxActive, waveRipplesActive } from "./interactionGates.js";
|
|
5
5
|
import { ParticleField } from "./particleField.js";
|
|
6
6
|
import { PALETTE_MAPS, buildBackgroundGradientCanvas, buildBackgroundImageCanvas, buildBackgroundMeshCanvas, buildPaletteTexture, canvasToTexture, configurePaletteTexture, drawBackgroundMediaFrame, loadPaletteImage, paletteMapCanvas, paletteSignature } from "./palette.js";
|
|
7
7
|
import { buildHeroPaletteCanvas, buildHeroPaletteTexture } from "./heroPalette.js";
|
|
@@ -174,6 +174,12 @@ function hexToLinearVec3(hex, target) {
|
|
|
174
174
|
*/
|
|
175
175
|
var WaveRenderer = class {
|
|
176
176
|
renderer;
|
|
177
|
+
/**
|
|
178
|
+
* Whether this renderer can draw yet. The WebGL backend is ready the moment it is constructed;
|
|
179
|
+
* the WebGPU subclass has to `await renderer.init()` first, and `renderOnce()` throws before that.
|
|
180
|
+
* The base class is always ready, so the WebGL path is unaffected.
|
|
181
|
+
*/
|
|
182
|
+
ready = true;
|
|
177
183
|
scene = new THREE.Scene();
|
|
178
184
|
camera;
|
|
179
185
|
group = new THREE.Group();
|
|
@@ -211,8 +217,17 @@ var WaveRenderer = class {
|
|
|
211
217
|
clipSphere = new THREE.Sphere();
|
|
212
218
|
clipTmpA = new THREE.Vector3();
|
|
213
219
|
clipTmpB = new THREE.Vector3();
|
|
214
|
-
/** Created by
|
|
220
|
+
/** Created by loadInteraction() once the runtime chunk lands, disposed when interaction turns off.
|
|
221
|
+
* Stays undefined for the frames between the config turning it on and the chunk arriving. */
|
|
215
222
|
interaction;
|
|
223
|
+
/** The chunk itself, kept for the applier tables. Set and cleared with `interaction`. */
|
|
224
|
+
interactionModule;
|
|
225
|
+
/** A fetch already in flight, so a per-frame refresh can't start a second one. */
|
|
226
|
+
interactionLoading = false;
|
|
227
|
+
/** setInteractionInput() calls made while the chunk was in flight, replayed once it lands. */
|
|
228
|
+
stagedInputs;
|
|
229
|
+
/** Set by dispose(), so an in-flight chunk can't attach listeners to a dead renderer. */
|
|
230
|
+
disposed = false;
|
|
216
231
|
/** Extra ortho-zoom MULTIPLIER from a cameraZoom binding (1 = none); applied in applyZoom().
|
|
217
232
|
* Protected so the studio's writeCameraToConfig() can divide it back out (keep it out of config). */
|
|
218
233
|
interactionZoom = 1;
|
|
@@ -250,12 +265,7 @@ var WaveRenderer = class {
|
|
|
250
265
|
this.config = ensureStudioConfig(config);
|
|
251
266
|
this.respectReducedMotion = options.respectReducedMotion ?? true;
|
|
252
267
|
this.skipIntroRamp = options.skipIntroRamp ?? false;
|
|
253
|
-
this.renderer =
|
|
254
|
-
antialias: true,
|
|
255
|
-
alpha: true,
|
|
256
|
-
preserveDrawingBuffer: true,
|
|
257
|
-
powerPreference: "high-performance"
|
|
258
|
-
});
|
|
268
|
+
this.renderer = this.createRenderer();
|
|
259
269
|
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
260
270
|
this.renderer.setClearColor(0, 0);
|
|
261
271
|
container.appendChild(this.renderer.domElement);
|
|
@@ -301,6 +311,24 @@ var WaveRenderer = class {
|
|
|
301
311
|
this.buildWaves();
|
|
302
312
|
this.resize();
|
|
303
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Build the backing three renderer. Overridden by the WebGPU subclass; called from the
|
|
316
|
+
* constructor, so it must not depend on subclass FIELDS (prototype methods are available during
|
|
317
|
+
* `super()`, field initialisers are not).
|
|
318
|
+
*/
|
|
319
|
+
createRenderer() {
|
|
320
|
+
return new THREE.WebGLRenderer({
|
|
321
|
+
antialias: true,
|
|
322
|
+
alpha: true,
|
|
323
|
+
preserveDrawingBuffer: true,
|
|
324
|
+
powerPreference: "high-performance"
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Await any asynchronous backend startup. A no-op on WebGL; the WebGPU subclass overrides it to
|
|
329
|
+
* `await renderer.init()` and then draw the first frame. Safe to call more than once.
|
|
330
|
+
*/
|
|
331
|
+
async init() {}
|
|
304
332
|
get segments() {
|
|
305
333
|
const q = this.config.quality / Math.sqrt(Math.max(1, this.config.waves.length));
|
|
306
334
|
return THREE.MathUtils.clamp(Math.round(BASE_SEGMENTS * q), 24, 360);
|
|
@@ -464,16 +492,7 @@ var WaveRenderer = class {
|
|
|
464
492
|
addWave() {
|
|
465
493
|
const geo = new WaveGeometry(this.segments);
|
|
466
494
|
const sc = this.config.waves[this.waves.length] ?? this.config.waves[0];
|
|
467
|
-
const material =
|
|
468
|
-
uniforms: this.makeUniforms(),
|
|
469
|
-
defines: this.waveDefines(sc),
|
|
470
|
-
vertexShader,
|
|
471
|
-
fragmentShader: sc?.theme === "wireframe" ? lineFragmentShader : fragmentShader,
|
|
472
|
-
transparent: true,
|
|
473
|
-
depthTest: true,
|
|
474
|
-
depthWrite: true,
|
|
475
|
-
side: THREE.DoubleSide
|
|
476
|
-
});
|
|
495
|
+
const material = this.createWaveMaterial(sc);
|
|
477
496
|
this.applyBlendMode(material, sc?.blendMode ?? "squared");
|
|
478
497
|
const mesh = new THREE.Mesh(geo.geometry, material);
|
|
479
498
|
mesh.frustumCulled = false;
|
|
@@ -491,6 +510,45 @@ var WaveRenderer = class {
|
|
|
491
510
|
});
|
|
492
511
|
}
|
|
493
512
|
/**
|
|
513
|
+
* Build the material for one wave. The GLSL backend compiles a ShaderMaterial with `#define`
|
|
514
|
+
* variants; the WebGPU subclass builds a TSL node graph instead. Both expose the same `uniforms`
|
|
515
|
+
* surface, so everything downstream in `refresh()` is shared.
|
|
516
|
+
*/
|
|
517
|
+
createWaveMaterial(sc) {
|
|
518
|
+
return new THREE.ShaderMaterial({
|
|
519
|
+
uniforms: this.makeUniforms(),
|
|
520
|
+
defines: this.waveDefines(sc),
|
|
521
|
+
vertexShader,
|
|
522
|
+
fragmentShader: sc?.theme === "wireframe" ? lineFragmentShader : fragmentShader,
|
|
523
|
+
transparent: true,
|
|
524
|
+
depthTest: true,
|
|
525
|
+
depthWrite: true,
|
|
526
|
+
side: THREE.DoubleSide
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Re-select this wave's shader variant when its config changes shape (theme, twist motion, helix,
|
|
531
|
+
* …). Returns true if the material needs recompiling. The GLSL backend swaps `defines` and the
|
|
532
|
+
* fragment source; the WebGPU subclass rebuilds the node graph, since a TSL variant is a
|
|
533
|
+
* different graph rather than a different define set.
|
|
534
|
+
*/
|
|
535
|
+
applyWaveVariant(wave, sc) {
|
|
536
|
+
const material = wave.material;
|
|
537
|
+
let changed = false;
|
|
538
|
+
const wantDefines = this.waveDefines(sc);
|
|
539
|
+
const curDefines = material.defines ?? {};
|
|
540
|
+
if (Object.keys(wantDefines).sort().join(",") !== Object.keys(curDefines).sort().join(",")) {
|
|
541
|
+
material.defines = wantDefines;
|
|
542
|
+
changed = true;
|
|
543
|
+
}
|
|
544
|
+
const wantFrag = sc.theme === "wireframe" ? lineFragmentShader : fragmentShader;
|
|
545
|
+
if (material.fragmentShader !== wantFrag) {
|
|
546
|
+
material.fragmentShader = wantFrag;
|
|
547
|
+
changed = true;
|
|
548
|
+
}
|
|
549
|
+
return changed;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
494
552
|
* Apply config.blendMode to a material. "squared" (the default) is the hero blend:
|
|
495
553
|
* CustomBlending with AddEquation, src = SrcColorFactor, dst = ZeroFactor, so the
|
|
496
554
|
* framebuffer result is fragColor² — the squaring deepens the colours into the vivid
|
|
@@ -516,7 +574,7 @@ var WaveRenderer = class {
|
|
|
516
574
|
s.geometry.dispose();
|
|
517
575
|
s.palette.dispose();
|
|
518
576
|
if (s.particleField) {
|
|
519
|
-
this.scene.remove(s.particleField.
|
|
577
|
+
this.scene.remove(s.particleField.object);
|
|
520
578
|
s.particleField.dispose();
|
|
521
579
|
}
|
|
522
580
|
}
|
|
@@ -538,7 +596,7 @@ var WaveRenderer = class {
|
|
|
538
596
|
s.geometry.dispose();
|
|
539
597
|
s.palette.dispose();
|
|
540
598
|
if (s.particleField) {
|
|
541
|
-
this.scene.remove(s.particleField.
|
|
599
|
+
this.scene.remove(s.particleField.object);
|
|
542
600
|
s.particleField.dispose();
|
|
543
601
|
}
|
|
544
602
|
}
|
|
@@ -566,17 +624,7 @@ var WaveRenderer = class {
|
|
|
566
624
|
const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
|
|
567
625
|
const u = wave.material.uniforms;
|
|
568
626
|
if (this.applyBlendMode(wave.material, sc.blendMode)) wave.material.needsUpdate = true;
|
|
569
|
-
|
|
570
|
-
const curDefines = wave.material.defines ?? {};
|
|
571
|
-
if (Object.keys(wantDefines).sort().join(",") !== Object.keys(curDefines).sort().join(",")) {
|
|
572
|
-
wave.material.defines = wantDefines;
|
|
573
|
-
wave.material.needsUpdate = true;
|
|
574
|
-
}
|
|
575
|
-
const wantFrag = sc.theme === "wireframe" ? lineFragmentShader : fragmentShader;
|
|
576
|
-
if (wave.material.fragmentShader !== wantFrag) {
|
|
577
|
-
wave.material.fragmentShader = wantFrag;
|
|
578
|
-
wave.material.needsUpdate = true;
|
|
579
|
-
}
|
|
627
|
+
if (this.applyWaveVariant(wave, sc)) wave.material.needsUpdate = true;
|
|
580
628
|
const stops = [...sc.palette].sort((a, b) => a.pos - b.pos);
|
|
581
629
|
const colorCount = Math.max(1, Math.min(stops.length, 8));
|
|
582
630
|
const colors = u.uColors.value;
|
|
@@ -731,18 +779,41 @@ var WaveRenderer = class {
|
|
|
731
779
|
if (this.config.transparentBackground) {
|
|
732
780
|
this.scene.background = null;
|
|
733
781
|
this.renderer.setClearColor(0, 0);
|
|
782
|
+
this.onBackgroundChanged();
|
|
734
783
|
return;
|
|
735
784
|
}
|
|
736
785
|
const matte = new THREE.Color(this.config.background);
|
|
737
786
|
this.renderer.setClearColor(matte, 1);
|
|
738
787
|
if (this.config.backgroundMode === "color") {
|
|
739
788
|
this.applyColorBackground(matte);
|
|
789
|
+
this.onBackgroundChanged();
|
|
740
790
|
return;
|
|
741
791
|
}
|
|
742
792
|
const { width, height } = this.backgroundCanvasSize(this.config.backgroundVideoUrl ? 2048 : 4096);
|
|
743
793
|
if (this.config.backgroundMode === "gradient") this.applyGradientBackground(width, height);
|
|
744
794
|
else this.applyImageBackground(matte, width, height);
|
|
795
|
+
this.onBackgroundChanged();
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Build a wave's particle field. The GLSL backend returns a `THREE.Points` field; the TSL backend
|
|
799
|
+
* returns an instanced-sprite one wired to this wave's own uniform registry.
|
|
800
|
+
*/
|
|
801
|
+
createParticleField(_wave, _sc, onReady) {
|
|
802
|
+
return new ParticleField(onReady);
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Largest texture edge the backend will accept, used to cap the background canvas. WebGL reports
|
|
806
|
+
* it on `capabilities`; WebGPU has no such object, so the subclass reads the device limit.
|
|
807
|
+
*/
|
|
808
|
+
maxTextureSize() {
|
|
809
|
+
return this.renderer.capabilities.maxTextureSize;
|
|
745
810
|
}
|
|
811
|
+
/**
|
|
812
|
+
* Called whenever the scene background changes. A no-op on WebGL, where `EffectComposer`'s
|
|
813
|
+
* RenderPass reads the scene afresh each frame; the WebGPU subclass rebuilds its post chain,
|
|
814
|
+
* because a `pass()` node captures the background state at the point its render context is built.
|
|
815
|
+
*/
|
|
816
|
+
onBackgroundChanged() {}
|
|
746
817
|
applyColorBackground(matte) {
|
|
747
818
|
this.clearBackgroundVideo();
|
|
748
819
|
this.backgroundTexture?.dispose();
|
|
@@ -872,7 +943,7 @@ var WaveRenderer = class {
|
|
|
872
943
|
backgroundCanvasSize(maxRequestedEdge = 4096) {
|
|
873
944
|
const rawWidth = this.outputSize?.width ?? Math.max(1, this.container.clientWidth);
|
|
874
945
|
const rawHeight = this.outputSize?.height ?? Math.max(1, this.container.clientHeight);
|
|
875
|
-
const maxEdge = Math.min(maxRequestedEdge, this.
|
|
946
|
+
const maxEdge = Math.min(maxRequestedEdge, this.maxTextureSize());
|
|
876
947
|
const scale = Math.min(1, maxEdge / Math.max(rawWidth, rawHeight));
|
|
877
948
|
return {
|
|
878
949
|
width: Math.max(1, Math.round(rawWidth * scale)),
|
|
@@ -1247,8 +1318,7 @@ var WaveRenderer = class {
|
|
|
1247
1318
|
this.renderer.domElement.style.width = "100%";
|
|
1248
1319
|
this.renderer.domElement.style.height = "100%";
|
|
1249
1320
|
}
|
|
1250
|
-
this.
|
|
1251
|
-
this.composer.setSize(w, h);
|
|
1321
|
+
this.resizePost(w, h, dpr);
|
|
1252
1322
|
const dw = w * dpr;
|
|
1253
1323
|
const dh = h * dpr;
|
|
1254
1324
|
this.postPass.uniforms.uResolution.value.set(dw, dh);
|
|
@@ -1334,14 +1404,28 @@ var WaveRenderer = class {
|
|
|
1334
1404
|
this.postPass.uniforms.uTime.value = t;
|
|
1335
1405
|
for (const w of this.waves) w.particleField?.setTime(t);
|
|
1336
1406
|
}
|
|
1407
|
+
/** Draw the composed frame. WebGL runs the EffectComposer; WebGPU runs a node post chain. */
|
|
1408
|
+
renderComposed() {
|
|
1409
|
+
this.composer.render();
|
|
1410
|
+
}
|
|
1411
|
+
/** Resize the post chain's render targets. */
|
|
1412
|
+
resizePost(w, h, dpr) {
|
|
1413
|
+
this.composer.setPixelRatio(dpr);
|
|
1414
|
+
this.composer.setSize(w, h);
|
|
1415
|
+
}
|
|
1416
|
+
/** Release the post chain's GPU resources. */
|
|
1417
|
+
disposePost() {
|
|
1418
|
+
this.composer.dispose();
|
|
1419
|
+
}
|
|
1337
1420
|
/** Render exactly one frame at the current time. */
|
|
1338
1421
|
renderOnce() {
|
|
1422
|
+
if (!this.ready) return;
|
|
1339
1423
|
this.updateBackgroundVideoFrame();
|
|
1340
1424
|
this.updateTime();
|
|
1341
1425
|
this.applyInteraction();
|
|
1342
1426
|
this.updateSceneFx();
|
|
1343
1427
|
this.updateClipPlanes();
|
|
1344
|
-
this.
|
|
1428
|
+
this.renderComposed();
|
|
1345
1429
|
this.onAfterRenderFrame();
|
|
1346
1430
|
}
|
|
1347
1431
|
/** Insert / sync / remove EACH wave's particle field — mirrors applyBloom's lazy lifecycle, per wave.
|
|
@@ -1351,17 +1435,18 @@ var WaveRenderer = class {
|
|
|
1351
1435
|
applyParticles() {
|
|
1352
1436
|
const loop = this.config.loopSeconds ?? 0;
|
|
1353
1437
|
this.waves.forEach((wave, i) => {
|
|
1354
|
-
const
|
|
1438
|
+
const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
|
|
1439
|
+
const cfg = sc?.particles;
|
|
1355
1440
|
if (cfg && cfg.count > 0) {
|
|
1356
1441
|
if (!wave.particleField) {
|
|
1357
|
-
wave.particleField =
|
|
1442
|
+
wave.particleField = this.createParticleField(wave, sc, () => {
|
|
1358
1443
|
if (!this.running) this.renderOnce();
|
|
1359
1444
|
});
|
|
1360
|
-
this.scene.add(wave.particleField.
|
|
1445
|
+
this.scene.add(wave.particleField.object);
|
|
1361
1446
|
}
|
|
1362
1447
|
wave.particleField.sync(cfg, loop);
|
|
1363
1448
|
} else if (wave.particleField) {
|
|
1364
|
-
this.scene.remove(wave.particleField.
|
|
1449
|
+
this.scene.remove(wave.particleField.object);
|
|
1365
1450
|
wave.particleField.dispose();
|
|
1366
1451
|
wave.particleField = void 0;
|
|
1367
1452
|
}
|
|
@@ -1425,10 +1510,11 @@ var WaveRenderer = class {
|
|
|
1425
1510
|
* refresh(); the compiled define set (POINTER_FX etc.) is handled separately by waveDefines(). */
|
|
1426
1511
|
syncInteraction() {
|
|
1427
1512
|
const active = interactionActive(this.config);
|
|
1428
|
-
if (active && !this.interaction) this.
|
|
1513
|
+
if (active && !this.interaction) this.loadInteraction();
|
|
1429
1514
|
else if (!active && this.interaction) {
|
|
1430
1515
|
this.interaction.dispose();
|
|
1431
1516
|
this.interaction = void 0;
|
|
1517
|
+
this.interactionModule = void 0;
|
|
1432
1518
|
this.interactionTimeOffset = 0;
|
|
1433
1519
|
if (this.interactionZoom !== 1) {
|
|
1434
1520
|
this.interactionZoom = 1;
|
|
@@ -1436,18 +1522,50 @@ var WaveRenderer = class {
|
|
|
1436
1522
|
}
|
|
1437
1523
|
}
|
|
1438
1524
|
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Fetch the interactivity runtime and attach it. Deliberately a DYNAMIC import: the controller,
|
|
1527
|
+
* its listeners, the applier tables and the tilt sensor are ~3.8 KB gzipped that a scene with no
|
|
1528
|
+
* `interaction` block never runs, and a static import would put all of it in every bundle. The
|
|
1529
|
+
* cost is that interaction goes live a chunk-fetch after the first frame instead of on it —
|
|
1530
|
+
* invisible in practice, since there is nothing to react to until a reader moves.
|
|
1531
|
+
*
|
|
1532
|
+
* A failed fetch is not fatal: the wave renders exactly as an inert one does, which is the same
|
|
1533
|
+
* thing that happens on a config with no interaction at all.
|
|
1534
|
+
*/
|
|
1535
|
+
async loadInteraction() {
|
|
1536
|
+
if (this.interactionLoading) return;
|
|
1537
|
+
this.interactionLoading = true;
|
|
1538
|
+
try {
|
|
1539
|
+
const mod = await import("./interaction.js");
|
|
1540
|
+
if (this.disposed || this.interaction || !interactionActive(this.config)) return;
|
|
1541
|
+
this.interactionModule = mod;
|
|
1542
|
+
this.interaction = new mod.InteractionController(this.container, () => this.config);
|
|
1543
|
+
if (this.stagedInputs) {
|
|
1544
|
+
for (const [name, value] of this.stagedInputs) this.interaction.setInput(name, value);
|
|
1545
|
+
this.stagedInputs = void 0;
|
|
1546
|
+
}
|
|
1547
|
+
this.onInteractionReady();
|
|
1548
|
+
} catch {} finally {
|
|
1549
|
+
this.interactionLoading = false;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
/** Hook for subclasses that hold state the controller must be told about once it exists (the
|
|
1553
|
+
* studio's scroll preview). No-op in the base renderer. */
|
|
1554
|
+
onInteractionReady() {}
|
|
1439
1555
|
/** Per-frame interaction write: dynamic pointer-field uniforms + bindings. No-op without a
|
|
1440
1556
|
* controller. While capturing it writes the REST state instead (pointer field zeroed, every bound
|
|
1441
1557
|
* param at its authored base) — merely skipping the write would freeze whatever live hover/scroll
|
|
1442
1558
|
* state the previous frame left in the uniforms, so exports wouldn't be deterministic. */
|
|
1443
1559
|
applyInteraction() {
|
|
1444
|
-
|
|
1560
|
+
const ic = this.interaction;
|
|
1561
|
+
const mod = this.interactionModule;
|
|
1562
|
+
if (!ic || !mod) return;
|
|
1445
1563
|
if (this.capturing) {
|
|
1446
|
-
this.applyInteractionRest();
|
|
1564
|
+
this.applyInteractionRest(mod);
|
|
1447
1565
|
return;
|
|
1448
1566
|
}
|
|
1449
|
-
if (anyPointerFxActive(this.config)) this.applyPointerField(
|
|
1450
|
-
this.applyBindings(
|
|
1567
|
+
if (anyPointerFxActive(this.config)) this.applyPointerField(ic);
|
|
1568
|
+
this.applyBindings(ic, mod);
|
|
1451
1569
|
}
|
|
1452
1570
|
/** Write the capture-frame interaction state: exactly what this config renders with no input —
|
|
1453
1571
|
* pointer presence + ripple envelopes zeroed (vPointerFall gates every hover effect to 0) and each
|
|
@@ -1455,7 +1573,7 @@ var WaveRenderer = class {
|
|
|
1455
1573
|
* the capture resumes mid-gesture; the trailing renderOnce() in captureImage restores the preview.
|
|
1456
1574
|
* interactionZoom is deliberately NOT reset — captureImage strips it from camera.zoom itself, and
|
|
1457
1575
|
* the post-capture restore depends on it being unchanged. */
|
|
1458
|
-
applyInteractionRest() {
|
|
1576
|
+
applyInteractionRest(mod) {
|
|
1459
1577
|
for (let i = 0; i < this.waves.length; i++) {
|
|
1460
1578
|
const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
|
|
1461
1579
|
if (!wavePointerFxActive(this.config, sc)) continue;
|
|
@@ -1473,7 +1591,7 @@ var WaveRenderer = class {
|
|
|
1473
1591
|
out: this.interactionSceneOut
|
|
1474
1592
|
};
|
|
1475
1593
|
for (const b of sceneBindings) {
|
|
1476
|
-
const applier = SCENE_APPLIERS[b.target];
|
|
1594
|
+
const applier = mod.SCENE_APPLIERS[b.target];
|
|
1477
1595
|
applier.apply(applier.base(this.config), sceneArgs);
|
|
1478
1596
|
}
|
|
1479
1597
|
}
|
|
@@ -1487,7 +1605,7 @@ var WaveRenderer = class {
|
|
|
1487
1605
|
if (!sc || !bindings || bindings.length === 0) continue;
|
|
1488
1606
|
const wave = this.waves[i];
|
|
1489
1607
|
for (const b of bindings) {
|
|
1490
|
-
const applier = WAVE_APPLIERS[b.target];
|
|
1608
|
+
const applier = mod.WAVE_APPLIERS[b.target];
|
|
1491
1609
|
applier.apply(applier.base(sc), {
|
|
1492
1610
|
u: wave.material.uniforms,
|
|
1493
1611
|
mesh: wave.mesh
|
|
@@ -1529,7 +1647,7 @@ var WaveRenderer = class {
|
|
|
1529
1647
|
}
|
|
1530
1648
|
/** Evaluate bindings via the applier tables: value = mix(from ?? base, to, smoothedSource). Scene
|
|
1531
1649
|
* bindings drive scene params; each wave's bindings drive that wave's uniforms. */
|
|
1532
|
-
applyBindings(ic) {
|
|
1650
|
+
applyBindings(ic, mod) {
|
|
1533
1651
|
this.interactionSceneOut.timeOffset = this.config.timeOffset ?? 0;
|
|
1534
1652
|
this.interactionSceneOut.zoom = this.config.cameraZoom ?? 1;
|
|
1535
1653
|
const sceneArgs = {
|
|
@@ -1537,7 +1655,7 @@ var WaveRenderer = class {
|
|
|
1537
1655
|
out: this.interactionSceneOut
|
|
1538
1656
|
};
|
|
1539
1657
|
for (const b of this.config.interaction?.bindings ?? []) {
|
|
1540
|
-
const applier = SCENE_APPLIERS[b.target];
|
|
1658
|
+
const applier = mod.SCENE_APPLIERS[b.target];
|
|
1541
1659
|
const value = THREE.MathUtils.lerp(b.from ?? applier.base(this.config), b.to, ic.bindingValue(b));
|
|
1542
1660
|
applier.apply(value, sceneArgs);
|
|
1543
1661
|
}
|
|
@@ -1547,7 +1665,7 @@ var WaveRenderer = class {
|
|
|
1547
1665
|
if (!sc || !bindings || bindings.length === 0) continue;
|
|
1548
1666
|
const wave = this.waves[i];
|
|
1549
1667
|
for (const b of bindings) {
|
|
1550
|
-
const applier = WAVE_APPLIERS[b.target];
|
|
1668
|
+
const applier = mod.WAVE_APPLIERS[b.target];
|
|
1551
1669
|
const value = THREE.MathUtils.lerp(b.from ?? applier.base(sc), b.to, ic.bindingValue(b));
|
|
1552
1670
|
applier.apply(value, {
|
|
1553
1671
|
u: wave.material.uniforms,
|
|
@@ -1562,9 +1680,45 @@ var WaveRenderer = class {
|
|
|
1562
1680
|
this.applyZoom();
|
|
1563
1681
|
}
|
|
1564
1682
|
}
|
|
1565
|
-
/** Feed a `custom:<name>` interaction input (developer API). No-op when interaction is off.
|
|
1683
|
+
/** Feed a `custom:<name>` interaction input (developer API). No-op when interaction is off.
|
|
1684
|
+
* Values fed before the interaction chunk lands are STAGED (last one per name wins) and replayed
|
|
1685
|
+
* when it does — otherwise a one-shot input sent right after `onReady` would vanish into the
|
|
1686
|
+
* fetch window. */
|
|
1566
1687
|
setInteractionInput(name, value) {
|
|
1567
|
-
this.interaction
|
|
1688
|
+
if (this.interaction) {
|
|
1689
|
+
this.interaction.setInput(name, value);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (typeof name !== "string" || !Number.isFinite(value)) return;
|
|
1693
|
+
if (!interactionActive(this.config)) return;
|
|
1694
|
+
(this.stagedInputs ??= /* @__PURE__ */ new Map()).set(name, value);
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Explicitly ask for the device-orientation sensor. OPTIONAL, and on iOS it opens a modal
|
|
1698
|
+
* permission dialog — so this belongs to a page where tilt is the point, not to a decorative
|
|
1699
|
+
* background, which should simply go without tilt there. Nothing calls this for you.
|
|
1700
|
+
*
|
|
1701
|
+
* CALL IT FROM A USER GESTURE: iOS 13+ only grants the sensor from inside a tap handler, and
|
|
1702
|
+
* awaiting anything before it (a fetch, a timeout) spends the gesture. Resolves true once
|
|
1703
|
+
* readings can flow — false when
|
|
1704
|
+
* the platform has no sensor, the scene declares no `interaction.tilt`, or the reader refused.
|
|
1705
|
+
* Where no permission is required the sensor is already live and this resolves true.
|
|
1706
|
+
*/
|
|
1707
|
+
enableTilt() {
|
|
1708
|
+
if (!this.interaction) {
|
|
1709
|
+
if (interactionActive(this.config)) this.loadInteraction();
|
|
1710
|
+
return Promise.resolve(false);
|
|
1711
|
+
}
|
|
1712
|
+
return this.interaction.enableTilt();
|
|
1713
|
+
}
|
|
1714
|
+
/** Where the tilt sensor stands. `"prompt"` is exactly when a tap-to-enable affordance helps. */
|
|
1715
|
+
tiltStatus() {
|
|
1716
|
+
if (this.interaction) return this.interaction.tiltStatus();
|
|
1717
|
+
return typeof window !== "undefined" && typeof window.DeviceOrientationEvent !== "undefined" ? "prompt" : "unsupported";
|
|
1718
|
+
}
|
|
1719
|
+
/** Take the next orientation reading as the neutral pose — for when the reader has changed grip. */
|
|
1720
|
+
recenterTilt() {
|
|
1721
|
+
this.interaction?.recenterTilt();
|
|
1568
1722
|
}
|
|
1569
1723
|
/** Re-evaluate play/pause after `config.paused` changes. */
|
|
1570
1724
|
refreshPlayback() {
|
|
@@ -1720,11 +1874,13 @@ var WaveRenderer = class {
|
|
|
1720
1874
|
this.applyCameraFromConfig();
|
|
1721
1875
|
}
|
|
1722
1876
|
dispose() {
|
|
1877
|
+
this.disposed = true;
|
|
1723
1878
|
cancelAnimationFrame(this.rafId);
|
|
1724
1879
|
cancelAnimationFrame(this.resizeRaf);
|
|
1725
1880
|
this.running = false;
|
|
1726
1881
|
this.interaction?.dispose();
|
|
1727
1882
|
this.interaction = void 0;
|
|
1883
|
+
this.interactionModule = void 0;
|
|
1728
1884
|
this.resizeObserver.disconnect();
|
|
1729
1885
|
this.intersectionObserver.disconnect();
|
|
1730
1886
|
this.motionQuery.removeEventListener("change", this.onMotionChange);
|
|
@@ -1747,7 +1903,7 @@ var WaveRenderer = class {
|
|
|
1747
1903
|
this.heatmapPass?.dispose();
|
|
1748
1904
|
this.paperTexturePass?.dispose();
|
|
1749
1905
|
this.halftoneCmykPass?.dispose();
|
|
1750
|
-
this.
|
|
1906
|
+
this.disposePost();
|
|
1751
1907
|
this.renderer.dispose();
|
|
1752
1908
|
this.renderer.domElement.remove();
|
|
1753
1909
|
}
|