Skip to content

Commit 64a8658

Browse files
refactor(studio): revert to absolute GSAP positions + shift-on-drag
Reverts the clip-relative position model and replaces it with absolute positions that Studio rewrites atomically when clips are dragged or left-edge-resized. - Remove applyClipRelativeOffsets runtime machinery and sentinel gate - Revert Studio hooks to write absolute positions - Revert globalTimeCompiler, AnimationCard, parser, keyframe cache - Add shiftGsapPositionsInHtml for atomic position shifting on drag/resize Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
1 parent 6f9ff69 commit 64a8658

22 files changed

Lines changed: 206 additions & 241 deletions

packages/core/src/parsers/gsapParser.test.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -585,20 +585,20 @@ describe("unresolvable value round-trip", () => {
585585
});
586586

587587
describe("gsapAnimationsToKeyframes", () => {
588-
it("converts clip-relative animations to keyframes", () => {
588+
it("converts animations to keyframes with element start offset", () => {
589589
const animations: GsapAnimation[] = [
590590
{
591591
id: "anim-1",
592592
targetSelector: "#el1",
593593
method: "set",
594-
position: 0,
594+
position: 2,
595595
properties: { x: 100, y: 200 },
596596
},
597597
{
598598
id: "anim-2",
599599
targetSelector: "#el1",
600600
method: "to",
601-
position: 1,
601+
position: 3,
602602
properties: { x: 300, y: 400 },
603603
duration: 1,
604604
ease: "power2.out",
@@ -608,9 +608,11 @@ describe("gsapAnimationsToKeyframes", () => {
608608
const keyframes = gsapAnimationsToKeyframes(animations, 2);
609609

610610
expect(keyframes).toHaveLength(2);
611+
// First keyframe: time = 2 - 2 = 0
611612
expect(keyframes[0].time).toBe(0);
612613
expect(keyframes[0].properties.x).toBe(100);
613614
expect(keyframes[0].properties.y).toBe(200);
615+
// Second keyframe: time = 3 - 2 = 1
614616
expect(keyframes[1].time).toBe(1);
615617
expect(keyframes[1].properties.x).toBe(300);
616618
expect(keyframes[1].ease).toBe("power2.out");
@@ -648,14 +650,14 @@ describe("gsapAnimationsToKeyframes", () => {
648650
id: "anim-1",
649651
targetSelector: "#el1",
650652
method: "set",
651-
position: 0,
653+
position: 5,
652654
properties: { x: 0, y: 0 },
653655
},
654656
{
655657
id: "anim-2",
656658
targetSelector: "#el1",
657659
method: "to",
658-
position: 1,
660+
position: 6,
659661
properties: { x: 100 },
660662
duration: 1,
661663
},
@@ -737,10 +739,10 @@ describe("keyframesToGsapAnimations", () => {
737739

738740
expect(animations).toHaveLength(2);
739741
expect(animations[0].method).toBe("set");
740-
expect(animations[0].position).toBe(0); // clip-relative
742+
expect(animations[0].position).toBe(2); // elementStartTime + 0
741743
expect(animations[0].properties.opacity).toBe(0);
742744
expect(animations[1].method).toBe("to");
743-
expect(animations[1].position).toBe(0); // prev keyframe time (clip-relative)
745+
expect(animations[1].position).toBe(2); // position of prev keyframe
744746
expect(animations[1].duration).toBe(1); // kf.time - prevKf.time
745747
expect(animations[1].ease).toBe("power2.out");
746748
});
@@ -1977,7 +1979,7 @@ describe("splitAnimationsInScript", () => {
19771979
expect(forNew).toHaveLength(1);
19781980
expect(forNew[0]!.method).toBe("set");
19791981
expect(forNew[0]!.properties.x).toBe(100);
1980-
expect(forNew[0]!.position).toBe(0);
1982+
expect(forNew[0]!.position).toBe(opts.splitTime);
19811983
});
19821984

19831985
it("retargets animation entirely in second half to new element", () => {
@@ -2125,7 +2127,7 @@ tl.to("#el1", { y: 200, duration: 1 }, 3);`;
21252127
expect(forOriginal.length).toBe(0);
21262128
expect(forNew.length).toBe(1);
21272129
expect(forNew[0]!.method).toBe("set");
2128-
expect(forNew[0]!.position).toBe(1);
2130+
expect(forNew[0]!.position).toBe(3);
21292131
});
21302132

21312133
it("inserts inherited state set before other tweens targeting new element", () => {

packages/core/src/parsers/gsapParser.ts

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,9 +1253,6 @@ function applyUpdatesToCall(call: TweenCallInfo, updates: Partial<GsapAnimation>
12531253
const posIdx = call.method === "fromTo" ? 3 : 2;
12541254
call.node.arguments[posIdx] = parseExpr(valueToCode(updates.position));
12551255
}
1256-
if (updates.targetSelector !== undefined) {
1257-
call.node.arguments[0] = parseExpr(valueToCode(updates.targetSelector));
1258-
}
12591256
}
12601257

12611258
/** Walk up to the enclosing ExpressionStatement path (for prune / insertAfter). */
@@ -1325,6 +1322,24 @@ export function updateAnimationInScript(
13251322
return recast.print(parsed.ast).code;
13261323
}
13271324

1325+
function updateAnimationSelector(script: string, animationId: string, newSelector: string): string {
1326+
let parsed: ParsedGsapAst;
1327+
try {
1328+
parsed = parseGsapAst(script);
1329+
} catch {
1330+
return script;
1331+
}
1332+
const target = parsed.located.find((l) => l.id === animationId);
1333+
if (!target) return script;
1334+
const selectorArg = target.call.path.node.arguments?.[0];
1335+
if (selectorArg?.type === "StringLiteral") {
1336+
selectorArg.value = newSelector;
1337+
} else if (selectorArg?.type === "Identifier") {
1338+
target.call.path.node.arguments[0] = { type: "StringLiteral", value: newSelector };
1339+
}
1340+
return recast.print(parsed.ast).code;
1341+
}
1342+
13281343
export function addAnimationToScript(
13291344
script: string,
13301345
animation: Omit<GsapAnimation, "id">,
@@ -1522,7 +1537,7 @@ export function splitAnimationsInScript(
15221537
if (matching.length === 0) return { script, skippedSelectors };
15231538

15241539
let result = script;
1525-
const relSplitTime = opts.splitTime - opts.elementStart;
1540+
const newElementStart = opts.splitTime;
15261541
const inheritedProps: Record<string, number | string> = {};
15271542

15281543
// Reverse iteration: updateAnimationSelector mutates selectors in the source
@@ -1535,19 +1550,16 @@ export function splitAnimationsInScript(
15351550
const animEnd = pos + dur;
15361551

15371552
if (anim.keyframes) {
1538-
if (pos >= relSplitTime) {
1539-
result = updateAnimationInScript(result, anim.id, {
1540-
position: pos - relSplitTime,
1541-
targetSelector: newSelector,
1542-
});
1543-
} else if (animEnd > relSplitTime) {
1553+
if (pos >= opts.splitTime) {
1554+
result = updateAnimationSelector(result, anim.id, newSelector);
1555+
} else if (animEnd > opts.splitTime) {
15441556
// Spanning keyframes can't be correctly split without renormalizing
15451557
// percentages and durations — leave on original, warn the caller.
15461558
skippedSelectors.push(`${originalSelector} (keyframes spanning split)`);
15471559
const kfs = anim.keyframes.keyframes;
15481560
for (const kf of kfs) {
15491561
const kfTime = pos + (kf.percentage / 100) * dur;
1550-
if (kfTime <= relSplitTime) {
1562+
if (kfTime <= opts.splitTime) {
15511563
for (const [k, v] of Object.entries(kf.properties)) {
15521564
inheritedProps[k] = v;
15531565
}
@@ -1565,18 +1577,15 @@ export function splitAnimationsInScript(
15651577
continue;
15661578
}
15671579

1568-
if (animEnd <= relSplitTime) {
1580+
if (animEnd <= opts.splitTime) {
15691581
for (const [k, v] of Object.entries(anim.properties)) {
15701582
inheritedProps[k] = v;
15711583
}
15721584
continue;
15731585
}
15741586

1575-
if (pos >= relSplitTime) {
1576-
result = updateAnimationInScript(result, anim.id, {
1577-
position: pos - relSplitTime,
1578-
targetSelector: newSelector,
1579-
});
1587+
if (pos >= opts.splitTime) {
1588+
result = updateAnimationSelector(result, anim.id, newSelector);
15801589
continue;
15811590
}
15821591

@@ -1585,7 +1594,7 @@ export function splitAnimationsInScript(
15851594
// For .fromTo() tweens we have explicit from-values; for .to() tweens
15861595
// we use accumulated state from prior animations, defaulting to 0 for
15871596
// unknown numeric properties (the standard GSAP transform initial state).
1588-
const progress = dur > 0 ? (relSplitTime - pos) / dur : 0;
1597+
const progress = dur > 0 ? (opts.splitTime - pos) / dur : 0;
15891598
const fromSource = anim.fromProperties ?? inheritedProps;
15901599
const midProps: Record<string, number | string> = {};
15911600
for (const [k, v] of Object.entries(anim.properties)) {
@@ -1597,17 +1606,17 @@ export function splitAnimationsInScript(
15971606
midProps[k] = fromVal + (v - fromVal) * progress;
15981607
}
15991608

1600-
const firstHalfDuration = relSplitTime - pos;
1609+
const firstHalfDuration = opts.splitTime - pos;
16011610
result = updateAnimationInScript(result, anim.id, {
16021611
duration: firstHalfDuration,
16031612
properties: midProps,
16041613
});
16051614

1606-
const secondHalfDuration = animEnd - relSplitTime;
1615+
const secondHalfDuration = animEnd - opts.splitTime;
16071616
const addResult = addAnimationToScript(result, {
16081617
targetSelector: newSelector,
16091618
method: "fromTo",
1610-
position: 0,
1619+
position: newElementStart,
16111620
duration: secondHalfDuration,
16121621
properties: { ...anim.properties },
16131622
fromProperties: { ...midProps },
@@ -1622,7 +1631,7 @@ export function splitAnimationsInScript(
16221631
}
16231632

16241633
if (Object.keys(inheritedProps).length > 0) {
1625-
result = insertInheritedStateSet(result, newSelector, 0, inheritedProps);
1634+
result = insertInheritedStateSet(result, newSelector, newElementStart, inheritedProps);
16261635
}
16271636

16281637
return { script: result, skippedSelectors };

packages/core/src/parsers/gsapSerialize.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export function validateCompositionGsap(script: string): ValidationResult {
238238
export function keyframesToGsapAnimations(
239239
elementId: string,
240240
keyframes: Keyframe[],
241-
_elementStartTime: number,
241+
elementStartTime: number,
242242
base?: { x?: number; y?: number; scale?: number },
243243
): GsapAnimation[] {
244244
const sorted = [...keyframes].sort((a, b) => a.time - b.time);
@@ -248,10 +248,11 @@ export function keyframesToGsapAnimations(
248248
const baseScale = base?.scale ?? 1;
249249

250250
sorted.forEach((kf, i) => {
251+
const absoluteTime = elementStartTime + kf.time;
251252
const isFirst = i === 0;
252253
const prevKf = i > 0 ? sorted[i - 1] : null;
253254
const duration = prevKf ? kf.time - prevKf.time : undefined;
254-
const position = prevKf ? prevKf.time : kf.time;
255+
const position = prevKf ? elementStartTime + prevKf.time : absoluteTime;
255256

256257
const properties: Record<string, number | string> = {};
257258
for (const [key, value] of Object.entries(kf.properties)) {
@@ -278,7 +279,7 @@ export function keyframesToGsapAnimations(
278279

279280
export function gsapAnimationsToKeyframes(
280281
animations: GsapAnimation[],
281-
_elementStartTime: number,
282+
elementStartTime: number,
282283
options?: {
283284
baseX?: number;
284285
baseY?: number;
@@ -302,7 +303,8 @@ export function gsapAnimationsToKeyframes(
302303
validMethods.includes(a.method) && typeof a.position === "number",
303304
)
304305
.map((a) => {
305-
const time = clampTimeToZero ? Math.max(0, a.position) : a.position;
306+
const relativeTimeRaw = a.position - elementStartTime;
307+
const time = clampTimeToZero ? Math.max(0, relativeTimeRaw) : relativeTimeRaw;
306308

307309
const properties: Partial<KeyframeProperties> = {};
308310
for (const [key, value] of Object.entries(a.properties)) {

packages/core/src/runtime/clipRelativeOffsets.test.ts

Lines changed: 0 additions & 84 deletions
This file was deleted.

packages/core/src/runtime/clipRelativeOffsets.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)