Files
hyperframes/skills/hyperframes-animation/adapters/gsap-transforms-and-perf.md
T
Vance IngallsandClaude Opus 4.8 082280e8de feat(lint): flag text-reflow props in gsap_non_transform_motion
Extend the rule beyond positional layout props to the text-reflow props
letterSpacing / wordSpacing / fontSize. Animating them reflows text and snaps
glyph positions to the pixel grid, so a slow ease-out tail micro-stutters
exactly like left/top — measured on a real composition, a slow letterSpacing
"settle" rendered 19/30 unique frames vs 30/30 for the transform-driven
motion in the same piece. They have no transform replacement (fix: settle via
scale or hold the value), and the snap happens during browser layout, upstream
of the canvas raster, so they are never html-in-canvas-exempt. width/height
stay excluded (legit animated uses — progress bars, reveals).

Restructure the finding's message/fixHint to compose per category (positional
-> x/y; reflow -> scale/hold; roundProps -> remove) instead of a two-branch
ternary. Skill guidance (gsap-transforms-and-perf.md) broadened: "layout
property" includes reflow; letterSpacing/fontSize named as the settle-trap.

Migrate the one surfaced positive: registry/components/vignette/demo.html
title settle letterSpacing -> a subtle scale settle (render-verified 20/20
unique frames, smooth). Blast radius across the registry was this one comp,
zero false positives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 10:36:59 -07:00

5.9 KiB
Raw Blame History

Transforms and Performance

Transform Aliases

Prefer GSAP's transform aliases over raw transform strings:

GSAP property Equivalent
x, y, z translateX/Y/Z (px)
xPercent, yPercent translateX/Y in %
scale, scaleX, scaleY scale
rotation rotate (deg)
rotationX, rotationY 3D rotate
skewX, skewY skew
transformOrigin transform-origin

Aliases let GSAP track and interpolate each axis independently, which prevents accidental overwrites between separate tweens on the same element.

autoAlpha

Prefer autoAlpha over opacity for show/hide:

gsap.to(".panel", { autoAlpha: 0, duration: 0.4 });

autoAlpha: 0 sets both opacity: 0 and visibility: hidden, which removes the element from hit-testing and accessibility tree at zero alpha — closer to "gone" than plain opacity: 0.

clearProps

Removes inline styles set by GSAP when the tween completes:

gsap.to(".item", { x: 100, rotation: 45, clearProps: "all" });
gsap.to(".item", { x: 100, rotation: 45, clearProps: "rotation,x" });

Useful at the end of an animation segment to hand the element back to CSS.

CSS Variables

gsap.to(".chart", { "--hue": 180, duration: 1 });

Animate any custom property. Works for color, length, number — anything CSS will interpolate.

Relative and Directional Values

  • Relative: "+=20", "-=10", "*=2".
  • Directional rotation: "360_cw", "-170_short", "90_ccw" — controls which way the angle takes when going between two values.

SVG Specifics

  • svgOrigin sets transform origin in the SVG's global coordinate space (not the element's local box). Do not combine svgOrigin with transformOrigin on the same element — pick one.
  • Animate SVG transform attributes via the same alias names (x, y, rotation) — GSAP handles the SVG-specific quirks.

Performance Rules

Animate transforms, not layout properties

Animate x, y, scale, rotation, opacity. Never animate left, right, top, bottom, width, height, margin*, the text-reflow props letterSpacing / wordSpacing / fontSize — and never roundProps.

This is a render-correctness rule in HyperFrames, not just a GPU-performance nicety. The renderer seeks frame-by-frame and screenshots each frame, and the browser compositor snaps layout properties to whole device pixels. On a fast tween the per-frame step is several pixels, so the snap is invisible; on a slow tween or a long ease-out tail the value moves less than a pixel per frame — it holds the same pixel for several frames, then jumps a whole one. The result is motion that looks smooth when fast but visibly stutters when slow. Transforms interpolate sub-pixel and stay smooth at any speed. roundProps forces the same integer snap onto a transform — don't use it.

"Layout property" is broader than position: anything that triggers reflow snaps the same way. letterSpacing / fontSize are the common trap — a slow "settle" that crawls letter-spacing or font-size by a fraction of a pixel per frame dwells on a handful of discrete glyph layouts (visible micro-stutter). For a text settle, animate scale (or hold the final value) instead. Unlike positional props, reflow props snap during browser layout — upstream of the canvas raster — so they stutter even in html-in-canvas, and the exception below does not apply to them.

Convert a position animation to a transform by leaving the element at its resting left/top in CSS and animating the offset with x/y:

// CSS: #card { left: 1340px; top: 540px }   ← resting position stays in CSS
tl.to("#card", { left: 1340, top: 540, duration: 1 }); // ✗ stutters
tl.fromTo("#card", { x: 640, y: 0 }, { x: 0, y: 0, duration: 1 }); // ✓ x/y = delta from CSS rest (640 = startLeft  1340)

For a parent-relative left: "100%" sweep, use xPercent: 100 only when the element is the full width of its container; otherwise convert to pixels (x: containerWidth).

The one exception: elements drawn through the html-in-canvas API — those under a <canvas layoutsubtree> ancestor, e.g. the liquid-glass-* blocks. The canvas rasterizes from sub-pixel getComputedStyle, so layout props don't snap there and those elements keep left/top. Everything the browser lays out (plain DOM) follows the rule.

The gsap_non_transform_motion lint rule is the backstop, not the teacher — reach for transforms from the start instead of animating layout props and waiting for lint to reject them.

will-change (sparingly)

.title {
  will-change: transform;
}

Only on elements that actually animate. Applied everywhere it becomes useless and burns memory.

gsap.quickTo for frequent updates (preview-only)

For high-frequency updates driven by events — pointer move, scroll, audio scrub — quickTo reuses the same tween instead of creating a new one each frame:

const xTo = gsap.quickTo("#cursor", "x", { duration: 0.4, ease: "power3" });
const yTo = gsap.quickTo("#cursor", "y", { duration: 0.4, ease: "power3" });

container.addEventListener("mousemove", (e) => {
  xTo(e.pageX);
  yTo(e.pageY);
});

Render mode has no input events. The renderer seeks frame-by-frame; mousemove, scroll, etc. never fire. quickTo's main use case applies in live preview in the browser only. For audio-reactive motion in renders, pre-extract audio data and drive the timeline declaratively (see ../rules/gsap-effects.md).

Stagger beats N tweens

One tween with stagger beats N tweens with manual delays for both readability and runtime cost.

Cleanup

In live preview, pause or kill() off-screen animations. Render mode is unaffected (the renderer drives time directly).