mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 10:46:06 +00:00
* 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.
149 lines
10 KiB
Markdown
149 lines
10 KiB
Markdown
---
|
||
name: anchored-layout-expand
|
||
description: Edge-pinned container grows (or collapses) along ONE axis and in-flow content reflows with it — a pill springs open downward into a dropdown, a panel grows a sub-task stack, an input card stretches as typed text wraps, a pane expands over a neighbor. Transform-only (mask + slide, or proxy-driven scaleY + counter-scale) because width/height tweens are forbidden; the push on subsequent content is a matched translate on the same tween.
|
||
metadata:
|
||
tags: expand, collapse, anchored, dropdown, menu, accordion, panel, reflow, push, mask, counter-scale, layout
|
||
---
|
||
|
||
# Anchored Layout Expand
|
||
|
||
> The law: **author the layout at its final (expanded) state in CSS, then fake the collapsed state with transforms.** The container never changes size — the _visible_ region does — and everything downstream rides a matched translate. The browser computes layout ONCE; every intermediate frame is pure transform.
|
||
|
||
THE one-axis growth primitive: a container pinned at one edge appears to grow along a single axis, and the in-flow content after it moves in perfect contact with the traveling edge — dropdown, sub-task stack, growing composer card, pane widening over a neighbor. Growth and push are ONE motion: if the panel's bottom edge and the pushed content ever separate or overlap, the illusion dies.
|
||
|
||
Distinct from [card-morph-anchor.md](card-morph-anchor.md) (a free-floating two-shot morph with no neighbors to push — this rule's container is a live layout participant), [spring-pop-entrance.md](spring-pop-entrance.md) (arrival at a point, no edge travel or reflow), and [reactive-displacement.md](reactive-displacement.md) (displacement by a colliding intruder; here content moves because the container's edge reached it — layout causality, not collision).
|
||
|
||
## How It Works
|
||
|
||
1. **Mask** — a wrapper at the final body height (`BODY_H`), `overflow: hidden`. Never tweened.
|
||
2. **Sheet** — the panel surface + content inside the mask, starting at `y: -BODY_H` (tucked above the mask window, behind the pinned header).
|
||
3. **Below** — ONE wrapper holding everything after the container, also starting at `y: -BODY_H`.
|
||
4. **Grow** — ONE `fromTo` drives sheet AND below from `y: -BODY_H → 0`. Shared tween ⇒ the descending bottom edge and the pushed content stay in exact contact by construction. Collapse = the same pair tweened back.
|
||
|
||
When the surface must visibly **stretch in place** (rows revealed top-first, or a pane growing sideways), use the proxy counter-scale variant below instead.
|
||
|
||
## Recipe
|
||
|
||
```html
|
||
<!-- inside a standard scene clip (hyperframes-core) -->
|
||
<div class="stack">
|
||
<div class="expander">
|
||
<div class="expander-head">{headerLabel}</div>
|
||
<div class="expand-mask" id="expand-mask" data-layout-allow-overflow>
|
||
<div class="expand-sheet" id="expand-sheet">
|
||
<div class="expand-row">{rowA}</div>
|
||
<div class="expand-row">{rowB}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- EVERYTHING that must be pushed lives in this one wrapper -->
|
||
<div class="below" id="below">{followingContent}</div>
|
||
</div>
|
||
```
|
||
|
||
```css
|
||
/* Layout is the EXPANDED end state — no collapsed geometry exists in CSS. */
|
||
.expander-head {
|
||
position: relative;
|
||
z-index: 2; /* the sheet slides out from UNDER the header */
|
||
}
|
||
.expand-mask {
|
||
height: BODY_H; /* authored final height — NEVER tweened */
|
||
overflow: hidden;
|
||
}
|
||
.expand-sheet {
|
||
height: BODY_H;
|
||
border-radius: 0 0 SHEET_RADIUS SHEET_RADIUS; /* bottom-only — header + sheet read as one grown card */
|
||
will-change: transform; /* + on .below */
|
||
}
|
||
```
|
||
|
||
```js
|
||
// BODY_H must equal the mask's CSS height exactly — measure once at build.
|
||
// (Montage caveat: per the contract, in a multi-scene master use an authored
|
||
// CSS-matched constant instead — later clips may not be laid out yet.)
|
||
const BODY_H = document.querySelector("#expand-mask").offsetHeight;
|
||
|
||
// The grow: ONE tween, BOTH sides of the seam.
|
||
tl.fromTo(
|
||
["#expand-sheet", "#below"],
|
||
{ y: -BODY_H },
|
||
{ y: 0, duration: GROW_DUR, ease: GROW_EASE },
|
||
GROW_AT,
|
||
);
|
||
|
||
// Garnish: rows already ride the sheet; the fade stagger makes them read as "options arriving".
|
||
tl.fromTo(
|
||
".expand-row",
|
||
{ opacity: 0 },
|
||
{ opacity: 1, duration: ROW_FADE_DUR, stagger: ROW_STAGGER, ease: "power2.out" },
|
||
GROW_AT + GROW_DUR * 0.25,
|
||
);
|
||
|
||
// Collapse — same machinery back; faster (closing is a snap decision).
|
||
tl.fromTo(
|
||
["#expand-sheet", "#below"],
|
||
{ y: 0 },
|
||
{ y: -BODY_H, duration: COLLAPSE_DUR, ease: "power3.in", immediateRender: false },
|
||
COLLAPSE_AT,
|
||
);
|
||
```
|
||
|
||
## Variations
|
||
|
||
- **Proxy counter-scale — surface stretches in place** (rows revealed top-first holding their screen positions; the "payload card expands from the tool-call line"). Drive mask `scaleY` and the sheet's exact inverse from ONE proxy — two independent tweens are wrong: eased midpoints of `s` and `1/s` are not inverses and the content squashes mid-grow. Net content scale is `s × 1/s = 1` every frame; seek-safe because everything derives from the one interpolated proxy.
|
||
|
||
```js
|
||
const grow = { h: COLLAPSED_H }; // 0 for fully collapsed
|
||
tl.fromTo(
|
||
grow,
|
||
{ h: COLLAPSED_H },
|
||
{
|
||
h: BODY_H,
|
||
duration: GROW_DUR,
|
||
ease: GROW_EASE,
|
||
onUpdate: () => {
|
||
const s = Math.max(grow.h / BODY_H, 0.0001); // clamp: no divide-by-zero
|
||
gsap.set("#expand-mask", { scaleY: s, transformOrigin: "50% 0%" });
|
||
gsap.set("#expand-sheet", { scaleY: 1 / s, transformOrigin: "50% 0%" });
|
||
gsap.set("#below", { y: grow.h - BODY_H });
|
||
},
|
||
},
|
||
GROW_AT,
|
||
);
|
||
```
|
||
|
||
- **One-axis pane expand (X)**: same machinery rotated 90° — pin the left edge, sheet from `x: -PANE_W` (or proxy `scaleX` + counter-scale, origin `0% 50%`). Decide the neighbor's fate explicitly: **overlap** (pane paints over it, no neighbor tween) or **push** (neighbor rides the same tween). Never both.
|
||
- **Typed-wrap growth** — the composer card gets taller as typed text wraps. Quantize: one short step per wrap boundary, each moving the pair by one `LINE_H`; wrap times come from the deterministic typing schedule ([discrete-text-sequence.md](discrete-text-sequence.md)), never measured at render time. Two battle-tested traps:
|
||
- **Composer cards have no pinned header** — a composer grows from its TOP edge (the send-button footer stays put), so a plain y-step clips the card's top out of the mask. Combine the proxy counter-scale with the wrap quantization (step the proxy by `LINE_H` at each wrap time) and split the surface into a **sheet** (carries the top radius) + **footer** (carries the bottom radius) so the growth seam stays invisible.
|
||
- **Wrap TIME vs wrap POSITION are two different authorities** — the typing schedule decides _when_ a wrap fires, the browser's line-breaking decides _where_ text actually wraps, and with proportional fonts they silently disagree. Author an explicit `\n` in the typed string (with `white-space: pre-wrap`) at the chosen split point so both derive from the same authored fact.
|
||
- **Springy open** (rare, explicitly-playful): `back.out(1.2)` — the edge overshoots a few px; the pushed content bounces with the panel (correct — they're in contact). Default stays `power3.out`.
|
||
- **Row grows a sub-task stack**: the row is the pinned header, the stack is the sheet, every later row lives in `#below`; chain several scopes for progressive disclosure.
|
||
- **FLIP hand-off**: if the container also TRAVELS to a new layout slot while resizing (prompt promoted to heading, card docking into a sidebar), that's a FLIP problem — `/hyperframes-keyframes` (FLIP recipes). This rule stays the in-place one-axis specialist.
|
||
|
||
## Values
|
||
|
||
| token | range | notes |
|
||
| ------------------------ | --------------------------- | --------------------------------------------------------------------- |
|
||
| BODY_H | measured / authored | drift from the CSS height = visible gap or overlap at full open |
|
||
| GROW_AT | trigger beat + 0–0.1s | growth needs a cause (click / wrap / status beat) or it reads haunted |
|
||
| GROW_DUR | 0.35–0.6s | below ~0.3s the pushed content appears to teleport |
|
||
| GROW_EASE | `power3.out` default | `back.out(1.1–1.3)` only for the playful register |
|
||
| ROW_STAGGER / \_FADE_DUR | 0.04–0.08s / 0.2–0.3s | start rows ~25% into the grow so none flash inside a closed panel |
|
||
| COLLAPSE_DUR | 0.2–0.35s, `power3.in` | faster than open |
|
||
| STEP_DUR / LINE_H | 0.12–0.2s / CSS line-height | typed-wrap variant; WRAP_TIMES from the typing script |
|
||
|
||
## Critical Constraints
|
||
|
||
- **NEVER tween `width` / `height` / `top` / `left` / `margin` / `padding`** — the mask's height is a CSS constant; only its children transform. Tweening the mask IS the forbidden move this rule replaces.
|
||
- **`data-layout-allow-overflow` on the mask** — the collapsed phase parks the sheet outside the mask's box by construction, which trips the `hyperframes check` layout gate (`container_overflow`). The flag is the sanctioned waiver: this overflow is the technique working as designed, not a bug.
|
||
- **Sheet + below share one tween (or one proxy)** — matched-but-separate tweens on the two sides of the contact edge are the classic seam bug.
|
||
- **Everything downstream rides `#below`** — content outside the wrapper is overlapped at t=0 and orphaned during the grow.
|
||
- **`overflow: hidden` on the mask** — without it the tucked sheet is visible above the header at t=0.
|
||
- **Counter-scale needs a proxy**, clamped `s ≥ 0.0001` (a fully-collapsed body divides by zero).
|
||
- **Deterministic sizes** — `BODY_H`, `LINE_H`, `WRAP_TIMES` are build-time constants or one-time measurements, never per-frame layout reads.
|
||
|
||
## See also
|
||
|
||
`cursor-click-ripple` (the igniting click) · `spring-pop-entrance` (richer per-row arrivals) · `discrete-text-sequence` (the typing that drives stepped growth) · `scale-swap-transition` (the grown menu's exit) · `/hyperframes-keyframes` FLIP (grow + travel).
|