feat(skills): c2v mining pass — 7 new blueprints, 10 new rules, compacted recipe corpus (#2680)

* feat(skills): c2v mining pass over animation blueprints and rules

Compacts ~45 existing animation rules/blueprints into tighter recipe form
(net -3.4k lines) and adds 17 mined from the c2v corpus:

- 7 blueprints: agent-progress-theater, camera-journey, fixed-anchor-cycle,
  panel-edit-live-sync, prompt-type-submit-generate,
  transcript-scroll-artifact-reveal, zoom-out-workspace-reveal
- 10 rules: 3d-camera-flight, anchored-layout-expand, chart-scrub-readout,
  chromatic-glitch, control-target-sync, cursor-drag, gradient-text-sweep,
  multi-cursor-choreography, particle-burst, theme-crossfade-morph






Both indexes updated.

* feat(skills): sync product-launch script bank with mined blueprint roles

The role->blueprint script bank in product-launch-video/story-design.md is
kept 1:1 with blueprints-index role declarations, which the c2v mining pass
expanded. Adds the 25 missing entries (script-shape descriptor + example
lines + pattern): 13 for the 7 new blueprints, 12 for role widenings on 7
existing ones (cursor-ui-demo, dataviz-countup, titlecard-reveal, et al.),
and states the 1:1 sync contract in the bank's intro.

* docs(skills): cover constellation-hub scatter-drift variant in the script bank

Review follow-up on #2680: the SOCIAL_PROOF constellation-hub entry
patterned only the orbit shape; the c2v pass added a scatter-drift
end-card variant with the opposite geometry (no hub, no ring). Adds an
example line and extends the pattern so a scatter-drift beat's VO isn't
steered toward the orbit shape.
This commit is contained in:
WaterrrForever
2026-07-21 17:28:55 +08:00
committed by GitHub
parent 8f171433f9
commit 853256403b
68 changed files with 4911 additions and 8179 deletions
@@ -7,200 +7,77 @@ metadata:
# Counting with Dynamic Scale
A number counts from A → B while its transform scale grows to the final size. The effect preserves escalating visual weight without tweening `font-size` or forcing text layout on every frame.
A number counts from A → B while its transform scale grows to the final size escalating visual weight ("this is impressive") without tweening `font-size` or forcing text layout on every frame. The final font size is static CSS; only the transform changes.
## How It Works
A single paused timeline drives **two synchronized tweens**:
Two synchronized tweens at the SAME timeline position with the SAME ease: (1) a proxy value rendered as text via `onUpdate` (`Math.round(...).toLocaleString()`), (2) the counter's transform `scale: START_SCALE → 1`, where `START_SCALE = START_SIZE / END_SIZE`. A suffix (`%`, `×`, `+`) slides in AFTER the count lands — the number gets its own beat — and a label fades in early.
1. The numeric value (rendered as DOM text via `onUpdate`)
2. The counter transform (`scale: START_SCALE``scale: 1`)
As the number gets bigger, the text grows in place — visually communicating “this is impressive” while keeping the final font size static in CSS.
## Easing
Pick by drama desired (the choice is discrete; coefficient is implicit):
| GSAP ease | Effect |
| ------------ | --------------------------------------------- |
| `power1.out` | Mild — slight deceleration |
| `power2.out` | Default — ease-out, fast start slow end |
| `power3.out` | Strong — dramatic deceleration ⭐ recommended |
| `expo.out` | Very dramatic — almost stops at the end |
`power3.out` matches the polynomial `1 - (1-x)^k` family at k ≈ 2.5 — number rushes up then slows dramatically at the peak.
## HTML
## Recipe
```html
<div
class="scene"
data-composition-id="counter-scene"
data-start="0"
data-duration="3"
data-track-index="0"
>
<div class="counter-wrap">
<span class="counter" id="counter">0</span><span class="counter-suffix">{suffix}</span>
</div>
<div class="counter-label">{label}</div>
<!-- inside a standard scene clip (hyperframes-core) -->
<div class="counter-wrap">
<span class="counter" id="counter">0</span><span class="counter-suffix">{suffix}</span>
</div>
<div class="counter-label">{label}</div>
```
## CSS
```css
.scene {
position: relative;
width: 100%;
height: 100%;
display: grid;
place-items: center;
background: {bgColor};
}
.counter-wrap {
display: flex;
align-items: baseline;
justify-content: center;
gap: 8px;
/* Fixed-width container prevents layout shift as digit count changes */
width: {counterContainerWidth};
text-align: center;
width: {counterContainerWidth}; /* fixed width — no layout shift as digit count changes */
}
.counter {
font-family: {font};
font-weight: 900;
color: {textColor};
/* MANDATORY — tabular-nums keeps digits the same width */
font-variant-numeric: tabular-nums;
/* Final size is static. GSAP animates transform scale, not font-size. */
font-variant-numeric: tabular-nums; /* MANDATORY — digits keep equal width */
display: inline-block;
font-size: {endSize};
font-size: {endSize}; /* final size is static; GSAP animates scale, not font-size */
transform-origin: center center;
letter-spacing: -2px;
line-height: 1;
}
.counter-suffix {
font-family: {font};
font-weight: 800;
color: {accentColor};
font-size: {suffixSize};
opacity: 0;
transform: translateY(20px);
}
.counter-label {
margin-top: 24px;
font-family: {font};
font-size: {labelSize};
color: {mutedTextColor};
text-align: center;
}
```
## GSAP Timeline
```js
const counter = document.getElementById("counter");
const state = { value: 0 };
const START_SCALE = START_SIZE / END_SIZE;
```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
const counter = document.getElementById("counter");
const state = { value: 0 };
const START_SCALE = START_SIZE / END_SIZE;
// Count value. onUpdate changes text only.
tl.to(
state,
{
value: TARGET_VALUE,
duration: COUNT_DUR,
ease: COUNT_EASE,
onUpdate: () => {
counter.textContent = Math.round(state.value).toLocaleString();
},
// Count value — onUpdate changes text only
tl.to(
state,
{
value: TARGET_VALUE,
duration: COUNT_DUR,
ease: COUNT_EASE,
onUpdate: () => {
counter.textContent = Math.round(state.value).toLocaleString();
},
0,
);
},
0,
);
// Visual growth uses a compositor transform and shares the count timing.
tl.fromTo(
counter,
{ scale: START_SCALE },
{ scale: 1, duration: COUNT_DUR, ease: COUNT_EASE },
0,
);
// Visual growth compositor transform sharing the count's timing + ease
tl.fromTo(counter, { scale: START_SCALE }, { scale: 1, duration: COUNT_DUR, ease: COUNT_EASE }, 0);
// Suffix slides in after count completes
tl.to(
".counter-suffix",
{
opacity: 1,
y: 0,
duration: SUFFIX_DUR,
ease: `back.out(${SUFFIX_BOUNCE_FACTOR})`,
},
COUNT_DUR,
);
// Suffix slides in AFTER the count completes
tl.to(
".counter-suffix",
{ opacity: 1, y: 0, duration: SUFFIX_DUR, ease: `back.out(${SUFFIX_BOUNCE_FACTOR})` },
COUNT_DUR,
);
// Label fades in early
tl.from(
".counter-label",
{
opacity: 0,
y: 12,
duration: LABEL_DUR,
ease: "power2.out",
},
LABEL_AT,
);
window.__timelines["counter-scene"] = tl;
</script>
// Label fades in early
tl.from(".counter-label", { opacity: 0, y: 12, duration: LABEL_DUR, ease: "power2.out" }, LABEL_AT);
```
## How to Choose Values
- **TARGET_VALUE** — the number the counter lands on
- Effects: 23 digits reads best at hero size; 4+ digits requires wider container
- Constraints: must fit horizontally at END_SIZE inside the container
- **START_SIZE / END_SIZE** — design inputs used once to calculate `START_SCALE`
- Range: START_SIZE ≈ 4060 % of END_SIZE
- Effects: smaller START_SIZE = more dramatic growth; larger = subtler
- Constraints: set CSS `font-size` to END_SIZE; never tween either value. END_SIZE × digit count must fit the container width without clipping
- **COUNT_DUR** — count + scale tween duration
- Range: 1.22.5 s
- Effects: shorter = aggressive; longer = settled, gives reading time
- Constraints: must allow the eye to read the digits scrolling past; below ~0.8 s reads as a flash
- **COUNT_EASE** — shared ease for the value and transform scale
- Discrete choice: `power2.out`, `power3.out`, `expo.out` (see table above)
- Constraint: avoid `back.out` / `elastic.out` — overshoot reads as unstable data
- **SUFFIX_DUR** — duration of the suffix slide-in
- Range: 0.30.6 s
- Effects: shorter = snap; longer = floats
- Constraints: must fire after the count lands (started at COUNT_DUR), not during
- **SUFFIX_BOUNCE_FACTOR** — back.out coefficient on the suffix entry
- Range: 1.42.0
- Effects: 1.4 = small overshoot; 2.0 = bouncy
- **LABEL_AT / LABEL_DUR** — when and how long the label fades in
- Range: LABEL_AT < COUNT_DUR / 2 (label arrives before count peaks); LABEL_DUR 0.40.7 s
## Variations
### Direct `innerText` tween (no proxy object)
The GSAP inspector reads `innerText` directly, so a number-only counter can skip the `state` proxy:
- **Direct `innerText` tween (no proxy)** — GSAP can tween `innerText` directly for a number-only counter; keep the proxy form when you need locale formatting or suffix logic. The scale tween stays separate either way:
```js
tl.to(
@@ -210,84 +87,29 @@ tl.to(
);
```
`snap: { innerText: 1 }` keeps it integer. Keep the proxy-object `onUpdate` form above when you need locale formatting (`toLocaleString`) or suffix logic. In either form, the synchronized scale remains a separate transform tween at timeline position `0`.
- **3D depth entry** — add a `tl.from(".counter", { z: -300, ... }, 0)` push-in; requires `perspective` on `.counter-wrap` and `transform-style: preserve-3d` on the counter.
- **Multi-stat coordinated reveal** — 3 stats counting in parallel share the SAME ease, duration, and start position so they finish together (a chord, not an arpeggio). Each stat usually also needs a paired graphic (bar / ring / stars) — don't stop at the number; see [stat-bars-and-fills.md](stat-bars-and-fills.md).
### 3D depth entry
## Values
Combine with `translateZ` for parallax-style depth on entry:
```js
tl.from(
".counter",
{
z: -300,
duration: 0.6,
ease: "power2.out",
// requires parent or .counter itself to have perspective set
},
0,
);
```
CSS prerequisite:
```css
.counter-wrap {
perspective: 1000px;
}
.counter {
transform-style: preserve-3d;
}
```
### Multi-stat coordinated reveal
For 3 stats counting in parallel, share the SAME ease and duration so they finish together — visually a chord, not arpeggio. Each stat usually also needs a **paired graphic** (bar / ring / stars) — don't stop at the number; see [stat-bars-and-fills.md](stat-bars-and-fills.md):
```js
["#stat1", "#stat2", "#stat3"].forEach((sel, i) => {
const obj = { v: 0 };
tl.to(
obj,
{
v: TARGETS[i],
duration: COUNT_DUR,
ease: COUNT_EASE,
onUpdate: () => (document.querySelector(sel).textContent = Math.round(obj.v)),
},
0,
); // same start position — chord
});
```
## Key Principles
- **Synchronized value + scale at one timeline position** so the two tweens share an ease and stay coordinated
- **`font-variant-numeric: tabular-nums` is mandatory** — without it digit-count transitions (e.g. 9 → 10 → 100) cause visible jitter as glyph widths change
- **Fixed-width container** as belt-and-suspenders — even with tabular-nums, glyph shape changes can shift baselines
- **Grow in place, don't bounce** — the number should feel weighty, not springy. `power3.out` ends at exact value; `back.out` overshoots and feels cartoonish
- **Start small enough to grow noticeably** (~50 % of final size); end large enough to feel decisive but not clip viewport
- **Never set `fontSize` in `onUpdate`** — final type size is static CSS; only the transform changes per frame
- **Suffix animates AFTER the count, not during** — gives the number its own beat
- **❗ Label is BIG TEXT, not a page-style tiny caption** — for VIDEO, a small paragraph-style caption below a hero-size number reads as visual noise. Use display-size, uppercase, tracked label so the layout is "two-line big-text"; the label is part of the headline, not a footer.
| token | range | notes |
| --------------------- | ------------------------------------------- | ----------------------------------------------------------------------------- |
| TARGET_VALUE | 23 digits ideal | 4+ digits needs a wider container; must fit at END_SIZE without clipping |
| START_SIZE / END_SIZE | START ≈ 4060% of END | design inputs used once for START_SCALE; never tween either |
| COUNT_DUR | 1.22.5s | below ~0.8s reads as a flash — the eye must read the digits scrolling past |
| COUNT_EASE | `power2.out` / `power3.out` ⭐ / `expo.out` | shared by value + scale; more `.out` = more dramatic deceleration at the peak |
| SUFFIX_DUR | 0.30.6s | fires at `COUNT_DUR`, never during the count |
| SUFFIX_BOUNCE_FACTOR | 1.42.0 | overshoot is fine on the suffix (it's punctuation, not data) |
| LABEL_AT / LABEL_DUR | AT < COUNT_DUR/2; 0.40.7s | label arrives before the count peaks |
## Critical Constraints
- **`tabular-nums` mandatory** — required CSS for layout stability
- **Timeline must be paused**: `gsap.timeline({ paused: true })`. Never `tl.play()`
- **Registry key = `data-composition-id`**: `window.__timelines["counter-scene"]` must match scene root
- **`onUpdate` mutates DOM**: HF runtime seeks the timeline frame-by-frame, so `onUpdate` runs on every seek call. Keep `onUpdate` work O(1) — set text only, with no style writes or DOM creation
- **`Math.round` not `Math.floor`** — half-way through the final integer should display the final value briefly, not the previous one
- **Avoid `back.out` / `elastic.out`** for the counter itself — overshoot makes the number look unstable (it's data, not decoration)
- **`tabular-nums` mandatory** + fixed-width container as belt-and-suspenders — without them digit-count transitions (9 → 10 → 100) jitter as glyph widths change.
- **Never set `fontSize` in `onUpdate`** — final type size is static CSS; only the transform changes per frame. Keep `onUpdate` O(1): set text only, no style writes or DOM creation.
- **`Math.round`, not `Math.floor`** — halfway through the final integer should already display the final value.
- **Avoid `back.out` / `elastic.out` on the counter itself** — overshoot makes the number look unstable (it's data, not decoration). Grow in place, don't bounce.
- **Label is BIG TEXT, not a page-style caption** — a tiny paragraph under a hero-size number reads as visual noise in video. Display-size, uppercase, tracked: the label is part of the headline.
## Combinations
## See also
- [stat-bars-and-fills.md](stat-bars-and-fills.md) — **the paired graphic beside the number** (growth bars / progress ring / star wipe). A stat scene is usually BOTH rules: the count-up here + a fill there. Give the fill the same ease and duration so number and graphic land as one beat.
- [svg-path-draw.md](svg-path-draw.md) — icons drawing in around the number
- [center-outward-expansion.md](center-outward-expansion.md) — related icons exploding outward synced to count peak
## Pairs with HF skills
- `/hyperframes-animation` — timeline + `onUpdate` API
- `/hyperframes-core` — composition wiring, `data-*` attributes
- `/hyperframes-cli``hyperframes lint` to verify scene
`stat-bars-and-fills` (the paired graphic — give it the same ease/duration so number and fill land as one beat) · `svg-path-draw` (icons drawing in around the number) · `center-outward-expansion` (icons bursting outward at the count peak).