Skill 18 · Hyperframes Animation
Subchapter 18.102
rules/vertical-spring-ticker.mdMarkdown5 KBView on GitHub
Multiple spring tweens are ADDED TOGETHER to produce total Y translation — each spring contributes one discrete “step”, so instead of a single linear scroll you get the slot-machine “click click click” rhythm with natural settling. Distinct from a continuous marquee: this rule’s semantics are discrete steps that land; for endless linear motion see .
A masked window of fixed height ITEM_HEIGHT (overflow: hidden) holds a vertical stack of items, each exactly ITEM_HEIGHT tall. Each spring holds a 0→1 progress; a shared onUpdate sums them and applies translateY(-sum × ITEM_HEIGHT). Springs fire sequentially with overlap (STEP_SPACING ≤ STEP_DUR), so each step snaps in while the previous is still settling — that overlap is what makes them additive, and the back.out overshoot is what makes each step read as a “click”.
<!-- inside a standard scene clip (hyperframes-core) -->
<div class="ticker" id="ticker">
<div class="stack-inner" id="stack-inner">
<div class="item">{item0}</div>
<div class="item">{item1}</div>
<div class="item">{itemN}</div>
</div>
</div>.ticker {
width: TICKER_WIDTH;
height: ITEM_HEIGHT; /* MUST match .item height exactly */
overflow: hidden; /* the mask is the window */
}
.stack-inner {
display: flex;
flex-direction: column; /* mandatory — vertical stacking */
}
.item {
height: ITEM_HEIGHT; /* MUST equal .ticker height */
display: flex;
align-items: center;
justify-content: center;
/* font-variant-numeric: tabular-nums; — for numeric tickers */
}const innerEl = document.getElementById("stack-inner");
const springs = Array.from({ length: STEPS }, () => ({ p: 0 }));
function applyTransform() {
const sumP = springs.reduce((acc, s) => acc + s.p, 0);
innerEl.style.transform = `translateY(${-sumP * ITEM_HEIGHT}px)`;
}
applyTransform(); // initial state
springs.forEach((spring, i) => {
tl.to(
spring,
{
p: 1,
duration: STEP_DUR,
ease: `back.out(${BOUNCE_FACTOR})`,
onUpdate: applyTransform,
},
STEP_START + i * STEP_SPACING,
);
});font-variant-numeric: tabular-nums required.translateY(${sumP * ITEM_HEIGHT}px)) and arrange items in reverse order.STEP_SPACING), a long pause, then one dramatic final step with a bigger BOUNCE_FACTOR. The pause is where the eye locks in.| token | range | notes |
|---|---|---|
| ITEM_HEIGHT | ~fontSize × 1.25 | must hold capital descenders; .ticker height MUST equal it exactly |
| TICKER_WIDTH | 30–60% viewport width | wide enough for the longest item without ellipsis |
| STEPS | 1–4 | number of transitions, not items; STEPS ≤ itemCount − 1 |
| STEP_DUR | 0.3–0.7s | under 0.3 the overshoot is invisible; over 0.7 the click reads as a slide |
| STEP_SPACING | 0.3–0.5s | ≤ STEP_DUR so springs overlap (additive); wider gaps read as a lazy linear scroll |
| BOUNCE_FACTOR | 1.4–2.5 | 1.4 gentle click / 2.0 firm / 2.5+ casino spin-and-land for a climax step |
Reference: ../examples/proof-logo-chain.html (204px, 1 step, 0.45s).
overflow: hidden on the container, not the inner stack; flex-direction: column on the stack.onUpdate — never tween the final position directly. Each spring contributing its OWN snap is the slot-machine pacing.back.out per step — non-overlapping steps or an out-only ease collapse into a linear scroll.innerHTML between steps — the ticker moves the SAME items via translate; swapping content shows the previous item AS the new one (broken illusion).tabular-nums for numeric tickers — variable digit widths break alignment.reactive-displacement (ticker pushed by an incoming element) · scale-swap-transition (ticker scales out after settling) · press-release-spring (button press triggers the spin).