mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
* 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).
111 lines
3.7 KiB
TypeScript
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 {};
|