Files
hyperframes/packages/core/src/runtime/window.d.ts
T
Miguel Ángel ac671bdf5c feat(core): add TypeGPU/WebGPU runtime adapter (#755)
* feat(core): add TypeGPU/WebGPU runtime adapter

Adds a deterministic seek adapter for compositions that render with
TypeGPU or raw WebGPU. Follows the same push+poll pattern as the
Three.js adapter:

- Sets `window.__hfTypegpuTime` on every seek so render loops can
  poll it instead of `performance.now()`.
- Dispatches a `"hf-seek"` CustomEvent on `window` so compositions
  can imperatively re-render a single frame at the new seek position.

Compositions listen for the event and update their time uniform:

```js
window.addEventListener("hf-seek", (e) => render(e.detail.time));
```

Works with TypeGPU (docs.swmansion.com/TypeGPU) and raw WebGPU alike.
No assumptions are made about pipeline construction — multiple canvases
or renderers are supported by sharing the same event.

- 9 unit tests, all pass
- wired in init.ts adapter array
- `__hfTypegpuTime` declared in window.d.ts

* fix(core): deduplicate hf-seek dispatch across GPU adapters

Both three and typegpu adapters previously dispatched the same
"hf-seek" CustomEvent independently, causing any composition that
registered a listener to receive two events per seek tick — doubling
per-scrub GPU work even though the renders are idempotent.

Fix: extract a shared `dispatchSeekEvent` helper (seek-dispatch.ts)
that deduplicates by exact float equality within the same synchronous
call stack. Both adapters now call this helper instead of dispatching
directly.

Also adds:
- `resetSeekDispatchState()` export for test isolation
- `beforeEach` reset in three.test.ts and typegpu.test.ts
- New typegpu test: "duplicate seek to same time fires event only once"
- Docstring additions to typegpu.ts: render-mode determinism contract
  (await device.queue.onSubmittedWorkDone()) and navigator.gpu feature
  detection guidance for composition authors

* feat(core): video-texture render compat + TypeGPU skill

Adds the missing pieces for video-backed WebGPU effects in render mode:

- `video-texture-compat.ts`: monkey-patches `GPUQueue.copyExternalImageToTexture`
  to detect the engine's injected `<img class="__render_frame__">` siblings and
  transparently substitute them for `<video>` sources. Headless Chrome can't
  supply decoded video frames to WebGPU, but the engine's pre-extracted frame
  images work. Falls through to the original path in preview mode.

- `patchVideoTextureCompat()` wired in init.ts after adapter array creation.

- `skills/typegpu/SKILL.md`: full authoring guide for TypeGPU/WebGPU compositions
  covering contract, timeline registration, video-backed effects, frosted blur
  via downsample pass, WGSL patterns, and deterministic rendering.

* test(producer): add typegpu-adapter regression test

Self-contained WebGPU composition with:
- Procedural gradient background (no video dependency)
- Animated ring driven by hf-seek time uniform
- Pulsing center glow
- Two GSAP-driven captions testing adapter sync

Verifies the TypeGPU adapter's hf-seek → WebGPU render pipeline
produces deterministic frames. workers: 1 for consistency.

Note: output.mp4 baseline needs to be generated in CI — the local
Docker image can't launch Chrome (ARM/x86 mismatch on Mac).
2026-05-13 03:31:56 +02:00

111 lines
3.7 KiB
TypeScript

import type { RuntimeTimelineMessage, RuntimeTimelineLike } from "./types";
import type { HyperframePickerApi } from "../inline-scripts/pickerApi";
import type { PlayerAPI } from "../core.types";
type ThreeClockLike = {
elapsedTime: number;
oldTime: number;
startTime: number;
getElapsedTime: () => number;
getDelta: () => number;
};
type ThreeAnimationMixerLike = {
setTime?: (time: number) => void;
update: (deltaTime: number) => ThreeAnimationMixerLike;
};
type ThreeLike = {
Clock?: {
prototype: ThreeClockLike;
};
AnimationMixer?: {
prototype: ThreeAnimationMixerLike;
};
};
declare global {
interface Window {
__timelines: Record<string, RuntimeTimelineLike>;
__player?: PlayerAPI;
__clipManifest?: RuntimeTimelineMessage;
__playerReady?: boolean;
__renderReady?: boolean;
__HF_PARITY_MODE?: boolean;
__HF_FPS?: number;
__HF_MAX_DURATION_SEC?: number;
__hfThreeTime?: number;
/**
* Current seek position in seconds, set by the TypeGPU/WebGPU adapter.
* Poll this from your WebGPU render loop instead of `performance.now()`
* to get the deterministic seek position.
*
* Also listen for the `"hf-seek"` CustomEvent on `window` for an
* imperative push signal: `window.addEventListener("hf-seek", e => render(e.detail.time))`.
*/
__hfTypegpuTime?: number;
__HF_PICKER_API?: HyperframePickerApi;
gsap?: {
timeline: (params?: { paused?: boolean }) => RuntimeTimelineLike;
ticker?: {
tick: () => void;
};
};
THREE?: ThreeLike;
/**
* Global anime.js instance (set by including the anime.iife.min.js script).
* The adapter uses `anime.running` for auto-discovery.
*/
anime?: {
(params: unknown): unknown;
timeline?: (params?: unknown) => unknown;
running: unknown[];
};
/**
* anime.js instances registered by compositions.
* The adapter seeks all instances when the player is seeked.
*
* Push your animation or timeline instance here:
* window.__hfAnime = window.__hfAnime || [];
* window.__hfAnime.push(anim);
*/
__hfAnime?: unknown[];
/**
* Global lottie-web instance (set by including the lottie.min.js script).
* The adapter uses `lottie.getRegisteredAnimations()` for auto-discovery.
*/
lottie?: {
loadAnimation: (params: unknown) => unknown;
getRegisteredAnimations: () => unknown[];
};
/**
* Lottie animation instances registered by compositions.
* The adapter seeks all instances when the player is seeked.
*
* Push your animation instance here after calling `lottie.loadAnimation()`:
* window.__hfLottie = window.__hfLottie || [];
* window.__hfLottie.push(anim);
*/
__hfLottie?: unknown[];
/**
* Render-time variable overrides injected by the engine when the user
* passes `hyperframes render --variables '<json>'`. Read indirectly via
* `window.__hyperframes.getVariables()` (or the named `getVariables`
* export from `@hyperframes/core`), which merges these over the
* declared defaults from `<html data-composition-variables="...">`.
*/
__hfVariables?: Record<string, unknown>;
/**
* Per-instance, pre-merged variables for sub-compositions. Keyed by the
* sub-composition's `data-composition-id`. Populated by the runtime
* composition loader at mount time: layers the host element's
* `data-variable-values` over the sub-comp's declared defaults so the
* scoped `getVariables()` exposed by `compositionScoping.ts` returns the
* resolved values for the instance currently executing.
*/
__hfVariablesByComp?: Record<string, Record<string, unknown>>;
}
}
export {};