Skill 18 · Hyperframes Animation
Subchapter 18.98
rules/stat-bars-and-fills.mdMarkdown5 KBView on GitHub
The graphics that give a stat visual weight beside its number: a small bar chart, a progress bar/ring filling to a percentage, or a star row filling to a fractional rating. Pair these with counting-dynamic-scale.md (the number) for a complete stat scene.
Layout blueprint — pick ONE and hold it across all stats:
Don’t mix blueprints between stats in one piece — that reads as inconsistent.
Bars grow from the baseline with a stagger; the last bar is the accent. Heights are authored in CSS (inline height per bar); GSAP only reveals scaleY: 0 → 1 — never animate height.
.bars {
display: flex;
align-items: flex-end;
gap: 14px;
height: 280px;
}
.bar {
width: 48px;
background: #3a4a64;
transform: scaleY(0);
transform-origin: bottom center; /* grow UP from the baseline, not from center */
}
.bar:last-child {
background: #ffc300; /* accent the final/current bar */
}tl.to(".bar", { scaleY: 1, duration: 0.7, ease: "power3.out", stagger: 0.08 }, 0.3);Bar form — scaleX from a left origin:
.track {
width: 520px;
height: 16px;
background: #1b263b;
border-radius: 8px;
overflow: hidden;
}
/* width:100% is REQUIRED — an absolutely-positioned fill with no width is 0px, and scaleX of 0 is
still 0 → the bar renders invisible (automated gates may miss a zero-width scaled element). */
.fill {
width: 100%;
height: 100%;
background: #ffc300;
transform: scaleX(0);
transform-origin: left center;
}const PCT = 0.92; // 92%
tl.to(".fill", { scaleX: PCT, duration: 1.0, ease: "power2.out" }, 0.3);Ring form — measured stroke draw (mechanics in svg-path-draw.md):
const ring = document.querySelector("#ring");
const LEN = ring.getTotalLength(); // measure, don't hard-code the circumference
ring.style.strokeDasharray = LEN;
ring.style.strokeDashoffset = LEN; // empty
// rotate the <circle> -90deg in CSS so the fill starts at 12 o'clock
tl.to(ring, { strokeDashoffset: LEN * (1 - 0.92), duration: 1.1, ease: "power2.out" }, 0.3);A gold star row revealed left-to-right to a fractional value (e.g. 4.6 / 5) via a clip wipe over a gold layer sitting on a gray layer.
<div class="stars">
<div class="stars-gray">★★★★★</div>
<div class="stars-gold" id="goldStars">★★★★★</div>
</div>.stars {
position: relative;
font-size: 64px;
letter-spacing: 8px;
}
.stars-gray {
color: #2b3548;
}
.stars-gold {
position: absolute;
inset: 0;
color: #ffc300;
width: 100%;
clip-path: inset(0 100% 0 0);
}const RATING = 4.6,
MAX = 5;
tl.to(
"#goldStars",
{ clipPath: `inset(0 ${100 - (RATING / MAX) * 100}% 0 0)`, duration: 1.0, ease: "power2.out" },
0.3,
);| token | range | notes |
|---|---|---|
| bar count | 4–6 | reads as “a trend” without clutter; the last bar is the current/accent value |
| fill duration | 0.8–1.2s | matched to the paired count-up so number and graphic land together (share the ease) |
| stagger | 0.06–0.1s | larger feels sluggish, 0 loses the build |
| accent hue | exactly one | bars/fill/stars all use the same accent, the rest is muted |
scaleY / scaleX / clipPath, never height/width tweens — author each bar’s final height in CSS and scale from 0.transform-origin must be bottom (bars grow up) / left (fills grow right) — the default center origin scales from the middle and looks wrong..fill needs width: 100% — a zero-width fill scaled by any factor is still invisible, and automated gates may miss it.getTotalLength(); a hard-coded circumference breaks if the radius changes.onUpdate must be O(1) (see counting-dynamic-scale.md).hyperframes-creative/references/data-in-motion.md.counting-dynamic-scale (the number beside the graphic — same ease/duration) · svg-path-draw (progress-ring draw mechanics) · hyperframes-creative/references/data-in-motion.md (stat layout + visual weight).