mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(studio): make GSAP tween editing work on real compositions (#1115)
The Design-panel GSAP editor only recognized tweens written as
tl.to(".selector", {...}) with inline string-literal targets, in a
contiguous block, with no interleaved setup. Every scaffolded
composition instead targets tweens through element variables
(const kicker = root.querySelector(".kicker"); tl.to(kicker, {...})),
wraps the script in an IIFE, and interleaves gsap.set() calls — so the
parser returned zero animations and the panel was inert.
Three coordinated fixes make it work end to end:
- Parser read: resolve querySelector / querySelectorAll / getElementById
variable targets (and inline lookup calls) back to their CSS selector,
so variable-targeted tweens are recognized.
- Parser write: replace the full re-serialize (preamble + tweens +
postamble) with in-place recast AST mutation. Edits now touch only the
targeted tween's vars/position node and reprint, preserving every
surrounding statement — gsap.set calls, element declarations, the IIFE
wrapper, comments and formatting. Previously the first edit would
discard all of that.
- Linter: build overlap/clip windows directly from the parser's
structured animations instead of a regex walk paired positionally with
the parsed list. The old pairing skipped variable targets and would
drift once the parser started returning them. Removes the now-dead
regex meta helpers.
- studio-api: extractGsapScriptBlock now searches inside <template>
content (sub-compositions wrap markup + the GSAP script in a template,
which linkedom's querySelectorAll doesn't descend into), and the
frontend matches tweens to the selected element by id OR selector
rather than id only (class-targeted elements have no id).
Verified end to end against a real 10-scene project: all compositions
now parse (previously 0), the panel populates editable tween cards, and
property/duration/ease edits round-trip while leaving the rest of the
script byte-for-byte intact.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
||||
import { getAnimationsForElement } from "./useGsapTweenCache";
|
||||
|
||||
function anim(targetSelector: string): GsapAnimation {
|
||||
return {
|
||||
id: `${targetSelector}-to-0`,
|
||||
targetSelector,
|
||||
method: "to",
|
||||
position: 0,
|
||||
properties: {},
|
||||
};
|
||||
}
|
||||
|
||||
describe("getAnimationsForElement", () => {
|
||||
const animations = [anim("#hero"), anim(".kicker"), anim(".kicker"), anim(".co-new")];
|
||||
|
||||
it("matches tweens by element id", () => {
|
||||
const result = getAnimationsForElement(animations, { id: "hero" });
|
||||
expect(result.map((a) => a.targetSelector)).toEqual(["#hero"]);
|
||||
});
|
||||
|
||||
it("matches class-targeted tweens by the element's selector", () => {
|
||||
// Real compositions target tweens by class (querySelector(".kicker")); the
|
||||
// selected element has no id, so id-only matching would miss these.
|
||||
const result = getAnimationsForElement(animations, { id: null, selector: ".kicker" });
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.every((a) => a.targetSelector === ".kicker")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches by id or selector when both are present", () => {
|
||||
const result = getAnimationsForElement(animations, { id: "hero", selector: ".co-new" });
|
||||
expect(result.map((a) => a.targetSelector).sort()).toEqual(["#hero", ".co-new"]);
|
||||
});
|
||||
|
||||
it("returns nothing when neither id nor selector is provided", () => {
|
||||
expect(getAnimationsForElement(animations, {})).toEqual([]);
|
||||
expect(getAnimationsForElement(animations, { id: null, selector: null })).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user