@hyperframes/studio 0.8.113 → 0.8.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-DzJbtQVO.js → hyperframes-player-C-9N8nk0.js} +1 -1
- package/dist/assets/{index-n5FUn1xv.js → index-BoUM6Xqb.js} +1 -1
- package/dist/assets/{index-DRhF3v4Z.js → index-CAvZ0AA-.js} +204 -204
- package/dist/assets/{index-DgwDQK5G.js → index-CVuorIMv.js} +1 -1
- package/dist/assets/index-t1LH6H5H.css +1 -0
- package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
- package/dist/chunk-5EKWJZLM.js.map +1 -0
- package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
- package/dist/{chunk-BIUXP3PG.js → chunk-TXZMMOMA.js} +43 -276
- package/dist/chunk-TXZMMOMA.js.map +1 -0
- package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-D7VYFJJD.js} +3 -5
- package/dist/index.d.ts +46 -22
- package/dist/index.html +2 -2
- package/dist/index.js +3899 -3487
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +10 -8
- package/src/App.tsx +84 -86
- package/src/components/EditorShell.tsx +3 -3
- package/src/components/LinkedSelectionToggle.tsx +3 -2
- package/src/components/StudioHeader.tsx +4 -4
- package/src/components/StudioLeftPanels.tsx +3 -3
- package/src/components/StudioOverlays.tsx +4 -4
- package/src/components/StudioRightPanels.tsx +14 -9
- package/src/components/StudioToast.tsx +1 -1
- package/src/components/TimelineToolbar.tsx +3 -3
- package/src/components/dock/Dock.tsx +5 -7
- package/src/components/editor/CanvasContextMenu.tsx +3 -2
- package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
- package/src/components/editor/DomEditCropHandles.tsx +7 -4
- package/src/components/editor/DomEditOverlay.tsx +5 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -10
- package/src/components/editor/InlineTextToolbar.tsx +11 -27
- package/src/components/editor/MotionPathOverlay.tsx +7 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
- package/src/components/editor/SnapToolbar.test.tsx +100 -1
- package/src/components/editor/SnapToolbar.tsx +43 -5
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
- package/src/components/editor/audioFxTelemetry.ts +2 -3
- package/src/components/editor/colorBrowserCanvas.test.ts +116 -0
- package/src/components/editor/colorValue.test.ts +16 -0
- package/src/components/editor/colorValue.ts +100 -24
- package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
- package/src/components/editor/cropResize.test.ts +11 -0
- package/src/components/editor/cropResize.ts +6 -6
- package/src/components/editor/domEditingDom.ts +4 -0
- package/src/components/editor/domEditingLayers.ts +1 -5
- package/src/components/editor/gradientValue.ts +28 -36
- package/src/components/editor/gsapAnimatesProperty.ts +0 -54
- package/src/components/editor/inlineTextStyleRange.test.ts +31 -1
- package/src/components/editor/inlineTextStyleRange.ts +5 -2
- package/src/components/editor/inlineTextStyleRead.ts +9 -0
- package/src/components/editor/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsDom.ts +2 -2
- package/src/components/editor/manualEditsParsing.ts +7 -0
- package/src/components/editor/manualEditsSeekReapply.ts +2 -2
- package/src/components/editor/marqueeCommit.test.tsx +106 -37
- package/src/components/editor/marqueeCommit.ts +24 -8
- package/src/components/editor/motionPathCommit.test.ts +29 -1
- package/src/components/editor/motionPathCommit.ts +21 -9
- package/src/components/editor/probeSourceElement.test.ts +45 -0
- package/src/components/editor/probeSourceElement.ts +39 -15
- package/src/components/editor/propertyPanelColor.tsx +1 -1
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
- package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
- package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
- package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
- package/src/components/editor/propertyPanelFxSection.tsx +30 -28
- package/src/components/editor/propertyPanelTypes.ts +6 -3
- package/src/components/editor/reapplyPositionEditsAfterSeek.test.ts +25 -0
- package/src/components/editor/useDomEditNudge.test.tsx +37 -0
- package/src/components/editor/useDomEditNudge.ts +2 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
- package/src/components/editor/useDomEditZOrder.ts +12 -2
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
- package/src/components/sidebar/AssetContextMenu.tsx +4 -4
- package/src/components/ui/menuStyle.test.ts +61 -0
- package/src/components/ui/menuStyle.ts +10 -0
- package/src/contexts/DomEditContext.tsx +1 -1
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/domEditExports.test.tsx +9 -1
- package/src/hooks/domEditCommitRunner.ts +1 -1
- package/src/hooks/elementOffsetStager.ts +5 -4
- package/src/hooks/gsapDragCommit.ts +2 -20
- package/src/hooks/gsapDragPositionCommit.ts +18 -3
- package/src/hooks/gsapGestureOutcome.test.ts +161 -0
- package/src/hooks/gsapGestureOutcome.ts +64 -0
- package/src/hooks/gsapKeyframeCommit.ts +3 -12
- package/src/hooks/gsapResizeIntercept.test.ts +33 -0
- package/src/hooks/gsapResizeIntercept.ts +1 -0
- package/src/hooks/gsapRuntimeKeyframes.ts +1 -1
- package/src/hooks/gsapRuntimeReaders.test.ts +48 -0
- package/src/hooks/gsapRuntimeReaders.ts +3 -90
- package/src/hooks/gsapScriptCommitTypes.ts +5 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +20 -0
- package/src/hooks/keyframeCacheAstLoad.ts +6 -5
- package/src/hooks/plainRotation.ts +3 -2
- package/src/hooks/previewPromotionSelection.test.tsx +1 -1
- package/src/hooks/refreshTimelineRowText.test.ts +65 -0
- package/src/hooks/refreshTimelineRowText.ts +29 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
- package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
- package/src/hooks/useAppHotkeys.ts +11 -5
- package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
- package/src/hooks/useDomEditTextCommits.test.tsx +18 -0
- package/src/hooks/useDomEditTextCommits.ts +5 -1
- package/src/hooks/useDomEditWiring.ts +4 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
- package/src/hooks/useDomGeometryCommit.ts +7 -4
- package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
- package/src/hooks/useDomGeometryCommits.ts +3 -2
- package/src/hooks/useDomSelection.test.ts +26 -0
- package/src/hooks/useDomSelection.ts +10 -3
- package/src/hooks/useDomSelectionTypes.ts +6 -1
- package/src/hooks/useEnableKeyframes.test.ts +2 -2
- package/src/hooks/useEnableKeyframes.ts +5 -3
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +102 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +31 -6
- package/src/hooks/useFrameCapture.ts +15 -8
- package/src/hooks/useGestureCommit.test.tsx +225 -55
- package/src/hooks/useGestureCommit.ts +90 -56
- package/src/hooks/useGestureTrailOverlay.tsx +33 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +11 -6
- package/src/hooks/useGsapAwareEditing.ts +96 -195
- package/src/hooks/useGsapAwareGroupMove.ts +179 -0
- package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
- package/src/hooks/useGsapKeyframeOps.ts +36 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +57 -0
- package/src/hooks/useGsapScriptCommits.ts +3 -0
- package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
- package/src/hooks/useGsapSelectionHandlers.ts +24 -18
- package/src/hooks/useGsapTweenCache.ts +18 -12
- package/src/hooks/usePanelLayout.ts +5 -1
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
- package/src/hooks/usePreviewInteraction.test.ts +21 -0
- package/src/hooks/usePreviewInteraction.ts +2 -0
- package/src/hooks/useRenderClipContent.test.ts +18 -0
- package/src/hooks/useRenderClipContent.ts +4 -5
- package/src/hooks/useShellSlices.ts +53 -0
- package/src/hooks/useStableHandlers.test.tsx +77 -0
- package/src/hooks/useStableHandlers.ts +44 -0
- package/src/hooks/useStudioSdkSessions.ts +2 -1
- package/src/hooks/useStudioSidePanels.test.tsx +44 -0
- package/src/hooks/useStudioSidePanels.tsx +23 -0
- package/src/hooks/useTimelineAddAtPlayhead.ts +16 -8
- package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
- package/src/hooks/useTimelineDeleteOps.ts +21 -27
- package/src/hooks/useTimelineEditing.test.tsx +1 -1
- package/src/hooks/useTimelineEditing.ts +33 -27
- package/src/player/components/AutomationSelectionMenu.tsx +3 -2
- package/src/player/components/ClipContextMenu.test.tsx +73 -0
- package/src/player/components/ClipContextMenu.tsx +33 -31
- package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
- package/src/player/components/OutOfSyncBadge.tsx +5 -5
- package/src/player/components/TextClipContent.test.ts +19 -0
- package/src/player/components/TextClipContent.tsx +27 -0
- package/src/player/components/TrackGapContextMenu.tsx +3 -6
- package/src/player/components/clipMenuToolItems.test.tsx +23 -0
- package/src/player/components/clipMenuToolItems.tsx +13 -9
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
- package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
- package/src/player/components/timelineClipGestureHandlers.ts +20 -2
- package/src/player/components/timelineEditing.ts +3 -1
- package/src/player/components/timelineLinkSelection.ts +35 -1
- package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
- package/src/player/components/useTimelineClipDrag.ts +6 -7
- package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
- package/src/player/hooks/timelinePlayerSync.ts +6 -1
- package/src/player/lib/timelineElementHelpers.test.ts +87 -1
- package/src/player/lib/timelineElementHelpers.ts +6 -15
- package/src/player/lib/timelineText.ts +40 -0
- package/src/player/store/timelineElement.ts +12 -0
- package/src/styles/theme.css +3 -0
- package/src/utils/frameCapture.ts +3 -1
- package/src/utils/gsapUndoRestore.browser.test.ts +4 -2
- package/src/utils/keyframeUsage.ts +79 -0
- package/src/utils/previewFeatureUsage.test.ts +32 -0
- package/src/utils/previewFeatureUsage.ts +46 -0
- package/src/utils/studioTelemetry.test.ts +107 -0
- package/src/utils/studioTelemetry.ts +49 -4
- package/src/webmcp/StudioAgentTools.tsx +2 -2
- package/dist/assets/index-DbfKVTId.css +0 -1
- package/dist/chunk-BIUXP3PG.js.map +0 -1
- package/dist/chunk-K47E2ZJA.js.map +0 -1
- /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
- /package/dist/{domEditingLayers-V67MS4WP.js.map → domEditingLayers-D7VYFJJD.js.map} +0 -0
|
@@ -79,7 +79,7 @@ describe("DOM editing package exports", () => {
|
|
|
79
79
|
front.id = "front";
|
|
80
80
|
parent.append(back, front);
|
|
81
81
|
document.body.append(parent);
|
|
82
|
-
const commitZ = vi.fn(() =>
|
|
82
|
+
const commitZ = vi.fn(async () => ({ durable: true, changed: true }));
|
|
83
83
|
const session = {
|
|
84
84
|
handleDomZIndexReorderCommit: commitZ,
|
|
85
85
|
} as unknown as Parameters<typeof DomEditProvider>[0]["value"];
|
|
@@ -107,6 +107,11 @@ describe("DOM editing package exports", () => {
|
|
|
107
107
|
await act(async () => {
|
|
108
108
|
expect(zOrder?.apply(sel, toFront)).toBe(true);
|
|
109
109
|
});
|
|
110
|
+
expect(trackStudioEvent).toHaveBeenCalledWith("feature_used", {
|
|
111
|
+
feature: "z_order",
|
|
112
|
+
surface: "preview",
|
|
113
|
+
method: "button",
|
|
114
|
+
});
|
|
110
115
|
expect(commitZ).toHaveBeenCalledTimes(1);
|
|
111
116
|
expect(commitZ).toHaveBeenCalledWith(
|
|
112
117
|
expect.arrayContaining([expect.objectContaining({ element: back, id: "back" })]),
|
|
@@ -159,3 +164,6 @@ describe("DOM editing package exports", () => {
|
|
|
159
164
|
await act(async () => root.unmount());
|
|
160
165
|
});
|
|
161
166
|
});
|
|
167
|
+
|
|
168
|
+
vi.mock("./utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
169
|
+
import { trackStudioEvent } from "./utils/studioTelemetry";
|
|
@@ -94,7 +94,7 @@ export type DomEditCommitDeclineReason =
|
|
|
94
94
|
| "persist-failed";
|
|
95
95
|
|
|
96
96
|
export type DomEditCommitOutcome =
|
|
97
|
-
| { ok: true; persistence?: DomEditPersistOutcome }
|
|
97
|
+
| { ok: true; persistence?: DomEditPersistOutcome; changed?: boolean }
|
|
98
98
|
| { ok: false; reason: DomEditCommitDeclineReason };
|
|
99
99
|
|
|
100
100
|
export function domEditCommitDeclined(reason: DomEditCommitDeclineReason): DomEditCommitOutcome {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DomEditPersistOutcome } from "./domEditCommitTypes";
|
|
1
2
|
import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing";
|
|
2
3
|
import {
|
|
3
4
|
applyElementPositionOffset,
|
|
@@ -23,7 +24,7 @@ export interface ElementOffsetStagerDeps {
|
|
|
23
24
|
selection: DomEditSelection,
|
|
24
25
|
patches: PatchOperation[],
|
|
25
26
|
options: { label: string; coalesceKey: string; coalesceMs?: number },
|
|
26
|
-
) => Promise<
|
|
27
|
+
) => Promise<DomEditPersistOutcome | undefined>;
|
|
27
28
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
28
29
|
readOnlyPreview?: boolean;
|
|
29
30
|
}
|
|
@@ -59,7 +60,7 @@ function stagePlainTranslate(
|
|
|
59
60
|
selection: DomEditSelection,
|
|
60
61
|
next: { x: number; y: number },
|
|
61
62
|
coalesceKey?: string,
|
|
62
|
-
): { save: () => Promise<
|
|
63
|
+
): { save: () => Promise<DomEditPersistOutcome | undefined>; rollback: () => void } {
|
|
63
64
|
const el = selection.element;
|
|
64
65
|
const before = captureStudioPathOffset(el);
|
|
65
66
|
const patches = writePlainMove(el, next);
|
|
@@ -85,9 +86,9 @@ export function stageElementOffset(
|
|
|
85
86
|
next: { x: number; y: number },
|
|
86
87
|
plainTranslate: boolean,
|
|
87
88
|
coalesceKey?: string,
|
|
88
|
-
): { save: () => Promise<
|
|
89
|
+
): { save: () => Promise<DomEditPersistOutcome | undefined>; rollback: () => void } {
|
|
89
90
|
const el = selection.element;
|
|
90
|
-
if (readOnlyPreview) return { save: () => Promise.resolve(), rollback: () => undefined };
|
|
91
|
+
if (readOnlyPreview) return { save: () => Promise.resolve(undefined), rollback: () => undefined };
|
|
91
92
|
if (plainTranslate) {
|
|
92
93
|
refuseGsapTakeover(el, showToast);
|
|
93
94
|
return stagePlainTranslate(commitPositionPatchToHtml, selection, next, coalesceKey);
|
|
@@ -13,7 +13,7 @@ import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeComp
|
|
|
13
13
|
import { roundTo3, roundToLayoutPx } from "../utils/rounding";
|
|
14
14
|
import { computeElementPercentage, writeTargetSelector } from "./gsapShared";
|
|
15
15
|
import { computeDraggedGsapPosition } from "./draggedGsapPosition";
|
|
16
|
-
import type {
|
|
16
|
+
import type { CommitMutation } from "./gsapScriptCommitTypes";
|
|
17
17
|
import { isGestureTransactionCommit, runGestureTransaction } from "./gestureTransaction";
|
|
18
18
|
import { setPatchFromUpdateProperty } from "./gsapDragStaticSetHelpers";
|
|
19
19
|
import { GsapEditBlockedError } from "./gsapEditOutcome";
|
|
@@ -23,25 +23,7 @@ export {
|
|
|
23
23
|
findSizeSetAnimation,
|
|
24
24
|
} from "./gsapDragStaticSetHelpers";
|
|
25
25
|
export interface GsapDragCommitCallbacks {
|
|
26
|
-
commitMutation:
|
|
27
|
-
selection: DomEditSelection,
|
|
28
|
-
mutation: Record<string, unknown>,
|
|
29
|
-
options: {
|
|
30
|
-
label: string;
|
|
31
|
-
coalesceKey?: string;
|
|
32
|
-
coalesceMs?: number;
|
|
33
|
-
softReload?: boolean;
|
|
34
|
-
skipReload?: boolean;
|
|
35
|
-
beforeReload?: () => void;
|
|
36
|
-
/**
|
|
37
|
-
* Value-only fast path: when set, `runCommit` patches the changed tween in
|
|
38
|
-
* the preview runtime in place (instant, no re-run) and only falls back to
|
|
39
|
-
* the soft reload if the patch can't be safely applied. Attached only to
|
|
40
|
-
* value-only `set` commits; structural/keyframe commits omit it.
|
|
41
|
-
*/
|
|
42
|
-
instantPatch?: { selector: string; change: RuntimeTweenChange };
|
|
43
|
-
},
|
|
44
|
-
) => Promise<void>;
|
|
26
|
+
commitMutation: CommitMutation;
|
|
45
27
|
fetchAnimations?: () => Promise<GsapAnimation[]>;
|
|
46
28
|
}
|
|
47
29
|
|
|
@@ -81,7 +81,12 @@ async function extendTweenAndAddKeyframe(
|
|
|
81
81
|
duration: roundTo3(newDuration),
|
|
82
82
|
keyframes: remappedKfs,
|
|
83
83
|
},
|
|
84
|
-
{
|
|
84
|
+
{
|
|
85
|
+
label: `Move layer (extended keyframe)`,
|
|
86
|
+
keyframeAction: "add",
|
|
87
|
+
softReload: true,
|
|
88
|
+
beforeReload,
|
|
89
|
+
},
|
|
85
90
|
);
|
|
86
91
|
}
|
|
87
92
|
|
|
@@ -339,7 +344,12 @@ export async function commitGsapPositionFromDrag(
|
|
|
339
344
|
keyframes: temporalKeyframes,
|
|
340
345
|
ease: "none",
|
|
341
346
|
},
|
|
342
|
-
{
|
|
347
|
+
{
|
|
348
|
+
label: "Move layer (new keyframe)",
|
|
349
|
+
keyframeAction: "add",
|
|
350
|
+
softReload: true,
|
|
351
|
+
beforeReload: restoreOffset,
|
|
352
|
+
},
|
|
343
353
|
);
|
|
344
354
|
return;
|
|
345
355
|
}
|
|
@@ -449,7 +459,12 @@ export async function commitGsapPositionFromDrag(
|
|
|
449
459
|
existingPosAnim
|
|
450
460
|
? { type: "replace-with-keyframes", animationId: existingPosAnim.id, ...baseKf }
|
|
451
461
|
: { type: "add-with-keyframes", ...baseKf },
|
|
452
|
-
{
|
|
462
|
+
{
|
|
463
|
+
label: "Move layer (from extended)",
|
|
464
|
+
keyframeAction: "add",
|
|
465
|
+
softReload: true,
|
|
466
|
+
beforeReload: restoreOffset,
|
|
467
|
+
},
|
|
453
468
|
);
|
|
454
469
|
} else {
|
|
455
470
|
const coalesceKey = `gsap:convert-drag:${anim.id}`;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { trackKeyframeCommit } from "../utils/keyframeUsage";
|
|
3
|
+
import { observeGsapGesture } from "./gsapGestureOutcome";
|
|
4
|
+
import type { CommitMutation, MutationResult } from "./gsapScriptCommitTypes";
|
|
5
|
+
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
6
|
+
|
|
7
|
+
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
8
|
+
beforeEach(() => vi.clearAllMocks());
|
|
9
|
+
const selection = {} as Parameters<CommitMutation>[0];
|
|
10
|
+
const mutation = {
|
|
11
|
+
type: "add-keyframe",
|
|
12
|
+
animationId: "private-target",
|
|
13
|
+
properties: { text: "private-content" },
|
|
14
|
+
};
|
|
15
|
+
function writer(result?: MutationResult): CommitMutation {
|
|
16
|
+
const commit: CommitMutation = async (_selection, _mutation, options) => {
|
|
17
|
+
if (result) options.onResult?.(result);
|
|
18
|
+
};
|
|
19
|
+
commit.batch = async (_calls, options) => {
|
|
20
|
+
if (result) options.onResult?.(result);
|
|
21
|
+
};
|
|
22
|
+
return commit;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("GSAP gesture usage", () => {
|
|
26
|
+
it("counts a multi-write add gesture once from successful writer results", async () => {
|
|
27
|
+
const observed = observeGsapGesture(
|
|
28
|
+
writer({ ok: true, changed: true, mutationChanges: [true] }),
|
|
29
|
+
);
|
|
30
|
+
const onResult = vi.fn();
|
|
31
|
+
await observed.commit!(
|
|
32
|
+
selection,
|
|
33
|
+
{ type: "convert-to-keyframes" },
|
|
34
|
+
{ label: "Convert", onResult },
|
|
35
|
+
);
|
|
36
|
+
await observed.commit!(selection, mutation, { label: "Add" });
|
|
37
|
+
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
38
|
+
expect(observed.finish()).toEqual({ ok: true, changed: true });
|
|
39
|
+
expect(onResult).toHaveBeenCalledWith({ ok: true, changed: true, mutationChanges: [true] });
|
|
40
|
+
expect(trackStudioEvent).toHaveBeenCalledExactlyOnceWith("keyframe", { action: "add" });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("counts a batched insertion once", async () => {
|
|
44
|
+
const observed = observeGsapGesture(
|
|
45
|
+
writer({ ok: true, changed: true, mutationChanges: [true, true] }),
|
|
46
|
+
);
|
|
47
|
+
await observed.commit!.batch!(
|
|
48
|
+
[
|
|
49
|
+
{ selection, mutation, options: { label: "Add" } },
|
|
50
|
+
{ selection, mutation, options: { label: "Add" } },
|
|
51
|
+
],
|
|
52
|
+
{ label: "Add together" },
|
|
53
|
+
);
|
|
54
|
+
expect(observed.finish().changed).toBe(true);
|
|
55
|
+
expect(trackStudioEvent).toHaveBeenCalledExactlyOnceWith("keyframe", { action: "add" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("retains insertion metadata from an earlier member of a mixed batch", async () => {
|
|
59
|
+
const observed = observeGsapGesture(
|
|
60
|
+
writer({ ok: true, changed: true, mutationChanges: [true, true] }),
|
|
61
|
+
);
|
|
62
|
+
await observed.commit!.batch!(
|
|
63
|
+
[
|
|
64
|
+
{
|
|
65
|
+
selection,
|
|
66
|
+
mutation: { type: "replace-with-keyframes" },
|
|
67
|
+
options: { label: "Extend", keyframeAction: "add" },
|
|
68
|
+
},
|
|
69
|
+
{ selection, mutation: { type: "update-keyframe" }, options: { label: "Update" } },
|
|
70
|
+
],
|
|
71
|
+
{ label: "Update" },
|
|
72
|
+
);
|
|
73
|
+
observed.finish();
|
|
74
|
+
expect(trackStudioEvent).toHaveBeenCalledExactlyOnceWith("keyframe", { action: "add" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("uses semantic insertion metadata for a replacement", async () => {
|
|
78
|
+
const observed = observeGsapGesture(
|
|
79
|
+
writer({ ok: true, changed: true, mutationChanges: [true] }),
|
|
80
|
+
);
|
|
81
|
+
await observed.commit!(
|
|
82
|
+
selection,
|
|
83
|
+
{ type: "replace-with-keyframes" },
|
|
84
|
+
{ label: "Replace", keyframeAction: "add" },
|
|
85
|
+
);
|
|
86
|
+
observed.finish();
|
|
87
|
+
expect(trackStudioEvent).toHaveBeenCalledExactlyOnceWith("keyframe", { action: "add" });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it.each([undefined, { ok: true }, { ok: true, changed: false }, { ok: false, changed: true }])(
|
|
91
|
+
"does not count resolution without a changed successful write (%j)",
|
|
92
|
+
async (result) => {
|
|
93
|
+
const observed = observeGsapGesture(writer(result));
|
|
94
|
+
await observed.commit!(selection, mutation, { label: "Add" });
|
|
95
|
+
expect(observed.finish()).toEqual({ ok: true, changed: false });
|
|
96
|
+
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
it("does not count missing writers or an unfinished group", async () => {
|
|
101
|
+
expect(observeGsapGesture(null).finish()).toEqual({ ok: true, changed: false });
|
|
102
|
+
const observed = observeGsapGesture(writer());
|
|
103
|
+
await observed.commit!(selection, mutation, { label: "Add" });
|
|
104
|
+
expect(observed.finish(true).changed).toBe(false);
|
|
105
|
+
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("reports a successful plain DOM gesture without inventing keyframes", () => {
|
|
109
|
+
expect(observeGsapGesture(null).finish(true).changed).toBe(true);
|
|
110
|
+
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it.each([
|
|
115
|
+
[[false, true], 0],
|
|
116
|
+
[[true, false], 1],
|
|
117
|
+
[undefined, 0],
|
|
118
|
+
] as const)(
|
|
119
|
+
"attributes mixed batches only to changed members (%j)",
|
|
120
|
+
async (mutationChanges, count) => {
|
|
121
|
+
const result = {
|
|
122
|
+
ok: true,
|
|
123
|
+
changed: true,
|
|
124
|
+
mutationChanges: mutationChanges && [...mutationChanges],
|
|
125
|
+
};
|
|
126
|
+
const observed = observeGsapGesture(writer(result));
|
|
127
|
+
const mutations = [mutation, { type: "update-property" }];
|
|
128
|
+
await observed.commit!.batch!(
|
|
129
|
+
mutations.map((mutation) => ({ selection, mutation, options: { label: "Edit" } })),
|
|
130
|
+
{ label: "Edit" },
|
|
131
|
+
);
|
|
132
|
+
expect(observed.finish()).toEqual({ ok: true, changed: true });
|
|
133
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(count);
|
|
134
|
+
if (count) expect(trackStudioEvent).toHaveBeenCalledWith("keyframe", { action: "add" });
|
|
135
|
+
vi.mocked(trackStudioEvent).mockClear();
|
|
136
|
+
trackKeyframeCommit(mutations, result, { label: "Edit" });
|
|
137
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(count);
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
it("excludes a suppressed preparation from mixed-batch attribution", async () => {
|
|
142
|
+
const result = { ok: true, changed: true, mutationChanges: [true, false] };
|
|
143
|
+
const calls = [
|
|
144
|
+
{
|
|
145
|
+
selection,
|
|
146
|
+
mutation: { type: "convert-to-keyframes" },
|
|
147
|
+
options: { label: "Prepare", keyframeTelemetry: false },
|
|
148
|
+
},
|
|
149
|
+
{ selection, mutation, options: { label: "Add" } },
|
|
150
|
+
];
|
|
151
|
+
const observed = observeGsapGesture(writer(result));
|
|
152
|
+
await observed.commit!.batch!(calls, { label: "Add" });
|
|
153
|
+
expect(observed.finish()).toEqual({ ok: true, changed: true });
|
|
154
|
+
trackKeyframeCommit(
|
|
155
|
+
calls.map((call) => call.mutation),
|
|
156
|
+
result,
|
|
157
|
+
{ label: "Add" },
|
|
158
|
+
calls.map((call) => call.options),
|
|
159
|
+
);
|
|
160
|
+
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommitMutationCall,
|
|
3
|
+
CommitMutation,
|
|
4
|
+
CommitMutationOptions,
|
|
5
|
+
MutationResult,
|
|
6
|
+
} from "./gsapScriptCommitTypes";
|
|
7
|
+
import {
|
|
8
|
+
keyframeUsageActions,
|
|
9
|
+
changedMutationIndices,
|
|
10
|
+
primaryKeyframeAction,
|
|
11
|
+
trackKeyframeUsage,
|
|
12
|
+
type KeyframeUsageAction,
|
|
13
|
+
} from "../utils/keyframeUsage";
|
|
14
|
+
import type { GeometryCommitResult } from "../utils/previewFeatureUsage";
|
|
15
|
+
|
|
16
|
+
export function observeGsapGesture(writer: CommitMutation | null) {
|
|
17
|
+
let changed = false;
|
|
18
|
+
let pendingResults = 0;
|
|
19
|
+
const actions = new Set<KeyframeUsageAction>();
|
|
20
|
+
const observe = (calls: CommitMutationCall[], options: CommitMutationOptions) => {
|
|
21
|
+
pendingResults += 1;
|
|
22
|
+
return {
|
|
23
|
+
...options,
|
|
24
|
+
keyframeTelemetry: false,
|
|
25
|
+
onResult: (result: MutationResult) => {
|
|
26
|
+
pendingResults -= 1;
|
|
27
|
+
options.onResult?.(result);
|
|
28
|
+
if (!result.ok || result.changed !== true) return;
|
|
29
|
+
changed = true;
|
|
30
|
+
const members = changedMutationIndices(
|
|
31
|
+
result,
|
|
32
|
+
calls.length,
|
|
33
|
+
calls.map((call) => call.options),
|
|
34
|
+
).map((index) => calls[index]!);
|
|
35
|
+
for (const action of keyframeUsageActions(
|
|
36
|
+
members.map((call) => call.mutation),
|
|
37
|
+
members.map((call) => call.options.keyframeAction),
|
|
38
|
+
))
|
|
39
|
+
actions.add(action);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
let commit: CommitMutation | null = null;
|
|
44
|
+
if (writer) {
|
|
45
|
+
commit = (selection, mutation, options) =>
|
|
46
|
+
writer(selection, mutation, observe([{ selection, mutation, options }], options));
|
|
47
|
+
if (writer.batch) {
|
|
48
|
+
const batch = writer.batch;
|
|
49
|
+
commit.batch = (calls, options) => batch(calls, observe(calls, options));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
commit,
|
|
54
|
+
recordDomResult: (result: { changed: boolean } | undefined) => {
|
|
55
|
+
changed ||= result?.changed === true;
|
|
56
|
+
},
|
|
57
|
+
finish: (domChanged = false): GeometryCommitResult => {
|
|
58
|
+
if (pendingResults !== 0) return { ok: true, changed: false };
|
|
59
|
+
const action = primaryKeyframeAction(actions);
|
|
60
|
+
if (action) trackKeyframeUsage(action);
|
|
61
|
+
return { ok: true, changed: changed || domChanged };
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -4,23 +4,14 @@ import { absoluteToPercentageForAnimation, findTweenAtTime } from "../utils/glob
|
|
|
4
4
|
import { PROPERTY_DEFAULTS, selectorFromSelection, writeTargetSelector } from "./gsapShared";
|
|
5
5
|
import { roundToCenti } from "../utils/rounding";
|
|
6
6
|
|
|
7
|
-
type
|
|
8
|
-
selection: DomEditSelection,
|
|
9
|
-
mutation: Record<string, unknown>,
|
|
10
|
-
options: {
|
|
11
|
-
label: string;
|
|
12
|
-
coalesceKey?: string;
|
|
13
|
-
softReload?: boolean;
|
|
14
|
-
skipReload?: boolean;
|
|
15
|
-
},
|
|
16
|
-
) => Promise<void>;
|
|
7
|
+
import type { CommitMutation } from "./gsapScriptCommitTypes";
|
|
17
8
|
|
|
18
9
|
export async function commitKeyframeAtTimeImpl(
|
|
19
10
|
selection: DomEditSelection,
|
|
20
11
|
absoluteTime: number,
|
|
21
12
|
animations: GsapAnimation[],
|
|
22
13
|
properties: Record<string, number | string>,
|
|
23
|
-
commitMutation:
|
|
14
|
+
commitMutation: CommitMutation,
|
|
24
15
|
): Promise<void> {
|
|
25
16
|
// Matching an authored tween is a string compare against what the author
|
|
26
17
|
// wrote, so it keeps using the selection's own selector; the NEW tween below
|
|
@@ -38,7 +29,7 @@ export async function commitKeyframeAtTimeImpl(
|
|
|
38
29
|
await commitMutation(
|
|
39
30
|
selection,
|
|
40
31
|
{ type: "convert-to-keyframes", animationId: tween.id },
|
|
41
|
-
{ label: "Convert to keyframes", skipReload: true },
|
|
32
|
+
{ label: "Convert to keyframes", keyframeTelemetry: false, skipReload: true },
|
|
42
33
|
);
|
|
43
34
|
}
|
|
44
35
|
|
|
@@ -3,10 +3,16 @@ import { afterEach, expect, it, vi } from "vitest";
|
|
|
3
3
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
4
4
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
5
5
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
6
|
+
import { observeGsapGesture } from "./gsapGestureOutcome";
|
|
7
|
+
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
8
|
+
import type { CommitMutation } from "./gsapScriptCommitTypes";
|
|
6
9
|
import { computeCurrentPercentage } from "./gsapDragCommit";
|
|
7
10
|
import { tryGsapResizeIntercept } from "./gsapResizeIntercept";
|
|
8
11
|
|
|
12
|
+
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
13
|
+
|
|
9
14
|
afterEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
10
16
|
vi.restoreAllMocks();
|
|
11
17
|
usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null });
|
|
12
18
|
});
|
|
@@ -553,3 +559,30 @@ it("hands the size to the element's CSS when its only tween is a fade", async ()
|
|
|
553
559
|
expect(handled).toEqual({ status: "element-size" });
|
|
554
560
|
expect(commitMutation).not.toHaveBeenCalled();
|
|
555
561
|
});
|
|
562
|
+
|
|
563
|
+
it.each([
|
|
564
|
+
[5, 1],
|
|
565
|
+
[0.5, 1],
|
|
566
|
+
[0.2, 0],
|
|
567
|
+
])("counts scale resize insertion at %ss", async (time, count) => {
|
|
568
|
+
const el = makeGradedElement();
|
|
569
|
+
const iframe = fakeIframe(el, { scale: 1, scaleX: 1, scaleY: 1 });
|
|
570
|
+
usePlayerStore.setState({ currentTime: time, activeKeyframePct: null });
|
|
571
|
+
const writer: CommitMutation = async (_selection, _mutation, options) => {
|
|
572
|
+
options.onResult?.({ ok: true, changed: true });
|
|
573
|
+
};
|
|
574
|
+
const outcome = observeGsapGesture(writer);
|
|
575
|
+
const result = await tryGsapResizeIntercept(
|
|
576
|
+
{ id: "clip", selector: "#clip", element: el } as DomEditSelection,
|
|
577
|
+
{ width: 700, height: 400 },
|
|
578
|
+
[keyframedScaleFixture()],
|
|
579
|
+
iframe,
|
|
580
|
+
outcome.commit!,
|
|
581
|
+
);
|
|
582
|
+
expect(result.status).toBe("persisted");
|
|
583
|
+
outcome.finish();
|
|
584
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(count);
|
|
585
|
+
if (count) expect(trackStudioEvent).toHaveBeenCalledWith("keyframe", { action: "add" });
|
|
586
|
+
el.remove();
|
|
587
|
+
document.querySelector("#__hf_color_grading_clip")?.remove();
|
|
588
|
+
});
|
|
@@ -541,6 +541,7 @@ export async function tryGsapResizeIntercept(
|
|
|
541
541
|
? `Resize (extended to ${ct.toFixed(2)}s)`
|
|
542
542
|
: `Resize (keyframe ${Math.round(((ct - newStart) / newDuration) * 1000) / 10}%)`,
|
|
543
543
|
softReload: true,
|
|
544
|
+
...(!collidingKf ? { keyframeAction: "add" as const } : {}),
|
|
544
545
|
},
|
|
545
546
|
);
|
|
546
547
|
return { status: "persisted", ownsDragOffset: await finalizeScaleResizeCommit() };
|
|
@@ -428,7 +428,7 @@ export const GSAP_TRANSFORM_KEYS = new Set(
|
|
|
428
428
|
);
|
|
429
429
|
|
|
430
430
|
/** Whether a live timeline tween or hold writes any of `channels` on `el`. Sync, no fetch. */
|
|
431
|
-
function gsapWritesChannels(el: Element, channels: string[]): boolean {
|
|
431
|
+
export function gsapWritesChannels(el: Element, channels: string[]): boolean {
|
|
432
432
|
const win = el.ownerDocument.defaultView as { __timelines?: Record<string, RuntimeTimeline> };
|
|
433
433
|
return Object.values(win?.__timelines ?? {}).some((tl) =>
|
|
434
434
|
(tl?.getChildren?.(true) ?? []).some(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { gsap } from "gsap";
|
|
3
4
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
4
5
|
import {
|
|
5
6
|
COLOR_GRADING_SOURCE_HIDDEN_ATTR,
|
|
@@ -109,3 +110,50 @@ describe("color-grading opacity truth", () => {
|
|
|
109
110
|
expect(readGsapProperty(iframe, "#clip", "opacity")).toBe(0.3);
|
|
110
111
|
});
|
|
111
112
|
});
|
|
113
|
+
|
|
114
|
+
describe("readAllAnimatedProperties with a sibling tween", () => {
|
|
115
|
+
it("leaves a sibling tween's rotation to that tween after an opacity keyframe edit", () => {
|
|
116
|
+
const el = document.createElement("div");
|
|
117
|
+
el.id = "clip";
|
|
118
|
+
document.body.appendChild(el);
|
|
119
|
+
const build = (keyframes: Record<string, Record<string, number>>) => {
|
|
120
|
+
const tl = gsap.timeline({ paused: true });
|
|
121
|
+
tl.to(el, { duration: 4, ease: "none", keyframes }, 0);
|
|
122
|
+
tl.to(el, { rotation: 90, duration: 3 }, 0);
|
|
123
|
+
return tl;
|
|
124
|
+
};
|
|
125
|
+
const live = build({ "0%": { opacity: 0 }, "100%": { opacity: 1 } });
|
|
126
|
+
live.seek(2);
|
|
127
|
+
const iframe = {
|
|
128
|
+
contentWindow: { __timelines: { main: live }, gsap },
|
|
129
|
+
contentDocument: document,
|
|
130
|
+
} as unknown as HTMLIFrameElement;
|
|
131
|
+
const anim = {
|
|
132
|
+
id: "#clip-to-0-visual",
|
|
133
|
+
targetSelector: "#clip",
|
|
134
|
+
method: "to",
|
|
135
|
+
properties: {},
|
|
136
|
+
keyframes: {
|
|
137
|
+
keyframes: [
|
|
138
|
+
{ percentage: 0, properties: { opacity: 0 } },
|
|
139
|
+
{ percentage: 100, properties: { opacity: 1 } },
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
} as unknown as GsapAnimation;
|
|
143
|
+
|
|
144
|
+
// commitKeyframeProps: these values plus the edit at the playhead, and backfilled into the rest.
|
|
145
|
+
const read = readAllAnimatedProperties(iframe, "#clip", anim);
|
|
146
|
+
live.kill();
|
|
147
|
+
gsap.set(el, { clearProps: "all" });
|
|
148
|
+
const edited = build({
|
|
149
|
+
"0%": { ...read, opacity: 0 },
|
|
150
|
+
"50%": { ...read, opacity: 0.3 },
|
|
151
|
+
"100%": { ...read, opacity: 1 },
|
|
152
|
+
});
|
|
153
|
+
edited.seek(3.2);
|
|
154
|
+
edited.seek(3.5);
|
|
155
|
+
|
|
156
|
+
expect(gsap.getProperty(el, "rotation")).toBe(90);
|
|
157
|
+
edited.kill();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
HF_COLOR_GRADING_CANVAS_ID_PREFIX,
|
|
9
9
|
} from "@hyperframes/core/color-grading";
|
|
10
10
|
import { getIframeGsap, queryIframeElement, type IframeGsap } from "./gsapShared";
|
|
11
|
+
import { gsapWritesChannels } from "./gsapRuntimeKeyframes";
|
|
11
12
|
import { roundTo3 } from "../utils/rounding";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -50,27 +51,6 @@ export function readGsapProperty(
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export const POSITION_PROPS = new Set(["x", "y", "xPercent", "yPercent"]);
|
|
53
|
-
const GSAP_CONFIG_KEYS = new Set([
|
|
54
|
-
"duration",
|
|
55
|
-
"ease",
|
|
56
|
-
"delay",
|
|
57
|
-
"stagger",
|
|
58
|
-
"id",
|
|
59
|
-
"onComplete",
|
|
60
|
-
"onUpdate",
|
|
61
|
-
"onStart",
|
|
62
|
-
"onRepeat",
|
|
63
|
-
"repeat",
|
|
64
|
-
"yoyo",
|
|
65
|
-
"repeatDelay",
|
|
66
|
-
"paused",
|
|
67
|
-
"immediateRender",
|
|
68
|
-
"lazy",
|
|
69
|
-
"overwrite",
|
|
70
|
-
"keyframes",
|
|
71
|
-
"parent",
|
|
72
|
-
]);
|
|
73
|
-
|
|
74
54
|
export function readAllAnimatedProperties(
|
|
75
55
|
iframe: HTMLIFrameElement | null,
|
|
76
56
|
selector: string,
|
|
@@ -115,74 +95,7 @@ export function readAllAnimatedProperties(
|
|
|
115
95
|
}
|
|
116
96
|
}
|
|
117
97
|
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
const win = iframe.contentWindow as unknown as { __timelines?: Record<string, unknown> };
|
|
121
|
-
const timelines = win.__timelines;
|
|
122
|
-
if (timelines) {
|
|
123
|
-
for (const tl of Object.values(timelines)) {
|
|
124
|
-
const tlObj = tl as {
|
|
125
|
-
getChildren?: (
|
|
126
|
-
deep: boolean,
|
|
127
|
-
) => Array<{ targets?: () => Element[]; vars?: Record<string, unknown> }>;
|
|
128
|
-
};
|
|
129
|
-
if (!tlObj?.getChildren) continue;
|
|
130
|
-
for (const child of tlObj.getChildren(true)) {
|
|
131
|
-
if (typeof child.targets !== "function") continue;
|
|
132
|
-
const targets = child.targets();
|
|
133
|
-
if (!targets.includes(el)) continue;
|
|
134
|
-
const vars = child.vars;
|
|
135
|
-
if (!vars) continue;
|
|
136
|
-
for (const k of Object.keys(vars)) {
|
|
137
|
-
if (!GSAP_CONFIG_KEYS.has(k) && inGroup(k)) otherTweenProps.add(k);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
} catch {}
|
|
143
|
-
for (const p of groupedPropKeys) otherTweenProps.delete(p);
|
|
144
|
-
|
|
145
|
-
// Tier 1: Transform + visual properties with universal CSS defaults.
|
|
146
|
-
// Safe to compare against hardcoded values — these are always 0 or 1
|
|
147
|
-
// regardless of the element's stylesheet.
|
|
148
|
-
const UNIVERSAL_BASELINE: Record<string, number> = {
|
|
149
|
-
opacity: 1,
|
|
150
|
-
scale: 1,
|
|
151
|
-
scaleX: 1,
|
|
152
|
-
scaleY: 1,
|
|
153
|
-
scaleZ: 1,
|
|
154
|
-
rotation: 0,
|
|
155
|
-
rotationX: 0,
|
|
156
|
-
rotationY: 0,
|
|
157
|
-
skewX: 0,
|
|
158
|
-
skewY: 0,
|
|
159
|
-
z: 0,
|
|
160
|
-
xPercent: 0,
|
|
161
|
-
yPercent: 0,
|
|
162
|
-
transformPerspective: 0,
|
|
163
|
-
blur: 0,
|
|
164
|
-
brightness: 1,
|
|
165
|
-
contrast: 1,
|
|
166
|
-
saturate: 1,
|
|
167
|
-
hueRotate: 0,
|
|
168
|
-
grayscale: 0,
|
|
169
|
-
sepia: 0,
|
|
170
|
-
invert: 0,
|
|
171
|
-
};
|
|
172
|
-
// Collect all properties that ANY tween on this element explicitly targets.
|
|
173
|
-
// Only capture baseline values for these — GSAP reports non-default values
|
|
174
|
-
// (scaleZ=0, brightness=0) for untouched properties, polluting keyframes.
|
|
175
|
-
const allTweenedProps = new Set([...groupedPropKeys, ...otherTweenProps]);
|
|
176
|
-
for (const [prop, defaultVal] of Object.entries(UNIVERSAL_BASELINE)) {
|
|
177
|
-
if (prop in result) continue;
|
|
178
|
-
if (!allTweenedProps.has(prop)) continue;
|
|
179
|
-
const val = readLiveGsapValue(gsap, el, prop);
|
|
180
|
-
if (Number.isFinite(val) && Math.round(val * 1000) !== Math.round(defaultVal * 1000)) {
|
|
181
|
-
result[prop] = roundTo3(val);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Tier 2: Element-dependent properties — their "default" depends on the
|
|
98
|
+
// Element-dependent properties — their "default" depends on the
|
|
186
99
|
// stylesheet, so we compare GSAP's runtime value against the element's
|
|
187
100
|
// computed CSS value. Only capture if GSAP has actively changed it.
|
|
188
101
|
const COMPUTED_BASELINE = [
|
|
@@ -209,7 +122,7 @@ export function readAllAnimatedProperties(
|
|
|
209
122
|
for (const prop of COMPUTED_BASELINE) {
|
|
210
123
|
if (prop in result) continue;
|
|
211
124
|
if (!inGroup(prop)) continue;
|
|
212
|
-
if (
|
|
125
|
+
if (gsapWritesChannels(el, [prop])) continue;
|
|
213
126
|
const gsapVal = Number(gsap.getProperty(el, prop));
|
|
214
127
|
if (!Number.isFinite(gsapVal)) continue;
|
|
215
128
|
let cssVal = NaN;
|
|
@@ -3,10 +3,12 @@ import type { Composition } from "@hyperframes/sdk";
|
|
|
3
3
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
4
4
|
import type { PublishSdkSession } from "../utils/sdkCutover";
|
|
5
5
|
import type { RuntimeTweenChange } from "./gsapRuntimePatch";
|
|
6
|
+
import type { KeyframeUsageAction } from "../utils/keyframeUsage";
|
|
6
7
|
|
|
7
8
|
export interface MutationResult {
|
|
8
9
|
ok: boolean;
|
|
9
10
|
changed?: boolean;
|
|
11
|
+
mutationChanges?: boolean[];
|
|
10
12
|
parsed?: ParsedGsap;
|
|
11
13
|
before?: string;
|
|
12
14
|
after?: string;
|
|
@@ -15,6 +17,9 @@ export interface MutationResult {
|
|
|
15
17
|
|
|
16
18
|
export interface CommitMutationOptions {
|
|
17
19
|
label: string;
|
|
20
|
+
keyframeTelemetry?: boolean;
|
|
21
|
+
keyframeAction?: KeyframeUsageAction;
|
|
22
|
+
keyframeProperty?: string;
|
|
18
23
|
/** Observe the durable writer result without duplicating the request path. */
|
|
19
24
|
onResult?: (result: MutationResult) => void;
|
|
20
25
|
coalesceKey?: string;
|