@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,161 @@
|
|
|
1
|
+
import { hasWebGL, prefersReducedData, prefersReducedMotion } from "./probe.js";
|
|
2
|
+
import { ensurePositioned, setupPoster } from "./poster.js";
|
|
3
|
+
//#region src/shell/createWave.ts
|
|
4
|
+
/**
|
|
5
|
+
* The shell implementation. `loadCore` is an explicit parameter (not read from options) so the
|
|
6
|
+
* standalone/CDN build can pass a synchronous core and NOT bundle the dynamic-import path — its
|
|
7
|
+
* output stays a single file. The public {@link createWave} supplies the dynamic-import default.
|
|
8
|
+
*/
|
|
9
|
+
function createWaveImpl(loadCore, container, config, options) {
|
|
10
|
+
const { lazy = true, rootMargin = "200px", webgl = "auto", respectReducedMotion = true, reducedMotionBehavior = "static", respectSaveData = true, fadeMs = 300 } = options;
|
|
11
|
+
let state = "poster";
|
|
12
|
+
let renderer = null;
|
|
13
|
+
let staged = { ...config };
|
|
14
|
+
if (options.paused !== void 0) staged.paused = options.paused;
|
|
15
|
+
let aborted = false;
|
|
16
|
+
let io = null;
|
|
17
|
+
let lostTimer;
|
|
18
|
+
let lossCount = 0;
|
|
19
|
+
ensurePositioned(container);
|
|
20
|
+
const poster = setupPoster(container, options.poster);
|
|
21
|
+
function setState(next) {
|
|
22
|
+
if (state === next) return;
|
|
23
|
+
state = next;
|
|
24
|
+
options.onStateChange?.(next);
|
|
25
|
+
}
|
|
26
|
+
function fallback(reason) {
|
|
27
|
+
setState("fallback");
|
|
28
|
+
poster?.show();
|
|
29
|
+
options.onFallback?.(reason);
|
|
30
|
+
}
|
|
31
|
+
function onContextRestored() {
|
|
32
|
+
clearTimeout(lostTimer);
|
|
33
|
+
}
|
|
34
|
+
function onContextLost() {
|
|
35
|
+
lossCount += 1;
|
|
36
|
+
clearTimeout(lostTimer);
|
|
37
|
+
if (lossCount >= 2) {
|
|
38
|
+
teardownRenderer();
|
|
39
|
+
fallback("context-lost");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
lostTimer = setTimeout(() => {
|
|
43
|
+
teardownRenderer();
|
|
44
|
+
fallback("context-lost");
|
|
45
|
+
}, 4e3);
|
|
46
|
+
}
|
|
47
|
+
function teardownRenderer() {
|
|
48
|
+
if (!renderer) return;
|
|
49
|
+
const canvas = renderer.renderer.domElement;
|
|
50
|
+
canvas.removeEventListener("webglcontextlost", onContextLost);
|
|
51
|
+
canvas.removeEventListener("webglcontextrestored", onContextRestored);
|
|
52
|
+
renderer.dispose();
|
|
53
|
+
renderer = null;
|
|
54
|
+
}
|
|
55
|
+
async function upgrade() {
|
|
56
|
+
setState("loading");
|
|
57
|
+
let core;
|
|
58
|
+
try {
|
|
59
|
+
core = await loadCore();
|
|
60
|
+
} catch {
|
|
61
|
+
if (!aborted) fallback("load-error");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (aborted) return;
|
|
65
|
+
const full = {
|
|
66
|
+
...core.createDefaultConfig(),
|
|
67
|
+
...staged
|
|
68
|
+
};
|
|
69
|
+
const rendererOptions = { respectReducedMotion };
|
|
70
|
+
renderer = new core.WaveRenderer(container, full, rendererOptions);
|
|
71
|
+
const canvas = renderer.renderer.domElement;
|
|
72
|
+
canvas.addEventListener("webglcontextlost", onContextLost, false);
|
|
73
|
+
canvas.addEventListener("webglcontextrestored", onContextRestored, false);
|
|
74
|
+
renderer.start();
|
|
75
|
+
setState("running");
|
|
76
|
+
options.onReady?.(renderer);
|
|
77
|
+
if (poster) requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
78
|
+
if (!aborted && renderer) poster.fadeOut(fadeMs);
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
function probeAndUpgrade() {
|
|
82
|
+
if (aborted) return;
|
|
83
|
+
if (webgl === "auto" && !hasWebGL()) {
|
|
84
|
+
fallback("no-webgl");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
upgrade();
|
|
88
|
+
}
|
|
89
|
+
function begin() {
|
|
90
|
+
if (webgl === "off") return;
|
|
91
|
+
if (respectSaveData && prefersReducedData()) return fallback("save-data");
|
|
92
|
+
if (respectReducedMotion && reducedMotionBehavior === "poster" && prefersReducedMotion()) return fallback("reduced-motion");
|
|
93
|
+
if (lazy && typeof IntersectionObserver !== "undefined") {
|
|
94
|
+
io = new IntersectionObserver((entries) => {
|
|
95
|
+
if (entries.some((e) => e.isIntersecting)) {
|
|
96
|
+
io?.disconnect();
|
|
97
|
+
io = null;
|
|
98
|
+
probeAndUpgrade();
|
|
99
|
+
}
|
|
100
|
+
}, { rootMargin });
|
|
101
|
+
io.observe(container);
|
|
102
|
+
} else probeAndUpgrade();
|
|
103
|
+
}
|
|
104
|
+
const handle = {
|
|
105
|
+
get state() {
|
|
106
|
+
return state;
|
|
107
|
+
},
|
|
108
|
+
get renderer() {
|
|
109
|
+
return renderer;
|
|
110
|
+
},
|
|
111
|
+
set(next) {
|
|
112
|
+
if (renderer) {
|
|
113
|
+
renderer.setConfig({
|
|
114
|
+
...renderer.getConfig(),
|
|
115
|
+
...next
|
|
116
|
+
});
|
|
117
|
+
renderer.refreshPlayback();
|
|
118
|
+
} else staged = {
|
|
119
|
+
...staged,
|
|
120
|
+
...next
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
play() {
|
|
124
|
+
if (renderer) {
|
|
125
|
+
renderer.getConfig().paused = false;
|
|
126
|
+
renderer.refreshPlayback();
|
|
127
|
+
} else staged.paused = false;
|
|
128
|
+
},
|
|
129
|
+
pause() {
|
|
130
|
+
if (renderer) {
|
|
131
|
+
renderer.getConfig().paused = true;
|
|
132
|
+
renderer.refreshPlayback();
|
|
133
|
+
} else staged.paused = true;
|
|
134
|
+
},
|
|
135
|
+
destroy() {
|
|
136
|
+
aborted = true;
|
|
137
|
+
io?.disconnect();
|
|
138
|
+
io = null;
|
|
139
|
+
clearTimeout(lostTimer);
|
|
140
|
+
teardownRenderer();
|
|
141
|
+
poster?.remove();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
begin();
|
|
145
|
+
return handle;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Mount a self-optimizing wave into a container: shows a poster immediately, then — lazily, and only
|
|
149
|
+
* when the browser can actually run it — fetches the engine, builds the renderer, and crossfades in.
|
|
150
|
+
* Falls back to the poster on no-WebGL / save-data / reduced-motion / context-loss / load errors.
|
|
151
|
+
* No static three import: the engine arrives via a dynamic import, so the shell stays tiny.
|
|
152
|
+
*/
|
|
153
|
+
function createWave(container, config = {}, options = {}) {
|
|
154
|
+
return createWaveImpl(options.loadCore ?? (() => import("../core-loader.js")), container, config, options);
|
|
155
|
+
}
|
|
156
|
+
/** The drop-in embed contract: an alias of {@link createWave}. */
|
|
157
|
+
const mountWave = createWave;
|
|
158
|
+
//#endregion
|
|
159
|
+
export { createWave, createWaveImpl, mountWave };
|
|
160
|
+
|
|
161
|
+
//# sourceMappingURL=createWave.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createWave.js","names":[],"sources":["../../src/shell/createWave.ts"],"sourcesContent":["import type { StudioConfig } from \"../config/model\";\nimport type { WaveRenderer, WaveRendererOptions } from \"../renderer/WaveRenderer\";\nimport { hasWebGL, prefersReducedMotion, prefersReducedData } from \"./probe\";\nimport { setupPoster, ensurePositioned, type Poster } from \"./poster\";\n\n/** Why the shell showed the poster instead of a live wave. */\nexport type FallbackReason =\n | \"no-webgl\"\n | \"reduced-motion\"\n | \"save-data\"\n | \"context-lost\"\n | \"load-error\";\n\n/** poster → loading → running, or → fallback (permanent poster). */\nexport type WaveState = \"poster\" | \"loading\" | \"running\" | \"fallback\";\n\n/** The heavy module fetched on upgrade. */\ntype CoreModule = typeof import(\"../core-loader\");\n\nexport interface WaveOptions {\n /** Poster URL / data-URI. Defaults to adopting the container's `<img data-wave3d-poster>` (SSR). */\n poster?: string;\n /** Wait until the container nears the viewport before fetching the engine. Default true. */\n lazy?: boolean;\n /** IntersectionObserver margin for the lazy trigger. Default \"200px\". */\n rootMargin?: string;\n /** \"auto\" probes WebGL (with failIfMajorPerformanceCaveat); \"force\" skips the probe; \"off\" stays a poster. */\n webgl?: \"auto\" | \"force\" | \"off\";\n /** Forward prefers-reduced-motion to the renderer (freezes to a full static frame). Default true. */\n respectReducedMotion?: boolean;\n /** With reduced motion: \"static\" upgrades to a frozen frame; \"poster\" stays a poster. Default \"static\". */\n reducedMotionBehavior?: \"static\" | \"poster\";\n /** Keep a permanent poster when the user has Save-Data on. Default true. */\n respectSaveData?: boolean;\n /** Poster→canvas crossfade duration (ms). Default 300. */\n fadeMs?: number;\n /** Start paused. */\n paused?: boolean;\n onReady?(renderer: WaveRenderer): void;\n onFallback?(reason: FallbackReason): void;\n onStateChange?(state: WaveState): void;\n /** Seam for the standalone/CDN build to supply the core synchronously (three already bundled). */\n loadCore?(): Promise<CoreModule>;\n}\n\nexport interface WaveHandle {\n readonly state: WaveState;\n readonly renderer: WaveRenderer | null;\n /** Merge a partial config. Staged before upgrade; after, setConfig() then refreshPlayback(). */\n set(config: Partial<StudioConfig>): void;\n play(): void;\n pause(): void;\n /** Safe to call in any state (aborts a pending upgrade, disposes a live renderer, removes the poster). */\n destroy(): void;\n}\n\n/**\n * The shell implementation. `loadCore` is an explicit parameter (not read from options) so the\n * standalone/CDN build can pass a synchronous core and NOT bundle the dynamic-import path — its\n * output stays a single file. The public {@link createWave} supplies the dynamic-import default.\n */\nexport function createWaveImpl(\n loadCore: () => Promise<CoreModule>,\n container: HTMLElement,\n config: Partial<StudioConfig>,\n options: WaveOptions,\n): WaveHandle {\n const {\n lazy = true,\n rootMargin = \"200px\",\n webgl = \"auto\",\n respectReducedMotion = true,\n reducedMotionBehavior = \"static\",\n respectSaveData = true,\n fadeMs = 300,\n } = options;\n\n let state: WaveState = \"poster\";\n let renderer: WaveRenderer | null = null;\n let staged: Partial<StudioConfig> = { ...config };\n if (options.paused !== undefined) staged.paused = options.paused;\n\n let aborted = false;\n let io: IntersectionObserver | null = null;\n let lostTimer: ReturnType<typeof setTimeout> | undefined;\n let lossCount = 0;\n\n ensurePositioned(container);\n const poster: Poster | null = setupPoster(container, options.poster);\n\n function setState(next: WaveState): void {\n if (state === next) return;\n state = next;\n options.onStateChange?.(next);\n }\n\n function fallback(reason: FallbackReason): void {\n setState(\"fallback\");\n poster?.show();\n options.onFallback?.(reason);\n }\n\n function onContextRestored(): void {\n clearTimeout(lostTimer); // three rebuilt the context in time; stay live\n }\n\n function onContextLost(): void {\n lossCount += 1;\n clearTimeout(lostTimer);\n if (lossCount >= 2) {\n teardownRenderer();\n fallback(\"context-lost\");\n return;\n }\n // three (WaveRenderer) tries to restore; if it hasn't within ~4s, give up to the poster.\n lostTimer = setTimeout(() => {\n teardownRenderer();\n fallback(\"context-lost\");\n }, 4000);\n }\n\n function teardownRenderer(): void {\n if (!renderer) return;\n const canvas = renderer.renderer.domElement;\n canvas.removeEventListener(\"webglcontextlost\", onContextLost);\n canvas.removeEventListener(\"webglcontextrestored\", onContextRestored);\n renderer.dispose();\n renderer = null;\n }\n\n async function upgrade(): Promise<void> {\n setState(\"loading\");\n let core: CoreModule;\n try {\n core = await loadCore();\n } catch {\n if (!aborted) fallback(\"load-error\");\n return;\n }\n if (aborted) return;\n\n const full: StudioConfig = { ...core.createDefaultConfig(), ...staged };\n const rendererOptions: WaveRendererOptions = { respectReducedMotion };\n renderer = new core.WaveRenderer(container, full, rendererOptions);\n const canvas = renderer.renderer.domElement;\n canvas.addEventListener(\"webglcontextlost\", onContextLost, false);\n canvas.addEventListener(\"webglcontextrestored\", onContextRestored, false);\n renderer.start();\n setState(\"running\");\n options.onReady?.(renderer);\n\n if (poster) {\n // Crossfade only after two frames, so the wave has definitely painted first.\n requestAnimationFrame(() =>\n requestAnimationFrame(() => {\n if (!aborted && renderer) poster.fadeOut(fadeMs);\n }),\n );\n }\n }\n\n function probeAndUpgrade(): void {\n if (aborted) return;\n if (webgl === \"auto\" && !hasWebGL()) {\n fallback(\"no-webgl\");\n return;\n }\n void upgrade();\n }\n\n function begin(): void {\n // Permanent-poster gates (checked before any lazy wait or engine fetch).\n if (webgl === \"off\") return; // deliberate poster-only mode — stay \"poster\", no fallback callback\n if (respectSaveData && prefersReducedData()) return fallback(\"save-data\");\n if (respectReducedMotion && reducedMotionBehavior === \"poster\" && prefersReducedMotion()) {\n return fallback(\"reduced-motion\");\n }\n if (lazy && typeof IntersectionObserver !== \"undefined\") {\n io = new IntersectionObserver(\n (entries) => {\n if (entries.some((e) => e.isIntersecting)) {\n io?.disconnect();\n io = null;\n probeAndUpgrade();\n }\n },\n { rootMargin },\n );\n io.observe(container);\n } else {\n probeAndUpgrade();\n }\n }\n\n const handle: WaveHandle = {\n get state() {\n return state;\n },\n get renderer() {\n return renderer;\n },\n set(next) {\n if (renderer) {\n renderer.setConfig({ ...renderer.getConfig(), ...next });\n renderer.refreshPlayback(); // setConfig doesn't re-evaluate `paused` on its own\n } else {\n staged = { ...staged, ...next };\n }\n },\n play() {\n if (renderer) {\n renderer.getConfig().paused = false;\n renderer.refreshPlayback();\n } else {\n staged.paused = false;\n }\n },\n pause() {\n if (renderer) {\n renderer.getConfig().paused = true;\n renderer.refreshPlayback();\n } else {\n staged.paused = true;\n }\n },\n destroy() {\n aborted = true;\n io?.disconnect();\n io = null;\n clearTimeout(lostTimer);\n teardownRenderer();\n poster?.remove();\n },\n };\n\n begin();\n return handle;\n}\n\n/**\n * Mount a self-optimizing wave into a container: shows a poster immediately, then — lazily, and only\n * when the browser can actually run it — fetches the engine, builds the renderer, and crossfades in.\n * Falls back to the poster on no-WebGL / save-data / reduced-motion / context-loss / load errors.\n * No static three import: the engine arrives via a dynamic import, so the shell stays tiny.\n */\nexport function createWave(\n container: HTMLElement,\n config: Partial<StudioConfig> = {},\n options: WaveOptions = {},\n): WaveHandle {\n return createWaveImpl(\n options.loadCore ?? (() => import(\"../core-loader\")),\n container,\n config,\n options,\n );\n}\n\n/** The drop-in embed contract: an alias of {@link createWave}. */\nexport const mountWave = createWave;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/shell/poster.ts
|
|
2
|
+
const POSTER_ATTR = "data-wave3d-poster";
|
|
3
|
+
/** Make the container a positioning context so the absolutely-positioned poster overlays the canvas. */
|
|
4
|
+
function ensurePositioned(container) {
|
|
5
|
+
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Create the poster image — or adopt an existing SSR `<img data-wave3d-poster>` already inside the
|
|
9
|
+
* container (no hydration flash). Positioned to cover the container and sit above the canvas.
|
|
10
|
+
* Returns null when there's neither a `src` nor an adoptable image (poster is optional).
|
|
11
|
+
*/
|
|
12
|
+
function setupPoster(container, src) {
|
|
13
|
+
let img = container.querySelector(`img[${POSTER_ATTR}]`);
|
|
14
|
+
if (!img && !src) return null;
|
|
15
|
+
if (!img) {
|
|
16
|
+
img = document.createElement("img");
|
|
17
|
+
img.setAttribute(POSTER_ATTR, "");
|
|
18
|
+
img.decoding = "async";
|
|
19
|
+
img.alt = "";
|
|
20
|
+
img.setAttribute("aria-hidden", "true");
|
|
21
|
+
container.appendChild(img);
|
|
22
|
+
}
|
|
23
|
+
if (src) img.src = src;
|
|
24
|
+
Object.assign(img.style, {
|
|
25
|
+
position: "absolute",
|
|
26
|
+
inset: "0",
|
|
27
|
+
width: "100%",
|
|
28
|
+
height: "100%",
|
|
29
|
+
objectFit: "cover",
|
|
30
|
+
pointerEvents: "none",
|
|
31
|
+
zIndex: "1",
|
|
32
|
+
opacity: "1",
|
|
33
|
+
visibility: "visible"
|
|
34
|
+
});
|
|
35
|
+
const el = img;
|
|
36
|
+
return {
|
|
37
|
+
el,
|
|
38
|
+
fadeOut(fadeMs) {
|
|
39
|
+
if (fadeMs <= 0) {
|
|
40
|
+
el.style.opacity = "0";
|
|
41
|
+
el.style.visibility = "hidden";
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
el.style.transition = `opacity ${fadeMs}ms ease`;
|
|
45
|
+
el.offsetWidth;
|
|
46
|
+
el.style.opacity = "0";
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
el.style.visibility = "hidden";
|
|
49
|
+
}, fadeMs + 50);
|
|
50
|
+
},
|
|
51
|
+
show() {
|
|
52
|
+
el.style.transition = "";
|
|
53
|
+
el.style.opacity = "1";
|
|
54
|
+
el.style.visibility = "visible";
|
|
55
|
+
},
|
|
56
|
+
remove() {
|
|
57
|
+
el.remove();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { ensurePositioned, setupPoster };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=poster.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poster.js","names":[],"sources":["../../src/shell/poster.ts"],"sourcesContent":["// Poster management for the shell: the static image shown first (server-rendered or generated)\n// that covers the container until the live wave has painted, then crossfades out.\n\nconst POSTER_ATTR = \"data-wave3d-poster\";\n\nexport interface Poster {\n readonly el: HTMLImageElement;\n /** Fade the poster to transparent over `fadeMs`, then hide it so it can't block the canvas. */\n fadeOut(fadeMs: number): void;\n /** Re-show the poster instantly (used when a live wave is torn down back to the fallback). */\n show(): void;\n /** Remove the poster element from the DOM. */\n remove(): void;\n}\n\n/** Make the container a positioning context so the absolutely-positioned poster overlays the canvas. */\nexport function ensurePositioned(container: HTMLElement): void {\n if (getComputedStyle(container).position === \"static\") container.style.position = \"relative\";\n}\n\n/**\n * Create the poster image — or adopt an existing SSR `<img data-wave3d-poster>` already inside the\n * container (no hydration flash). Positioned to cover the container and sit above the canvas.\n * Returns null when there's neither a `src` nor an adoptable image (poster is optional).\n */\nexport function setupPoster(container: HTMLElement, src?: string): Poster | null {\n let img = container.querySelector<HTMLImageElement>(`img[${POSTER_ATTR}]`);\n if (!img && !src) return null;\n if (!img) {\n img = document.createElement(\"img\");\n img.setAttribute(POSTER_ATTR, \"\");\n img.decoding = \"async\";\n img.alt = \"\";\n img.setAttribute(\"aria-hidden\", \"true\");\n container.appendChild(img);\n }\n if (src) img.src = src;\n Object.assign(img.style, {\n position: \"absolute\",\n inset: \"0\",\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n pointerEvents: \"none\", // clicks/scrolls pass through to the canvas beneath\n zIndex: \"1\", // above the WebGL canvas (which sits at the default z-order)\n opacity: \"1\",\n visibility: \"visible\",\n });\n const el = img;\n return {\n el,\n fadeOut(fadeMs) {\n if (fadeMs <= 0) {\n el.style.opacity = \"0\";\n el.style.visibility = \"hidden\";\n return;\n }\n el.style.transition = `opacity ${fadeMs}ms ease`;\n void el.offsetWidth; // force a style flush so the transition runs from the current opacity (1)\n el.style.opacity = \"0\";\n // Hide after the fade so the (transparent) poster can never intercept anything; the timer is\n // rAF-independent so it still completes in a throttled/backgrounded tab.\n setTimeout(() => {\n el.style.visibility = \"hidden\";\n }, fadeMs + 50);\n },\n show() {\n el.style.transition = \"\";\n el.style.opacity = \"1\";\n el.style.visibility = \"visible\";\n },\n remove() {\n el.remove();\n },\n };\n}\n"],"mappings":";AAGA,MAAM,cAAc;;AAapB,SAAgB,iBAAiB,WAA8B;CAC7D,IAAI,iBAAiB,SAAS,CAAC,CAAC,aAAa,UAAU,UAAU,MAAM,WAAW;AACpF;;;;;;AAOA,SAAgB,YAAY,WAAwB,KAA6B;CAC/E,IAAI,MAAM,UAAU,cAAgC,OAAO,YAAY,EAAE;CACzE,IAAI,CAAC,OAAO,CAAC,KAAK,OAAO;CACzB,IAAI,CAAC,KAAK;EACR,MAAM,SAAS,cAAc,KAAK;EAClC,IAAI,aAAa,aAAa,EAAE;EAChC,IAAI,WAAW;EACf,IAAI,MAAM;EACV,IAAI,aAAa,eAAe,MAAM;EACtC,UAAU,YAAY,GAAG;CAC3B;CACA,IAAI,KAAK,IAAI,MAAM;CACnB,OAAO,OAAO,IAAI,OAAO;EACvB,UAAU;EACV,OAAO;EACP,OAAO;EACP,QAAQ;EACR,WAAW;EACX,eAAe;EACf,QAAQ;EACR,SAAS;EACT,YAAY;CACd,CAAC;CACD,MAAM,KAAK;CACX,OAAO;EACL;EACA,QAAQ,QAAQ;GACd,IAAI,UAAU,GAAG;IACf,GAAG,MAAM,UAAU;IACnB,GAAG,MAAM,aAAa;IACtB;GACF;GACA,GAAG,MAAM,aAAa,WAAW,OAAO;GACxC,GAAQ;GACR,GAAG,MAAM,UAAU;GAGnB,iBAAiB;IACf,GAAG,MAAM,aAAa;GACxB,GAAG,SAAS,EAAE;EAChB;EACA,OAAO;GACL,GAAG,MAAM,aAAa;GACtB,GAAG,MAAM,UAAU;GACnB,GAAG,MAAM,aAAa;EACxB;EACA,SAAS;GACP,GAAG,OAAO;EACZ;CACF;AACF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/shell/probe.ts
|
|
2
|
+
/**
|
|
3
|
+
* Synchronously test whether the browser can give us a usable WebGL context. Uses
|
|
4
|
+
* `failIfMajorPerformanceCaveat` so a software/blocklisted renderer (which would run the wave at a
|
|
5
|
+
* slideshow framerate) reports as unavailable and we keep the poster. Releases the throwaway
|
|
6
|
+
* context immediately via WEBGL_lose_context so probing doesn't consume one of the browser's ~16
|
|
7
|
+
* live contexts.
|
|
8
|
+
*/
|
|
9
|
+
function hasWebGL() {
|
|
10
|
+
if (typeof document === "undefined") return false;
|
|
11
|
+
try {
|
|
12
|
+
const canvas = document.createElement("canvas");
|
|
13
|
+
const attrs = { failIfMajorPerformanceCaveat: true };
|
|
14
|
+
const gl = canvas.getContext("webgl2", attrs) ?? canvas.getContext("webgl", attrs);
|
|
15
|
+
if (!gl) return false;
|
|
16
|
+
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** True when the OS/browser is set to reduce motion. */
|
|
23
|
+
function prefersReducedMotion() {
|
|
24
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
25
|
+
}
|
|
26
|
+
/** True when the user has asked for reduced data usage (Save-Data / Data Saver). */
|
|
27
|
+
function prefersReducedData() {
|
|
28
|
+
if (typeof navigator === "undefined") return false;
|
|
29
|
+
return navigator.connection?.saveData === true;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { hasWebGL, prefersReducedData, prefersReducedMotion };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe.js","names":[],"sources":["../../src/shell/probe.ts"],"sourcesContent":["// WebGL capability probe for the shell. Kept dependency-free (no three) so it can run before the\n// heavy renderer chunk is fetched — the shell decides poster-vs-upgrade from this.\n\n/**\n * Synchronously test whether the browser can give us a usable WebGL context. Uses\n * `failIfMajorPerformanceCaveat` so a software/blocklisted renderer (which would run the wave at a\n * slideshow framerate) reports as unavailable and we keep the poster. Releases the throwaway\n * context immediately via WEBGL_lose_context so probing doesn't consume one of the browser's ~16\n * live contexts.\n */\nexport function hasWebGL(): boolean {\n if (typeof document === \"undefined\") return false;\n try {\n const canvas = document.createElement(\"canvas\");\n const attrs: WebGLContextAttributes = { failIfMajorPerformanceCaveat: true };\n const gl =\n canvas.getContext(\"webgl2\", attrs) ??\n (canvas.getContext(\"webgl\", attrs) as WebGLRenderingContext | null);\n if (!gl) return false;\n gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n return true;\n } catch {\n return false;\n }\n}\n\n/** True when the OS/browser is set to reduce motion. */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n\n/** True when the user has asked for reduced data usage (Save-Data / Data Saver). */\nexport function prefersReducedData(): boolean {\n if (typeof navigator === \"undefined\") return false;\n const conn = (navigator as Navigator & { connection?: { saveData?: boolean } }).connection;\n return conn?.saveData === true;\n}\n"],"mappings":";;;;;;;;AAUA,SAAgB,WAAoB;CAClC,IAAI,OAAO,aAAa,aAAa,OAAO;CAC5C,IAAI;EACF,MAAM,SAAS,SAAS,cAAc,QAAQ;EAC9C,MAAM,QAAgC,EAAE,8BAA8B,KAAK;EAC3E,MAAM,KACJ,OAAO,WAAW,UAAU,KAAK,KAChC,OAAO,WAAW,SAAS,KAAK;EACnC,IAAI,CAAC,IAAI,OAAO;EAChB,GAAG,aAAa,oBAAoB,CAAC,EAAE,YAAY;EACnD,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAgB,uBAAgC;CAC9C,OACE,OAAO,WAAW,eAClB,OAAO,OAAO,eAAe,cAC7B,OAAO,WAAW,kCAAkC,CAAC,CAAC;AAE1D;;AAGA,SAAgB,qBAA8B;CAC5C,IAAI,OAAO,cAAc,aAAa,OAAO;CAE7C,OADc,UAAkE,YACnE,aAAa;AAC5B"}
|