@wave3d/core 0.7.0 → 0.9.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 +78 -4
- package/dist/config/model.js +12 -2
- package/dist/config/model.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/presets.js +35 -0
- package/dist/presets.js.map +1 -1
- package/dist/renderer/WaveRenderer.d.ts +146 -12
- package/dist/renderer/WaveRenderer.js +218 -56
- 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 -28
- package/dist/renderer/particleField.js +173 -16
- 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/shaders.js +178 -86
- package/dist/renderer/shaders.js.map +1 -1
- 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 +2219 -1929
- 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,8 +1,8 @@
|
|
|
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 { anyPointerFxActive, interactionActive, wavePointerFxActive, waveRipplesActive } from "./interactionGates.js";
|
|
4
5
|
import { ParticleField } from "./particleField.js";
|
|
5
|
-
import { InteractionController, SCENE_APPLIERS, WAVE_APPLIERS, anyPointerFxActive, interactionActive, wavePointerFxActive, waveRipplesActive } from "./interaction.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";
|
|
8
8
|
import * as THREE from "three";
|
|
@@ -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,15 +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 =
|
|
1358
|
-
|
|
1442
|
+
wave.particleField = this.createParticleField(wave, sc, () => {
|
|
1443
|
+
if (!this.running) this.renderOnce();
|
|
1444
|
+
});
|
|
1445
|
+
this.scene.add(wave.particleField.object);
|
|
1359
1446
|
}
|
|
1360
1447
|
wave.particleField.sync(cfg, loop);
|
|
1361
1448
|
} else if (wave.particleField) {
|
|
1362
|
-
this.scene.remove(wave.particleField.
|
|
1449
|
+
this.scene.remove(wave.particleField.object);
|
|
1363
1450
|
wave.particleField.dispose();
|
|
1364
1451
|
wave.particleField = void 0;
|
|
1365
1452
|
}
|
|
@@ -1401,8 +1488,10 @@ var WaveRenderer = class {
|
|
|
1401
1488
|
});
|
|
1402
1489
|
});
|
|
1403
1490
|
}
|
|
1404
|
-
/** The
|
|
1405
|
-
*
|
|
1491
|
+
/** The subset of waveDefines() the particle program shares: the shape gates (what waveShape reads,
|
|
1492
|
+
* so the dust matches its wave's deform) plus the pointer gates (what pointerField reads, so the
|
|
1493
|
+
* dust reacts to the same cursor). Every one of these is derived from CONFIG alone — live input
|
|
1494
|
+
* must never reach here, or each frame would flip the define set and recompile the point program. */
|
|
1406
1495
|
shapeDefines(sc) {
|
|
1407
1496
|
const all = this.waveDefines(sc);
|
|
1408
1497
|
const out = {};
|
|
@@ -1411,7 +1500,9 @@ var WaveRenderer = class {
|
|
|
1411
1500
|
"DETAIL_OCTAVE",
|
|
1412
1501
|
"HELIX",
|
|
1413
1502
|
"TWIST_MOTION",
|
|
1414
|
-
"RADIAL"
|
|
1503
|
+
"RADIAL",
|
|
1504
|
+
"POINTER_FX",
|
|
1505
|
+
"POINTER_RIPPLES"
|
|
1415
1506
|
]) if (k in all) out[k] = "";
|
|
1416
1507
|
return out;
|
|
1417
1508
|
}
|
|
@@ -1419,10 +1510,11 @@ var WaveRenderer = class {
|
|
|
1419
1510
|
* refresh(); the compiled define set (POINTER_FX etc.) is handled separately by waveDefines(). */
|
|
1420
1511
|
syncInteraction() {
|
|
1421
1512
|
const active = interactionActive(this.config);
|
|
1422
|
-
if (active && !this.interaction) this.
|
|
1513
|
+
if (active && !this.interaction) this.loadInteraction();
|
|
1423
1514
|
else if (!active && this.interaction) {
|
|
1424
1515
|
this.interaction.dispose();
|
|
1425
1516
|
this.interaction = void 0;
|
|
1517
|
+
this.interactionModule = void 0;
|
|
1426
1518
|
this.interactionTimeOffset = 0;
|
|
1427
1519
|
if (this.interactionZoom !== 1) {
|
|
1428
1520
|
this.interactionZoom = 1;
|
|
@@ -1430,18 +1522,50 @@ var WaveRenderer = class {
|
|
|
1430
1522
|
}
|
|
1431
1523
|
}
|
|
1432
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() {}
|
|
1433
1555
|
/** Per-frame interaction write: dynamic pointer-field uniforms + bindings. No-op without a
|
|
1434
1556
|
* controller. While capturing it writes the REST state instead (pointer field zeroed, every bound
|
|
1435
1557
|
* param at its authored base) — merely skipping the write would freeze whatever live hover/scroll
|
|
1436
1558
|
* state the previous frame left in the uniforms, so exports wouldn't be deterministic. */
|
|
1437
1559
|
applyInteraction() {
|
|
1438
|
-
|
|
1560
|
+
const ic = this.interaction;
|
|
1561
|
+
const mod = this.interactionModule;
|
|
1562
|
+
if (!ic || !mod) return;
|
|
1439
1563
|
if (this.capturing) {
|
|
1440
|
-
this.applyInteractionRest();
|
|
1564
|
+
this.applyInteractionRest(mod);
|
|
1441
1565
|
return;
|
|
1442
1566
|
}
|
|
1443
|
-
if (anyPointerFxActive(this.config)) this.applyPointerField(
|
|
1444
|
-
this.applyBindings(
|
|
1567
|
+
if (anyPointerFxActive(this.config)) this.applyPointerField(ic);
|
|
1568
|
+
this.applyBindings(ic, mod);
|
|
1445
1569
|
}
|
|
1446
1570
|
/** Write the capture-frame interaction state: exactly what this config renders with no input —
|
|
1447
1571
|
* pointer presence + ripple envelopes zeroed (vPointerFall gates every hover effect to 0) and each
|
|
@@ -1449,7 +1573,7 @@ var WaveRenderer = class {
|
|
|
1449
1573
|
* the capture resumes mid-gesture; the trailing renderOnce() in captureImage restores the preview.
|
|
1450
1574
|
* interactionZoom is deliberately NOT reset — captureImage strips it from camera.zoom itself, and
|
|
1451
1575
|
* the post-capture restore depends on it being unchanged. */
|
|
1452
|
-
applyInteractionRest() {
|
|
1576
|
+
applyInteractionRest(mod) {
|
|
1453
1577
|
for (let i = 0; i < this.waves.length; i++) {
|
|
1454
1578
|
const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
|
|
1455
1579
|
if (!wavePointerFxActive(this.config, sc)) continue;
|
|
@@ -1467,7 +1591,7 @@ var WaveRenderer = class {
|
|
|
1467
1591
|
out: this.interactionSceneOut
|
|
1468
1592
|
};
|
|
1469
1593
|
for (const b of sceneBindings) {
|
|
1470
|
-
const applier = SCENE_APPLIERS[b.target];
|
|
1594
|
+
const applier = mod.SCENE_APPLIERS[b.target];
|
|
1471
1595
|
applier.apply(applier.base(this.config), sceneArgs);
|
|
1472
1596
|
}
|
|
1473
1597
|
}
|
|
@@ -1481,7 +1605,7 @@ var WaveRenderer = class {
|
|
|
1481
1605
|
if (!sc || !bindings || bindings.length === 0) continue;
|
|
1482
1606
|
const wave = this.waves[i];
|
|
1483
1607
|
for (const b of bindings) {
|
|
1484
|
-
const applier = WAVE_APPLIERS[b.target];
|
|
1608
|
+
const applier = mod.WAVE_APPLIERS[b.target];
|
|
1485
1609
|
applier.apply(applier.base(sc), {
|
|
1486
1610
|
u: wave.material.uniforms,
|
|
1487
1611
|
mesh: wave.mesh
|
|
@@ -1523,7 +1647,7 @@ var WaveRenderer = class {
|
|
|
1523
1647
|
}
|
|
1524
1648
|
/** Evaluate bindings via the applier tables: value = mix(from ?? base, to, smoothedSource). Scene
|
|
1525
1649
|
* bindings drive scene params; each wave's bindings drive that wave's uniforms. */
|
|
1526
|
-
applyBindings(ic) {
|
|
1650
|
+
applyBindings(ic, mod) {
|
|
1527
1651
|
this.interactionSceneOut.timeOffset = this.config.timeOffset ?? 0;
|
|
1528
1652
|
this.interactionSceneOut.zoom = this.config.cameraZoom ?? 1;
|
|
1529
1653
|
const sceneArgs = {
|
|
@@ -1531,7 +1655,7 @@ var WaveRenderer = class {
|
|
|
1531
1655
|
out: this.interactionSceneOut
|
|
1532
1656
|
};
|
|
1533
1657
|
for (const b of this.config.interaction?.bindings ?? []) {
|
|
1534
|
-
const applier = SCENE_APPLIERS[b.target];
|
|
1658
|
+
const applier = mod.SCENE_APPLIERS[b.target];
|
|
1535
1659
|
const value = THREE.MathUtils.lerp(b.from ?? applier.base(this.config), b.to, ic.bindingValue(b));
|
|
1536
1660
|
applier.apply(value, sceneArgs);
|
|
1537
1661
|
}
|
|
@@ -1541,7 +1665,7 @@ var WaveRenderer = class {
|
|
|
1541
1665
|
if (!sc || !bindings || bindings.length === 0) continue;
|
|
1542
1666
|
const wave = this.waves[i];
|
|
1543
1667
|
for (const b of bindings) {
|
|
1544
|
-
const applier = WAVE_APPLIERS[b.target];
|
|
1668
|
+
const applier = mod.WAVE_APPLIERS[b.target];
|
|
1545
1669
|
const value = THREE.MathUtils.lerp(b.from ?? applier.base(sc), b.to, ic.bindingValue(b));
|
|
1546
1670
|
applier.apply(value, {
|
|
1547
1671
|
u: wave.material.uniforms,
|
|
@@ -1556,9 +1680,45 @@ var WaveRenderer = class {
|
|
|
1556
1680
|
this.applyZoom();
|
|
1557
1681
|
}
|
|
1558
1682
|
}
|
|
1559
|
-
/** 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. */
|
|
1560
1687
|
setInteractionInput(name, value) {
|
|
1561
|
-
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();
|
|
1562
1722
|
}
|
|
1563
1723
|
/** Re-evaluate play/pause after `config.paused` changes. */
|
|
1564
1724
|
refreshPlayback() {
|
|
@@ -1714,11 +1874,13 @@ var WaveRenderer = class {
|
|
|
1714
1874
|
this.applyCameraFromConfig();
|
|
1715
1875
|
}
|
|
1716
1876
|
dispose() {
|
|
1877
|
+
this.disposed = true;
|
|
1717
1878
|
cancelAnimationFrame(this.rafId);
|
|
1718
1879
|
cancelAnimationFrame(this.resizeRaf);
|
|
1719
1880
|
this.running = false;
|
|
1720
1881
|
this.interaction?.dispose();
|
|
1721
1882
|
this.interaction = void 0;
|
|
1883
|
+
this.interactionModule = void 0;
|
|
1722
1884
|
this.resizeObserver.disconnect();
|
|
1723
1885
|
this.intersectionObserver.disconnect();
|
|
1724
1886
|
this.motionQuery.removeEventListener("change", this.onMotionChange);
|
|
@@ -1741,7 +1903,7 @@ var WaveRenderer = class {
|
|
|
1741
1903
|
this.heatmapPass?.dispose();
|
|
1742
1904
|
this.paperTexturePass?.dispose();
|
|
1743
1905
|
this.halftoneCmykPass?.dispose();
|
|
1744
|
-
this.
|
|
1906
|
+
this.disposePost();
|
|
1745
1907
|
this.renderer.dispose();
|
|
1746
1908
|
this.renderer.domElement.remove();
|
|
1747
1909
|
}
|