@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.
Files changed (71) hide show
  1. package/README.md +3 -1
  2. package/dist/config/model.d.ts +48 -2
  3. package/dist/config/model.js +8 -1
  4. package/dist/config/model.js.map +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/renderer/WaveRenderer.d.ts +142 -10
  7. package/dist/renderer/WaveRenderer.js +209 -53
  8. package/dist/renderer/WaveRenderer.js.map +1 -1
  9. package/dist/renderer/WaveRendererGPU.js +291 -0
  10. package/dist/renderer/WaveRendererGPU.js.map +1 -0
  11. package/dist/renderer/gpu-loader.js +2 -0
  12. package/dist/renderer/index.d.ts +3 -2
  13. package/dist/renderer/interaction.d.ts +29 -0
  14. package/dist/renderer/interaction.js +77 -31
  15. package/dist/renderer/interaction.js.map +1 -1
  16. package/dist/renderer/interactionGates.js +59 -0
  17. package/dist/renderer/interactionGates.js.map +1 -0
  18. package/dist/renderer/particleField.d.ts +1 -59
  19. package/dist/renderer/particleField.js +49 -29
  20. package/dist/renderer/particleField.js.map +1 -1
  21. package/dist/renderer/particleFieldGPU.js +141 -0
  22. package/dist/renderer/particleFieldGPU.js.map +1 -0
  23. package/dist/renderer/tilt.d.ts +17 -0
  24. package/dist/renderer/tilt.js +124 -0
  25. package/dist/renderer/tilt.js.map +1 -0
  26. package/dist/renderer/tsl/color.js +129 -0
  27. package/dist/renderer/tsl/color.js.map +1 -0
  28. package/dist/renderer/tsl/noise.js +83 -0
  29. package/dist/renderer/tsl/noise.js.map +1 -0
  30. package/dist/renderer/tsl/packedArray.js +76 -0
  31. package/dist/renderer/tsl/packedArray.js.map +1 -0
  32. package/dist/renderer/tsl/particleMaterial.js +133 -0
  33. package/dist/renderer/tsl/particleMaterial.js.map +1 -0
  34. package/dist/renderer/tsl/particleUniforms.js +44 -0
  35. package/dist/renderer/tsl/particleUniforms.js.map +1 -0
  36. package/dist/renderer/tsl/pointerField.js +73 -0
  37. package/dist/renderer/tsl/pointerField.js.map +1 -0
  38. package/dist/renderer/tsl/post.js +63 -0
  39. package/dist/renderer/tsl/post.js.map +1 -0
  40. package/dist/renderer/tsl/postChain.js +56 -0
  41. package/dist/renderer/tsl/postChain.js.map +1 -0
  42. package/dist/renderer/tsl/postEffects.js +221 -0
  43. package/dist/renderer/tsl/postEffects.js.map +1 -0
  44. package/dist/renderer/tsl/types.js +28 -0
  45. package/dist/renderer/tsl/types.js.map +1 -0
  46. package/dist/renderer/tsl/uniforms.js +152 -0
  47. package/dist/renderer/tsl/uniforms.js.map +1 -0
  48. package/dist/renderer/tsl/waveMaterial.js +185 -0
  49. package/dist/renderer/tsl/waveMaterial.js.map +1 -0
  50. package/dist/renderer/tsl/waveShape.js +108 -0
  51. package/dist/renderer/tsl/waveShape.js.map +1 -0
  52. package/dist/shell/createWave.d.ts +29 -0
  53. package/dist/shell/createWave.js +39 -11
  54. package/dist/shell/createWave.js.map +1 -1
  55. package/dist/shell/probe.js +18 -1
  56. package/dist/shell/probe.js.map +1 -1
  57. package/dist/standalone/wave3d.standalone.js +1811 -1594
  58. package/dist/standalone/wave3d.standalone.webgpu.js +36647 -0
  59. package/dist/standalone.d.ts +10 -3
  60. package/dist/standalone.js +10 -3
  61. package/dist/standalone.js.map +1 -1
  62. package/dist/studio/StudioWaveRenderer.d.ts +4 -0
  63. package/dist/studio/StudioWaveRenderer.js +9 -0
  64. package/dist/studio/StudioWaveRenderer.js.map +1 -1
  65. package/dist/studio/StudioWaveRendererGPU.js +18 -0
  66. package/dist/studio/StudioWaveRendererGPU.js.map +1 -0
  67. package/dist/studio/index.d.ts +12 -2
  68. package/dist/studio/index.js +14 -1
  69. package/dist/studio/index.js.map +1 -0
  70. package/package.json +12 -3
  71. package/skills/wave3d/SKILL.md +29 -2
