Skip to content

Commit 74fcd32

Browse files
committed
feat(sdk,core): phase 3b — 8 gsap/label ops + setClassStyle
1 parent ea74cb7 commit 74fcd32

17 files changed

Lines changed: 1477 additions & 93 deletions

packages/core/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@
7474
"import": "./src/parsers/gsapParser.ts",
7575
"types": "./src/parsers/gsapParser.ts"
7676
},
77+
"./gsap-parser-acorn": {
78+
"import": "./src/parsers/gsapParserAcorn.ts",
79+
"types": "./src/parsers/gsapParserAcorn.ts"
80+
},
81+
"./gsap-writer-acorn": {
82+
"import": "./src/parsers/gsapWriterAcorn.ts",
83+
"types": "./src/parsers/gsapWriterAcorn.ts"
84+
},
7785
"./gsap-constants": {
7886
"import": "./src/parsers/gsapConstants.ts",
7987
"types": "./src/parsers/gsapConstants.ts"
@@ -153,6 +161,14 @@
153161
"import": "./dist/parsers/gsapParser.js",
154162
"types": "./dist/parsers/gsapParser.d.ts"
155163
},
164+
"./gsap-parser-acorn": {
165+
"import": "./dist/parsers/gsapParserAcorn.js",
166+
"types": "./dist/parsers/gsapParserAcorn.d.ts"
167+
},
168+
"./gsap-writer-acorn": {
169+
"import": "./dist/parsers/gsapWriterAcorn.js",
170+
"types": "./dist/parsers/gsapWriterAcorn.d.ts"
171+
},
156172
"./gsap-constants": {
157173
"import": "./dist/parsers/gsapConstants.js",
158174
"types": "./dist/parsers/gsapConstants.d.ts"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// fallow-ignore-file duplication
1+
// fallow-ignore-file code-duplication
22
/**
33
* T6b — acorn vs golden differential harness.
44
*

packages/core/src/parsers/gsapParserAcorn.full.test.ts

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// fallow-ignore-file duplication
1+
// fallow-ignore-file code-duplication
22
/**
33
* T6d: parse-parity suite — runs the full gsapParser.test.ts parse scenarios
44
* against parseGsapScriptAcorn. Write-path tests are it.skip'd; those live
@@ -912,3 +912,140 @@ describe("native GSAP keyframes parsing", () => {
912912
expect(Object.keys(anim.properties)).toHaveLength(0);
913913
});
914914
});
915+
916+
// ── motionPath parsing ────────────────────────────────────────────────────────
917+
918+
describe("motionPath parsing", () => {
919+
it("parses motionPath with waypoint array and curviness", () => {
920+
const script = `
921+
const tl = gsap.timeline({ paused: true });
922+
tl.to("#el", {
923+
motionPath: {
924+
path: [{x: 0, y: 0}, {x: 200, y: -100}, {x: 400, y: 50}],
925+
curviness: 1.5
926+
},
927+
duration: 2
928+
}, 0);
929+
`;
930+
const result = parseGsapScript(script);
931+
expect(result.animations).toHaveLength(1);
932+
const anim = result.animations[0];
933+
934+
expect(anim.arcPath).toBeDefined();
935+
expect(anim.arcPath!.enabled).toBe(true);
936+
expect(anim.arcPath!.segments).toHaveLength(2);
937+
expect(anim.arcPath!.segments[0].curviness).toBe(1.5);
938+
expect(anim.arcPath!.segments[1].curviness).toBe(1.5);
939+
940+
expect(anim.keyframes).toBeDefined();
941+
expect(anim.keyframes!.keyframes).toHaveLength(3);
942+
expect(anim.keyframes!.keyframes[0].properties.x).toBe(0);
943+
expect(anim.keyframes!.keyframes[0].properties.y).toBe(0);
944+
expect(anim.keyframes!.keyframes[1].properties.x).toBe(200);
945+
expect(anim.keyframes!.keyframes[1].properties.y).toBe(-100);
946+
expect(anim.keyframes!.keyframes[2].properties.x).toBe(400);
947+
expect(anim.keyframes!.keyframes[2].properties.y).toBe(50);
948+
});
949+
950+
it("parses motionPath with type cubic and explicit control points", () => {
951+
const script = `
952+
const tl = gsap.timeline({ paused: true });
953+
tl.to("#el", {
954+
motionPath: {
955+
path: [
956+
{x: 0, y: 0},
957+
{x: 50, y: -80}, {x: 150, y: -120},
958+
{x: 200, y: -100},
959+
{x: 250, y: -80}, {x: 350, y: 30},
960+
{x: 400, y: 50}
961+
],
962+
type: "cubic"
963+
},
964+
duration: 2
965+
}, 0);
966+
`;
967+
const result = parseGsapScript(script);
968+
const anim = result.animations[0];
969+
970+
expect(anim.arcPath).toBeDefined();
971+
expect(anim.arcPath!.segments).toHaveLength(2);
972+
973+
expect(anim.arcPath!.segments[0].cp1).toEqual({ x: 50, y: -80 });
974+
expect(anim.arcPath!.segments[0].cp2).toEqual({ x: 150, y: -120 });
975+
976+
expect(anim.arcPath!.segments[1].cp1).toEqual({ x: 250, y: -80 });
977+
expect(anim.arcPath!.segments[1].cp2).toEqual({ x: 350, y: 30 });
978+
979+
expect(anim.keyframes!.keyframes).toHaveLength(3);
980+
expect(anim.keyframes!.keyframes[0].properties).toEqual({ x: 0, y: 0 });
981+
expect(anim.keyframes!.keyframes[1].properties).toEqual({ x: 200, y: -100 });
982+
expect(anim.keyframes!.keyframes[2].properties).toEqual({ x: 400, y: 50 });
983+
});
984+
985+
it("parses motionPath with autoRotate", () => {
986+
const script = `
987+
const tl = gsap.timeline({ paused: true });
988+
tl.to("#el", {
989+
motionPath: {
990+
path: [{x: 0, y: 0}, {x: 200, y: 100}],
991+
autoRotate: true
992+
},
993+
duration: 1
994+
}, 0);
995+
`;
996+
const result = parseGsapScript(script);
997+
const anim = result.animations[0];
998+
expect(anim.arcPath!.autoRotate).toBe(true);
999+
});
1000+
1001+
it("merges motionPath waypoints into existing keyframes", () => {
1002+
const script = `
1003+
const tl = gsap.timeline({ paused: true });
1004+
tl.to("#el", {
1005+
motionPath: {
1006+
path: [{x: 0, y: 0}, {x: 200, y: 100}],
1007+
curviness: 2
1008+
},
1009+
keyframes: {
1010+
"0%": { opacity: 1 },
1011+
"100%": { opacity: 0 }
1012+
},
1013+
duration: 2
1014+
}, 0);
1015+
`;
1016+
const result = parseGsapScript(script);
1017+
const anim = result.animations[0];
1018+
1019+
expect(anim.arcPath).toBeDefined();
1020+
expect(anim.arcPath!.segments).toHaveLength(1);
1021+
expect(anim.arcPath!.segments[0].curviness).toBe(2);
1022+
1023+
expect(anim.keyframes!.keyframes).toHaveLength(2);
1024+
expect(anim.keyframes!.keyframes[0].properties).toEqual({ opacity: 1, x: 0, y: 0 });
1025+
expect(anim.keyframes!.keyframes[1].properties).toEqual({ opacity: 0, x: 200, y: 100 });
1026+
});
1027+
1028+
it("skips motionPath with fewer than 2 waypoints", () => {
1029+
const script = `
1030+
const tl = gsap.timeline({ paused: true });
1031+
tl.to("#el", {
1032+
motionPath: { path: [{x: 0, y: 0}] },
1033+
duration: 1
1034+
}, 0);
1035+
`;
1036+
const result = parseGsapScript(script);
1037+
expect(result.animations[0].arcPath).toBeUndefined();
1038+
});
1039+
1040+
it("tween without motionPath parses identically to before", () => {
1041+
const script = `
1042+
const tl = gsap.timeline({ paused: true });
1043+
tl.to("#el", { x: 100, y: 200, duration: 1 }, 0);
1044+
`;
1045+
const result = parseGsapScript(script);
1046+
const anim = result.animations[0];
1047+
expect(anim.arcPath).toBeUndefined();
1048+
expect(anim.properties.x).toBe(100);
1049+
expect(anim.properties.y).toBe(200);
1050+
});
1051+
});

packages/core/src/parsers/gsapParserAcorn.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// fallow-ignore-file duplication
1+
// fallow-ignore-file code-duplication
22
/**
33
* Browser-safe GSAP read path — acorn + acorn-walk.
44
*
@@ -1046,6 +1046,7 @@ function assignStableIds(anims: Omit<GsapAnimation, "id">[]): GsapAnimation[] {
10461046
export interface ParsedGsapAcornForWrite {
10471047
ast: any;
10481048
timelineVar: string;
1049+
hasTimeline: boolean;
10491050
located: Array<{ id: string; call: TweenCallInfo; animation: GsapAnimation }>;
10501051
}
10511052

@@ -1075,7 +1076,7 @@ export function parseGsapScriptAcornForWrite(script: string): ParsedGsapAcornFor
10751076
call,
10761077
animation: animations[i]!,
10771078
}));
1078-
return { ast, timelineVar, located };
1079+
return { ast, timelineVar, hasTimeline: detection.timelineVar !== null, located };
10791080
} catch {
10801081
return null;
10811082
}

packages/core/src/parsers/gsapSerialize.ts

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export function serializeGsapAnimations(
9191
b.resolvedStart ?? (typeof b.position === "number" ? b.position : Number.MAX_SAFE_INTEGER);
9292
return aNum - bNum;
9393
});
94+
// fallow-ignore-next-line complexity
9495
const lines = sorted.map((anim) => {
9596
const selector = `"${anim.targetSelector}"`;
9697
const props: Record<string, number | string> = { ...anim.properties };
@@ -198,7 +199,6 @@ export function getAnimationsForElementId(
198199
const FORBIDDEN_GSAP_PATTERNS: Array<{ pattern: RegExp; message: string }> = [
199200
{ pattern: /\.call\s*\(/, message: "call() method not allowed" },
200201
{ pattern: /\.add\s*\(/, message: "add() method not allowed" },
201-
{ pattern: /\.addLabel\s*\(/, message: "addLabel() method not allowed" },
202202
{ pattern: /\.addPause\s*\(/, message: "addPause() method not allowed" },
203203
{ pattern: /gsap\.registerEffect\s*\(/, message: "registerEffect() not allowed" },
204204
{ pattern: /ScrollTrigger/, message: "ScrollTrigger not allowed" },
@@ -245,6 +245,7 @@ export function keyframesToGsapAnimations(
245245
const baseY = base?.y ?? 0;
246246
const baseScale = base?.scale ?? 1;
247247

248+
// fallow-ignore-next-line complexity
248249
sorted.forEach((kf, i) => {
249250
const absoluteTime = elementStartTime + kf.time;
250251
const isFirst = i === 0;
@@ -295,41 +296,44 @@ export function gsapAnimationsToKeyframes(
295296
const baseTimeEpsilon = 0.001;
296297
const baseValueEpsilon = 0.00001;
297298

298-
return animations
299-
.filter((a) => validMethods.includes(a.method) && typeof a.position === "number")
300-
.map((a) => {
301-
const relativeTimeRaw = (a.position as number) - elementStartTime;
302-
const time = clampTimeToZero ? Math.max(0, relativeTimeRaw) : relativeTimeRaw;
303-
304-
const properties: Partial<KeyframeProperties> = {};
305-
for (const [key, value] of Object.entries(a.properties)) {
306-
if (typeof value !== "number") continue;
307-
if (key === "x") properties.x = value - baseX;
308-
else if (key === "y") properties.y = value - baseY;
309-
else if (key === "scale") {
310-
properties.scale = baseScale !== 0 ? value / baseScale : value;
311-
} else {
312-
(properties as Record<string, number>)[key] = value;
299+
return (
300+
animations
301+
.filter((a) => validMethods.includes(a.method) && typeof a.position === "number")
302+
// fallow-ignore-next-line complexity
303+
.map((a) => {
304+
const relativeTimeRaw = (a.position as number) - elementStartTime;
305+
const time = clampTimeToZero ? Math.max(0, relativeTimeRaw) : relativeTimeRaw;
306+
307+
const properties: Partial<KeyframeProperties> = {};
308+
for (const [key, value] of Object.entries(a.properties)) {
309+
if (typeof value !== "number") continue;
310+
if (key === "x") properties.x = value - baseX;
311+
else if (key === "y") properties.y = value - baseY;
312+
else if (key === "scale") {
313+
properties.scale = baseScale !== 0 ? value / baseScale : value;
314+
} else {
315+
(properties as Record<string, number>)[key] = value;
316+
}
313317
}
314-
}
315318

316-
if (
317-
skipBaseSet &&
318-
a.method === "set" &&
319-
time < baseTimeEpsilon &&
320-
Object.values(properties).every(
321-
(v) => typeof v === "number" && Math.abs(v) < baseValueEpsilon,
322-
)
323-
) {
324-
return null;
325-
}
319+
if (
320+
skipBaseSet &&
321+
a.method === "set" &&
322+
time < baseTimeEpsilon &&
323+
Object.values(properties).every(
324+
(v) => typeof v === "number" && Math.abs(v) < baseValueEpsilon,
325+
)
326+
) {
327+
return null;
328+
}
326329

327-
return {
328-
id: a.id.replace(/^.*-kf-/, ""),
329-
time,
330-
properties: properties as KeyframeProperties,
331-
ease: a.ease,
332-
};
333-
})
334-
.filter((kf): kf is NonNullable<typeof kf> => kf !== null) as Keyframe[];
330+
return {
331+
id: a.id.replace(/^.*-kf-/, ""),
332+
time,
333+
properties: properties as KeyframeProperties,
334+
ease: a.ease,
335+
};
336+
})
337+
.filter((kf): kf is NonNullable<typeof kf> => kf !== null) as Keyframe[]
338+
);
335339
}

packages/core/src/parsers/gsapWriter.acorn.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// fallow-ignore-file duplication
1+
// fallow-ignore-file code-duplication
22
/**
33
* T6c — acorn write path with magic-string offset-splice.
44
*

0 commit comments

Comments
 (0)