mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat: allow clip animation + ship <hyperframes-player> web component (#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 #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`
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
export const PLAYER_STYLES = /* css */ `
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
.hfp-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.hfp-iframe {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hfp-poster {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
object-fit: contain;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hfp-controls {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||
color: #fff;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-size: 13px;
|
||||
z-index: 10;
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.hfp-controls.hfp-hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hfp-play-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.hfp-play-btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.hfp-play-btn svg,
|
||||
.hfp-play-btn svg * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hfp-scrubber {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hfp-scrubber:hover {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.hfp-progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hfp-time {
|
||||
flex-shrink: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
opacity: 0.9;
|
||||
}
|
||||
`;
|
||||
|
||||
export const PLAY_ICON = `<svg width="24" height="24" viewBox="0 0 18 18" fill="currentColor"><polygon points="4,2 16,9 4,16"/></svg>`;
|
||||
export const PAUSE_ICON = `<svg width="24" height="24" viewBox="0 0 18 18" fill="currentColor"><rect x="3" y="2" width="4" height="14"/><rect x="11" y="2" width="4" height="14"/></svg>`;
|
||||
Reference in New Issue
Block a user