@hyperframes/studio 0.8.115 → 0.8.116
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-C-9N8nk0.js → hyperframes-player-mGYvPVpw.js} +1 -1
- package/dist/assets/{index-BoUM6Xqb.js → index-BWHt0-ir.js} +1 -1
- package/dist/assets/{index-CAvZ0AA-.js → index-DYNaXPVf.js} +201 -201
- package/dist/assets/{index-CVuorIMv.js → index-Dr3hNpvS.js} +1 -1
- package/dist/{chunk-TXZMMOMA.js → chunk-3F47EFCT.js} +100 -2
- package/dist/chunk-3F47EFCT.js.map +1 -0
- package/dist/{domEditingLayers-D7VYFJJD.js → domEditingLayers-OL7GLSS4.js} +2 -2
- package/dist/index.d.ts +104 -22
- package/dist/index.html +1 -1
- package/dist/index.js +1131 -1044
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/captions/hooks/useCaptionSync.ts +10 -6
- package/src/components/ProjectUnreachableBanner.tsx +3 -3
- package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
- package/src/components/editor/domGroupMoveBy.ts +95 -0
- package/src/components/editor/dragUndoPaint.test.ts +5 -5
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
- package/src/components/editor/manualEditingAvailability.ts +8 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -6
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/probeSourceElement.ts +2 -1
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useFxCarveNodes.ts +2 -1
- package/src/components/editor/useFxLevelling.ts +2 -1
- package/src/components/feedback/projectProvenance.ts +2 -1
- package/src/components/nle/NLEContext.test.ts +1 -0
- package/src/components/nle/NLEContext.tsx +2 -1
- package/src/components/nle/NLEPreview.tsx +12 -10
- package/src/components/nle/PreviewPoster.tsx +2 -1
- package/src/components/renders/useFfmpegStatus.ts +2 -1
- package/src/components/renders/useRenderQueue.ts +5 -4
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/hooks/domSelectionTestHarness.ts +23 -0
- package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
- package/src/hooks/domStyleCommit.ts +8 -1
- package/src/hooks/draggedGsapPosition.ts +10 -0
- package/src/hooks/gsapDragCommit.test.ts +75 -164
- package/src/hooks/gsapDragCommit.ts +2 -105
- package/src/hooks/gsapDragPositionCommit.ts +30 -393
- package/src/hooks/gsapEditOutcome.ts +22 -2
- package/src/hooks/gsapGroupPreflight.ts +34 -0
- package/src/hooks/gsapMutationClient.ts +4 -3
- package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
- package/src/hooks/gsapParsedTween.test.ts +62 -0
- package/src/hooks/gsapParsedTween.ts +243 -0
- package/src/hooks/gsapPositionDetection.test.ts +78 -0
- package/src/hooks/gsapPositionDetection.ts +52 -33
- package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
- package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
- package/src/hooks/gsapResizeIntercept.test.ts +63 -11
- package/src/hooks/gsapResizeIntercept.ts +79 -169
- package/src/hooks/gsapResizeSweep.test.ts +16 -33
- package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
- package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
- package/src/hooks/gsapRuntimeBridge.ts +105 -131
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +8 -0
- package/src/hooks/gsapShared.ts +7 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +26 -58
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
- package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
- package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
- package/src/hooks/gsapValueAtPlayhead.ts +292 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
- package/src/hooks/keyframeCacheAstLoad.ts +2 -1
- package/src/hooks/newTweenTarget.test.ts +41 -76
- package/src/hooks/timelineTimingSync.test.ts +1 -0
- package/src/hooks/timelineTimingSync.ts +2 -1
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useBlockCatalog.ts +2 -1
- package/src/hooks/useCaptionDetection.ts +2 -1
- package/src/hooks/useCompositionContentLoader.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
- package/src/hooks/useDomEditSession.test.tsx +30 -1
- package/src/hooks/useDomEditSession.ts +10 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +23 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -2
- package/src/hooks/useDomGeometryCommit.ts +1 -0
- package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
- package/src/hooks/useEditHistoryActions.test.tsx +15 -0
- package/src/hooks/useEditHistoryActions.ts +16 -5
- package/src/hooks/useElementLifecycleOps.ts +2 -1
- package/src/hooks/useEnableKeyframes.test.ts +32 -1
- package/src/hooks/useEnableKeyframes.ts +2 -1
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +19 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +4 -2
- package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
- package/src/hooks/useFileManager.ts +14 -10
- package/src/hooks/useFileTree.ts +3 -2
- package/src/hooks/useFrameCapture.ts +5 -1
- package/src/hooks/useFreezeFrame.ts +3 -2
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +17 -29
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +5 -0
- package/src/hooks/useGsapAwareGroupMove.ts +16 -16
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapScriptCommits.test.tsx +3 -1
- package/src/hooks/useGsapScriptCommits.ts +12 -10
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +277 -1
- package/src/hooks/usePersistentEditHistory.ts +47 -8
- package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
- package/src/hooks/usePreviewPersistence.ts +13 -11
- package/src/hooks/useProjectFileWriter.ts +20 -19
- package/src/hooks/useRemoveBackground.ts +2 -1
- package/src/hooks/useSdkSession.ts +2 -1
- package/src/hooks/useServerConnection.ts +4 -3
- package/src/hooks/useStudioSelectionPublisher.ts +2 -1
- package/src/hooks/useThumbnailStill.test.ts +100 -0
- package/src/hooks/useThumbnailStill.ts +37 -0
- package/src/hooks/useTimelineDeleteOps.ts +2 -1
- package/src/index.ts +4 -0
- package/src/player/components/AudioWaveform.tsx +3 -2
- package/src/player/components/CompositionThumbnail.tsx +2 -1
- package/src/player/components/clipMenuNormalize.ts +2 -1
- package/src/player/components/clipPeakMap.ts +2 -1
- package/src/player/hooks/useTimelinePlayer.ts +5 -1
- package/src/player/lib/thumbnailScheduler.test.ts +16 -0
- package/src/player/lib/thumbnailScheduler.ts +10 -2
- package/src/player/sceneSwap.ts +2 -1
- package/src/telemetry/client.ts +2 -1
- package/src/utils/blockInstaller.ts +2 -1
- package/src/utils/elementGsap.test.ts +9 -0
- package/src/utils/elementGsap.ts +7 -0
- package/src/utils/gsapSoftReload.ts +2 -3
- package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
- package/src/utils/gsapUndoRestore.ts +3 -6
- package/src/utils/previewPrefetch.ts +2 -1
- package/src/utils/razorSplitTransaction.ts +12 -8
- package/src/utils/softReloadTargets.ts +1 -3
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +4 -1
- package/src/utils/studioHelpers.ts +7 -1
- package/src/utils/studioPendingEdits.test.ts +23 -6
- package/src/utils/studioPendingEdits.ts +32 -8
- package/src/utils/studioTelemetry.ts +2 -1
- package/src/utils/timelineCompositionInsert.ts +3 -2
- package/src/webmcp/StudioAgentTools.tsx +2 -1
- package/dist/chunk-TXZMMOMA.js.map +0 -1
- /package/dist/{domEditingLayers-D7VYFJJD.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
|
@@ -7,7 +7,8 @@ import { observeGsapGesture } from "./gsapGestureOutcome";
|
|
|
7
7
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
8
8
|
import type { CommitMutation } from "./gsapScriptCommitTypes";
|
|
9
9
|
import { computeCurrentPercentage } from "./gsapDragCommit";
|
|
10
|
-
import { tryGsapResizeIntercept } from "./gsapResizeIntercept";
|
|
10
|
+
import { commitSizeAtPlayhead, tryGsapResizeIntercept } from "./gsapResizeIntercept";
|
|
11
|
+
import { liveTween, previewWith, tween } from "./gsapParsedTween.test-helpers";
|
|
11
12
|
|
|
12
13
|
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
13
14
|
|
|
@@ -15,6 +16,8 @@ afterEach(() => {
|
|
|
15
16
|
vi.clearAllMocks();
|
|
16
17
|
vi.restoreAllMocks();
|
|
17
18
|
usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null });
|
|
19
|
+
// One #clip per test: leftovers would make every tween on it look shared.
|
|
20
|
+
document.body.innerHTML = "";
|
|
18
21
|
});
|
|
19
22
|
|
|
20
23
|
/**
|
|
@@ -42,10 +45,18 @@ function fakeIframe(el: HTMLElement, gsapValues: Record<string, number>) {
|
|
|
42
45
|
// The element's OPACITY intro tween lives on the timeline: unfiltered
|
|
43
46
|
// capture would pick `opacity` up via the other-tween sweep.
|
|
44
47
|
const opacityIntro = { targets: () => [el], vars: { opacity: 0, duration: 0.8 } };
|
|
48
|
+
const scaleIntro = liveTween(
|
|
49
|
+
el,
|
|
50
|
+
{ start: 0.2, duration: 0.8, vars: { scale: 0.9 }, ends: { scaleX: [0.9, 1] } },
|
|
51
|
+
{ from: true },
|
|
52
|
+
);
|
|
45
53
|
return {
|
|
46
54
|
contentWindow: {
|
|
47
|
-
__timelines: { main: { getChildren: () => [opacityIntro] } },
|
|
48
|
-
gsap: {
|
|
55
|
+
__timelines: { main: { getChildren: () => [opacityIntro, scaleIntro] } },
|
|
56
|
+
gsap: {
|
|
57
|
+
getProperty: (_el: Element, prop: string) => gsapValues[prop] ?? 0,
|
|
58
|
+
defaults: () => ({ ease: "power1.out" }),
|
|
59
|
+
},
|
|
49
60
|
},
|
|
50
61
|
contentDocument: document,
|
|
51
62
|
} as unknown as HTMLIFrameElement;
|
|
@@ -295,24 +306,26 @@ async function runResize(
|
|
|
295
306
|
return committed;
|
|
296
307
|
}
|
|
297
308
|
|
|
298
|
-
it("scale-route resize
|
|
309
|
+
it("scale-route resize keyframes scale through the group filter, not width/height", async () => {
|
|
299
310
|
const el = makeGradedElement();
|
|
300
311
|
const iframe = fakeIframe(el, { scale: 1, scaleX: 1, scaleY: 1, opacity: 0, rotation: 0 });
|
|
301
312
|
// uniform: 800/640 === 450/360
|
|
302
313
|
const committed = await runResize(el, iframe, { width: 800, height: 450 });
|
|
303
314
|
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// Group filter: the opacity intro tween must NOT leak into the
|
|
308
|
-
expect(
|
|
309
|
-
expect(
|
|
315
|
+
const keyframes = committed.find((m) => m.type === "replace-with-keyframes")?.keyframes as
|
|
316
|
+
| Array<{ properties: Record<string, number> }>
|
|
317
|
+
| undefined;
|
|
318
|
+
// Group filter: the opacity intro tween must NOT leak into the scale keyframes.
|
|
319
|
+
expect(keyframes?.some((k) => "opacity" in k.properties)).toBe(false);
|
|
320
|
+
expect(keyframes?.some((k) => "scale" in k.properties)).toBe(true);
|
|
310
321
|
|
|
311
322
|
// Every committed property is scale-group — the resize never writes
|
|
312
323
|
// width/height for a scale-driven element (the double-apply bug class).
|
|
313
324
|
const allProps = committed.flatMap((m) => [
|
|
314
325
|
...Object.keys((m.properties as Record<string, unknown>) ?? {}),
|
|
315
|
-
...
|
|
326
|
+
...((m.keyframes as Array<{ properties: object }>) ?? []).flatMap((k) =>
|
|
327
|
+
Object.keys(k.properties),
|
|
328
|
+
),
|
|
316
329
|
]);
|
|
317
330
|
expect(allProps).not.toContain("width");
|
|
318
331
|
expect(allProps).not.toContain("height");
|
|
@@ -560,6 +573,45 @@ it("hands the size to the element's CSS when its only tween is a fade", async ()
|
|
|
560
573
|
expect(commitMutation).not.toHaveBeenCalled();
|
|
561
574
|
});
|
|
562
575
|
|
|
576
|
+
it.each([
|
|
577
|
+
["stays put", { x: 0, y: 0 }, { width: 300 }, undefined],
|
|
578
|
+
["moves", { x: -40, y: 0 }, { width: 300, x: 10, y: 0 }, true],
|
|
579
|
+
])(
|
|
580
|
+
"a size written at the playhead carries the anchor only when the box %s",
|
|
581
|
+
async (_, offset, written, owns) => {
|
|
582
|
+
const selection = titleSelection();
|
|
583
|
+
const el = selection.element;
|
|
584
|
+
el.setAttribute("data-hf-drag-gsap-base-x", "50");
|
|
585
|
+
el.setAttribute("data-hf-drag-gsap-base-y", "0");
|
|
586
|
+
const slide = tween({
|
|
587
|
+
id: "#title-to-0",
|
|
588
|
+
targetSelector: "#title",
|
|
589
|
+
propertyGroup: undefined,
|
|
590
|
+
method: "to",
|
|
591
|
+
properties: { x: 100, width: 320 },
|
|
592
|
+
resolvedStart: 0,
|
|
593
|
+
duration: 2,
|
|
594
|
+
ease: "none",
|
|
595
|
+
});
|
|
596
|
+
usePlayerStore.setState({ currentTime: 1 });
|
|
597
|
+
const commitMutation = vi.fn();
|
|
598
|
+
const live = liveTween(el, { start: 0, duration: 2, vars: slide.properties });
|
|
599
|
+
|
|
600
|
+
const outcome = await commitSizeAtPlayhead(
|
|
601
|
+
selection,
|
|
602
|
+
slide,
|
|
603
|
+
{ width: 300 },
|
|
604
|
+
previewWith(el, [live]),
|
|
605
|
+
offset,
|
|
606
|
+
{ commitMutation },
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
expect(outcome.ownsDragOffset).toBe(owns);
|
|
610
|
+
const mutation = commitMutation.mock.calls[0]![1];
|
|
611
|
+
expect(mutation.keyframes[0].properties).toEqual(written);
|
|
612
|
+
},
|
|
613
|
+
);
|
|
614
|
+
|
|
563
615
|
it.each([
|
|
564
616
|
[5, 1],
|
|
565
617
|
[0.5, 1],
|
|
@@ -14,43 +14,34 @@ import {
|
|
|
14
14
|
} from "../components/editor/manualEditsTypes";
|
|
15
15
|
import { setElementGsapPosition, setElementGsapScale } from "../utils/elementGsap";
|
|
16
16
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
17
|
-
import {
|
|
17
|
+
import { readGsapProperty } from "./gsapRuntimeReaders";
|
|
18
18
|
import {
|
|
19
19
|
commitStaticGsapPosition,
|
|
20
20
|
commitStaticGsapSize,
|
|
21
|
-
commitKeyframedSizeFromResize,
|
|
22
21
|
computeCurrentPercentage,
|
|
23
22
|
findExistingPositionWrite,
|
|
24
23
|
findSizeSetAnimation,
|
|
25
|
-
materializeIfDynamic,
|
|
26
24
|
} from "./gsapDragCommit";
|
|
27
25
|
import type { GsapDragCommitCallbacks } from "./gsapDragCommit";
|
|
28
|
-
import { computeDraggedGsapPosition } from "./draggedGsapPosition";
|
|
26
|
+
import { computeDraggedGsapPosition, restoreDragOffset } from "./draggedGsapPosition";
|
|
29
27
|
import { pickClosestToPlayhead, readGsapPositionFromIframe } from "./gsapPositionDetection";
|
|
30
28
|
import { commitWholePropertyOffset } from "./gsapWholePropertyOffsetCommit";
|
|
31
29
|
import { commitGsapPositionFromDrag } from "./gsapDragPositionCommit";
|
|
32
|
-
import {
|
|
30
|
+
import { resolveTweenDuration } from "../utils/globalTimeCompiler";
|
|
33
31
|
import { isInstantHold, selectorFromSelection, writeTargetSelector } from "./gsapShared";
|
|
34
32
|
import { roundTo3, roundToLayoutPx } from "../utils/rounding";
|
|
35
33
|
import { resolveGroupTween } from "./gsapRuntimeBridge";
|
|
36
34
|
import { logResize } from "../utils/resizeDebug";
|
|
37
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
animationWritesAnyProperty,
|
|
37
|
+
assertGsapEditPersisted,
|
|
38
|
+
type GsapEditOutcome,
|
|
39
|
+
} from "./gsapEditOutcome";
|
|
40
|
+
import { commitValueAtPlayhead } from "./gsapValueAtPlayhead";
|
|
38
41
|
import { preflightGsapResizeIntercept, resizeRoute } from "./gsapResizePreflight";
|
|
39
42
|
|
|
40
|
-
const IDENTITY_ONE_PROPS = new Set(["opacity", "autoAlpha", "scale", "scaleX", "scaleY"]);
|
|
41
43
|
const SIZE_PROPS = new Set(["width", "height"]);
|
|
42
|
-
|
|
43
|
-
/** Build identity (zero / one) values for each property in `source`. */
|
|
44
|
-
function synthesizeIdentityProps(
|
|
45
|
-
source: Record<string, number | string>,
|
|
46
|
-
): Record<string, number | string> {
|
|
47
|
-
const id: Record<string, number | string> = {};
|
|
48
|
-
for (const [k, v] of Object.entries(source)) {
|
|
49
|
-
if (typeof v === "number") id[k] = IDENTITY_ONE_PROPS.has(k) ? 1 : 0;
|
|
50
|
-
else id[k] = v;
|
|
51
|
-
}
|
|
52
|
-
return id;
|
|
53
|
-
}
|
|
44
|
+
const POSITION_XY = new Set(["x", "y"]);
|
|
54
45
|
|
|
55
46
|
/**
|
|
56
47
|
* The element's box before the resize draft ran, in CSS pixels.
|
|
@@ -72,6 +63,51 @@ function originalBoxSize(
|
|
|
72
63
|
return Number.isFinite(inline) && inline > 0 ? inline : null;
|
|
73
64
|
}
|
|
74
65
|
|
|
66
|
+
/** The box's width and height before the draft, for a size tween that animates only one. */
|
|
67
|
+
function preGestureBoxSize(el: HTMLElement): Record<string, number> {
|
|
68
|
+
const width = originalBoxSize(el, STUDIO_ORIGINAL_BOX_WIDTH_ATTR, "width");
|
|
69
|
+
const height = originalBoxSize(el, STUDIO_ORIGINAL_BOX_HEIGHT_ATTR, "height");
|
|
70
|
+
return { ...(width != null && { width }), ...(height != null && { height }) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A size write at the playhead. When the same tween animates position, the resize's anchor move
|
|
74
|
+
* goes into this one write: a second write to the tween in the same gesture would plan on stale ids. */
|
|
75
|
+
export async function commitSizeAtPlayhead(
|
|
76
|
+
selection: DomEditSelection,
|
|
77
|
+
anim: GsapAnimation,
|
|
78
|
+
size: Record<string, number>,
|
|
79
|
+
iframe: HTMLIFrameElement | null,
|
|
80
|
+
dragOffset: { x: number; y: number } | undefined,
|
|
81
|
+
callbacks: GsapDragCommitCallbacks,
|
|
82
|
+
): Promise<GsapEditOutcome> {
|
|
83
|
+
const selector = selectorFromSelection(selection);
|
|
84
|
+
const moves = !!dragOffset && (dragOffset.x !== 0 || dragOffset.y !== 0) && !!selector;
|
|
85
|
+
const anchor =
|
|
86
|
+
moves && animationWritesAnyProperty(anim, POSITION_XY)
|
|
87
|
+
? computeDraggedGsapPosition(
|
|
88
|
+
selection.element,
|
|
89
|
+
dragOffset,
|
|
90
|
+
readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 },
|
|
91
|
+
)
|
|
92
|
+
: null;
|
|
93
|
+
const written = await commitValueAtPlayhead(
|
|
94
|
+
selection,
|
|
95
|
+
anim,
|
|
96
|
+
anchor ? { ...size, x: anchor.newX, y: anchor.newY } : size,
|
|
97
|
+
iframe,
|
|
98
|
+
callbacks,
|
|
99
|
+
{
|
|
100
|
+
label: "Resize",
|
|
101
|
+
backfill: {
|
|
102
|
+
...preGestureBoxSize(selection.element),
|
|
103
|
+
...(anchor && { x: anchor.baseGsapX, y: anchor.baseGsapY }),
|
|
104
|
+
},
|
|
105
|
+
...(anchor && { beforeReload: () => restoreDragOffset(selection.element) }),
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
return written.status === "persisted" && anchor ? { ...written, ownsDragOffset: true } : written;
|
|
109
|
+
}
|
|
110
|
+
|
|
75
111
|
/**
|
|
76
112
|
* Whether this tween already states scale as `scaleX`/`scaleY`.
|
|
77
113
|
*
|
|
@@ -98,6 +134,7 @@ export async function tryGsapResizeIntercept(
|
|
|
98
134
|
iframe: HTMLIFrameElement | null,
|
|
99
135
|
commitMutation: GsapDragCommitCallbacks["commitMutation"],
|
|
100
136
|
fetchFallbackAnimations?: () => Promise<GsapAnimation[]>,
|
|
137
|
+
dragOffset?: { x: number; y: number },
|
|
101
138
|
): Promise<GsapEditOutcome> {
|
|
102
139
|
const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
|
|
103
140
|
const outcome = preflightGsapResizeIntercept(selection, animations, iframe, fetchedAnimations);
|
|
@@ -120,7 +157,7 @@ export async function tryGsapResizeIntercept(
|
|
|
120
157
|
postSplitFetch,
|
|
121
158
|
);
|
|
122
159
|
|
|
123
|
-
|
|
160
|
+
const anim =
|
|
124
161
|
resolved?.anim && animationWritesAnyProperty(resolved.anim, resizeProperties)
|
|
125
162
|
? resolved.anim
|
|
126
163
|
: null;
|
|
@@ -166,15 +203,11 @@ export async function tryGsapResizeIntercept(
|
|
|
166
203
|
);
|
|
167
204
|
if (animatedTween) {
|
|
168
205
|
logResize("intercept-route", { route: "keyframed-size", tweenId: animatedTween.id });
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
animatedTween,
|
|
175
|
-
{ commitMutation, fetchAnimations: fetchFallbackAnimations },
|
|
176
|
-
);
|
|
177
|
-
if (handled) return { status: "persisted" };
|
|
206
|
+
const sized = { width: roundToLayoutPx(size.width), height: roundToLayoutPx(size.height) };
|
|
207
|
+
return commitSizeAtPlayhead(selection, animatedTween, sized, iframe, dragOffset, {
|
|
208
|
+
commitMutation,
|
|
209
|
+
fetchAnimations: fetchFallbackAnimations,
|
|
210
|
+
});
|
|
178
211
|
}
|
|
179
212
|
}
|
|
180
213
|
|
|
@@ -196,16 +229,10 @@ export async function tryGsapResizeIntercept(
|
|
|
196
229
|
|
|
197
230
|
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
|
198
231
|
const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
|
|
199
|
-
if (activeKeyframePct != null) setActiveKeyframePct(null);
|
|
200
232
|
const selector = selectorFromSelection(selection);
|
|
201
|
-
// Scope every capture to the resize group — same contract as the rotation
|
|
202
|
-
// intercept. Unfiltered, an opacity-touching intro tween on the element
|
|
203
|
-
// would ride into resize conversions/backfills (the Fix-2 bake class).
|
|
204
|
-
const runtimeProps = selector
|
|
205
|
-
? readAllAnimatedProperties(iframe, selector, anim, resizeGroup)
|
|
206
|
-
: {};
|
|
207
233
|
|
|
208
234
|
let resizeProps: Record<string, number>;
|
|
235
|
+
let resizeBackfill: Record<string, number>;
|
|
209
236
|
let scaleDraftEl: HTMLElement | null = null;
|
|
210
237
|
let scaleDraftDropPoint: { x: number; y: number } | null = null;
|
|
211
238
|
/** The scale this commit is putting on the element, for the finalize step. */
|
|
@@ -262,6 +289,7 @@ export async function tryGsapResizeIntercept(
|
|
|
262
289
|
resizeProps = useScaleLonghands
|
|
263
290
|
? { scaleX: newScaleX, scaleY: newScaleY }
|
|
264
291
|
: { scale: newScaleX };
|
|
292
|
+
resizeBackfill = { scaleX: liveScaleX, scaleY: liveScaleY };
|
|
265
293
|
logResize("intercept-route", {
|
|
266
294
|
route: "scale-tween",
|
|
267
295
|
cssW,
|
|
@@ -295,6 +323,7 @@ export async function tryGsapResizeIntercept(
|
|
|
295
323
|
width: roundToLayoutPx(size.width),
|
|
296
324
|
height: roundToLayoutPx(size.height),
|
|
297
325
|
};
|
|
326
|
+
resizeBackfill = preGestureBoxSize(selection.element);
|
|
298
327
|
}
|
|
299
328
|
// Finalize a scale-route commit: tear down the gesture's inline width/height
|
|
300
329
|
// draft (leaving it applied compounds with the committed scale — the element
|
|
@@ -404,10 +433,12 @@ export async function tryGsapResizeIntercept(
|
|
|
404
433
|
);
|
|
405
434
|
if (positionTween) {
|
|
406
435
|
logResize("scale-finalize", { route: "position-keyframe", tweenId: positionTween.id });
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
436
|
+
assertGsapEditPersisted(
|
|
437
|
+
await commitGsapPositionFromDrag(selection, positionTween, delta, base, iframe, {
|
|
438
|
+
commitMutation,
|
|
439
|
+
fetchAnimations: fetchFallbackAnimations,
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
411
442
|
return true;
|
|
412
443
|
}
|
|
413
444
|
const existingSet = findExistingPositionWrite(currentAnimations, selector, selection.element);
|
|
@@ -435,135 +466,14 @@ export async function tryGsapResizeIntercept(
|
|
|
435
466
|
return { status: "persisted", ownsDragOffset: await finalizeScaleResizeCommit() };
|
|
436
467
|
}
|
|
437
468
|
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const newId = await materializeIfDynamic(anim, iframe, commitMutation, selection);
|
|
447
|
-
if (newId) anim = { ...anim, id: newId };
|
|
448
|
-
} else if (!anim.keyframes) {
|
|
449
|
-
const resolvedFromValues = selector
|
|
450
|
-
? readAllAnimatedProperties(iframe, selector, anim, resizeGroup)
|
|
451
|
-
: undefined;
|
|
452
|
-
await commitMutation(
|
|
453
|
-
selection,
|
|
454
|
-
{ type: "convert-to-keyframes", animationId: anim.id, resolvedFromValues },
|
|
455
|
-
{ label: "Convert to keyframes for resize" },
|
|
456
|
-
);
|
|
457
|
-
if (fetchFallbackAnimations) {
|
|
458
|
-
const fresh = await fetchFallbackAnimations();
|
|
459
|
-
const refreshed = fresh.find(
|
|
460
|
-
(a) => a.targetSelector === anim!.targetSelector && a.keyframes,
|
|
461
|
-
);
|
|
462
|
-
if (refreshed) anim = refreshed;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// A NON-uniform scale must also take the full-rewrite path: it mixes
|
|
468
|
-
// scaleX/scaleY into a tween whose existing keyframes may carry the uniform
|
|
469
|
-
// `scale` shorthand, and GSAP's percentage keyframes animate each property
|
|
470
|
-
// name independently — a shorthand/longhand mix would leave the old `scale`
|
|
471
|
-
// sub-tween running against the new scaleX/scaleY. The rewrite below
|
|
472
|
-
// normalizes every keyframe to the longhands. For an in-range resize the
|
|
473
|
-
// min/max window math below degenerates to the tween's own start/duration,
|
|
474
|
-
// so timing is unchanged.
|
|
475
|
-
if ((outsideRange || useScaleLonghands) && ts !== null) {
|
|
476
|
-
// For flat tweens, synthesize the keyframes from the tween's properties
|
|
477
|
-
const kfs =
|
|
478
|
-
anim.keyframes?.keyframes ??
|
|
479
|
-
(() => {
|
|
480
|
-
const fromProps =
|
|
481
|
-
anim.method === "from" || anim.method === "fromTo"
|
|
482
|
-
? { ...anim.properties }
|
|
483
|
-
: synthesizeIdentityProps(anim.properties);
|
|
484
|
-
const toProps =
|
|
485
|
-
anim.method === "from"
|
|
486
|
-
? synthesizeIdentityProps(anim.properties)
|
|
487
|
-
: { ...anim.properties };
|
|
488
|
-
return [
|
|
489
|
-
{ percentage: 0, properties: fromProps },
|
|
490
|
-
{ percentage: 100, properties: toProps },
|
|
491
|
-
];
|
|
492
|
-
})();
|
|
493
|
-
const newStart = Math.min(ct, ts);
|
|
494
|
-
const newEnd = Math.max(ct, ts + td);
|
|
495
|
-
const newDuration = Math.max(0.01, newEnd - newStart);
|
|
496
|
-
const existingKfs = kfs;
|
|
497
|
-
const remapped: Array<{ percentage: number; properties: Record<string, number | string> }> = [];
|
|
498
|
-
for (const kf of existingKfs) {
|
|
499
|
-
const absTime = ts + (kf.percentage / 100) * td;
|
|
500
|
-
const newPct = Math.round(((absTime - newStart) / newDuration) * 1000) / 10;
|
|
501
|
-
const props = { ...kf.properties };
|
|
502
|
-
// Normalize the uniform `scale` shorthand to longhands when this commit
|
|
503
|
-
// writes scaleX/scaleY, so the tween never mixes the two forms.
|
|
504
|
-
if (nonUniformScale && "scale" in props) {
|
|
505
|
-
const uniform = props.scale;
|
|
506
|
-
if (typeof uniform === "number") {
|
|
507
|
-
props.scaleX = uniform;
|
|
508
|
-
props.scaleY = uniform;
|
|
509
|
-
}
|
|
510
|
-
delete props.scale;
|
|
511
|
-
}
|
|
512
|
-
// Only backfill properties that the animation already had (x, y, scale).
|
|
513
|
-
// Don't backfill width/height — they should only appear on the resize keyframe.
|
|
514
|
-
for (const k of Object.keys(resizeProps)) {
|
|
515
|
-
if (k in props) continue;
|
|
516
|
-
if (k === "width" || k === "height") continue;
|
|
517
|
-
props[k] = IDENTITY_ONE_PROPS.has(k) ? 1 : 0;
|
|
518
|
-
}
|
|
519
|
-
remapped.push({ percentage: newPct, properties: props });
|
|
520
|
-
}
|
|
521
|
-
const targetPct = Math.round(((ct - newStart) / newDuration) * 1000) / 10;
|
|
522
|
-
// An in-range rewrite can land on an existing keyframe's percentage —
|
|
523
|
-
// merge into it instead of emitting a duplicate step.
|
|
524
|
-
const collidingKf = remapped.find((kf) => Math.abs(kf.percentage - targetPct) < 0.05);
|
|
525
|
-
if (collidingKf) Object.assign(collidingKf.properties, resizeProps);
|
|
526
|
-
else remapped.push({ percentage: targetPct, properties: resizeProps });
|
|
527
|
-
remapped.sort((a, b) => a.percentage - b.percentage);
|
|
528
|
-
|
|
529
|
-
await commitMutation(
|
|
530
|
-
selection,
|
|
531
|
-
{
|
|
532
|
-
type: "replace-with-keyframes",
|
|
533
|
-
animationId: anim.id,
|
|
534
|
-
targetSelector: anim.targetSelector,
|
|
535
|
-
position: roundTo3(newStart),
|
|
536
|
-
duration: roundTo3(newDuration),
|
|
537
|
-
keyframes: remapped,
|
|
538
|
-
},
|
|
539
|
-
{
|
|
540
|
-
label: outsideRange
|
|
541
|
-
? `Resize (extended to ${ct.toFixed(2)}s)`
|
|
542
|
-
: `Resize (keyframe ${Math.round(((ct - newStart) / newDuration) * 1000) / 10}%)`,
|
|
543
|
-
softReload: true,
|
|
544
|
-
...(!collidingKf ? { keyframeAction: "add" as const } : {}),
|
|
545
|
-
},
|
|
546
|
-
);
|
|
547
|
-
return { status: "persisted", ownsDragOffset: await finalizeScaleResizeCommit() };
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const backfillDefaults: Record<string, number> = {};
|
|
551
|
-
for (const k of Object.keys(runtimeProps)) {
|
|
552
|
-
if (SIZE_PROPS.has(k)) continue;
|
|
553
|
-
backfillDefaults[k] = IDENTITY_ONE_PROPS.has(k) ? 1 : 0;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
await commitMutation(
|
|
557
|
-
selection,
|
|
558
|
-
{
|
|
559
|
-
type: "add-keyframe",
|
|
560
|
-
animationId: anim.id,
|
|
561
|
-
percentage: pct,
|
|
562
|
-
properties: resizeProps,
|
|
563
|
-
backfillDefaults,
|
|
564
|
-
},
|
|
565
|
-
{ label: `Resize (keyframe ${pct}%)`, softReload: true },
|
|
566
|
-
);
|
|
469
|
+
const callbacks = { commitMutation, fetchAnimations: fetchFallbackAnimations };
|
|
470
|
+
if (resizeGroup === "size")
|
|
471
|
+
return commitSizeAtPlayhead(selection, anim, resizeProps, iframe, dragOffset, callbacks);
|
|
472
|
+
const written = await commitValueAtPlayhead(selection, anim, resizeProps, iframe, callbacks, {
|
|
473
|
+
label: "Resize",
|
|
474
|
+
backfill: resizeBackfill,
|
|
475
|
+
});
|
|
476
|
+
if (written.status !== "persisted") return written;
|
|
567
477
|
return { status: "persisted", ownsDragOffset: await finalizeScaleResizeCommit() };
|
|
568
478
|
}
|
|
569
479
|
|
|
@@ -23,57 +23,40 @@ import { classifyTweenPropertyGroup } from "@hyperframes/core/gsap-parser";
|
|
|
23
23
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
24
24
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
25
25
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
26
|
+
import { elTween, resetGsapEditState } from "./gsapParsedTween.test-helpers";
|
|
26
27
|
import { tryGsapResizeIntercept } from "./gsapResizeIntercept";
|
|
27
28
|
|
|
28
|
-
afterEach(
|
|
29
|
-
vi.restoreAllMocks();
|
|
30
|
-
usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null });
|
|
31
|
-
document.body.innerHTML = "";
|
|
32
|
-
});
|
|
29
|
+
afterEach(resetGsapEditState);
|
|
33
30
|
|
|
34
31
|
type Props = Record<string, number>;
|
|
35
32
|
|
|
36
|
-
function tween(id: string, properties: Props, duration: number): GsapAnimation {
|
|
37
|
-
return {
|
|
38
|
-
id,
|
|
39
|
-
targetSelector: "#el",
|
|
40
|
-
propertyGroup: classifyTweenPropertyGroup(properties),
|
|
41
|
-
method: "to",
|
|
42
|
-
properties,
|
|
43
|
-
position: 0,
|
|
44
|
-
resolvedStart: 0,
|
|
45
|
-
duration,
|
|
46
|
-
...(duration === 0 ? { extras: { immediateRender: "__raw:true" } } : {}),
|
|
47
|
-
} as unknown as GsapAnimation;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
33
|
/** The dimensions an element's animations actually vary across. */
|
|
51
34
|
const SCALE = {
|
|
52
35
|
none: null,
|
|
53
|
-
"instant hold": () =>
|
|
54
|
-
tween: () =>
|
|
55
|
-
longhands: () =>
|
|
36
|
+
"instant hold": () => elTween("#el-scale", { scale: 1.2 }, 0),
|
|
37
|
+
tween: () => elTween("#el-scale", { scale: 1.2 }, 2),
|
|
38
|
+
longhands: () => elTween("#el-scale", { scaleX: 1.2, scaleY: 1.1 }, 2),
|
|
56
39
|
} as const;
|
|
57
40
|
const SIZE = {
|
|
58
41
|
none: null,
|
|
59
|
-
"instant hold": () =>
|
|
60
|
-
tween: () =>
|
|
42
|
+
"instant hold": () => elTween("#el-size", { width: 300, height: 200 }, 0),
|
|
43
|
+
tween: () => elTween("#el-size", { width: 300, height: 200 }, 2),
|
|
61
44
|
} as const;
|
|
62
45
|
const POSITION = {
|
|
63
46
|
none: null,
|
|
64
|
-
"static hold": () =>
|
|
65
|
-
tween: () =>
|
|
47
|
+
"static hold": () => elTween("#el-position", { x: 40, y: 60 }, 0),
|
|
48
|
+
tween: () => elTween("#el-position", { x: 40, y: 60 }, 2),
|
|
66
49
|
} as const;
|
|
67
50
|
const EXTRA = {
|
|
68
51
|
none: null,
|
|
69
52
|
// What a 3D card carries, and the shape that produced two same-group ids.
|
|
70
53
|
"3d and rotation": () => [
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
54
|
+
elTween("#el-other", { rotationY: -540, rotationX: 720, _auto: 0 }, 0),
|
|
55
|
+
elTween("#el-rotation", { rotation: 720 }, 0),
|
|
56
|
+
elTween("#el-other-2", { z: 50 }, 0),
|
|
74
57
|
],
|
|
75
58
|
// A tween that already spans two groups, which the resize has to split.
|
|
76
|
-
"a mixed tween": () => [
|
|
59
|
+
"a mixed tween": () => [elTween("#el-mixed", { scale: 1.2, width: 300, height: 200 }, 0)],
|
|
77
60
|
} as const;
|
|
78
61
|
|
|
79
62
|
interface Recorded {
|
|
@@ -113,7 +96,7 @@ function fakeSource(initial: GsapAnimation[]) {
|
|
|
113
96
|
current = [
|
|
114
97
|
...current.filter((animation) => animation.id !== id),
|
|
115
98
|
...[...byGroup].map(([group, properties]) =>
|
|
116
|
-
|
|
99
|
+
elTween(`#el-split-${group}`, properties, target.duration ?? 0),
|
|
117
100
|
),
|
|
118
101
|
];
|
|
119
102
|
};
|
|
@@ -123,7 +106,7 @@ function fakeSource(initial: GsapAnimation[]) {
|
|
|
123
106
|
if (mutation.type === "split-into-property-groups") return id && split(id);
|
|
124
107
|
if (!id) {
|
|
125
108
|
if (mutation.type === "add")
|
|
126
|
-
current = [...current,
|
|
109
|
+
current = [...current, elTween(`#el-added-${current.length}`, properties, 0)];
|
|
127
110
|
return;
|
|
128
111
|
}
|
|
129
112
|
mergeInto(id, properties);
|
|
@@ -224,7 +207,7 @@ it(`sweeps ${CASES.length} animated shapes without a stale id or a mixed tween`,
|
|
|
224
207
|
*/
|
|
225
208
|
function scaleAt(id: string, position: number): GsapAnimation {
|
|
226
209
|
return {
|
|
227
|
-
...
|
|
210
|
+
...elTween(id, { scale: 1 }, 2),
|
|
228
211
|
position,
|
|
229
212
|
resolvedStart: position,
|
|
230
213
|
keyframes: {
|
|
@@ -121,7 +121,7 @@ describe("tryGsapDragIntercept: which tweens a word's drag may use", () => {
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
describe("tryGsapDragIntercept: one drag is one undo step", () => {
|
|
124
|
-
it("records the
|
|
124
|
+
it("records the move under one gesture key, without splitting the mixed tween", async () => {
|
|
125
125
|
document.body.innerHTML = `<div id="hero"></div>`;
|
|
126
126
|
const hero = document.getElementById("hero")!;
|
|
127
127
|
const mixed = {
|
|
@@ -133,12 +133,8 @@ describe("tryGsapDragIntercept: one drag is one undo step", () => {
|
|
|
133
133
|
position: 0,
|
|
134
134
|
resolvedStart: 0,
|
|
135
135
|
duration: 1,
|
|
136
|
+
ease: "none",
|
|
136
137
|
} as unknown as GsapAnimation;
|
|
137
|
-
const split = [
|
|
138
|
-
{ ...mixed, id: "#hero-fromTo-0-position", propertyGroup: "position" },
|
|
139
|
-
{ ...mixed, id: "#hero-fromTo-0-opacity", propertyGroup: "opacity" },
|
|
140
|
-
] as GsapAnimation[];
|
|
141
|
-
let fetches = 0;
|
|
142
138
|
const commitMutation = vi.fn();
|
|
143
139
|
await tryGsapDragIntercept(
|
|
144
140
|
{ id: "hero", selector: "#hero", element: hero } as unknown as DomEditSelection,
|
|
@@ -146,10 +142,12 @@ describe("tryGsapDragIntercept: one drag is one undo step", () => {
|
|
|
146
142
|
[mixed],
|
|
147
143
|
null,
|
|
148
144
|
commitMutation,
|
|
149
|
-
async () =>
|
|
145
|
+
async () => [mixed],
|
|
150
146
|
);
|
|
147
|
+
const types = commitMutation.mock.calls.map((call) => call[1].type);
|
|
151
148
|
const options = commitMutation.mock.calls.map((call) => call[2]);
|
|
152
|
-
expect(
|
|
149
|
+
expect(types).not.toContain("split-into-property-groups");
|
|
150
|
+
expect(options.length).toBeGreaterThan(0);
|
|
153
151
|
expect(new Set(options.map((o) => o.coalesceKey)).size).toBe(1);
|
|
154
152
|
expect(options[0].coalesceKey).toMatch(/^gsap:drag:/);
|
|
155
153
|
expect(options.every((o) => o.coalesceMs === Number.POSITIVE_INFINITY)).toBe(true);
|
|
@@ -23,7 +23,7 @@ function fakeIframe(elId: string, children: unknown[]): HTMLIFrameElement {
|
|
|
23
23
|
return {
|
|
24
24
|
contentWindow: {
|
|
25
25
|
__timelines: { "index.html": timeline },
|
|
26
|
-
gsap: { getProperty: () => 0 },
|
|
26
|
+
gsap: { getProperty: () => 0, defaults: () => ({ ease: "power1.out" }) },
|
|
27
27
|
},
|
|
28
28
|
contentDocument: { querySelector: (sel: string) => (sel === `#${elId}` ? el : null) },
|
|
29
29
|
} as unknown as HTMLIFrameElement;
|
|
@@ -451,7 +451,8 @@ describe("tryGsapDragIntercept — autoKeyframeEnabled toggle (#1808)", () => {
|
|
|
451
451
|
fakeIframe("puck-b", []),
|
|
452
452
|
commitMutation,
|
|
453
453
|
);
|
|
454
|
-
|
|
454
|
+
const mutations = commitMutation.mock.calls.map(([, mutation]) => mutation);
|
|
455
|
+
return { handled, mutations, types: mutations.map((mutation) => mutation.type) };
|
|
455
456
|
}
|
|
456
457
|
|
|
457
458
|
it("shifts the whole tween instead of adding a keyframe when the toggle is off", async () => {
|
|
@@ -461,10 +462,40 @@ describe("tryGsapDragIntercept — autoKeyframeEnabled toggle (#1808)", () => {
|
|
|
461
462
|
expect(types).not.toContain("add-keyframe");
|
|
462
463
|
});
|
|
463
464
|
|
|
464
|
-
it("
|
|
465
|
-
|
|
465
|
+
it("shifts a linear flat tween as keyframes that stay linear", async () => {
|
|
466
|
+
usePlayerStore.setState({ autoKeyframeEnabled: false, currentTime: 1 });
|
|
467
|
+
const commitMutation = vi.fn();
|
|
468
|
+
const linear = { ...stalePositionAnim, ease: "none" } as GsapAnimation;
|
|
469
|
+
const live = {
|
|
470
|
+
targets: () => [{ id: "puck-b" }],
|
|
471
|
+
vars: { x: -180, y: -60, ease: "none" },
|
|
472
|
+
duration: () => 2,
|
|
473
|
+
startTime: () => 1,
|
|
474
|
+
};
|
|
475
|
+
await tryGsapDragIntercept(
|
|
476
|
+
selection,
|
|
477
|
+
{ x: -50, y: 0 },
|
|
478
|
+
[linear],
|
|
479
|
+
fakeIframe("puck-b", [live]),
|
|
480
|
+
commitMutation,
|
|
481
|
+
);
|
|
482
|
+
const [mutation] = commitMutation.mock.calls.map(([, m]) => m);
|
|
483
|
+
expect(mutation).toMatchObject({ type: "replace-with-keyframes", easeEach: "none" });
|
|
484
|
+
expect(mutation).not.toHaveProperty("ease", "none");
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("still changes only the keyframe at the playhead when the toggle is on (default)", async () => {
|
|
488
|
+
const { handled, mutations } = await runAutoKeyframeDrag(true);
|
|
466
489
|
expect(handled).toEqual({ status: "persisted" });
|
|
467
|
-
expect(
|
|
490
|
+
expect(mutations).toEqual([
|
|
491
|
+
expect.objectContaining({
|
|
492
|
+
type: "replace-with-keyframes",
|
|
493
|
+
keyframes: [
|
|
494
|
+
{ percentage: 0, properties: { x: 0, y: 0 } },
|
|
495
|
+
{ percentage: 100, properties: { x: -50, y: 0 } },
|
|
496
|
+
],
|
|
497
|
+
}),
|
|
498
|
+
]);
|
|
468
499
|
});
|
|
469
500
|
});
|
|
470
501
|
|
|
@@ -540,7 +571,11 @@ describe("tryGsapDragIntercept — motion paths", () => {
|
|
|
540
571
|
],
|
|
541
572
|
ease: "none",
|
|
542
573
|
},
|
|
543
|
-
expect.objectContaining({
|
|
574
|
+
expect.objectContaining({
|
|
575
|
+
label: "Move layer (new keyframe)",
|
|
576
|
+
keyframeAction: "add",
|
|
577
|
+
softReload: true,
|
|
578
|
+
}),
|
|
544
579
|
);
|
|
545
580
|
expect(commitMutation.mock.calls.map(([, mutation]) => mutation.type)).not.toContain(
|
|
546
581
|
"add-motion-path-point",
|