mirror of
https://github.com/hanzoai/frames.git
synced 2026-09-24 00:08:03 +00:00
fix(skills): add rendering guardrails to hyperframes skill (#217)
## Summary Adds critical rendering constraints to the `hyperframes` skill discovered from eval analysis of 27 agent-generated compositions. These guardrails prevent agents from producing compositions that technically work but render poorly. ## What it fixes | Rule Added | Eval Prompts Affected | Issue | | --- | --- | --- | | Ban `repeat: -1` | #20 loading-spinner (2.0/5) | Infinite timeline broke capture engine | | Ban async timeline construction | #16 particle-logo (2.6/5) | Timeline empty at capture time | | Min font size 16px (labels), 20px (body) | #7, #8, #13, #14, #15, #19 | Illegible text after encoding | | Ban full-screen dark linear gradients | #3, #5, #10, #14 | H.264 color banding | | `<link>` fonts over CSS `@import` | #7, #24 | Font loading race conditions | ## Changes - **Rules section**: Added `repeat: -1` ban, async timeline ban, items 8-9 to "Never do" list - **Typography section**: Expanded font size guidance with specific minimums per text role (headlines, body, labels) - **New "Backgrounds and Color" section**: Guidance on avoiding gradient banding - **Output Checklist**: 5 new items covering all new constraints ## Test plan - [ ] Run eval with updated skill and compare avg quality scores - [x] Skill renders correctly in `/hyperframes` invocation
This commit is contained in:
@@ -92,12 +92,16 @@ Video must be `muted playsinline`. Audio is always a separate `<audio>` element:
|
||||
|
||||
## Rules (Non-Negotiable)
|
||||
|
||||
**Deterministic:** No `Math.random()`, `Date.now()`, or time-based logic.
|
||||
**Deterministic:** No `Math.random()`, `Date.now()`, or time-based logic. Use a seeded PRNG if you need pseudo-random values (e.g. mulberry32).
|
||||
|
||||
**GSAP:** Only animate visual properties (`opacity`, `x`, `y`, `scale`, `rotation`, `color`, `backgroundColor`, `borderRadius`, transforms). Do NOT animate `visibility`, `display`, or call `video.play()`/`audio.play()`.
|
||||
|
||||
**Animation conflicts:** Never animate the same property on the same element from multiple timelines simultaneously.
|
||||
|
||||
**No `repeat: -1`:** Infinite-repeat timelines break the capture engine. Calculate the exact repeat count from composition duration: `repeat: Math.ceil(duration / cycleDuration) - 1`.
|
||||
|
||||
**Synchronous timeline construction:** Never build timelines inside `async`/`await`, `setTimeout`, or Promises. The capture engine reads `window.__timelines` synchronously after page load. If you need fonts loaded first, use a synchronous `document.fonts.load()` call or rely on `font-display: block` — the engine waits for the page `load` event.
|
||||
|
||||
**Never do:**
|
||||
|
||||
1. Forget `window.__timelines` registration
|
||||
@@ -107,16 +111,27 @@ Video must be `muted playsinline`. Audio is always a separate `<audio>` element:
|
||||
5. Animate video element dimensions — animate a wrapper div
|
||||
6. Call play/pause/seek on media — framework owns playback
|
||||
7. Create a top-level container without `data-composition-id`
|
||||
8. Use `repeat: -1` on any timeline or tween — always finite repeats
|
||||
9. Build timelines asynchronously (inside `async`, `setTimeout`, `Promise`)
|
||||
|
||||
## Typography and Assets
|
||||
|
||||
- Every composition loads its own fonts (`@import` or `@font-face`)
|
||||
- Use `font-display: block` for local fonts
|
||||
- Load fonts via `<link>` tags with `display=block` in `<head>`, NOT via CSS `@import` — `@import` is async and may not complete before the first frame capture
|
||||
- Use `font-display: block` for `@font-face` declarations
|
||||
- Add `crossorigin="anonymous"` to external media
|
||||
- Minimum readable text: 20px landscape, 18px portrait
|
||||
- **Minimum font sizes for rendered video (1080p at DPR 1):**
|
||||
- Body/label text: 20px minimum (landscape), 18px minimum (portrait)
|
||||
- Data labels, axis labels, footnotes: 16px minimum — anything smaller becomes illegible after encoding
|
||||
- Headlines: 36px+ recommended
|
||||
- Avoid sub-14px text entirely — it will be unreadable in the final MP4
|
||||
- For dynamic text overflow, use `window.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight })` — returns `{ fontSize, fits }`
|
||||
- All files live at the project root alongside `index.html`; sub-compositions use `../`
|
||||
|
||||
### Backgrounds and Color
|
||||
|
||||
- **Avoid full-screen linear gradients on dark backgrounds** — H.264 encoding creates visible color banding. Prefer: solid colors, radial gradients with limited range, or subtle noise/texture overlays to break up banding.
|
||||
- For dark themes, use solid `#000` or `#0A0A0A` with localized radial glows rather than a linear gradient spanning the full viewport.
|
||||
|
||||
## Editing Existing Compositions
|
||||
|
||||
- Read the full composition first — match existing fonts, colors, animation patterns
|
||||
@@ -129,6 +144,11 @@ Video must be `muted playsinline`. Audio is always a separate `<audio>` element:
|
||||
- [ ] Compositions in own HTML files, loaded via `data-composition-src`
|
||||
- [ ] `<template>` wrapper on sub-compositions
|
||||
- [ ] `window.__timelines` registered for every composition
|
||||
- [ ] Timeline construction is synchronous (no async/await wrapping timeline code)
|
||||
- [ ] No `repeat: -1` on any tween or nested timeline
|
||||
- [ ] No text below 16px (data labels, footnotes) or 20px (body text)
|
||||
- [ ] No full-screen linear dark gradients (use radial or solid + localized glow)
|
||||
- [ ] Fonts loaded via `<link>` with `display=block`, not CSS `@import`
|
||||
- [ ] 100% deterministic
|
||||
- [ ] Each composition includes GSAP script tag
|
||||
- [ ] `npx hyperframes lint` and `npx hyperframes validate` both pass
|
||||
|
||||
Reference in New Issue
Block a user