@wave3d/core 0.9.0 → 0.11.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 (53) hide show
  1. package/dist/config/model.d.ts +209 -6
  2. package/dist/config/model.js +85 -3
  3. package/dist/config/model.js.map +1 -1
  4. package/dist/index.d.ts +3 -2
  5. package/dist/index.js +3 -2
  6. package/dist/presets.js +295 -0
  7. package/dist/presets.js.map +1 -1
  8. package/dist/renderer/WaveGeometry.js +21 -0
  9. package/dist/renderer/WaveGeometry.js.map +1 -1
  10. package/dist/renderer/WaveRenderer.d.ts +62 -0
  11. package/dist/renderer/WaveRenderer.js +349 -10
  12. package/dist/renderer/WaveRenderer.js.map +1 -1
  13. package/dist/renderer/WaveRendererGPU.js +32 -2
  14. package/dist/renderer/WaveRendererGPU.js.map +1 -1
  15. package/dist/renderer/interaction.js +12 -0
  16. package/dist/renderer/interaction.js.map +1 -1
  17. package/dist/renderer/particleField.js +26 -2
  18. package/dist/renderer/particleField.js.map +1 -1
  19. package/dist/renderer/particleFieldGPU.js +6 -0
  20. package/dist/renderer/particleFieldGPU.js.map +1 -1
  21. package/dist/renderer/shaders.js +748 -13
  22. package/dist/renderer/shaders.js.map +1 -1
  23. package/dist/renderer/tsl/dissolve.js +56 -0
  24. package/dist/renderer/tsl/dissolve.js.map +1 -0
  25. package/dist/renderer/tsl/particleMaterial.js +46 -8
  26. package/dist/renderer/tsl/particleMaterial.js.map +1 -1
  27. package/dist/renderer/tsl/uniforms.js +40 -1
  28. package/dist/renderer/tsl/uniforms.js.map +1 -1
  29. package/dist/renderer/tsl/waveMaterial.js +275 -26
  30. package/dist/renderer/tsl/waveMaterial.js.map +1 -1
  31. package/dist/renderer/tsl/waveShape.js +31 -10
  32. package/dist/renderer/tsl/waveShape.js.map +1 -1
  33. package/dist/renderer/wavePath.js +189 -0
  34. package/dist/renderer/wavePath.js.map +1 -0
  35. package/dist/shell/createWave.d.ts +23 -3
  36. package/dist/shell/createWave.js +5 -4
  37. package/dist/shell/createWave.js.map +1 -1
  38. package/dist/shell/probe.d.ts +28 -0
  39. package/dist/shell/probe.js +58 -11
  40. package/dist/shell/probe.js.map +1 -1
  41. package/dist/standalone/wave3d.standalone.js +3401 -2108
  42. package/dist/standalone/wave3d.standalone.webgpu.js +7372 -5848
  43. package/dist/standalone.d.ts +2 -2
  44. package/dist/standalone.js +2 -2
  45. package/dist/studio/StudioWaveRenderer.d.ts +115 -8
  46. package/dist/studio/StudioWaveRenderer.js +588 -14
  47. package/dist/studio/StudioWaveRenderer.js.map +1 -1
  48. package/dist/studio/index.d.ts +2 -2
  49. package/dist/studio/index.js.map +1 -1
  50. package/dist/studio/randomize.js +0 -1
  51. package/dist/studio/randomize.js.map +1 -1
  52. package/package.json +1 -1
  53. package/skills/wave3d/SKILL.md +142 -5
