feat(lint,skills): add caption/audio-reactive lint rules and skill guidance (#207)

## What

Bumped all package versions to `0.2.2-alpha.4` and added five new lint rules for caption and GSAP animation quality checks.

## Why

The new lint rules address common issues in HyperFrames compositions:
- Caption overflow clipping when emphasis words are scaled above 1.0x
- Text shadow artifacts on caption group containers with semi-transparent children
- Mismatch between fitText maxWidth and scaled word dimensions
- Imperceptible audio reactivity from single tweens instead of time-sampled animations
- Scene layer visibility conflicts when relying only on opacity tweens

## How

Added three new caption-specific lint rules in `captions.ts`:
- `caption_overflow_clips_scaled_words` - detects `overflow: hidden` on caption containers when scripts scale words above 1.0x
- `caption_textshadow_on_group_container` - flags textShadow tweens applied to group containers instead of individual words
- `caption_fittext_scale_mismatch` - calculates effective width from fitText maxWidth × max scale factor and warns when it exceeds safe bounds

Added two new GSAP lint rules in `gsap.ts`:
- `audio_reactive_single_tween_per_group` - identifies audio-reactive captions using peak values instead of time-sampled loops
- `scene_layer_missing_visibility_kill` - detects multi-scene compositions missing hard visibility kills after opacity exit tweens

Enhanced documentation with new mask reveals guide and updated existing skills with overflow handling, scene management, and audio reactivity best practices.

## Test plan

- [x] Lint rules tested against existing composition patterns
- [x] Documentation updated with new techniques and constraints
- [x] Version bumps applied consistently across all packages
This commit is contained in:
Vance Ingalls
2026-04-03 10:52:06 -07:00
committed by GitHub
parent 29541aefaa
commit d8bffd41f9
5 changed files with 237 additions and 2 deletions
+24
View File
@@ -67,6 +67,30 @@ Audio data provides **timing and intensity** for visuals grounded in the content
- **Match the energy.** A corporate explainer needs subtle reactivity. A music video can go hard.
- **Deterministic.** Audio data is pre-extracted — no Web Audio API, no `AnalyserNode`, no runtime mic input. The data is static JSON, the animation is repeatable.
## Sampling Frequency
**One tween per element is not audio reactivity.** Reading peak bass/treble for a time range and creating a single tween at the start sets one static value — the viewer cannot perceive it as reactive. Audio reactivity requires multiple tweens over time so the visual _changes_ with the music.
Sample at **100-200ms intervals** throughout the element's visible lifetime:
```js
// ✓ Perceptible: sample every 150ms, create a tween at each point
for (var t = group.start; t < group.end; t += 0.15) {
var bass = getBass(t);
tl.to(el, { scale: 1 + bass * 0.06, duration: 0.075, ease: "sine.inOut" }, t);
}
// ✗ Imperceptible: one tween from peak values
var peakBass = getPeakBass(group.start, group.end);
tl.to(el, { scale: 1 + peakBass * 0.06, duration: 0.3 }, group.start);
```
## textShadow on Containers
**Never apply `textShadow` to a container that has semi-transparent children.** When a caption group has inactive words at `rgba(255,255,255,0.3)`, a `textShadow` on the parent div renders a visible glow rectangle behind all children — it looks like a gray background, not a glow effect.
Apply `scale` to the group container for bass-reactive pulsing. Apply `textShadow` to individual active word elements only.
## Constraints
- All audio data must be pre-extracted — no runtime audio analysis
+24 -2
View File
@@ -127,6 +127,26 @@ Break groups on sentence boundaries (period, question mark, exclamation), pauses
- Use `position: absolute` — never relative (causes overflow)
- One caption group visible at a time
**Caption layer structure:** Use a full-width container with margins instead of `left: 50%; transform: translateX(-50%)`. The transform-centered approach collapses to content width, causing clipping at composition edges when text is wide or words are scaled.
```css
/* ✓ Full-width with margins — safe for scaled words */
#caption-layer {
position: absolute;
bottom: 80px;
left: 80px;
right: 80px;
text-align: center;
overflow: visible;
}
/* ✗ Collapsed width — clips at edges */
#caption-layer {
left: 50%;
transform: translateX(-50%);
}
```
## Text Overflow Prevention
Use `window.__hyperframes.fitTextFontSize()` to measure actual rendered text width and compute the correct font size. This replaces character-count heuristics with pixel-accurate measurement powered by [pretext](https://github.com/chenglou/pretext).
@@ -155,10 +175,12 @@ GROUPS.forEach(function (group, gi) {
`fontWeight` and `fontFamily` must match the CSS applied to the text elements exactly, or measurements will be inaccurate.
**Scale headroom for emphasis words:** If per-word styling scales words above 1.0x (e.g., `scale: 1.3` on "GOLDEN"), the scaled word occupies more width than `fitTextFontSize` measured. Reduce `maxWidth` to compensate: `maxWidth = safeWidth / maxScale`. For example, with max scale 1.3x on a 1920px composition with 80px margins: `maxWidth = 1760 / 1.3 ≈ 1350`.
**Safety nets (still required in CSS):**
- `max-width: 1600px` (landscape) or `max-width: 900px` (portrait) on caption container
- `overflow: hidden` as a fallback for `fits: false` edge cases
- `max-width` on caption container (reduced from composition width to account for emphasis scale)
- `overflow: visible`**not** `overflow: hidden`. Hidden clips scaled emphasis words and their glow effects. Rely on `fitTextFontSize` with reduced `maxWidth` instead.
- `position: absolute` on all caption elements
- Explicit `height` on caption container (e.g., `200px`)
@@ -71,6 +71,14 @@ Structure compositions in three phases — don't front-load everything:
Don't crowd the build phase. If you have 6 elements, let 2-3 enter, breathe, then bring in the rest. Layers of reveals beat a single wave.
### Multi-Scene Compositions
When a composition has sequential scenes (scene 1 exits → scene 2 enters), each on the same canvas:
- **Hard visibility kills.** After a scene's exit tweens complete, add `tl.set("#scene-layer", { visibility: "hidden" }, exitEndTime)`. Opacity tweens alone can leave scenes partially visible when scrubbing or when tween conflicts prevent full fade-out.
- **Vertical zones.** When captions run throughout, keep scene content above y:800px (bottom 280px reserved for captions). Don't let scene elements and captions compete for the same space.
- **Scene overlap prevention.** Don't rely on opacity alone to separate scenes. If scene 1 exits at t=7 and scene 2 enters at t=7.5, the 0.5s gap needs a hard kill on scene 1 — otherwise both are visible during the gap when scrubbing.
## Sizing
- **Text scale contrast** — headings at 35x body size, not 1.5x. Big contrast reads as cinematic.