Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/core/src/parsers/gsapParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,23 @@ describe("stagger/yoyo/repeat round-trip", () => {
expect(updatedScript).toContain("stagger: 0.1");
expect(updatedScript).toContain("opacity: 0.5");
});

it("apply-to-all (resetKeyframeEases) sets easeEach and strips every per-keyframe ease", () => {
const script = `
const tl = gsap.timeline({ paused: true });
tl.to("#card", { keyframes: { "0%": { x: 0 }, "30%": { x: 50, ease: "custom(M0,0 C0.333,0 0.667,1 1,1)" }, "70%": { x: 80, ease: "power2.in" }, "100%": { x: 100 }, easeEach: "power2.out" }, duration: 1 }, 0);
`;
const parsed = parseGsapScript(script);
const animId = parsed.animations[0].id;
const result = updateAnimationInScript(script, animId, {
easeEach: "back.out",
resetKeyframeEases: true,
});
expect(result).toContain('easeEach: "back.out"');
// Every per-keyframe override is gone — the single easeEach governs all segments.
expect(result).not.toContain('ease: "custom');
expect(result).not.toContain('ease: "power2.in"');
});
});

describe("unresolvable value round-trip", () => {
Expand Down
19 changes: 17 additions & 2 deletions packages/core/src/parsers/gsapParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1243,9 +1243,23 @@ function applyEaseUpdate(varsArg: AstNode, ease: string): void {
}
}

/**
* "Apply to all segments": drop every per-keyframe `ease` override so the single
* `easeEach` governs all segments uniformly (AE select-all + F9). Mirrors the
* acorn writer's resetKeyframeEases branch.
*/
function stripKeyframeEases(varsArg: AstNode): void {
const kfNode = findKeyframesObjectNode(varsArg);
const props = kfNode?.properties;
if (!Array.isArray(props)) return;
for (const entry of props) {
if (isObjectProperty(entry)) removeVarsKey(entry.value, "ease");
}
}

function applyUpdatesToCall(
call: TweenCallInfo,
updates: Partial<GsapAnimation> & { easeEach?: string },
updates: Partial<GsapAnimation> & { easeEach?: string; resetKeyframeEases?: boolean },
): void {
if (updates.properties) reconcileEditableProperties(call.varsArg, updates.properties);
if (updates.fromProperties && call.method === "fromTo" && call.fromArg) {
Expand All @@ -1254,6 +1268,7 @@ function applyUpdatesToCall(
if (updates.duration !== undefined) setVarsKey(call.varsArg, "duration", updates.duration);
if (updates.easeEach !== undefined) applyEaseUpdate(call.varsArg, updates.easeEach);
else if (updates.ease !== undefined) applyEaseUpdate(call.varsArg, updates.ease);
if (updates.resetKeyframeEases) stripKeyframeEases(call.varsArg);
if (updates.position !== undefined) {
const posIdx = call.method === "fromTo" ? 3 : 2;
call.node.arguments[posIdx] = parseExpr(valueToCode(updates.position));
Expand Down Expand Up @@ -1315,7 +1330,7 @@ function buildTweenStatementCode(timelineVar: string, anim: Omit<GsapAnimation,
export function updateAnimationInScript(
script: string,
animationId: string,
updates: Partial<GsapAnimation> & { easeEach?: string },
updates: Partial<GsapAnimation> & { easeEach?: string; resetKeyframeEases?: boolean },
): string {
let parsed: ParsedGsapAst;
try {
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/parsers/gsapWriter.acorn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
updateAnimationInScript,
updateKeyframeInScript,
} from "./gsapWriterAcorn.js";
import { parseGsapScript } from "./gsapParser.js";

// ---------------------------------------------------------------------------
// Fixture scripts
Expand Down Expand Up @@ -272,6 +273,27 @@ describe("T6c — keyframe write ops", () => {
expect(result).toContain("{ x: 1480, y: 160 }");
});

it("updateAnimationInScript apply-to-all sets easeEach and strips per-keyframe eases", () => {
const script =
"const tl = gsap.timeline();\n" +
'tl.to("#box", { keyframes: { "0%": { x: 0 }, "50%": { x: 50, ease: "power2.in" }, "100%": { x: 100, ease: "back.out" }, easeEach: "none" }, duration: 1 }, 0);';
const id = parseGsapScript(script).animations[0]!.id;
const result = updateAnimationInScript(script, id, {
easeEach: "power2.out",
resetKeyframeEases: true,
});
// easeEach updated to the chosen ease …
expect(result).toContain('easeEach: "power2.out"');
// … and every per-keyframe override is gone, so all segments use easeEach.
expect(result).not.toContain('ease: "power2.in"');
expect(result).not.toContain('ease: "back.out"');
// keyframe property values are preserved.
const kf = parseGsapScript(result).animations[0]!.keyframes!;
expect(kf.easeEach).toBe("power2.out");
expect(kf.keyframes.every((k) => k.ease === undefined)).toBe(true);
expect(kf.keyframes.map((k) => k.properties.x)).toEqual([0, 50, 100]);
});

it("addKeyframeToScript — ARRAY-form normalizes to object form + inserts 50%", () => {
const script =
"const tl = gsap.timeline();\n" +
Expand Down
20 changes: 17 additions & 3 deletions packages/core/src/parsers/gsapWriterAcorn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ function findInsertionPoint(parsed: ParsedGsapAcornForWrite): number | null {
export function updateAnimationInScript(
script: string,
animationId: string,
updates: Partial<GsapAnimation> & { easeEach?: string },
updates: Partial<GsapAnimation> & { easeEach?: string; resetKeyframeEases?: boolean },
): string {
if (!Object.keys(updates).length) return script;
const parsed = parseGsapScriptAcornForWrite(script);
Expand Down Expand Up @@ -327,8 +327,22 @@ export function updateAnimationInScript(
const easeValue = updates.easeEach ?? updates.ease;
if (easeValue !== undefined) {
const kfNode = keyframesObjectNode(call.varsArg);
if (kfNode) upsertProp(ms, kfNode, "easeEach", easeValue);
else upsertProp(ms, call.varsArg, "ease", easeValue);
if (kfNode) {
upsertProp(ms, kfNode, "easeEach", easeValue);
// "Apply to all segments": drop every per-keyframe `ease` override so the
// single easeEach governs all segments uniformly (AE select-all + F9).
if (updates.resetKeyframeEases) {
for (const kfEntry of kfNode.properties ?? []) {
if (!isObjectProperty(kfEntry)) continue;
const val = kfEntry.value;
if (val?.type !== "ObjectExpression") continue;
const easeNode = findPropertyNode(val, "ease");
if (easeNode) removeProp(ms, easeNode, val.properties);
}
}
} else {
upsertProp(ms, call.varsArg, "ease", easeValue);
}
}
if (updates.extras) {
for (const [key, value] of Object.entries(updates.extras)) {
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/studio-api/routes/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,13 @@ type GsapMutationRequest =
| {
type: "update-meta";
animationId: string;
updates: { duration?: number; ease?: string; easeEach?: string; position?: number };
updates: {
duration?: number;
ease?: string;
easeEach?: string;
position?: number;
resetKeyframeEases?: boolean;
};
}
| {
type: "add";
Expand Down
2 changes: 2 additions & 0 deletions packages/studio/src/components/StudioRightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export function StudioRightPanel({
handleUpdateArcSegment,
handleUnroll,
handleUpdateKeyframeEase,
handleSetAllKeyframeEases,
handleGsapAddKeyframe,
handleGsapRemoveKeyframe,
handleGsapConvertToKeyframes,
Expand Down Expand Up @@ -276,6 +277,7 @@ export function StudioRightPanel({
onUpdateArcSegment={handleUpdateArcSegment}
onUnroll={handleUnroll}
onUpdateKeyframeEase={handleUpdateKeyframeEase}
onSetAllKeyframeEases={handleSetAllKeyframeEases}
recordingState={recordingState}
recordingDuration={recordingDuration}
onToggleRecording={onToggleRecording}
Expand Down
6 changes: 6 additions & 0 deletions packages/studio/src/components/editor/AnimationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const AnimationCard = memo(function AnimationCard({
onSetArcPath,
onUpdateArcSegment,
onUpdateKeyframeEase,
onSetAllKeyframeEases,
onUnroll,
}: AnimationCardProps) {
const [expanded, setExpanded] = useState(defaultExpanded);
Expand Down Expand Up @@ -249,6 +250,11 @@ export const AnimationCard = memo(function AnimationCard({
expandedPct={expandedKfPct}
onToggle={setExpandedKfPct}
onEaseCommit={(pct, ease) => onUpdateKeyframeEase(animation.id, pct, ease)}
onApplyAll={
onSetAllKeyframeEases
? (ease) => onSetAllKeyframeEases(animation.id, ease)
: undefined
}
/>
) : (
<>
Expand Down
Loading
Loading