mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
refactor(skills,lint): migrate kinetic-letter-in off letterSpacing; document rule design
The kinetic-letter-in motion-primitive (a music-to-video reference authors copy) animated the word's letterSpacing as a settle — a reflow tween that micro-stutters under seek-by-frame capture, and outside the registry scan so the rule never fired on it. The chars are already per-glyph spans, so migrate the settle to a per-glyph x spread ((0.04em − −0.04em) × 280px = 22.4px/gap, centered about index 3), with a comment naming the hazard and the rule. Render-verified: faithful, smooth. Document two intentional design choices in gsap_non_transform_motion so future readers don't read them as misses: - No per-line/per-file suppression by design — the stance is fix-the-motion, not silence-the-rule; every plain-DOM case has a faithful transform equivalent. - set() is skipped intentionally: a set() that seats an integer-snapped layout position before a later transform tween is a single from-state frame, not motion. Also hoist loadParseGsapScript() above the per-script loop (the other async rules do the same; dynamic-import cache makes it equivalent, but the placement no longer reads as load-bearing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b95f2ca25e
commit
1c0eef835f
@@ -1128,6 +1128,12 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
// them does not integer-snap and does not stutter. We resolve each tween's target to
|
||||
// its element(s) and skip the finding only when EVERY target is html-in-canvas; a
|
||||
// grouped tween that also touches a plain-DOM element (which does stutter) still fires.
|
||||
//
|
||||
// No suppression by design: there is intentionally no per-line/per-file opt-out (unlike
|
||||
// eslint-disable). The stance is fix-the-motion, not silence-the-rule — a plain-DOM
|
||||
// layout-prop animation always has a faithful transform equivalent (per-glyph x for
|
||||
// spacing, scale for size, x/y for position). An author who has consciously accepted a
|
||||
// stutter still has no flag to flip; that is deliberate, not a missing feature.
|
||||
async ({ scripts, tags, source }) => {
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
|
||||
@@ -1182,6 +1188,10 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
// they are never html-in-canvas-exempt. (width/height are deliberately omitted: they
|
||||
// have legitimate animated uses — progress bars, reveals — and would over-report.)
|
||||
const REFLOW_PROPS = ["letterSpacing", "wordSpacing", "fontSize"];
|
||||
// Resolve the parser once, above the loop (the other async rules in this file do the
|
||||
// same); the dynamic-import cache makes per-iteration calls equivalent, but hoisting
|
||||
// keeps the placement from reading as load-bearing.
|
||||
const parseGsapScript = await loadParseGsapScript();
|
||||
for (const script of scripts) {
|
||||
if (!/gsap\.timeline/.test(script.content)) continue;
|
||||
|
||||
@@ -1195,7 +1205,6 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
// those would let real stutter-prone tweens escape. The parser also gives real AST
|
||||
// keys, so a nested `{}` value (an onComplete body, modifiers) and a layout-prop
|
||||
// name appearing inside a string value can't be misread — both hazards of a raw scan.
|
||||
const parseGsapScript = await loadParseGsapScript();
|
||||
const parsed = parseGsapScript(script.content);
|
||||
const calls: GsapTransformCall[] = [
|
||||
...parsed.animations.map((anim) => ({
|
||||
@@ -1215,7 +1224,9 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
];
|
||||
|
||||
for (const call of calls) {
|
||||
// set() is instantaneous — it never animates, so it cannot stutter.
|
||||
// set() is instantaneous — it never animates, so it cannot stutter. A set() that
|
||||
// seats an integer-snapped layout position (e.g. tl.set("#x",{left:100})) before a
|
||||
// later transform tween is a single from-state frame, not motion; intentionally skipped.
|
||||
if (call.method === "set") continue;
|
||||
// Object.hasOwn, not `in`: a tween property named `toString`/`constructor` would
|
||||
// match the prototype chain and resolve LAYOUT_FIX[p] to an inherited function.
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"files": 23
|
||||
},
|
||||
"music-to-video": {
|
||||
"hash": "c188d0d159b926c2",
|
||||
"hash": "32ddcc8895eb021a",
|
||||
"files": 132
|
||||
},
|
||||
"pr-to-video": {
|
||||
|
||||
@@ -84,7 +84,13 @@
|
||||
{ y: 80, opacity: 0, stagger: 0.045, ease: "back.out(2)", duration: 0.55 },
|
||||
0.05,
|
||||
);
|
||||
tl.to("#hero", { letterSpacing: "0.04em", duration: 0.6, ease: "power2.out" }, 0.7);
|
||||
// Letters loosen apart as they settle. Animate each glyph's x (a transform), NOT the
|
||||
// word's letter-spacing: animating letter-spacing reflows text and snaps glyph positions
|
||||
// to the pixel grid, which micro-stutters on a slow settle under the seek-by-frame
|
||||
// capture engine (the gsap_non_transform_motion lint rule flags it). The chars are
|
||||
// already per-glyph spans, so spread them directly: (0.04em − (−0.04em)) × 280px = 22.4px
|
||||
// per gap, centered about index 3 of the 7-letter word.
|
||||
tl.to(".char", { x: (i) => (i - 3) * 22.4, duration: 0.6, ease: "power2.out" }, 0.7);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user