@hyperframes/studio 0.8.115 → 0.8.117
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-5fcgSstJ.js} +1 -1
- package/dist/assets/{index-CAvZ0AA-.js → index-C5qKdpgl.js} +205 -203
- package/dist/assets/{index-BoUM6Xqb.js → index-XJSawwYe.js} +1 -1
- package/dist/assets/{index-CVuorIMv.js → index-tg2LXUeu.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 +132 -41
- package/dist/index.html +1 -1
- package/dist/index.js +1184 -1084
- 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/DomEditOverlay.tsx +1 -0
- package/src/components/editor/domEditOverlayGestures.ts +1 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +2 -2
- 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 +8 -9
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/plainTranslate.ts +5 -3
- package/src/components/editor/probeSourceElement.ts +2 -1
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/propertyPanelHelpers.ts +2 -2
- package/src/components/editor/propertyPanelTransformCommit.ts +2 -2
- package/src/components/editor/rotationDraft.ts +2 -1
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useDomEditOverlayGestures.ts +2 -2
- 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/gsapRuntimeKeyframes.ts +16 -3
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +9 -1
- 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/plainRotation.ts +5 -1
- 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/useDomEditPersist.test.tsx +335 -0
- 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.test.tsx +5 -1
- package/src/hooks/useDomGeometryCommit.ts +7 -9
- 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.test.ts +39 -3
- package/src/hooks/useFreezeFrame.ts +7 -3
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +42 -29
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +17 -8
- package/src/hooks/useGsapAwareGroupMove.ts +18 -18
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapScriptCommits.test.tsx +72 -2
- package/src/hooks/useGsapScriptCommits.ts +21 -17
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +302 -3
- package/src/hooks/usePersistentEditHistory.ts +50 -17
- 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.test.ts +39 -0
- package/src/utils/gsapSoftReload.ts +9 -14
- package/src/utils/gsapUndoRestore.browser.test.ts +94 -41
- 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 +5 -6
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +5 -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
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
2
|
-
import { PROPERTY_GROUPS } from "@hyperframes/parsers/gsap-constants";
|
|
3
2
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
4
3
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
5
|
-
import { resolveTweenStart
|
|
4
|
+
import { resolveTweenStart } from "../utils/globalTimeCompiler";
|
|
6
5
|
import { KEYFRAME_PCT_MATCH, resolveEditableTweenDuration } from "./gsapShared";
|
|
7
6
|
import { roundTo3 } from "../utils/rounding";
|
|
8
7
|
import { computeDraggedGsapPosition } from "./draggedGsapPosition";
|
|
@@ -10,8 +9,9 @@ import {
|
|
|
10
9
|
type GsapDragCommitCallbacks,
|
|
11
10
|
computeCurrentPercentage,
|
|
12
11
|
parkPlayheadOnKeyframe,
|
|
13
|
-
materializeIfDynamic,
|
|
14
12
|
} from "./gsapDragCommit";
|
|
13
|
+
import type { GsapEditOutcome } from "./gsapEditOutcome";
|
|
14
|
+
import { commitValueAtPlayhead, planValueEdit } from "./gsapValueAtPlayhead";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The tween's keyframes with one inserted at `percentage`. Any existing keyframe
|
|
@@ -38,237 +38,26 @@ export function buildTemporalArcKeyframes(
|
|
|
38
38
|
].sort((a, b) => a.percentage - b.percentage);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/** commitGsapPositionFromDrag's refusal, decided without writing. */
|
|
42
|
+
export function gsapPositionFromDragOutcome(
|
|
42
43
|
selection: DomEditSelection,
|
|
43
44
|
anim: GsapAnimation,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const newStart = Math.min(targetTime, tweenStart);
|
|
54
|
-
const newEnd = Math.max(targetTime, tweenEnd);
|
|
55
|
-
const newDuration = Math.max(0.01, newEnd - newStart);
|
|
56
|
-
const existingKfs = anim.keyframes?.keyframes ?? [];
|
|
57
|
-
const remappedKfs: Array<{ percentage: number; properties: Record<string, number | string> }> =
|
|
58
|
-
[];
|
|
59
|
-
for (const kf of existingKfs) {
|
|
60
|
-
const absTime = tweenStart + (kf.percentage / 100) * tweenDuration;
|
|
61
|
-
const newPct = Math.round(((absTime - newStart) / newDuration) * 1000) / 10;
|
|
62
|
-
const props: Record<string, number | string> = { ...kf.properties };
|
|
63
|
-
for (const k of Object.keys(properties)) {
|
|
64
|
-
if (!(k in props) && backfillDefaults?.[k] != null) props[k] = backfillDefaults[k];
|
|
65
|
-
}
|
|
66
|
-
remappedKfs.push({ percentage: newPct, properties: props });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const targetPct = Math.round(((targetTime - newStart) / newDuration) * 1000) / 10;
|
|
70
|
-
remappedKfs.push({ percentage: targetPct, properties });
|
|
71
|
-
|
|
72
|
-
remappedKfs.sort((a, b) => a.percentage - b.percentage);
|
|
73
|
-
|
|
74
|
-
await callbacks.commitMutation(
|
|
75
|
-
selection,
|
|
76
|
-
{
|
|
77
|
-
type: "replace-with-keyframes",
|
|
78
|
-
animationId: anim.id,
|
|
79
|
-
targetSelector: anim.targetSelector,
|
|
80
|
-
position: roundTo3(newStart),
|
|
81
|
-
duration: roundTo3(newDuration),
|
|
82
|
-
keyframes: remappedKfs,
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
label: `Move layer (extended keyframe)`,
|
|
86
|
-
keyframeAction: "add",
|
|
87
|
-
softReload: true,
|
|
88
|
-
beforeReload,
|
|
89
|
-
},
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// fallow-ignore-next-line complexity
|
|
94
|
-
async function commitKeyframedPosition(
|
|
95
|
-
selection: DomEditSelection,
|
|
96
|
-
anim: GsapAnimation,
|
|
97
|
-
properties: Record<string, number>,
|
|
98
|
-
callbacks: GsapDragCommitCallbacks,
|
|
99
|
-
beforeReload?: () => void,
|
|
100
|
-
backfillDefaults?: Record<string, number>,
|
|
101
|
-
): Promise<void> {
|
|
102
|
-
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
|
103
|
-
const computedPct = computeCurrentPercentage(selection, anim);
|
|
104
|
-
const pct = activeKeyframePct ?? computedPct;
|
|
105
|
-
await callbacks.commitMutation(
|
|
106
|
-
selection,
|
|
107
|
-
{
|
|
108
|
-
type: "add-keyframe",
|
|
109
|
-
animationId: anim.id,
|
|
110
|
-
percentage: pct,
|
|
111
|
-
properties,
|
|
112
|
-
...(backfillDefaults ? { backfillDefaults } : {}),
|
|
113
|
-
},
|
|
114
|
-
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload },
|
|
115
|
-
);
|
|
116
|
-
if (activeKeyframePct != null) {
|
|
117
|
-
setActiveKeyframePct(null);
|
|
118
|
-
parkPlayheadOnKeyframe(anim, pct);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
interface DragRuntimeGsap {
|
|
123
|
-
getProperty: (target: Element, key: string) => unknown;
|
|
124
|
-
set: (target: Element, vars: Record<string, unknown>) => void;
|
|
125
|
-
}
|
|
126
|
-
interface DragRuntimeTimeline {
|
|
127
|
-
seek: (time: number) => void;
|
|
128
|
-
}
|
|
129
|
-
interface DragRuntime {
|
|
130
|
-
gsapLib: DragRuntimeGsap;
|
|
131
|
-
el: Element;
|
|
132
|
-
mainTl: DragRuntimeTimeline;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function resolveDragRuntime(
|
|
136
|
-
iframe: HTMLIFrameElement | null | undefined,
|
|
137
|
-
selector: string | undefined,
|
|
138
|
-
): DragRuntime | null {
|
|
139
|
-
if (!iframe || !selector) return null;
|
|
140
|
-
const win = iframe.contentWindow as
|
|
141
|
-
| (Window & {
|
|
142
|
-
gsap?: Partial<DragRuntimeGsap>;
|
|
143
|
-
__timelines?: Record<string, Partial<DragRuntimeTimeline>>;
|
|
144
|
-
})
|
|
145
|
-
| null;
|
|
146
|
-
const gsap = win?.gsap;
|
|
147
|
-
if (typeof gsap?.getProperty !== "function" || typeof gsap.set !== "function") return null;
|
|
148
|
-
let el: Element | null = null;
|
|
149
|
-
try {
|
|
150
|
-
el = iframe.contentDocument?.querySelector(selector) ?? null;
|
|
151
|
-
} catch {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
if (!el) return null;
|
|
155
|
-
const timelines = win?.__timelines;
|
|
156
|
-
const mainTl = timelines ? Object.values(timelines)[0] : undefined;
|
|
157
|
-
if (typeof mainTl?.seek !== "function") return null;
|
|
158
|
-
return {
|
|
159
|
-
gsapLib: gsap as DragRuntimeGsap,
|
|
160
|
-
el,
|
|
161
|
-
mainTl: mainTl as DragRuntimeTimeline,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// fallow-ignore-next-line complexity
|
|
166
|
-
async function commitFlatViaKeyframes(
|
|
167
|
-
selection: DomEditSelection,
|
|
168
|
-
anim: GsapAnimation,
|
|
169
|
-
properties: Record<string, number>,
|
|
170
|
-
callbacks: GsapDragCommitCallbacks,
|
|
171
|
-
beforeReload?: () => void,
|
|
172
|
-
iframe?: HTMLIFrameElement | null,
|
|
173
|
-
selector?: string,
|
|
174
|
-
backfillDefaults?: Record<string, number>,
|
|
175
|
-
): Promise<void> {
|
|
176
|
-
const ct = usePlayerStore.getState().currentTime;
|
|
177
|
-
const ts = resolveTweenStart(anim);
|
|
178
|
-
const td = resolveEditableTweenDuration(anim, selection);
|
|
179
|
-
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
|
180
|
-
const outsideRange =
|
|
181
|
-
activeKeyframePct == null && ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
|
182
|
-
|
|
183
|
-
const resolvedFromValues: Record<string, number | string> = {};
|
|
184
|
-
const runtime = resolveDragRuntime(iframe, selector);
|
|
185
|
-
if (runtime && ts !== null) {
|
|
186
|
-
const { gsapLib, el, mainTl } = runtime;
|
|
187
|
-
const draggedValues: Record<string, number> = {};
|
|
188
|
-
const channelsTheClearWipes = new Set([
|
|
189
|
-
...Object.keys(properties),
|
|
190
|
-
...PROPERTY_GROUPS.position,
|
|
191
|
-
]);
|
|
192
|
-
for (const key of channelsTheClearWipes) {
|
|
193
|
-
const v = Number(gsapLib.getProperty(el, key));
|
|
194
|
-
if (Number.isFinite(v)) draggedValues[key] = v;
|
|
195
|
-
}
|
|
196
|
-
try {
|
|
197
|
-
gsapLib.set(el, { clearProps: Object.keys(properties).join(",") });
|
|
198
|
-
mainTl.seek(ts);
|
|
199
|
-
for (const key of Object.keys(properties)) {
|
|
200
|
-
const v = Number(gsapLib.getProperty(el, key));
|
|
201
|
-
if (Number.isFinite(v)) resolvedFromValues[key] = roundTo3(v);
|
|
202
|
-
}
|
|
203
|
-
mainTl.seek(ct);
|
|
204
|
-
} catch {
|
|
205
|
-
for (const key of Object.keys(resolvedFromValues)) delete resolvedFromValues[key];
|
|
206
|
-
} finally {
|
|
207
|
-
if (Object.keys(draggedValues).length > 0) gsapLib.set(el, draggedValues);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (outsideRange && ts !== null) {
|
|
212
|
-
const coalesceKey = `gsap:convert-drag:${anim.id}`;
|
|
213
|
-
await callbacks.commitMutation(
|
|
214
|
-
selection,
|
|
215
|
-
{
|
|
216
|
-
type: "convert-to-keyframes",
|
|
217
|
-
animationId: anim.id,
|
|
218
|
-
...(Object.keys(resolvedFromValues).length > 0 ? { resolvedFromValues } : {}),
|
|
219
|
-
},
|
|
220
|
-
{ label: "Convert to keyframes for drag", skipReload: true, coalesceKey },
|
|
221
|
-
);
|
|
222
|
-
const fresh = callbacks.fetchAnimations ? await callbacks.fetchAnimations() : [];
|
|
223
|
-
// By id first: a target with several tweens (two `to`s on the same selector)
|
|
224
|
-
// matches the selector lookup on whichever one happens to be first, and the
|
|
225
|
-
// extend-and-add below would then rewrite a tween the drag never touched.
|
|
226
|
-
const converted =
|
|
227
|
-
fresh.find((a) => a.id === anim.id && a.keyframes) ??
|
|
228
|
-
fresh.find((a) => a.targetSelector === anim.targetSelector && a.keyframes) ??
|
|
229
|
-
anim;
|
|
230
|
-
const convertedStart = resolveTweenStart(converted) ?? ts;
|
|
231
|
-
const convertedDur = resolveTweenDuration(converted) || td;
|
|
232
|
-
await extendTweenAndAddKeyframe(
|
|
233
|
-
selection,
|
|
234
|
-
converted,
|
|
235
|
-
properties,
|
|
236
|
-
ct,
|
|
237
|
-
convertedStart,
|
|
238
|
-
convertedDur,
|
|
239
|
-
callbacks,
|
|
240
|
-
beforeReload,
|
|
241
|
-
);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const coalesceKey = `gsap:convert-drag:${anim.id}`;
|
|
246
|
-
// fallow-ignore-next-line code-duplication
|
|
247
|
-
await callbacks.commitMutation(
|
|
248
|
-
selection,
|
|
249
|
-
{
|
|
250
|
-
type: "convert-to-keyframes",
|
|
251
|
-
animationId: anim.id,
|
|
252
|
-
...(Object.keys(resolvedFromValues).length > 0 ? { resolvedFromValues } : {}),
|
|
253
|
-
},
|
|
254
|
-
{ label: "Convert to keyframes for drag", skipReload: true, coalesceKey },
|
|
255
|
-
);
|
|
256
|
-
const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
|
|
257
|
-
const editedSelected = activeKeyframePct != null;
|
|
258
|
-
if (editedSelected) setActiveKeyframePct(null);
|
|
259
|
-
|
|
260
|
-
await callbacks.commitMutation(
|
|
261
|
-
selection,
|
|
262
|
-
{
|
|
263
|
-
type: "add-keyframe",
|
|
264
|
-
animationId: anim.id,
|
|
265
|
-
percentage: pct,
|
|
266
|
-
properties,
|
|
267
|
-
...(backfillDefaults ? { backfillDefaults } : {}),
|
|
268
|
-
},
|
|
269
|
-
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload, coalesceKey },
|
|
45
|
+
studioOffset: { x: number; y: number },
|
|
46
|
+
gsapPos: { x: number; y: number },
|
|
47
|
+
iframe: HTMLIFrameElement | null,
|
|
48
|
+
): GsapEditOutcome {
|
|
49
|
+
if (anim.arcPath?.enabled) return { status: "persisted" };
|
|
50
|
+
const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
|
|
51
|
+
selection.element,
|
|
52
|
+
studioOffset,
|
|
53
|
+
gsapPos,
|
|
270
54
|
);
|
|
271
|
-
|
|
55
|
+
const plan = planValueEdit(selection, anim, { x: newX, y: newY }, iframe, {
|
|
56
|
+
backfill: { x: baseGsapX, y: baseGsapY },
|
|
57
|
+
});
|
|
58
|
+
return plan.ok
|
|
59
|
+
? { status: "persisted" }
|
|
60
|
+
: { status: "blocked", reason: "keyframes-uneditable", detail: plan.reason };
|
|
272
61
|
}
|
|
273
62
|
|
|
274
63
|
// fallow-ignore-next-line code-duplication
|
|
@@ -279,9 +68,8 @@ export async function commitGsapPositionFromDrag(
|
|
|
279
68
|
studioOffset: { x: number; y: number },
|
|
280
69
|
gsapPos: { x: number; y: number },
|
|
281
70
|
iframe: HTMLIFrameElement | null,
|
|
282
|
-
selector: string,
|
|
283
71
|
callbacks: GsapDragCommitCallbacks,
|
|
284
|
-
): Promise<
|
|
72
|
+
): Promise<GsapEditOutcome> {
|
|
285
73
|
const el = selection.element;
|
|
286
74
|
// fallow-ignore-next-line code-duplication
|
|
287
75
|
const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
|
|
@@ -298,8 +86,6 @@ export async function commitGsapPositionFromDrag(
|
|
|
298
86
|
el.removeAttribute("data-hf-drag-initial-offset-y");
|
|
299
87
|
};
|
|
300
88
|
|
|
301
|
-
const backfillDefaults: Record<string, number> = { x: baseGsapX, y: baseGsapY };
|
|
302
|
-
const ct = usePlayerStore.getState().currentTime;
|
|
303
89
|
if (anim.arcPath?.enabled) {
|
|
304
90
|
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
|
305
91
|
const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
|
|
@@ -324,14 +110,15 @@ export async function commitGsapPositionFromDrag(
|
|
|
324
110
|
);
|
|
325
111
|
setActiveKeyframePct(null);
|
|
326
112
|
parkPlayheadOnKeyframe(anim, pct);
|
|
327
|
-
return;
|
|
113
|
+
return { status: "persisted" };
|
|
328
114
|
}
|
|
329
115
|
|
|
330
116
|
const tweenStart = resolveTweenStart(anim);
|
|
331
117
|
// Clip-wide, same as applyArcKeyframeAtPlayhead: authoring GSAP's 0.5s
|
|
332
118
|
// default here would collapse a duration-less arc's window on drag.
|
|
333
119
|
const tweenDuration = resolveEditableTweenDuration(anim, selection);
|
|
334
|
-
if (tweenStart === null || tweenDuration <= 0 || keyframes.length < 2)
|
|
120
|
+
if (tweenStart === null || tweenDuration <= 0 || keyframes.length < 2)
|
|
121
|
+
return { status: "persisted" };
|
|
335
122
|
const temporalKeyframes = buildTemporalArcKeyframes(anim, pct, { x: newX, y: newY });
|
|
336
123
|
await callbacks.commitMutation(
|
|
337
124
|
selection,
|
|
@@ -351,161 +138,11 @@ export async function commitGsapPositionFromDrag(
|
|
|
351
138
|
beforeReload: restoreOffset,
|
|
352
139
|
},
|
|
353
140
|
);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
if (anim.keyframes) {
|
|
357
|
-
const newId = await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
|
|
358
|
-
const effectiveAnim = newId ? { ...anim, id: newId } : anim;
|
|
359
|
-
const dragProps: Record<string, number> = { x: newX, y: newY };
|
|
360
|
-
|
|
361
|
-
const ts = resolveTweenStart(effectiveAnim);
|
|
362
|
-
const td = resolveEditableTweenDuration(effectiveAnim, selection);
|
|
363
|
-
const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
|
364
|
-
const hasSelectedKeyframe = usePlayerStore.getState().activeKeyframePct != null;
|
|
365
|
-
if (outsideRange && !hasSelectedKeyframe) {
|
|
366
|
-
await extendTweenAndAddKeyframe(
|
|
367
|
-
selection,
|
|
368
|
-
effectiveAnim,
|
|
369
|
-
dragProps,
|
|
370
|
-
ct,
|
|
371
|
-
ts,
|
|
372
|
-
td,
|
|
373
|
-
callbacks,
|
|
374
|
-
restoreOffset,
|
|
375
|
-
backfillDefaults,
|
|
376
|
-
);
|
|
377
|
-
} else {
|
|
378
|
-
await commitKeyframedPosition(
|
|
379
|
-
selection,
|
|
380
|
-
effectiveAnim,
|
|
381
|
-
dragProps,
|
|
382
|
-
callbacks,
|
|
383
|
-
restoreOffset,
|
|
384
|
-
backfillDefaults,
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
} else if (anim.method === "from" || anim.method === "fromTo") {
|
|
388
|
-
const ct = usePlayerStore.getState().currentTime;
|
|
389
|
-
const ts = resolveTweenStart(anim);
|
|
390
|
-
const td = resolveEditableTweenDuration(anim, selection);
|
|
391
|
-
const hasSelectedKeyframe = usePlayerStore.getState().activeKeyframePct != null;
|
|
392
|
-
const outsideRange =
|
|
393
|
-
!hasSelectedKeyframe && ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
|
394
|
-
const dragProps: Record<string, number> = { x: newX, y: newY };
|
|
395
|
-
|
|
396
|
-
if (outsideRange && ts !== null) {
|
|
397
|
-
await callbacks.commitMutation(
|
|
398
|
-
selection,
|
|
399
|
-
{ type: "split-into-property-groups", animationId: anim.id },
|
|
400
|
-
{ label: "Split from() for drag", skipReload: true },
|
|
401
|
-
);
|
|
402
|
-
|
|
403
|
-
const allAnims = callbacks.fetchAnimations ? await callbacks.fetchAnimations() : [];
|
|
404
|
-
const existingPosAnim = allAnims.find(
|
|
405
|
-
(a) => a.propertyGroup === "position" && a.targetSelector === anim.targetSelector,
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
if (existingPosAnim?.keyframes) {
|
|
409
|
-
const posTs = resolveTweenStart(existingPosAnim);
|
|
410
|
-
const posTd = resolveEditableTweenDuration(existingPosAnim, selection);
|
|
411
|
-
if (posTs !== null) {
|
|
412
|
-
await extendTweenAndAddKeyframe(
|
|
413
|
-
selection,
|
|
414
|
-
existingPosAnim,
|
|
415
|
-
{ x: newX, y: newY },
|
|
416
|
-
ct,
|
|
417
|
-
posTs,
|
|
418
|
-
posTd,
|
|
419
|
-
callbacks,
|
|
420
|
-
restoreOffset,
|
|
421
|
-
backfillDefaults,
|
|
422
|
-
);
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const newStart = Math.min(ct, ts);
|
|
428
|
-
const newEnd = Math.max(ct, ts + td);
|
|
429
|
-
const newDuration = Math.max(0.01, newEnd - newStart);
|
|
430
|
-
const dragBefore = ct < ts;
|
|
431
|
-
const origStartPct = Math.round(((ts - newStart) / newDuration) * 1000) / 10;
|
|
432
|
-
const origEndPct = Math.round(((ts + td - newStart) / newDuration) * 1000) / 10;
|
|
433
|
-
|
|
434
|
-
const keyframes: Array<{ percentage: number; properties: Record<string, number | string> }> =
|
|
435
|
-
[];
|
|
436
|
-
if (dragBefore) {
|
|
437
|
-
keyframes.push({ percentage: 0, properties: { x: newX, y: newY } });
|
|
438
|
-
if (origStartPct > 0.5 && origStartPct < 99.5) {
|
|
439
|
-
keyframes.push({ percentage: origStartPct, properties: { x: 0, y: 0 } });
|
|
440
|
-
}
|
|
441
|
-
keyframes.push({ percentage: 100, properties: { x: 0, y: 0 } });
|
|
442
|
-
} else {
|
|
443
|
-
keyframes.push({ percentage: 0, properties: { x: 0, y: 0 } });
|
|
444
|
-
if (origEndPct > 0.5 && origEndPct < 99.5) {
|
|
445
|
-
keyframes.push({ percentage: origEndPct, properties: { x: 0, y: 0 } });
|
|
446
|
-
}
|
|
447
|
-
keyframes.push({ percentage: 100, properties: { x: newX, y: newY } });
|
|
448
|
-
}
|
|
449
|
-
keyframes.sort((a, b) => a.percentage - b.percentage);
|
|
450
|
-
|
|
451
|
-
const baseKf = {
|
|
452
|
-
targetSelector: anim.targetSelector,
|
|
453
|
-
position: roundTo3(newStart),
|
|
454
|
-
duration: roundTo3(newDuration),
|
|
455
|
-
keyframes,
|
|
456
|
-
};
|
|
457
|
-
await callbacks.commitMutation(
|
|
458
|
-
selection,
|
|
459
|
-
existingPosAnim
|
|
460
|
-
? { type: "replace-with-keyframes", animationId: existingPosAnim.id, ...baseKf }
|
|
461
|
-
: { type: "add-with-keyframes", ...baseKf },
|
|
462
|
-
{
|
|
463
|
-
label: "Move layer (from extended)",
|
|
464
|
-
keyframeAction: "add",
|
|
465
|
-
softReload: true,
|
|
466
|
-
beforeReload: restoreOffset,
|
|
467
|
-
},
|
|
468
|
-
);
|
|
469
|
-
} else {
|
|
470
|
-
const coalesceKey = `gsap:convert-drag:${anim.id}`;
|
|
471
|
-
await callbacks.commitMutation(
|
|
472
|
-
selection,
|
|
473
|
-
{
|
|
474
|
-
type: "convert-to-keyframes",
|
|
475
|
-
animationId: anim.id,
|
|
476
|
-
},
|
|
477
|
-
{ label: "Convert from() for drag", skipReload: true, coalesceKey },
|
|
478
|
-
);
|
|
479
|
-
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
|
480
|
-
const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
|
|
481
|
-
if (activeKeyframePct != null) setActiveKeyframePct(null);
|
|
482
|
-
await callbacks.commitMutation(
|
|
483
|
-
selection,
|
|
484
|
-
{
|
|
485
|
-
type: "add-keyframe",
|
|
486
|
-
animationId: anim.id,
|
|
487
|
-
percentage: pct,
|
|
488
|
-
properties: dragProps,
|
|
489
|
-
...(backfillDefaults ? { backfillDefaults } : {}),
|
|
490
|
-
},
|
|
491
|
-
{
|
|
492
|
-
label: `Move layer (keyframe ${pct}%)`,
|
|
493
|
-
softReload: true,
|
|
494
|
-
beforeReload: restoreOffset,
|
|
495
|
-
coalesceKey,
|
|
496
|
-
},
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
} else {
|
|
500
|
-
await commitFlatViaKeyframes(
|
|
501
|
-
selection,
|
|
502
|
-
anim,
|
|
503
|
-
{ x: newX, y: newY },
|
|
504
|
-
callbacks,
|
|
505
|
-
restoreOffset,
|
|
506
|
-
iframe,
|
|
507
|
-
selector,
|
|
508
|
-
backfillDefaults,
|
|
509
|
-
);
|
|
141
|
+
return { status: "persisted" };
|
|
510
142
|
}
|
|
143
|
+
return commitValueAtPlayhead(selection, anim, { x: newX, y: newY }, iframe, callbacks, {
|
|
144
|
+
label: "Move layer",
|
|
145
|
+
backfill: { x: baseGsapX, y: baseGsapY },
|
|
146
|
+
beforeReload: restoreOffset,
|
|
147
|
+
});
|
|
511
148
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { editabilityForProvenance, type GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
2
2
|
|
|
3
|
-
export type GsapEditBlockReason =
|
|
3
|
+
export type GsapEditBlockReason =
|
|
4
|
+
| "no-selector"
|
|
5
|
+
| "unroll-required"
|
|
6
|
+
| "source-uneditable"
|
|
7
|
+
| "keyframes-uneditable"
|
|
8
|
+
| "mixed-files";
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* Which of the nine situations produced a block. The user-facing `reason` stays
|
|
@@ -21,7 +26,18 @@ export type GsapEditBlockDetail =
|
|
|
21
26
|
| "no-position-tween"
|
|
22
27
|
| "live-rotation-no-source-tween"
|
|
23
28
|
| "live-resize-no-source-tween"
|
|
24
|
-
| "zero-duration-tween"
|
|
29
|
+
| "zero-duration-tween"
|
|
30
|
+
| PlayheadEditRefusal;
|
|
31
|
+
|
|
32
|
+
/** Why an edit at the playhead could not be written as keyframes of the file's tween. */
|
|
33
|
+
export type PlayheadEditRefusal =
|
|
34
|
+
| "eased-keyframes"
|
|
35
|
+
| "simple-array-keyframes"
|
|
36
|
+
| "unknown-ease"
|
|
37
|
+
| "implicit-end-unknown"
|
|
38
|
+
| "not-a-tween"
|
|
39
|
+
| "no-timing"
|
|
40
|
+
| "shared-tween";
|
|
25
41
|
|
|
26
42
|
export type GsapEditOutcome =
|
|
27
43
|
| {
|
|
@@ -52,6 +68,10 @@ export const GSAP_EDIT_BLOCK_COPY: Record<GsapEditBlockReason, string> = {
|
|
|
52
68
|
"unroll-required":
|
|
53
69
|
"This motion comes from a helper or loop. Choose Unroll to edit it explicitly.",
|
|
54
70
|
"source-uneditable": "This animation is computed at runtime. Edit the animation in the Code tab.",
|
|
71
|
+
"keyframes-uneditable":
|
|
72
|
+
"Studio can't add this edit to the animation's keyframes. Edit this animation in the Code tab.",
|
|
73
|
+
"mixed-files":
|
|
74
|
+
"These layers are animated in different files. Move each file's layers separately.",
|
|
55
75
|
};
|
|
56
76
|
|
|
57
77
|
export class GsapEditBlockedError extends Error {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
2
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
|
+
import { GsapEditBlockedError } from "./gsapEditOutcome";
|
|
4
|
+
import { gsapWriteFile } from "./gsapScriptCommitTypes";
|
|
5
|
+
|
|
6
|
+
/** The first member a group's preflight refused, else one from a second file. */
|
|
7
|
+
export function firstPreflightFailure(
|
|
8
|
+
results: PromiseSettledResult<void>[],
|
|
9
|
+
updates: DomEditGroupPathOffsetCommit[],
|
|
10
|
+
savedOnElement: Map<DomEditSelection, boolean>,
|
|
11
|
+
activeCompPath: string | null | undefined,
|
|
12
|
+
): { error: unknown; selection: DomEditSelection } | null {
|
|
13
|
+
for (const [index, result] of results.entries()) {
|
|
14
|
+
if (result.status !== "rejected") continue;
|
|
15
|
+
const selection = updates[index]?.selection;
|
|
16
|
+
if (selection) return { error: result.reason, selection };
|
|
17
|
+
}
|
|
18
|
+
return secondFile(updates, savedOnElement, activeCompPath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The group's script writes go out as one batch to one file, so members from two files refuse. */
|
|
22
|
+
function secondFile(
|
|
23
|
+
updates: DomEditGroupPathOffsetCommit[],
|
|
24
|
+
savedOnElement: Map<DomEditSelection, boolean>,
|
|
25
|
+
activeCompPath: string | null | undefined,
|
|
26
|
+
): { error: unknown; selection: DomEditSelection } | null {
|
|
27
|
+
const scripted = updates.filter(({ selection }) => !savedOnElement.has(selection));
|
|
28
|
+
const fileOf = (s: DomEditSelection) => gsapWriteFile(s, activeCompPath);
|
|
29
|
+
const file = scripted[0] && fileOf(scripted[0].selection);
|
|
30
|
+
const other = scripted.find(({ selection }) => fileOf(selection) !== file);
|
|
31
|
+
return other
|
|
32
|
+
? { error: new GsapEditBlockedError("mixed-files"), selection: other.selection }
|
|
33
|
+
: null;
|
|
34
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { studioWriteHeaders } from "../utils/studioFileVersion";
|
|
2
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
2
3
|
|
|
3
4
|
export class GsapPreviewConvergenceError extends Error {}
|
|
4
5
|
export class GsapOwnershipProtocolError extends GsapPreviewConvergenceError {}
|
|
5
6
|
|
|
6
7
|
/** Verify rollback ownership support before any GSAP mutation can land. */
|
|
7
8
|
export async function requireGsapOwnershipProtocol(projectId: string): Promise<void> {
|
|
8
|
-
const response = await
|
|
9
|
+
const response = await studioApiFetch(
|
|
9
10
|
`/api/projects/${encodeURIComponent(projectId)}/gsap-mutation-capabilities`,
|
|
10
11
|
);
|
|
11
12
|
if (!response.ok) {
|
|
@@ -29,7 +30,7 @@ export async function rollbackOwnedMutation(
|
|
|
29
30
|
if (targetPath.includes("\0") || targetPath.includes("..")) {
|
|
30
31
|
throw new Error(`Unsafe path: ${targetPath}`);
|
|
31
32
|
}
|
|
32
|
-
const response = await
|
|
33
|
+
const response = await studioApiFetch(
|
|
33
34
|
`/api/projects/${encodeURIComponent(projectId)}/gsap-mutation-rollback/${encodeURIComponent(targetPath)}`,
|
|
34
35
|
{
|
|
35
36
|
method: "POST",
|
|
@@ -95,7 +96,7 @@ export async function postGsapMutation(
|
|
|
95
96
|
): Promise<GsapMutationStatus> {
|
|
96
97
|
let response: Response;
|
|
97
98
|
try {
|
|
98
|
-
response = await
|
|
99
|
+
response = await studioApiFetch(
|
|
99
100
|
`/api/projects/${encodeURIComponent(projectId)}/gsap-mutations/${encodeURIComponent(filePath)}`,
|
|
100
101
|
{
|
|
101
102
|
method: "POST",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
import { classifyTweenPropertyGroup } from "@hyperframes/core/gsap-parser";
|
|
3
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
4
|
+
import { usePlayerStore } from "../player/store/playerStore";
|
|
5
|
+
|
|
6
|
+
/** A property's `[start, end]` as GSAP parsed it from the file when the tween first rendered. */
|
|
7
|
+
type Ends = Record<string, [number, number]>;
|
|
8
|
+
|
|
9
|
+
/** A GSAP 3 tween as the preview runtime holds it, with the property tweens the parse readers walk. */
|
|
10
|
+
export function liveTween(
|
|
11
|
+
target: Element,
|
|
12
|
+
tween: { start: number; duration: number; vars: Record<string, unknown>; ends?: Ends },
|
|
13
|
+
{ from = false, parts }: { from?: boolean; parts?: unknown[] } = {},
|
|
14
|
+
) {
|
|
15
|
+
let head: Record<string, unknown> | undefined;
|
|
16
|
+
for (const [p, [s, e]] of Object.entries(tween.ends ?? {}).reverse())
|
|
17
|
+
head = from ? { p, s: e, c: s - e, _next: head } : { p, s, c: e - s, _next: head };
|
|
18
|
+
return {
|
|
19
|
+
targets: () => [target],
|
|
20
|
+
startTime: () => tween.start,
|
|
21
|
+
duration: () => tween.duration,
|
|
22
|
+
vars: tween.vars,
|
|
23
|
+
_from: from,
|
|
24
|
+
...(head && { _pt: { d: { _pt: head } } }),
|
|
25
|
+
...(parts && { timeline: { getChildren: () => parts } }),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A preview iframe whose one timeline holds `tweens`, answering `getProperty` from `live`. */
|
|
30
|
+
export function previewWith(
|
|
31
|
+
element: Element,
|
|
32
|
+
tweens: unknown[],
|
|
33
|
+
live: Record<string, number> = {},
|
|
34
|
+
): HTMLIFrameElement {
|
|
35
|
+
return {
|
|
36
|
+
contentWindow: {
|
|
37
|
+
__timelines: { main: { getChildren: () => tweens, duration: () => 10 } },
|
|
38
|
+
gsap: {
|
|
39
|
+
getProperty: (_el: Element, prop: string) => live[prop] ?? 0,
|
|
40
|
+
defaults: () => ({ ease: "power1.out" }),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
contentDocument: element.ownerDocument,
|
|
44
|
+
} as unknown as HTMLIFrameElement;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const tween = (fields: Partial<GsapAnimation>): GsapAnimation =>
|
|
48
|
+
({ targetSelector: "#box", propertyGroup: "position", ...fields }) as GsapAnimation;
|
|
49
|
+
|
|
50
|
+
/** A `to` tween on `#el` from 0, grouped as the parser would; duration 0 is an immediate-render hold. */
|
|
51
|
+
export function elTween(
|
|
52
|
+
id: string,
|
|
53
|
+
properties: Record<string, number>,
|
|
54
|
+
duration: number,
|
|
55
|
+
ease?: string,
|
|
56
|
+
): GsapAnimation {
|
|
57
|
+
return {
|
|
58
|
+
id,
|
|
59
|
+
targetSelector: "#el",
|
|
60
|
+
propertyGroup: classifyTweenPropertyGroup(properties),
|
|
61
|
+
method: "to",
|
|
62
|
+
properties,
|
|
63
|
+
position: 0,
|
|
64
|
+
resolvedStart: 0,
|
|
65
|
+
duration,
|
|
66
|
+
...(duration === 0 ? { extras: { immediateRender: "__raw:true" } } : ease && { ease }),
|
|
67
|
+
} as unknown as GsapAnimation;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Per-test cleanup for the intercept sweeps: mocks, the playhead store and the document. */
|
|
71
|
+
export function resetGsapEditState(): void {
|
|
72
|
+
vi.restoreAllMocks();
|
|
73
|
+
usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null });
|
|
74
|
+
document.body.innerHTML = "";
|
|
75
|
+
}
|