--- name: motion-quality description: "Make HyperFrames motion read cleanly frame by frame: text that lands without a tick, scales without shaking, and stays sharp. Use when animating text (titles, wordmarks, counters, captions), choosing an ease or a transformOrigin, or reviewing why a render looks jittery, soft or late. Covers the pixel rules measured on the HyperFrames engine, the lint codes that catch them statically, and glintmotion_motion_check, which measures the rendered frames." --- # Motion quality A still frame cannot show a sub-pixel bug. A word that freezes for five frames or shakes by half a pixel looks fine in every screenshot and wrong in the video. So: write the motion, then **measure the frames**, then fix what the numbers show. The rules below were measured on the HyperFrames engine (Chromium, device scale 1, 30 fps). They are about what the rasteriser does with GSAP transforms, not about taste. ## The three instruments | Tool | When | What it sees | | -------------------------------------------- | ------------------------- | -------------------------------------------------------------- | | `glintmotion_check` (or `hyperframes lint`) | after every edit | the code: eases, origins, `will-change` on text | | `glintmotion_motion_check` | when text moves or scales | the frames: each moving element alone, captured frame by frame | | `glintmotion_contact_sheet` / `studio_frame` | layout and pacing | single frames, not motion | `glintmotion_motion_check` isolates each tween's targets (everything else hidden, so grain, video and neighbours cannot mask a defect) and returns, per finding, the target, its tween's times, a per-frame `pattern` (`m` moved, `=` same pixels as the frame before) and a fix. Pass `from`/`to` to check one scene. Expect about 0.3 s per tween. | Finding | Pattern | Meaning | | -------------- | -------------- | ----------------------------------------------------------------------------------- | | `tail_tick` | `mmmmm=m=====` | settles, jumps one more pixel, stops: reads as a tick, not a landing | | `early_finish` | `mmmmmm======` | a third or more of the tween shows no movement; the visible end is `visibleUntil` | | `late_start` | `=====mmmmmm` | an ease-in spends its first frames under a pixel; it really starts at `visibleFrom` | | `stall` | `mmm====mmm` | stops, then moves again: a visible hitch | | `judder` | — | text whose ink goes up and down while it should move one way | ## Rule 1 — Text snaps to whole pixels vertically Glyphs are placed on whole device pixels vertically. Any part of a tween that moves text less than one pixel per frame vertically produces identical frames. Measured (96 px serif, `from({ y: 50 })`, 0.47 s = 14 frames): `expo.out` gives the pattern `mmmmmmm=m=====`, **6 identical frames** and a one-pixel tick before the end (`tail_tick`). The same move with `power2.out` lands without a finding. - For text moving on `y` / `yPercent`, prefer `power2.out` or `power1.out`. Keep `expo.out` and `power4.out` for large travel, short durations, or non-text elements. - Shorten rather than soften: a 0.4 s `power3.out` reads better than a 0.8 s `expo.out` that is idle for its last 0.25 s. - Lint: `gsap_ease_freezes_on_frame_clock` estimates the frozen frames from distance, duration, ease and fps. ## Rule 2 — Scale text from its baseline, not its centre Scaling around the centre moves the baseline every frame; snapped to whole pixels, the text shakes up and down. Measured (96 px serif, `line-height: 1.1`, `scale: 1.6 → 1`, linear, 40 frames): with the default centre origin the ink's vertical centre **reverses direction 37 times in 39 frame steps**. With `transformOrigin` at `50% 75%`, `50% 80%`, `50% 85%` or on a measured baseline: **no judder**. ```js tl.fromTo( "#title", { scale: 1.6, transformOrigin: "50% 80%" }, { scale: 1, duration: 1.2, ease: "power2.out" }, 0, ); ``` - `50% 80%` is the simple choice for one line at a normal `line-height`. Unusual line-heights or fonts move the baseline: check with `glintmotion_motion_check`. - Exact baseline: a zero-size probe at the end of the line sits on it. ```html