@@ -1,8 +1,11 @@
1
- import { CameraFit, StudioConfig, WaveConfig } from "../config/model.js";
1
+ import { BlendMode, CameraFit, StudioConfig, WaveConfig } from "../config/model.js";
2
2
  import { WaveGeometry } from "./WaveGeometry.js";
3
- import { ParticleField } from "./particleField.js";
3
+ import { ParticleFrame } from "./particleField.js";
4
+ import { TiltStatus } from "./tilt.js";
4
5
  import { InteractionController } from "./interaction.js";
5
6
  import * as THREE from "three";
7
+ import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
8
+ import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
6
9
  //#region src/renderer/WaveRenderer.d.ts
7
10
  /** Reference frame (world units) the orthographic camera fills at cameraZoom 1. The wave is
8
11
  * framed by mapping this FRAME_W × FRAME_H rectangle (centred on cameraTarget) onto the canvas,
@@ -61,15 +64,50 @@ declare class WavePalette {
61
64
  private clearVideo;
62
65
  dispose(): void;
63
66
  }
67
+ /**
68
+ * The uniform surface both backends expose. The GLSL path holds `THREE.IUniform`s; the TSL path
69
+ * holds TSL uniform nodes and array wrappers. Both are written the same way — `u.uFoo.value = x`,
70
+ * `u.uColors.value[i].set(...)` — which is what lets the ~116 config-sync writes in `refresh()`
71
+ * stay backend-agnostic.
72
+ */
73
+ type WaveUniforms = Record<string, {
74
+ value: unknown;
75
+ }>;
76
+ /** A wave material, whichever backend built it. */
77
+ type WaveMaterial = THREE.Material & {
78
+ uniforms: WaveUniforms;
79
+ };
80
+ /**
81
+ * A wave's particle field, whichever backend built it.
82
+ *
83
+ * The TSL field is a `THREE.Sprite` where the GLSL one is a `THREE.Points` (WebGPU has no point
84
+ * size), so the scene node is reached through `object` rather than either concrete type.
85
+ */
86
+ interface WaveParticleField {
87
+ readonly object: THREE.Object3D;
88
+ sync(cfg: NonNullable<WaveConfig["particles"]>, loopSeconds: number): void;
89
+ frame(f: ParticleFrame, pixelRatio: number): void;
90
+ configure(shape: {
91
+ defines?: Record<string, string>;
92
+ uniforms?: Record<string, {
93
+ value: unknown;
94
+ }>;
95
+ matrixWorld: THREE.Matrix4;
96
+ speed: number;
97
+ seed: number;
98
+ }): void;
99
+ setTime(t: number): void;
100
+ dispose(): void;
101
+ }
64
102
  type Wave = {
65
103
  mesh: THREE.Mesh;
66
- material: THREE.ShaderMaterial;
104
+ material: WaveMaterial;
67
105
  geometry: WaveGeometry;
68
106
  /** This wave's own 2D palette texture + optional video. */
69
107
  palette: WavePalette;
70
108
  /** This wave's own particle / dust field — created when its `particles.count` first goes >0,
71
109
  * disposed at 0 / absent (the WavePalette lifecycle pattern). Undefined = no dust for this wave. */
72
- particleField?: ParticleField;
110
+ particleField?: WaveParticleField;
73
111
  };
