@wave3d/core 0.1.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/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/config/model.d.ts +240 -0
- package/dist/config/model.js +496 -0
- package/dist/config/model.js.map +1 -0
- package/dist/core-loader.d.ts +10 -0
- package/dist/core-loader.js +12 -0
- package/dist/core-loader.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/presets.d.ts +8 -0
- package/dist/presets.js +544 -0
- package/dist/presets.js.map +1 -0
- package/dist/renderer/WaveGeometry.d.ts +31 -0
- package/dist/renderer/WaveGeometry.js +92 -0
- package/dist/renderer/WaveGeometry.js.map +1 -0
- package/dist/renderer/WaveRenderer.d.ts +232 -0
- package/dist/renderer/WaveRenderer.js +1118 -0
- package/dist/renderer/WaveRenderer.js.map +1 -0
- package/dist/renderer/heroPalette.d.ts +10 -0
- package/dist/renderer/heroPalette.js +34 -0
- package/dist/renderer/heroPalette.js.map +1 -0
- package/dist/renderer/index.d.ts +4 -0
- package/dist/renderer/index.js +4 -0
- package/dist/renderer/palette.d.ts +67 -0
- package/dist/renderer/palette.js +535 -0
- package/dist/renderer/palette.js.map +1 -0
- package/dist/renderer/shaders.js +545 -0
- package/dist/renderer/shaders.js.map +1 -0
- package/dist/shell/createWave.d.ts +58 -0
- package/dist/shell/createWave.js +161 -0
- package/dist/shell/createWave.js.map +1 -0
- package/dist/shell/poster.js +64 -0
- package/dist/shell/poster.js.map +1 -0
- package/dist/shell/probe.js +34 -0
- package/dist/shell/probe.js.map +1 -0
- package/dist/standalone/wave3d.standalone.js +13507 -0
- package/dist/standalone.d.ts +13 -0
- package/dist/standalone.js +17 -0
- package/dist/standalone.js.map +1 -0
- package/dist/studio/StudioWaveRenderer.d.ts +187 -0
- package/dist/studio/StudioWaveRenderer.js +905 -0
- package/dist/studio/StudioWaveRenderer.js.map +1 -0
- package/dist/studio/index.d.ts +3 -0
- package/dist/studio/index.js +3 -0
- package/dist/studio/randomize.d.ts +28 -0
- package/dist/studio/randomize.js +264 -0
- package/dist/studio/randomize.js.map +1 -0
- package/dist/util/base64.js +14 -0
- package/dist/util/base64.js.map +1 -0
- package/dist/util/math.js +18 -0
- package/dist/util/math.js.map +1 -0
- package/package.json +76 -0
- package/skills/wave3d/SKILL.md +158 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
import { ensureStudioConfig } from "../config/model.js";
|
|
2
|
+
import { fragmentShader, lineFragmentShader, postFragmentShader, postVertexShader, vertexShader } from "./shaders.js";
|
|
3
|
+
import { WaveGeometry } from "./WaveGeometry.js";
|
|
4
|
+
import { PALETTE_MAPS, buildBackgroundGradientCanvas, buildBackgroundImageCanvas, buildBackgroundMeshCanvas, buildPaletteTexture, canvasToTexture, configurePaletteTexture, drawBackgroundMediaFrame, loadPaletteImage, paletteMapCanvas, paletteSignature } from "./palette.js";
|
|
5
|
+
import { buildHeroPaletteCanvas, buildHeroPaletteTexture } from "./heroPalette.js";
|
|
6
|
+
import * as THREE from "three";
|
|
7
|
+
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
|
|
8
|
+
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
|
|
9
|
+
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
|
|
10
|
+
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
|
|
11
|
+
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
|
|
12
|
+
//#region src/renderer/WaveRenderer.ts
|
|
13
|
+
const BASE_SEGMENTS = 220;
|
|
14
|
+
/** Reference frame (world units) the orthographic camera fills at cameraZoom 1. The wave is
|
|
15
|
+
* framed by COVERING this FRAME_W × FRAME_H rectangle (centred on cameraTarget) into the canvas
|
|
16
|
+
* — scaled to fill both dimensions, cropping the aspect overflow — so a given cameraZoom /
|
|
17
|
+
* cameraTarget frames the wave the SAME at any canvas size or aspect (only the cropped margin
|
|
18
|
+
* differs). FRAME_H = FRAME_W / (16/9) makes the reference a 16:9 rectangle; for canvases wider
|
|
19
|
+
* than that the width binds, narrower ones zoom in to fill instead of
|
|
20
|
+
* showing empty bands. This is what makes a saved preset reproduce on anyone's screen. */
|
|
21
|
+
const FRAME_W = 1333;
|
|
22
|
+
const FRAME_H = 750;
|
|
23
|
+
/**
|
|
24
|
+
* Per-wave 2D palette texture (+ optional looping video). One instance per wave, so each
|
|
25
|
+
* wave carries its own palette. Guarded by a signature so it only rebuilds when that wave's
|
|
26
|
+
* palette actually changes (not every refresh).
|
|
27
|
+
*/
|
|
28
|
+
var WavePalette = class {
|
|
29
|
+
makeVideo;
|
|
30
|
+
onVideoReady;
|
|
31
|
+
texture;
|
|
32
|
+
sig = "";
|
|
33
|
+
video;
|
|
34
|
+
videoUrl = "";
|
|
35
|
+
failedUrl = "";
|
|
36
|
+
constructor(makeVideo, onVideoReady) {
|
|
37
|
+
this.makeVideo = makeVideo;
|
|
38
|
+
this.onVideoReady = onVideoReady;
|
|
39
|
+
}
|
|
40
|
+
/** Rebuild this wave's palette texture from its config (video / custom image / stops /
|
|
41
|
+
* built-in map / hero LUT) and point the wave's palette uniforms at it. */
|
|
42
|
+
apply(cfg, uniforms) {
|
|
43
|
+
const videoUrl = cfg.paletteVideoUrl;
|
|
44
|
+
const url = cfg.paletteImageUrl;
|
|
45
|
+
const source = cfg.paletteSource ?? "hero";
|
|
46
|
+
let sig;
|
|
47
|
+
let build;
|
|
48
|
+
if (videoUrl) {
|
|
49
|
+
this.ensureVideo(videoUrl);
|
|
50
|
+
if (this.video?.readyState && this.video.readyState >= 2) {
|
|
51
|
+
sig = "video|" + videoUrl;
|
|
52
|
+
build = () => configurePaletteTexture(new THREE.VideoTexture(this.video));
|
|
53
|
+
} else {
|
|
54
|
+
sig = "video-loading|" + videoUrl;
|
|
55
|
+
build = () => buildHeroPaletteTexture();
|
|
56
|
+
}
|
|
57
|
+
} else if (url) {
|
|
58
|
+
this.clearVideo();
|
|
59
|
+
sig = "url|" + url;
|
|
60
|
+
build = () => loadPaletteImage(url);
|
|
61
|
+
} else if (source === "stops") {
|
|
62
|
+
this.clearVideo();
|
|
63
|
+
const opts = {
|
|
64
|
+
stops: cfg.palette,
|
|
65
|
+
edgeColor: cfg.paletteEdgeColor ?? "#8e9dff",
|
|
66
|
+
edgeAmount: cfg.paletteEdgeAmount ?? .3
|
|
67
|
+
};
|
|
68
|
+
sig = "stops|" + paletteSignature(opts);
|
|
69
|
+
build = () => buildPaletteTexture(opts);
|
|
70
|
+
} else if (PALETTE_MAPS[source]) {
|
|
71
|
+
this.clearVideo();
|
|
72
|
+
sig = "map|" + source;
|
|
73
|
+
build = () => canvasToTexture(paletteMapCanvas(PALETTE_MAPS[source]));
|
|
74
|
+
} else {
|
|
75
|
+
this.clearVideo();
|
|
76
|
+
sig = "hero";
|
|
77
|
+
build = () => buildHeroPaletteTexture();
|
|
78
|
+
}
|
|
79
|
+
if (sig !== this.sig || !this.texture) {
|
|
80
|
+
this.texture?.dispose();
|
|
81
|
+
this.texture = build();
|
|
82
|
+
this.sig = sig;
|
|
83
|
+
}
|
|
84
|
+
uniforms.uPalette.value = this.texture;
|
|
85
|
+
uniforms.uUsePalette.value = cfg.usePaletteTexture === false ? 0 : 1;
|
|
86
|
+
uniforms.uPaletteRaw.value = 1;
|
|
87
|
+
}
|
|
88
|
+
/** Play/pause this wave's palette video with the render loop. */
|
|
89
|
+
syncPlayback(cfg, running) {
|
|
90
|
+
const v = this.video;
|
|
91
|
+
if (!v) return;
|
|
92
|
+
if (running && cfg.gradientType !== "mesh" && cfg.usePaletteTexture !== false && !!cfg.paletteVideoUrl) v.play().catch(() => {});
|
|
93
|
+
else v.pause();
|
|
94
|
+
}
|
|
95
|
+
ensureVideo(url) {
|
|
96
|
+
if (this.videoUrl === url || this.failedUrl === url) return;
|
|
97
|
+
this.clearVideo();
|
|
98
|
+
this.videoUrl = url;
|
|
99
|
+
const video = this.makeVideo(url);
|
|
100
|
+
video.addEventListener("loadeddata", () => {
|
|
101
|
+
this.video = video;
|
|
102
|
+
this.failedUrl = "";
|
|
103
|
+
this.sig = "";
|
|
104
|
+
this.onVideoReady();
|
|
105
|
+
}, { once: true });
|
|
106
|
+
video.addEventListener("error", () => {
|
|
107
|
+
this.failedUrl = url;
|
|
108
|
+
this.sig = "";
|
|
109
|
+
this.onVideoReady();
|
|
110
|
+
}, { once: true });
|
|
111
|
+
this.video = video;
|
|
112
|
+
video.load();
|
|
113
|
+
}
|
|
114
|
+
clearVideo() {
|
|
115
|
+
if (this.video) {
|
|
116
|
+
this.video.pause();
|
|
117
|
+
this.video.removeAttribute("src");
|
|
118
|
+
this.video.load();
|
|
119
|
+
}
|
|
120
|
+
this.video = void 0;
|
|
121
|
+
this.videoUrl = "";
|
|
122
|
+
}
|
|
123
|
+
dispose() {
|
|
124
|
+
this.texture?.dispose();
|
|
125
|
+
this.texture = void 0;
|
|
126
|
+
this.clearVideo();
|
|
127
|
+
this.failedUrl = "";
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const HEX_SCRATCH = new THREE.Color();
|
|
131
|
+
/** Convert an sRGB hex string to a linear-space RGB vector (three's ColorManagement does the
|
|
132
|
+
* sRGB→linear conversion on parse). Exported for the studio subclass's live light-uniform push. */
|
|
133
|
+
function hexToLinearVec3(hex, target) {
|
|
134
|
+
const c = HEX_SCRATCH.set(hex);
|
|
135
|
+
return target.set(c.r, c.g, c.b);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Renders a gradient "wave of light" from a {@link StudioConfig}. Framework-agnostic:
|
|
139
|
+
* it needs only a DOM container and a config. The studio mutates the config in
|
|
140
|
+
* place and calls `refresh()` / `rebuild()`.
|
|
141
|
+
*/
|
|
142
|
+
var WaveRenderer = class {
|
|
143
|
+
renderer;
|
|
144
|
+
scene = new THREE.Scene();
|
|
145
|
+
camera;
|
|
146
|
+
group = new THREE.Group();
|
|
147
|
+
composer;
|
|
148
|
+
postPass;
|
|
149
|
+
/** Optional bloom pass — created lazily when bloomStrength first goes >0, removed at 0. */
|
|
150
|
+
bloomPass;
|
|
151
|
+
container;
|
|
152
|
+
respectReducedMotion;
|
|
153
|
+
skipIntroRamp;
|
|
154
|
+
config;
|
|
155
|
+
waves = [];
|
|
156
|
+
backgroundTexture;
|
|
157
|
+
backgroundSig = "";
|
|
158
|
+
backgroundImage;
|
|
159
|
+
backgroundImageUrl = "";
|
|
160
|
+
failedBackgroundImageUrl = "";
|
|
161
|
+
backgroundVideo;
|
|
162
|
+
backgroundVideoUrl = "";
|
|
163
|
+
failedBackgroundVideoUrl = "";
|
|
164
|
+
backgroundVideoCanvas;
|
|
165
|
+
/** Authored default camera pose, for "Reset camera". */
|
|
166
|
+
homeCamPos = new THREE.Vector3();
|
|
167
|
+
homeCamTarget = new THREE.Vector3();
|
|
168
|
+
clipBox = new THREE.Box3();
|
|
169
|
+
clipSphere = new THREE.Sphere();
|
|
170
|
+
clipTmpA = new THREE.Vector3();
|
|
171
|
+
clipTmpB = new THREE.Vector3();
|
|
172
|
+
clock = new THREE.Clock();
|
|
173
|
+
time = 0;
|
|
174
|
+
rafId = 0;
|
|
175
|
+
running = false;
|
|
176
|
+
started = false;
|
|
177
|
+
visible = true;
|
|
178
|
+
pageVisible = true;
|
|
179
|
+
reducedMotion = false;
|
|
180
|
+
/** Intro ramp: eases animation time 0→1 over ~1s on load (when config.introRamp). */
|
|
181
|
+
introTimeRamp = 0;
|
|
182
|
+
resizeObserver;
|
|
183
|
+
intersectionObserver;
|
|
184
|
+
motionQuery;
|
|
185
|
+
capturing = false;
|
|
186
|
+
/** Fixed backing-buffer dimensions used by the studio's visible export frame. Embeds leave
|
|
187
|
+
* this unset and continue to resize responsively with their container and device DPR. */
|
|
188
|
+
outputSize;
|
|
189
|
+
constructor(container, config, options = {}) {
|
|
190
|
+
this.container = container;
|
|
191
|
+
this.config = ensureStudioConfig(config);
|
|
192
|
+
this.respectReducedMotion = options.respectReducedMotion ?? true;
|
|
193
|
+
this.skipIntroRamp = options.skipIntroRamp ?? false;
|
|
194
|
+
this.renderer = new THREE.WebGLRenderer({
|
|
195
|
+
antialias: true,
|
|
196
|
+
alpha: true,
|
|
197
|
+
preserveDrawingBuffer: true,
|
|
198
|
+
powerPreference: "high-performance"
|
|
199
|
+
});
|
|
200
|
+
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
201
|
+
this.renderer.setClearColor(0, 0);
|
|
202
|
+
container.appendChild(this.renderer.domElement);
|
|
203
|
+
this.renderer.domElement.addEventListener("webglcontextlost", this.onContextLost, false);
|
|
204
|
+
this.renderer.domElement.addEventListener("webglcontextrestored", this.onContextRestored, false);
|
|
205
|
+
this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 1, 1e4);
|
|
206
|
+
this.camera.position.set(config.cameraPosition.x, config.cameraPosition.y, config.cameraPosition.z);
|
|
207
|
+
this.camera.zoom = config.cameraZoom ?? 1;
|
|
208
|
+
this.camera.lookAt(config.cameraTarget.x, config.cameraTarget.y, config.cameraTarget.z);
|
|
209
|
+
this.camera.updateProjectionMatrix();
|
|
210
|
+
this.homeCamPos.copy(this.camera.position);
|
|
211
|
+
this.homeCamTarget.set(config.cameraTarget.x, config.cameraTarget.y, config.cameraTarget.z);
|
|
212
|
+
this.scene.add(this.group);
|
|
213
|
+
this.composer = new EffectComposer(this.renderer);
|
|
214
|
+
this.composer.addPass(new RenderPass(this.scene, this.camera));
|
|
215
|
+
this.postPass = new ShaderPass({
|
|
216
|
+
uniforms: {
|
|
217
|
+
tDiffuse: { value: null },
|
|
218
|
+
uResolution: { value: new THREE.Vector2(1, 1) },
|
|
219
|
+
uBlurAmount: { value: config.blur },
|
|
220
|
+
uBlurSamples: { value: 6 },
|
|
221
|
+
uGrainAmount: { value: config.grain },
|
|
222
|
+
uTime: { value: 0 }
|
|
223
|
+
},
|
|
224
|
+
vertexShader: postVertexShader,
|
|
225
|
+
fragmentShader: postFragmentShader
|
|
226
|
+
});
|
|
227
|
+
this.composer.addPass(this.postPass);
|
|
228
|
+
this.composer.addPass(new OutputPass());
|
|
229
|
+
this.motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
230
|
+
this.reducedMotion = this.respectReducedMotion && this.motionQuery.matches;
|
|
231
|
+
this.motionQuery.addEventListener("change", this.onMotionChange);
|
|
232
|
+
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
233
|
+
this.intersectionObserver = new IntersectionObserver((entries) => {
|
|
234
|
+
this.visible = entries[0]?.isIntersecting ?? true;
|
|
235
|
+
this.updateRunning();
|
|
236
|
+
}, { rootMargin: "100px" });
|
|
237
|
+
this.intersectionObserver.observe(container);
|
|
238
|
+
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
239
|
+
this.resizeObserver.observe(container);
|
|
240
|
+
this.applyBackground();
|
|
241
|
+
this.buildWaves();
|
|
242
|
+
this.resize();
|
|
243
|
+
}
|
|
244
|
+
get segments() {
|
|
245
|
+
const q = this.config.quality / Math.sqrt(Math.max(1, this.config.waves.length));
|
|
246
|
+
return THREE.MathUtils.clamp(Math.round(BASE_SEGMENTS * q), 24, 360);
|
|
247
|
+
}
|
|
248
|
+
makeUniforms() {
|
|
249
|
+
const colors = [];
|
|
250
|
+
const colorPos = [];
|
|
251
|
+
for (let i = 0; i < 8; i++) {
|
|
252
|
+
colors.push(new THREE.Vector3(1, 1, 1));
|
|
253
|
+
colorPos.push(i / 7);
|
|
254
|
+
}
|
|
255
|
+
const meshPointPos = [];
|
|
256
|
+
const meshPointColor = [];
|
|
257
|
+
const meshPointInfluence = [];
|
|
258
|
+
for (let i = 0; i < 8; i++) {
|
|
259
|
+
meshPointPos.push(new THREE.Vector2(.5, .5));
|
|
260
|
+
meshPointColor.push(new THREE.Vector3(1, 1, 1));
|
|
261
|
+
meshPointInfluence.push(.65);
|
|
262
|
+
}
|
|
263
|
+
const lightPos = [];
|
|
264
|
+
const lightColor = [];
|
|
265
|
+
const lightIntensity = [];
|
|
266
|
+
for (let i = 0; i < 8; i++) {
|
|
267
|
+
lightPos.push(new THREE.Vector3());
|
|
268
|
+
lightColor.push(new THREE.Vector3(1, 1, 1));
|
|
269
|
+
lightIntensity.push(0);
|
|
270
|
+
}
|
|
271
|
+
const bandBounds = [];
|
|
272
|
+
const bandParams = [];
|
|
273
|
+
const bandParaPow = [];
|
|
274
|
+
for (let i = 0; i < 4; i++) {
|
|
275
|
+
bandBounds.push(new THREE.Vector4());
|
|
276
|
+
bandParams.push(new THREE.Vector4());
|
|
277
|
+
bandParaPow.push(0);
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
uTime: { value: 0 },
|
|
281
|
+
uSpeed: { value: .05 },
|
|
282
|
+
uSeed: { value: 0 },
|
|
283
|
+
uDispFreqX: { value: .003234 },
|
|
284
|
+
uDispFreqZ: { value: .00799 },
|
|
285
|
+
uDispAmount: { value: 6.051 },
|
|
286
|
+
uDetailFreq: { value: .04 },
|
|
287
|
+
uDetailAmount: { value: 0 },
|
|
288
|
+
uTwFreqX: { value: -.055 },
|
|
289
|
+
uTwFreqY: { value: .077 },
|
|
290
|
+
uTwFreqZ: { value: -.518 },
|
|
291
|
+
uTwPowX: { value: 3.95 },
|
|
292
|
+
uTwPowY: { value: 5.85 },
|
|
293
|
+
uTwPowZ: { value: 6.33 },
|
|
294
|
+
uLoopSeconds: { value: 0 },
|
|
295
|
+
uColors: { value: colors },
|
|
296
|
+
uColorPos: { value: colorPos },
|
|
297
|
+
uColorCount: { value: 2 },
|
|
298
|
+
uGradType: { value: 0 },
|
|
299
|
+
uGradAngle: { value: 0 },
|
|
300
|
+
uGradShift: { value: .15 },
|
|
301
|
+
uMeshPointPos: { value: meshPointPos },
|
|
302
|
+
uMeshPointColor: { value: meshPointColor },
|
|
303
|
+
uMeshPointInfluence: { value: meshPointInfluence },
|
|
304
|
+
uMeshPointCount: { value: 0 },
|
|
305
|
+
uMeshSoftness: { value: .62 },
|
|
306
|
+
uPalette: { value: null },
|
|
307
|
+
uUsePalette: { value: 1 },
|
|
308
|
+
uPaletteRaw: { value: 1 },
|
|
309
|
+
uPaletteScale: { value: new THREE.Vector2(1, 1) },
|
|
310
|
+
uPaletteOffset: { value: new THREE.Vector2(0, 0) },
|
|
311
|
+
uPaletteRotation: { value: 0 },
|
|
312
|
+
uDebug: { value: 0 },
|
|
313
|
+
uSheen: { value: 1 },
|
|
314
|
+
uRoundness: { value: .35 },
|
|
315
|
+
uIridescence: { value: 0 },
|
|
316
|
+
uDepthTint: { value: 0 },
|
|
317
|
+
uDepthTintColor: { value: new THREE.Vector3() },
|
|
318
|
+
uHueShift: { value: 0 },
|
|
319
|
+
uContrast: { value: 1 },
|
|
320
|
+
uSaturation: { value: 1 },
|
|
321
|
+
uFiberCount: { value: 90 },
|
|
322
|
+
uFiberStrength: { value: .25 },
|
|
323
|
+
uTexture: { value: 0 },
|
|
324
|
+
uCreaseLight: { value: .15 },
|
|
325
|
+
uCreaseSharpness: { value: 2 },
|
|
326
|
+
uCreaseSoftness: { value: 1 },
|
|
327
|
+
uEdgeFade: { value: .06 },
|
|
328
|
+
uEdgeFeather: { value: .1 },
|
|
329
|
+
uOpacity: { value: 1 },
|
|
330
|
+
uSquared: { value: 1 },
|
|
331
|
+
uResolution: { value: new THREE.Vector2(1, 1) },
|
|
332
|
+
uAmbient: { value: .45 },
|
|
333
|
+
uNumLights: { value: 1 },
|
|
334
|
+
uLightPos: { value: lightPos },
|
|
335
|
+
uLightColor: { value: lightColor },
|
|
336
|
+
uLightIntensity: { value: lightIntensity },
|
|
337
|
+
uNumNoiseBands: { value: 0 },
|
|
338
|
+
uNoiseBandBounds: { value: bandBounds },
|
|
339
|
+
uNoiseBandParams: { value: bandParams },
|
|
340
|
+
uNoiseBandParaPow: { value: bandParaPow },
|
|
341
|
+
uLineAmount: { value: 425 },
|
|
342
|
+
uLineThickness: { value: 1 },
|
|
343
|
+
uLineDerivativePower: { value: .95 },
|
|
344
|
+
uMaxWidth: { value: 1232 },
|
|
345
|
+
uClearColor: { value: new THREE.Vector3(1, 1, 1) }
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/** Vertex-shader #defines for a wave: TWIST_MOTION (per-wave animated twist wobble) and
|
|
349
|
+
* LOOP_MOTION (scene-level seamless loop). Both select #ifdef-gated code paths; an empty
|
|
350
|
+
* object compiles the default (linear-time) program. */
|
|
351
|
+
waveDefines(sc) {
|
|
352
|
+
const defines = {};
|
|
353
|
+
if (sc?.twistMotion) defines.TWIST_MOTION = "";
|
|
354
|
+
if ((this.config.loopSeconds ?? 0) > 0) defines.LOOP_MOTION = "";
|
|
355
|
+
if ((sc?.detailAmount ?? 0) !== 0) defines.DETAIL_OCTAVE = "";
|
|
356
|
+
if ((sc?.depthTint ?? 0) > 0) defines.DEPTH_TINT = "";
|
|
357
|
+
if ((sc?.edgeFeather ?? .1) !== .1) defines.EDGE_FEATHER = "";
|
|
358
|
+
return defines;
|
|
359
|
+
}
|
|
360
|
+
addWave() {
|
|
361
|
+
const geo = new WaveGeometry(this.segments);
|
|
362
|
+
const sc = this.config.waves[this.waves.length] ?? this.config.waves[0];
|
|
363
|
+
const material = new THREE.ShaderMaterial({
|
|
364
|
+
uniforms: this.makeUniforms(),
|
|
365
|
+
defines: this.waveDefines(sc),
|
|
366
|
+
vertexShader,
|
|
367
|
+
fragmentShader: sc?.theme === "wireframe" ? lineFragmentShader : fragmentShader,
|
|
368
|
+
transparent: true,
|
|
369
|
+
depthTest: true,
|
|
370
|
+
depthWrite: true,
|
|
371
|
+
side: THREE.DoubleSide
|
|
372
|
+
});
|
|
373
|
+
this.applyBlendMode(material, sc?.blendMode ?? "squared");
|
|
374
|
+
const mesh = new THREE.Mesh(geo.geometry, material);
|
|
375
|
+
mesh.frustumCulled = false;
|
|
376
|
+
this.group.add(mesh);
|
|
377
|
+
const palette = new WavePalette((url) => this.createLoopingVideo(url), () => {
|
|
378
|
+
this.updatePaletteTextures();
|
|
379
|
+
this.syncVideoPlayback();
|
|
380
|
+
if (!this.running) this.renderOnce();
|
|
381
|
+
});
|
|
382
|
+
this.waves.push({
|
|
383
|
+
mesh,
|
|
384
|
+
material,
|
|
385
|
+
geometry: geo,
|
|
386
|
+
palette
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Apply config.blendMode to a material. "squared" (the default) is the hero blend:
|
|
391
|
+
* CustomBlending with AddEquation, src = SrcColorFactor, dst = ZeroFactor, so the
|
|
392
|
+
* framebuffer result is fragColor² — the squaring deepens the colours into the vivid
|
|
393
|
+
* hero look (without it the wave reads pastel). "additive"/"normal"/"multiply" are
|
|
394
|
+
* authoring overrides. Multiply uses Three's premultiplied-alpha path; the custom
|
|
395
|
+
* fragment shaders premultiply their output when Three injects PREMULTIPLIED_ALPHA.
|
|
396
|
+
* Returns true if material state changed (caller flags needsUpdate).
|
|
397
|
+
*/
|
|
398
|
+
applyBlendMode(material, mode) {
|
|
399
|
+
const squared = mode === "squared";
|
|
400
|
+
const blending = mode === "additive" ? THREE.AdditiveBlending : mode === "multiply" ? THREE.MultiplyBlending : THREE.NormalBlending;
|
|
401
|
+
const premultipliedAlpha = mode === "multiply" || squared;
|
|
402
|
+
material.uniforms.uSquared.value = squared ? 1 : 0;
|
|
403
|
+
if (material.blending === blending && material.premultipliedAlpha === premultipliedAlpha) return false;
|
|
404
|
+
material.blending = blending;
|
|
405
|
+
material.premultipliedAlpha = premultipliedAlpha;
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
disposeWaves() {
|
|
409
|
+
for (const s of this.waves) {
|
|
410
|
+
this.group.remove(s.mesh);
|
|
411
|
+
s.material.dispose();
|
|
412
|
+
s.geometry.dispose();
|
|
413
|
+
s.palette.dispose();
|
|
414
|
+
}
|
|
415
|
+
this.waves = [];
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Reconcile the wave pool to `waveCount` WITHOUT tearing everything down:
|
|
419
|
+
* keep existing waves (so the compiled shader program is never deleted and
|
|
420
|
+
* re-compiled — that churn can crash some GPU drivers), add/remove only the
|
|
421
|
+
* delta, and resize each geometry to the current quality.
|
|
422
|
+
*/
|
|
423
|
+
buildWaves() {
|
|
424
|
+
const target = Math.max(1, this.config.waves.length);
|
|
425
|
+
while (this.waves.length > target) {
|
|
426
|
+
const s = this.waves.pop();
|
|
427
|
+
if (!s) break;
|
|
428
|
+
this.group.remove(s.mesh);
|
|
429
|
+
s.material.dispose();
|
|
430
|
+
s.geometry.dispose();
|
|
431
|
+
s.palette.dispose();
|
|
432
|
+
}
|
|
433
|
+
while (this.waves.length < target) this.addWave();
|
|
434
|
+
const segments = this.segments;
|
|
435
|
+
this.waves.forEach((s, i) => {
|
|
436
|
+
s.geometry.resize(segments);
|
|
437
|
+
s.mesh.renderOrder = i;
|
|
438
|
+
});
|
|
439
|
+
this.refresh();
|
|
440
|
+
}
|
|
441
|
+
/** Re-read per-frame-independent values from the (mutated) config. */
|
|
442
|
+
refresh() {
|
|
443
|
+
this.applyBackground();
|
|
444
|
+
this.applyPost();
|
|
445
|
+
if (!this.isCameraExternallyDriven()) {
|
|
446
|
+
const p = this.config.cameraPosition;
|
|
447
|
+
const tg = this.config.cameraTarget;
|
|
448
|
+
this.camera.position.set(p.x, p.y, p.z);
|
|
449
|
+
this.camera.lookAt(tg.x, tg.y, tg.z);
|
|
450
|
+
}
|
|
451
|
+
this.waves.forEach((wave, i) => {
|
|
452
|
+
const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
|
|
453
|
+
const u = wave.material.uniforms;
|
|
454
|
+
if (this.applyBlendMode(wave.material, sc.blendMode)) wave.material.needsUpdate = true;
|
|
455
|
+
const wantDefines = this.waveDefines(sc);
|
|
456
|
+
const curDefines = wave.material.defines ?? {};
|
|
457
|
+
if (Object.keys(wantDefines).sort().join(",") !== Object.keys(curDefines).sort().join(",")) {
|
|
458
|
+
wave.material.defines = wantDefines;
|
|
459
|
+
wave.material.needsUpdate = true;
|
|
460
|
+
}
|
|
461
|
+
const wantFrag = sc.theme === "wireframe" ? lineFragmentShader : fragmentShader;
|
|
462
|
+
if (wave.material.fragmentShader !== wantFrag) {
|
|
463
|
+
wave.material.fragmentShader = wantFrag;
|
|
464
|
+
wave.material.needsUpdate = true;
|
|
465
|
+
}
|
|
466
|
+
const stops = [...sc.palette].sort((a, b) => a.pos - b.pos);
|
|
467
|
+
const colorCount = Math.max(1, Math.min(stops.length, 8));
|
|
468
|
+
const colors = u.uColors.value;
|
|
469
|
+
const colorPos = u.uColorPos.value;
|
|
470
|
+
for (let c = 0; c < 8; c++) {
|
|
471
|
+
const stop = stops[Math.min(c, colorCount - 1)] ?? {
|
|
472
|
+
color: "#ffffff",
|
|
473
|
+
pos: 0
|
|
474
|
+
};
|
|
475
|
+
hexToLinearVec3(stop.color, colors[c]);
|
|
476
|
+
colorPos[c] = stop.pos;
|
|
477
|
+
}
|
|
478
|
+
u.uColorCount.value = colorCount;
|
|
479
|
+
u.uGradType.value = sc.gradientType === "radial" ? 1 : sc.gradientType === "conic" ? 2 : sc.gradientType === "mesh" ? 3 : 0;
|
|
480
|
+
u.uGradAngle.value = (sc.gradientAngle ?? 0) * Math.PI / 180;
|
|
481
|
+
u.uGradShift.value = sc.gradientShift ?? 0;
|
|
482
|
+
const meshPoints = sc.meshGradientPoints.slice(0, 8);
|
|
483
|
+
const meshPointPos = u.uMeshPointPos.value;
|
|
484
|
+
const meshPointColor = u.uMeshPointColor.value;
|
|
485
|
+
const meshPointInfluence = u.uMeshPointInfluence.value;
|
|
486
|
+
for (let pointIndex = 0; pointIndex < 8; pointIndex++) {
|
|
487
|
+
const point = meshPoints[pointIndex] ?? meshPoints[meshPoints.length - 1];
|
|
488
|
+
if (!point) continue;
|
|
489
|
+
meshPointPos[pointIndex].set(point.x, point.y);
|
|
490
|
+
hexToLinearVec3(point.color, meshPointColor[pointIndex]);
|
|
491
|
+
meshPointInfluence[pointIndex] = point.influence;
|
|
492
|
+
}
|
|
493
|
+
u.uMeshPointCount.value = meshPoints.length;
|
|
494
|
+
u.uMeshSoftness.value = sc.meshGradientSoftness;
|
|
495
|
+
u.uPaletteScale.value.set(sc.paletteTextureScale?.x ?? 1, sc.paletteTextureScale?.y ?? 1);
|
|
496
|
+
u.uPaletteOffset.value.set(sc.paletteTextureOffset?.x ?? 0, sc.paletteTextureOffset?.y ?? 0);
|
|
497
|
+
u.uPaletteRotation.value = (sc.paletteTextureRotation ?? 0) * Math.PI / 180;
|
|
498
|
+
u.uHueShift.value = sc.hueShift;
|
|
499
|
+
u.uContrast.value = sc.colorContrast;
|
|
500
|
+
u.uSaturation.value = sc.colorSaturation;
|
|
501
|
+
u.uLineAmount.value = sc.lineAmount ?? 425;
|
|
502
|
+
u.uLineThickness.value = sc.lineThickness ?? 1;
|
|
503
|
+
u.uLineDerivativePower.value = sc.lineDerivativePower ?? .95;
|
|
504
|
+
u.uMaxWidth.value = sc.maxWidth ?? 1232;
|
|
505
|
+
hexToLinearVec3(this.config.background, u.uClearColor.value);
|
|
506
|
+
u.uFiberCount.value = sc.fiberCount;
|
|
507
|
+
u.uFiberStrength.value = sc.fiberStrength;
|
|
508
|
+
u.uTexture.value = sc.texture;
|
|
509
|
+
u.uCreaseLight.value = sc.creaseLight;
|
|
510
|
+
u.uCreaseSharpness.value = sc.creaseSharpness;
|
|
511
|
+
u.uCreaseSoftness.value = sc.creaseSoftness;
|
|
512
|
+
u.uSheen.value = sc.sheen ?? 1;
|
|
513
|
+
u.uRoundness.value = sc.roundness ?? .35;
|
|
514
|
+
u.uIridescence.value = sc.iridescence ?? 0;
|
|
515
|
+
u.uDepthTint.value = sc.depthTint ?? 0;
|
|
516
|
+
hexToLinearVec3(sc.depthTintColor ?? "#0a2540", u.uDepthTintColor.value);
|
|
517
|
+
u.uEdgeFade.value = sc.edgeFade;
|
|
518
|
+
u.uEdgeFeather.value = sc.edgeFeather ?? .1;
|
|
519
|
+
const lights = this.config.lights ?? [];
|
|
520
|
+
u.uAmbient.value = this.config.ambient ?? .45;
|
|
521
|
+
u.uNumLights.value = Math.min(lights.length, 8);
|
|
522
|
+
const lPos = u.uLightPos.value;
|
|
523
|
+
const lCol = u.uLightColor.value;
|
|
524
|
+
const lInt = u.uLightIntensity.value;
|
|
525
|
+
for (let li = 0; li < 8; li++) {
|
|
526
|
+
const light = lights[li];
|
|
527
|
+
if (light) {
|
|
528
|
+
lPos[li].set(light.position.x, light.position.y, light.position.z);
|
|
529
|
+
hexToLinearVec3(light.color, lCol[li]);
|
|
530
|
+
lInt[li] = light.intensity;
|
|
531
|
+
} else lInt[li] = 0;
|
|
532
|
+
}
|
|
533
|
+
const bands = sc.noiseBands ?? [];
|
|
534
|
+
u.uNumNoiseBands.value = Math.min(bands.length, 4);
|
|
535
|
+
const bBounds = u.uNoiseBandBounds.value;
|
|
536
|
+
const bParams = u.uNoiseBandParams.value;
|
|
537
|
+
const bPara = u.uNoiseBandParaPow.value;
|
|
538
|
+
for (let bi = 0; bi < 4; bi++) {
|
|
539
|
+
const band = bands[bi];
|
|
540
|
+
if (band) {
|
|
541
|
+
bBounds[bi].set(band.startX, band.endX, band.startY, band.endY);
|
|
542
|
+
bParams[bi].set(band.feather, band.strength, band.frequency, band.colorAttenuation);
|
|
543
|
+
bPara[bi] = band.parabolaPower;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
u.uSpeed.value = sc.speed;
|
|
547
|
+
u.uSeed.value = sc.seed;
|
|
548
|
+
u.uLoopSeconds.value = this.config.loopSeconds ?? 0;
|
|
549
|
+
u.uDispFreqX.value = sc.displaceFrequency.x;
|
|
550
|
+
u.uDispFreqZ.value = sc.displaceFrequency.y;
|
|
551
|
+
u.uDispAmount.value = sc.displaceAmount;
|
|
552
|
+
u.uDetailFreq.value = sc.detailFrequency ?? .04;
|
|
553
|
+
u.uDetailAmount.value = sc.detailAmount ?? 0;
|
|
554
|
+
u.uTwFreqX.value = sc.twistFrequency.x;
|
|
555
|
+
u.uTwFreqY.value = sc.twistFrequency.y;
|
|
556
|
+
u.uTwFreqZ.value = sc.twistFrequency.z;
|
|
557
|
+
u.uTwPowX.value = sc.twistPower.x;
|
|
558
|
+
u.uTwPowY.value = sc.twistPower.y;
|
|
559
|
+
u.uTwPowZ.value = sc.twistPower.z;
|
|
560
|
+
wave.mesh.scale.set(sc.scale.x, sc.scale.y, sc.scale.z);
|
|
561
|
+
wave.mesh.rotation.set(THREE.MathUtils.degToRad(sc.rotation.x), THREE.MathUtils.degToRad(sc.rotation.y), THREE.MathUtils.degToRad(sc.rotation.z));
|
|
562
|
+
wave.mesh.position.set(sc.position.x, sc.position.y, sc.position.z);
|
|
563
|
+
u.uOpacity.value = sc.opacity;
|
|
564
|
+
});
|
|
565
|
+
this.updatePaletteTextures();
|
|
566
|
+
this.syncVideoPlayback();
|
|
567
|
+
this.group.scale.set(this.config.mirrorH ? -1 : 1, this.config.mirrorV ? -1 : 1, 1);
|
|
568
|
+
this.onAfterRefresh();
|
|
569
|
+
if (!this.running) this.renderOnce();
|
|
570
|
+
}
|
|
571
|
+
/** Point every wave's palette sampler at its own WavePalette texture (each rebuilt only
|
|
572
|
+
* when that wave's palette actually changes — see WavePalette.apply). */
|
|
573
|
+
updatePaletteTextures() {
|
|
574
|
+
this.waves.forEach((wave, i) => {
|
|
575
|
+
const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
|
|
576
|
+
wave.palette.apply(sc, wave.material.uniforms);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/** Dev: 0 = normal, 1 = visualise the crease value, 2 = visualise derivative normal. */
|
|
580
|
+
setDebug(v) {
|
|
581
|
+
for (const s of this.waves) s.material.uniforms.uDebug.value = v;
|
|
582
|
+
this.renderOnce();
|
|
583
|
+
}
|
|
584
|
+
/** Rebuild geometry + waves (call when waveCount or quality changes). */
|
|
585
|
+
rebuild() {
|
|
586
|
+
this.buildWaves();
|
|
587
|
+
}
|
|
588
|
+
applyBackground() {
|
|
589
|
+
if (this.config.transparentBackground) {
|
|
590
|
+
this.scene.background = null;
|
|
591
|
+
this.renderer.setClearColor(0, 0);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const matte = new THREE.Color(this.config.background);
|
|
595
|
+
this.renderer.setClearColor(matte, 1);
|
|
596
|
+
if (this.config.backgroundMode === "color") {
|
|
597
|
+
this.applyColorBackground(matte);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const { width, height } = this.backgroundCanvasSize(this.config.backgroundVideoUrl ? 2048 : 4096);
|
|
601
|
+
if (this.config.backgroundMode === "gradient") this.applyGradientBackground(width, height);
|
|
602
|
+
else this.applyImageBackground(matte, width, height);
|
|
603
|
+
}
|
|
604
|
+
applyColorBackground(matte) {
|
|
605
|
+
this.clearBackgroundVideo();
|
|
606
|
+
this.backgroundTexture?.dispose();
|
|
607
|
+
this.backgroundTexture = void 0;
|
|
608
|
+
this.backgroundSig = "";
|
|
609
|
+
this.scene.background = matte;
|
|
610
|
+
}
|
|
611
|
+
applyGradientBackground(width, height) {
|
|
612
|
+
this.clearBackgroundVideo();
|
|
613
|
+
const gradType = this.config.backgroundGradientType;
|
|
614
|
+
if (gradType === "mesh") {
|
|
615
|
+
const pts = this.config.backgroundMeshPoints ?? [];
|
|
616
|
+
const softness = this.config.backgroundMeshSoftness ?? .62;
|
|
617
|
+
const ptSig = pts.map((p) => `${p.color}@${p.x.toFixed(3)},${p.y.toFixed(3)},${p.influence.toFixed(3)}`).join(",");
|
|
618
|
+
const sig = [
|
|
619
|
+
"mesh",
|
|
620
|
+
softness.toFixed(3),
|
|
621
|
+
ptSig,
|
|
622
|
+
width,
|
|
623
|
+
height
|
|
624
|
+
].join("|");
|
|
625
|
+
if (sig !== this.backgroundSig || !this.backgroundTexture) {
|
|
626
|
+
const canvas = buildBackgroundMeshCanvas(pts, softness, width, height);
|
|
627
|
+
this.backgroundTexture?.dispose();
|
|
628
|
+
this.backgroundTexture = canvasToTexture(canvas);
|
|
629
|
+
this.backgroundSig = sig;
|
|
630
|
+
}
|
|
631
|
+
this.scene.background = this.backgroundTexture;
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const source = this.config.backgroundGradientSource ?? "stops";
|
|
635
|
+
const def = PALETTE_MAPS[source];
|
|
636
|
+
const stops = source !== "stops" && def?.kind === "gradient" && def.stops ? def.stops : this.config.backgroundPalette;
|
|
637
|
+
const stopSig = stops.map((stop) => `${stop.color}@${stop.pos.toFixed(3)}`).join(",");
|
|
638
|
+
const sig = [
|
|
639
|
+
"gradient",
|
|
640
|
+
source,
|
|
641
|
+
gradType,
|
|
642
|
+
this.config.backgroundGradientAngle,
|
|
643
|
+
stopSig,
|
|
644
|
+
width,
|
|
645
|
+
height
|
|
646
|
+
].join("|");
|
|
647
|
+
if (sig !== this.backgroundSig || !this.backgroundTexture) {
|
|
648
|
+
const canvas = buildBackgroundGradientCanvas({
|
|
649
|
+
stops,
|
|
650
|
+
type: gradType,
|
|
651
|
+
angle: this.config.backgroundGradientAngle,
|
|
652
|
+
width,
|
|
653
|
+
height
|
|
654
|
+
});
|
|
655
|
+
this.backgroundTexture?.dispose();
|
|
656
|
+
this.backgroundTexture = canvasToTexture(canvas);
|
|
657
|
+
this.backgroundSig = sig;
|
|
658
|
+
}
|
|
659
|
+
this.scene.background = this.backgroundTexture;
|
|
660
|
+
}
|
|
661
|
+
/** Image mode: a live video, a user-loaded image, or a built-in map. `matte` shows while an
|
|
662
|
+
* async source is still loading. */
|
|
663
|
+
applyImageBackground(matte, width, height) {
|
|
664
|
+
const fit = this.config.backgroundImageFit ?? "cover";
|
|
665
|
+
const zoom = this.config.backgroundImageZoom ?? 1;
|
|
666
|
+
const position = this.config.backgroundImagePosition ?? {
|
|
667
|
+
x: 0,
|
|
668
|
+
y: 0
|
|
669
|
+
};
|
|
670
|
+
const videoUrl = this.config.backgroundVideoUrl;
|
|
671
|
+
const customUrl = this.config.backgroundImageUrl;
|
|
672
|
+
let source;
|
|
673
|
+
let sourceWidth;
|
|
674
|
+
let sourceHeight;
|
|
675
|
+
let sourceSig;
|
|
676
|
+
if (videoUrl) {
|
|
677
|
+
this.ensureBackgroundVideo(videoUrl);
|
|
678
|
+
if (!this.backgroundVideo || this.backgroundVideo.readyState < 2) {
|
|
679
|
+
this.scene.background = matte;
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
source = this.backgroundVideo;
|
|
683
|
+
sourceWidth = this.backgroundVideo.videoWidth;
|
|
684
|
+
sourceHeight = this.backgroundVideo.videoHeight;
|
|
685
|
+
sourceSig = `video|${videoUrl}`;
|
|
686
|
+
} else if (customUrl) {
|
|
687
|
+
this.clearBackgroundVideo();
|
|
688
|
+
if (!this.backgroundImage || this.backgroundImageUrl !== customUrl || !this.backgroundImage.complete) {
|
|
689
|
+
this.loadBackgroundImage(customUrl);
|
|
690
|
+
this.scene.background = matte;
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
source = this.backgroundImage;
|
|
694
|
+
sourceWidth = this.backgroundImage.naturalWidth;
|
|
695
|
+
sourceHeight = this.backgroundImage.naturalHeight;
|
|
696
|
+
sourceSig = `custom|${customUrl}`;
|
|
697
|
+
} else {
|
|
698
|
+
this.clearBackgroundVideo();
|
|
699
|
+
const imageSource = this.config.backgroundImageSource ?? "vaporwave";
|
|
700
|
+
const canvas = imageSource === "hero" ? buildHeroPaletteCanvas() : PALETTE_MAPS[imageSource]?.kind === "image" ? paletteMapCanvas(PALETTE_MAPS[imageSource], Math.max(width, height)) : null;
|
|
701
|
+
if (!canvas) {
|
|
702
|
+
this.scene.background = matte;
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
source = canvas;
|
|
706
|
+
sourceWidth = canvas.width;
|
|
707
|
+
sourceHeight = canvas.height;
|
|
708
|
+
sourceSig = `map|${imageSource}`;
|
|
709
|
+
}
|
|
710
|
+
const sig = [
|
|
711
|
+
"image",
|
|
712
|
+
sourceSig,
|
|
713
|
+
fit,
|
|
714
|
+
zoom,
|
|
715
|
+
position.x,
|
|
716
|
+
position.y,
|
|
717
|
+
this.config.background,
|
|
718
|
+
width,
|
|
719
|
+
height
|
|
720
|
+
].join("|");
|
|
721
|
+
if (sig !== this.backgroundSig || !this.backgroundTexture) {
|
|
722
|
+
const canvas = buildBackgroundImageCanvas(source, sourceWidth, sourceHeight, width, height, fit, this.config.background, zoom, position.x, position.y);
|
|
723
|
+
this.backgroundTexture?.dispose();
|
|
724
|
+
this.backgroundTexture = canvasToTexture(canvas);
|
|
725
|
+
this.backgroundSig = sig;
|
|
726
|
+
this.backgroundVideoCanvas = videoUrl ? canvas : void 0;
|
|
727
|
+
}
|
|
728
|
+
this.scene.background = this.backgroundTexture;
|
|
729
|
+
}
|
|
730
|
+
backgroundCanvasSize(maxRequestedEdge = 4096) {
|
|
731
|
+
const rawWidth = this.outputSize?.width ?? Math.max(1, this.container.clientWidth);
|
|
732
|
+
const rawHeight = this.outputSize?.height ?? Math.max(1, this.container.clientHeight);
|
|
733
|
+
const maxEdge = Math.min(maxRequestedEdge, this.renderer.capabilities.maxTextureSize);
|
|
734
|
+
const scale = Math.min(1, maxEdge / Math.max(rawWidth, rawHeight));
|
|
735
|
+
return {
|
|
736
|
+
width: Math.max(1, Math.round(rawWidth * scale)),
|
|
737
|
+
height: Math.max(1, Math.round(rawHeight * scale))
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
loadBackgroundImage(url) {
|
|
741
|
+
if (this.backgroundImageUrl === url || this.failedBackgroundImageUrl === url) return;
|
|
742
|
+
this.backgroundImageUrl = url;
|
|
743
|
+
this.backgroundImage = void 0;
|
|
744
|
+
const image = new Image();
|
|
745
|
+
image.decoding = "async";
|
|
746
|
+
if (!url.startsWith("data:") && !url.startsWith("blob:")) image.crossOrigin = "anonymous";
|
|
747
|
+
image.addEventListener("load", () => {
|
|
748
|
+
if (this.config.backgroundImageUrl !== url) return;
|
|
749
|
+
this.backgroundImage = image;
|
|
750
|
+
this.failedBackgroundImageUrl = "";
|
|
751
|
+
this.backgroundSig = "";
|
|
752
|
+
this.applyBackground();
|
|
753
|
+
if (!this.running) this.renderOnce();
|
|
754
|
+
}, { once: true });
|
|
755
|
+
image.addEventListener("error", () => {
|
|
756
|
+
if (this.config.backgroundImageUrl !== url) return;
|
|
757
|
+
this.failedBackgroundImageUrl = url;
|
|
758
|
+
this.backgroundSig = "";
|
|
759
|
+
this.applyBackground();
|
|
760
|
+
if (!this.running) this.renderOnce();
|
|
761
|
+
}, { once: true });
|
|
762
|
+
image.src = url;
|
|
763
|
+
}
|
|
764
|
+
ensureBackgroundVideo(url) {
|
|
765
|
+
if (this.backgroundVideoUrl === url || this.failedBackgroundVideoUrl === url) return;
|
|
766
|
+
this.clearBackgroundVideo();
|
|
767
|
+
this.backgroundVideoUrl = url;
|
|
768
|
+
const video = this.createLoopingVideo(url);
|
|
769
|
+
video.addEventListener("loadeddata", () => {
|
|
770
|
+
if (this.config.backgroundVideoUrl !== url) return;
|
|
771
|
+
this.backgroundVideo = video;
|
|
772
|
+
this.failedBackgroundVideoUrl = "";
|
|
773
|
+
this.backgroundSig = "";
|
|
774
|
+
this.applyBackground();
|
|
775
|
+
this.syncVideoPlayback();
|
|
776
|
+
if (!this.running) this.renderOnce();
|
|
777
|
+
}, { once: true });
|
|
778
|
+
video.addEventListener("error", () => {
|
|
779
|
+
if (this.config.backgroundVideoUrl !== url) return;
|
|
780
|
+
this.failedBackgroundVideoUrl = url;
|
|
781
|
+
this.backgroundSig = "";
|
|
782
|
+
this.applyBackground();
|
|
783
|
+
if (!this.running) this.renderOnce();
|
|
784
|
+
}, { once: true });
|
|
785
|
+
this.backgroundVideo = video;
|
|
786
|
+
video.load();
|
|
787
|
+
}
|
|
788
|
+
clearBackgroundVideo() {
|
|
789
|
+
if (this.backgroundVideo) {
|
|
790
|
+
this.backgroundVideo.pause();
|
|
791
|
+
this.backgroundVideo.removeAttribute("src");
|
|
792
|
+
this.backgroundVideo.load();
|
|
793
|
+
}
|
|
794
|
+
this.backgroundVideo = void 0;
|
|
795
|
+
this.backgroundVideoUrl = "";
|
|
796
|
+
this.failedBackgroundVideoUrl = "";
|
|
797
|
+
this.backgroundVideoCanvas = void 0;
|
|
798
|
+
}
|
|
799
|
+
createLoopingVideo(url) {
|
|
800
|
+
const video = document.createElement("video");
|
|
801
|
+
video.muted = true;
|
|
802
|
+
video.defaultMuted = true;
|
|
803
|
+
video.loop = true;
|
|
804
|
+
video.playsInline = true;
|
|
805
|
+
video.preload = "auto";
|
|
806
|
+
if (!url.startsWith("data:") && !url.startsWith("blob:")) video.crossOrigin = "anonymous";
|
|
807
|
+
video.src = url;
|
|
808
|
+
return video;
|
|
809
|
+
}
|
|
810
|
+
syncVideoPlayback() {
|
|
811
|
+
const backgroundActive = this.running && !this.config.transparentBackground && this.config.backgroundMode === "image" && !!this.config.backgroundVideoUrl;
|
|
812
|
+
if (this.backgroundVideo) if (backgroundActive) this.backgroundVideo.play().catch(() => {});
|
|
813
|
+
else this.backgroundVideo.pause();
|
|
814
|
+
this.waves.forEach((wave, i) => {
|
|
815
|
+
const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
|
|
816
|
+
wave.palette.syncPlayback(sc, this.running);
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
updateBackgroundVideoFrame() {
|
|
820
|
+
const video = this.backgroundVideo;
|
|
821
|
+
const canvas = this.backgroundVideoCanvas;
|
|
822
|
+
if (!video || !canvas || video.readyState < 2 || !this.backgroundTexture) return;
|
|
823
|
+
const position = this.config.backgroundImagePosition ?? {
|
|
824
|
+
x: 0,
|
|
825
|
+
y: 0
|
|
826
|
+
};
|
|
827
|
+
drawBackgroundMediaFrame(canvas, video, video.videoWidth, video.videoHeight, this.config.backgroundImageFit ?? "cover", this.config.background, this.config.backgroundImageZoom ?? 1, position.x, position.y);
|
|
828
|
+
this.backgroundTexture.needsUpdate = true;
|
|
829
|
+
}
|
|
830
|
+
applyPost() {
|
|
831
|
+
const u = this.postPass.uniforms;
|
|
832
|
+
u.uBlurAmount.value = this.config.blur;
|
|
833
|
+
u.uGrainAmount.value = this.config.grain;
|
|
834
|
+
u.uBlurSamples.value = Math.round(this.config.blurSamples ?? 6);
|
|
835
|
+
this.applyBloom();
|
|
836
|
+
}
|
|
837
|
+
/** Insert / tune / remove the bloom pass. strength 0 removes it from the composer entirely, so
|
|
838
|
+
* cost and pixels are identical to bloom-off; the pass (and its mip-chain render targets) is
|
|
839
|
+
* created lazily the first time bloom is enabled and disposed when turned back off. It sits
|
|
840
|
+
* right after the scene RenderPass so it blooms the wave before the grain/blur pass. */
|
|
841
|
+
applyBloom() {
|
|
842
|
+
const strength = this.config.bloomStrength ?? 0;
|
|
843
|
+
if (strength > 0) {
|
|
844
|
+
if (!this.bloomPass) {
|
|
845
|
+
const size = this.renderer.getDrawingBufferSize(new THREE.Vector2());
|
|
846
|
+
this.bloomPass = new UnrealBloomPass(size, strength, this.config.bloomRadius ?? .4, this.config.bloomThreshold ?? .85);
|
|
847
|
+
this.composer.insertPass(this.bloomPass, 1);
|
|
848
|
+
}
|
|
849
|
+
this.bloomPass.strength = strength;
|
|
850
|
+
this.bloomPass.radius = this.config.bloomRadius ?? .4;
|
|
851
|
+
this.bloomPass.threshold = this.config.bloomThreshold ?? .85;
|
|
852
|
+
} else if (this.bloomPass) {
|
|
853
|
+
this.composer.removePass(this.bloomPass);
|
|
854
|
+
this.bloomPass.dispose();
|
|
855
|
+
this.bloomPass = void 0;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
onResize = () => {
|
|
859
|
+
this.resize();
|
|
860
|
+
};
|
|
861
|
+
onContextLost = (e) => {
|
|
862
|
+
e.preventDefault();
|
|
863
|
+
cancelAnimationFrame(this.rafId);
|
|
864
|
+
this.running = false;
|
|
865
|
+
};
|
|
866
|
+
onContextRestored = () => {
|
|
867
|
+
this.disposeWaves();
|
|
868
|
+
this.backgroundTexture?.dispose();
|
|
869
|
+
this.backgroundTexture = void 0;
|
|
870
|
+
this.backgroundSig = "";
|
|
871
|
+
this.buildWaves();
|
|
872
|
+
this.resize();
|
|
873
|
+
this.updateRunning();
|
|
874
|
+
};
|
|
875
|
+
resize() {
|
|
876
|
+
const w = this.outputSize?.width ?? Math.max(1, this.container.clientWidth);
|
|
877
|
+
const h = this.outputSize?.height ?? Math.max(1, this.container.clientHeight);
|
|
878
|
+
const dpr = this.outputSize ? 1 : Math.min(window.devicePixelRatio || 1, this.config.dprMax);
|
|
879
|
+
this.renderer.setPixelRatio(dpr);
|
|
880
|
+
this.renderer.setSize(w, h, !this.outputSize);
|
|
881
|
+
if (this.outputSize) {
|
|
882
|
+
this.renderer.domElement.style.width = "100%";
|
|
883
|
+
this.renderer.domElement.style.height = "100%";
|
|
884
|
+
}
|
|
885
|
+
this.composer.setPixelRatio(dpr);
|
|
886
|
+
this.composer.setSize(w, h);
|
|
887
|
+
const dw = w * dpr;
|
|
888
|
+
const dh = h * dpr;
|
|
889
|
+
this.postPass.uniforms.uResolution.value.set(dw, dh);
|
|
890
|
+
for (const s of this.waves) s.material.uniforms.uResolution.value.set(dw, dh);
|
|
891
|
+
this.camera.left = -dw / 2;
|
|
892
|
+
this.camera.right = dw / 2;
|
|
893
|
+
this.camera.top = dh / 2;
|
|
894
|
+
this.camera.bottom = -dh / 2;
|
|
895
|
+
this.applyZoom();
|
|
896
|
+
this.applyBackground();
|
|
897
|
+
this.onAfterResize();
|
|
898
|
+
if (!this.running) this.renderOnce();
|
|
899
|
+
}
|
|
900
|
+
/** Set an exact output buffer while CSS scales the canvas into the on-screen export frame. */
|
|
901
|
+
setOutputSize(width, height) {
|
|
902
|
+
this.outputSize = {
|
|
903
|
+
width: THREE.MathUtils.clamp(Math.round(width), 64, 8192),
|
|
904
|
+
height: THREE.MathUtils.clamp(Math.round(height), 64, 8192)
|
|
905
|
+
};
|
|
906
|
+
this.resize();
|
|
907
|
+
}
|
|
908
|
+
start() {
|
|
909
|
+
this.started = true;
|
|
910
|
+
this.updateRunning();
|
|
911
|
+
}
|
|
912
|
+
stop() {
|
|
913
|
+
this.started = false;
|
|
914
|
+
this.updateRunning();
|
|
915
|
+
}
|
|
916
|
+
onMotionChange = (e) => {
|
|
917
|
+
this.reducedMotion = this.respectReducedMotion && e.matches;
|
|
918
|
+
this.updateRunning();
|
|
919
|
+
};
|
|
920
|
+
onVisibilityChange = () => {
|
|
921
|
+
this.pageVisible = document.visibilityState === "visible";
|
|
922
|
+
this.updateRunning();
|
|
923
|
+
};
|
|
924
|
+
updateRunning() {
|
|
925
|
+
const shouldAnimate = this.started && this.visible && this.pageVisible && !this.config.paused && !this.reducedMotion;
|
|
926
|
+
if (shouldAnimate && !this.running) {
|
|
927
|
+
this.running = true;
|
|
928
|
+
this.clock.start();
|
|
929
|
+
this.clock.getDelta();
|
|
930
|
+
this.rafId = requestAnimationFrame(this.loop);
|
|
931
|
+
} else if (!shouldAnimate && this.running) {
|
|
932
|
+
this.running = false;
|
|
933
|
+
cancelAnimationFrame(this.rafId);
|
|
934
|
+
}
|
|
935
|
+
if (!this.running) {
|
|
936
|
+
this.introTimeRamp = 1;
|
|
937
|
+
this.renderOnce();
|
|
938
|
+
}
|
|
939
|
+
this.syncVideoPlayback();
|
|
940
|
+
}
|
|
941
|
+
loop = () => {
|
|
942
|
+
if (!this.running) return;
|
|
943
|
+
this.time += this.clock.getDelta();
|
|
944
|
+
if (this.introTimeRamp < 1) this.introTimeRamp = Math.min(1, this.introTimeRamp + .016);
|
|
945
|
+
this.renderOnce();
|
|
946
|
+
this.rafId = requestAnimationFrame(this.loop);
|
|
947
|
+
};
|
|
948
|
+
/** Advance the per-frame clock uniforms (geometry itself is static). Time model:
|
|
949
|
+
* time = elapsed·introTimeRamp + timeOffset — the ramp eases the animation in on load. */
|
|
950
|
+
updateTime() {
|
|
951
|
+
const ramp = this.config.introRamp === false || this.skipIntroRamp ? 1 : this.introTimeRamp;
|
|
952
|
+
const t = this.time * ramp + (this.config.timeOffset ?? 0);
|
|
953
|
+
for (let i = 0; i < this.waves.length; i++) {
|
|
954
|
+
const u = this.waves[i].material.uniforms;
|
|
955
|
+
u.uTime.value = t;
|
|
956
|
+
const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
|
|
957
|
+
const dx = sc.paletteDriftX ?? 0;
|
|
958
|
+
const dy = sc.paletteDriftY ?? 0;
|
|
959
|
+
if (dx !== 0 || dy !== 0) {
|
|
960
|
+
const base = sc.paletteTextureOffset;
|
|
961
|
+
u.uPaletteOffset.value.set(base.x + dx * t, base.y + dy * t);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
this.postPass.uniforms.uTime.value = t;
|
|
965
|
+
}
|
|
966
|
+
/** Render exactly one frame at the current time. */
|
|
967
|
+
renderOnce() {
|
|
968
|
+
this.updateBackgroundVideoFrame();
|
|
969
|
+
this.updateTime();
|
|
970
|
+
this.updateClipPlanes();
|
|
971
|
+
this.composer.render();
|
|
972
|
+
this.onAfterRenderFrame();
|
|
973
|
+
}
|
|
974
|
+
/** Re-evaluate play/pause after `config.paused` changes. */
|
|
975
|
+
refreshPlayback() {
|
|
976
|
+
this.updateRunning();
|
|
977
|
+
}
|
|
978
|
+
/** Jump the camera to the config's authored framing (cameraPosition / cameraTarget /
|
|
979
|
+
* cameraZoom). Called on whole-config swaps (preset / reset / randomize / import). The base
|
|
980
|
+
* applies the pose directly; the studio subclass overrides it to also drive the orbit target
|
|
981
|
+
* and sync the panel. Hook ⑤. */
|
|
982
|
+
applyCameraFromConfig() {
|
|
983
|
+
const p = this.config.cameraPosition;
|
|
984
|
+
const tg = this.config.cameraTarget;
|
|
985
|
+
this.camera.position.set(p.x, p.y, p.z);
|
|
986
|
+
this.camera.up.set(0, 1, 0);
|
|
987
|
+
this.camera.lookAt(tg.x, tg.y, tg.z);
|
|
988
|
+
this.applyZoom();
|
|
989
|
+
if (!this.running) this.renderOnce();
|
|
990
|
+
}
|
|
991
|
+
/** Hook ①: true while orbit or an edit gizmo owns the camera, so refresh() won't reset it. */
|
|
992
|
+
isCameraExternallyDriven() {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
/** Hook ②: called at the end of refresh(), before the trailing renderOnce(). */
|
|
996
|
+
onAfterRefresh() {}
|
|
997
|
+
/** Hook ③: called at the end of renderOnce(), after the composed frame is drawn. */
|
|
998
|
+
onAfterRenderFrame() {}
|
|
999
|
+
/** Hook ④: called at the end of resize(), before the trailing renderOnce(). */
|
|
1000
|
+
onAfterResize() {}
|
|
1001
|
+
/** Responsive ortho zoom: COVER the FRAME_W × FRAME_H reference frame onto the canvas so the
|
|
1002
|
+
* wave frames the same at any size/aspect/dpr (only the cropped margin differs), times the
|
|
1003
|
+
* user's cameraZoom. `max(...)` = cover (fill both axes, crop overflow); `min(...)` would be
|
|
1004
|
+
* contain (fit with letterbox bands). Cover keeps the wave filling the frame on every screen. */
|
|
1005
|
+
applyZoom() {
|
|
1006
|
+
const dw = this.camera.right - this.camera.left;
|
|
1007
|
+
const dh = this.camera.top - this.camera.bottom;
|
|
1008
|
+
this.camera.zoom = Math.max(dw / FRAME_W, dh / 750) * (this.config.cameraZoom ?? 1);
|
|
1009
|
+
this.camera.updateProjectionMatrix();
|
|
1010
|
+
}
|
|
1011
|
+
/** Fit the orthographic near/far planes to the scene before every render, so no part of a wave
|
|
1012
|
+
* is ever clipped as the camera orbits / dollies / pans (or when waves are added or scaled).
|
|
1013
|
+
*
|
|
1014
|
+
* The camera is *constructed* with fixed 1..10000 planes that only suit the authored hero
|
|
1015
|
+
* framing; once the view moves, the wave's depth extent along the view axis easily crosses
|
|
1016
|
+
* them and the GPU hard-slices the geometry along a flat plane — chunks of the wave vanish.
|
|
1017
|
+
*
|
|
1018
|
+
* The in-shader twist rotates each vertex about its LOCAL origin, so it can never move a vertex
|
|
1019
|
+
* further from that origin than the base geometry already sits. A sphere at each mesh's origin
|
|
1020
|
+
* (radius = base extent × the mesh's largest world scale, plus the Y displacement, ×1.2 slack)
|
|
1021
|
+
* therefore safely contains the fully-deformed wave. We union those, then bracket the union
|
|
1022
|
+
* along the view axis with a margin. Bracketing to the scene (rather than a fixed huge slab)
|
|
1023
|
+
* keeps clip-space depth scene-normalised — so the wireframe theme's depth fade, which reads
|
|
1024
|
+
* gl_Position.z, looks the same at any camera distance instead of washing out. Runs each frame;
|
|
1025
|
+
* it's a handful of vector ops over 1–8 meshes with no allocation. */
|
|
1026
|
+
updateClipPlanes() {
|
|
1027
|
+
this.clipBox.makeEmpty();
|
|
1028
|
+
for (const wave of this.waves) {
|
|
1029
|
+
const mesh = wave.mesh;
|
|
1030
|
+
mesh.updateWorldMatrix(true, false);
|
|
1031
|
+
const bs = mesh.geometry.boundingSphere;
|
|
1032
|
+
if (!bs) continue;
|
|
1033
|
+
const center = this.clipTmpA.setFromMatrixPosition(mesh.matrixWorld);
|
|
1034
|
+
const disp = Math.abs(Number(wave.material.uniforms.uDispAmount.value) || 0);
|
|
1035
|
+
const radius = (bs.center.length() + bs.radius + disp) * mesh.matrixWorld.getMaxScaleOnAxis() * 1.2;
|
|
1036
|
+
this.clipBox.expandByPoint(this.clipTmpB.copy(center).addScalar(radius));
|
|
1037
|
+
this.clipBox.expandByPoint(this.clipTmpB.copy(center).addScalar(-radius));
|
|
1038
|
+
}
|
|
1039
|
+
if (this.clipBox.isEmpty()) return;
|
|
1040
|
+
this.clipBox.getBoundingSphere(this.clipSphere);
|
|
1041
|
+
const viewDir = this.camera.getWorldDirection(this.clipTmpA);
|
|
1042
|
+
const centerDepth = this.clipTmpB.copy(this.clipSphere.center).sub(this.camera.position).dot(viewDir);
|
|
1043
|
+
const radius = this.clipSphere.radius;
|
|
1044
|
+
const margin = radius * .25 + 10;
|
|
1045
|
+
this.camera.near = centerDepth - radius - margin;
|
|
1046
|
+
this.camera.far = centerDepth + radius + margin;
|
|
1047
|
+
this.camera.updateProjectionMatrix();
|
|
1048
|
+
}
|
|
1049
|
+
/** A world-space position delta that drops a duplicated wave into open frame space beside the
|
|
1050
|
+
* one it was copied from — screen-left and a touch down, sized to the visible frame — instead
|
|
1051
|
+
* of hidden exactly on top of it or (for the hero, which fills the right of the frame) pushed
|
|
1052
|
+
* off-frame. Camera-relative: it's "left on screen" no matter how the view is rotated/zoomed,
|
|
1053
|
+
* because it's built from the camera's right/up axes and the frame's visible world size. */
|
|
1054
|
+
async captureImage(mime, transparent = true, quality) {
|
|
1055
|
+
const prev = this.config.transparentBackground;
|
|
1056
|
+
if (transparent !== prev) {
|
|
1057
|
+
this.config.transparentBackground = transparent;
|
|
1058
|
+
this.applyBackground();
|
|
1059
|
+
}
|
|
1060
|
+
let blob = null;
|
|
1061
|
+
try {
|
|
1062
|
+
this.capturing = true;
|
|
1063
|
+
this.renderOnce();
|
|
1064
|
+
blob = await new Promise((resolve) => this.canvas.toBlob(resolve, mime, quality));
|
|
1065
|
+
} finally {
|
|
1066
|
+
this.capturing = false;
|
|
1067
|
+
if (transparent !== prev) {
|
|
1068
|
+
this.config.transparentBackground = prev;
|
|
1069
|
+
this.applyBackground();
|
|
1070
|
+
}
|
|
1071
|
+
this.renderOnce();
|
|
1072
|
+
}
|
|
1073
|
+
if (!blob || blob.type !== mime) throw new Error(`Failed to capture ${mime}`);
|
|
1074
|
+
return blob;
|
|
1075
|
+
}
|
|
1076
|
+
captureStream(fps = 60) {
|
|
1077
|
+
return this.canvas.captureStream(fps);
|
|
1078
|
+
}
|
|
1079
|
+
get canvas() {
|
|
1080
|
+
return this.renderer.domElement;
|
|
1081
|
+
}
|
|
1082
|
+
getConfig() {
|
|
1083
|
+
return this.config;
|
|
1084
|
+
}
|
|
1085
|
+
setConfig(config) {
|
|
1086
|
+
const next = ensureStudioConfig(config);
|
|
1087
|
+
const structural = next.waves.length !== this.waves.length || next.quality !== this.config.quality;
|
|
1088
|
+
this.config = next;
|
|
1089
|
+
if (structural) this.rebuild();
|
|
1090
|
+
else this.refresh();
|
|
1091
|
+
this.applyCameraFromConfig();
|
|
1092
|
+
}
|
|
1093
|
+
dispose() {
|
|
1094
|
+
cancelAnimationFrame(this.rafId);
|
|
1095
|
+
this.running = false;
|
|
1096
|
+
this.resizeObserver.disconnect();
|
|
1097
|
+
this.intersectionObserver.disconnect();
|
|
1098
|
+
this.motionQuery.removeEventListener("change", this.onMotionChange);
|
|
1099
|
+
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
1100
|
+
this.renderer.domElement.removeEventListener("webglcontextlost", this.onContextLost);
|
|
1101
|
+
this.renderer.domElement.removeEventListener("webglcontextrestored", this.onContextRestored);
|
|
1102
|
+
this.clearBackgroundVideo();
|
|
1103
|
+
this.backgroundTexture?.dispose();
|
|
1104
|
+
for (const s of this.waves) {
|
|
1105
|
+
s.material.dispose();
|
|
1106
|
+
s.geometry.dispose();
|
|
1107
|
+
s.palette.dispose();
|
|
1108
|
+
}
|
|
1109
|
+
this.bloomPass?.dispose();
|
|
1110
|
+
this.composer.dispose();
|
|
1111
|
+
this.renderer.dispose();
|
|
1112
|
+
this.renderer.domElement.remove();
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
//#endregion
|
|
1116
|
+
export { FRAME_H, FRAME_W, WaveRenderer, hexToLinearVec3 };
|
|
1117
|
+
|
|
1118
|
+
//# sourceMappingURL=WaveRenderer.js.map
|