@wave3d/core 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/config/model.d.ts +48 -2
- package/dist/config/model.js +8 -1
- package/dist/config/model.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/renderer/WaveRenderer.d.ts +142 -10
- package/dist/renderer/WaveRenderer.js +209 -53
- package/dist/renderer/WaveRenderer.js.map +1 -1
- package/dist/renderer/WaveRendererGPU.js +291 -0
- package/dist/renderer/WaveRendererGPU.js.map +1 -0
- package/dist/renderer/gpu-loader.js +2 -0
- package/dist/renderer/index.d.ts +3 -2
- package/dist/renderer/interaction.d.ts +29 -0
- package/dist/renderer/interaction.js +77 -31
- package/dist/renderer/interaction.js.map +1 -1
- package/dist/renderer/interactionGates.js +59 -0
- package/dist/renderer/interactionGates.js.map +1 -0
- package/dist/renderer/particleField.d.ts +1 -59
- package/dist/renderer/particleField.js +49 -29
- package/dist/renderer/particleField.js.map +1 -1
- package/dist/renderer/particleFieldGPU.js +141 -0
- package/dist/renderer/particleFieldGPU.js.map +1 -0
- package/dist/renderer/tilt.d.ts +17 -0
- package/dist/renderer/tilt.js +124 -0
- package/dist/renderer/tilt.js.map +1 -0
- package/dist/renderer/tsl/color.js +129 -0
- package/dist/renderer/tsl/color.js.map +1 -0
- package/dist/renderer/tsl/noise.js +83 -0
- package/dist/renderer/tsl/noise.js.map +1 -0
- package/dist/renderer/tsl/packedArray.js +76 -0
- package/dist/renderer/tsl/packedArray.js.map +1 -0
- package/dist/renderer/tsl/particleMaterial.js +133 -0
- package/dist/renderer/tsl/particleMaterial.js.map +1 -0
- package/dist/renderer/tsl/particleUniforms.js +44 -0
- package/dist/renderer/tsl/particleUniforms.js.map +1 -0
- package/dist/renderer/tsl/pointerField.js +73 -0
- package/dist/renderer/tsl/pointerField.js.map +1 -0
- package/dist/renderer/tsl/post.js +63 -0
- package/dist/renderer/tsl/post.js.map +1 -0
- package/dist/renderer/tsl/postChain.js +56 -0
- package/dist/renderer/tsl/postChain.js.map +1 -0
- package/dist/renderer/tsl/postEffects.js +221 -0
- package/dist/renderer/tsl/postEffects.js.map +1 -0
- package/dist/renderer/tsl/types.js +28 -0
- package/dist/renderer/tsl/types.js.map +1 -0
- package/dist/renderer/tsl/uniforms.js +152 -0
- package/dist/renderer/tsl/uniforms.js.map +1 -0
- package/dist/renderer/tsl/waveMaterial.js +185 -0
- package/dist/renderer/tsl/waveMaterial.js.map +1 -0
- package/dist/renderer/tsl/waveShape.js +108 -0
- package/dist/renderer/tsl/waveShape.js.map +1 -0
- package/dist/shell/createWave.d.ts +29 -0
- package/dist/shell/createWave.js +39 -11
- package/dist/shell/createWave.js.map +1 -1
- package/dist/shell/probe.js +18 -1
- package/dist/shell/probe.js.map +1 -1
- package/dist/standalone/wave3d.standalone.js +1811 -1594
- package/dist/standalone/wave3d.standalone.webgpu.js +36647 -0
- package/dist/standalone.d.ts +10 -3
- package/dist/standalone.js +10 -3
- package/dist/standalone.js.map +1 -1
- package/dist/studio/StudioWaveRenderer.d.ts +4 -0
- package/dist/studio/StudioWaveRenderer.js +9 -0
- package/dist/studio/StudioWaveRenderer.js.map +1 -1
- package/dist/studio/StudioWaveRendererGPU.js +18 -0
- package/dist/studio/StudioWaveRendererGPU.js.map +1 -0
- package/dist/studio/index.d.ts +12 -2
- package/dist/studio/index.js +14 -1
- package/dist/studio/index.js.map +1 -0
- package/package.json +12 -3
- package/skills/wave3d/SKILL.md +29 -2
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { wavePointerFxActive, waveRipplesActive } from "./interactionGates.js";
|
|
2
|
+
import { WaveRenderer } from "./WaveRenderer.js";
|
|
3
|
+
import { floatUniform, vec2Uniform } from "./tsl/types.js";
|
|
4
|
+
import { ParticleFieldGPU } from "./particleFieldGPU.js";
|
|
5
|
+
import { makeTslUniforms } from "./tsl/uniforms.js";
|
|
6
|
+
import { buildWaveMaterial } from "./tsl/waveMaterial.js";
|
|
7
|
+
import { buildPostChain } from "./tsl/postChain.js";
|
|
8
|
+
import * as THREE from "three";
|
|
9
|
+
import { RenderPipeline, WebGPUCoordinateSystem, WebGPURenderer } from "three/webgpu";
|
|
10
|
+
import { pass, screenUV, texture, vec2 } from "three/tsl";
|
|
11
|
+
//#region src/renderer/WaveRendererGPU.ts
|
|
12
|
+
/**
|
|
13
|
+
* The WebGPU / TSL backend.
|
|
14
|
+
*
|
|
15
|
+
* Overrides only the four things that are actually backend-specific — the three renderer, the
|
|
16
|
+
* wave material, how a shader variant is re-selected, and the post chain. Everything else (config
|
|
17
|
+
* sync, camera, background, resize, interaction, capture, particles) is shared, because both
|
|
18
|
+
* backends expose the same `uniforms` surface. The overrides are packaged as the
|
|
19
|
+
* {@link withTslBackend} mixin so they can also layer over the studio's editor subclass;
|
|
20
|
+
* {@link WaveRendererGPU} is that mixin applied to the plain {@link WaveRenderer}.
|
|
21
|
+
*
|
|
22
|
+
* `WebGPURenderer` falls back to a WebGL2 backend on its own when WebGPU is unavailable, so this
|
|
23
|
+
* class is not "the WebGPU-only path" — it is the TSL path, which runs on either backend. What it
|
|
24
|
+
* must not do is get imported from the package entry: `three/webgpu` pulls the whole node system
|
|
25
|
+
* (~200 KB gzipped), so this module is only ever reached through a dynamic import.
|
|
26
|
+
*/
|
|
27
|
+
/** The flag set that decides a wave's node graph — the TSL twin of `waveDefines()`. */
|
|
28
|
+
function variantKey(f) {
|
|
29
|
+
return [
|
|
30
|
+
f.theme,
|
|
31
|
+
f.loopMotion && "loop",
|
|
32
|
+
f.detailOctave && "detail",
|
|
33
|
+
f.helix && "helix",
|
|
34
|
+
f.twistMotion && "twist",
|
|
35
|
+
f.radial && "radial",
|
|
36
|
+
f.depthTint && "depthTint",
|
|
37
|
+
f.edgeFeather && "edgeFeather",
|
|
38
|
+
f.rungs && "rungs",
|
|
39
|
+
f.pointerFx && "pointer",
|
|
40
|
+
f.pointerRipples && "ripples",
|
|
41
|
+
f.webgpuClipZ && "gpuz"
|
|
42
|
+
].filter(Boolean).join(",");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The TSL backend as a mixin, so the same override set can sit on either base: over
|
|
46
|
+
* {@link WaveRenderer} for embeds (the {@link WaveRendererGPU} export below), or over the studio's
|
|
47
|
+
* editor subclass (`studio/StudioWaveRendererGPU.ts`). That composition works because the two
|
|
48
|
+
* override DISJOINT hook sets — the editor overrides the camera/overlay hooks, this backend the
|
|
49
|
+
* renderer/material/post hooks — so layering order doesn't matter.
|
|
50
|
+
*
|
|
51
|
+
* `Base` is constrained to (and internally cast as) `typeof WaveRenderer` because TypeScript
|
|
52
|
+
* forbids protected-member access from a class extending a bare type parameter. The cast is sound
|
|
53
|
+
* for any WaveRenderer subclass, and returning `TBase` hands the composed class back with its real
|
|
54
|
+
* base's type (the editor API survives on the studio composition).
|
|
55
|
+
*/
|
|
56
|
+
function withTslBackend(Base) {
|
|
57
|
+
class WithTslBackend extends Base {
|
|
58
|
+
post;
|
|
59
|
+
/**
|
|
60
|
+
* Lazily built, NOT a field initialiser. Subclass field initialisers run only after `super()`
|
|
61
|
+
* returns, and the base constructor already renders (buildWaves → resize → renderOnce), so a
|
|
62
|
+
* field here would still be `undefined` at first draw.
|
|
63
|
+
*/
|
|
64
|
+
postUniformsCache;
|
|
65
|
+
/** The effect set the current chain was built for; a change rebuilds it, as the WebGL path
|
|
66
|
+
* inserts and removes passes. */
|
|
67
|
+
postFlagsKey = "";
|
|
68
|
+
createRenderer() {
|
|
69
|
+
this.ready = false;
|
|
70
|
+
return new WebGPURenderer({
|
|
71
|
+
antialias: false,
|
|
72
|
+
alpha: true,
|
|
73
|
+
powerPreference: "high-performance"
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
get postUniforms() {
|
|
77
|
+
this.postUniformsCache ??= {
|
|
78
|
+
uBlurAmount: floatUniform(0),
|
|
79
|
+
uBlurSamples: floatUniform(6),
|
|
80
|
+
uGrainAmount: floatUniform(0),
|
|
81
|
+
uBloomStrength: floatUniform(0),
|
|
82
|
+
uBloomRadius: floatUniform(.4),
|
|
83
|
+
uBloomThreshold: floatUniform(.85),
|
|
84
|
+
uInnerLight: floatUniform(0),
|
|
85
|
+
uInnerLightDensity: floatUniform(.5),
|
|
86
|
+
uInnerLightDecay: floatUniform(.95),
|
|
87
|
+
uInnerLightCenter: vec2Uniform(.5, .15),
|
|
88
|
+
uHalftone: floatUniform(0),
|
|
89
|
+
uHalftoneCell: floatUniform(6),
|
|
90
|
+
uHalftoneAngle: floatUniform(.4),
|
|
91
|
+
uHeatmap: floatUniform(0),
|
|
92
|
+
uHalftoneCmyk: floatUniform(0),
|
|
93
|
+
uHalftoneCmykCell: floatUniform(6),
|
|
94
|
+
uPaper: floatUniform(0),
|
|
95
|
+
uPaperScale: floatUniform(2),
|
|
96
|
+
uDitherStrength: floatUniform(0),
|
|
97
|
+
uDitherScale: floatUniform(2),
|
|
98
|
+
uDitherSteps: floatUniform(4)
|
|
99
|
+
};
|
|
100
|
+
return this.postUniformsCache;
|
|
101
|
+
}
|
|
102
|
+
/** Start the WebGPU backend, then draw the first frame. Safe to call more than once. */
|
|
103
|
+
async init() {
|
|
104
|
+
if (this.ready) return;
|
|
105
|
+
await this.renderer.init();
|
|
106
|
+
this.ready = true;
|
|
107
|
+
this.resize();
|
|
108
|
+
this.refresh();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* True when the live backend uses [0,1] clip Z rather than [-1,1]. Read from the renderer rather
|
|
112
|
+
* than assumed, because WebGPURenderer silently falls back to a WebGL2 backend — in which case
|
|
113
|
+
* the clip convention is the WebGL one and the depth fade must NOT be remapped.
|
|
114
|
+
*/
|
|
115
|
+
get webgpuClipZ() {
|
|
116
|
+
return this.renderer.coordinateSystem === WebGPUCoordinateSystem;
|
|
117
|
+
}
|
|
118
|
+
flagsFor(sc) {
|
|
119
|
+
const bindsDetail = sc?.interaction?.bindings?.some((b) => b.target === "detailAmount") ?? false;
|
|
120
|
+
const bindsHelix = sc?.interaction?.bindings?.some((b) => b.target.startsWith("helix")) ?? false;
|
|
121
|
+
const pointer = !!sc && wavePointerFxActive(this.config, sc);
|
|
122
|
+
return {
|
|
123
|
+
theme: sc?.theme === "wireframe" ? "wireframe" : "solid",
|
|
124
|
+
loopMotion: (this.config.loopSeconds ?? 0) > 0,
|
|
125
|
+
detailOctave: (sc?.detailAmount ?? 0) !== 0 || bindsDetail,
|
|
126
|
+
helix: (sc?.helixRadius ?? 0) !== 0 || (sc?.helixRoll ?? 0) !== 0 || bindsHelix,
|
|
127
|
+
twistMotion: !!sc?.twistMotion,
|
|
128
|
+
radial: (sc?.radialAmount ?? 0) !== 0,
|
|
129
|
+
depthTint: (sc?.depthTint ?? 0) > 0,
|
|
130
|
+
edgeFeather: (sc?.edgeFeather ?? .1) !== .1,
|
|
131
|
+
rungs: sc?.theme === "wireframe" && (sc.rungAmount ?? 0) > 0,
|
|
132
|
+
pointerFx: pointer,
|
|
133
|
+
pointerRipples: pointer && waveRipplesActive(this.config, sc),
|
|
134
|
+
webgpuClipZ: this.webgpuClipZ
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* An instanced-sprite particle field wired to THIS wave's uniform registry.
|
|
139
|
+
*
|
|
140
|
+
* That wiring is the whole reason this override exists: the GLSL field has to mirror the wave's
|
|
141
|
+
* shape and pointer uniforms into its own material every frame, whereas here the dust reads the
|
|
142
|
+
* ribbon's nodes directly and cannot drift out of sync with it.
|
|
143
|
+
*/
|
|
144
|
+
createParticleField(wave, sc, onReady) {
|
|
145
|
+
const { tsl } = wave.material.userData;
|
|
146
|
+
const f = this.flagsFor(sc);
|
|
147
|
+
return new ParticleFieldGPU({
|
|
148
|
+
uniforms: tsl,
|
|
149
|
+
flags: {
|
|
150
|
+
loopMotion: f.loopMotion,
|
|
151
|
+
detailOctave: f.detailOctave,
|
|
152
|
+
helix: f.helix,
|
|
153
|
+
twistMotion: f.twistMotion,
|
|
154
|
+
radial: f.radial,
|
|
155
|
+
pointerFx: f.pointerFx,
|
|
156
|
+
pointerRipples: f.pointerRipples
|
|
157
|
+
}
|
|
158
|
+
}, onReady);
|
|
159
|
+
}
|
|
160
|
+
createWaveMaterial(sc) {
|
|
161
|
+
const u = makeTslUniforms(this.renderer.getDrawingBufferSize(new THREE.Vector2()));
|
|
162
|
+
const flags = this.flagsFor(sc);
|
|
163
|
+
const material = buildWaveMaterial(u, flags);
|
|
164
|
+
material.uniforms = u;
|
|
165
|
+
material.userData = {
|
|
166
|
+
variant: variantKey(flags),
|
|
167
|
+
tsl: u
|
|
168
|
+
};
|
|
169
|
+
return material;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A TSL variant is a different GRAPH, not a different define set, so a variant change rebuilds
|
|
173
|
+
* the material and re-points the mesh at it. The uniform registry is carried over untouched, so
|
|
174
|
+
* no config state is lost and no value has to be re-synced.
|
|
175
|
+
*/
|
|
176
|
+
applyWaveVariant(wave, sc) {
|
|
177
|
+
const current = wave.material;
|
|
178
|
+
const flags = this.flagsFor(sc);
|
|
179
|
+
const key = variantKey(flags);
|
|
180
|
+
if (current.userData.variant === key) return false;
|
|
181
|
+
const rebuilt = buildWaveMaterial(current.userData.tsl, flags);
|
|
182
|
+
rebuilt.uniforms = current.uniforms;
|
|
183
|
+
rebuilt.userData = {
|
|
184
|
+
variant: key,
|
|
185
|
+
tsl: current.userData.tsl
|
|
186
|
+
};
|
|
187
|
+
rebuilt.blending = current.blending;
|
|
188
|
+
rebuilt.premultipliedAlpha = current.premultipliedAlpha;
|
|
189
|
+
wave.mesh.material = rebuilt;
|
|
190
|
+
wave.material = rebuilt;
|
|
191
|
+
current.dispose();
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
/** Which effects the config currently asks for — the node twin of applyPost()'s pass juggling. */
|
|
195
|
+
postFlags() {
|
|
196
|
+
const c = this.config;
|
|
197
|
+
return {
|
|
198
|
+
bloom: (c.bloomStrength ?? 0) > 0,
|
|
199
|
+
innerLight: (c.innerLight ?? 0) > 0,
|
|
200
|
+
halftone: (c.halftone ?? 0) > 0,
|
|
201
|
+
heatmap: (c.heatmap ?? 0) > 0,
|
|
202
|
+
halftoneCmyk: (c.halftoneCmyk ?? 0) > 0,
|
|
203
|
+
paperTexture: (c.paperTexture ?? 0) > 0,
|
|
204
|
+
dither: (c.dither ?? 0) > 0
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/** Push the config into the post uniforms. Mirrors applyPost() / the per-effect apply* methods. */
|
|
208
|
+
syncPostUniforms() {
|
|
209
|
+
const c = this.config;
|
|
210
|
+
const u = this.postUniforms;
|
|
211
|
+
u.uBlurAmount.value = c.blur;
|
|
212
|
+
u.uGrainAmount.value = c.grain;
|
|
213
|
+
u.uBlurSamples.value = Math.round(c.blurSamples ?? 6);
|
|
214
|
+
u.uBloomStrength.value = c.bloomStrength ?? 0;
|
|
215
|
+
u.uBloomRadius.value = c.bloomRadius ?? .4;
|
|
216
|
+
u.uBloomThreshold.value = c.bloomThreshold ?? .85;
|
|
217
|
+
u.uInnerLight.value = c.innerLight ?? 0;
|
|
218
|
+
u.uInnerLightDensity.value = c.innerLightDensity ?? .5;
|
|
219
|
+
u.uInnerLightDecay.value = c.innerLightDecay ?? .95;
|
|
220
|
+
u.uInnerLightCenter.value.set(c.innerLightX ?? .5, c.innerLightY ?? .15);
|
|
221
|
+
u.uHalftone.value = c.halftone ?? 0;
|
|
222
|
+
u.uHalftoneCell.value = Math.max(2, c.halftoneCell ?? 6);
|
|
223
|
+
u.uHalftoneAngle.value = c.halftoneAngle ?? .4;
|
|
224
|
+
u.uHeatmap.value = c.heatmap ?? 0;
|
|
225
|
+
u.uHalftoneCmyk.value = c.halftoneCmyk ?? 0;
|
|
226
|
+
u.uHalftoneCmykCell.value = Math.max(2, c.halftoneCmykCell ?? 6);
|
|
227
|
+
u.uPaper.value = c.paperTexture ?? 0;
|
|
228
|
+
u.uPaperScale.value = Math.max(.5, c.paperTextureScale ?? 2);
|
|
229
|
+
u.uDitherStrength.value = c.dither ?? 0;
|
|
230
|
+
u.uDitherScale.value = Math.max(1, c.ditherScale ?? 2);
|
|
231
|
+
u.uDitherSteps.value = Math.max(2, Math.round(c.ditherSteps ?? 4));
|
|
232
|
+
}
|
|
233
|
+
ensurePost() {
|
|
234
|
+
const flags = this.postFlags();
|
|
235
|
+
const key = JSON.stringify(flags);
|
|
236
|
+
if (this.post && key !== this.postFlagsKey) this.disposePost();
|
|
237
|
+
if (!this.post) {
|
|
238
|
+
this.postFlagsKey = key;
|
|
239
|
+
this.post = new RenderPipeline(this.renderer);
|
|
240
|
+
this.post.outputColorTransform = false;
|
|
241
|
+
this.post.outputNode = buildPostChain(pass(this.scene, this.camera), this.postUniforms, flags);
|
|
242
|
+
}
|
|
243
|
+
return this.post;
|
|
244
|
+
}
|
|
245
|
+
renderComposed() {
|
|
246
|
+
for (const wave of this.waves) wave.material.userData.tsl.packed.sync();
|
|
247
|
+
this.syncPostUniforms();
|
|
248
|
+
this.ensurePost().render();
|
|
249
|
+
}
|
|
250
|
+
resizePost(w, h, dpr) {
|
|
251
|
+
this.renderer.setPixelRatio(dpr);
|
|
252
|
+
this.renderer.setSize(w, h, false);
|
|
253
|
+
}
|
|
254
|
+
disposePost() {
|
|
255
|
+
this.post?.dispose();
|
|
256
|
+
this.post = void 0;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* WebGPU exposes no `capabilities` object; the equivalent limit lives on the adapter's device.
|
|
260
|
+
* Falls back to 8192, the value every WebGPU implementation is required to support, for the
|
|
261
|
+
* window before `init()` resolves — the base class calls this while sizing the background.
|
|
262
|
+
*/
|
|
263
|
+
maxTextureSize() {
|
|
264
|
+
return (this.renderer.backend?.device)?.limits?.maxTextureDimension2D ?? 8192;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Rebuild the post chain when the background changes.
|
|
268
|
+
*
|
|
269
|
+
* A `pass()` node captures the scene's background at the point its render context is first built.
|
|
270
|
+
* Because the chain is created lazily on the first draw — when `scene.background` is still null —
|
|
271
|
+
* a background set afterwards would never appear, rendering every non-transparent preset on a
|
|
272
|
+
* transparent canvas. Background changes are user-driven, not per-frame, so rebuilding here is
|
|
273
|
+
* cheap; rebuilding every frame (which also works) is not.
|
|
274
|
+
*/
|
|
275
|
+
onBackgroundChanged() {
|
|
276
|
+
const bg = this.scene.background;
|
|
277
|
+
const isTexture = !!bg && bg.isTexture === true;
|
|
278
|
+
this.scene.backgroundNode = isTexture ? texture(bg).sample(vec2(screenUV.x, screenUV.y.oneMinus())) : null;
|
|
279
|
+
this.disposePost();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return WithTslBackend;
|
|
283
|
+
}
|
|
284
|
+
/** The TSL/WebGPU renderer over the plain {@link WaveRenderer} base — what embeds reach through
|
|
285
|
+
* `gpu-loader.ts`. A named class (not a bare `withTslBackend(WaveRenderer)` const) so consumers
|
|
286
|
+
* keep `new WaveRendererGPU(...)`, `instanceof`, and a stable class name. */
|
|
287
|
+
var WaveRendererGPU = class extends withTslBackend(WaveRenderer) {};
|
|
288
|
+
//#endregion
|
|
289
|
+
export { WaveRendererGPU, withTslBackend };
|
|
290
|
+
|
|
291
|
+
//# sourceMappingURL=WaveRendererGPU.js.map
|
|
@@ -0,0 +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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"}
|
package/dist/renderer/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TiltStatus } from "./tilt.js";
|
|
2
|
+
import { FRAME_H, FRAME_W, WaveMaterial, WaveParticleField, WaveRenderer, WaveRendererOptions, WaveUniforms, frameZoom, hexToLinearVec3 } from "./WaveRenderer.js";
|
|
2
3
|
import { BackgroundGradientOptions, PALETTE_MAPS, PaletteMapDef, PaletteTextureOptions, buildBackgroundGradientCanvas, buildBackgroundImageCanvas, buildBackgroundMeshCanvas, buildPaletteCanvas, buildPaletteTexture, canvasToTexture, configurePaletteTexture, drawBackgroundMediaFrame, loadPaletteImage, paletteMapCanvas, paletteSignature, renderMeshGradient } from "./palette.js";
|
|
3
4
|
import { buildHeroPaletteCanvas, buildHeroPaletteTexture } from "./heroPalette.js";
|
|
4
|
-
export { BackgroundGradientOptions, FRAME_H, FRAME_W, PALETTE_MAPS, PaletteMapDef, PaletteTextureOptions, WaveRenderer, WaveRendererOptions, buildBackgroundGradientCanvas, buildBackgroundImageCanvas, buildBackgroundMeshCanvas, buildHeroPaletteCanvas, buildHeroPaletteTexture, buildPaletteCanvas, buildPaletteTexture, canvasToTexture, configurePaletteTexture, drawBackgroundMediaFrame, frameZoom, hexToLinearVec3, loadPaletteImage, paletteMapCanvas, paletteSignature, renderMeshGradient };
|
|
5
|
+
export { BackgroundGradientOptions, FRAME_H, FRAME_W, PALETTE_MAPS, PaletteMapDef, PaletteTextureOptions, type TiltStatus, WaveMaterial, WaveParticleField, WaveRenderer, WaveRendererOptions, WaveUniforms, buildBackgroundGradientCanvas, buildBackgroundImageCanvas, buildBackgroundMeshCanvas, buildHeroPaletteCanvas, buildHeroPaletteTexture, buildPaletteCanvas, buildPaletteTexture, canvasToTexture, configurePaletteTexture, drawBackgroundMediaFrame, frameZoom, hexToLinearVec3, loadPaletteImage, paletteMapCanvas, paletteSignature, renderMeshGradient };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SceneInteractionBinding, StudioConfig, WaveInteractionBinding } from "../config/model.js";
|
|
2
|
+
import { TiltStatus } from "./tilt.js";
|
|
2
3
|
import * as THREE from "three";
|
|
3
4
|
//#region src/renderer/interaction.d.ts
|
|
4
5
|
type AnyBinding = WaveInteractionBinding | SceneInteractionBinding;
|
|
@@ -42,6 +43,9 @@ declare class InteractionController {
|
|
|
42
43
|
private readonly velNdc;
|
|
43
44
|
private presence;
|
|
44
45
|
private presenceTarget;
|
|
46
|
+
/** Whether a REAL pointer is on the element. Tracked apart from `presenceTarget` because tilt can
|
|
47
|
+
* raise presence too (tilt.pointer), and it must yield the moment a finger or cursor shows up. */
|
|
48
|
+
private pointerPresent;
|
|
45
49
|
private press;
|
|
46
50
|
private pressTarget;
|
|
47
51
|
private pointerSpeed;
|
|
@@ -49,6 +53,10 @@ declare class InteractionController {
|
|
|
49
53
|
private scrollPrev;
|
|
50
54
|
private scrollVel;
|
|
51
55
|
private appearLatched;
|
|
56
|
+
/** The orientation sensor, built only for a scene that declares `interaction.tilt`. */
|
|
57
|
+
private tilt;
|
|
58
|
+
private tiltX;
|
|
59
|
+
private tiltY;
|
|
52
60
|
private readonly customInputs;
|
|
53
61
|
private readonly ripples;
|
|
54
62
|
private readonly fields;
|
|
@@ -70,6 +78,27 @@ declare class InteractionController {
|
|
|
70
78
|
private spawnRipple;
|
|
71
79
|
/** Advance all smoothed state by `dt` seconds. Called from the render loop with the same delta. */
|
|
72
80
|
update(dt: number): void;
|
|
81
|
+
/**
|
|
82
|
+
* Build or drop the tilt sensor as the config declares it, then advance the smoothed axes. The
|
|
83
|
+
* sensor is created lazily and only for a scene that asked for it, so a cursor-only scene attaches
|
|
84
|
+
* no orientation listener at all — and a studio edit that adds or removes the block is picked up
|
|
85
|
+
* on the next frame, the same way the controller itself is.
|
|
86
|
+
*/
|
|
87
|
+
private updateTilt;
|
|
88
|
+
/** The sensor, built on first use. Null when the platform has none (every desktop browser). */
|
|
89
|
+
private ensureTilt;
|
|
90
|
+
/**
|
|
91
|
+
* Ask for the orientation sensor, which on iOS 13+ MUST happen inside a user gesture (a tap
|
|
92
|
+
* handler — not a `setTimeout` or a promise chain that outlives the gesture). Resolves true once
|
|
93
|
+
* readings can flow. False means the platform has no sensor, the scene declares no `tilt` block,
|
|
94
|
+
* or the reader refused. Everywhere that needs no permission, tilt is already live and this
|
|
95
|
+
* simply resolves true.
|
|
96
|
+
*/
|
|
97
|
+
enableTilt(): Promise<boolean>;
|
|
98
|
+
/** Where the sensor stands — `"prompt"` is exactly when a tap-to-enable affordance would help. */
|
|
99
|
+
tiltStatus(): TiltStatus;
|
|
100
|
+
/** Take the next reading as the neutral pose (the reader has changed grip). */
|
|
101
|
+
recenterTilt(): void;
|
|
73
102
|
private updateBindings;
|
|
74
103
|
/** Advance one binding's smoothed source value by `dt` and mark it live in `seenBindings`. */
|
|
75
104
|
private advanceBinding;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { clamp01 } from "../util/math.js";
|
|
2
|
+
import { tiltActive } from "./interactionGates.js";
|
|
3
|
+
import { TiltSource } from "./tilt.js";
|
|
2
4
|
import * as THREE from "three";
|
|
5
|
+
//#region src/renderer/interaction.ts
|
|
3
6
|
const RIPPLE_LIFETIME = 1.5;
|
|
4
7
|
const VELOCITY_TAU = .08;
|
|
5
8
|
const POINTER_SPEED_REF = 4;
|
|
6
9
|
const SCROLL_VELOCITY_REF = 2;
|
|
7
10
|
const SCROLL_VELOCITY_TAU = .15;
|
|
8
11
|
const DEFAULT_POINTER_TAU = .12;
|
|
12
|
+
const DEFAULT_TILT_TAU = .18;
|
|
9
13
|
const DEFAULT_BINDING_TAU = .25;
|
|
10
14
|
const POINTER_SPRING_ZETA = .7;
|
|
11
15
|
const MIN_POINTER_TAU = .02;
|
|
@@ -132,36 +136,6 @@ const SCENE_APPLIERS = {
|
|
|
132
136
|
a.post.uGrainAmount.value = v;
|
|
133
137
|
})
|
|
134
138
|
};
|
|
135
|
-
/** The global master switch: only `scene.interaction.enabled === false` turns the whole layer off. */
|
|
136
|
-
function notDisabled(cfg) {
|
|
137
|
-
return cfg.interaction?.enabled !== false;
|
|
138
|
-
}
|
|
139
|
-
/** Whether a wave has a pointer field (hover effects, or a click ripple). */
|
|
140
|
-
function waveHasPointerField(w) {
|
|
141
|
-
const it = w.interaction;
|
|
142
|
-
return !!it && (!!it.hover || (it.press?.ripple ?? 0) > 0);
|
|
143
|
-
}
|
|
144
|
-
/** Whether this wave has an active pointer field → its POINTER_FX shader path compiles. */
|
|
145
|
-
function wavePointerFxActive(cfg, w) {
|
|
146
|
-
return notDisabled(cfg) && waveHasPointerField(w);
|
|
147
|
-
}
|
|
148
|
-
/** Whether this wave has active click ripples → its nested POINTER_RIPPLES path compiles. */
|
|
149
|
-
function waveRipplesActive(cfg, w) {
|
|
150
|
-
return notDisabled(cfg) && (w.interaction?.press?.ripple ?? 0) > 0;
|
|
151
|
-
}
|
|
152
|
-
/** Whether ANY wave has a pointer field (so the renderer bothers writing the shared pointer uniforms). */
|
|
153
|
-
function anyPointerFxActive(cfg) {
|
|
154
|
-
return notDisabled(cfg) && cfg.waves.some(waveHasPointerField);
|
|
155
|
-
}
|
|
156
|
-
/** Whether the interaction layer should run at all (any wave interaction, or any scene binding). */
|
|
157
|
-
function interactionActive(cfg) {
|
|
158
|
-
if (!notDisabled(cfg)) return false;
|
|
159
|
-
if ((cfg.interaction?.bindings?.length ?? 0) > 0) return true;
|
|
160
|
-
return cfg.waves.some((w) => {
|
|
161
|
-
const it = w.interaction;
|
|
162
|
-
return !!it && (!!it.hover || (it.press?.ripple ?? 0) > 0 || (it.bindings?.length ?? 0) > 0);
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
139
|
/**
|
|
166
140
|
* Owns the one cursor's input + scroll + press/appear/custom and all smoothing. Constructed by the
|
|
167
141
|
* renderer when {@link interactionActive} first turns true, disposed when it turns false. All
|
|
@@ -178,6 +152,9 @@ var InteractionController = class {
|
|
|
178
152
|
velNdc = new THREE.Vector2();
|
|
179
153
|
presence = 0;
|
|
180
154
|
presenceTarget = 0;
|
|
155
|
+
/** Whether a REAL pointer is on the element. Tracked apart from `presenceTarget` because tilt can
|
|
156
|
+
* raise presence too (tilt.pointer), and it must yield the moment a finger or cursor shows up. */
|
|
157
|
+
pointerPresent = false;
|
|
181
158
|
press = 0;
|
|
182
159
|
pressTarget = 0;
|
|
183
160
|
pointerSpeed = 0;
|
|
@@ -185,6 +162,10 @@ var InteractionController = class {
|
|
|
185
162
|
scrollPrev = 0;
|
|
186
163
|
scrollVel = 0;
|
|
187
164
|
appearLatched = false;
|
|
165
|
+
/** The orientation sensor, built only for a scene that declares `interaction.tilt`. */
|
|
166
|
+
tilt = null;
|
|
167
|
+
tiltX = .5;
|
|
168
|
+
tiltY = .5;
|
|
188
169
|
customInputs = /* @__PURE__ */ new Map();
|
|
189
170
|
ripples = [];
|
|
190
171
|
fields = [];
|
|
@@ -224,28 +205,33 @@ var InteractionController = class {
|
|
|
224
205
|
}
|
|
225
206
|
onPointerEnter = (e) => {
|
|
226
207
|
if (this.ignore(e)) return;
|
|
208
|
+
this.pointerPresent = true;
|
|
227
209
|
this.presenceTarget = 1;
|
|
228
210
|
this.setNdcTarget(e);
|
|
229
211
|
};
|
|
230
212
|
onPointerMove = (e) => {
|
|
231
213
|
if (this.ignore(e)) return;
|
|
232
214
|
if (e.pointerType === "touch" && this.pressTarget < .5) return;
|
|
215
|
+
this.pointerPresent = true;
|
|
233
216
|
this.presenceTarget = 1;
|
|
234
217
|
this.setNdcTarget(e);
|
|
235
218
|
};
|
|
236
219
|
onPointerLeave = (e) => {
|
|
237
220
|
if (this.ignore(e)) return;
|
|
221
|
+
this.pointerPresent = false;
|
|
238
222
|
this.presenceTarget = 0;
|
|
239
223
|
this.ndcTarget.set(0, 0);
|
|
240
224
|
};
|
|
241
225
|
onPointerCancel = (e) => {
|
|
242
226
|
if (this.ignore(e)) return;
|
|
227
|
+
this.pointerPresent = false;
|
|
243
228
|
this.pressTarget = 0;
|
|
244
229
|
this.presenceTarget = 0;
|
|
245
230
|
this.ndcTarget.set(0, 0);
|
|
246
231
|
};
|
|
247
232
|
onPointerDown = (e) => {
|
|
248
233
|
if (this.ignore(e)) return;
|
|
234
|
+
this.pointerPresent = true;
|
|
249
235
|
this.pressTarget = 1;
|
|
250
236
|
this.presenceTarget = 1;
|
|
251
237
|
this.setNdcTarget(e);
|
|
@@ -256,6 +242,7 @@ var InteractionController = class {
|
|
|
256
242
|
if (this.ignore(e)) return;
|
|
257
243
|
this.pressTarget = 0;
|
|
258
244
|
if (e.pointerType === "touch") {
|
|
245
|
+
this.pointerPresent = false;
|
|
259
246
|
this.presenceTarget = 0;
|
|
260
247
|
this.ndcTarget.set(0, 0);
|
|
261
248
|
}
|
|
@@ -279,6 +266,7 @@ var InteractionController = class {
|
|
|
279
266
|
if (!cfg) return;
|
|
280
267
|
const d = Math.max(dt, 0);
|
|
281
268
|
const kPointer = alpha(DEFAULT_POINTER_TAU, d);
|
|
269
|
+
this.updateTilt(cfg, d);
|
|
282
270
|
this.ndcPrev.copy(this.ndc);
|
|
283
271
|
this.ndc.lerp(this.ndcTarget, kPointer);
|
|
284
272
|
this.presence += (this.presenceTarget - this.presence) * kPointer;
|
|
@@ -322,6 +310,58 @@ var InteractionController = class {
|
|
|
322
310
|
}
|
|
323
311
|
this.updateBindings(cfg, d);
|
|
324
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Build or drop the tilt sensor as the config declares it, then advance the smoothed axes. The
|
|
315
|
+
* sensor is created lazily and only for a scene that asked for it, so a cursor-only scene attaches
|
|
316
|
+
* no orientation listener at all — and a studio edit that adds or removes the block is picked up
|
|
317
|
+
* on the next frame, the same way the controller itself is.
|
|
318
|
+
*/
|
|
319
|
+
updateTilt(cfg, dt) {
|
|
320
|
+
if (!tiltActive(cfg)) {
|
|
321
|
+
if (this.tilt) {
|
|
322
|
+
this.tilt.dispose();
|
|
323
|
+
this.tilt = null;
|
|
324
|
+
this.tiltX = this.tiltY = .5;
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const src = this.ensureTilt();
|
|
329
|
+
if (!src) return;
|
|
330
|
+
const tiltCfg = cfg.interaction?.tilt;
|
|
331
|
+
const k = alpha(Math.max(tiltCfg?.smoothing ?? DEFAULT_TILT_TAU, 0), dt);
|
|
332
|
+
this.tiltX += (src.x - this.tiltX) * k;
|
|
333
|
+
this.tiltY += (src.y - this.tiltY) * k;
|
|
334
|
+
if (tiltCfg?.pointer && src.live && !this.pointerPresent) {
|
|
335
|
+
this.ndcTarget.set(this.tiltX * 2 - 1, -(this.tiltY * 2 - 1));
|
|
336
|
+
this.presenceTarget = 1;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** The sensor, built on first use. Null when the platform has none (every desktop browser). */
|
|
340
|
+
ensureTilt() {
|
|
341
|
+
if (!this.tilt && TiltSource.supported()) this.tilt = new TiltSource(() => this.cfg()?.interaction?.tilt);
|
|
342
|
+
return this.tilt;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Ask for the orientation sensor, which on iOS 13+ MUST happen inside a user gesture (a tap
|
|
346
|
+
* handler — not a `setTimeout` or a promise chain that outlives the gesture). Resolves true once
|
|
347
|
+
* readings can flow. False means the platform has no sensor, the scene declares no `tilt` block,
|
|
348
|
+
* or the reader refused. Everywhere that needs no permission, tilt is already live and this
|
|
349
|
+
* simply resolves true.
|
|
350
|
+
*/
|
|
351
|
+
async enableTilt() {
|
|
352
|
+
const cfg = this.cfg();
|
|
353
|
+
if (!cfg || !tiltActive(cfg)) return false;
|
|
354
|
+
return await this.ensureTilt()?.enable() ?? false;
|
|
355
|
+
}
|
|
356
|
+
/** Where the sensor stands — `"prompt"` is exactly when a tap-to-enable affordance would help. */
|
|
357
|
+
tiltStatus() {
|
|
358
|
+
if (!TiltSource.supported()) return "unsupported";
|
|
359
|
+
return this.tilt?.status ?? "prompt";
|
|
360
|
+
}
|
|
361
|
+
/** Take the next reading as the neutral pose (the reader has changed grip). */
|
|
362
|
+
recenterTilt() {
|
|
363
|
+
this.tilt?.recenter();
|
|
364
|
+
}
|
|
325
365
|
updateBindings(cfg, dt) {
|
|
326
366
|
const seen = this.seenBindings;
|
|
327
367
|
seen.clear();
|
|
@@ -365,6 +405,8 @@ var InteractionController = class {
|
|
|
365
405
|
case "press": return this.press;
|
|
366
406
|
case "scrollVelocity": return clamp01(this.scrollVel / SCROLL_VELOCITY_REF);
|
|
367
407
|
case "appear": return this.appearLatched ? 1 : 0;
|
|
408
|
+
case "tiltX": return this.tiltX;
|
|
409
|
+
case "tiltY": return this.tiltY;
|
|
368
410
|
default: return this.customInputs.get(source.slice(7)) ?? 0;
|
|
369
411
|
}
|
|
370
412
|
}
|
|
@@ -409,8 +451,10 @@ var InteractionController = class {
|
|
|
409
451
|
*/
|
|
410
452
|
settle() {
|
|
411
453
|
this.presence = this.presenceTarget = 0;
|
|
454
|
+
this.pointerPresent = false;
|
|
412
455
|
this.press = this.pressTarget = 0;
|
|
413
456
|
this.pointerSpeed = 0;
|
|
457
|
+
this.tiltX = this.tiltY = .5;
|
|
414
458
|
this.velNdc.set(0, 0);
|
|
415
459
|
this.ndc.set(0, 0);
|
|
416
460
|
this.ndcTarget.set(0, 0);
|
|
@@ -473,11 +517,13 @@ var InteractionController = class {
|
|
|
473
517
|
c.removeEventListener("pointercancel", this.onPointerCancel);
|
|
474
518
|
c.removeEventListener("pointerdown", this.onPointerDown);
|
|
475
519
|
c.removeEventListener("pointerup", this.onPointerUp);
|
|
520
|
+
this.tilt?.dispose();
|
|
521
|
+
this.tilt = null;
|
|
476
522
|
this.customInputs.clear();
|
|
477
523
|
this.bindingState.clear();
|
|
478
524
|
}
|
|
479
525
|
};
|
|
480
526
|
//#endregion
|
|
481
|
-
export { InteractionController, SCENE_APPLIERS, WAVE_APPLIERS
|
|
527
|
+
export { InteractionController, SCENE_APPLIERS, WAVE_APPLIERS };
|
|
482
528
|
|
|
483
529
|
//# sourceMappingURL=interaction.js.map
|