@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 +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 SnapshotOptions {\n /** Image MIME type. Default `\"image/webp\"`. */\n type?: string;\n /** Encoder quality 0–1 for lossy types. */\n quality?: number;\n /** Render with a transparent background. Default true. */\n transparent?: boolean;\n /** Render a fixed animation-time for a reproducible frame (default: the live frame). Poster\n * captures should pass `0` — the frame the wave opens on, so the file doesn't churn per capture. */\n time?: number;\n}\n\nexport interface WaveHandle {\n readonly state: WaveState;\n readonly renderer: WaveRenderer | null;\n /** Capture the current live frame as an image Blob (a poster you can host/cache). Resolves `null`\n * until the wave is running (poster / fallback / pre-upgrade) — wait for {@link WaveOptions.onReady}\n * (or the element's `wave3d-ready` event) first. */\n snapshot(options?: SnapshotOptions): Promise<Blob | 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 snapshot(opts = {}) {\n if (!renderer) return Promise.resolve(null);\n const { type = \"image/webp\", quality, transparent = true, time } = opts;\n return renderer.captureImage(type, transparent, quality, time);\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":";;;;;;;;AA6EA,SAAgB,eACd,UACA,WACA,QACA,SACY;CACZ,MAAM,EACJ,OAAO,MACP,aAAa,SACb,QAAQ,QACR,uBAAuB,MACvB,wBAAwB,UACxB,kBAAkB,MAClB,SAAS,QACP;CAEJ,IAAI,QAAmB;CACvB,IAAI,WAAgC;CACpC,IAAI,SAAgC,EAAE,GAAG,OAAO;CAChD,IAAI,QAAQ,WAAW,KAAA,GAAW,OAAO,SAAS,QAAQ;CAE1D,IAAI,UAAU;CACd,IAAI,KAAkC;CACtC,IAAI;CACJ,IAAI,YAAY;CAEhB,iBAAiB,SAAS;CAC1B,MAAM,SAAwB,YAAY,WAAW,QAAQ,MAAM;CAEnE,SAAS,SAAS,MAAuB;EACvC,IAAI,UAAU,MAAM;EACpB,QAAQ;EACR,QAAQ,gBAAgB,IAAI;CAC9B;CAEA,SAAS,SAAS,QAA8B;EAC9C,SAAS,UAAU;EACnB,QAAQ,KAAK;EACb,QAAQ,aAAa,MAAM;CAC7B;CAEA,SAAS,oBAA0B;EACjC,aAAa,SAAS;CACxB;CAEA,SAAS,gBAAsB;EAC7B,aAAa;EACb,aAAa,SAAS;EACtB,IAAI,aAAa,GAAG;GAClB,iBAAiB;GACjB,SAAS,cAAc;GACvB;EACF;EAEA,YAAY,iBAAiB;GAC3B,iBAAiB;GACjB,SAAS,cAAc;EACzB,GAAG,GAAI;CACT;CAEA,SAAS,mBAAyB;EAChC,IAAI,CAAC,UAAU;EACf,MAAM,SAAS,SAAS,SAAS;EACjC,OAAO,oBAAoB,oBAAoB,aAAa;EAC5D,OAAO,oBAAoB,wBAAwB,iBAAiB;EACpE,SAAS,QAAQ;EACjB,WAAW;CACb;CAEA,eAAe,UAAyB;EACtC,SAAS,SAAS;EAClB,IAAI;EACJ,IAAI;GACF,OAAO,MAAM,SAAS;EACxB,QAAQ;GACN,IAAI,CAAC,SAAS,SAAS,YAAY;GACnC;EACF;EACA,IAAI,SAAS;EAEb,MAAM,OAAqB;GAAE,GAAG,KAAK,oBAAoB;GAAG,GAAG;EAAO;EACtE,MAAM,kBAAuC,EAAE,qBAAqB;EACpE,WAAW,IAAI,KAAK,aAAa,WAAW,MAAM,eAAe;EACjE,MAAM,SAAS,SAAS,SAAS;EACjC,OAAO,iBAAiB,oBAAoB,eAAe,KAAK;EAChE,OAAO,iBAAiB,wBAAwB,mBAAmB,KAAK;EACxE,SAAS,MAAM;EACf,SAAS,SAAS;EAClB,QAAQ,UAAU,QAAQ;EAE1B,IAAI,QAEF,4BACE,4BAA4B;GAC1B,IAAI,CAAC,WAAW,UAAU,OAAO,QAAQ,MAAM;EACjD,CAAC,CACH;CAEJ;CAEA,SAAS,kBAAwB;EAC/B,IAAI,SAAS;EACb,IAAI,UAAU,UAAU,CAAC,SAAS,GAAG;GACnC,SAAS,UAAU;GACnB;EACF;EACA,QAAa;CACf;CAEA,SAAS,QAAc;EAErB,IAAI,UAAU,OAAO;EACrB,IAAI,mBAAmB,mBAAmB,GAAG,OAAO,SAAS,WAAW;EACxE,IAAI,wBAAwB,0BAA0B,YAAY,qBAAqB,GACrF,OAAO,SAAS,gBAAgB;EAElC,IAAI,QAAQ,OAAO,yBAAyB,aAAa;GACvD,KAAK,IAAI,sBACN,YAAY;IACX,IAAI,QAAQ,MAAM,MAAM,EAAE,cAAc,GAAG;KACzC,IAAI,WAAW;KACf,KAAK;KACL,gBAAgB;IAClB;GACF,GACA,EAAE,WAAW,CACf;GACA,GAAG,QAAQ,SAAS;EACtB,OACE,gBAAgB;CAEpB;CAEA,MAAM,SAAqB;EACzB,IAAI,QAAQ;GACV,OAAO;EACT;EACA,IAAI,WAAW;GACb,OAAO;EACT;EACA,SAAS,OAAO,CAAC,GAAG;GAClB,IAAI,CAAC,UAAU,OAAO,QAAQ,QAAQ,IAAI;GAC1C,MAAM,EAAE,OAAO,cAAc,SAAS,cAAc,MAAM,SAAS;GACnE,OAAO,SAAS,aAAa,MAAM,aAAa,SAAS,IAAI;EAC/D;EACA,IAAI,MAAM;GACR,IAAI,UAAU;IACZ,SAAS,UAAU;KAAE,GAAG,SAAS,UAAU;KAAG,GAAG;IAAK,CAAC;IACvD,SAAS,gBAAgB;GAC3B,OACE,SAAS;IAAE,GAAG;IAAQ,GAAG;GAAK;EAElC;EACA,OAAO;GACL,IAAI,UAAU;IACZ,SAAS,UAAU,CAAC,CAAC,SAAS;IAC9B,SAAS,gBAAgB;GAC3B,OACE,OAAO,SAAS;EAEpB;EACA,QAAQ;GACN,IAAI,UAAU;IACZ,SAAS,UAAU,CAAC,CAAC,SAAS;IAC9B,SAAS,gBAAgB;GAC3B,OACE,OAAO,SAAS;EAEpB;EACA,UAAU;GACR,UAAU;GACV,IAAI,WAAW;GACf,KAAK;GACL,aAAa,SAAS;GACtB,iBAAiB;GACjB,QAAQ,OAAO;EACjB;CACF;CAEA,MAAM;CACN,OAAO;AACT;;;;;;;AAQA,SAAgB,WACd,WACA,SAAgC,CAAC,GACjC,UAAuB,CAAC,GACZ;CACZ,OAAO,eACL,QAAQ,mBAAmB,OAAO,uBAClC,WACA,QACA,OACF;AACF;;AAGA,MAAa,YAAY"}
|
|
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, type PosterFit } from \"./poster\";\n\nexport type { PosterFit } 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 /** Poster `object-fit`. Default `\"fill\"` — matches the canvas (which renders edge-to-edge at the\n * container's aspect), so a poster captured at that aspect hands off with no visible jump. Use\n * `\"cover\"` to crop a different-aspect placeholder instead of stretching it. See {@link PosterFit}. */\n posterFit?: PosterFit;\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 SnapshotOptions {\n /** Image MIME type. Default `\"image/webp\"`. */\n type?: string;\n /** Encoder quality 0–1 for lossy types. */\n quality?: number;\n /** Render with a transparent background. Default true. */\n transparent?: boolean;\n /** Render a fixed animation-time for a reproducible frame (default: the live frame). Poster\n * captures should pass `0` — the frame the wave opens on, so the file doesn't churn per capture. */\n time?: number;\n}\n\nexport interface WaveHandle {\n readonly state: WaveState;\n readonly renderer: WaveRenderer | null;\n /** Capture the current live frame as an image Blob (a poster you can host/cache). Resolves `null`\n * until the wave is running (poster / fallback / pre-upgrade) — wait for {@link WaveOptions.onReady}\n * (or the element's `wave3d-ready` event) first. */\n snapshot(options?: SnapshotOptions): Promise<Blob | null>;\n /** Merge a partial config. Staged before upgrade; after, setConfig() then refreshPlayback(). */\n set(config: Partial<StudioConfig>): void;\n /** Feed a `custom:<name>` interaction input for `custom:*` bindings. Staged (last value per name)\n * before upgrade and replayed once the renderer is live; a no-op if no binding consumes it. */\n setInteractionInput(name: string, value: number): 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 // Interaction inputs fed before the renderer exists — last value per name, replayed on upgrade.\n const stagedInputs = new Map<string, number>();\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, options.posterFit);\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 for (const [name, value] of stagedInputs) renderer.setInteractionInput(name, value);\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 snapshot(opts = {}) {\n if (!renderer) return Promise.resolve(null);\n const { type = \"image/webp\", quality, transparent = true, time } = opts;\n return renderer.captureImage(type, transparent, quality, time);\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 setInteractionInput(name, value) {\n if (renderer) renderer.setInteractionInput(name, value);\n else stagedInputs.set(name, value);\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,12 @@
|
|
|
1
|
+
//#region src/shell/poster.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* How the poster image maps into the container box (its CSS `object-fit`).
|
|
4
|
+
* `"fill"` (default) stretches it edge-to-edge, exactly like the canvas — which renders at the
|
|
5
|
+
* container's own aspect — so a poster captured at that aspect aligns pixel-for-pixel and the
|
|
6
|
+
* poster→canvas handoff has no visible jump. `"cover"` crops to preserve the poster's own aspect
|
|
7
|
+
* (pick it for a non-wave / different-aspect placeholder where distortion would look worse).
|
|
8
|
+
*/
|
|
9
|
+
type PosterFit = "cover" | "contain" | "fill";
|
|
10
|
+
//#endregion
|
|
11
|
+
export { PosterFit };
|
|
12
|
+
//# sourceMappingURL=poster.d.ts.map
|
package/dist/shell/poster.js
CHANGED
|
@@ -6,10 +6,11 @@ function ensurePositioned(container) {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* Create the poster image — or adopt an existing SSR `<img data-wave3d-poster>` already inside the
|
|
9
|
-
* container (no hydration flash). Positioned to
|
|
9
|
+
* container (no hydration flash). Positioned to overlay the container and sit above the canvas;
|
|
10
|
+
* `fit` (default `"fill"`) sets how it maps into the box — see {@link PosterFit}.
|
|
10
11
|
* Returns null when there's neither a `src` nor an adoptable image (poster is optional).
|
|
11
12
|
*/
|
|
12
|
-
function setupPoster(container, src) {
|
|
13
|
+
function setupPoster(container, src, fit = "fill") {
|
|
13
14
|
let img = container.querySelector(`img[${POSTER_ATTR}]`);
|
|
14
15
|
if (!img && !src) return null;
|
|
15
16
|
if (!img) {
|
|
@@ -26,7 +27,7 @@ function setupPoster(container, src) {
|
|
|
26
27
|
inset: "0",
|
|
27
28
|
width: "100%",
|
|
28
29
|
height: "100%",
|
|
29
|
-
objectFit:
|
|
30
|
+
objectFit: fit,
|
|
30
31
|
pointerEvents: "none",
|
|
31
32
|
zIndex: "1",
|
|
32
33
|
opacity: "1",
|
package/dist/shell/poster.js.map
CHANGED
|
@@ -1 +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
|
|
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\n/**\n * How the poster image maps into the container box (its CSS `object-fit`).\n * `\"fill\"` (default) stretches it edge-to-edge, exactly like the canvas — which renders at the\n * container's own aspect — so a poster captured at that aspect aligns pixel-for-pixel and the\n * poster→canvas handoff has no visible jump. `\"cover\"` crops to preserve the poster's own aspect\n * (pick it for a non-wave / different-aspect placeholder where distortion would look worse).\n */\nexport type PosterFit = \"cover\" | \"contain\" | \"fill\";\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 overlay the container and sit above the canvas;\n * `fit` (default `\"fill\"`) sets how it maps into the box — see {@link PosterFit}.\n * Returns null when there's neither a `src` nor an adoptable image (poster is optional).\n */\nexport function setupPoster(\n container: HTMLElement,\n src?: string,\n fit: PosterFit = \"fill\",\n): 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: fit, // \"fill\" by default so the poster maps into the box like the canvas does\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;;AAsBpB,SAAgB,iBAAiB,WAA8B;CAC7D,IAAI,iBAAiB,SAAS,CAAC,CAAC,aAAa,UAAU,UAAU,MAAM,WAAW;AACpF;;;;;;;AAQA,SAAgB,YACd,WACA,KACA,MAAiB,QACF;CACf,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"}
|