Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/studio/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ export function StudioApp() {
panelLayout.rightInspectorPanes,
panelLayout.rightCollapsed,
isPlaying,
domEditSession.domEditSelection,
gestureState === "recording",
);
useStudioUrlState({
Expand Down
122 changes: 122 additions & 0 deletions packages/studio/src/components/editor/gsapAnimationCallbacks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, expect, it, vi } from "vitest";
import {
type GsapAnimationEditCallbacks,
withTrackedGsapAnimationCallbacks,
} from "./gsapAnimationCallbacks";

function requiredCallbacks(): GsapAnimationEditCallbacks {
return {
onUpdateProperty: vi.fn(),
onUpdateMeta: vi.fn(),
onDeleteAnimation: vi.fn(),
onAddProperty: vi.fn(),
onRemoveProperty: vi.fn(),
};
}

function requireCallback<T>(callback: T | undefined): T {
if (callback === undefined) throw new Error("expected callback to be present");
return callback;
}

describe("withTrackedGsapAnimationCallbacks", () => {
it("keeps absent optional callbacks absent and passes preview callbacks through unchanged", () => {
const callbacks = requiredCallbacks();
const onLivePreview = vi.fn();
const onLivePreviewEnd = vi.fn();
callbacks.onLivePreview = onLivePreview;
callbacks.onLivePreviewEnd = onLivePreviewEnd;

const tracked = withTrackedGsapAnimationCallbacks(callbacks, vi.fn());

expect(tracked.onUpdateFromProperty).toBeUndefined();
expect(tracked.onAddFromProperty).toBeUndefined();
expect(tracked.onRemoveFromProperty).toBeUndefined();
expect(tracked.onSetArcPath).toBeUndefined();
expect(tracked.onUpdateArcSegment).toBeUndefined();
expect(tracked.onUpdateKeyframeEase).toBeUndefined();
expect(tracked.onSetAllKeyframeEases).toBeUndefined();
expect(tracked.onUnroll).toBeUndefined();
expect(tracked.onLivePreview).toBe(onLivePreview);
expect(tracked.onLivePreviewEnd).toBe(onLivePreviewEnd);
});

it("tracks each edit once before invoking its mutation callback", () => {
const events: string[] = [];
const mutation = (name: string) => () => events.push(`mutate:${name}`);
const callbacks: GsapAnimationEditCallbacks = {
onUpdateProperty: mutation("update-property"),
onUpdateMeta: mutation("update-meta"),
onDeleteAnimation: mutation("delete"),
onAddProperty: mutation("add-property"),
onRemoveProperty: mutation("remove-property"),
onUpdateFromProperty: mutation("update-from"),
onAddFromProperty: mutation("add-from"),
onRemoveFromProperty: mutation("remove-from"),
onSetArcPath: mutation("arc-path"),
onUpdateArcSegment: mutation("arc-segment"),
onUpdateKeyframeEase: mutation("keyframe-ease"),
onSetAllKeyframeEases: mutation("all-eases"),
onUnroll: mutation("unroll"),
};
const tracked = withTrackedGsapAnimationCallbacks(callbacks, (control, name) => {
events.push(`track:${control}:${name}`);
});

tracked.onUpdateProperty("a1", "visibility", 1);
tracked.onUpdateProperty("a1", "filter", "blur(2px)");
tracked.onUpdateProperty("a1", "opacity", 0.5);
tracked.onUpdateMeta("a1", { duration: 2, ease: "none", position: 1 });
tracked.onDeleteAnimation("a1");
tracked.onAddProperty("a1", "scale");
tracked.onRemoveProperty("a1", "scale");
requireCallback(tracked.onUpdateFromProperty)("a1", "clipPath", "none");
requireCallback(tracked.onAddFromProperty)("a1", "x");
requireCallback(tracked.onRemoveFromProperty)("a1", "x");
requireCallback(tracked.onSetArcPath)("a1", { enabled: true });
requireCallback(tracked.onSetArcPath)("a1", { enabled: true, autoRotate: true });
requireCallback(tracked.onUpdateArcSegment)("a1", 1, {});
requireCallback(tracked.onUpdateArcSegment)("a1", 1, { curviness: 0.5 });
requireCallback(tracked.onUpdateKeyframeEase)("a1", 50, "power2.out");
requireCallback(tracked.onSetAllKeyframeEases)("a1", "none");
requireCallback(tracked.onUnroll)("a1");

expect(events).toEqual([
"track:toggle:visibility",
"mutate:update-property",
"track:text:filter",
"mutate:update-property",
"track:metric:opacity",
"mutate:update-property",
"track:metric:Length",
"track:select:Speed",
"track:metric:Starts at",
"mutate:update-meta",
"track:button:Remove animation",
"mutate:delete",
"track:select:Add effect property",
"mutate:add-property",
"track:button:Remove scale",
"mutate:remove-property",
"track:text:clipPath",
"mutate:update-from",
"track:select:Add from property",
"mutate:add-from",
"track:button:Remove from x",
"mutate:remove-from",
"track:toggle:Arc motion",
"mutate:arc-path",
"track:toggle:Auto rotate",
"mutate:arc-path",
"track:button:Reset arc segment 2",
"mutate:arc-segment",
"mutate:arc-segment",
"track:select:Keyframe ease",
"mutate:keyframe-ease",
"track:select:All keyframe eases",
"mutate:all-eases",
"track:button:Unroll animation",
"mutate:unroll",
]);
});
});
99 changes: 98 additions & 1 deletion packages/studio/src/components/editor/gsapAnimationCallbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ export interface GsapAnimationEditCallbacks {
onUnroll?: (animationId: string) => void;
}

type TrackDesignInput = (control: string, name: string) => void;

function trackAnimationProperty(track: TrackDesignInput, property: string): void {
const control =
property === "visibility"
? "toggle"
: property === "filter" || property === "clipPath"
? "text"
: "metric";
track(control, property);
}

// User-facing control label for each animation-meta field. The ease control is
// labelled "Speed" in the card UI, so ease/easeEach map there.
const ANIMATION_META_LABELS: Record<string, { control: string; name: string }> = {
Expand All @@ -52,7 +64,7 @@ const ANIMATION_META_LABELS: Record<string, { control: string; name: string }> =
* control's usage count.
*/
export function trackAnimationMetaUpdate(
track: (control: string, name: string) => void,
track: TrackDesignInput,
updates: Record<string, unknown>,
): void {
for (const key of Object.keys(updates)) {
Expand All @@ -61,3 +73,88 @@ export function trackAnimationMetaUpdate(
else track("select", key);
}
}

/**
* Add design-input telemetry to the shared animation-edit callback surface.
* Optional callbacks remain absent, pass-through preview callbacks keep their
* original identity, and every tracked event fires once before its mutation.
*/
export function withTrackedGsapAnimationCallbacks(
callbacks: GsapAnimationEditCallbacks,
track: TrackDesignInput,
): GsapAnimationEditCallbacks {
return {
onUpdateProperty: (animationId, property, value) => {
trackAnimationProperty(track, property);
callbacks.onUpdateProperty(animationId, property, value);
},
onUpdateMeta: (animationId, updates) => {
trackAnimationMetaUpdate(track, updates);
callbacks.onUpdateMeta(animationId, updates);
},
onDeleteAnimation: (animationId) => {
track("button", "Remove animation");
callbacks.onDeleteAnimation(animationId);
},
onAddProperty: (animationId, property) => {
track("select", "Add effect property");
callbacks.onAddProperty(animationId, property);
},
onRemoveProperty: (animationId, property) => {
track("button", `Remove ${property}`);
callbacks.onRemoveProperty(animationId, property);
},
onUpdateFromProperty: callbacks.onUpdateFromProperty
? (animationId, property, value) => {
trackAnimationProperty(track, property);
callbacks.onUpdateFromProperty?.(animationId, property, value);
}
: undefined,
onAddFromProperty: callbacks.onAddFromProperty
? (animationId, property) => {
track("select", "Add from property");
callbacks.onAddFromProperty?.(animationId, property);
}
: undefined,
onRemoveFromProperty: callbacks.onRemoveFromProperty
? (animationId, property) => {
track("button", `Remove from ${property}`);
callbacks.onRemoveFromProperty?.(animationId, property);
}
: undefined,
onLivePreview: callbacks.onLivePreview,
onLivePreviewEnd: callbacks.onLivePreviewEnd,
onSetArcPath: callbacks.onSetArcPath
? (animationId, config) => {
track("toggle", config.autoRotate !== undefined ? "Auto rotate" : "Arc motion");
callbacks.onSetArcPath?.(animationId, config);
}
: undefined,
onUpdateArcSegment: callbacks.onUpdateArcSegment
? (animationId, segmentIndex, update) => {
if (update.curviness === undefined) {
track("button", `Reset arc segment ${segmentIndex + 1}`);
}
callbacks.onUpdateArcSegment?.(animationId, segmentIndex, update);
}
: undefined,
onUpdateKeyframeEase: callbacks.onUpdateKeyframeEase
? (animationId, percentage, ease) => {
track("select", "Keyframe ease");
callbacks.onUpdateKeyframeEase?.(animationId, percentage, ease);
}
: undefined,
onSetAllKeyframeEases: callbacks.onSetAllKeyframeEases
? (animationId, ease) => {
track("select", "All keyframe eases");
callbacks.onSetAllKeyframeEases?.(animationId, ease);
}
: undefined,
onUnroll: callbacks.onUnroll
? (animationId) => {
track("button", "Unroll animation");
callbacks.onUnroll?.(animationId);
}
: undefined,
};
}
Loading
Loading