Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"files": 17
},
"hyperframes-animation": {
"hash": "3947516ec450f3aa",
"files": 121
"hash": "34856794427ae403",
"files": 123
},
"hyperframes-audio": {
"hash": "b39bac771e873eae",
Expand Down
1 change: 1 addition & 0 deletions skills/hyperframes-animation/adapters/css-animations.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ Use CSS custom properties to avoid duplicating keyframes:
- Decorative loops with a known repeat count.
- Mask, glow, shimmer, grain, and subtle parallax layers.
- Simple one-element entrances where a full JS timeline would be excessive.
- A physical spring settle without GSAP: bake the closed-form spring into a `linear()` timing function with `bakeSpring()` (`gsap-easing-and-stagger.md` → Spring Eases → bakeSpring); the duration is still the helper's.

## Avoid

Expand Down
125 changes: 119 additions & 6 deletions skills/hyperframes-animation/adapters/gsap-easing-and-stagger.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,12 @@ tl.fromTo(
);
```

| dampingFraction | overshoot | register |
| ----------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **1.0 (default)** | none (monotone) | The house settle — the exact curve `power3.out` approximates. Product / enterprise / serious tone. |
| 0.80–0.85 | ~1–1.5% | "Alive, not bouncy" — the iOS system default register. The overshoot is felt, not seen. |
| 0.60–0.70 | ~5–10% | Explicitly-playful ONLY (same rule as `back.out`, which this replaces — a spring's second-order settle reads physical where `back` reads cartoon). |
| < 0.55 | > 12% | Don't. Cartoon-wobble territory. |
| dampingFraction | overshoot | register |
| ----------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **1.0 (default)** | none (monotone) | The house settle — the exact curve `power3.out` approximates. Product / enterprise / serious tone. |
| 0.80–0.85 | ~1–1.5% | "Alive, not bouncy" — the iOS system default register. The overshoot is felt, not seen. |
| 0.60–0.70 | ~5–10% | Explicitly-playful ONLY (same rule as `back.out`, which this replaces — a spring's second-order settle reads physical where `back` reads cartoon). |
| < 0.55 | > 12% | Don't — as an entrance. Cartoon-wobble territory for an arrival. The one sanctioned home for this band is impact **recovery** (Recovery Registers below), never the travel itself. |

| response | duration (ζ=1) | feel |
| --------- | -------------- | ------------------------------------------------------------ |
Expand All @@ -135,6 +135,119 @@ Craft notes:
- **At ζ<1, overshooting curves go on transforms only** — never on `opacity` (it would push past 1) or color. Split opacity onto its own `power2.out` tween at the same timeline position.
- **Doctrine unchanged**: ζ below ~0.8 is still the rare, explicitly-playful exception (`rules/spring-pop-entrance.md`). The default of this section is ζ=1 — real spring physics is not a license for bounce.

### Named Registers

Feel words over the same helper — a lookup table, not a second API. Each register is a `{ response, dampingFraction }` pair that shipped through a deterministic reference build (24 s reel, 2026-09-08, running this helper: every spring tween consuming the helper's duration verbatim, double-render bit-identical). The durations below are what `springEase` returns for the pair; take them from the helper, never from this table.

```javascript
const SPRING_REGISTERS = {
// Entrance / settle voices — arrivals, lockups, hero landings (ζ ≥ 0.8 per the doctrine above).
snappy: { response: 0.22, dampingFraction: 0.9 }, // tight snap: chips, small UI
"heavy-settle": { response: 0.8, dampingFraction: 1 }, // weighted lockup — the settle IS the shot
// Recovery voices — ONLY the spring-back after a contact (see Recovery Registers). Never an arrival.
bouncy: { response: 0.4, dampingFraction: 0.5 },
wobbly: { response: 0.5, dampingFraction: 0.28 },
};
const spring = (feel) => springEase(SPRING_REGISTERS[feel]);

const land = spring("heavy-settle");
tl.fromTo(
"#lockup",
{ y: 80, opacity: 0 },
{ y: 0, opacity: 1, duration: land.duration, ease: land.ease },
1.2,
);
```

| register | response | ζ | duration (from the helper) | use |
| -------------- | -------- | ---- | -------------------------- | ---------------------------------------------------------- |
| `snappy` | 0.22 | 0.90 | ≈ 0.29s | tight snap — chips, badges, small UI; ~0.1% overshoot |
| `heavy-settle` | 0.80 | 1.00 | ≈ 1.18s | weighted hero landing, end card, wordmark lockup; monotone |

#### Recovery Registers (impact recovery only)

`bouncy` and `wobbly` sit inside the "< 0.55 — Don't" band on purpose. They are **deformation-recovery** voices — the spring-back of a body after it lands and squashes, a control after release, the settle-back of a chain of followers after the leader's arrival — not entrance eases. A recovery starts _at_ the contact frame and moves the element by a small fraction of the arrival travel — a few percent of a large element's height, up to about its own height for a chip or badge — never the travel itself, so a 16–40% overshoot of that small displacement reads as material (rubber, jelly, drag), where the same overshoot on the arrival travel reads as cartoon.

| register | response | ζ | overshoot | duration (from the helper) | recovery context |
| -------- | -------- | ---- | --------- | -------------------------- | ------------------------------------------------------------------------- |
| `bouncy` | 0.40 | 0.50 | ~16% | ≈ 0.81s | soft-body landing recovery, a released press, a chain's settle-back move |
| `wobbly` | 0.50 | 0.28 | ~40% | ≈ 1.90s | rubber / jelly tier — the wobble _is_ the material read; rare, deliberate |

- The arrival keeps the entrance doctrine (ζ ≥ 0.8, or `power3.out`); only the post-contact recovery may go `bouncy` / `wobbly`.
- Recovery goes on a transform or a deformation proxy that was just displaced — never on `opacity`, never on the arrival travel.
- Longer flight in a chain or trail comes from `response`, not from the duration (next section).

### Duration Is an Output — the Greppable Criterion

`springEase` returns the settle time and the tween consumes it verbatim, so the audit is mechanical: a spring tween's `duration:` is `<spring>.duration` with **no arithmetic on it**.

```bash
grep -nE 'duration\s*\*|\*\s*[A-Za-z_.]*duration' index.html # spring tweens: zero hits
```

The `css` / `waapi` lanes need milliseconds: cast once through a helper (`ms(s.duration)`, next section) so a unit conversion never reads as arithmetic on a spring duration. A longer or shorter flight comes from `response` — `springEase({ ...SPRING_REGISTERS.bouncy, response: 0.4 * 1.4 })` — the same normalized curve over a physics-derived settle. A stretched duration draws the identical pixels (it re-times the same curve) and is still the anti-pattern the grep catches: it hides the physics parameter from the reader and from the next edit, and it is the first thing to drift when a beat gets re-timed.

### bakeSpring — the Same Spring in the CSS-Keyframes and WAAPI Lanes

`@keyframes` and `element.animate()` can't take a function ease, but both accept CSS `linear()` — a piecewise-linear easing with explicit stops. Bake the spring into one at setup; the curve is then a pure function of the animation's own time, so the `css` and `waapi` adapters seek it like any other keyframe animation (`css-animations.md`, `waapi.md`).

```javascript
// Curvature-adaptive sampling: stops cluster where the curve bends (the overshoot lobes).
function bakeSpring(spring, { maxPts = 75 } = {}) {
const dense = 400;
const pts = [[0, 0]];
const curv = [];
for (let i = 1; i < dense; i++) {
const y0 = spring.ease((i - 1) / dense);
const y1 = spring.ease(i / dense);
const y2 = spring.ease((i + 1) / dense);
curv.push(Math.abs(y2 - 2 * y1 + y0)); // second difference ≈ local curvature
}
const total = curv.reduce((a, b) => a + b, 0) || 1;
const budget = maxPts - 2;
let acc = 0;
for (let i = 1; i < dense; i++) {
acc += (curv[i - 1] / total) * budget;
if (acc >= 1) {
pts.push([i / dense, spring.ease(i / dense)]);
acc = 0;
}
}
pts.push([1, 1]);
const css = `linear(${pts.map(([x, y]) => `${y.toFixed(5)} ${(x * 100).toFixed(3)}%`).join(", ")})`;
return { points: pts, css }; // at most maxPts stops; the four registers land at 57–61 with the default
}

const ms = (seconds) => Math.round(seconds * 1000); // the css / waapi lanes take milliseconds — one unit cast, kept out of the tween sites
const s = springEase(SPRING_REGISTERS.snappy);
const baked = bakeSpring(s);
// CSS lane — the duration is still the helper's:
style.textContent = `#chip { animation: chip-in ${ms(s.duration)}ms ${baked.css} 200ms 1 both; }`;
// WAAPI lane:
chip
.animate([{ transform: "translateY(-220px)" }, { transform: "translateY(0)" }], {
duration: ms(s.duration),
delay: 200,
easing: baked.css,
fill: "both",
iterations: 1,
})
.pause();
```

Parity against the analytic ease is a function of travel: the bake error is a fraction of the curve, so it grows with the distance the element moves. Computed against this file's `springEase` (script sampling of the bake at 4000 points, 2026-09-08):

| register | points at the default cap | worst error, 1000 px travel | at 220 px | with `maxPts: 200` |
| -------------- | ------------------------- | --------------------------- | --------- | ------------------ |
| `snappy` | 61 | 2.3 px | 0.5 px | 115 pts → 0.5 px |
| `heavy-settle` | 58 | 2.5 px | 0.5 px | 110 pts → 0.6 px |
| `bouncy` | 61 | 3.0 px | 0.7 px | 123 pts → 1.0 px |
| `wobbly` | 57 | 7.3 px | 1.6 px | 107 pts → 1.9 px |

In the render itself (one composition, the `wobbly` register at `maxPts: 200`, the GSAP analytic ease beside the CSS-keyframes and WAAPI lanes on the same travel, 1-px edge measurement on every frame of the tween): both baked lanes stay within **1 px** of the analytic chip over 220 px of travel and within **2 px** over 900 px.

Rule of thumb: keep `|baked − analytic| × travel ≤ 2 px`. The default cap holds `snappy`, `heavy-settle` and `bouncy` to that tolerance up to roughly 650 px of travel (the two entrance registers to about 800 px); the `wobbly` register's lobes want `maxPts: 200` for the same tolerance at 1000 px. Raise `maxPts` rather than accepting a visible step.

## Stagger

```javascript
Expand Down
1 change: 1 addition & 0 deletions skills/hyperframes-animation/adapters/waapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ document.querySelectorAll(".token").forEach((token, index) => {
- Lightweight DOM motion where CSS keyframes are too rigid and GSAP is unnecessary.
- Generated animations from structured data.
- Simple timelines that can be represented as keyframes, delays, and offsets.
- A physical spring settle as the `easing` string: bake the closed-form spring into `linear()` with `bakeSpring()` (`gsap-easing-and-stagger.md` → Spring Eases → bakeSpring); pass the helper's duration.

## Composition Duration

Expand Down
2 changes: 2 additions & 0 deletions skills/hyperframes-animation/rules-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ A rule's own **Critical Constraints** section lists only what is SPECIFIC to tha
<card-morph-anchor path="rules/card-morph-anchor.md">Container morphs apparent size + corner radius + surface treatment between two shots, then fades to reveal the real target underneath. HyperFrames substitutes uniform `scale` for the forbidden `width`/`height` tween, plus paint-only `borderRadius`/`background`/`boxShadow`. Tags: morph, anchor, transition, border-radius, container, shape, handoff</card-morph-anchor>
<theme-crossfade-morph path="rules/theme-crossfade-morph.md">Whole-theme in-place morph under a fixed anchor — background, typography, radii, icons, chrome and logos blend simultaneously (~0.3s) through N pre-styled skins while one anchor element never moves. Stacked complete layers + opacity-only crossfade, anchor rendered once on top (or per-layer at identical geometry); static camera. Single container instead → `card-morph-anchor`. Tags: theme, skin, crossfade, morph, anchor, reskin, cycle, ui</theme-crossfade-morph>
<spring-pop-entrance path="rules/spring-pop-entrance.md">The canonical ENTRANCE pop — an element (or staggered group) arrives by springing `scale: 0 → 1` with `back.out` overshoot, `fromTo` so it's correct at t=0 under seek. Single hero, staggered group (≤500ms cap), overshoot tuned by personality. Distinct from `press-release-spring` (a click/press reaction). Tags: spring, entrance, pop, scale-in, overshoot, stagger, arrival</spring-pop-entrance>
<follow-through path="rules/follow-through.md">Velocity-continuous ring-out composed INTO any ease that arrives with speed (`none`, an `in` ease, a slam) — measure the base curve's arrival velocity numerically, extend the window by a decaying-sine tail proportional to it, so a fast arrival over-rings and a slow one barely stirs. One tween, one pure composed ease; does nothing on `out` eases by design. Tags: follow-through, overshoot, ring-out, impact, secondary-motion, composed-ease</follow-through>
<inertia-chain path="rules/inertia-chain.md">Follow-the-leader secondary motion — every follower runs the leader's IDENTICAL tween time-shifted by `i × DELTA` (≈ 2 frames) on the master timeline, so a stack or trail reads as one body with drag; the tip settles last. The slide-in runs an entrance register; only a post-contact settle-back may use a recovery register, whose overshoot ripples down the chain. Longer-travel followers scale `response`, never the duration. Distinct from an entrance stagger. Tags: inertia, chain, drag, follow-the-leader, trail, stack, secondary-motion</inertia-chain>
<motion-blur-streak path="rules/motion-blur-streak.md">Fake directional velocity blur on a fast entrance / camera push-through — blur peaks at max speed, resolves to 0 at the settle. Two paths: SVG `feGaussianBlur` stdDeviation on the motion axis (proxy-tweened), or a deterministic echo/ghost trail that collapses into the lead. Entrances / mid-shot only. Tags: motion-blur, streak, velocity, ghost, echo, fast</motion-blur-streak>
<waterfall-entry path="rules/waterfall-entry.md">Staggered ARRIVAL cascade — words/elements whip in from below, each starting before the previous settles, an accelerating wave that resolves composed. Title cards, segment openers, list intros. Binary 0→1 opacity via `tl.set` — never fade an arrival. Tags: entrance, cascade, stagger, kinetic-text, title-card, arrival, waterfall</waterfall-entry>
<particle-burst path="rules/particle-burst.md">Deterministic particle / confetti events — confetti pop that bursts up and drifts down on gravity (optional instant-shrink), dot burst from behind text, glyph dissolve to particles. Fixed pool, index-seeded launch values, one `ease: "none"` driver whose onUpdate computes each particle as a pure ballistic function of time — scrub-safe mid-flight, ≤ ~40 particles. Tags: particles, confetti, burst, dissolve, ballistic, deterministic, punctuation</particle-burst>
Expand Down
Loading
Loading