Skip to content

Commit 6dcbb55

Browse files
authored
feat(sdk,core): phase 3b — 8 gsap/label ops + setClassStyle (#1379)
1 parent 8b56e55 commit 6dcbb55

17 files changed

Lines changed: 1440 additions & 59 deletions

packages/core/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@
8383
"import": "./src/parsers/gsapParser.ts",
8484
"types": "./src/parsers/gsapParser.ts"
8585
},
86+
"./gsap-parser-acorn": {
87+
"import": "./src/parsers/gsapParserAcorn.ts",
88+
"types": "./src/parsers/gsapParserAcorn.ts"
89+
},
90+
"./gsap-writer-acorn": {
91+
"import": "./src/parsers/gsapWriterAcorn.ts",
92+
"types": "./src/parsers/gsapWriterAcorn.ts"
93+
},
8694
"./gsap-constants": {
8795
"import": "./src/parsers/gsapConstants.ts",
8896
"types": "./src/parsers/gsapConstants.ts"
@@ -166,6 +174,14 @@
166174
"import": "./dist/parsers/gsapParser.js",
167175
"types": "./dist/parsers/gsapParser.d.ts"
168176
},
177+
"./gsap-parser-acorn": {
178+
"import": "./dist/parsers/gsapParserAcorn.js",
179+
"types": "./dist/parsers/gsapParserAcorn.d.ts"
180+
},
181+
"./gsap-writer-acorn": {
182+
"import": "./dist/parsers/gsapWriterAcorn.js",
183+
"types": "./dist/parsers/gsapWriterAcorn.d.ts"
184+
},
169185
"./gsap-constants": {
170186
"import": "./dist/parsers/gsapConstants.js",
171187
"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: 2 additions & 1 deletion
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 };
@@ -200,7 +201,6 @@ export function getAnimationsForElementId(
200201
const FORBIDDEN_GSAP_PATTERNS: Array<{ pattern: RegExp; message: string }> = [
201202
{ pattern: /\.call\s*\(/, message: "call() method not allowed" },
202203
{ pattern: /\.add\s*\(/, message: "add() method not allowed" },
203-
{ pattern: /\.addLabel\s*\(/, message: "addLabel() method not allowed" },
204204
{ pattern: /\.addPause\s*\(/, message: "addPause() method not allowed" },
205205
{ pattern: /gsap\.registerEffect\s*\(/, message: "registerEffect() not allowed" },
206206
{ pattern: /ScrollTrigger/, message: "ScrollTrigger not allowed" },
@@ -247,6 +247,7 @@ export function keyframesToGsapAnimations(
247247
const baseY = base?.y ?? 0;
248248
const baseScale = base?.scale ?? 1;
249249

250+
// fallow-ignore-next-line complexity
250251
sorted.forEach((kf, i) => {
251252
const absoluteTime = elementStartTime + kf.time;
252253
const isFirst = i === 0;

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)