@@ -28,14 +28,21 @@ import { pass, screenUV, texture, vec2 } from "three/tsl";
28
28
  function variantKey(f) {
29
29
  return [
30
30
  f.theme,
31
+ f.vertexNormal && "vnormal",
31
32
  f.loopMotion && "loop",
32
33
  f.detailOctave && "detail",
33
34
  f.helix && "helix",
34
35
  f.twistMotion && "twist",
35
36
  f.radial && "radial",
37
+ f.path && "path",
36
38
  f.depthTint && "depthTint",
37
39
  f.edgeFeather && "edgeFeather",
38
40
  f.rungs && "rungs",
41
+ f.lineSharp && "lineSharp",
42
+ f.lineClearGaps && "clearGaps",
43
+ f.lineLight && "lineLight",
44
+ f.premultiplied && "premul",
45
+ f.dissolve && "dissolve",
39
46
  f.pointerFx && "pointer",
40
47
  f.pointerRipples && "ripples",
41
48
  f.webgpuClipZ && "gpuz"
@@ -118,17 +125,25 @@ function withTslBackend(Base) {
118
125
  flagsFor(sc) {
119
126
  const bindsDetail = sc?.interaction?.bindings?.some((b) => b.target === "detailAmount") ?? false;
120
127
  const bindsHelix = sc?.interaction?.bindings?.some((b) => b.target.startsWith("helix")) ?? false;
128
+ const bindsDissolve = sc?.interaction?.bindings?.some((b) => b.target === "dissolveAmount") ?? false;
121
129
  const pointer = !!sc && wavePointerFxActive(this.config, sc);
122
130
  return {
123
- theme: sc?.theme === "wireframe" ? "wireframe" : "solid",
131
+ theme: sc?.theme === "wireframe" ? "wireframe" : sc?.theme === "glass" ? "glass" : "solid",
132
+ vertexNormal: sc?.theme === "glass",
124
133
  loopMotion: (this.config.loopSeconds ?? 0) > 0,
125
134
  detailOctave: (sc?.detailAmount ?? 0) !== 0 || bindsDetail,
126
135
  helix: (sc?.helixRadius ?? 0) !== 0 || (sc?.helixRoll ?? 0) !== 0 || bindsHelix,
127
136
  twistMotion: !!sc?.twistMotion,
128
137
  radial: (sc?.radialAmount ?? 0) !== 0,
138
+ path: !!sc?.path && sc.path.length >= 2,
129
139
  depthTint: (sc?.depthTint ?? 0) > 0,
130
140
  edgeFeather: (sc?.edgeFeather ?? .1) !== .1,
131
141
  rungs: sc?.theme === "wireframe" && (sc.rungAmount ?? 0) > 0,
142
+ lineSharp: sc?.theme === "wireframe" && (sc.lineSharpness ?? 0) > 0,
143
+ premultiplied: ((m) => m === "squared" || m === "multiply")(sc?.blendMode ?? "squared"),
144
+ lineClearGaps: sc?.theme === "wireframe" && !!sc.lineGapColor && (sc.lineGapColor === "transparent" || /^#[0-9a-f]{8}$/i.test(sc.lineGapColor)),
145
+ lineLight: sc?.theme === "wireframe" && (sc.lineLight ?? 0) > 0,
146
+ dissolve: !!sc?.dissolve && ((sc.dissolve.amount ?? 0) > 0 || bindsDissolve),
132
147
  pointerFx: pointer,
133
148
  pointerRipples: pointer && waveRipplesActive(this.config, sc),
134
149
  webgpuClipZ: this.webgpuClipZ
@@ -152,8 +167,10 @@ function withTslBackend(Base) {
152
167
  helix: f.helix,
153
168
  twistMotion: f.twistMotion,
154
169
  radial: f.radial,
170
+ path: f.path,
155
171
  pointerFx: f.pointerFx,
156
- pointerRipples: f.pointerRipples
172
+ pointerRipples: f.pointerRipples,
173
+ dissolve: f.dissolve
157
174
  }
158
175
  }, onReady);
159
176
  }
@@ -191,6 +208,18 @@ function withTslBackend(Base) {
191
208
  current.dispose();
192
209
  return false;
193
210
  }
211
+ /** The layer-count companion as a node material: same uniform registry, same flags. */
212
+ createLayerMaterial(sc, material) {
213
+ const { tsl } = material.userData;
214
+ return buildWaveMaterial(tsl, {
215
+ ...this.flagsFor(sc),
216
+ layerPass: true,
217
+ vertexNormal: false
218
+ });
219
+ }
220
+ layerVariantKey(material) {
221
+ return material.userData.variant;
222
+ }
194
223
  /** Which effects the config currently asks for — the node twin of applyPost()'s pass juggling. */
195
224
  postFlags() {
196
225
  const c = this.config;
@@ -244,6 +273,7 @@ function withTslBackend(Base) {
244
273
  }
245
274
  renderComposed() {
246
275
  for (const wave of this.waves) wave.material.userData.tsl.packed.sync();
276
+ this.renderGlassPasses();
247
277
  this.syncPostUniforms();
248
278
  this.ensurePost().render();
249
279
  }
@@ -1 +1 @@
1
- {"version":3,"file":"WaveRendererGPU.js","names":[],"sources":["../../src/renderer/WaveRendererGPU.ts"],"sourcesContent":["/**\n * The WebGPU / TSL backend.\n *\n * Overrides only the four things that are actually backend-specific — the three renderer, the\n * wave material, how a shader variant is re-selected, and the post chain. Everything else (config\n * sync, camera, background, resize, interaction, capture, particles) is shared, because both\n * backends expose the same `uniforms` surface. The overrides are packaged as the\n * {@link withTslBackend} mixin so they can also layer over the studio's editor subclass;\n * {@link WaveRendererGPU} is that mixin applied to the plain {@link WaveRenderer}.\n *\n * `WebGPURenderer` falls back to a WebGL2 backend on its own when WebGPU is unavailable, so this\n * class is not \"the WebGPU-only path\" — it is the TSL path, which runs on either backend. What it\n * must not do is get imported from the package entry: `three/webgpu` pulls the whole node system\n * (~200 KB gzipped), so this module is only ever reached through a dynamic import.\n */\nimport * as THREE from \"three\";\nimport { WebGPURenderer, RenderPipeline, WebGPUCoordinateSystem } from \"three/webgpu\";\nimport { pass, texture, screenUV, vec2 } from \"three/tsl\";\nimport type { WaveConfig } from \"../config/model\";\nimport { WaveRenderer, type WaveMaterial, type WaveParticleField } from \"./WaveRenderer\";\nimport { ParticleFieldGPU } from \"./particleFieldGPU\";\nimport { makeTslUniforms, type WaveTslUniforms } from \"./tsl/uniforms\";\nimport { buildWaveMaterial, type WaveMaterialFlags } from \"./tsl/waveMaterial\";\nimport { wavePointerFxActive, waveRipplesActive } from \"./interactionGates\";\nimport { buildPostChain, type PostChainUniforms, type PostFlags } from \"./tsl/postChain\";\nimport { floatUniform, vec2Uniform } from \"./tsl/types\";\n\n/** The flag set that decides a wave's node graph — the TSL twin of `waveDefines()`. */\nfunction variantKey(f: WaveMaterialFlags): string {\n return [\n f.theme,\n f.loopMotion && \"loop\",\n f.detailOctave && \"detail\",\n f.helix && \"helix\",\n f.twistMotion && \"twist\",\n f.radial && \"radial\",\n f.depthTint && \"depthTint\",\n f.edgeFeather && \"edgeFeather\",\n f.rungs && \"rungs\",\n f.pointerFx && \"pointer\",\n f.pointerRipples && \"ripples\",\n f.webgpuClipZ && \"gpuz\",\n ]\n .filter(Boolean)\n .join(\",\");\n}\n\ntype TslMaterial = WaveMaterial & { userData: { variant: string; tsl: WaveTslUniforms } };\n\n/**\n * The TSL backend as a mixin, so the same override set can sit on either base: over\n * {@link WaveRenderer} for embeds (the {@link WaveRendererGPU} export below), or over the studio's\n * editor subclass (`studio/StudioWaveRendererGPU.ts`). That composition works because the two\n * override DISJOINT hook sets — the editor overrides the camera/overlay hooks, this backend the\n * renderer/material/post hooks — so layering order doesn't matter.\n *\n * `Base` is constrained to (and internally cast as) `typeof WaveRenderer` because TypeScript\n * forbids protected-member access from a class extending a bare type parameter. The cast is sound\n * for any WaveRenderer subclass, and returning `TBase` hands the composed class back with its real\n * base's type (the editor API survives on the studio composition).\n */\nexport function withTslBackend<TBase extends typeof WaveRenderer>(Base: TBase): TBase {\n class WithTslBackend extends (Base as typeof WaveRenderer) {\n declare readonly renderer: THREE.WebGLRenderer & WebGPURenderer;\n private post?: RenderPipeline;\n /**\n * Lazily built, NOT a field initialiser. Subclass field initialisers run only after `super()`\n * returns, and the base constructor already renders (buildWaves → resize → renderOnce), so a\n * field here would still be `undefined` at first draw.\n */\n private postUniformsCache?: PostChainUniforms;\n /** The effect set the current chain was built for; a change rebuilds it, as the WebGL path\n * inserts and removes passes. */\n private postFlagsKey = \"\";\n\n protected override createRenderer(): THREE.WebGLRenderer {\n // Called from the BASE constructor, which is the only hook that runs early enough to stop the\n // first render: `ready` cannot be cleared from this subclass's constructor body or a field\n // initialiser, since both run after super() has already drawn a frame.\n this.ready = false;\n // No preserveDrawingBuffer: WebGPU has no such option and does not need one — the canvas is\n // configured COPY_SRC, so toBlob()/toDataURL() readback works, which is what captureImage,\n // the poster capture and the studio thumbnails rely on.\n return new WebGPURenderer({\n // antialias: false to MATCH the WebGL path, not because MSAA is unwanted. Everything there\n // renders through EffectComposer, whose render target is created without `samples` — so the\n // WebGL canvas's `antialias: true` never applies and that path is effectively unaliased.\n // Enabling MSAA here would make WebGPU quietly render smoother edges than WebGL, which is a\n // visual change disguised as a port. It is a real upgrade to opt into later, deliberately.\n antialias: false,\n alpha: true,\n powerPreference: \"high-performance\",\n }) as unknown as THREE.WebGLRenderer;\n }\n\n private get postUniforms() {\n this.postUniformsCache ??= {\n uBlurAmount: floatUniform(0),\n uBlurSamples: floatUniform(6),\n uGrainAmount: floatUniform(0),\n uBloomStrength: floatUniform(0),\n uBloomRadius: floatUniform(0.4),\n uBloomThreshold: floatUniform(0.85),\n uInnerLight: floatUniform(0),\n uInnerLightDensity: floatUniform(0.5),\n uInnerLightDecay: floatUniform(0.95),\n uInnerLightCenter: vec2Uniform(0.5, 0.15),\n uHalftone: floatUniform(0),\n uHalftoneCell: floatUniform(6),\n uHalftoneAngle: floatUniform(0.4),\n uHeatmap: floatUniform(0),\n uHalftoneCmyk: floatUniform(0),\n uHalftoneCmykCell: floatUniform(6),\n uPaper: floatUniform(0),\n uPaperScale: floatUniform(2),\n uDitherStrength: floatUniform(0),\n uDitherScale: floatUniform(2),\n uDitherSteps: floatUniform(4),\n };\n return this.postUniformsCache;\n }\n\n /** Start the WebGPU backend, then draw the first frame. Safe to call more than once. */\n override async init(): Promise<void> {\n if (this.ready) return;\n await (this.renderer as unknown as WebGPURenderer).init();\n this.ready = true;\n // The constructor's buildWaves()/resize() ran against a not-yet-drawable backend, so their\n // trailing renderOnce() was skipped. Catch up now that the backend is live.\n this.resize();\n this.refresh();\n }\n\n /**\n * True when the live backend uses [0,1] clip Z rather than [-1,1]. Read from the renderer rather\n * than assumed, because WebGPURenderer silently falls back to a WebGL2 backend — in which case\n * the clip convention is the WebGL one and the depth fade must NOT be remapped.\n */\n private get webgpuClipZ(): boolean {\n const cs = (this.renderer as unknown as { coordinateSystem: number }).coordinateSystem;\n return cs === (WebGPUCoordinateSystem as number);\n }\n\n private flagsFor(sc: WaveConfig | undefined): WaveMaterialFlags {\n const bindsDetail =\n sc?.interaction?.bindings?.some((b) => b.target === \"detailAmount\") ?? false;\n const bindsHelix =\n sc?.interaction?.bindings?.some((b) => b.target.startsWith(\"helix\")) ?? false;\n const pointer = !!sc && wavePointerFxActive(this.config, sc);\n return {\n theme: sc?.theme === \"wireframe\" ? \"wireframe\" : \"solid\",\n loopMotion: (this.config.loopSeconds ?? 0) > 0,\n detailOctave: (sc?.detailAmount ?? 0) !== 0 || bindsDetail,\n helix: (sc?.helixRadius ?? 0) !== 0 || (sc?.helixRoll ?? 0) !== 0 || bindsHelix,\n twistMotion: !!sc?.twistMotion,\n radial: (sc?.radialAmount ?? 0) !== 0,\n depthTint: (sc?.depthTint ?? 0) > 0,\n edgeFeather: (sc?.edgeFeather ?? 0.1) !== 0.1,\n rungs: sc?.theme === \"wireframe\" && (sc.rungAmount ?? 0) > 0,\n pointerFx: pointer,\n pointerRipples: pointer && waveRipplesActive(this.config, sc as WaveConfig),\n webgpuClipZ: this.webgpuClipZ,\n };\n }\n\n /**\n * An instanced-sprite particle field wired to THIS wave's uniform registry.\n *\n * That wiring is the whole reason this override exists: the GLSL field has to mirror the wave's\n * shape and pointer uniforms into its own material every frame, whereas here the dust reads the\n * ribbon's nodes directly and cannot drift out of sync with it.\n */\n protected override createParticleField(\n wave: { material: WaveMaterial },\n sc: WaveConfig,\n onReady: () => void,\n ): WaveParticleField {\n const { tsl } = (wave.material as TslMaterial).userData;\n const f = this.flagsFor(sc);\n return new ParticleFieldGPU(\n {\n uniforms: tsl,\n flags: {\n loopMotion: f.loopMotion,\n detailOctave: f.detailOctave,\n helix: f.helix,\n twistMotion: f.twistMotion,\n radial: f.radial,\n pointerFx: f.pointerFx,\n pointerRipples: f.pointerRipples,\n },\n },\n onReady,\n );\n }\n\n protected override createWaveMaterial(sc: WaveConfig | undefined): WaveMaterial {\n const u = makeTslUniforms(this.renderer.getDrawingBufferSize(new THREE.Vector2()));\n const flags = this.flagsFor(sc);\n const material = buildWaveMaterial(u, flags) as unknown as TslMaterial;\n // The shared config-sync path reaches uniforms through `material.uniforms`; the node registry\n // is that same surface, so refresh() needs no backend branch.\n material.uniforms = u as unknown as WaveMaterial[\"uniforms\"];\n material.userData = { variant: variantKey(flags), tsl: u };\n return material;\n }\n\n /**\n * A TSL variant is a different GRAPH, not a different define set, so a variant change rebuilds\n * the material and re-points the mesh at it. The uniform registry is carried over untouched, so\n * no config state is lost and no value has to be re-synced.\n */\n protected override applyWaveVariant(\n wave: { mesh: THREE.Mesh; material: WaveMaterial },\n sc: WaveConfig,\n ): boolean {\n const current = wave.material as TslMaterial;\n const flags = this.flagsFor(sc);\n const key = variantKey(flags);\n if (current.userData.variant === key) return false;\n\n const rebuilt = buildWaveMaterial(current.userData.tsl, flags) as unknown as TslMaterial;\n rebuilt.uniforms = current.uniforms;\n rebuilt.userData = { variant: key, tsl: current.userData.tsl };\n rebuilt.blending = current.blending;\n rebuilt.premultipliedAlpha = current.premultipliedAlpha;\n wave.mesh.material = rebuilt;\n wave.material = rebuilt;\n current.dispose();\n return false; // the mesh already points at a fresh material; nothing to recompile in place\n }\n\n // ---- Post chain --------------------------------------------------------------------------\n\n /** Which effects the config currently asks for — the node twin of applyPost()'s pass juggling. */\n private postFlags(): PostFlags {\n const c = this.config;\n return {\n bloom: (c.bloomStrength ?? 0) > 0,\n innerLight: (c.innerLight ?? 0) > 0,\n halftone: (c.halftone ?? 0) > 0,\n heatmap: (c.heatmap ?? 0) > 0,\n halftoneCmyk: (c.halftoneCmyk ?? 0) > 0,\n paperTexture: (c.paperTexture ?? 0) > 0,\n dither: (c.dither ?? 0) > 0,\n };\n }\n\n /** Push the config into the post uniforms. Mirrors applyPost() / the per-effect apply* methods. */\n private syncPostUniforms(): void {\n const c = this.config;\n const u = this.postUniforms;\n u.uBlurAmount.value = c.blur;\n u.uGrainAmount.value = c.grain;\n u.uBlurSamples.value = Math.round(c.blurSamples ?? 6);\n u.uBloomStrength.value = c.bloomStrength ?? 0;\n u.uBloomRadius.value = c.bloomRadius ?? 0.4;\n u.uBloomThreshold.value = c.bloomThreshold ?? 0.85;\n u.uInnerLight.value = c.innerLight ?? 0;\n u.uInnerLightDensity.value = c.innerLightDensity ?? 0.5;\n u.uInnerLightDecay.value = c.innerLightDecay ?? 0.95;\n u.uInnerLightCenter.value.set(c.innerLightX ?? 0.5, c.innerLightY ?? 0.15);\n u.uHalftone.value = c.halftone ?? 0;\n u.uHalftoneCell.value = Math.max(2, c.halftoneCell ?? 6);\n u.uHalftoneAngle.value = c.halftoneAngle ?? 0.4;\n u.uHeatmap.value = c.heatmap ?? 0;\n u.uHalftoneCmyk.value = c.halftoneCmyk ?? 0;\n u.uHalftoneCmykCell.value = Math.max(2, c.halftoneCmykCell ?? 6);\n u.uPaper.value = c.paperTexture ?? 0;\n u.uPaperScale.value = Math.max(0.5, c.paperTextureScale ?? 2);\n u.uDitherStrength.value = c.dither ?? 0;\n u.uDitherScale.value = Math.max(1, c.ditherScale ?? 2);\n u.uDitherSteps.value = Math.max(2, Math.round(c.ditherSteps ?? 4));\n }\n\n private ensurePost(): RenderPipeline {\n const flags = this.postFlags();\n const key = JSON.stringify(flags);\n if (this.post && key !== this.postFlagsKey) this.disposePost();\n if (!this.post) {\n this.postFlagsKey = key;\n this.post = new RenderPipeline(this.renderer as unknown as WebGPURenderer);\n // The chain places renderOutput() itself, so the finish-zone effects see display-space\n // colour exactly as they do after OutputPass on the WebGL path.\n this.post.outputColorTransform = false;\n this.post.outputNode = buildPostChain(\n pass(this.scene, this.camera),\n this.postUniforms,\n flags,\n ) as never;\n }\n return this.post;\n }\n\n protected override renderComposed(): void {\n // Fold each wave's array-valued uniforms into its shared packed buffer. The config sync writes\n // the logical arrays (`u.uColors.value[i].set(...)`) exactly as on the WebGL path; this is the\n // one extra step that layout needs. Cheap — ~52 vec4s per wave.\n for (const wave of this.waves) {\n (wave.material as TslMaterial).userData.tsl.packed.sync();\n }\n this.syncPostUniforms();\n this.ensurePost().render();\n }\n\n protected override resizePost(w: number, h: number, dpr: number): void {\n this.renderer.setPixelRatio(dpr);\n this.renderer.setSize(w, h, false);\n }\n\n protected override disposePost(): void {\n this.post?.dispose();\n this.post = undefined;\n }\n\n /**\n * WebGPU exposes no `capabilities` object; the equivalent limit lives on the adapter's device.\n * Falls back to 8192, the value every WebGPU implementation is required to support, for the\n * window before `init()` resolves — the base class calls this while sizing the background.\n */\n protected override maxTextureSize(): number {\n const device = (this.renderer as unknown as { backend?: { device?: GPUDevice } }).backend\n ?.device;\n return device?.limits?.maxTextureDimension2D ?? 8192;\n }\n\n /**\n * Rebuild the post chain when the background changes.\n *\n * A `pass()` node captures the scene's background at the point its render context is first built.\n * Because the chain is created lazily on the first draw — when `scene.background` is still null —\n * a background set afterwards would never appear, rendering every non-transparent preset on a\n * transparent canvas. Background changes are user-driven, not per-frame, so rebuilding here is\n * cheap; rebuilding every frame (which also works) is not.\n */\n protected override onBackgroundChanged(): void {\n // The node renderer does NOT support a plain Texture background. Background.update() handles\n // exactly three cases — null, `isColor`, and `isNode` — and anything else hits\n // \"Renderer: Unsupported background configuration.\" So the gradient / image / video backgrounds,\n // which the WebGL renderer takes as `scene.background = texture`, have to be re-expressed as a\n // node. `getBackgroundNode(scene) || scene.background` means backgroundNode wins where set, so\n // `scene.background` can be left alone for the WebGL path's benefit.\n const bg = this.scene.background as THREE.Texture | THREE.Color | null;\n const isTexture = !!bg && (bg as THREE.Texture).isTexture === true;\n // V is flipped because these background textures are CanvasTextures, which default to\n // flipY = true. The WebGL background path honours that flag when it draws the texture across\n // the screen; sampling by screenUV here does not, so the gradient lands mirrored. Costs ~1.4\n // mae on a gradient-background preset and shows up as concentric banding in a diff.\n this.scene.backgroundNode = isTexture\n ? (texture(bg as THREE.Texture).sample(vec2(screenUV.x, screenUV.y.oneMinus())) as never)\n : null;\n this.disposePost();\n }\n }\n return WithTslBackend as unknown as TBase;\n}\n\n/** The TSL/WebGPU renderer over the plain {@link WaveRenderer} base — what embeds reach through\n * `gpu-loader.ts`. A named class (not a bare `withTslBackend(WaveRenderer)` const) so consumers\n * keep `new WaveRendererGPU(...)`, `instanceof`, and a stable class name. */\nexport class WaveRendererGPU extends withTslBackend(WaveRenderer) {}\n"],"mappings":"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"}
1
+ {"version":3,"file":"WaveRendererGPU.js","names":[],"sources":["../../src/renderer/WaveRendererGPU.ts"],"sourcesContent":["/**\n * The WebGPU / TSL backend.\n *\n * Overrides only the four things that are actually backend-specific — the three renderer, the\n * wave material, how a shader variant is re-selected, and the post chain. Everything else (config\n * sync, camera, background, resize, interaction, capture, particles) is shared, because both\n * backends expose the same `uniforms` surface. The overrides are packaged as the\n * {@link withTslBackend} mixin so they can also layer over the studio's editor subclass;\n * {@link WaveRendererGPU} is that mixin applied to the plain {@link WaveRenderer}.\n *\n * `WebGPURenderer` falls back to a WebGL2 backend on its own when WebGPU is unavailable, so this\n * class is not \"the WebGPU-only path\" — it is the TSL path, which runs on either backend. What it\n * must not do is get imported from the package entry: `three/webgpu` pulls the whole node system\n * (~200 KB gzipped), so this module is only ever reached through a dynamic import.\n */\nimport * as THREE from \"three\";\nimport { WebGPURenderer, RenderPipeline, WebGPUCoordinateSystem } from \"three/webgpu\";\nimport { pass, texture, screenUV, vec2 } from \"three/tsl\";\nimport type { WaveConfig } from \"../config/model\";\nimport { WaveRenderer, type WaveMaterial, type WaveParticleField } from \"./WaveRenderer\";\nimport { ParticleFieldGPU } from \"./particleFieldGPU\";\nimport { makeTslUniforms, type WaveTslUniforms } from \"./tsl/uniforms\";\nimport { buildWaveMaterial, type WaveMaterialFlags } from \"./tsl/waveMaterial\";\nimport { wavePointerFxActive, waveRipplesActive } from \"./interactionGates\";\nimport { buildPostChain, type PostChainUniforms, type PostFlags } from \"./tsl/postChain\";\nimport { floatUniform, vec2Uniform } from \"./tsl/types\";\n\n/** The flag set that decides a wave's node graph — the TSL twin of `waveDefines()`. */\nfunction variantKey(f: WaveMaterialFlags): string {\n return [\n f.theme,\n f.vertexNormal && \"vnormal\",\n f.loopMotion && \"loop\",\n f.detailOctave && \"detail\",\n f.helix && \"helix\",\n f.twistMotion && \"twist\",\n f.radial && \"radial\",\n f.path && \"path\",\n f.depthTint && \"depthTint\",\n f.edgeFeather && \"edgeFeather\",\n f.rungs && \"rungs\",\n f.lineSharp && \"lineSharp\",\n f.lineClearGaps && \"clearGaps\",\n f.lineLight && \"lineLight\",\n f.premultiplied && \"premul\",\n f.dissolve && \"dissolve\",\n f.pointerFx && \"pointer\",\n f.pointerRipples && \"ripples\",\n f.webgpuClipZ && \"gpuz\",\n ]\n .filter(Boolean)\n .join(\",\");\n}\n\ntype TslMaterial = WaveMaterial & { userData: { variant: string; tsl: WaveTslUniforms } };\n\n/**\n * The TSL backend as a mixin, so the same override set can sit on either base: over\n * {@link WaveRenderer} for embeds (the {@link WaveRendererGPU} export below), or over the studio's\n * editor subclass (`studio/StudioWaveRendererGPU.ts`). That composition works because the two\n * override DISJOINT hook sets — the editor overrides the camera/overlay hooks, this backend the\n * renderer/material/post hooks — so layering order doesn't matter.\n *\n * `Base` is constrained to (and internally cast as) `typeof WaveRenderer` because TypeScript\n * forbids protected-member access from a class extending a bare type parameter. The cast is sound\n * for any WaveRenderer subclass, and returning `TBase` hands the composed class back with its real\n * base's type (the editor API survives on the studio composition).\n */\nexport function withTslBackend<TBase extends typeof WaveRenderer>(Base: TBase): TBase {\n class WithTslBackend extends (Base as typeof WaveRenderer) {\n declare readonly renderer: THREE.WebGLRenderer & WebGPURenderer;\n private post?: RenderPipeline;\n /**\n * Lazily built, NOT a field initialiser. Subclass field initialisers run only after `super()`\n * returns, and the base constructor already renders (buildWaves → resize → renderOnce), so a\n * field here would still be `undefined` at first draw.\n */\n private postUniformsCache?: PostChainUniforms;\n /** The effect set the current chain was built for; a change rebuilds it, as the WebGL path\n * inserts and removes passes. */\n private postFlagsKey = \"\";\n\n protected override createRenderer(): THREE.WebGLRenderer {\n // Called from the BASE constructor, which is the only hook that runs early enough to stop the\n // first render: `ready` cannot be cleared from this subclass's constructor body or a field\n // initialiser, since both run after super() has already drawn a frame.\n this.ready = false;\n // No preserveDrawingBuffer: WebGPU has no such option and does not need one — the canvas is\n // configured COPY_SRC, so toBlob()/toDataURL() readback works, which is what captureImage,\n // the poster capture and the studio thumbnails rely on.\n return new WebGPURenderer({\n // antialias: false to MATCH the WebGL path, not because MSAA is unwanted. Everything there\n // renders through EffectComposer, whose render target is created without `samples` — so the\n // WebGL canvas's `antialias: true` never applies and that path is effectively unaliased.\n // Enabling MSAA here would make WebGPU quietly render smoother edges than WebGL, which is a\n // visual change disguised as a port. It is a real upgrade to opt into later, deliberately.\n antialias: false,\n alpha: true,\n powerPreference: \"high-performance\",\n }) as unknown as THREE.WebGLRenderer;\n }\n\n private get postUniforms() {\n this.postUniformsCache ??= {\n uBlurAmount: floatUniform(0),\n uBlurSamples: floatUniform(6),\n uGrainAmount: floatUniform(0),\n uBloomStrength: floatUniform(0),\n uBloomRadius: floatUniform(0.4),\n uBloomThreshold: floatUniform(0.85),\n uInnerLight: floatUniform(0),\n uInnerLightDensity: floatUniform(0.5),\n uInnerLightDecay: floatUniform(0.95),\n uInnerLightCenter: vec2Uniform(0.5, 0.15),\n uHalftone: floatUniform(0),\n uHalftoneCell: floatUniform(6),\n uHalftoneAngle: floatUniform(0.4),\n uHeatmap: floatUniform(0),\n uHalftoneCmyk: floatUniform(0),\n uHalftoneCmykCell: floatUniform(6),\n uPaper: floatUniform(0),\n uPaperScale: floatUniform(2),\n uDitherStrength: floatUniform(0),\n uDitherScale: floatUniform(2),\n uDitherSteps: floatUniform(4),\n };\n return this.postUniformsCache;\n }\n\n /** Start the WebGPU backend, then draw the first frame. Safe to call more than once. */\n override async init(): Promise<void> {\n if (this.ready) return;\n await (this.renderer as unknown as WebGPURenderer).init();\n this.ready = true;\n // The constructor's buildWaves()/resize() ran against a not-yet-drawable backend, so their\n // trailing renderOnce() was skipped. Catch up now that the backend is live.\n this.resize();\n this.refresh();\n }\n\n /**\n * True when the live backend uses [0,1] clip Z rather than [-1,1]. Read from the renderer rather\n * than assumed, because WebGPURenderer silently falls back to a WebGL2 backend — in which case\n * the clip convention is the WebGL one and the depth fade must NOT be remapped.\n */\n private get webgpuClipZ(): boolean {\n const cs = (this.renderer as unknown as { coordinateSystem: number }).coordinateSystem;\n return cs === (WebGPUCoordinateSystem as number);\n }\n\n private flagsFor(sc: WaveConfig | undefined): WaveMaterialFlags {\n const bindsDetail =\n sc?.interaction?.bindings?.some((b) => b.target === \"detailAmount\") ?? false;\n const bindsHelix =\n sc?.interaction?.bindings?.some((b) => b.target.startsWith(\"helix\")) ?? false;\n const bindsDissolve =\n sc?.interaction?.bindings?.some((b) => b.target === \"dissolveAmount\") ?? false;\n const pointer = !!sc && wavePointerFxActive(this.config, sc);\n return {\n theme: sc?.theme === \"wireframe\" ? \"wireframe\" : sc?.theme === \"glass\" ? \"glass\" : \"solid\",\n vertexNormal: sc?.theme === \"glass\",\n loopMotion: (this.config.loopSeconds ?? 0) > 0,\n detailOctave: (sc?.detailAmount ?? 0) !== 0 || bindsDetail,\n helix: (sc?.helixRadius ?? 0) !== 0 || (sc?.helixRoll ?? 0) !== 0 || bindsHelix,\n twistMotion: !!sc?.twistMotion,\n radial: (sc?.radialAmount ?? 0) !== 0,\n path: !!sc?.path && sc.path.length >= 2,\n depthTint: (sc?.depthTint ?? 0) > 0,\n edgeFeather: (sc?.edgeFeather ?? 0.1) !== 0.1,\n rungs: sc?.theme === \"wireframe\" && (sc.rungAmount ?? 0) > 0,\n lineSharp: sc?.theme === \"wireframe\" && (sc.lineSharpness ?? 0) > 0,\n // The same rule applyBlendMode uses to set material.premultipliedAlpha, so the graph and\n // the blend factors can never disagree about which convention the output is in.\n premultiplied: ((m) => m === \"squared\" || m === \"multiply\")(sc?.blendMode ?? \"squared\"),\n lineClearGaps:\n sc?.theme === \"wireframe\" &&\n !!sc.lineGapColor &&\n (sc.lineGapColor === \"transparent\" || /^#[0-9a-f]{8}$/i.test(sc.lineGapColor)),\n lineLight: sc?.theme === \"wireframe\" && (sc.lineLight ?? 0) > 0,\n dissolve: !!sc?.dissolve && ((sc.dissolve.amount ?? 0) > 0 || bindsDissolve),\n pointerFx: pointer,\n pointerRipples: pointer && waveRipplesActive(this.config, sc as WaveConfig),\n webgpuClipZ: this.webgpuClipZ,\n };\n }\n\n /**\n * An instanced-sprite particle field wired to THIS wave's uniform registry.\n *\n * That wiring is the whole reason this override exists: the GLSL field has to mirror the wave's\n * shape and pointer uniforms into its own material every frame, whereas here the dust reads the\n * ribbon's nodes directly and cannot drift out of sync with it.\n */\n protected override createParticleField(\n wave: { material: WaveMaterial },\n sc: WaveConfig,\n onReady: () => void,\n ): WaveParticleField {\n const { tsl } = (wave.material as TslMaterial).userData;\n const f = this.flagsFor(sc);\n return new ParticleFieldGPU(\n {\n uniforms: tsl,\n flags: {\n loopMotion: f.loopMotion,\n detailOctave: f.detailOctave,\n helix: f.helix,\n twistMotion: f.twistMotion,\n radial: f.radial,\n path: f.path,\n pointerFx: f.pointerFx,\n pointerRipples: f.pointerRipples,\n dissolve: f.dissolve,\n },\n },\n onReady,\n );\n }\n\n protected override createWaveMaterial(sc: WaveConfig | undefined): WaveMaterial {\n const u = makeTslUniforms(this.renderer.getDrawingBufferSize(new THREE.Vector2()));\n const flags = this.flagsFor(sc);\n const material = buildWaveMaterial(u, flags) as unknown as TslMaterial;\n // The shared config-sync path reaches uniforms through `material.uniforms`; the node registry\n // is that same surface, so refresh() needs no backend branch.\n material.uniforms = u as unknown as WaveMaterial[\"uniforms\"];\n material.userData = { variant: variantKey(flags), tsl: u };\n return material;\n }\n\n /**\n * A TSL variant is a different GRAPH, not a different define set, so a variant change rebuilds\n * the material and re-points the mesh at it. The uniform registry is carried over untouched, so\n * no config state is lost and no value has to be re-synced.\n */\n protected override applyWaveVariant(\n wave: { mesh: THREE.Mesh; material: WaveMaterial },\n sc: WaveConfig,\n ): boolean {\n const current = wave.material as TslMaterial;\n const flags = this.flagsFor(sc);\n const key = variantKey(flags);\n if (current.userData.variant === key) return false;\n\n const rebuilt = buildWaveMaterial(current.userData.tsl, flags) as unknown as TslMaterial;\n rebuilt.uniforms = current.uniforms;\n rebuilt.userData = { variant: key, tsl: current.userData.tsl };\n rebuilt.blending = current.blending;\n rebuilt.premultipliedAlpha = current.premultipliedAlpha;\n wave.mesh.material = rebuilt;\n wave.material = rebuilt;\n current.dispose();\n return false; // the mesh already points at a fresh material; nothing to recompile in place\n }\n\n /** The layer-count companion as a node material: same uniform registry, same flags. */\n protected override createLayerMaterial(sc: WaveConfig, material: WaveMaterial): THREE.Material {\n const { tsl } = (material as TslMaterial).userData;\n return buildWaveMaterial(tsl, { ...this.flagsFor(sc), layerPass: true, vertexNormal: false });\n }\n\n protected override layerVariantKey(material: WaveMaterial): string {\n return (material as TslMaterial).userData.variant;\n }\n\n // ---- Post chain --------------------------------------------------------------------------\n\n /** Which effects the config currently asks for — the node twin of applyPost()'s pass juggling. */\n private postFlags(): PostFlags {\n const c = this.config;\n return {\n bloom: (c.bloomStrength ?? 0) > 0,\n innerLight: (c.innerLight ?? 0) > 0,\n halftone: (c.halftone ?? 0) > 0,\n heatmap: (c.heatmap ?? 0) > 0,\n halftoneCmyk: (c.halftoneCmyk ?? 0) > 0,\n paperTexture: (c.paperTexture ?? 0) > 0,\n dither: (c.dither ?? 0) > 0,\n };\n }\n\n /** Push the config into the post uniforms. Mirrors applyPost() / the per-effect apply* methods. */\n private syncPostUniforms(): void {\n const c = this.config;\n const u = this.postUniforms;\n u.uBlurAmount.value = c.blur;\n u.uGrainAmount.value = c.grain;\n u.uBlurSamples.value = Math.round(c.blurSamples ?? 6);\n u.uBloomStrength.value = c.bloomStrength ?? 0;\n u.uBloomRadius.value = c.bloomRadius ?? 0.4;\n u.uBloomThreshold.value = c.bloomThreshold ?? 0.85;\n u.uInnerLight.value = c.innerLight ?? 0;\n u.uInnerLightDensity.value = c.innerLightDensity ?? 0.5;\n u.uInnerLightDecay.value = c.innerLightDecay ?? 0.95;\n u.uInnerLightCenter.value.set(c.innerLightX ?? 0.5, c.innerLightY ?? 0.15);\n u.uHalftone.value = c.halftone ?? 0;\n u.uHalftoneCell.value = Math.max(2, c.halftoneCell ?? 6);\n u.uHalftoneAngle.value = c.halftoneAngle ?? 0.4;\n u.uHeatmap.value = c.heatmap ?? 0;\n u.uHalftoneCmyk.value = c.halftoneCmyk ?? 0;\n u.uHalftoneCmykCell.value = Math.max(2, c.halftoneCmykCell ?? 6);\n u.uPaper.value = c.paperTexture ?? 0;\n u.uPaperScale.value = Math.max(0.5, c.paperTextureScale ?? 2);\n u.uDitherStrength.value = c.dither ?? 0;\n u.uDitherScale.value = Math.max(1, c.ditherScale ?? 2);\n u.uDitherSteps.value = Math.max(2, Math.round(c.ditherSteps ?? 4));\n }\n\n private ensurePost(): RenderPipeline {\n const flags = this.postFlags();\n const key = JSON.stringify(flags);\n if (this.post && key !== this.postFlagsKey) this.disposePost();\n if (!this.post) {\n this.postFlagsKey = key;\n this.post = new RenderPipeline(this.renderer as unknown as WebGPURenderer);\n // The chain places renderOutput() itself, so the finish-zone effects see display-space\n // colour exactly as they do after OutputPass on the WebGL path.\n this.post.outputColorTransform = false;\n this.post.outputNode = buildPostChain(\n pass(this.scene, this.camera),\n this.postUniforms,\n flags,\n ) as never;\n }\n return this.post;\n }\n\n protected override renderComposed(): void {\n // Fold each wave's array-valued uniforms into its shared packed buffer. The config sync writes\n // the logical arrays (`u.uColors.value[i].set(...)`) exactly as on the WebGL path; this is the\n // one extra step that layout needs. Cheap — ~52 vec4s per wave.\n for (const wave of this.waves) {\n (wave.material as TslMaterial).userData.tsl.packed.sync();\n }\n // Same glass captures the WebGL path takes, and for the same reason — the backdrop, the layer\n // count and the normals all have to exist before the shaded frame reads them.\n this.renderGlassPasses();\n this.syncPostUniforms();\n this.ensurePost().render();\n }\n\n protected override resizePost(w: number, h: number, dpr: number): void {\n this.renderer.setPixelRatio(dpr);\n this.renderer.setSize(w, h, false);\n }\n\n protected override disposePost(): void {\n this.post?.dispose();\n this.post = undefined;\n }\n\n /**\n * WebGPU exposes no `capabilities` object; the equivalent limit lives on the adapter's device.\n * Falls back to 8192, the value every WebGPU implementation is required to support, for the\n * window before `init()` resolves — the base class calls this while sizing the background.\n */\n protected override maxTextureSize(): number {\n const device = (this.renderer as unknown as { backend?: { device?: GPUDevice } }).backend\n ?.device;\n return device?.limits?.maxTextureDimension2D ?? 8192;\n }\n\n /**\n * Rebuild the post chain when the background changes.\n *\n * A `pass()` node captures the scene's background at the point its render context is first built.\n * Because the chain is created lazily on the first draw — when `scene.background` is still null —\n * a background set afterwards would never appear, rendering every non-transparent preset on a\n * transparent canvas. Background changes are user-driven, not per-frame, so rebuilding here is\n * cheap; rebuilding every frame (which also works) is not.\n */\n protected override onBackgroundChanged(): void {\n // The node renderer does NOT support a plain Texture background. Background.update() handles\n // exactly three cases — null, `isColor`, and `isNode` — and anything else hits\n // \"Renderer: Unsupported background configuration.\" So the gradient / image / video backgrounds,\n // which the WebGL renderer takes as `scene.background = texture`, have to be re-expressed as a\n // node. `getBackgroundNode(scene) || scene.background` means backgroundNode wins where set, so\n // `scene.background` can be left alone for the WebGL path's benefit.\n const bg = this.scene.background as THREE.Texture | THREE.Color | null;\n const isTexture = !!bg && (bg as THREE.Texture).isTexture === true;\n // V is flipped because these background textures are CanvasTextures, which default to\n // flipY = true. The WebGL background path honours that flag when it draws the texture across\n // the screen; sampling by screenUV here does not, so the gradient lands mirrored. Costs ~1.4\n // mae on a gradient-background preset and shows up as concentric banding in a diff.\n this.scene.backgroundNode = isTexture\n ? (texture(bg as THREE.Texture).sample(vec2(screenUV.x, screenUV.y.oneMinus())) as never)\n : null;\n this.disposePost();\n }\n }\n return WithTslBackend as unknown as TBase;\n}\n\n/** The TSL/WebGPU renderer over the plain {@link WaveRenderer} base — what embeds reach through\n * `gpu-loader.ts`. A named class (not a bare `withTslBackend(WaveRenderer)` const) so consumers\n * keep `new WaveRendererGPU(...)`, `instanceof`, and a stable class name. */\nexport class WaveRendererGPU extends withTslBackend(WaveRenderer) {}\n"],"mappings":"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"}
@@ -114,6 +114,18 @@ const WAVE_APPLIERS = {
114
114
  helixRadius: waveApplier((w) => w.helixRadius ?? 0, (v, a) => {
115
115
  a.u.uHelixRadius.value = v;
116
116
  }),
117
+ glassRipple: waveApplier((w) => w.glassRipple ?? 0, (v, a) => {
118
+ a.u.uGlassRipple.value = v;
119
+ }),
120
+ glassStrength: waveApplier((w) => w.glassStrength ?? 90, (v, a) => {
121
+ a.u.uGlassStrength.value = v;
122
+ }),
123
+ glassTint: waveApplier((w) => w.glassTint ?? .12, (v, a) => {
124
+ a.u.uGlassTint.value = v;
125
+ }),
126
+ dissolveAmount: waveApplier((w) => w.dissolve?.amount ?? 0, (v, a) => {
127
+ a.u.uDissolveAmount.value = v;
128
+ }),
117
129
  positionX: waveApplier((w) => w.position.x, (v, a) => {
118
130
  a.mesh.position.x = v;
119
131
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"interaction.js","names":[],"sources":["../../src/renderer/interaction.ts"],"sourcesContent":["// The optional interactivity runtime for the wave renderer: a per-wave pointer field (localized\n// cursor effects) + per-wave and scene input→param bindings, driven by ONE shared cursor/scroll.\n// It lives in renderer/ so it stays below the shell/studio/index layers (depcruise); it may import\n// only `three`, ../config/model, and ../util/math.\n//\n// Split of responsibility with WaveRenderer: this controller owns ALL input + smoothing (the one\n// cursor's position / presence / press / velocity, scroll progress + velocity, the `appear` latch,\n// custom inputs, click ripples, and every binding's smoothed 0..1 source value — keyed by binding\n// identity so scene + per-wave binding lists all get their own smoothing). The renderer reads\n// sample() / bindingValue() once per frame and writes uniforms — the pointer field per wave, and\n// bindings via the WAVE_APPLIERS / SCENE_APPLIERS tables. Bindings NEVER mutate `config`.\nimport * as THREE from \"three\";\nimport { clamp01 } from \"../util/math\";\nimport { RIPPLE_SLOTS, tiltActive } from \"./interactionGates\";\nimport { type TiltStatus, TiltSource } from \"./tilt\";\nimport type {\n InteractionSource,\n SceneInteractionBinding,\n SceneInteractionTarget,\n StudioConfig,\n WaveConfig,\n WaveInteractionBinding,\n WaveInteractionTarget,\n} from \"../config/model\";\n\nconst RIPPLE_LIFETIME = 1.5; // seconds a ripple lives (crest travels out + fades by then)\nconst VELOCITY_TAU = 0.08; // pointer-velocity smoothing time constant (seconds)\nconst POINTER_SPEED_REF = 4.0; // NDC/s that normalizes pointerSpeed to 1.0\nconst SCROLL_VELOCITY_REF = 2.0; // progress/s that normalizes scrollVelocity to 1.0\nconst SCROLL_VELOCITY_TAU = 0.15; // scroll-velocity smoothing (seconds)\nconst DEFAULT_POINTER_TAU = 0.12; // pointer-follow smoothing default (seconds)\nconst DEFAULT_TILT_TAU = 0.18; // device-tilt smoothing default (seconds); noisier than a cursor\nconst DEFAULT_BINDING_TAU = 0.25; // per-binding source smoothing default (seconds)\nconst POINTER_SPRING_ZETA = 0.7; // pointer-field damping ratio (<1 → slight overshoot = \"weight\")\nconst MIN_POINTER_TAU = 0.02; // floor before smoothing→spring frequency (omega = 1/tau)\nconst SPRING_MAX_STEP = 1 / 120; // substep the spring below this dt so it stays stable after a stall\nconst SPRING_MAX_SUBSTEPS = 6;\n\ntype AnyBinding = WaveInteractionBinding | SceneInteractionBinding;\n\n/** Frame-rate-independent exponential smoothing factor for time constant `tau` (seconds). */\nfunction alpha(tau: number, dt: number): number {\n return tau > 0 ? 1 - Math.exp(-dt / tau) : 1;\n}\n\n/**\n * Advance a damped spring (`pos`/`vel`) toward `target` by `dt`, using semi-implicit (symplectic)\n * Euler. `omega` is the natural angular frequency (≈ 1/response-time), `zeta` the damping ratio\n * (<1 underdamped → overshoots and settles; 1 critical; >1 sluggish). Unlike a first-order lag this\n * carries momentum, so motion has weight and settles instead of creeping to a dead stop. Substeps\n * when `dt` spikes (e.g. the tab was backgrounded) so a stiff spring can't blow up; ~1 step at 60fps.\n */\nfunction springVec2(\n pos: THREE.Vector2,\n vel: THREE.Vector2,\n target: THREE.Vector2,\n omega: number,\n zeta: number,\n dt: number,\n): void {\n if (dt <= 0) return;\n const steps =\n dt > SPRING_MAX_STEP ? Math.min(Math.ceil(dt / SPRING_MAX_STEP), SPRING_MAX_SUBSTEPS) : 1;\n const h = dt / steps;\n const k = omega * omega;\n const c = 2 * zeta * omega;\n for (let s = 0; s < steps; s++) {\n vel.x += (k * (target.x - pos.x) - c * vel.x) * h;\n vel.y += (k * (target.y - pos.y) - c * vel.y) * h;\n pos.x += vel.x * h;\n pos.y += vel.y * h;\n }\n}\n\n// ---- Binding applier tables -----------------------------------------------------------------\n\n/** What a wave-scoped applier writes into: one wave's uniforms + its mesh transform. */\nexport interface WaveApplyArgs {\n u: Record<string, THREE.IUniform>;\n mesh: THREE.Object3D;\n}\n/** What a scene-scoped applier writes into: the post-pass uniforms + a small out-param the renderer\n * seeds (0 / 1) each frame and reads back (the interaction time-offset + zoom multiplier). */\nexport interface SceneApplyArgs {\n post: Record<string, THREE.IUniform>;\n out: { timeOffset: number; zoom: number };\n}\ninterface WaveApplier {\n base(w: WaveConfig): number;\n apply(value: number, a: WaveApplyArgs): void;\n}\ninterface SceneApplier {\n base(c: StudioConfig): number;\n apply(value: number, a: SceneApplyArgs): void;\n}\n\nconst waveApplier = (\n base: (w: WaveConfig) => number,\n apply: (value: number, a: WaveApplyArgs) => void,\n): WaveApplier => ({ base, apply });\nconst sceneApplier = (\n base: (c: StudioConfig) => number,\n apply: (value: number, a: SceneApplyArgs) => void,\n): SceneApplier => ({ base, apply });\n\n/**\n * Per-wave binding targets → (how to read the authored base value, how to write the modulated one).\n * Each base() mirrors the exact fallback refresh() uses, so a binding at rest (from omitted, source\n * 0) writes the same value the renderer already had — no visible jump. This object is the runtime\n * source of truth for {@link WaveInteractionTarget} (enforced by `satisfies`).\n */\nexport const WAVE_APPLIERS = {\n displaceAmount: waveApplier(\n (w) => w.displaceAmount,\n (v, a) => {\n a.u.uDispAmount.value = v;\n },\n ),\n detailAmount: waveApplier(\n (w) => w.detailAmount ?? 0,\n (v, a) => {\n a.u.uDetailAmount.value = v;\n },\n ),\n twistPowerX: waveApplier(\n (w) => w.twistPower.x,\n (v, a) => {\n a.u.uTwPowX.value = v;\n },\n ),\n twistPowerY: waveApplier(\n (w) => w.twistPower.y,\n (v, a) => {\n a.u.uTwPowY.value = v;\n },\n ),\n twistPowerZ: waveApplier(\n (w) => w.twistPower.z,\n (v, a) => {\n a.u.uTwPowZ.value = v;\n },\n ),\n twistFrequencyX: waveApplier(\n (w) => w.twistFrequency.x,\n (v, a) => {\n a.u.uTwFreqX.value = v;\n },\n ),\n twistFrequencyY: waveApplier(\n (w) => w.twistFrequency.y,\n (v, a) => {\n a.u.uTwFreqY.value = v;\n },\n ),\n twistFrequencyZ: waveApplier(\n (w) => w.twistFrequency.z,\n (v, a) => {\n a.u.uTwFreqZ.value = v;\n },\n ),\n hueShift: waveApplier(\n (w) => w.hueShift,\n (v, a) => {\n a.u.uHueShift.value = v;\n },\n ),\n gradientShift: waveApplier(\n (w) => w.gradientShift ?? 0,\n (v, a) => {\n a.u.uGradShift.value = v;\n },\n ),\n colorSaturation: waveApplier(\n (w) => w.colorSaturation,\n (v, a) => {\n a.u.uSaturation.value = v;\n },\n ),\n opacity: waveApplier(\n (w) => w.opacity,\n (v, a) => {\n a.u.uOpacity.value = v;\n },\n ),\n lineThickness: waveApplier(\n (w) => w.lineThickness ?? 1,\n (v, a) => {\n a.u.uLineThickness.value = v;\n },\n ),\n lineAmount: waveApplier(\n (w) => w.lineAmount ?? 425,\n (v, a) => {\n a.u.uLineAmount.value = v;\n },\n ),\n fiberStrength: waveApplier(\n (w) => w.fiberStrength,\n (v, a) => {\n a.u.uFiberStrength.value = v;\n },\n ),\n sheen: waveApplier(\n (w) => w.sheen ?? 1,\n (v, a) => {\n a.u.uSheen.value = v;\n },\n ),\n iridescence: waveApplier(\n (w) => w.iridescence ?? 0,\n (v, a) => {\n a.u.uIridescence.value = v;\n },\n ),\n // Helix: phase spins the coil, turns winds/unwinds it, radius opens and closes it. All three are\n // inert unless the wave already has a helix, so waveDefines compiles HELIX for a wave that binds\n // one but authors radius/roll at 0 (same reason detailAmount does — see bindsDetail there).\n helixPhase: waveApplier(\n (w) => w.helixPhase ?? 0,\n (v, a) => {\n a.u.uHelixPhase.value = v;\n },\n ),\n helixTurns: waveApplier(\n (w) => w.helixTurns ?? 0,\n (v, a) => {\n a.u.uHelixTurns.value = v;\n },\n ),\n helixRadius: waveApplier(\n (w) => w.helixRadius ?? 0,\n (v, a) => {\n a.u.uHelixRadius.value = v;\n },\n ),\n positionX: waveApplier(\n (w) => w.position.x,\n (v, a) => {\n a.mesh.position.x = v;\n },\n ),\n positionY: waveApplier(\n (w) => w.position.y,\n (v, a) => {\n a.mesh.position.y = v;\n },\n ),\n} satisfies Record<WaveInteractionTarget, WaveApplier>;\n\n/** Scene-level binding targets. base() mirrors updateTime() / applyZoom() / applyPost() fallbacks. */\nexport const SCENE_APPLIERS = {\n timeOffset: sceneApplier(\n (c) => c.timeOffset ?? 0,\n (v, a) => {\n a.out.timeOffset = v;\n },\n ),\n cameraZoom: sceneApplier(\n (c) => c.cameraZoom ?? 1,\n (v, a) => {\n a.out.zoom = v;\n },\n ),\n blur: sceneApplier(\n (c) => c.blur,\n (v, a) => {\n a.post.uBlurAmount.value = v;\n },\n ),\n grain: sceneApplier(\n (c) => c.grain,\n (v, a) => {\n a.post.uGrainAmount.value = v;\n },\n ),\n} satisfies Record<SceneInteractionTarget, SceneApplier>;\n\n// ---- Active-state predicates (keyed off config only, so input never triggers a recompile) ----\n\n// ---- Sample shape + the controller ----------------------------------------------------------\n\ninterface RippleSlot {\n origin: THREE.Vector2; // NDC\n age: number; // seconds since spawn\n amp: number; // 0..1 decay envelope (0 = free slot)\n}\ninterface RippleState extends RippleSlot {\n active: boolean;\n}\n\n/** A per-frame snapshot of the pointer-field state. Fields are LIVE references into the controller's\n * state — read them synchronously each frame; don't retain them. */\nexport interface InteractionSample {\n /** Smoothed pointer position, NDC (-1..1). */\n ndc: THREE.Vector2;\n /** Smoothed pointer presence 0..1 (→ uPointerActive). */\n presence: number;\n /** Click-ripple ring buffer (amp = shared 0..1 envelope; 0 = free slot). */\n ripples: readonly RippleSlot[];\n}\n\n/** A wave's own smoothed pointer-field state — trails the shared cursor at the wave's own rate. */\nexport interface PointerField {\n /** Smoothed pointer position for this wave, NDC (-1..1). */\n ndc: THREE.Vector2;\n /** Spring velocity of `ndc` (NDC/s) — internal spring state, not read by the renderer. */\n vel: THREE.Vector2;\n /** Smoothed pointer presence 0..1 for this wave. */\n presence: number;\n}\n\n/**\n * Owns the one cursor's input + scroll + press/appear/custom and all smoothing. Constructed by the\n * renderer when {@link interactionActive} first turns true, disposed when it turns false. All\n * listeners are passive and container-scoped (the poster overlay passes events through).\n */\nexport class InteractionController {\n /** Studio-only scroll preview: when non-null, overrides the computed scroll progress. */\n scrollOverride: number | null = null;\n\n private readonly ndc = new THREE.Vector2();\n private readonly ndcTarget = new THREE.Vector2();\n private readonly ndcPrev = new THREE.Vector2();\n private readonly velNdc = new THREE.Vector2();\n private presence = 0;\n private presenceTarget = 0;\n /** Whether a REAL pointer is on the element. Tracked apart from `presenceTarget` because tilt can\n * raise presence too (tilt.pointer), and it must yield the moment a finger or cursor shows up. */\n private pointerPresent = false;\n private press = 0;\n private pressTarget = 0;\n private pointerSpeed = 0;\n private scroll = 0;\n private scrollPrev = 0;\n private scrollVel = 0;\n private appearLatched = false;\n /** The orientation sensor, built only for a scene that declares `interaction.tilt`. */\n private tilt: TiltSource | null = null;\n private tiltX = 0.5; // smoothed 0..1 (0.5 = neutral pose), mirroring the pointer's own smoothing\n private tiltY = 0.5;\n private readonly customInputs = new Map<string, number>();\n private readonly ripples: RippleState[] = [];\n // Per-wave pointer-field state (index-parallel to config.waves); each trails the cursor at its own\n // hover smoothing. Grown/shrunk in update().\n private readonly fields: PointerField[] = [];\n // Per-binding smoothing state, keyed by binding-object identity (covers scene + every wave list).\n private readonly bindingState = new Map<\n AnyBinding,\n { value: number; source: InteractionSource }\n >();\n // Scratch set reused by updateBindings every frame (cleared, never reallocated).\n private readonly seenBindings = new Set<AnyBinding>();\n private readonly out: InteractionSample;\n\n constructor(\n private readonly container: HTMLElement,\n private readonly cfg: () => StudioConfig | undefined,\n ) {\n for (let i = 0; i < RIPPLE_SLOTS; i++) {\n this.ripples.push({ origin: new THREE.Vector2(), age: 0, amp: 0, active: false });\n }\n this.out = { ndc: this.ndc, presence: 0, ripples: this.ripples };\n const opts = { passive: true } as const;\n container.addEventListener(\"pointerenter\", this.onPointerEnter, opts);\n container.addEventListener(\"pointermove\", this.onPointerMove, opts);\n container.addEventListener(\"pointerleave\", this.onPointerLeave, opts);\n container.addEventListener(\"pointercancel\", this.onPointerCancel, opts);\n container.addEventListener(\"pointerdown\", this.onPointerDown, opts);\n container.addEventListener(\"pointerup\", this.onPointerUp, opts);\n }\n\n /** Ignore coarse (touch) pointers unless the scene opts in with interaction.touch. */\n private ignore(e: PointerEvent): boolean {\n return e.pointerType === \"touch\" && this.cfg()?.interaction?.touch !== true;\n }\n\n private setNdcTarget(e: PointerEvent): void {\n const rect = this.container.getBoundingClientRect();\n if (rect.width <= 0 || rect.height <= 0) return;\n this.ndcTarget.set(\n ((e.clientX - rect.left) / rect.width) * 2 - 1,\n -(((e.clientY - rect.top) / rect.height) * 2 - 1),\n );\n }\n\n private onPointerEnter = (e: PointerEvent): void => {\n if (this.ignore(e)) return;\n this.pointerPresent = true;\n this.presenceTarget = 1;\n this.setNdcTarget(e);\n };\n private onPointerMove = (e: PointerEvent): void => {\n if (this.ignore(e)) return;\n if (e.pointerType === \"touch\" && this.pressTarget < 0.5) return; // touch: only track while down\n this.pointerPresent = true;\n this.presenceTarget = 1;\n this.setNdcTarget(e);\n };\n private onPointerLeave = (e: PointerEvent): void => {\n if (this.ignore(e)) return;\n this.pointerPresent = false;\n this.presenceTarget = 0;\n this.ndcTarget.set(0, 0); // relax toward centre → pointerX/Y rest at 0.5\n };\n private onPointerCancel = (e: PointerEvent): void => {\n if (this.ignore(e)) return;\n this.pointerPresent = false;\n this.pressTarget = 0;\n this.presenceTarget = 0;\n this.ndcTarget.set(0, 0);\n };\n private onPointerDown = (e: PointerEvent): void => {\n if (this.ignore(e)) return;\n this.pointerPresent = true;\n this.pressTarget = 1;\n this.presenceTarget = 1;\n this.setNdcTarget(e);\n // Spawn a ripple only if some wave actually wants ripples (else it is wasted state).\n const cfg = this.cfg();\n if (cfg && cfg.waves.some((w) => (w.interaction?.press?.ripple ?? 0) > 0)) this.spawnRipple();\n };\n private onPointerUp = (e: PointerEvent): void => {\n if (this.ignore(e)) return;\n this.pressTarget = 0;\n if (e.pointerType === \"touch\") {\n this.pointerPresent = false;\n this.presenceTarget = 0; // touch has no hover — presence ends with the touch\n this.ndcTarget.set(0, 0);\n }\n };\n\n /** Spawn a normalized ripple (envelope 0..1) at the click NDC; per-wave amplitude scales it in the\n * shader. Reuses a free slot or evicts the oldest. */\n private spawnRipple(): void {\n let slot = this.ripples.find((r) => !r.active);\n if (!slot) {\n slot = this.ripples[0];\n for (const r of this.ripples) if (r.age > slot.age) slot = r;\n }\n slot.origin.copy(this.ndcTarget);\n slot.age = 0;\n slot.amp = 1;\n slot.active = true;\n }\n\n /** Advance all smoothed state by `dt` seconds. Called from the render loop with the same delta. */\n update(dt: number): void {\n const cfg = this.cfg();\n if (!cfg) return;\n const d = Math.max(dt, 0);\n // The SHARED pointer state feeds binding sources (hover / pointerX-Y / pointerSpeed / press) at a\n // fixed baseline; each wave's FIELD trails at its own hover smoothing further below.\n const kPointer = alpha(DEFAULT_POINTER_TAU, d);\n\n // Device tilt, BEFORE the pointer block: with `tilt.pointer` on it writes the same ndcTarget the\n // cursor would, and everything downstream (the shared smoothing, each wave's spring) must see it\n // in the frame it changed rather than one frame late.\n this.updateTilt(cfg, d);\n\n // Pointer position + presence + press.\n this.ndcPrev.copy(this.ndc);\n this.ndc.lerp(this.ndcTarget, kPointer);\n this.presence += (this.presenceTarget - this.presence) * kPointer;\n this.press += (this.pressTarget - this.press) * kPointer;\n\n // Velocity (own tau) from the smoothed-position delta.\n if (d > 1e-5) {\n const kv = alpha(VELOCITY_TAU, d);\n this.velNdc.x += ((this.ndc.x - this.ndcPrev.x) / d - this.velNdc.x) * kv;\n this.velNdc.y += ((this.ndc.y - this.ndcPrev.y) / d - this.velNdc.y) * kv;\n }\n this.pointerSpeed = this.presence * clamp01(this.velNdc.length() / POINTER_SPEED_REF);\n\n // Per-wave pointer FIELD: each wave's position is a damped SPRING toward the raw cursor target,\n // so a stack trails the cursor at different rates (parallax) and — because the spring is slightly\n // underdamped — carries a little weight: it overshoots and settles instead of creeping to a dead\n // stop. Presence stays a plain ramp (a spring there could dip below 0 and invert the effect).\n // A wave's hover `smoothing` sets the spring frequency (omega = 1/tau).\n const waves = cfg.waves;\n if (this.fields.length > waves.length) this.fields.length = waves.length;\n for (let i = 0; i < waves.length; i++) {\n let f = this.fields[i];\n if (!f) {\n f = {\n ndc: this.ndcTarget.clone(),\n vel: new THREE.Vector2(),\n presence: this.presenceTarget,\n };\n this.fields[i] = f;\n }\n const tau = Math.max(\n waves[i].interaction?.hover?.smoothing ?? DEFAULT_POINTER_TAU,\n MIN_POINTER_TAU,\n );\n springVec2(f.ndc, f.vel, this.ndcTarget, 1 / tau, POINTER_SPRING_ZETA, d);\n f.presence += (this.presenceTarget - f.presence) * alpha(tau, d);\n }\n\n // Scroll progress + velocity.\n const rawScroll = this.scrollOverride ?? this.computeScroll();\n if (d > 1e-5) {\n const sv = Math.abs(rawScroll - this.scrollPrev) / d;\n this.scrollVel += (sv - this.scrollVel) * alpha(SCROLL_VELOCITY_TAU, d);\n }\n this.scrollPrev = rawScroll;\n this.scroll = rawScroll;\n\n // Appear latch: the render loop is visibility-gated, so the first update() IS first-visible.\n this.appearLatched = true;\n\n // Ripples: age + quadratic-decay envelope.\n for (const r of this.ripples) {\n if (!r.active) continue;\n r.age += d;\n const env = Math.max(0, 1 - r.age / RIPPLE_LIFETIME);\n r.amp = env * env;\n if (r.amp <= 0) r.active = false;\n }\n\n this.updateBindings(cfg, d);\n }\n\n /**\n * Build or drop the tilt sensor as the config declares it, then advance the smoothed axes. The\n * sensor is created lazily and only for a scene that asked for it, so a cursor-only scene attaches\n * no orientation listener at all — and a studio edit that adds or removes the block is picked up\n * on the next frame, the same way the controller itself is.\n */\n private updateTilt(cfg: StudioConfig, dt: number): void {\n if (!tiltActive(cfg)) {\n if (this.tilt) {\n this.tilt.dispose();\n this.tilt = null;\n this.tiltX = this.tiltY = 0.5;\n }\n return;\n }\n const src = this.ensureTilt();\n if (!src) return;\n const tiltCfg = cfg.interaction?.tilt;\n const k = alpha(Math.max(tiltCfg?.smoothing ?? DEFAULT_TILT_TAU, 0), dt);\n this.tiltX += (src.x - this.tiltX) * k;\n this.tiltY += (src.y - this.tiltY) * k;\n\n // Opt-in stand-in for the cursor: a phone has no pointer, so a scene whose bindings and hover\n // fields all read the cursor would sit at dead centre forever. Only while no real pointer is on\n // the element — a finger beats the sensor the instant it lands. NDC y is +1 at the TOP, and\n // tiltY counts DOWN the page (the way a ball rolls), hence the negation.\n if (tiltCfg?.pointer && src.live && !this.pointerPresent) {\n this.ndcTarget.set(this.tiltX * 2 - 1, -(this.tiltY * 2 - 1));\n this.presenceTarget = 1;\n }\n }\n\n /** The sensor, built on first use. Null when the platform has none (every desktop browser). */\n private ensureTilt(): TiltSource | null {\n if (!this.tilt && TiltSource.supported()) {\n this.tilt = new TiltSource(() => this.cfg()?.interaction?.tilt);\n }\n return this.tilt;\n }\n\n /**\n * Ask for the orientation sensor, which on iOS 13+ MUST happen inside a user gesture (a tap\n * handler — not a `setTimeout` or a promise chain that outlives the gesture). Resolves true once\n * readings can flow. False means the platform has no sensor, the scene declares no `tilt` block,\n * or the reader refused. Everywhere that needs no permission, tilt is already live and this\n * simply resolves true.\n */\n async enableTilt(): Promise<boolean> {\n const cfg = this.cfg();\n if (!cfg || !tiltActive(cfg)) return false;\n return (await this.ensureTilt()?.enable()) ?? false;\n }\n\n /** Where the sensor stands — `\"prompt\"` is exactly when a tap-to-enable affordance would help. */\n tiltStatus(): TiltStatus {\n if (!TiltSource.supported()) return \"unsupported\";\n return this.tilt?.status ?? \"prompt\";\n }\n\n /** Take the next reading as the neutral pose (the reader has changed grip). */\n recenterTilt(): void {\n this.tilt?.recenter();\n }\n\n // Indexed loops + a reused scratch set (no per-frame closure/array/Set) — this runs every frame.\n private updateBindings(cfg: StudioConfig, dt: number): void {\n const seen = this.seenBindings;\n seen.clear();\n const sceneBindings = cfg.interaction?.bindings;\n if (sceneBindings) {\n for (let i = 0; i < sceneBindings.length; i++) this.advanceBinding(sceneBindings[i], dt);\n }\n for (let w = 0; w < cfg.waves.length; w++) {\n const bindings = cfg.waves[w].interaction?.bindings;\n if (!bindings) continue;\n for (let i = 0; i < bindings.length; i++) this.advanceBinding(bindings[i], dt);\n }\n // Prune state for bindings that no longer exist (edited/removed slots). advanceBinding puts every\n // seen binding in the map, so map ⊇ seen — equal sizes means nothing is stale to walk for.\n if (this.bindingState.size > seen.size) {\n for (const key of this.bindingState.keys()) if (!seen.has(key)) this.bindingState.delete(key);\n }\n }\n\n /** Advance one binding's smoothed source value by `dt` and mark it live in `seenBindings`. */\n private advanceBinding(b: AnyBinding, dt: number): void {\n this.seenBindings.add(b);\n const raw = this.rawSource(b.source);\n let st = this.bindingState.get(b);\n // (Re)initialise on first sight or when the slot's source changed (studio edit): `appear`\n // ramps from 0 (entrance), every other source snaps to its current value.\n if (!st || st.source !== b.source) {\n st = { value: b.source === \"appear\" ? 0 : raw, source: b.source };\n this.bindingState.set(b, st);\n }\n st.value += (raw - st.value) * alpha(b.smoothing ?? DEFAULT_BINDING_TAU, dt);\n }\n\n /** The current smoothed 0..1 value of a binding's source (0 if the binding is unknown). */\n bindingValue(b: AnyBinding): number {\n return this.bindingState.get(b)?.value ?? 0;\n }\n\n /** The current raw (un-per-binding-smoothed) 0..1 value of a source signal. */\n private rawSource(source: InteractionSource): number {\n switch (source) {\n case \"scroll\":\n return this.scroll;\n case \"hover\":\n return this.presence;\n case \"pointerX\":\n return (this.ndc.x + 1) * 0.5;\n case \"pointerY\":\n return (this.ndc.y + 1) * 0.5;\n case \"pointerSpeed\":\n return this.pointerSpeed;\n case \"press\":\n return this.press;\n case \"scrollVelocity\":\n return clamp01(this.scrollVel / SCROLL_VELOCITY_REF);\n case \"appear\":\n return this.appearLatched ? 1 : 0;\n case \"tiltX\":\n return this.tiltX;\n case \"tiltY\":\n return this.tiltY;\n default:\n // custom:<name> — fed by setInput(name, value).\n return this.customInputs.get(source.slice(\"custom:\".length)) ?? 0;\n }\n }\n\n /** Container progress through the viewport: 0 as it enters from below, 1 once scrolled past. */\n private computeScroll(): number {\n const rect = this.container.getBoundingClientRect();\n const vh = window.innerHeight || document.documentElement.clientHeight || 1;\n return clamp01((vh - rect.top) / (vh + rect.height));\n }\n\n /** The shared pointer-field state + ripples for the renderer (live references — read synchronously). */\n sample(): InteractionSample {\n this.out.presence = this.presence;\n return this.out;\n }\n\n /** This wave's smoothed pointer-field state (it trails the cursor at its own hover smoothing), or\n * null if the wave hasn't been advanced by update() yet (treat as rest). */\n fieldFor(waveIdx: number): PointerField | null {\n return this.fields[waveIdx] ?? null;\n }\n\n /** Velocity-driven agitation drive 0..1 (how fast the cursor is moving, presence-gated). The\n * renderer scales each wave's hover `agitate` by this, so the churn tracks the gesture instead\n * of buzzing at a constant rate whenever the cursor is merely present. */\n pointerFlux(): number {\n return this.pointerSpeed;\n }\n\n /** Smoothed pointer velocity, NDC/s (direction + speed of the drag). The renderer feeds it to the\n * drag-wake shader so the trailing trough forms behind the motion. Live reference — read per frame. */\n pointerVelocity(): THREE.Vector2 {\n return this.velNdc;\n }\n\n /** Feed a `custom:<name>` input (developer API; staged/forwarded by the shell). */\n setInput(name: string, value: number): void {\n if (typeof name !== \"string\" || !Number.isFinite(value)) return;\n this.customInputs.set(name, value);\n }\n\n /**\n * Collapse to the settled resting state for the single frame drawn when the loop stops (paused /\n * reduced-motion / offscreen): presence / velocity / press / pointerSpeed → 0, ripples cleared,\n * scroll → its current raw value, pointer → centre, and `appear` → 1 (reduced-motion users must\n * see the FINAL entered state). Custom inputs KEEP their last explicit values. Each binding snaps\n * to its settled source so the one settled frame shows the final look.\n */\n settle(): void {\n this.presence = this.presenceTarget = 0;\n this.pointerPresent = false;\n this.press = this.pressTarget = 0;\n this.pointerSpeed = 0;\n this.tiltX = this.tiltY = 0.5; // a settled frame is the neutral pose, like the centred cursor\n this.velNdc.set(0, 0);\n this.ndc.set(0, 0);\n this.ndcTarget.set(0, 0);\n this.ndcPrev.set(0, 0);\n for (const f of this.fields) {\n f.ndc.set(0, 0);\n f.vel.set(0, 0);\n f.presence = 0;\n }\n for (const r of this.ripples) {\n r.age = 0;\n r.amp = 0;\n r.active = false;\n }\n const rawScroll = this.scrollOverride ?? this.computeScroll();\n this.scroll = this.scrollPrev = rawScroll;\n this.scrollVel = 0;\n this.appearLatched = true;\n const cfg = this.cfg();\n if (cfg) {\n this.bindingState.clear();\n const snap = (b: AnyBinding): void => {\n this.bindingState.set(b, { value: this.rawSource(b.source), source: b.source });\n };\n for (const b of cfg.interaction?.bindings ?? []) snap(b);\n for (const w of cfg.waves) for (const b of w.interaction?.bindings ?? []) snap(b);\n }\n }\n\n /**\n * Snap scroll progress + the scroll-sourced bindings to the current override at once, leaving\n * every other input (pointer / press / appear / velocity / custom) advancing live. Used by the\n * studio scroll preview: the studio page never really scrolls, so dragging the preview slider is a\n * manual scrub that must reflect the instant you move it — not on the next animation frame, which\n * the browser fully suspends whenever the tab isn't foreground. Unlike settle() (which collapses\n * ALL input to rest for a paused still frame), this touches only the scroll signal.\n */\n snapScroll(): void {\n const raw = this.scrollOverride ?? this.computeScroll();\n this.scroll = this.scrollPrev = raw;\n this.scrollVel = 0; // a static scrub has no velocity\n const cfg = this.cfg();\n if (!cfg) return;\n const snap = (b: AnyBinding): void => {\n if (b.source === \"scroll\" || b.source === \"scrollVelocity\") {\n this.bindingState.set(b, { value: this.rawSource(b.source), source: b.source });\n }\n };\n for (const b of cfg.interaction?.bindings ?? []) snap(b);\n for (const w of cfg.waves) for (const b of w.interaction?.bindings ?? []) snap(b);\n }\n\n dispose(): void {\n const c = this.container;\n c.removeEventListener(\"pointerenter\", this.onPointerEnter);\n c.removeEventListener(\"pointermove\", this.onPointerMove);\n c.removeEventListener(\"pointerleave\", this.onPointerLeave);\n c.removeEventListener(\"pointercancel\", this.onPointerCancel);\n c.removeEventListener(\"pointerdown\", this.onPointerDown);\n c.removeEventListener(\"pointerup\", this.onPointerUp);\n this.tilt?.dispose();\n this.tilt = null;\n this.customInputs.clear();\n this.bindingState.clear();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"interaction.js","names":[],"sources":["../../src/renderer/interaction.ts"],"sourcesContent":["// The optional interactivity runtime for the wave renderer: a per-wave pointer field (localized\n// cursor effects) + per-wave and scene input→param bindings, driven by ONE shared cursor/scroll.\n// It lives in renderer/ so it stays below the shell/studio/index layers (depcruise); it may import\n// only `three`, ../config/model, and ../util/math.\n//\n// Split of responsibility with WaveRenderer: this controller owns ALL input + smoothing (the one\n// cursor's position / presence / press / velocity, scroll progress + velocity, the `appear` latch,\n// custom inputs, click ripples, and every binding's smoothed 0..1 source value — keyed by binding\n// identity so scene + per-wave binding lists all get their own smoothing). The renderer reads\n// sample() / bindingValue() once per frame and writes uniforms — the pointer field per wave, and\n// bindings via the WAVE_APPLIERS / SCENE_APPLIERS tables. Bindings NEVER mutate `config`.\nimport * as THREE from \"three\";\nimport { clamp01 } from \"../util/math\";\nimport { RIPPLE_SLOTS, tiltActive } from \"./interactionGates\";\nimport { type TiltStatus, TiltSource } from \"./tilt\";\nimport type {\n InteractionSource,\n SceneInteractionBinding,\n SceneInteractionTarget,\n StudioConfig,\n WaveConfig,\n WaveInteractionBinding,\n WaveInteractionTarget,\n} from \"../config/model\";\n\nconst RIPPLE_LIFETIME = 1.5; // seconds a ripple lives (crest travels out + fades by then)\nconst VELOCITY_TAU = 0.08; // pointer-velocity smoothing time constant (seconds)\nconst POINTER_SPEED_REF = 4.0; // NDC/s that normalizes pointerSpeed to 1.0\nconst SCROLL_VELOCITY_REF = 2.0; // progress/s that normalizes scrollVelocity to 1.0\nconst SCROLL_VELOCITY_TAU = 0.15; // scroll-velocity smoothing (seconds)\nconst DEFAULT_POINTER_TAU = 0.12; // pointer-follow smoothing default (seconds)\nconst DEFAULT_TILT_TAU = 0.18; // device-tilt smoothing default (seconds); noisier than a cursor\nconst DEFAULT_BINDING_TAU = 0.25; // per-binding source smoothing default (seconds)\nconst POINTER_SPRING_ZETA = 0.7; // pointer-field damping ratio (<1 → slight overshoot = \"weight\")\nconst MIN_POINTER_TAU = 0.02; // floor before smoothing→spring frequency (omega = 1/tau)\nconst SPRING_MAX_STEP = 1 / 120; // substep the spring below this dt so it stays stable after a stall\nconst SPRING_MAX_SUBSTEPS = 6;\n\ntype AnyBinding = WaveInteractionBinding | SceneInteractionBinding;\n\n/** Frame-rate-independent exponential smoothing factor for time constant `tau` (seconds). */\nfunction alpha(tau: number, dt: number): number {\n return tau > 0 ? 1 - Math.exp(-dt / tau) : 1;\n}\n\n/**\n * Advance a damped spring (`pos`/`vel`) toward `target` by `dt`, using semi-implicit (symplectic)\n * Euler. `omega` is the natural angular frequency (≈ 1/response-time), `zeta` the damping ratio\n * (<1 underdamped → overshoots and settles; 1 critical; >1 sluggish). Unlike a first-order lag this\n * carries momentum, so motion has weight and settles instead of creeping to a dead stop. Substeps\n * when `dt` spikes (e.g. the tab was backgrounded) so a stiff spring can't blow up; ~1 step at 60fps.\n */\nfunction springVec2(\n pos: THREE.Vector2,\n vel: THREE.Vector2,\n target: THREE.Vector2,\n omega: number,\n zeta: number,\n dt: number,\n): void {\n if (dt <= 0) return;\n const steps =\n dt > SPRING_MAX_STEP ? Math.min(Math.ceil(dt / SPRING_MAX_STEP), SPRING_MAX_SUBSTEPS) : 1;\n const h = dt / steps;\n const k = omega * omega;\n const c = 2 * zeta * omega;\n for (let s = 0; s < steps; s++) {\n vel.x += (k * (target.x - pos.x) - c * vel.x) * h;\n vel.y += (k * (target.y - pos.y) - c * vel.y) * h;\n pos.x += vel.x * h;\n pos.y += vel.y * h;\n }\n}\n\n// ---- Binding applier tables -----------------------------------------------------------------\n\n/** What a wave-scoped applier writes into: one wave's uniforms + its mesh transform. */\nexport interface WaveApplyArgs {\n u: Record<string, THREE.IUniform>;\n mesh: THREE.Object3D;\n}\n/** What a scene-scoped applier writes into: the post-pass uniforms + a small out-param the renderer\n * seeds (0 / 1) each frame and reads back (the interaction time-offset + zoom multiplier). */\nexport interface SceneApplyArgs {\n post: Record<string, THREE.IUniform>;\n out: { timeOffset: number; zoom: number };\n}\ninterface WaveApplier {\n base(w: WaveConfig): number;\n apply(value: number, a: WaveApplyArgs): void;\n}\ninterface SceneApplier {\n base(c: StudioConfig): number;\n apply(value: number, a: SceneApplyArgs): void;\n}\n\nconst waveApplier = (\n base: (w: WaveConfig) => number,\n apply: (value: number, a: WaveApplyArgs) => void,\n): WaveApplier => ({ base, apply });\nconst sceneApplier = (\n base: (c: StudioConfig) => number,\n apply: (value: number, a: SceneApplyArgs) => void,\n): SceneApplier => ({ base, apply });\n\n/**\n * Per-wave binding targets → (how to read the authored base value, how to write the modulated one).\n * Each base() mirrors the exact fallback refresh() uses, so a binding at rest (from omitted, source\n * 0) writes the same value the renderer already had — no visible jump. This object is the runtime\n * source of truth for {@link WaveInteractionTarget} (enforced by `satisfies`).\n */\nexport const WAVE_APPLIERS = {\n displaceAmount: waveApplier(\n (w) => w.displaceAmount,\n (v, a) => {\n a.u.uDispAmount.value = v;\n },\n ),\n detailAmount: waveApplier(\n (w) => w.detailAmount ?? 0,\n (v, a) => {\n a.u.uDetailAmount.value = v;\n },\n ),\n twistPowerX: waveApplier(\n (w) => w.twistPower.x,\n (v, a) => {\n a.u.uTwPowX.value = v;\n },\n ),\n twistPowerY: waveApplier(\n (w) => w.twistPower.y,\n (v, a) => {\n a.u.uTwPowY.value = v;\n },\n ),\n twistPowerZ: waveApplier(\n (w) => w.twistPower.z,\n (v, a) => {\n a.u.uTwPowZ.value = v;\n },\n ),\n twistFrequencyX: waveApplier(\n (w) => w.twistFrequency.x,\n (v, a) => {\n a.u.uTwFreqX.value = v;\n },\n ),\n twistFrequencyY: waveApplier(\n (w) => w.twistFrequency.y,\n (v, a) => {\n a.u.uTwFreqY.value = v;\n },\n ),\n twistFrequencyZ: waveApplier(\n (w) => w.twistFrequency.z,\n (v, a) => {\n a.u.uTwFreqZ.value = v;\n },\n ),\n hueShift: waveApplier(\n (w) => w.hueShift,\n (v, a) => {\n a.u.uHueShift.value = v;\n },\n ),\n gradientShift: waveApplier(\n (w) => w.gradientShift ?? 0,\n (v, a) => {\n a.u.uGradShift.value = v;\n },\n ),\n colorSaturation: waveApplier(\n (w) => w.colorSaturation,\n (v, a) => {\n a.u.uSaturation.value = v;\n },\n ),\n opacity: waveApplier(\n (w) => w.opacity,\n (v, a) => {\n a.u.uOpacity.value = v;\n },\n ),\n lineThickness: waveApplier(\n (w) => w.lineThickness ?? 1,\n (v, a) => {\n a.u.uLineThickness.value = v;\n },\n ),\n lineAmount: waveApplier(\n (w) => w.lineAmount ?? 425,\n (v, a) => {\n a.u.uLineAmount.value = v;\n },\n ),\n fiberStrength: waveApplier(\n (w) => w.fiberStrength,\n (v, a) => {\n a.u.uFiberStrength.value = v;\n },\n ),\n sheen: waveApplier(\n (w) => w.sheen ?? 1,\n (v, a) => {\n a.u.uSheen.value = v;\n },\n ),\n iridescence: waveApplier(\n (w) => w.iridescence ?? 0,\n (v, a) => {\n a.u.uIridescence.value = v;\n },\n ),\n // Helix: phase spins the coil, turns winds/unwinds it, radius opens and closes it. All three are\n // inert unless the wave already has a helix, so waveDefines compiles HELIX for a wave that binds\n // one but authors radius/roll at 0 (same reason detailAmount does — see bindsDetail there).\n helixPhase: waveApplier(\n (w) => w.helixPhase ?? 0,\n (v, a) => {\n a.u.uHelixPhase.value = v;\n },\n ),\n helixTurns: waveApplier(\n (w) => w.helixTurns ?? 0,\n (v, a) => {\n a.u.uHelixTurns.value = v;\n },\n ),\n helixRadius: waveApplier(\n (w) => w.helixRadius ?? 0,\n (v, a) => {\n a.u.uHelixRadius.value = v;\n },\n ),\n // The dissolve front. Writing the wave's own uniform is enough for the dust too: the particle\n // field MIRRORS this wave's uniforms every frame in updateSceneFx(), which runs immediately after\n // applyInteraction() — the same path the pointer uniforms already take.\n glassRipple: waveApplier(\n (w) => w.glassRipple ?? 0,\n (v, a) => {\n a.u.uGlassRipple.value = v;\n },\n ),\n glassStrength: waveApplier(\n (w) => w.glassStrength ?? 90,\n (v, a) => {\n a.u.uGlassStrength.value = v;\n },\n ),\n glassTint: waveApplier(\n (w) => w.glassTint ?? 0.12,\n (v, a) => {\n a.u.uGlassTint.value = v;\n },\n ),\n dissolveAmount: waveApplier(\n (w) => w.dissolve?.amount ?? 0,\n (v, a) => {\n a.u.uDissolveAmount.value = v;\n },\n ),\n positionX: waveApplier(\n (w) => w.position.x,\n (v, a) => {\n a.mesh.position.x = v;\n },\n ),\n positionY: waveApplier(\n (w) => w.position.y,\n (v, a) => {\n a.mesh.position.y = v;\n },\n ),\n} satisfies Record<WaveInteractionTarget, WaveApplier>;\n\n/** Scene-level binding targets. base() mirrors updateTime() / applyZoom() / applyPost() fallbacks. */\nexport const SCENE_APPLIERS = {\n timeOffset: sceneApplier(\n (c) => c.timeOffset ?? 0,\n (v, a) => {\n a.out.timeOffset = v;\n },\n ),\n cameraZoom: sceneApplier(\n (c) => c.cameraZoom ?? 1,\n (v, a) => {\n a.out.zoom = v;\n },\n ),\n blur: sceneApplier(\n (c) => c.blur,\n (v, a) => {\n a.post.uBlurAmount.value = v;\n },\n ),\n grain: sceneApplier(\n (c) => c.grain,\n (v, a) => {\n a.post.uGrainAmount.value = v;\n },\n ),\n} satisfies Record<SceneInteractionTarget, SceneApplier>;\n\n// ---- Active-state predicates (keyed off config only, so input never triggers a recompile) ----\n\n// ---- Sample shape + the controller ----------------------------------------------------------\n\ninterface RippleSlot {\n origin: THREE.Vector2; // NDC\n age: number; // seconds since spawn\n amp: number; // 0..1 decay envelope (0 = free slot)\n}\ninterface RippleState extends RippleSlot {\n active: boolean;\n}\n\n/** A per-frame snapshot of the pointer-field state. Fields are LIVE references into the controller's\n * state — read them synchronously each frame; don't retain them. */\nexport interface InteractionSample {\n /** Smoothed pointer position, NDC (-1..1). */\n ndc: THREE.Vector2;\n /** Smoothed pointer presence 0..1 (→ uPointerActive). */\n presence: number;\n /** Click-ripple ring buffer (amp = shared 0..1 envelope; 0 = free slot). */\n ripples: readonly RippleSlot[];\n}\n\n/** A wave's own smoothed pointer-field state — trails the shared cursor at the wave's own rate. */\nexport interface PointerField {\n /** Smoothed pointer position for this wave, NDC (-1..1). */\n ndc: THREE.Vector2;\n /** Spring velocity of `ndc` (NDC/s) — internal spring state, not read by the renderer. */\n vel: THREE.Vector2;\n /** Smoothed pointer presence 0..1 for this wave. */\n presence: number;\n}\n\n/**\n * Owns the one cursor's input + scroll + press/appear/custom and all smoothing. Constructed by the\n * renderer when {@link interactionActive} first turns true, disposed when it turns false. All\n * listeners are passive and container-scoped (the poster overlay passes events through).\n */\nexport class InteractionController {\n /** Studio-only scroll preview: when non-null, overrides the computed scroll progress. */\n scrollOverride: number | null = null;\n\n private readonly ndc = new THREE.Vector2();\n private readonly ndcTarget = new THREE.Vector2();\n private readonly ndcPrev = new THREE.Vector2();\n private readonly velNdc = new THREE.Vector2();\n private presence = 0;\n private presenceTarget = 0;\n /** Whether a REAL pointer is on the element. Tracked apart from `presenceTarget` because tilt can\n * raise presence too (tilt.pointer), and it must yield the moment a finger or cursor shows up. */\n private pointerPresent = false;\n private press = 0;\n private pressTarget = 0;\n private pointerSpeed = 0;\n private scroll = 0;\n private scrollPrev = 0;\n private scrollVel = 0;\n private appearLatched = false;\n /** The orientation sensor, built only for a scene that declares `interaction.tilt`. */\n private tilt: TiltSource | null = null;\n private tiltX = 0.5; // smoothed 0..1 (0.5 = neutral pose), mirroring the pointer's own smoothing\n private tiltY = 0.5;\n private readonly customInputs = new Map<string, number>();\n private readonly ripples: RippleState[] = [];\n // Per-wave pointer-field state (index-parallel to config.waves); each trails the cursor at its own\n // hover smoothing. Grown/shrunk in update().\n private readonly fields: PointerField[] = [];\n // Per-binding smoothing state, keyed by binding-object identity (covers scene + every wave list).\n private readonly bindingState = new Map<\n AnyBinding,\n { value: number; source: InteractionSource }\n >();\n // Scratch set reused by updateBindings every frame (cleared, never reallocated).\n private readonly seenBindings = new Set<AnyBinding>();\n private readonly out: InteractionSample;\n\n constructor(\n private readonly container: HTMLElement,\n private readonly cfg: () => StudioConfig | undefined,\n ) {\n for (let i = 0; i < RIPPLE_SLOTS; i++) {\n this.ripples.push({ origin: new THREE.Vector2(), age: 0, amp: 0, active: false });\n }\n this.out = { ndc: this.ndc, presence: 0, ripples: this.ripples };\n const opts = { passive: true } as const;\n container.addEventListener(\"pointerenter\", this.onPointerEnter, opts);\n container.addEventListener(\"pointermove\", this.onPointerMove, opts);\n container.addEventListener(\"pointerleave\", this.onPointerLeave, opts);\n container.addEventListener(\"pointercancel\", this.onPointerCancel, opts);\n container.addEventListener(\"pointerdown\", this.onPointerDown, opts);\n container.addEventListener(\"pointerup\", this.onPointerUp, opts);\n }\n\n /** Ignore coarse (touch) pointers unless the scene opts in with interaction.touch. */\n private ignore(e: PointerEvent): boolean {\n return e.pointerType === \"touch\" && this.cfg()?.interaction?.touch !== true;\n }\n\n private setNdcTarget(e: PointerEvent): void {\n const rect = this.container.getBoundingClientRect();\n if (rect.width <= 0 || rect.height <= 0) return;\n this.ndcTarget.set(\n ((e.clientX - rect.left) / rect.width) * 2 - 1,\n -(((e.clientY - rect.top) / rect.height) * 2 - 1),\n );\n }\n\n private onPointerEnter = (e: PointerEvent): void => {\n if (this.ignore(e)) return;\n this.pointerPresent = true;\n this.presenceTarget = 1;\n this.setNdcTarget(e);\n };\n private onPointerMove = (e: PointerEvent): void => {\n if (this.ignore(e)) return;\n if (e.pointerType === \"touch\" && this.pressTarget < 0.5) return; // touch: only track while down\n this.pointerPresent = true;\n this.presenceTarget = 1;\n this.setNdcTarget(e);\n };\n private onPointerLeave = (e: PointerEvent): void => {\n if (this.ignore(e)) return;\n this.pointerPresent = false;\n this.presenceTarget = 0;\n this.ndcTarget.set(0, 0); // relax toward centre → pointerX/Y rest at 0.5\n };\n private onPointerCancel = (e: PointerEvent): void => {\n if (this.ignore(e)) return;\n this.pointerPresent = false;\n this.pressTarget = 0;\n this.presenceTarget = 0;\n this.ndcTarget.set(0, 0);\n };\n private onPointerDown = (e: PointerEvent): void => {\n if (this.ignore(e)) return;\n this.pointerPresent = true;\n this.pressTarget = 1;\n this.presenceTarget = 1;\n this.setNdcTarget(e);\n // Spawn a ripple only if some wave actually wants ripples (else it is wasted state).\n const cfg = this.cfg();\n if (cfg && cfg.waves.some((w) => (w.interaction?.press?.ripple ?? 0) > 0)) this.spawnRipple();\n };\n private onPointerUp = (e: PointerEvent): void => {\n if (this.ignore(e)) return;\n this.pressTarget = 0;\n if (e.pointerType === \"touch\") {\n this.pointerPresent = false;\n this.presenceTarget = 0; // touch has no hover — presence ends with the touch\n this.ndcTarget.set(0, 0);\n }\n };\n\n /** Spawn a normalized ripple (envelope 0..1) at the click NDC; per-wave amplitude scales it in the\n * shader. Reuses a free slot or evicts the oldest. */\n private spawnRipple(): void {\n let slot = this.ripples.find((r) => !r.active);\n if (!slot) {\n slot = this.ripples[0];\n for (const r of this.ripples) if (r.age > slot.age) slot = r;\n }\n slot.origin.copy(this.ndcTarget);\n slot.age = 0;\n slot.amp = 1;\n slot.active = true;\n }\n\n /** Advance all smoothed state by `dt` seconds. Called from the render loop with the same delta. */\n update(dt: number): void {\n const cfg = this.cfg();\n if (!cfg) return;\n const d = Math.max(dt, 0);\n // The SHARED pointer state feeds binding sources (hover / pointerX-Y / pointerSpeed / press) at a\n // fixed baseline; each wave's FIELD trails at its own hover smoothing further below.\n const kPointer = alpha(DEFAULT_POINTER_TAU, d);\n\n // Device tilt, BEFORE the pointer block: with `tilt.pointer` on it writes the same ndcTarget the\n // cursor would, and everything downstream (the shared smoothing, each wave's spring) must see it\n // in the frame it changed rather than one frame late.\n this.updateTilt(cfg, d);\n\n // Pointer position + presence + press.\n this.ndcPrev.copy(this.ndc);\n this.ndc.lerp(this.ndcTarget, kPointer);\n this.presence += (this.presenceTarget - this.presence) * kPointer;\n this.press += (this.pressTarget - this.press) * kPointer;\n\n // Velocity (own tau) from the smoothed-position delta.\n if (d > 1e-5) {\n const kv = alpha(VELOCITY_TAU, d);\n this.velNdc.x += ((this.ndc.x - this.ndcPrev.x) / d - this.velNdc.x) * kv;\n this.velNdc.y += ((this.ndc.y - this.ndcPrev.y) / d - this.velNdc.y) * kv;\n }\n this.pointerSpeed = this.presence * clamp01(this.velNdc.length() / POINTER_SPEED_REF);\n\n // Per-wave pointer FIELD: each wave's position is a damped SPRING toward the raw cursor target,\n // so a stack trails the cursor at different rates (parallax) and — because the spring is slightly\n // underdamped — carries a little weight: it overshoots and settles instead of creeping to a dead\n // stop. Presence stays a plain ramp (a spring there could dip below 0 and invert the effect).\n // A wave's hover `smoothing` sets the spring frequency (omega = 1/tau).\n const waves = cfg.waves;\n if (this.fields.length > waves.length) this.fields.length = waves.length;\n for (let i = 0; i < waves.length; i++) {\n let f = this.fields[i];\n if (!f) {\n f = {\n ndc: this.ndcTarget.clone(),\n vel: new THREE.Vector2(),\n presence: this.presenceTarget,\n };\n this.fields[i] = f;\n }\n const tau = Math.max(\n waves[i].interaction?.hover?.smoothing ?? DEFAULT_POINTER_TAU,\n MIN_POINTER_TAU,\n );\n springVec2(f.ndc, f.vel, this.ndcTarget, 1 / tau, POINTER_SPRING_ZETA, d);\n f.presence += (this.presenceTarget - f.presence) * alpha(tau, d);\n }\n\n // Scroll progress + velocity.\n const rawScroll = this.scrollOverride ?? this.computeScroll();\n if (d > 1e-5) {\n const sv = Math.abs(rawScroll - this.scrollPrev) / d;\n this.scrollVel += (sv - this.scrollVel) * alpha(SCROLL_VELOCITY_TAU, d);\n }\n this.scrollPrev = rawScroll;\n this.scroll = rawScroll;\n\n // Appear latch: the render loop is visibility-gated, so the first update() IS first-visible.\n this.appearLatched = true;\n\n // Ripples: age + quadratic-decay envelope.\n for (const r of this.ripples) {\n if (!r.active) continue;\n r.age += d;\n const env = Math.max(0, 1 - r.age / RIPPLE_LIFETIME);\n r.amp = env * env;\n if (r.amp <= 0) r.active = false;\n }\n\n this.updateBindings(cfg, d);\n }\n\n /**\n * Build or drop the tilt sensor as the config declares it, then advance the smoothed axes. The\n * sensor is created lazily and only for a scene that asked for it, so a cursor-only scene attaches\n * no orientation listener at all — and a studio edit that adds or removes the block is picked up\n * on the next frame, the same way the controller itself is.\n */\n private updateTilt(cfg: StudioConfig, dt: number): void {\n if (!tiltActive(cfg)) {\n if (this.tilt) {\n this.tilt.dispose();\n this.tilt = null;\n this.tiltX = this.tiltY = 0.5;\n }\n return;\n }\n const src = this.ensureTilt();\n if (!src) return;\n const tiltCfg = cfg.interaction?.tilt;\n const k = alpha(Math.max(tiltCfg?.smoothing ?? DEFAULT_TILT_TAU, 0), dt);\n this.tiltX += (src.x - this.tiltX) * k;\n this.tiltY += (src.y - this.tiltY) * k;\n\n // Opt-in stand-in for the cursor: a phone has no pointer, so a scene whose bindings and hover\n // fields all read the cursor would sit at dead centre forever. Only while no real pointer is on\n // the element — a finger beats the sensor the instant it lands. NDC y is +1 at the TOP, and\n // tiltY counts DOWN the page (the way a ball rolls), hence the negation.\n if (tiltCfg?.pointer && src.live && !this.pointerPresent) {\n this.ndcTarget.set(this.tiltX * 2 - 1, -(this.tiltY * 2 - 1));\n this.presenceTarget = 1;\n }\n }\n\n /** The sensor, built on first use. Null when the platform has none (every desktop browser). */\n private ensureTilt(): TiltSource | null {\n if (!this.tilt && TiltSource.supported()) {\n this.tilt = new TiltSource(() => this.cfg()?.interaction?.tilt);\n }\n return this.tilt;\n }\n\n /**\n * Ask for the orientation sensor, which on iOS 13+ MUST happen inside a user gesture (a tap\n * handler — not a `setTimeout` or a promise chain that outlives the gesture). Resolves true once\n * readings can flow. False means the platform has no sensor, the scene declares no `tilt` block,\n * or the reader refused. Everywhere that needs no permission, tilt is already live and this\n * simply resolves true.\n */\n async enableTilt(): Promise<boolean> {\n const cfg = this.cfg();\n if (!cfg || !tiltActive(cfg)) return false;\n return (await this.ensureTilt()?.enable()) ?? false;\n }\n\n /** Where the sensor stands — `\"prompt\"` is exactly when a tap-to-enable affordance would help. */\n tiltStatus(): TiltStatus {\n if (!TiltSource.supported()) return \"unsupported\";\n return this.tilt?.status ?? \"prompt\";\n }\n\n /** Take the next reading as the neutral pose (the reader has changed grip). */\n recenterTilt(): void {\n this.tilt?.recenter();\n }\n\n // Indexed loops + a reused scratch set (no per-frame closure/array/Set) — this runs every frame.\n private updateBindings(cfg: StudioConfig, dt: number): void {\n const seen = this.seenBindings;\n seen.clear();\n const sceneBindings = cfg.interaction?.bindings;\n if (sceneBindings) {\n for (let i = 0; i < sceneBindings.length; i++) this.advanceBinding(sceneBindings[i], dt);\n }\n for (let w = 0; w < cfg.waves.length; w++) {\n const bindings = cfg.waves[w].interaction?.bindings;\n if (!bindings) continue;\n for (let i = 0; i < bindings.length; i++) this.advanceBinding(bindings[i], dt);\n }\n // Prune state for bindings that no longer exist (edited/removed slots). advanceBinding puts every\n // seen binding in the map, so map ⊇ seen — equal sizes means nothing is stale to walk for.\n if (this.bindingState.size > seen.size) {\n for (const key of this.bindingState.keys()) if (!seen.has(key)) this.bindingState.delete(key);\n }\n }\n\n /** Advance one binding's smoothed source value by `dt` and mark it live in `seenBindings`. */\n private advanceBinding(b: AnyBinding, dt: number): void {\n this.seenBindings.add(b);\n const raw = this.rawSource(b.source);\n let st = this.bindingState.get(b);\n // (Re)initialise on first sight or when the slot's source changed (studio edit): `appear`\n // ramps from 0 (entrance), every other source snaps to its current value.\n if (!st || st.source !== b.source) {\n st = { value: b.source === \"appear\" ? 0 : raw, source: b.source };\n this.bindingState.set(b, st);\n }\n st.value += (raw - st.value) * alpha(b.smoothing ?? DEFAULT_BINDING_TAU, dt);\n }\n\n /** The current smoothed 0..1 value of a binding's source (0 if the binding is unknown). */\n bindingValue(b: AnyBinding): number {\n return this.bindingState.get(b)?.value ?? 0;\n }\n\n /** The current raw (un-per-binding-smoothed) 0..1 value of a source signal. */\n private rawSource(source: InteractionSource): number {\n switch (source) {\n case \"scroll\":\n return this.scroll;\n case \"hover\":\n return this.presence;\n case \"pointerX\":\n return (this.ndc.x + 1) * 0.5;\n case \"pointerY\":\n return (this.ndc.y + 1) * 0.5;\n case \"pointerSpeed\":\n return this.pointerSpeed;\n case \"press\":\n return this.press;\n case \"scrollVelocity\":\n return clamp01(this.scrollVel / SCROLL_VELOCITY_REF);\n case \"appear\":\n return this.appearLatched ? 1 : 0;\n case \"tiltX\":\n return this.tiltX;\n case \"tiltY\":\n return this.tiltY;\n default:\n // custom:<name> — fed by setInput(name, value).\n return this.customInputs.get(source.slice(\"custom:\".length)) ?? 0;\n }\n }\n\n /** Container progress through the viewport: 0 as it enters from below, 1 once scrolled past. */\n private computeScroll(): number {\n const rect = this.container.getBoundingClientRect();\n const vh = window.innerHeight || document.documentElement.clientHeight || 1;\n return clamp01((vh - rect.top) / (vh + rect.height));\n }\n\n /** The shared pointer-field state + ripples for the renderer (live references — read synchronously). */\n sample(): InteractionSample {\n this.out.presence = this.presence;\n return this.out;\n }\n\n /** This wave's smoothed pointer-field state (it trails the cursor at its own hover smoothing), or\n * null if the wave hasn't been advanced by update() yet (treat as rest). */\n fieldFor(waveIdx: number): PointerField | null {\n return this.fields[waveIdx] ?? null;\n }\n\n /** Velocity-driven agitation drive 0..1 (how fast the cursor is moving, presence-gated). The\n * renderer scales each wave's hover `agitate` by this, so the churn tracks the gesture instead\n * of buzzing at a constant rate whenever the cursor is merely present. */\n pointerFlux(): number {\n return this.pointerSpeed;\n }\n\n /** Smoothed pointer velocity, NDC/s (direction + speed of the drag). The renderer feeds it to the\n * drag-wake shader so the trailing trough forms behind the motion. Live reference — read per frame. */\n pointerVelocity(): THREE.Vector2 {\n return this.velNdc;\n }\n\n /** Feed a `custom:<name>` input (developer API; staged/forwarded by the shell). */\n setInput(name: string, value: number): void {\n if (typeof name !== \"string\" || !Number.isFinite(value)) return;\n this.customInputs.set(name, value);\n }\n\n /**\n * Collapse to the settled resting state for the single frame drawn when the loop stops (paused /\n * reduced-motion / offscreen): presence / velocity / press / pointerSpeed → 0, ripples cleared,\n * scroll → its current raw value, pointer → centre, and `appear` → 1 (reduced-motion users must\n * see the FINAL entered state). Custom inputs KEEP their last explicit values. Each binding snaps\n * to its settled source so the one settled frame shows the final look.\n */\n settle(): void {\n this.presence = this.presenceTarget = 0;\n this.pointerPresent = false;\n this.press = this.pressTarget = 0;\n this.pointerSpeed = 0;\n this.tiltX = this.tiltY = 0.5; // a settled frame is the neutral pose, like the centred cursor\n this.velNdc.set(0, 0);\n this.ndc.set(0, 0);\n this.ndcTarget.set(0, 0);\n this.ndcPrev.set(0, 0);\n for (const f of this.fields) {\n f.ndc.set(0, 0);\n f.vel.set(0, 0);\n f.presence = 0;\n }\n for (const r of this.ripples) {\n r.age = 0;\n r.amp = 0;\n r.active = false;\n }\n const rawScroll = this.scrollOverride ?? this.computeScroll();\n this.scroll = this.scrollPrev = rawScroll;\n this.scrollVel = 0;\n this.appearLatched = true;\n const cfg = this.cfg();\n if (cfg) {\n this.bindingState.clear();\n const snap = (b: AnyBinding): void => {\n this.bindingState.set(b, { value: this.rawSource(b.source), source: b.source });\n };\n for (const b of cfg.interaction?.bindings ?? []) snap(b);\n for (const w of cfg.waves) for (const b of w.interaction?.bindings ?? []) snap(b);\n }\n }\n\n /**\n * Snap scroll progress + the scroll-sourced bindings to the current override at once, leaving\n * every other input (pointer / press / appear / velocity / custom) advancing live. Used by the\n * studio scroll preview: the studio page never really scrolls, so dragging the preview slider is a\n * manual scrub that must reflect the instant you move it — not on the next animation frame, which\n * the browser fully suspends whenever the tab isn't foreground. Unlike settle() (which collapses\n * ALL input to rest for a paused still frame), this touches only the scroll signal.\n */\n snapScroll(): void {\n const raw = this.scrollOverride ?? this.computeScroll();\n this.scroll = this.scrollPrev = raw;\n this.scrollVel = 0; // a static scrub has no velocity\n const cfg = this.cfg();\n if (!cfg) return;\n const snap = (b: AnyBinding): void => {\n if (b.source === \"scroll\" || b.source === \"scrollVelocity\") {\n this.bindingState.set(b, { value: this.rawSource(b.source), source: b.source });\n }\n };\n for (const b of cfg.interaction?.bindings ?? []) snap(b);\n for (const w of cfg.waves) for (const b of w.interaction?.bindings ?? []) snap(b);\n }\n\n dispose(): void {\n const c = this.container;\n c.removeEventListener(\"pointerenter\", this.onPointerEnter);\n c.removeEventListener(\"pointermove\", this.onPointerMove);\n c.removeEventListener(\"pointerleave\", this.onPointerLeave);\n c.removeEventListener(\"pointercancel\", this.onPointerCancel);\n c.removeEventListener(\"pointerdown\", this.onPointerDown);\n c.removeEventListener(\"pointerup\", this.onPointerUp);\n this.tilt?.dispose();\n this.tilt = null;\n this.customInputs.clear();\n this.bindingState.clear();\n }\n}\n"],"mappings":"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"}
@@ -36,7 +36,8 @@ const SHAPE_INDEX = {
36
36
  soft: 1,
37
37
  ring: 2,
38
38
  star: 3,
39
- streak: 4
39
+ streak: 4,
40
+ square: 5
40
41
  };
41
42
  /** The owning wave's shape uniforms mirrored onto the particle material so the dust rides the same
42
43
  * deform as the ribbon. Names match the wave material's uniforms 1:1 (copied by value in configure). */
@@ -55,12 +56,22 @@ const SHAPE_UNIFORMS = [
55
56
  "uHelixTurns",
56
57
  "uHelixRadius",
57
58
  "uHelixRoll",
59
+ "uPathTex",
58
60
  "uHelixPhase",
59
61
  "uRadialAmount",
60
62
  "uRadialArc",
61
63
  "uRadialSpread",
62
64
  "uRadialRadius",
63
- "uRadialCenter"
65
+ "uRadialCenter",
66
+ "uRadialCone",
67
+ "uRadialSwirl",
68
+ "uDissolveAmount",
69
+ "uDissolveBand",
70
+ "uDissolveScale",
71
+ "uDissolveBlocky",
72
+ "uDissolveAxis",
73
+ "uDissolveReverse",
74
+ "uDissolveDust"
64
75
  ];
65
76
  /** The owning wave's POINTER-FIELD uniforms, mirrored the same way so the dust reads the exact
66
77
  * cursor state the ribbon does (see pointerFieldChunk). Only uploaded under POINTER_FX — configure()
@@ -211,12 +222,22 @@ var ParticleField = class {
211
222
  uHelixTurns: { value: 0 },
212
223
  uHelixRadius: { value: 0 },
213
224
  uHelixRoll: { value: 0 },
225
+ uPathTex: { value: null },
214
226
  uHelixPhase: { value: 0 },
215
227
  uRadialAmount: { value: 0 },
216
228
  uRadialArc: { value: 0 },
217
229
  uRadialSpread: { value: 0 },
218
230
  uRadialRadius: { value: 0 },
219
231
  uRadialCenter: { value: 0 },
232
+ uRadialCone: { value: 0 },
233
+ uRadialSwirl: { value: 0 },
234
+ uDissolveAmount: { value: 0 },
235
+ uDissolveBand: { value: .35 },
236
+ uDissolveScale: { value: 90 },
237
+ uDissolveBlocky: { value: .6 },
238
+ uDissolveAxis: { value: 0 },
239
+ uDissolveReverse: { value: 0 },
240
+ uDissolveDust: { value: 1 },
220
241
  uShedModel: { value: new THREE.Matrix4() },
221
242
  uShedSpeed: { value: 0 },
222
243
  uShedSeed: { value: 0 },
@@ -275,6 +296,8 @@ var ParticleField = class {
275
296
  u.uSwirl.value = cfg.swirl ?? 0;
276
297
  u.uWander.value = cfg.wander ?? 0;
277
298
  u.uShape.value = SHAPE_INDEX[cfg.shape ?? "glitter"] ?? 0;
299
+ const blend = cfg.blend === "normal" ? THREE.NormalBlending : THREE.AdditiveBlending;
300
+ if (this.material.blending !== blend) this.material.blending = blend;
278
301
  u.uPartShove.value = cfg.pointerShove ?? 1;
279
302
  this.syncSprite(cfg.shape === "sprite" ? cfg.spriteUrl ?? "" : "");
280
303
  setLinear(u.uColor.value, cfg.color ?? "#ffcf8a");
@@ -358,6 +381,7 @@ var ParticleField = class {
358
381
  if (!from || !to) return;
359
382
  const dst = to.value;
360
383
  if (typeof from.value === "number" || Array.isArray(from.value)) to.value = from.value;
384
+ else if (from.value === null || from.value?.isTexture) to.value = from.value;
361
385
  else if (dst && typeof dst.copy === "function") dst.copy(from.value);
362
386
  }
363
387
  /** Advance the field to scene time `t` (= the same `t` the waves get). */