74
112
  /** Convert an sRGB hex string to a linear-space RGB vector (three's ColorManagement does the
75
113
  * sRGB→linear conversion on parse). Exported for the studio subclass's live light-uniform push. */
@@ -81,11 +119,17 @@ declare function hexToLinearVec3(hex: string, target: THREE.Vector3): THREE.Vect
81
119
  */
82
120
  declare class WaveRenderer {
83
121
  readonly renderer: THREE.WebGLRenderer;
122
+ /**
123
+ * Whether this renderer can draw yet. The WebGL backend is ready the moment it is constructed;
124
+ * the WebGPU subclass has to `await renderer.init()` first, and `renderOnce()` throws before that.
125
+ * The base class is always ready, so the WebGL path is unaffected.
126
+ */
127
+ protected ready: boolean;
84
128
  protected readonly scene: THREE.Scene<THREE.Object3DEventMap>;
85
129
  protected readonly camera: THREE.OrthographicCamera;
86
130
  protected readonly group: THREE.Group<THREE.Object3DEventMap>;
87
- private readonly composer;
88
- private readonly postPass;
131
+ protected composer: EffectComposer;
132
+ protected postPass: ShaderPass;
89
133
  /** Optional bloom pass — created lazily when bloomStrength first goes >0, removed at 0. */
90
134
  private bloomPass?;
91
135
  private ditherPass?;
@@ -118,8 +162,17 @@ declare class WaveRenderer {
118
162
  private readonly clipSphere;
119
163
  private readonly clipTmpA;
120
164
  private readonly clipTmpB;
121
- /** Created by syncInteraction() when interaction turns on, disposed when it turns off. */
165
+ /** Created by loadInteraction() once the runtime chunk lands, disposed when interaction turns off.
166
+ * Stays undefined for the frames between the config turning it on and the chunk arriving. */
122
167
  protected interaction?: InteractionController;
168
+ /** The chunk itself, kept for the applier tables. Set and cleared with `interaction`. */
169
+ private interactionModule?;
170
+ /** A fetch already in flight, so a per-frame refresh can't start a second one. */
171
+ private interactionLoading;
172
+ /** setInteractionInput() calls made while the chunk was in flight, replayed once it lands. */
173
+ private stagedInputs?;
174
+ /** Set by dispose(), so an in-flight chunk can't attach listeners to a dead renderer. */
175
+ private disposed;
123
176
  /** Extra ortho-zoom MULTIPLIER from a cameraZoom binding (1 = none); applied in applyZoom().
124
177
  * Protected so the studio's writeCameraToConfig() can divide it back out (keep it out of config). */
125
178
  protected interactionZoom: number;
@@ -150,6 +203,17 @@ declare class WaveRenderer {
150
203
  * this unset and continue to resize responsively with their container and device DPR. */
151
204
  private outputSize?;
152
205
  constructor(container: HTMLElement, config: StudioConfig, options?: WaveRendererOptions);
206
+ /**
207
+ * Build the backing three renderer. Overridden by the WebGPU subclass; called from the
208
+ * constructor, so it must not depend on subclass FIELDS (prototype methods are available during
209
+ * `super()`, field initialisers are not).
210
+ */
211
+ protected createRenderer(): THREE.WebGLRenderer;
212
+ /**
213
+ * Await any asynchronous backend startup. A no-op on WebGL; the WebGPU subclass overrides it to
214
+ * `await renderer.init()` and then draw the first frame. Safe to call more than once.
215
+ */
216
+ init(): Promise<void>;
153
217
  private get segments();
154
218
  private makeUniforms;
155
219
  /** Vertex-shader #defines for a wave: TWIST_MOTION (per-wave animated twist wobble) and
@@ -157,6 +221,19 @@ declare class WaveRenderer {
157
221
  * object compiles the default (linear-time) program. */
158
222
  private waveDefines;
159
223
  private addWave;
224
+ /**
225
+ * Build the material for one wave. The GLSL backend compiles a ShaderMaterial with `#define`
226
+ * variants; the WebGPU subclass builds a TSL node graph instead. Both expose the same `uniforms`
227
+ * surface, so everything downstream in `refresh()` is shared.
228
+ */
229
+ protected createWaveMaterial(sc: WaveConfig | undefined): WaveMaterial;
230
+ /**
231
+ * Re-select this wave's shader variant when its config changes shape (theme, twist motion, helix,
232
+ * …). Returns true if the material needs recompiling. The GLSL backend swaps `defines` and the
233
+ * fragment source; the WebGPU subclass rebuilds the node graph, since a TSL variant is a
234
+ * different graph rather than a different define set.
235
+ */
236
+ protected applyWaveVariant(wave: Wave, sc: WaveConfig): boolean;
160
237
  /**
161
238
  * Apply config.blendMode to a material. "squared" (the default) is the hero blend:
162
239
  * CustomBlending with AddEquation, src = SrcColorFactor, dst = ZeroFactor, so the
@@ -166,7 +243,7 @@ declare class WaveRenderer {
166
243
  * fragment shaders premultiply their output when Three injects PREMULTIPLIED_ALPHA.
167
244
  * Returns true if material state changed (caller flags needsUpdate).
168
245
  */
169
- private applyBlendMode;
246
+ protected applyBlendMode(material: WaveMaterial, mode: BlendMode): boolean;
170
247
  private disposeWaves;
171
248
  /**
172
249
  * Reconcile the wave pool to `waveCount` WITHOUT tearing everything down:
@@ -185,6 +262,22 @@ declare class WaveRenderer {
185
262
  /** Rebuild geometry + waves (call when waveCount or quality changes). */
186
263
  rebuild(): void;
187
264
  private applyBackground;
265
+ /**
266
+ * Build a wave's particle field. The GLSL backend returns a `THREE.Points` field; the TSL backend
267
+ * returns an instanced-sprite one wired to this wave's own uniform registry.
268
+ */
269
+ protected createParticleField(_wave: Wave, _sc: WaveConfig, onReady: () => void): WaveParticleField;
270
+ /**
271
+ * Largest texture edge the backend will accept, used to cap the background canvas. WebGL reports
272
+ * it on `capabilities`; WebGPU has no such object, so the subclass reads the device limit.
273
+ */
274
+ protected maxTextureSize(): number;
275
+ /**
276
+ * Called whenever the scene background changes. A no-op on WebGL, where `EffectComposer`'s
277
+ * RenderPass reads the scene afresh each frame; the WebGPU subclass rebuilds its post chain,
278
+ * because a `pass()` node captures the background state at the point its render context is built.
279
+ */
280
+ protected onBackgroundChanged(): void;
188
281
  private applyColorBackground;
189
282
  private applyGradientBackground;
190
283
  /** Image mode: a live video, a user-loaded image, or a built-in map. `matte` shows while an
@@ -265,6 +358,12 @@ declare class WaveRenderer {
265
358
  /** Advance the per-frame time uniforms (geometry itself is static). Time model:
266
359
  * time = elapsed·introTimeRamp + timeOffset — the ramp eases the animation in on load. */
267
360
  private updateTime;
361
+ /** Draw the composed frame. WebGL runs the EffectComposer; WebGPU runs a node post chain. */
362
+ protected renderComposed(): void;
363
+ /** Resize the post chain's render targets. */
364
+ protected resizePost(w: number, h: number, dpr: number): void;
365
+ /** Release the post chain's GPU resources. */
366
+ protected disposePost(): void;
268
367
  /** Render exactly one frame at the current time. */
269
368
  renderOnce(): void;
270
369
  /** Insert / sync / remove EACH wave's particle field — mirrors applyBloom's lazy lifecycle, per wave.
@@ -285,6 +384,20 @@ declare class WaveRenderer {
285
384
  /** Create/dispose the interaction controller as config toggles interaction on/off. Called from
286
385
  * refresh(); the compiled define set (POINTER_FX etc.) is handled separately by waveDefines(). */
287
386
  private syncInteraction;
387
+ /**
388
+ * Fetch the interactivity runtime and attach it. Deliberately a DYNAMIC import: the controller,
389
+ * its listeners, the applier tables and the tilt sensor are ~3.8 KB gzipped that a scene with no
390
+ * `interaction` block never runs, and a static import would put all of it in every bundle. The
391
+ * cost is that interaction goes live a chunk-fetch after the first frame instead of on it —
392
+ * invisible in practice, since there is nothing to react to until a reader moves.
393
+ *
394
+ * A failed fetch is not fatal: the wave renders exactly as an inert one does, which is the same
395
+ * thing that happens on a config with no interaction at all.
396
+ */
397
+ private loadInteraction;
398
+ /** Hook for subclasses that hold state the controller must be told about once it exists (the
399
+ * studio's scroll preview). No-op in the base renderer. */
400
+ protected onInteractionReady(): void;
288
401
  /** Per-frame interaction write: dynamic pointer-field uniforms + bindings. No-op without a
289
402
  * controller. While capturing it writes the REST state instead (pointer field zeroed, every bound
290
403
  * param at its authored base) — merely skipping the write would freeze whatever live hover/scroll
@@ -304,8 +417,27 @@ declare class WaveRenderer {
304
417
  /** Evaluate bindings via the applier tables: value = mix(from ?? base, to, smoothedSource). Scene
305
418
  * bindings drive scene params; each wave's bindings drive that wave's uniforms. */
306
419
  private applyBindings;
307
- /** Feed a `custom:<name>` interaction input (developer API). No-op when interaction is off. */
420
+ /** Feed a `custom:<name>` interaction input (developer API). No-op when interaction is off.
421
+ * Values fed before the interaction chunk lands are STAGED (last one per name wins) and replayed
422
+ * when it does — otherwise a one-shot input sent right after `onReady` would vanish into the
423
+ * fetch window. */
308
424
  setInteractionInput(name: string, value: number): void;
425
+ /**
426
+ * Explicitly ask for the device-orientation sensor. OPTIONAL, and on iOS it opens a modal
427
+ * permission dialog — so this belongs to a page where tilt is the point, not to a decorative
428
+ * background, which should simply go without tilt there. Nothing calls this for you.
429
+ *
430
+ * CALL IT FROM A USER GESTURE: iOS 13+ only grants the sensor from inside a tap handler, and
431
+ * awaiting anything before it (a fetch, a timeout) spends the gesture. Resolves true once
432
+ * readings can flow — false when
433
+ * the platform has no sensor, the scene declares no `interaction.tilt`, or the reader refused.
434
+ * Where no permission is required the sensor is already live and this resolves true.
435
+ */
436
+ enableTilt(): Promise<boolean>;
437
+ /** Where the tilt sensor stands. `"prompt"` is exactly when a tap-to-enable affordance helps. */
438
+ tiltStatus(): TiltStatus;
439
+ /** Take the next orientation reading as the neutral pose — for when the reader has changed grip. */
440
+ recenterTilt(): void;
309
441
  /** Re-evaluate play/pause after `config.paused` changes. */
310
442
  refreshPlayback(): void;
311
443
  /** Jump the camera to the config's authored framing (cameraPosition / cameraTarget /
@@ -357,5 +489,5 @@ declare class WaveRenderer {
357
489
  dispose(): void;
358
490
  }
359
491
  //#endregion
360
- export { FRAME_H, FRAME_W, WaveRenderer, WaveRendererOptions, frameZoom, hexToLinearVec3 };
492
+ export { FRAME_H, FRAME_W, WaveMaterial, WaveParticleField, WaveRenderer, WaveRendererOptions, WaveUniforms, frameZoom, hexToLinearVec3 };
361
493
  //# sourceMappingURL=WaveRenderer.d.ts.map