Skip to content

Commit 5655dab

Browse files
feat: allow clip animation + ship <hyperframes-player> web component (heygen-com#209)
## Summary Two independent initiatives that improve agent DX and expand HyperFrames' reach. ### Initiative 1: Fix the Clip Animation Footgun - `gsap_animates_clip_element` lint rule now uses smart detection — only errors when GSAP animates `visibility` or `display` on a clip element - All other properties (opacity, transform, x, y, scale, etc.) are allowed silently - This was the heygen-com#1 agent failure in QA (10/10 agents hit it on v0.2.1) ### Initiative 2: `<hyperframes-player>` Web Component - New `@hyperframes/player` package — zero dependencies, 3.3KB gzipped - Iframe-based web component with Shadow DOM for perfect isolation - Video-like API: `play()`, `pause()`, `seek()`, `currentTime`, `duration`, events - Controls overlay with play/pause, scrubber (mouse + touch), time display, auto-hide - Full docs page at `docs/packages/player.mdx` ## Before / After ### Clip animation lint **Before (10/10 agents hit this):** ``` ✗ gsap_animates_clip_element: GSAP animation targets a clip element. Selector "#title" resolves to element <div id="title" class="clip">. The framework manages clip visibility — animate an inner wrapper instead. Fix: Wrap content in a child <div> and target that with GSAP. ``` **After (only errors on actual conflicts):** ``` # This passes lint — no error: tl.from("#title", { opacity: 0, y: -50, scale: 0.8 }, 0); # This still errors — actual conflict with runtime: tl.to("#title", { visibility: "hidden" }, 3); ✗ gsap_animates_clip_element: GSAP animation sets visibility on a clip element. Fix: Remove the visibility/display tween. Use opacity for fade effects. ``` ### Embeddable player **Before:** No way to embed a composition in a web page. **After:** ```html <script src="https://cdn.jsdelivr.net/npm/@hyperframes/player"></script> <hyperframes-player src="./composition/index.html" controls></hyperframes-player> ``` ```js const player = document.querySelector('hyperframes-player'); player.play(); player.pause(); player.seek(2.5); player.addEventListener('ready', (e) => console.log('Duration:', e.detail.duration)); ``` ## Test plan - [x] 427 core tests pass (20 GSAP lint tests with smart detection) - [x] 7 player tests pass (formatTime + element registration) - [x] TypeScript compiles cleanly (core + player) - [x] Lint: GSAP animating clip with safe props → 0 errors - [x] Lint: GSAP animating clip with `visibility` → 1 error (correct) - [x] Player builds to 3.3KB gzipped ESM - [x] Lockfile updated for CI - [x] Docs page added at `docs/packages/player.mdx`
1 parent baa3d81 commit 5655dab

18 files changed

Lines changed: 1332 additions & 25 deletions

File tree

CLAUDE.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ packages/
5858
cli/ → hyperframes CLI (create, preview, lint, render)
5959
core/ → Types, parsers, generators, linter, runtime, frame adapters
6060
engine/ → Seekable page-to-video capture engine (Puppeteer + FFmpeg)
61+
player/ → Embeddable <hyperframes-player> web component
6162
producer/ → Full rendering pipeline (capture + encode + audio mix)
6263
studio/ → Browser-based composition editor UI
6364
```
@@ -94,6 +95,7 @@ When adding a new CLI command:
9495
## Key Concepts
9596

9697
- **Compositions** are HTML files with `data-*` attributes defining timeline, tracks, and media
98+
- **Clips** can be animated directly with GSAP. The only restriction: don't animate `visibility` or `display` on clip elements — the runtime manages those.
9799
- **Frame Adapters** bridge animation runtimes (GSAP, Lottie, CSS) to the capture engine
98100
- **Producer** orchestrates capture → encode → audio mix into final MP4
99101
- **BeginFrame rendering** uses `HeadlessExperimental.beginFrame` for deterministic frame capture
@@ -185,3 +187,29 @@ Use `npx hyperframes tts --list` for the full set, or pass any valid Kokoro voic
185187

186188
- Python 3.8+ (auto-installs `kokoro-onnx` package on first run)
187189
- Model downloads automatically on first use (~311 MB model + ~27 MB voices, cached in `~/.cache/hyperframes/tts/`)
190+
191+
## Embeddable Player
192+
193+
The `@hyperframes/player` package provides a `<hyperframes-player>` web component for embedding
194+
compositions in any web page. Zero dependencies, works with any framework.
195+
196+
### Quick reference
197+
198+
```html
199+
<!-- Load the player (CDN or npm) -->
200+
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/player"></script>
201+
202+
<!-- Embed a composition -->
203+
<hyperframes-player src="./my-composition/index.html" controls></hyperframes-player>
204+
```
205+
206+
### JavaScript API
207+
208+
```js
209+
const player = document.querySelector("hyperframes-player");
210+
player.play();
211+
player.pause();
212+
player.seek(2.5);
213+
console.log(player.currentTime, player.duration, player.paused);
214+
player.addEventListener("ready", (e) => console.log("Duration:", e.detail.duration));
215+
```

Dockerfile.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ ENV PATH="/root/.bun/bin:$PATH"
6868
COPY package.json bun.lock ./
6969
COPY packages/core/package.json packages/core/package.json
7070
COPY packages/engine/package.json packages/engine/package.json
71+
COPY packages/player/package.json packages/player/package.json
7172
COPY packages/producer/package.json packages/producer/package.json
7273
COPY packages/cli/package.json packages/cli/package.json
7374
COPY packages/studio/package.json packages/studio/package.json

bun.lock

Lines changed: 16 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"pages": [
6767
"packages/core",
6868
"packages/engine",
69+
"packages/player",
6970
"packages/producer",
7071
"packages/studio",
7172
"packages/cli"

docs/packages/player.mdx

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
---
2+
title: "@hyperframes/player"
3+
description: "Embeddable web component for playing HyperFrames compositions in any web page."
4+
---
5+
6+
The player package provides a `<hyperframes-player>` custom element that embeds a HyperFrames composition anywhere — in any framework or plain HTML. Zero dependencies, 3KB gzipped.
7+
8+
```bash
9+
npm install @hyperframes/player
10+
```
11+
12+
## When to Use
13+
14+
**Use `@hyperframes/player` when you need to:**
15+
- Embed a rendered composition in a website, dashboard, or app
16+
- Add a video-like player to a landing page or product demo
17+
- Show compositions in documentation or blog posts
18+
19+
**Use a different package if you want to:**
20+
- Edit compositions interactively — use the [studio](/packages/studio)
21+
- Preview during development — use the [CLI](/packages/cli) (`npx hyperframes preview`)
22+
- Render to MP4 — use the [CLI](/packages/cli) or [producer](/packages/producer)
23+
24+
## Quick Start
25+
26+
### Via CDN
27+
28+
```html
29+
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/player"></script>
30+
31+
<hyperframes-player
32+
src="./my-composition/index.html"
33+
controls
34+
autoplay
35+
muted
36+
style="width: 100%; max-width: 800px; aspect-ratio: 16/9"
37+
></hyperframes-player>
38+
```
39+
40+
### Via npm
41+
42+
```js
43+
import '@hyperframes/player';
44+
```
45+
46+
```html
47+
<hyperframes-player src="/compositions/intro.html" controls></hyperframes-player>
48+
```
49+
50+
## HTML Attributes
51+
52+
| Attribute | Type | Default | Description |
53+
|-----------|------|---------|-------------|
54+
| `src` | string | required | URL or relative path to composition HTML |
55+
| `width` | number | 1920 | Composition width in pixels |
56+
| `height` | number | 1080 | Composition height in pixels |
57+
| `controls` | boolean | false | Show playback controls overlay |
58+
| `autoplay` | boolean | false | Start playing on load |
59+
| `loop` | boolean | false | Loop playback |
60+
| `muted` | boolean | true | Mute audio (required for autoplay in most browsers) |
61+
| `poster` | string || Image URL to show before first play |
62+
| `playback-rate` | number | 1 | Playback speed multiplier |
63+
64+
## JavaScript API
65+
66+
The player mirrors the native `<video>` element API:
67+
68+
```js
69+
const player = document.querySelector('hyperframes-player');
70+
71+
// Playback
72+
player.play();
73+
player.pause();
74+
player.seek(2.5); // seek to 2.5 seconds
75+
76+
// Properties
77+
player.currentTime; // number — current position in seconds
78+
player.currentTime = 5; // seek to 5 seconds
79+
player.duration; // number — total duration
80+
player.paused; // boolean
81+
player.ready; // boolean — true after composition loads
82+
player.playbackRate; // number — get/set speed
83+
player.muted; // boolean — get/set mute
84+
player.loop; // boolean — get/set loop
85+
```
86+
87+
## Events
88+
89+
```js
90+
const player = document.querySelector('hyperframes-player');
91+
92+
player.addEventListener('ready', (e) => {
93+
console.log('Duration:', e.detail.duration);
94+
});
95+
96+
player.addEventListener('timeupdate', (e) => {
97+
console.log('Time:', e.detail.currentTime);
98+
});
99+
100+
player.addEventListener('play', () => console.log('Playing'));
101+
player.addEventListener('pause', () => console.log('Paused'));
102+
player.addEventListener('ended', () => console.log('Ended'));
103+
player.addEventListener('error', (e) => console.error(e.detail.message));
104+
```
105+
106+
| Event | Detail | Description |
107+
|-------|--------|-------------|
108+
| `ready` | `{ duration }` | Composition loaded and timeline discovered |
109+
| `timeupdate` | `{ currentTime }` | Fires during playback (~30fps) |
110+
| `play` || Playback started |
111+
| `pause` || Playback paused |
112+
| `ended` || Playback reached end |
113+
| `error` | `{ message }` | Load or runtime error |
114+
115+
## Framework Examples
116+
117+
### React
118+
119+
```jsx
120+
import '@hyperframes/player';
121+
122+
function VideoPreview({ src }) {
123+
return (
124+
<hyperframes-player
125+
src={src}
126+
controls
127+
style={{ width: '100%', maxWidth: 800 }}
128+
/>
129+
);
130+
}
131+
```
132+
133+
### Vue
134+
135+
```vue
136+
<template>
137+
<hyperframes-player :src="compositionUrl" controls />
138+
</template>
139+
140+
<script setup>
141+
import '@hyperframes/player';
142+
const compositionUrl = './compositions/intro.html';
143+
</script>
144+
```
145+
146+
### Programmatic
147+
148+
```js
149+
import '@hyperframes/player';
150+
151+
const player = document.createElement('hyperframes-player');
152+
player.src = './my-composition/index.html';
153+
player.controls = true;
154+
player.addEventListener('ready', () => player.play());
155+
document.getElementById('player-container').appendChild(player);
156+
```
157+
158+
## Architecture
159+
160+
The player uses an iframe inside a Shadow DOM container. This provides:
161+
162+
- **Isolation** — composition CSS/JS can't leak into or conflict with your page
163+
- **Security** — iframe sandbox restricts composition capabilities
164+
- **Scaling** — auto-scales the composition to fit the player's container via CSS transforms
165+
166+
The player communicates with the composition via the HyperFrames runtime bridge protocol (`postMessage`). Existing compositions work without modification.
167+
168+
## Controls
169+
170+
When the `controls` attribute is present, a minimal overlay appears at the bottom:
171+
172+
- **Play/Pause** button (left)
173+
- **Scrub bar** with drag support (mouse + touch)
174+
- **Time display** showing current / total duration (right)
175+
- Auto-hides after 3 seconds of inactivity, reappears on hover

packages/cli/src/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const isHelp = process.argv.includes("--help") || process.argv.includes("-h");
2525

2626
const subCommands = {
2727
init: () => import("./commands/init.js").then((m) => m.default),
28+
play: () => import("./commands/play.js").then((m) => m.default),
2829
preview: () => import("./commands/preview.js").then((m) => m.default),
2930
render: () => import("./commands/render.js").then((m) => m.default),
3031
lint: () => import("./commands/lint.js").then((m) => m.default),

0 commit comments

Comments
 (0)