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:
Miguel Ángel
2026-04-07 03:38:59 +02:00
committed by GitHub
parent 0fc9efaf57
commit 066fb146ef
+24 -4
View File
@@ -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