@wave3d/core 0.4.1 → 0.6.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 +2 -6
- package/dist/config/model.d.ts +53 -2
- package/dist/config/model.js +133 -61
- package/dist/config/model.js.map +1 -1
- package/dist/core-loader.d.ts +0 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/presets.d.ts +0 -1
- package/dist/presets.js +105 -0
- package/dist/presets.js.map +1 -1
- package/dist/renderer/WaveGeometry.d.ts +0 -1
- package/dist/renderer/WaveGeometry.js.map +1 -1
- package/dist/renderer/WaveRenderer.d.ts +62 -14
- package/dist/renderer/WaveRenderer.js +126 -17
- package/dist/renderer/WaveRenderer.js.map +1 -1
- package/dist/renderer/heroPalette.d.ts +0 -1
- package/dist/renderer/index.d.ts +2 -2
- package/dist/renderer/index.js +2 -2
- package/dist/renderer/interaction.d.ts +0 -2
- package/dist/renderer/interaction.js +9 -0
- package/dist/renderer/interaction.js.map +1 -1
- package/dist/renderer/palette.d.ts +0 -1
- package/dist/renderer/shaders.js +46 -0
- package/dist/renderer/shaders.js.map +1 -1
- package/dist/shell/createWave.d.ts +0 -1
- package/dist/standalone/wave3d.standalone.js +501 -294
- package/dist/standalone.d.ts +2 -3
- package/dist/standalone.js +2 -2
- package/dist/studio/StudioWaveRenderer.d.ts +4 -1
- package/dist/studio/StudioWaveRenderer.js +10 -3
- package/dist/studio/StudioWaveRenderer.js.map +1 -1
- package/dist/studio/randomize.d.ts +0 -1
- package/dist/studio/thumbnail.d.ts +0 -1
- package/dist/studio/thumbnail.js +5 -1
- package/dist/studio/thumbnail.js.map +1 -1
- package/package.json +5 -3
- package/skills/wave3d/SKILL.md +30 -12
|
@@ -1,18 +1,30 @@
|
|
|
1
|
-
import { StudioConfig, WaveConfig } from "../config/model.js";
|
|
1
|
+
import { CameraFit, StudioConfig, WaveConfig } from "../config/model.js";
|
|
2
2
|
import { WaveGeometry } from "./WaveGeometry.js";
|
|
3
3
|
import { InteractionController } from "./interaction.js";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
-
|
|
6
5
|
//#region src/renderer/WaveRenderer.d.ts
|
|
7
6
|
/** Reference frame (world units) the orthographic camera fills at cameraZoom 1. The wave is
|
|
8
|
-
* framed by
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* showing empty bands. This is what makes a saved preset reproduce on anyone's screen. */
|
|
7
|
+
* framed by mapping this FRAME_W × FRAME_H rectangle (centred on cameraTarget) onto the canvas,
|
|
8
|
+
* so a given cameraZoom / cameraTarget frames the wave the SAME at any canvas size or aspect
|
|
9
|
+
* (only the margin differs). FRAME_H = FRAME_W / (16/9) makes the reference a 16:9 rectangle.
|
|
10
|
+
* This is what makes a saved preset reproduce on anyone's screen. See {@link frameZoom} for how
|
|
11
|
+
* a canvas of a different aspect is reconciled against it. */
|
|
14
12
|
declare const FRAME_W = 1333;
|
|
15
13
|
declare const FRAME_H = 750;
|
|
14
|
+
/**
|
|
15
|
+
* The responsive base zoom: how many device pixels one world unit occupies so the FRAME_W × FRAME_H
|
|
16
|
+
* reference lands on a `dw × dh` (device px) canvas under `fit`, then clamped so at least
|
|
17
|
+
* `minVisibleWidth` of the frame's width survives.
|
|
18
|
+
*
|
|
19
|
+
* Shared by the renderer (which applies it) and the studio (which inverts it to persist a
|
|
20
|
+
* scroll-zoom back into config.cameraZoom) — one implementation, so the two cannot drift.
|
|
21
|
+
*
|
|
22
|
+
* The clamp is a pure zoom CEILING layered on top of the fit, which is what lets both knobs
|
|
23
|
+
* coexist: it can only widen the view, never tighten it, so it is inert for `contain`/`width`
|
|
24
|
+
* (already at or below that zoom) and bites exactly where the crop hurts — `cover`/`height` on a
|
|
25
|
+
* canvas narrower than 16:9. `minVisibleWidth` 0 disables it and restores pure-fit behaviour.
|
|
26
|
+
*/
|
|
27
|
+
declare function frameZoom(dw: number, dh: number, fit: CameraFit, minVisibleWidth?: number): number;
|
|
16
28
|
interface WaveRendererOptions {
|
|
17
29
|
/** Honor prefers-reduced-motion by freezing animation. Default true. */
|
|
18
30
|
respectReducedMotion?: boolean;
|
|
@@ -51,7 +63,8 @@ declare class WavePalette {
|
|
|
51
63
|
type Wave = {
|
|
52
64
|
mesh: THREE.Mesh;
|
|
53
65
|
material: THREE.ShaderMaterial;
|
|
54
|
-
geometry: WaveGeometry;
|
|
66
|
+
geometry: WaveGeometry;
|
|
67
|
+
/** This wave's own 2D palette texture + optional video. */
|
|
55
68
|
palette: WavePalette;
|
|
56
69
|
};
|
|
57
70
|
/** Convert an sRGB hex string to a linear-space RGB vector (three's ColorManagement does the
|
|
@@ -120,6 +133,11 @@ declare class WaveRenderer {
|
|
|
120
133
|
private readonly resizeObserver;
|
|
121
134
|
private readonly intersectionObserver;
|
|
122
135
|
private readonly motionQuery;
|
|
136
|
+
/** Re-armed at the live devicePixelRatio on every change — see watchDpr(). */
|
|
137
|
+
private dprQuery?;
|
|
138
|
+
/** Pending coalesced resize (0 = none), and the metrics the last resize() actually applied. */
|
|
139
|
+
private resizeRaf;
|
|
140
|
+
private lastResize?;
|
|
123
141
|
protected capturing: boolean;
|
|
124
142
|
/** Fixed backing-buffer dimensions used by the studio's visible export frame. Embeds leave
|
|
125
143
|
* this unset and continue to resize responsively with their container and device DPR. */
|
|
@@ -197,7 +215,35 @@ declare class WaveRenderer {
|
|
|
197
215
|
private applyPaperTexture;
|
|
198
216
|
/** CMYK halftone: four rotated dot screens (cyan/magenta/yellow/black). Finish zone. */
|
|
199
217
|
private applyHalftoneCmyk;
|
|
218
|
+
/** Coalesce observer-driven resizes to one per frame, and drop any that don't move a device pixel.
|
|
219
|
+
*
|
|
220
|
+
* resize() is expensive — composer.setSize reallocates every pass's render target, and
|
|
221
|
+
* applyBackground() rebuilds a container-sized canvas + texture for gradient/image backgrounds.
|
|
222
|
+
* The old 1:1 `observe → resize()` paid that for observations that changed nothing: the observer
|
|
223
|
+
* reports fractional content-box sizes, so sub-pixel layout shifts (and anything that rounds to
|
|
224
|
+
* the same backing buffer) triggered a full reallocation, as did every observation while an
|
|
225
|
+
* export frame is pinned and the container is not what drives the buffer at all.
|
|
226
|
+
*
|
|
227
|
+
* Genuine per-frame changes — a mobile URL bar collapsing animates the container height — still
|
|
228
|
+
* resize every frame. That work is necessary; the canvas would otherwise stretch. What is
|
|
229
|
+
* removed is the redundant work, not the real work.
|
|
230
|
+
*
|
|
231
|
+
* Only this path is throttled. `resize()` itself stays synchronous and unconditional — context
|
|
232
|
+
* restore and setOutputSize must re-apply immediately, and on a fresh GPU context the metrics
|
|
233
|
+
* are unchanged but the resources are not. */
|
|
200
234
|
private onResize;
|
|
235
|
+
/** Re-arm the DPR watch and re-render at the new device-pixel ratio.
|
|
236
|
+
*
|
|
237
|
+
* ResizeObserver watches the CSS box only, so browser zoom or dragging the window to a monitor
|
|
238
|
+
* with a different DPR changes devicePixelRatio without changing the box — the backing buffer
|
|
239
|
+
* stayed at the old resolution and the wave went soft until something else forced a resize. */
|
|
240
|
+
private onDprChange;
|
|
241
|
+
/** A `(resolution: Xdppx)` query only fires when we LEAVE the current ratio, so it is re-armed at
|
|
242
|
+
* the new one on every change. */
|
|
243
|
+
private watchDpr;
|
|
244
|
+
/** The backing-buffer metrics resize() will apply: the export frame when one is pinned, else the
|
|
245
|
+
* container box at the (clamped) device-pixel ratio. */
|
|
246
|
+
private viewportMetrics;
|
|
201
247
|
private onContextLost;
|
|
202
248
|
private onContextRestored;
|
|
203
249
|
resize(): void;
|
|
@@ -253,11 +299,13 @@ declare class WaveRenderer {
|
|
|
253
299
|
protected onAfterRenderFrame(): void;
|
|
254
300
|
/** Hook ④: called at the end of resize(), before the trailing renderOnce(). */
|
|
255
301
|
protected onAfterResize(): void;
|
|
256
|
-
/** Responsive ortho zoom:
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* contain (fit with letterbox bands). Cover keeps the wave filling the frame on every screen. */
|
|
302
|
+
/** Responsive ortho zoom: map the FRAME_W × FRAME_H reference frame onto the canvas (per
|
|
303
|
+
* config.cameraFit / cameraMinVisibleWidth — see {@link frameZoom}) so the wave frames the same
|
|
304
|
+
* at any size/aspect/dpr, times the user's cameraZoom. */
|
|
260
305
|
protected applyZoom(): void;
|
|
306
|
+
/** The responsive base zoom for the current config's framing policy, before the cameraZoom
|
|
307
|
+
* multiplier. Subclasses invert this to recover cameraZoom from a live camera. */
|
|
308
|
+
protected baseFrameZoom(dw: number, dh: number): number;
|
|
261
309
|
/** Fit the orthographic near/far planes to the scene before every render, so no part of a wave
|
|
262
310
|
* is ever clipped as the camera orbits / dollies / pans (or when waves are added or scaled).
|
|
263
311
|
*
|
|
@@ -287,5 +335,5 @@ declare class WaveRenderer {
|
|
|
287
335
|
dispose(): void;
|
|
288
336
|
}
|
|
289
337
|
//#endregion
|
|
290
|
-
export { FRAME_H, FRAME_W, WaveRenderer, WaveRendererOptions, hexToLinearVec3 };
|
|
338
|
+
export { FRAME_H, FRAME_W, WaveRenderer, WaveRendererOptions, frameZoom, hexToLinearVec3 };
|
|
291
339
|
//# sourceMappingURL=WaveRenderer.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ensureStudioConfig } from "../config/model.js";
|
|
2
|
-
import { ditherFragmentShader, fragmentShader, halftoneCmykFragmentShader, halftoneFragmentShader, heatmapFragmentShader, innerLightFragmentShader, lineFragmentShader, paperTextureFragmentShader, postFragmentShader, postVertexShader, vertexShader } from "./shaders.js";
|
|
3
2
|
import { WaveGeometry } from "./WaveGeometry.js";
|
|
3
|
+
import { ditherFragmentShader, fragmentShader, halftoneCmykFragmentShader, halftoneFragmentShader, heatmapFragmentShader, innerLightFragmentShader, lineFragmentShader, paperTextureFragmentShader, postFragmentShader, postVertexShader, vertexShader } from "./shaders.js";
|
|
4
4
|
import { InteractionController, SCENE_APPLIERS, WAVE_APPLIERS, anyPointerFxActive, interactionActive, wavePointerFxActive, waveRipplesActive } from "./interaction.js";
|
|
5
5
|
import { PALETTE_MAPS, buildBackgroundGradientCanvas, buildBackgroundImageCanvas, buildBackgroundMeshCanvas, buildPaletteTexture, canvasToTexture, configurePaletteTexture, drawBackgroundMediaFrame, loadPaletteImage, paletteMapCanvas, paletteSignature } from "./palette.js";
|
|
6
6
|
import { buildHeroPaletteCanvas, buildHeroPaletteTexture } from "./heroPalette.js";
|
|
@@ -13,15 +13,46 @@ import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"
|
|
|
13
13
|
//#region src/renderer/WaveRenderer.ts
|
|
14
14
|
const BASE_SEGMENTS = 220;
|
|
15
15
|
/** Reference frame (world units) the orthographic camera fills at cameraZoom 1. The wave is
|
|
16
|
-
* framed by
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* showing empty bands. This is what makes a saved preset reproduce on anyone's screen. */
|
|
16
|
+
* framed by mapping this FRAME_W × FRAME_H rectangle (centred on cameraTarget) onto the canvas,
|
|
17
|
+
* so a given cameraZoom / cameraTarget frames the wave the SAME at any canvas size or aspect
|
|
18
|
+
* (only the margin differs). FRAME_H = FRAME_W / (16/9) makes the reference a 16:9 rectangle.
|
|
19
|
+
* This is what makes a saved preset reproduce on anyone's screen. See {@link frameZoom} for how
|
|
20
|
+
* a canvas of a different aspect is reconciled against it. */
|
|
22
21
|
const FRAME_W = 1333;
|
|
23
22
|
const FRAME_H = 750;
|
|
24
23
|
/**
|
|
24
|
+
* The responsive base zoom: how many device pixels one world unit occupies so the FRAME_W × FRAME_H
|
|
25
|
+
* reference lands on a `dw × dh` (device px) canvas under `fit`, then clamped so at least
|
|
26
|
+
* `minVisibleWidth` of the frame's width survives.
|
|
27
|
+
*
|
|
28
|
+
* Shared by the renderer (which applies it) and the studio (which inverts it to persist a
|
|
29
|
+
* scroll-zoom back into config.cameraZoom) — one implementation, so the two cannot drift.
|
|
30
|
+
*
|
|
31
|
+
* The clamp is a pure zoom CEILING layered on top of the fit, which is what lets both knobs
|
|
32
|
+
* coexist: it can only widen the view, never tighten it, so it is inert for `contain`/`width`
|
|
33
|
+
* (already at or below that zoom) and bites exactly where the crop hurts — `cover`/`height` on a
|
|
34
|
+
* canvas narrower than 16:9. `minVisibleWidth` 0 disables it and restores pure-fit behaviour.
|
|
35
|
+
*/
|
|
36
|
+
function frameZoom(dw, dh, fit, minVisibleWidth = 0) {
|
|
37
|
+
const byWidth = dw / FRAME_W;
|
|
38
|
+
const byHeight = dh / 750;
|
|
39
|
+
let zoom;
|
|
40
|
+
switch (fit) {
|
|
41
|
+
case "contain":
|
|
42
|
+
zoom = Math.min(byWidth, byHeight);
|
|
43
|
+
break;
|
|
44
|
+
case "width":
|
|
45
|
+
zoom = byWidth;
|
|
46
|
+
break;
|
|
47
|
+
case "height":
|
|
48
|
+
zoom = byHeight;
|
|
49
|
+
break;
|
|
50
|
+
default: zoom = Math.max(byWidth, byHeight);
|
|
51
|
+
}
|
|
52
|
+
if (minVisibleWidth > 0) zoom = Math.min(zoom, dw / (FRAME_W * minVisibleWidth));
|
|
53
|
+
return zoom;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
25
56
|
* Per-wave 2D palette texture (+ optional looping video). One instance per wave, so each
|
|
26
57
|
* wave carries its own palette. Guarded by a signature so it only rebuilds when that wave's
|
|
27
58
|
* palette actually changes (not every refresh).
|
|
@@ -201,6 +232,11 @@ var WaveRenderer = class {
|
|
|
201
232
|
resizeObserver;
|
|
202
233
|
intersectionObserver;
|
|
203
234
|
motionQuery;
|
|
235
|
+
/** Re-armed at the live devicePixelRatio on every change — see watchDpr(). */
|
|
236
|
+
dprQuery;
|
|
237
|
+
/** Pending coalesced resize (0 = none), and the metrics the last resize() actually applied. */
|
|
238
|
+
resizeRaf = 0;
|
|
239
|
+
lastResize;
|
|
204
240
|
capturing = false;
|
|
205
241
|
/** Fixed backing-buffer dimensions used by the studio's visible export frame. Embeds leave
|
|
206
242
|
* this unset and continue to resize responsively with their container and device DPR. */
|
|
@@ -256,6 +292,7 @@ var WaveRenderer = class {
|
|
|
256
292
|
this.intersectionObserver.observe(container);
|
|
257
293
|
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
258
294
|
this.resizeObserver.observe(container);
|
|
295
|
+
this.watchDpr();
|
|
259
296
|
this.applyBackground();
|
|
260
297
|
this.buildWaves();
|
|
261
298
|
this.resize();
|
|
@@ -355,7 +392,7 @@ var WaveRenderer = class {
|
|
|
355
392
|
uEdgeFeather: { value: .1 },
|
|
356
393
|
uOpacity: { value: 1 },
|
|
357
394
|
uSquared: { value: 1 },
|
|
358
|
-
uResolution: { value: new THREE.Vector2(
|
|
395
|
+
uResolution: { value: this.renderer.getDrawingBufferSize(new THREE.Vector2()) },
|
|
359
396
|
uAmbient: { value: .45 },
|
|
360
397
|
uNumLights: { value: 1 },
|
|
361
398
|
uLightPos: { value: lightPos },
|
|
@@ -370,6 +407,12 @@ var WaveRenderer = class {
|
|
|
370
407
|
uLineDerivativePower: { value: .95 },
|
|
371
408
|
uMaxWidth: { value: 1232 },
|
|
372
409
|
uClearColor: { value: new THREE.Vector3(1, 1, 1) },
|
|
410
|
+
uHelixTurns: { value: 0 },
|
|
411
|
+
uHelixRadius: { value: 0 },
|
|
412
|
+
uHelixRoll: { value: 0 },
|
|
413
|
+
uHelixPhase: { value: 0 },
|
|
414
|
+
uRungAmount: { value: 0 },
|
|
415
|
+
uRungThickness: { value: 1 },
|
|
373
416
|
uPointer: { value: new THREE.Vector2(0, 0) },
|
|
374
417
|
uPointerActive: { value: 0 },
|
|
375
418
|
uPointerRadius: { value: .6 },
|
|
@@ -399,6 +442,9 @@ var WaveRenderer = class {
|
|
|
399
442
|
if ((sc?.detailAmount ?? 0) !== 0 || bindsDetail) defines.DETAIL_OCTAVE = "";
|
|
400
443
|
if ((sc?.depthTint ?? 0) > 0) defines.DEPTH_TINT = "";
|
|
401
444
|
if ((sc?.edgeFeather ?? .1) !== .1) defines.EDGE_FEATHER = "";
|
|
445
|
+
const bindsHelix = sc?.interaction?.bindings?.some((b) => b.target.startsWith("helix")) ?? false;
|
|
446
|
+
if ((sc?.helixRadius ?? 0) !== 0 || (sc?.helixRoll ?? 0) !== 0 || bindsHelix) defines.HELIX = "";
|
|
447
|
+
if (sc?.theme === "wireframe" && (sc.rungAmount ?? 0) > 0) defines.RUNGS = "";
|
|
402
448
|
if (sc && wavePointerFxActive(this.config, sc)) {
|
|
403
449
|
defines.POINTER_FX = "";
|
|
404
450
|
if (waveRipplesActive(this.config, sc)) defines.POINTER_RIPPLES = "";
|
|
@@ -550,6 +596,8 @@ var WaveRenderer = class {
|
|
|
550
596
|
u.uLineAmount.value = sc.lineAmount ?? 425;
|
|
551
597
|
u.uLineThickness.value = sc.lineThickness ?? 1;
|
|
552
598
|
u.uLineDerivativePower.value = sc.lineDerivativePower ?? .95;
|
|
599
|
+
u.uRungAmount.value = sc.rungAmount ?? 0;
|
|
600
|
+
u.uRungThickness.value = sc.rungThickness ?? 1;
|
|
553
601
|
u.uMaxWidth.value = sc.maxWidth ?? 1232;
|
|
554
602
|
hexToLinearVec3(this.config.background, u.uClearColor.value);
|
|
555
603
|
u.uFiberCount.value = sc.fiberCount;
|
|
@@ -606,6 +654,10 @@ var WaveRenderer = class {
|
|
|
606
654
|
u.uTwPowX.value = sc.twistPower.x;
|
|
607
655
|
u.uTwPowY.value = sc.twistPower.y;
|
|
608
656
|
u.uTwPowZ.value = sc.twistPower.z;
|
|
657
|
+
u.uHelixTurns.value = sc.helixTurns ?? 0;
|
|
658
|
+
u.uHelixRadius.value = sc.helixRadius ?? 0;
|
|
659
|
+
u.uHelixRoll.value = sc.helixRoll ?? 0;
|
|
660
|
+
u.uHelixPhase.value = sc.helixPhase ?? 0;
|
|
609
661
|
wave.mesh.scale.set(sc.scale.x, sc.scale.y, sc.scale.z);
|
|
610
662
|
wave.mesh.rotation.set(THREE.MathUtils.degToRad(sc.rotation.x), THREE.MathUtils.degToRad(sc.rotation.y), THREE.MathUtils.degToRad(sc.rotation.z));
|
|
611
663
|
wave.mesh.position.set(sc.position.x, sc.position.y, sc.position.z);
|
|
@@ -1093,9 +1145,57 @@ var WaveRenderer = class {
|
|
|
1093
1145
|
this.halftoneCmykPass = void 0;
|
|
1094
1146
|
}
|
|
1095
1147
|
}
|
|
1148
|
+
/** Coalesce observer-driven resizes to one per frame, and drop any that don't move a device pixel.
|
|
1149
|
+
*
|
|
1150
|
+
* resize() is expensive — composer.setSize reallocates every pass's render target, and
|
|
1151
|
+
* applyBackground() rebuilds a container-sized canvas + texture for gradient/image backgrounds.
|
|
1152
|
+
* The old 1:1 `observe → resize()` paid that for observations that changed nothing: the observer
|
|
1153
|
+
* reports fractional content-box sizes, so sub-pixel layout shifts (and anything that rounds to
|
|
1154
|
+
* the same backing buffer) triggered a full reallocation, as did every observation while an
|
|
1155
|
+
* export frame is pinned and the container is not what drives the buffer at all.
|
|
1156
|
+
*
|
|
1157
|
+
* Genuine per-frame changes — a mobile URL bar collapsing animates the container height — still
|
|
1158
|
+
* resize every frame. That work is necessary; the canvas would otherwise stretch. What is
|
|
1159
|
+
* removed is the redundant work, not the real work.
|
|
1160
|
+
*
|
|
1161
|
+
* Only this path is throttled. `resize()` itself stays synchronous and unconditional — context
|
|
1162
|
+
* restore and setOutputSize must re-apply immediately, and on a fresh GPU context the metrics
|
|
1163
|
+
* are unchanged but the resources are not. */
|
|
1096
1164
|
onResize = () => {
|
|
1165
|
+
if (this.resizeRaf) return;
|
|
1166
|
+
this.resizeRaf = requestAnimationFrame(() => {
|
|
1167
|
+
this.resizeRaf = 0;
|
|
1168
|
+
const next = this.viewportMetrics();
|
|
1169
|
+
const last = this.lastResize;
|
|
1170
|
+
if (last && next.w === last.w && next.h === last.h && next.dpr === last.dpr) return;
|
|
1171
|
+
this.resize();
|
|
1172
|
+
});
|
|
1173
|
+
};
|
|
1174
|
+
/** Re-arm the DPR watch and re-render at the new device-pixel ratio.
|
|
1175
|
+
*
|
|
1176
|
+
* ResizeObserver watches the CSS box only, so browser zoom or dragging the window to a monitor
|
|
1177
|
+
* with a different DPR changes devicePixelRatio without changing the box — the backing buffer
|
|
1178
|
+
* stayed at the old resolution and the wave went soft until something else forced a resize. */
|
|
1179
|
+
onDprChange = () => {
|
|
1180
|
+
this.watchDpr();
|
|
1097
1181
|
this.resize();
|
|
1098
1182
|
};
|
|
1183
|
+
/** A `(resolution: Xdppx)` query only fires when we LEAVE the current ratio, so it is re-armed at
|
|
1184
|
+
* the new one on every change. */
|
|
1185
|
+
watchDpr() {
|
|
1186
|
+
this.dprQuery?.removeEventListener("change", this.onDprChange);
|
|
1187
|
+
this.dprQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio || 1}dppx)`);
|
|
1188
|
+
this.dprQuery.addEventListener("change", this.onDprChange);
|
|
1189
|
+
}
|
|
1190
|
+
/** The backing-buffer metrics resize() will apply: the export frame when one is pinned, else the
|
|
1191
|
+
* container box at the (clamped) device-pixel ratio. */
|
|
1192
|
+
viewportMetrics() {
|
|
1193
|
+
return {
|
|
1194
|
+
w: this.outputSize?.width ?? Math.max(1, this.container.clientWidth),
|
|
1195
|
+
h: this.outputSize?.height ?? Math.max(1, this.container.clientHeight),
|
|
1196
|
+
dpr: this.outputSize ? 1 : Math.min(window.devicePixelRatio || 1, this.config.dprMax)
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1099
1199
|
onContextLost = (e) => {
|
|
1100
1200
|
e.preventDefault();
|
|
1101
1201
|
cancelAnimationFrame(this.rafId);
|
|
@@ -1111,9 +1211,12 @@ var WaveRenderer = class {
|
|
|
1111
1211
|
this.updateRunning();
|
|
1112
1212
|
};
|
|
1113
1213
|
resize() {
|
|
1114
|
-
const w
|
|
1115
|
-
|
|
1116
|
-
|
|
1214
|
+
const { w, h, dpr } = this.viewportMetrics();
|
|
1215
|
+
this.lastResize = {
|
|
1216
|
+
w,
|
|
1217
|
+
h,
|
|
1218
|
+
dpr
|
|
1219
|
+
};
|
|
1117
1220
|
this.renderer.setPixelRatio(dpr);
|
|
1118
1221
|
this.renderer.setSize(w, h, !this.outputSize);
|
|
1119
1222
|
if (this.outputSize) {
|
|
@@ -1387,16 +1490,20 @@ var WaveRenderer = class {
|
|
|
1387
1490
|
onAfterRenderFrame() {}
|
|
1388
1491
|
/** Hook ④: called at the end of resize(), before the trailing renderOnce(). */
|
|
1389
1492
|
onAfterResize() {}
|
|
1390
|
-
/** Responsive ortho zoom:
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
* contain (fit with letterbox bands). Cover keeps the wave filling the frame on every screen. */
|
|
1493
|
+
/** Responsive ortho zoom: map the FRAME_W × FRAME_H reference frame onto the canvas (per
|
|
1494
|
+
* config.cameraFit / cameraMinVisibleWidth — see {@link frameZoom}) so the wave frames the same
|
|
1495
|
+
* at any size/aspect/dpr, times the user's cameraZoom. */
|
|
1394
1496
|
applyZoom() {
|
|
1395
1497
|
const dw = this.camera.right - this.camera.left;
|
|
1396
1498
|
const dh = this.camera.top - this.camera.bottom;
|
|
1397
|
-
this.camera.zoom =
|
|
1499
|
+
this.camera.zoom = this.baseFrameZoom(dw, dh) * (this.config.cameraZoom ?? 1) * this.interactionZoom;
|
|
1398
1500
|
this.camera.updateProjectionMatrix();
|
|
1399
1501
|
}
|
|
1502
|
+
/** The responsive base zoom for the current config's framing policy, before the cameraZoom
|
|
1503
|
+
* multiplier. Subclasses invert this to recover cameraZoom from a live camera. */
|
|
1504
|
+
baseFrameZoom(dw, dh) {
|
|
1505
|
+
return frameZoom(dw, dh, this.config.cameraFit ?? "cover", this.config.cameraMinVisibleWidth ?? 0);
|
|
1506
|
+
}
|
|
1400
1507
|
/** Fit the orthographic near/far planes to the scene before every render, so no part of a wave
|
|
1401
1508
|
* is ever clipped as the camera orbits / dollies / pans (or when waves are added or scaled).
|
|
1402
1509
|
*
|
|
@@ -1509,12 +1616,14 @@ var WaveRenderer = class {
|
|
|
1509
1616
|
}
|
|
1510
1617
|
dispose() {
|
|
1511
1618
|
cancelAnimationFrame(this.rafId);
|
|
1619
|
+
cancelAnimationFrame(this.resizeRaf);
|
|
1512
1620
|
this.running = false;
|
|
1513
1621
|
this.interaction?.dispose();
|
|
1514
1622
|
this.interaction = void 0;
|
|
1515
1623
|
this.resizeObserver.disconnect();
|
|
1516
1624
|
this.intersectionObserver.disconnect();
|
|
1517
1625
|
this.motionQuery.removeEventListener("change", this.onMotionChange);
|
|
1626
|
+
this.dprQuery?.removeEventListener("change", this.onDprChange);
|
|
1518
1627
|
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
1519
1628
|
this.renderer.domElement.removeEventListener("webglcontextlost", this.onContextLost);
|
|
1520
1629
|
this.renderer.domElement.removeEventListener("webglcontextrestored", this.onContextRestored);
|
|
@@ -1538,6 +1647,6 @@ var WaveRenderer = class {
|
|
|
1538
1647
|
}
|
|
1539
1648
|
};
|
|
1540
1649
|
//#endregion
|
|
1541
|
-
export { FRAME_H, FRAME_W, WaveRenderer, hexToLinearVec3 };
|
|
1650
|
+
export { FRAME_H, FRAME_W, WaveRenderer, frameZoom, hexToLinearVec3 };
|
|
1542
1651
|
|
|
1543
1652
|
//# sourceMappingURL=WaveRenderer.js.map
|