Skip to content

Commit 4af8802

Browse files
committed
feat(skills): spring registers, bakeSpring for the CSS/WAAPI lanes, follow-through + inertia-chain
Extends adapters/gsap-easing-and-stagger.md → Spring Eases on top of the existing springEase: named registers as a feel-word lookup (snappy / heavy-settle for entrances; bouncy / wobbly as a separate impact-recovery table, with the "< 0.55 — Don't" row amended to name that exception), the greppable duration-is-an-output criterion, and bakeSpring() emitting a CSS linear() easing for the css and waapi adapters with measured parity. Adds two rules — follow-through (a velocity- continuous ring composed into any ease that arrives with speed) and inertia-chain (followers replay the leader's identical tween, time-shifted by frames) — their rules-index rows, and one-line pointers in spring-pop-entrance.md, css-animations.md and waapi.md.
1 parent 09f359c commit 4af8802

8 files changed

Lines changed: 309 additions & 9 deletions

File tree

‎skills-manifest.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
"files": 29
2323
},
2424
"hyperframes-animation": {
25-
"hash": "bcee69f2229cda71",
26-
"files": 122
25+
"hash": "6dce388872281b63",
26+
"files": 124
2727
},
2828
"hyperframes-audio": {
2929
"hash": "e96de47ed2e6097a",

‎skills/hyperframes-animation/adapters/css-animations.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ Use CSS custom properties to avoid duplicating keyframes:
100100
- Decorative loops with a known repeat count.
101101
- Mask, glow, shimmer, grain, and subtle parallax layers.
102102
- Simple one-element entrances where a full JS timeline would be excessive.
103+
- 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.
103104

104105
## Avoid
105106

‎skills/hyperframes-animation/adapters/gsap-easing-and-stagger.md‎

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,12 @@ tl.fromTo(
116116
);
117117
```
118118

119-
| dampingFraction | overshoot | register |
120-
| ----------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
121-
| **1.0 (default)** | none (monotone) | The house settle — the exact curve `power3.out` approximates. Product / enterprise / serious tone. |
122-
| 0.80–0.85 | ~1–1.5% | "Alive, not bouncy" — the iOS system default register. The overshoot is felt, not seen. |
123-
| 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). |
124-
| < 0.55 | > 12% | Don't. Cartoon-wobble territory. |
119+
| dampingFraction | overshoot | register |
120+
| ----------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
121+
| **1.0 (default)** | none (monotone) | The house settle — the exact curve `power3.out` approximates. Product / enterprise / serious tone. |
122+
| 0.80–0.85 | ~1–1.5% | "Alive, not bouncy" — the iOS system default register. The overshoot is felt, not seen. |
123+
| 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). |
124+
| < 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. |
125125

126126
| response | duration (ζ=1) | feel |
127127
| --------- | -------------- | ------------------------------------------------------------ |
@@ -135,6 +135,119 @@ Craft notes:
135135
- **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.
136136
- **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.
137137

138+
### Named Registers
139+
140+
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.
141+
142+
```javascript
143+
const SPRING_REGISTERS = {
144+
// Entrance / settle voices — arrivals, lockups, hero landings (ζ ≥ 0.8 per the doctrine above).
145+
snappy: { response: 0.22, dampingFraction: 0.9 }, // tight snap: chips, small UI
146+
"heavy-settle": { response: 0.8, dampingFraction: 1 }, // weighted lockup — the settle IS the shot
147+
// Recovery voices — ONLY the spring-back after a contact (see Recovery Registers). Never an arrival.
148+
bouncy: { response: 0.4, dampingFraction: 0.5 },
149+
wobbly: { response: 0.5, dampingFraction: 0.28 },
150+
};
151+
const spring = (feel) => springEase(SPRING_REGISTERS[feel]);
152+
153+
const land = spring("heavy-settle");
154+
tl.fromTo(
155+
"#lockup",
156+
{ y: 80, opacity: 0 },
157+
{ y: 0, opacity: 1, duration: land.duration, ease: land.ease },
158+
1.2,
159+
);
160+
```
161+
162+
| register | response | ζ | duration (from the helper) | use |
163+
| -------------- | -------- | ---- | -------------------------- | ---------------------------------------------------------- |
164+
| `snappy` | 0.22 | 0.90 | ≈ 0.29s | tight snap — chips, badges, small UI; ~0.1% overshoot |
165+
| `heavy-settle` | 0.80 | 1.00 | ≈ 1.18s | weighted hero landing, end card, wordmark lockup; monotone |
166+
167+
#### Recovery Registers (impact recovery only)
168+
169+
`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.
170+
171+
| register | response | ζ | overshoot | duration (from the helper) | recovery context |
172+
| -------- | -------- | ---- | --------- | -------------------------- | ------------------------------------------------------------------------- |
173+
| `bouncy` | 0.40 | 0.50 | ~16% | ≈ 0.81s | soft-body landing recovery, a released press, a chain's settle-back move |
174+
| `wobbly` | 0.50 | 0.28 | ~40% | ≈ 1.90s | rubber / jelly tier — the wobble _is_ the material read; rare, deliberate |
175+
176+
- The arrival keeps the entrance doctrine (ζ ≥ 0.8, or `power3.out`); only the post-contact recovery may go `bouncy` / `wobbly`.
177+
- Recovery goes on a transform or a deformation proxy that was just displaced — never on `opacity`, never on the arrival travel.
178+
- Longer flight in a chain or trail comes from `response`, not from the duration (next section).
179+
180+
### Duration Is an Output — the Greppable Criterion
181+
182+
`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**.
183+
184+
```bash
185+
grep -nE 'duration\s*\*|\*\s*[A-Za-z_.]*duration' index.html # spring tweens: zero hits
186+
```
187+
188+
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.
189+
190+
### bakeSpring — the Same Spring in the CSS-Keyframes and WAAPI Lanes
191+
192+
`@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`).
193+
194+
```javascript
195+
// Curvature-adaptive sampling: stops cluster where the curve bends (the overshoot lobes).
196+
function bakeSpring(spring, { maxPts = 75 } = {}) {
197+
const dense = 400;
198+
const pts = [[0, 0]];
199+
const curv = [];
200+
for (let i = 1; i < dense; i++) {
201+
const y0 = spring.ease((i - 1) / dense);
202+
const y1 = spring.ease(i / dense);
203+
const y2 = spring.ease((i + 1) / dense);
204+
curv.push(Math.abs(y2 - 2 * y1 + y0)); // second difference ≈ local curvature
205+
}
206+
const total = curv.reduce((a, b) => a + b, 0) || 1;
207+
const budget = maxPts - 2;
208+
let acc = 0;
209+
for (let i = 1; i < dense; i++) {
210+
acc += (curv[i - 1] / total) * budget;
211+
if (acc >= 1) {
212+
pts.push([i / dense, spring.ease(i / dense)]);
213+
acc = 0;
214+
}
215+
}
216+
pts.push([1, 1]);
217+
const css = `linear(${pts.map(([x, y]) => `${y.toFixed(5)} ${(x * 100).toFixed(3)}%`).join(", ")})`;
218+
return { points: pts, css }; // at most maxPts stops; the four registers land at 57–61 with the default
219+
}
220+
221+
const ms = (seconds) => Math.round(seconds * 1000); // the css / waapi lanes take milliseconds — one unit cast, kept out of the tween sites
222+
const s = springEase(SPRING_REGISTERS.snappy);
223+
const baked = bakeSpring(s);
224+
// CSS lane — the duration is still the helper's:
225+
style.textContent = `#chip { animation: chip-in ${ms(s.duration)}ms ${baked.css} 200ms 1 both; }`;
226+
// WAAPI lane:
227+
chip
228+
.animate([{ transform: "translateY(-220px)" }, { transform: "translateY(0)" }], {
229+
duration: ms(s.duration),
230+
delay: 200,
231+
easing: baked.css,
232+
fill: "both",
233+
iterations: 1,
234+
})
235+
.pause();
236+
```
237+
238+
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):
239+
240+
| register | points at the default cap | worst error, 1000 px travel | at 220 px | with `maxPts: 200` |
241+
| -------------- | ------------------------- | --------------------------- | --------- | ------------------ |
242+
| `snappy` | 61 | 2.3 px | 0.5 px | 115 pts → 0.5 px |
243+
| `heavy-settle` | 58 | 2.5 px | 0.5 px | 110 pts → 0.6 px |
244+
| `bouncy` | 61 | 3.0 px | 0.7 px | 123 pts → 1.0 px |
245+
| `wobbly` | 57 | 7.3 px | 1.6 px | 107 pts → 1.9 px |
246+
247+
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.
248+
249+
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.
250+
138251
## Stagger
139252

140253
```javascript

‎skills/hyperframes-animation/adapters/waapi.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ document.querySelectorAll(".token").forEach((token, index) => {
6969
- Lightweight DOM motion where CSS keyframes are too rigid and GSAP is unnecessary.
7070
- Generated animations from structured data.
7171
- Simple timelines that can be represented as keyframes, delays, and offsets.
72+
- 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.
7273

7374
## Composition Duration
7475

‎skills/hyperframes-animation/rules-index.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ A rule's own **Critical Constraints** section lists only what is SPECIFIC to tha
9393
<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>
9494
<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>
9595
<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>
96+
<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>
97+
<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>
9698
<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. Per-frame-driven form for an element riding a baked track: velocity-gated text-free silhouette ghosts (or a rotate/blur/counter-rotate filter sandwich when a true blur is the look), streak axis from `atan2` of the track delta. Tags: motion-blur, streak, velocity, ghost, echo, fast, driven, tracked</motion-blur-streak>
9799
<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>
98100
<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>

0 commit comments

Comments
 (0)