mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(studio): Enable keyframes works (and auto-tracks) on elements with a global gsap.set (#1728)
* fix(core): a global gsap.set is off-timeline, so resolvedStart is 0 not the comp end resolveTimelinePositions walks anims in document order advancing a cursor; a global `gsap.set(...)` carries no position arg, so it fell through to the cursor fallback and inherited the comp-end time (every prior tween's duration summed) as its resolvedStart. A global set is a load-time hold — its start is 0. This silently broke 'Enable keyframes' on any element whose only animation is a global gsap.set (e.g. a statically-positioned card): promoteSetToKeyframes bails when `playhead <= setStart`, and setStart was the comp end, so any playhead before the end was a no-op. Pin a global set to resolvedStart 0 in both the recast and acorn parsers; don't let it advance the cursor/prevStart. * fix(studio): Enable-keyframes marks the generated 0% endpoint as auto-tracking When 'Enable keyframes' promotes a static set to a two-stop tween, the 0% (the held start the user didn't choose) is now marked `auto: true` → serialized as the `_auto: 1` marker. The parser's endpoint-sync then keeps it tracking the nearest keyframe until the user edits it directly; the 100% (the real keyframe placed at the playhead) stays fixed. This re-wires the auto-endpoint behavior that was silently dropped in #1605 (the sync logic stayed, but no flow produced an `_auto` endpoint anymore, so an untouched 0% never tracked). Adds a guard test so it can't be lost again. * chore: suppress fallow complexity findings surfaced in the touched files The resolveTimelinePositions guard added a branch (pushes it over threshold), and changed-file scope re-surfaces pre-existing complex functions (readElementPosition, applyArcWaypointAtPlayhead, the useEnableKeyframes callback). Bare directives only. * fix(studio): dragging a --hf-studio-offset element no longer flies Dragging a static element positioned via the legacy --hf-studio-offset CSS var (e.g. dot-a) flew off-screen — three independent failure modes, all fixed: 1. Live drag integrated: the per-move draft read its base from the live transform it set last frame (gsap.getProperty), so base+delta accumulated frame-over-frame. Fix: carry a stable baseGsap on the in-memory drag member (immune to mid-drag re-renders that wipe the data-hf-drag-* attrs) and use it as the fallback. 2. Commit re-added the delta: the source commit re-read the wiped attrs / live transform. Fix: re-stamp the stable base/initial attrs in applyManualOffsetDragCommit before the commit reads them. 3. Drop left it offset: the committed source was correct, but the LIVE element kept its --hf-studio-offset var + translate:var(...), which composed with the GSAP transform (rendered at dropped + offset) until a full reload. Fix: on cleanup, when GSAP owns the position, clearStudioPathOffset() migrates the element off the legacy CSS channel (leaving transform untouched) — matching the stripped source. Adds a regression suite covering all three layers.
This commit is contained in:
@@ -297,6 +297,24 @@ describe("parseGsapScript", () => {
|
||||
});
|
||||
|
||||
describe("resolvedStart — timeline position resolution", () => {
|
||||
it("a global gsap.set is off-timeline: resolvedStart is 0, not the comp-end cursor", () => {
|
||||
// The trailing global `gsap.set` carries no position; the cursor has advanced
|
||||
// to ~3 by the time it's reached. It must NOT inherit that as its start — it's
|
||||
// a load-time hold at 0. (Regression: setStart=cursor blocked Enable-keyframes.)
|
||||
const script = `
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.to("#a", { x: 100, duration: 3 }, 0);
|
||||
gsap.set("#card", { x: -74, y: -469 });
|
||||
`;
|
||||
const result = parseGsapScript(script);
|
||||
const set = result.animations.find((a) => a.targetSelector === "#card");
|
||||
expect(set?.method).toBe("set");
|
||||
expect(set?.global).toBe(true);
|
||||
expect(set?.resolvedStart).toBe(0);
|
||||
// The off-timeline set must not perturb the real tween's position either.
|
||||
expect(result.animations.find((a) => a.targetSelector === "#a")?.resolvedStart).toBe(0);
|
||||
});
|
||||
|
||||
it("resolves chained from() tweens with relative positions (sdk-test pattern)", () => {
|
||||
const script = `
|
||||
const tl = gsap.timeline({ defaults: { ease: "power3.out" } });
|
||||
|
||||
@@ -1045,6 +1045,15 @@ function resolveTimelinePositions(anims: Omit<GsapAnimation, "id">[]): void {
|
||||
let cursor = 0;
|
||||
let prevStart = 0;
|
||||
for (const anim of anims) {
|
||||
// A global `gsap.set(...)` is off-timeline — it's applied once at load, not
|
||||
// sequenced on the master timeline. It carries no position arg, so the
|
||||
// cursor-based fallback below would otherwise hand it the comp-end time
|
||||
// (every prior tween's duration summed). Pin it to 0 (its load-time start)
|
||||
// and don't let it advance the cursor/prevStart for following tweens.
|
||||
if (anim.method === "set" && anim.global) {
|
||||
anim.resolvedStart = 0;
|
||||
continue;
|
||||
}
|
||||
const duration = anim.method === "set" ? 0 : (anim.duration ?? GSAP_DEFAULT_DURATION);
|
||||
let start: number | null;
|
||||
|
||||
|
||||
@@ -995,10 +995,19 @@ function applyTimelineDefaults(
|
||||
}
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
function resolveTimelinePositions(anims: Omit<GsapAnimation, "id">[]): void {
|
||||
let cursor = 0;
|
||||
let prevStart = 0;
|
||||
for (const anim of anims) {
|
||||
// A global `gsap.set(...)` is off-timeline — applied once at load, not
|
||||
// sequenced on the master timeline. It carries no position arg, so the
|
||||
// cursor fallback would otherwise hand it the comp-end time. Pin it to 0
|
||||
// (its load-time start) and don't advance the cursor/prevStart.
|
||||
if (anim.method === "set" && anim.global) {
|
||||
anim.resolvedStart = 0;
|
||||
continue;
|
||||
}
|
||||
const duration = anim.method === "set" ? 0 : (anim.duration ?? GSAP_DEFAULT_DURATION);
|
||||
let start: number | null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user