@wave3d/core 0.2.2 → 0.4.1
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 +47 -0
- package/dist/config/model.d.ts +138 -1
- package/dist/config/model.js +144 -1
- package/dist/config/model.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/presets.js +15 -0
- package/dist/presets.js.map +1 -1
- package/dist/renderer/WaveRenderer.d.ts +59 -0
- package/dist/renderer/WaveRenderer.js +422 -8
- package/dist/renderer/WaveRenderer.js.map +1 -1
- package/dist/renderer/interaction.d.ts +119 -0
- package/dist/renderer/interaction.js +474 -0
- package/dist/renderer/interaction.js.map +1 -0
- package/dist/renderer/palette.js +14 -0
- package/dist/renderer/palette.js.map +1 -1
- package/dist/renderer/shaders.js +313 -1
- package/dist/renderer/shaders.js.map +1 -1
- package/dist/shell/createWave.d.ts +9 -1
- package/dist/shell/createWave.js +7 -1
- package/dist/shell/createWave.js.map +1 -1
- package/dist/shell/poster.d.ts +12 -0
- package/dist/shell/poster.js +4 -3
- package/dist/shell/poster.js.map +1 -1
- package/dist/standalone/wave3d.standalone.js +1016 -223
- package/dist/standalone.d.ts +2 -2
- package/dist/standalone.js +2 -2
- package/dist/studio/StudioWaveRenderer.d.ts +15 -0
- package/dist/studio/StudioWaveRenderer.js +32 -1
- package/dist/studio/StudioWaveRenderer.js.map +1 -1
- package/dist/studio/index.d.ts +2 -2
- package/dist/studio/index.js +2 -2
- package/dist/studio/randomize.d.ts +7 -4
- package/dist/studio/randomize.js +52 -5
- package/dist/studio/randomize.js.map +1 -1
- package/package.json +1 -1
- package/skills/wave3d/SKILL.md +60 -6
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ensureStudioConfig } from "../config/model.js";
|
|
2
|
-
import { fragmentShader, lineFragmentShader, postFragmentShader, postVertexShader, vertexShader } from "./shaders.js";
|
|
2
|
+
import { ditherFragmentShader, fragmentShader, halftoneCmykFragmentShader, halftoneFragmentShader, heatmapFragmentShader, innerLightFragmentShader, lineFragmentShader, paperTextureFragmentShader, postFragmentShader, postVertexShader, vertexShader } from "./shaders.js";
|
|
3
3
|
import { WaveGeometry } from "./WaveGeometry.js";
|
|
4
|
+
import { InteractionController, SCENE_APPLIERS, WAVE_APPLIERS, anyPointerFxActive, interactionActive, wavePointerFxActive, waveRipplesActive } from "./interaction.js";
|
|
4
5
|
import { PALETTE_MAPS, buildBackgroundGradientCanvas, buildBackgroundImageCanvas, buildBackgroundMeshCanvas, buildPaletteTexture, canvasToTexture, configurePaletteTexture, drawBackgroundMediaFrame, loadPaletteImage, paletteMapCanvas, paletteSignature } from "./palette.js";
|
|
5
6
|
import { buildHeroPaletteCanvas, buildHeroPaletteTexture } from "./heroPalette.js";
|
|
6
7
|
import * as THREE from "three";
|
|
@@ -148,6 +149,12 @@ var WaveRenderer = class {
|
|
|
148
149
|
postPass;
|
|
149
150
|
/** Optional bloom pass — created lazily when bloomStrength first goes >0, removed at 0. */
|
|
150
151
|
bloomPass;
|
|
152
|
+
ditherPass;
|
|
153
|
+
innerLightPass;
|
|
154
|
+
halftonePass;
|
|
155
|
+
heatmapPass;
|
|
156
|
+
paperTexturePass;
|
|
157
|
+
halftoneCmykPass;
|
|
151
158
|
container;
|
|
152
159
|
respectReducedMotion;
|
|
153
160
|
skipIntroRamp;
|
|
@@ -169,6 +176,18 @@ var WaveRenderer = class {
|
|
|
169
176
|
clipSphere = new THREE.Sphere();
|
|
170
177
|
clipTmpA = new THREE.Vector3();
|
|
171
178
|
clipTmpB = new THREE.Vector3();
|
|
179
|
+
/** Created by syncInteraction() when interaction turns on, disposed when it turns off. */
|
|
180
|
+
interaction;
|
|
181
|
+
/** Extra ortho-zoom MULTIPLIER from a cameraZoom binding (1 = none); applied in applyZoom().
|
|
182
|
+
* Protected so the studio's writeCameraToConfig() can divide it back out (keep it out of config). */
|
|
183
|
+
interactionZoom = 1;
|
|
184
|
+
/** Extra time-offset DELTA from a timeOffset binding (0 = none); added in updateTime(). */
|
|
185
|
+
interactionTimeOffset = 0;
|
|
186
|
+
/** Scene-binding out-params: appliers write into this, applyBindings() reads it back. */
|
|
187
|
+
interactionSceneOut = {
|
|
188
|
+
timeOffset: 0,
|
|
189
|
+
zoom: 1
|
|
190
|
+
};
|
|
172
191
|
timer = new THREE.Timer();
|
|
173
192
|
time = 0;
|
|
174
193
|
rafId = 0;
|
|
@@ -276,6 +295,14 @@ var WaveRenderer = class {
|
|
|
276
295
|
bandParams.push(new THREE.Vector4());
|
|
277
296
|
bandParaPow.push(0);
|
|
278
297
|
}
|
|
298
|
+
const rippleOrigin = [];
|
|
299
|
+
const rippleAge = [];
|
|
300
|
+
const rippleAmp = [];
|
|
301
|
+
for (let i = 0; i < 4; i++) {
|
|
302
|
+
rippleOrigin.push(new THREE.Vector2());
|
|
303
|
+
rippleAge.push(0);
|
|
304
|
+
rippleAmp.push(0);
|
|
305
|
+
}
|
|
279
306
|
return {
|
|
280
307
|
uTime: { value: 0 },
|
|
281
308
|
uSpeed: { value: .05 },
|
|
@@ -342,7 +369,23 @@ var WaveRenderer = class {
|
|
|
342
369
|
uLineThickness: { value: 1 },
|
|
343
370
|
uLineDerivativePower: { value: .95 },
|
|
344
371
|
uMaxWidth: { value: 1232 },
|
|
345
|
-
uClearColor: { value: new THREE.Vector3(1, 1, 1) }
|
|
372
|
+
uClearColor: { value: new THREE.Vector3(1, 1, 1) },
|
|
373
|
+
uPointer: { value: new THREE.Vector2(0, 0) },
|
|
374
|
+
uPointerActive: { value: 0 },
|
|
375
|
+
uPointerRadius: { value: .6 },
|
|
376
|
+
uPointerAspect: { value: 1 },
|
|
377
|
+
uPointerAgitate: { value: 0 },
|
|
378
|
+
uPointerPush: { value: 0 },
|
|
379
|
+
uPointerWake: { value: 0 },
|
|
380
|
+
uPointerVel: { value: new THREE.Vector2(0, 0) },
|
|
381
|
+
uShapeFlow: { value: 0 },
|
|
382
|
+
uPointerThin: { value: 0 },
|
|
383
|
+
uPointerHue: { value: 0 },
|
|
384
|
+
uPointerLighten: { value: 0 },
|
|
385
|
+
uPointerRipple: { value: 0 },
|
|
386
|
+
uRippleOrigin: { value: rippleOrigin },
|
|
387
|
+
uRippleAge: { value: rippleAge },
|
|
388
|
+
uRippleAmp: { value: rippleAmp }
|
|
346
389
|
};
|
|
347
390
|
}
|
|
348
391
|
/** Vertex-shader #defines for a wave: TWIST_MOTION (per-wave animated twist wobble) and
|
|
@@ -352,9 +395,14 @@ var WaveRenderer = class {
|
|
|
352
395
|
const defines = {};
|
|
353
396
|
if (sc?.twistMotion) defines.TWIST_MOTION = "";
|
|
354
397
|
if ((this.config.loopSeconds ?? 0) > 0) defines.LOOP_MOTION = "";
|
|
355
|
-
|
|
398
|
+
const bindsDetail = sc?.interaction?.bindings?.some((b) => b.target === "detailAmount") ?? false;
|
|
399
|
+
if ((sc?.detailAmount ?? 0) !== 0 || bindsDetail) defines.DETAIL_OCTAVE = "";
|
|
356
400
|
if ((sc?.depthTint ?? 0) > 0) defines.DEPTH_TINT = "";
|
|
357
401
|
if ((sc?.edgeFeather ?? .1) !== .1) defines.EDGE_FEATHER = "";
|
|
402
|
+
if (sc && wavePointerFxActive(this.config, sc)) {
|
|
403
|
+
defines.POINTER_FX = "";
|
|
404
|
+
if (waveRipplesActive(this.config, sc)) defines.POINTER_RIPPLES = "";
|
|
405
|
+
}
|
|
358
406
|
return defines;
|
|
359
407
|
}
|
|
360
408
|
addWave() {
|
|
@@ -442,6 +490,7 @@ var WaveRenderer = class {
|
|
|
442
490
|
refresh() {
|
|
443
491
|
this.applyBackground();
|
|
444
492
|
this.applyPost();
|
|
493
|
+
this.syncInteraction();
|
|
445
494
|
if (!this.isCameraExternallyDriven()) {
|
|
446
495
|
const p = this.config.cameraPosition;
|
|
447
496
|
const tg = this.config.cameraTarget;
|
|
@@ -562,6 +611,23 @@ var WaveRenderer = class {
|
|
|
562
611
|
wave.mesh.position.set(sc.position.x, sc.position.y, sc.position.z);
|
|
563
612
|
u.uOpacity.value = sc.opacity;
|
|
564
613
|
});
|
|
614
|
+
const sharedRadius = (this.config.interaction?.radius ?? .3) * 2;
|
|
615
|
+
const sharedFlow = this.config.interaction?.ribbonFlow ?? .8;
|
|
616
|
+
this.waves.forEach((wave, i) => {
|
|
617
|
+
const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
|
|
618
|
+
if (!wavePointerFxActive(this.config, sc)) return;
|
|
619
|
+
const u = wave.material.uniforms;
|
|
620
|
+
const h = sc.interaction?.hover;
|
|
621
|
+
u.uPointerRadius.value = sharedRadius;
|
|
622
|
+
u.uPointerAgitate.value = h?.agitate ?? 0;
|
|
623
|
+
u.uPointerPush.value = h?.push ?? 0;
|
|
624
|
+
u.uPointerWake.value = h?.wake ?? 0;
|
|
625
|
+
u.uPointerThin.value = h?.thin ?? 0;
|
|
626
|
+
u.uPointerHue.value = h?.hueShift ?? 0;
|
|
627
|
+
u.uPointerLighten.value = h?.lighten ?? 0;
|
|
628
|
+
u.uPointerRipple.value = sc.interaction?.press?.ripple ?? 0;
|
|
629
|
+
u.uShapeFlow.value = sharedFlow;
|
|
630
|
+
});
|
|
565
631
|
this.updatePaletteTextures();
|
|
566
632
|
this.syncVideoPlayback();
|
|
567
633
|
this.group.scale.set(this.config.mirrorH ? -1 : 1, this.config.mirrorV ? -1 : 1, 1);
|
|
@@ -833,6 +899,12 @@ var WaveRenderer = class {
|
|
|
833
899
|
u.uGrainAmount.value = this.config.grain;
|
|
834
900
|
u.uBlurSamples.value = Math.round(this.config.blurSamples ?? 6);
|
|
835
901
|
this.applyBloom();
|
|
902
|
+
this.applyInnerLight();
|
|
903
|
+
this.applyHalftone();
|
|
904
|
+
this.applyHeatmap();
|
|
905
|
+
this.applyHalftoneCmyk();
|
|
906
|
+
this.applyPaperTexture();
|
|
907
|
+
this.applyDither();
|
|
836
908
|
}
|
|
837
909
|
/** Insert / tune / remove the bloom pass. strength 0 removes it from the composer entirely, so
|
|
838
910
|
* cost and pixels are identical to bloom-off; the pass (and its mip-chain render targets) is
|
|
@@ -855,6 +927,172 @@ var WaveRenderer = class {
|
|
|
855
927
|
this.bloomPass = void 0;
|
|
856
928
|
}
|
|
857
929
|
}
|
|
930
|
+
/** Insert / tune / remove the dithering pass — a self-contained "layered" post shader (an ordered
|
|
931
|
+
* Bayer dither, in the spirit of paper-design/shaders). Like bloom, dither 0 removes the pass
|
|
932
|
+
* entirely so cost and pixels match dither-off, and it's created lazily on first enable. It is
|
|
933
|
+
* appended AFTER OutputPass so it runs last and quantizes display-space colour (tone-mapped +
|
|
934
|
+
* sRGB) — dithering the linear composer buffer would crush the steps in the shadows. */
|
|
935
|
+
applyDither() {
|
|
936
|
+
const strength = this.config.dither ?? 0;
|
|
937
|
+
if (strength > 0) {
|
|
938
|
+
if (!this.ditherPass) {
|
|
939
|
+
this.ditherPass = new ShaderPass({
|
|
940
|
+
uniforms: {
|
|
941
|
+
tDiffuse: { value: null },
|
|
942
|
+
uResolution: { value: this.renderer.getDrawingBufferSize(new THREE.Vector2()) },
|
|
943
|
+
uDitherStrength: { value: strength },
|
|
944
|
+
uDitherScale: { value: this.config.ditherScale ?? 2 },
|
|
945
|
+
uDitherSteps: { value: this.config.ditherSteps ?? 4 }
|
|
946
|
+
},
|
|
947
|
+
vertexShader: postVertexShader,
|
|
948
|
+
fragmentShader: ditherFragmentShader
|
|
949
|
+
});
|
|
950
|
+
this.composer.addPass(this.ditherPass);
|
|
951
|
+
}
|
|
952
|
+
const u = this.ditherPass.uniforms;
|
|
953
|
+
u.uDitherStrength.value = strength;
|
|
954
|
+
u.uDitherScale.value = Math.max(1, this.config.ditherScale ?? 2);
|
|
955
|
+
u.uDitherSteps.value = Math.max(2, Math.round(this.config.ditherSteps ?? 4));
|
|
956
|
+
} else if (this.ditherPass) {
|
|
957
|
+
this.composer.removePass(this.ditherPass);
|
|
958
|
+
this.ditherPass.dispose();
|
|
959
|
+
this.ditherPass = void 0;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/** Insert / tune / remove the innerLight pass — volumetric light streaks scattered from the bright
|
|
963
|
+
* wave toward a light point (innerLightX/Y in UV). Scene zone (index 1) so it scatters the raw wave
|
|
964
|
+
* like bloom. innerLight 0 removes the pass entirely; created lazily on first enable. */
|
|
965
|
+
applyInnerLight() {
|
|
966
|
+
const strength = this.config.innerLight ?? 0;
|
|
967
|
+
if (strength > 0) {
|
|
968
|
+
const cx = this.config.innerLightX ?? .5;
|
|
969
|
+
const cy = this.config.innerLightY ?? .15;
|
|
970
|
+
if (!this.innerLightPass) {
|
|
971
|
+
this.innerLightPass = new ShaderPass({
|
|
972
|
+
uniforms: {
|
|
973
|
+
tDiffuse: { value: null },
|
|
974
|
+
uInnerLight: { value: strength },
|
|
975
|
+
uInnerLightDensity: { value: this.config.innerLightDensity ?? .5 },
|
|
976
|
+
uInnerLightDecay: { value: this.config.innerLightDecay ?? .95 },
|
|
977
|
+
uInnerLightCenter: { value: new THREE.Vector2(cx, cy) }
|
|
978
|
+
},
|
|
979
|
+
vertexShader: postVertexShader,
|
|
980
|
+
fragmentShader: innerLightFragmentShader
|
|
981
|
+
});
|
|
982
|
+
this.composer.insertPass(this.innerLightPass, 1);
|
|
983
|
+
}
|
|
984
|
+
const u = this.innerLightPass.uniforms;
|
|
985
|
+
u.uInnerLight.value = strength;
|
|
986
|
+
u.uInnerLightDensity.value = this.config.innerLightDensity ?? .5;
|
|
987
|
+
u.uInnerLightDecay.value = this.config.innerLightDecay ?? .95;
|
|
988
|
+
u.uInnerLightCenter.value.set(cx, cy);
|
|
989
|
+
} else if (this.innerLightPass) {
|
|
990
|
+
this.composer.removePass(this.innerLightPass);
|
|
991
|
+
this.innerLightPass.dispose();
|
|
992
|
+
this.innerLightPass = void 0;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
/** Insert / tune / remove the halftone pass — a rotated dot screen (dot size scales with local
|
|
996
|
+
* brightness) over the finished image. halftone 0 removes the pass; created lazily on enable. */
|
|
997
|
+
applyHalftone() {
|
|
998
|
+
const strength = this.config.halftone ?? 0;
|
|
999
|
+
if (strength > 0) {
|
|
1000
|
+
if (!this.halftonePass) {
|
|
1001
|
+
this.halftonePass = new ShaderPass({
|
|
1002
|
+
uniforms: {
|
|
1003
|
+
tDiffuse: { value: null },
|
|
1004
|
+
uResolution: { value: this.renderer.getDrawingBufferSize(new THREE.Vector2()) },
|
|
1005
|
+
uHalftone: { value: strength },
|
|
1006
|
+
uHalftoneCell: { value: this.config.halftoneCell ?? 6 },
|
|
1007
|
+
uHalftoneAngle: { value: this.config.halftoneAngle ?? .4 }
|
|
1008
|
+
},
|
|
1009
|
+
vertexShader: postVertexShader,
|
|
1010
|
+
fragmentShader: halftoneFragmentShader
|
|
1011
|
+
});
|
|
1012
|
+
this.composer.addPass(this.halftonePass);
|
|
1013
|
+
}
|
|
1014
|
+
const u = this.halftonePass.uniforms;
|
|
1015
|
+
u.uHalftone.value = strength;
|
|
1016
|
+
u.uHalftoneCell.value = Math.max(2, this.config.halftoneCell ?? 6);
|
|
1017
|
+
u.uHalftoneAngle.value = this.config.halftoneAngle ?? .4;
|
|
1018
|
+
} else if (this.halftonePass) {
|
|
1019
|
+
this.composer.removePass(this.halftonePass);
|
|
1020
|
+
this.halftonePass.dispose();
|
|
1021
|
+
this.halftonePass = void 0;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
/** Heatmap: recolour the final image by luminance → thermal palette. Finish zone. */
|
|
1025
|
+
applyHeatmap() {
|
|
1026
|
+
const strength = this.config.heatmap ?? 0;
|
|
1027
|
+
if (strength > 0) {
|
|
1028
|
+
if (!this.heatmapPass) {
|
|
1029
|
+
this.heatmapPass = new ShaderPass({
|
|
1030
|
+
uniforms: {
|
|
1031
|
+
tDiffuse: { value: null },
|
|
1032
|
+
uHeatmap: { value: strength }
|
|
1033
|
+
},
|
|
1034
|
+
vertexShader: postVertexShader,
|
|
1035
|
+
fragmentShader: heatmapFragmentShader
|
|
1036
|
+
});
|
|
1037
|
+
this.composer.addPass(this.heatmapPass);
|
|
1038
|
+
}
|
|
1039
|
+
this.heatmapPass.uniforms.uHeatmap.value = strength;
|
|
1040
|
+
} else if (this.heatmapPass) {
|
|
1041
|
+
this.composer.removePass(this.heatmapPass);
|
|
1042
|
+
this.heatmapPass.dispose();
|
|
1043
|
+
this.heatmapPass = void 0;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/** Paper texture: fibrous substrate shading multiplied over the image. Finish zone. */
|
|
1047
|
+
applyPaperTexture() {
|
|
1048
|
+
const strength = this.config.paperTexture ?? 0;
|
|
1049
|
+
if (strength > 0) {
|
|
1050
|
+
if (!this.paperTexturePass) {
|
|
1051
|
+
this.paperTexturePass = new ShaderPass({
|
|
1052
|
+
uniforms: {
|
|
1053
|
+
tDiffuse: { value: null },
|
|
1054
|
+
uPaper: { value: strength },
|
|
1055
|
+
uPaperScale: { value: this.config.paperTextureScale ?? 2 }
|
|
1056
|
+
},
|
|
1057
|
+
vertexShader: postVertexShader,
|
|
1058
|
+
fragmentShader: paperTextureFragmentShader
|
|
1059
|
+
});
|
|
1060
|
+
this.composer.addPass(this.paperTexturePass);
|
|
1061
|
+
}
|
|
1062
|
+
const u = this.paperTexturePass.uniforms;
|
|
1063
|
+
u.uPaper.value = strength;
|
|
1064
|
+
u.uPaperScale.value = Math.max(.5, this.config.paperTextureScale ?? 2);
|
|
1065
|
+
} else if (this.paperTexturePass) {
|
|
1066
|
+
this.composer.removePass(this.paperTexturePass);
|
|
1067
|
+
this.paperTexturePass.dispose();
|
|
1068
|
+
this.paperTexturePass = void 0;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
/** CMYK halftone: four rotated dot screens (cyan/magenta/yellow/black). Finish zone. */
|
|
1072
|
+
applyHalftoneCmyk() {
|
|
1073
|
+
const strength = this.config.halftoneCmyk ?? 0;
|
|
1074
|
+
if (strength > 0) {
|
|
1075
|
+
if (!this.halftoneCmykPass) {
|
|
1076
|
+
this.halftoneCmykPass = new ShaderPass({
|
|
1077
|
+
uniforms: {
|
|
1078
|
+
tDiffuse: { value: null },
|
|
1079
|
+
uHalftoneCmyk: { value: strength },
|
|
1080
|
+
uHalftoneCmykCell: { value: this.config.halftoneCmykCell ?? 6 }
|
|
1081
|
+
},
|
|
1082
|
+
vertexShader: postVertexShader,
|
|
1083
|
+
fragmentShader: halftoneCmykFragmentShader
|
|
1084
|
+
});
|
|
1085
|
+
this.composer.addPass(this.halftoneCmykPass);
|
|
1086
|
+
}
|
|
1087
|
+
const u = this.halftoneCmykPass.uniforms;
|
|
1088
|
+
u.uHalftoneCmyk.value = strength;
|
|
1089
|
+
u.uHalftoneCmykCell.value = Math.max(2, this.config.halftoneCmykCell ?? 6);
|
|
1090
|
+
} else if (this.halftoneCmykPass) {
|
|
1091
|
+
this.composer.removePass(this.halftoneCmykPass);
|
|
1092
|
+
this.halftoneCmykPass.dispose();
|
|
1093
|
+
this.halftoneCmykPass = void 0;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
858
1096
|
onResize = () => {
|
|
859
1097
|
this.resize();
|
|
860
1098
|
};
|
|
@@ -887,6 +1125,8 @@ var WaveRenderer = class {
|
|
|
887
1125
|
const dw = w * dpr;
|
|
888
1126
|
const dh = h * dpr;
|
|
889
1127
|
this.postPass.uniforms.uResolution.value.set(dw, dh);
|
|
1128
|
+
if (this.ditherPass) this.ditherPass.uniforms.uResolution.value.set(dw, dh);
|
|
1129
|
+
if (this.halftonePass) this.halftonePass.uniforms.uResolution.value.set(dw, dh);
|
|
890
1130
|
for (const s of this.waves) s.material.uniforms.uResolution.value.set(dw, dh);
|
|
891
1131
|
this.camera.left = -dw / 2;
|
|
892
1132
|
this.camera.right = dw / 2;
|
|
@@ -933,6 +1173,7 @@ var WaveRenderer = class {
|
|
|
933
1173
|
}
|
|
934
1174
|
if (!this.running) {
|
|
935
1175
|
this.introTimeRamp = 1;
|
|
1176
|
+
this.interaction?.settle();
|
|
936
1177
|
this.renderOnce();
|
|
937
1178
|
}
|
|
938
1179
|
this.syncVideoPlayback();
|
|
@@ -940,7 +1181,9 @@ var WaveRenderer = class {
|
|
|
940
1181
|
loop = () => {
|
|
941
1182
|
if (!this.running) return;
|
|
942
1183
|
this.timer.update();
|
|
943
|
-
|
|
1184
|
+
const dt = this.timer.getDelta();
|
|
1185
|
+
this.time += dt;
|
|
1186
|
+
this.interaction?.update(dt);
|
|
944
1187
|
if (this.introTimeRamp < 1) this.introTimeRamp = Math.min(1, this.introTimeRamp + .016);
|
|
945
1188
|
this.renderOnce();
|
|
946
1189
|
this.rafId = requestAnimationFrame(this.loop);
|
|
@@ -949,7 +1192,7 @@ var WaveRenderer = class {
|
|
|
949
1192
|
* time = elapsed·introTimeRamp + timeOffset — the ramp eases the animation in on load. */
|
|
950
1193
|
updateTime() {
|
|
951
1194
|
const ramp = this.config.introRamp === false || this.skipIntroRamp ? 1 : this.introTimeRamp;
|
|
952
|
-
const t = this.time * ramp + (this.config.timeOffset ?? 0);
|
|
1195
|
+
const t = this.time * ramp + (this.config.timeOffset ?? 0) + this.interactionTimeOffset;
|
|
953
1196
|
for (let i = 0; i < this.waves.length; i++) {
|
|
954
1197
|
const u = this.waves[i].material.uniforms;
|
|
955
1198
|
u.uTime.value = t;
|
|
@@ -967,10 +1210,156 @@ var WaveRenderer = class {
|
|
|
967
1210
|
renderOnce() {
|
|
968
1211
|
this.updateBackgroundVideoFrame();
|
|
969
1212
|
this.updateTime();
|
|
1213
|
+
this.applyInteraction();
|
|
970
1214
|
this.updateClipPlanes();
|
|
971
1215
|
this.composer.render();
|
|
972
1216
|
this.onAfterRenderFrame();
|
|
973
1217
|
}
|
|
1218
|
+
/** Create/dispose the interaction controller as config toggles interaction on/off. Called from
|
|
1219
|
+
* refresh(); the compiled define set (POINTER_FX etc.) is handled separately by waveDefines(). */
|
|
1220
|
+
syncInteraction() {
|
|
1221
|
+
const active = interactionActive(this.config);
|
|
1222
|
+
if (active && !this.interaction) this.interaction = new InteractionController(this.container, () => this.config);
|
|
1223
|
+
else if (!active && this.interaction) {
|
|
1224
|
+
this.interaction.dispose();
|
|
1225
|
+
this.interaction = void 0;
|
|
1226
|
+
this.interactionTimeOffset = 0;
|
|
1227
|
+
if (this.interactionZoom !== 1) {
|
|
1228
|
+
this.interactionZoom = 1;
|
|
1229
|
+
this.applyZoom();
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
/** Per-frame interaction write: dynamic pointer-field uniforms + bindings. No-op without a
|
|
1234
|
+
* controller. While capturing it writes the REST state instead (pointer field zeroed, every bound
|
|
1235
|
+
* param at its authored base) — merely skipping the write would freeze whatever live hover/scroll
|
|
1236
|
+
* state the previous frame left in the uniforms, so exports wouldn't be deterministic. */
|
|
1237
|
+
applyInteraction() {
|
|
1238
|
+
if (!this.interaction) return;
|
|
1239
|
+
if (this.capturing) {
|
|
1240
|
+
this.applyInteractionRest();
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (anyPointerFxActive(this.config)) this.applyPointerField(this.interaction);
|
|
1244
|
+
this.applyBindings(this.interaction);
|
|
1245
|
+
}
|
|
1246
|
+
/** Write the capture-frame interaction state: exactly what this config renders with no input —
|
|
1247
|
+
* pointer presence + ripple envelopes zeroed (vPointerFall gates every hover effect to 0) and each
|
|
1248
|
+
* bound param at its authored base. Live controller state is left untouched, so the frame after
|
|
1249
|
+
* the capture resumes mid-gesture; the trailing renderOnce() in captureImage restores the preview.
|
|
1250
|
+
* interactionZoom is deliberately NOT reset — captureImage strips it from camera.zoom itself, and
|
|
1251
|
+
* the post-capture restore depends on it being unchanged. */
|
|
1252
|
+
applyInteractionRest() {
|
|
1253
|
+
for (let i = 0; i < this.waves.length; i++) {
|
|
1254
|
+
const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
|
|
1255
|
+
if (!wavePointerFxActive(this.config, sc)) continue;
|
|
1256
|
+
const u = this.waves[i].material.uniforms;
|
|
1257
|
+
u.uPointerActive.value = 0;
|
|
1258
|
+
const rAmp = u.uRippleAmp.value;
|
|
1259
|
+
for (let r = 0; r < 4; r++) rAmp[r] = 0;
|
|
1260
|
+
}
|
|
1261
|
+
const sceneBindings = this.config.interaction?.bindings;
|
|
1262
|
+
if (sceneBindings) {
|
|
1263
|
+
this.interactionSceneOut.timeOffset = this.config.timeOffset ?? 0;
|
|
1264
|
+
this.interactionSceneOut.zoom = this.config.cameraZoom ?? 1;
|
|
1265
|
+
const sceneArgs = {
|
|
1266
|
+
post: this.postPass.uniforms,
|
|
1267
|
+
out: this.interactionSceneOut
|
|
1268
|
+
};
|
|
1269
|
+
for (const b of sceneBindings) {
|
|
1270
|
+
const applier = SCENE_APPLIERS[b.target];
|
|
1271
|
+
applier.apply(applier.base(this.config), sceneArgs);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (this.interactionTimeOffset !== 0) {
|
|
1275
|
+
this.interactionTimeOffset = 0;
|
|
1276
|
+
this.updateTime();
|
|
1277
|
+
}
|
|
1278
|
+
for (let i = 0; i < this.waves.length; i++) {
|
|
1279
|
+
const sc = this.config.waves[i];
|
|
1280
|
+
const bindings = sc?.interaction?.bindings;
|
|
1281
|
+
if (!sc || !bindings || bindings.length === 0) continue;
|
|
1282
|
+
const wave = this.waves[i];
|
|
1283
|
+
for (const b of bindings) {
|
|
1284
|
+
const applier = WAVE_APPLIERS[b.target];
|
|
1285
|
+
applier.apply(applier.base(sc), {
|
|
1286
|
+
u: wave.material.uniforms,
|
|
1287
|
+
mesh: wave.mesh
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
/** Write the dynamic pointer-field uniforms to every wave that HAS a pointer field. Position /
|
|
1293
|
+
* presence are PER WAVE (each trails the cursor at its own hover smoothing); ripple origins/ages
|
|
1294
|
+
* are shared. Per-wave amplitudes were already pushed statically in refresh(). */
|
|
1295
|
+
applyPointerField(ic) {
|
|
1296
|
+
const dw = this.camera.right - this.camera.left;
|
|
1297
|
+
const dh = this.camera.top - this.camera.bottom;
|
|
1298
|
+
const aspect = dh !== 0 ? dw / dh : 1;
|
|
1299
|
+
const ripples = ic.sample().ripples;
|
|
1300
|
+
const agitateDrive = Math.min(1, .2 + .85 * ic.pointerFlux());
|
|
1301
|
+
const vel = ic.pointerVelocity();
|
|
1302
|
+
for (let i = 0; i < this.waves.length; i++) {
|
|
1303
|
+
const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
|
|
1304
|
+
if (!wavePointerFxActive(this.config, sc)) continue;
|
|
1305
|
+
const u = this.waves[i].material.uniforms;
|
|
1306
|
+
u.uPointerAgitate.value = (sc.interaction?.hover?.agitate ?? 0) * agitateDrive;
|
|
1307
|
+
u.uPointerVel.value.copy(vel);
|
|
1308
|
+
const f = ic.fieldFor(i);
|
|
1309
|
+
if (f) {
|
|
1310
|
+
u.uPointer.value.copy(f.ndc);
|
|
1311
|
+
u.uPointerActive.value = f.presence;
|
|
1312
|
+
} else u.uPointerActive.value = 0;
|
|
1313
|
+
u.uPointerAspect.value = aspect;
|
|
1314
|
+
const rOrigin = u.uRippleOrigin.value;
|
|
1315
|
+
const rAge = u.uRippleAge.value;
|
|
1316
|
+
const rAmp = u.uRippleAmp.value;
|
|
1317
|
+
for (let r = 0; r < 4; r++) {
|
|
1318
|
+
rOrigin[r].copy(ripples[r].origin);
|
|
1319
|
+
rAge[r] = ripples[r].age;
|
|
1320
|
+
rAmp[r] = ripples[r].amp;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
/** Evaluate bindings via the applier tables: value = mix(from ?? base, to, smoothedSource). Scene
|
|
1325
|
+
* bindings drive scene params; each wave's bindings drive that wave's uniforms. */
|
|
1326
|
+
applyBindings(ic) {
|
|
1327
|
+
this.interactionSceneOut.timeOffset = this.config.timeOffset ?? 0;
|
|
1328
|
+
this.interactionSceneOut.zoom = this.config.cameraZoom ?? 1;
|
|
1329
|
+
const sceneArgs = {
|
|
1330
|
+
post: this.postPass.uniforms,
|
|
1331
|
+
out: this.interactionSceneOut
|
|
1332
|
+
};
|
|
1333
|
+
for (const b of this.config.interaction?.bindings ?? []) {
|
|
1334
|
+
const applier = SCENE_APPLIERS[b.target];
|
|
1335
|
+
const value = THREE.MathUtils.lerp(b.from ?? applier.base(this.config), b.to, ic.bindingValue(b));
|
|
1336
|
+
applier.apply(value, sceneArgs);
|
|
1337
|
+
}
|
|
1338
|
+
for (let i = 0; i < this.waves.length; i++) {
|
|
1339
|
+
const sc = this.config.waves[i];
|
|
1340
|
+
const bindings = sc?.interaction?.bindings;
|
|
1341
|
+
if (!sc || !bindings || bindings.length === 0) continue;
|
|
1342
|
+
const wave = this.waves[i];
|
|
1343
|
+
for (const b of bindings) {
|
|
1344
|
+
const applier = WAVE_APPLIERS[b.target];
|
|
1345
|
+
const value = THREE.MathUtils.lerp(b.from ?? applier.base(sc), b.to, ic.bindingValue(b));
|
|
1346
|
+
applier.apply(value, {
|
|
1347
|
+
u: wave.material.uniforms,
|
|
1348
|
+
mesh: wave.mesh
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
this.interactionTimeOffset = this.interactionSceneOut.timeOffset - (this.config.timeOffset ?? 0);
|
|
1353
|
+
const nextZoom = this.interactionSceneOut.zoom / (this.config.cameraZoom || 1);
|
|
1354
|
+
if (nextZoom !== this.interactionZoom) {
|
|
1355
|
+
this.interactionZoom = nextZoom;
|
|
1356
|
+
this.applyZoom();
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
/** Feed a `custom:<name>` interaction input (developer API). No-op when interaction is off. */
|
|
1360
|
+
setInteractionInput(name, value) {
|
|
1361
|
+
this.interaction?.setInput(name, value);
|
|
1362
|
+
}
|
|
974
1363
|
/** Re-evaluate play/pause after `config.paused` changes. */
|
|
975
1364
|
refreshPlayback() {
|
|
976
1365
|
this.updateRunning();
|
|
@@ -1005,7 +1394,7 @@ var WaveRenderer = class {
|
|
|
1005
1394
|
applyZoom() {
|
|
1006
1395
|
const dw = this.camera.right - this.camera.left;
|
|
1007
1396
|
const dh = this.camera.top - this.camera.bottom;
|
|
1008
|
-
this.camera.zoom = Math.max(dw / FRAME_W, dh / 750) * (this.config.cameraZoom ?? 1);
|
|
1397
|
+
this.camera.zoom = Math.max(dw / FRAME_W, dh / 750) * (this.config.cameraZoom ?? 1) * this.interactionZoom;
|
|
1009
1398
|
this.camera.updateProjectionMatrix();
|
|
1010
1399
|
}
|
|
1011
1400
|
/** Fit the orthographic near/far planes to the scene before every render, so no part of a wave
|
|
@@ -1025,14 +1414,21 @@ var WaveRenderer = class {
|
|
|
1025
1414
|
* it's a handful of vector ops over 1–8 meshes with no allocation. */
|
|
1026
1415
|
updateClipPlanes() {
|
|
1027
1416
|
this.clipBox.makeEmpty();
|
|
1028
|
-
for (
|
|
1417
|
+
for (let i = 0; i < this.waves.length; i++) {
|
|
1418
|
+
const wave = this.waves[i];
|
|
1029
1419
|
const mesh = wave.mesh;
|
|
1030
1420
|
mesh.updateWorldMatrix(true, false);
|
|
1031
1421
|
const bs = mesh.geometry.boundingSphere;
|
|
1032
1422
|
if (!bs) continue;
|
|
1033
1423
|
const center = this.clipTmpA.setFromMatrixPosition(mesh.matrixWorld);
|
|
1034
1424
|
const disp = Math.abs(Number(wave.material.uniforms.uDispAmount.value) || 0);
|
|
1035
|
-
const
|
|
1425
|
+
const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
|
|
1426
|
+
let pointerDisp = 0;
|
|
1427
|
+
if (wavePointerFxActive(this.config, sc)) {
|
|
1428
|
+
const h = sc.interaction?.hover;
|
|
1429
|
+
pointerDisp = (h?.agitate ?? 0) + Math.abs(h?.push ?? 0) + (h?.wake ?? 0) + (sc.interaction?.press?.ripple ?? 0);
|
|
1430
|
+
}
|
|
1431
|
+
const radius = (bs.center.length() + bs.radius + disp + pointerDisp) * mesh.matrixWorld.getMaxScaleOnAxis() * 1.2;
|
|
1036
1432
|
this.clipBox.expandByPoint(this.clipTmpB.copy(center).addScalar(radius));
|
|
1037
1433
|
this.clipBox.expandByPoint(this.clipTmpB.copy(center).addScalar(-radius));
|
|
1038
1434
|
}
|
|
@@ -1065,12 +1461,22 @@ var WaveRenderer = class {
|
|
|
1065
1461
|
this.introTimeRamp = 1;
|
|
1066
1462
|
}
|
|
1067
1463
|
let blob = null;
|
|
1464
|
+
const prevZoom = this.camera.zoom;
|
|
1465
|
+
const strippingZoom = this.interactionZoom !== 1;
|
|
1068
1466
|
try {
|
|
1069
1467
|
this.capturing = true;
|
|
1468
|
+
if (strippingZoom) {
|
|
1469
|
+
this.camera.zoom = prevZoom / this.interactionZoom;
|
|
1470
|
+
this.camera.updateProjectionMatrix();
|
|
1471
|
+
}
|
|
1070
1472
|
this.renderOnce();
|
|
1071
1473
|
blob = await new Promise((resolve) => this.canvas.toBlob(resolve, mime, quality));
|
|
1072
1474
|
} finally {
|
|
1073
1475
|
this.capturing = false;
|
|
1476
|
+
if (strippingZoom) {
|
|
1477
|
+
this.camera.zoom = prevZoom;
|
|
1478
|
+
this.camera.updateProjectionMatrix();
|
|
1479
|
+
}
|
|
1074
1480
|
if (transparent !== prevBg) {
|
|
1075
1481
|
this.config.transparentBackground = prevBg;
|
|
1076
1482
|
this.applyBackground();
|
|
@@ -1104,6 +1510,8 @@ var WaveRenderer = class {
|
|
|
1104
1510
|
dispose() {
|
|
1105
1511
|
cancelAnimationFrame(this.rafId);
|
|
1106
1512
|
this.running = false;
|
|
1513
|
+
this.interaction?.dispose();
|
|
1514
|
+
this.interaction = void 0;
|
|
1107
1515
|
this.resizeObserver.disconnect();
|
|
1108
1516
|
this.intersectionObserver.disconnect();
|
|
1109
1517
|
this.motionQuery.removeEventListener("change", this.onMotionChange);
|
|
@@ -1118,6 +1526,12 @@ var WaveRenderer = class {
|
|
|
1118
1526
|
s.palette.dispose();
|
|
1119
1527
|
}
|
|
1120
1528
|
this.bloomPass?.dispose();
|
|
1529
|
+
this.ditherPass?.dispose();
|
|
1530
|
+
this.innerLightPass?.dispose();
|
|
1531
|
+
this.halftonePass?.dispose();
|
|
1532
|
+
this.heatmapPass?.dispose();
|
|
1533
|
+
this.paperTexturePass?.dispose();
|
|
1534
|
+
this.halftoneCmykPass?.dispose();
|
|
1121
1535
|
this.composer.dispose();
|
|
1122
1536
|
this.renderer.dispose();
|
|
1123
1537
|
this.renderer.domElement.remove();
|