@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
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
2
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
|
+
import { usePlayerStore } from "../player/store/playerStore";
|
|
4
|
+
import { resolveTweenDuration, resolveTweenStart } from "../utils/globalTimeCompiler";
|
|
5
|
+
import { roundTo3 } from "../utils/rounding";
|
|
6
|
+
import {
|
|
7
|
+
materializeIfDynamic,
|
|
8
|
+
parkPlayheadOnKeyframe,
|
|
9
|
+
type GsapDragCommitCallbacks,
|
|
10
|
+
} from "./gsapDragCommit";
|
|
11
|
+
import type { GsapEditOutcome, PlayheadEditRefusal } from "./gsapEditOutcome";
|
|
12
|
+
import {
|
|
13
|
+
findParsedTween,
|
|
14
|
+
parsedImplicitEndValue,
|
|
15
|
+
parsedTweenEase,
|
|
16
|
+
withExactStepTimes,
|
|
17
|
+
withLiveTiming,
|
|
18
|
+
} from "./gsapParsedTween";
|
|
19
|
+
import { KEYFRAME_PCT_MATCH } from "./gsapShared";
|
|
20
|
+
import { tweenReach } from "./gsapTweenReach";
|
|
21
|
+
|
|
22
|
+
type Props = Record<string, number | string>;
|
|
23
|
+
interface Keyframe {
|
|
24
|
+
percentage: number;
|
|
25
|
+
properties: Props;
|
|
26
|
+
ease?: string;
|
|
27
|
+
auto?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A tween's un-animated value of `prop` at its start or end, from GSAP's own parse; null when unknown. */
|
|
31
|
+
export type ImplicitEndValue = (prop: string, end: "start" | "end") => number | null;
|
|
32
|
+
|
|
33
|
+
export interface PlayheadEdit {
|
|
34
|
+
anim: GsapAnimation;
|
|
35
|
+
/** The playhead's composition time, or a keyframe the user selected in the lane. */
|
|
36
|
+
at: { time: number } | { percentage: number };
|
|
37
|
+
/** Every channel here must already be animated by `anim`. */
|
|
38
|
+
values: Record<string, number>;
|
|
39
|
+
implicitEndValue: ImplicitEndValue;
|
|
40
|
+
/** The flat tween's ease as GSAP resolved it, for a tween that authors none. */
|
|
41
|
+
parsedEase?: string | null;
|
|
42
|
+
/** A channel of the edit the tween does not animate yet, held at this value on its keyframes. */
|
|
43
|
+
backfill?: Record<string, number>;
|
|
44
|
+
/** Hold the backfill from the tween's start too, where GSAP would otherwise begin at the old value. */
|
|
45
|
+
holdFromStart?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type PlayheadEditPlan =
|
|
49
|
+
| {
|
|
50
|
+
ok: true;
|
|
51
|
+
mutation: {
|
|
52
|
+
type: "replace-with-keyframes";
|
|
53
|
+
animationId: string;
|
|
54
|
+
targetSelector: string;
|
|
55
|
+
position: number;
|
|
56
|
+
duration: number;
|
|
57
|
+
keyframes: Keyframe[];
|
|
58
|
+
easeEach?: string;
|
|
59
|
+
};
|
|
60
|
+
/** A keyframe was added, not only changed: keyframe usage counts it as an add. */
|
|
61
|
+
added: boolean;
|
|
62
|
+
}
|
|
63
|
+
| { ok: false; reason: PlayheadEditRefusal };
|
|
64
|
+
|
|
65
|
+
// GSAP 3.14 defaults: a percentage keyframe eases its segment power1.inOut; an array step is linear.
|
|
66
|
+
const PERCENTAGE_SEGMENT_EASE = "power1.inOut";
|
|
67
|
+
const ARRAY_STEP_EASE = "none";
|
|
68
|
+
const roundPct = (pct: number) => Math.round(pct * 1000) / 1000;
|
|
69
|
+
const isLinear = (ease: string | undefined) => !ease || ease === "none" || ease === "linear";
|
|
70
|
+
|
|
71
|
+
interface Normalized {
|
|
72
|
+
keyframes: Keyframe[];
|
|
73
|
+
easeEach?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function refuse(reason: PlayheadEditRefusal): { ok: false; reason: PlayheadEditRefusal } {
|
|
77
|
+
return { ok: false, reason };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// fallow-ignore-next-line complexity
|
|
81
|
+
function normalize(edit: PlayheadEdit): Normalized | { reason: PlayheadEditRefusal } {
|
|
82
|
+
const { anim } = edit;
|
|
83
|
+
const data = anim.keyframes;
|
|
84
|
+
if (data) {
|
|
85
|
+
if (!isLinear(anim.ease) || !isLinear(data.ease)) return { reason: "eased-keyframes" };
|
|
86
|
+
if (data.format === "simple-array") return { reason: "simple-array-keyframes" };
|
|
87
|
+
const arrayStep = data.format === "object-array";
|
|
88
|
+
return {
|
|
89
|
+
keyframes: data.keyframes.map((kf) => ({
|
|
90
|
+
...kf,
|
|
91
|
+
properties: { ...kf.properties },
|
|
92
|
+
...(arrayStep ? { ease: kf.ease ?? ARRAY_STEP_EASE } : {}),
|
|
93
|
+
})),
|
|
94
|
+
...(data.easeEach && !arrayStep ? { easeEach: data.easeEach } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// The flat tween's ease becomes easeEach: a top-level ease would leave each segment power1.inOut.
|
|
98
|
+
const easeEach = anim.ease ?? edit.parsedEase;
|
|
99
|
+
if (!easeEach) return { reason: "unknown-ease" };
|
|
100
|
+
const to = { percentage: 100, properties: { ...anim.properties } };
|
|
101
|
+
if (anim.method === "to") return { keyframes: [to], easeEach };
|
|
102
|
+
if (anim.method === "fromTo") {
|
|
103
|
+
return { keyframes: [{ percentage: 0, properties: { ...anim.fromProperties } }, to], easeEach };
|
|
104
|
+
}
|
|
105
|
+
if (anim.method === "from") {
|
|
106
|
+
const end: Props = {};
|
|
107
|
+
for (const prop of Object.keys(anim.properties)) {
|
|
108
|
+
const value = edit.implicitEndValue(prop, "end");
|
|
109
|
+
if (value == null) return { reason: "implicit-end-unknown" };
|
|
110
|
+
end[prop] = value;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
keyframes: [
|
|
114
|
+
{ percentage: 0, properties: { ...anim.properties } },
|
|
115
|
+
{ percentage: 100, properties: end },
|
|
116
|
+
],
|
|
117
|
+
easeEach,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { reason: "not-a-tween" };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function valueAt(keyframes: Keyframe[], prop: string, side: "first" | "last") {
|
|
124
|
+
const carrying = keyframes.filter((kf) => kf.properties[prop] != null);
|
|
125
|
+
const kf = side === "first" ? carrying[0] : carrying.at(-1);
|
|
126
|
+
return kf ? { percentage: kf.percentage, value: kf.properties[prop]! } : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function upsert(keyframes: Keyframe[], percentage: number, properties: Props, ease?: string) {
|
|
130
|
+
const hit = keyframes.find((kf) => Math.abs(kf.percentage - percentage) <= KEYFRAME_PCT_MATCH);
|
|
131
|
+
if (hit) {
|
|
132
|
+
Object.assign(hit.properties, properties);
|
|
133
|
+
return hit;
|
|
134
|
+
}
|
|
135
|
+
const added = { percentage, properties, ...(ease ? { ease } : {}) };
|
|
136
|
+
keyframes.push(added);
|
|
137
|
+
return added;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Every channel the tween animates, held at its value at the tween's authored start or end. */
|
|
141
|
+
function heldEnds(
|
|
142
|
+
norm: Normalized,
|
|
143
|
+
end: "start" | "end",
|
|
144
|
+
implicit: ImplicitEndValue,
|
|
145
|
+
backfilled: Record<string, number>,
|
|
146
|
+
) {
|
|
147
|
+
const props = new Set(norm.keyframes.flatMap((kf) => Object.keys(kf.properties)));
|
|
148
|
+
const held: Props = {};
|
|
149
|
+
for (const prop of props) {
|
|
150
|
+
const known = valueAt(norm.keyframes, prop, end === "start" ? "first" : "last");
|
|
151
|
+
// A channel holds its last keyframe after it; only a start can be implicit.
|
|
152
|
+
const value =
|
|
153
|
+
known && (end === "end" || known.percentage <= 0)
|
|
154
|
+
? known.value
|
|
155
|
+
: (backfilled[prop] ?? implicit(prop, "start"));
|
|
156
|
+
if (value == null) return null;
|
|
157
|
+
held[prop] = value;
|
|
158
|
+
}
|
|
159
|
+
return held;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** On a keyframe, change it; between two, add one at the playhead; outside the tween, add one there
|
|
163
|
+
* and keep the authored ends. Values come from the file's tween or GSAP's parse, never the DOM. */
|
|
164
|
+
// fallow-ignore-next-line complexity
|
|
165
|
+
export function planValueAtPlayhead(edit: PlayheadEdit): PlayheadEditPlan {
|
|
166
|
+
const { anim, values } = edit;
|
|
167
|
+
const start = resolveTweenStart(anim);
|
|
168
|
+
const duration = resolveTweenDuration(anim);
|
|
169
|
+
if (start == null || !(duration > 0)) return refuse("no-timing");
|
|
170
|
+
const norm = normalize(edit);
|
|
171
|
+
if ("reason" in norm) return refuse(norm.reason);
|
|
172
|
+
const keyframes = norm.keyframes;
|
|
173
|
+
const authored = keyframes.length;
|
|
174
|
+
// GSAP runs `scale` beside scaleX/scaleY and the longhands win, so a per-axis edit splits it.
|
|
175
|
+
if ("scaleX" in values || "scaleY" in values) {
|
|
176
|
+
for (const kf of keyframes) {
|
|
177
|
+
const { scale, ...rest } = kf.properties;
|
|
178
|
+
if (scale != null) kf.properties = { scaleX: scale, scaleY: scale, ...rest };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const backfilled: Record<string, number> = {};
|
|
182
|
+
for (const [prop, value] of Object.entries(edit.backfill ?? {})) {
|
|
183
|
+
if (!(prop in values) || keyframes.some((kf) => kf.properties[prop] != null)) continue;
|
|
184
|
+
if (edit.holdFromStart && !keyframes.some((kf) => kf.percentage <= 0))
|
|
185
|
+
keyframes.unshift({ percentage: 0, properties: {} });
|
|
186
|
+
for (const kf of keyframes) kf.properties[prop] = value;
|
|
187
|
+
backfilled[prop] = value;
|
|
188
|
+
}
|
|
189
|
+
if (Object.keys(values).some((prop) => !keyframes.some((kf) => kf.properties[prop] != null)))
|
|
190
|
+
return refuse("implicit-end-unknown");
|
|
191
|
+
let position = start;
|
|
192
|
+
let span = duration;
|
|
193
|
+
|
|
194
|
+
const pct =
|
|
195
|
+
"percentage" in edit.at
|
|
196
|
+
? edit.at.percentage
|
|
197
|
+
: roundPct(((edit.at.time - start) / duration) * 100);
|
|
198
|
+
if (pct >= -KEYFRAME_PCT_MATCH && pct <= 100 + KEYFRAME_PCT_MATCH) {
|
|
199
|
+
const at = Math.min(100, Math.max(0, pct));
|
|
200
|
+
const next = [...keyframes]
|
|
201
|
+
.sort((a, b) => a.percentage - b.percentage)
|
|
202
|
+
.find((kf) => kf.percentage > at + KEYFRAME_PCT_MATCH);
|
|
203
|
+
// The new keyframe splits next's segment, so it takes next's ease.
|
|
204
|
+
upsert(keyframes, at, { ...values }, next?.ease);
|
|
205
|
+
} else {
|
|
206
|
+
const time = (edit.at as { time: number }).time;
|
|
207
|
+
const before = time < start;
|
|
208
|
+
const held = heldEnds(norm, before ? "start" : "end", edit.implicitEndValue, backfilled);
|
|
209
|
+
if (!held) return refuse("implicit-end-unknown");
|
|
210
|
+
position = before ? time : start;
|
|
211
|
+
span = before ? start + duration - time : time - start;
|
|
212
|
+
for (const kf of keyframes) {
|
|
213
|
+
kf.percentage = roundPct(
|
|
214
|
+
((start + (kf.percentage / 100) * duration - position) / span) * 100,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const oldEdge = roundPct((((before ? start : start + duration) - position) / span) * 100);
|
|
218
|
+
// The span the tween gains is new and linear; the authored segments keep their eases.
|
|
219
|
+
const linear = isLinear(norm.easeEach ?? PERCENTAGE_SEGMENT_EASE) ? undefined : "none";
|
|
220
|
+
const edge = upsert(keyframes, oldEdge, held, linear);
|
|
221
|
+
if (before && linear) edge.ease ??= linear;
|
|
222
|
+
upsert(keyframes, before ? 0 : 100, { ...held, ...values }, before ? undefined : linear);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
keyframes.sort((a, b) => a.percentage - b.percentage);
|
|
226
|
+
return {
|
|
227
|
+
ok: true,
|
|
228
|
+
mutation: {
|
|
229
|
+
type: "replace-with-keyframes",
|
|
230
|
+
animationId: anim.id,
|
|
231
|
+
targetSelector: anim.targetSelector,
|
|
232
|
+
position: roundTo3(position),
|
|
233
|
+
duration: roundTo3(span),
|
|
234
|
+
keyframes,
|
|
235
|
+
...(norm.easeEach ? { easeEach: norm.easeEach } : {}),
|
|
236
|
+
},
|
|
237
|
+
added: keyframes.length > authored,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** commitValueAtPlayhead's plan, writing nothing, so a group can plan every member first. */
|
|
242
|
+
export function planValueEdit(
|
|
243
|
+
selection: DomEditSelection,
|
|
244
|
+
anim: GsapAnimation,
|
|
245
|
+
values: Record<string, number>,
|
|
246
|
+
iframe: HTMLIFrameElement | null,
|
|
247
|
+
{ backfill, holdFromStart }: Pick<PlayheadEdit, "backfill" | "holdFromStart"> = {},
|
|
248
|
+
): PlayheadEditPlan {
|
|
249
|
+
// One keyframe of a tween its siblings share would move them all.
|
|
250
|
+
if (tweenReach(anim, selection.element) === "shared") return refuse("shared-tween");
|
|
251
|
+
const { activeKeyframePct, currentTime } = usePlayerStore.getState();
|
|
252
|
+
const tween = findParsedTween(iframe, selection.element, anim);
|
|
253
|
+
const timed = withExactStepTimes(anim, tween);
|
|
254
|
+
return planValueAtPlayhead({
|
|
255
|
+
anim: withLiveTiming(timed, tween),
|
|
256
|
+
at: activeKeyframePct != null ? { percentage: activeKeyframePct } : { time: currentTime },
|
|
257
|
+
values,
|
|
258
|
+
backfill,
|
|
259
|
+
holdFromStart,
|
|
260
|
+
implicitEndValue: parsedImplicitEndValue(tween),
|
|
261
|
+
parsedEase: parsedTweenEase(iframe, tween),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Writes `values` into `anim` at the playhead (or the keyframe selected in the lane). */
|
|
266
|
+
export async function commitValueAtPlayhead(
|
|
267
|
+
selection: DomEditSelection,
|
|
268
|
+
anim: GsapAnimation,
|
|
269
|
+
values: Record<string, number>,
|
|
270
|
+
iframe: HTMLIFrameElement | null,
|
|
271
|
+
callbacks: GsapDragCommitCallbacks,
|
|
272
|
+
options: Pick<PlayheadEdit, "backfill" | "holdFromStart"> & {
|
|
273
|
+
label: string;
|
|
274
|
+
beforeReload?: () => void;
|
|
275
|
+
},
|
|
276
|
+
): Promise<GsapEditOutcome> {
|
|
277
|
+
await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
|
|
278
|
+
const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
|
|
279
|
+
const plan = planValueEdit(selection, anim, values, iframe, options);
|
|
280
|
+
if (!plan.ok) return { status: "blocked", reason: "keyframes-uneditable", detail: plan.reason };
|
|
281
|
+
await callbacks.commitMutation(selection, plan.mutation, {
|
|
282
|
+
label: options.label,
|
|
283
|
+
softReload: true,
|
|
284
|
+
beforeReload: options.beforeReload,
|
|
285
|
+
...(plan.added && { keyframeAction: "add" as const }),
|
|
286
|
+
});
|
|
287
|
+
if (activeKeyframePct != null) {
|
|
288
|
+
setActiveKeyframePct(null);
|
|
289
|
+
parkPlayheadOnKeyframe(anim, activeKeyframePct);
|
|
290
|
+
}
|
|
291
|
+
return { status: "persisted" };
|
|
292
|
+
}
|
|
@@ -26,6 +26,29 @@ function recordingCallbacks(): {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** Rotates a 2s `#box` tween made of `fields` to 50deg at its end; returns what was committed. */
|
|
30
|
+
async function rotateTo50AtEnd(fields: Record<string, unknown>) {
|
|
31
|
+
const anim = {
|
|
32
|
+
id: "#box-rotate",
|
|
33
|
+
targetSelector: "#box",
|
|
34
|
+
method: "to",
|
|
35
|
+
resolvedStart: 0,
|
|
36
|
+
duration: 2,
|
|
37
|
+
...fields,
|
|
38
|
+
} as unknown as GsapAnimation;
|
|
39
|
+
const { mutations, callbacks } = recordingCallbacks();
|
|
40
|
+
await commitWholePropertyOffset(
|
|
41
|
+
selection(),
|
|
42
|
+
anim,
|
|
43
|
+
{ rotation: 50 },
|
|
44
|
+
100,
|
|
45
|
+
null,
|
|
46
|
+
callbacks,
|
|
47
|
+
"Rotate",
|
|
48
|
+
);
|
|
49
|
+
return mutations;
|
|
50
|
+
}
|
|
51
|
+
|
|
29
52
|
describe("commitWholePropertyOffset", () => {
|
|
30
53
|
it("shifts every keyframe of a keyframed tween by the delta at the nearest keyframe", async () => {
|
|
31
54
|
const anim = {
|
|
@@ -159,4 +182,24 @@ describe("commitWholePropertyOffset", () => {
|
|
|
159
182
|
{ type: "update-properties", animationId: "#box-hold", properties: { width: 300 } },
|
|
160
183
|
]);
|
|
161
184
|
});
|
|
185
|
+
|
|
186
|
+
it("keeps a linear flat tween linear: its ease becomes the keyframes' easeEach", async () => {
|
|
187
|
+
// A tween-level ease leaves each percentage segment power1.inOut, so the box drifts mid-segment.
|
|
188
|
+
const mutations = await rotateTo50AtEnd({ ease: "none", properties: { rotation: 40 } });
|
|
189
|
+
expect(mutations[0]).toMatchObject({ type: "replace-with-keyframes", easeEach: "none" });
|
|
190
|
+
expect(mutations[0]).not.toHaveProperty("ease", "none");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("keeps a keyframed tween's easeEach", async () => {
|
|
194
|
+
const mutations = await rotateTo50AtEnd({
|
|
195
|
+
keyframes: {
|
|
196
|
+
easeEach: "none",
|
|
197
|
+
keyframes: [
|
|
198
|
+
{ percentage: 0, properties: { rotation: 10 } },
|
|
199
|
+
{ percentage: 100, properties: { rotation: 40 } },
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
expect(mutations[0]).toMatchObject({ easeEach: "none" });
|
|
204
|
+
});
|
|
162
205
|
});
|
|
@@ -6,7 +6,7 @@ import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
|
6
6
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
7
7
|
import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeCompiler";
|
|
8
8
|
import { roundTo3 } from "../utils/rounding";
|
|
9
|
-
import { PROPERTY_DEFAULTS } from "./gsapShared";
|
|
9
|
+
import { keyframeEases, PROPERTY_DEFAULTS } from "./gsapShared";
|
|
10
10
|
import { synthesizeFlatTweenKeyframes } from "./gsapTweenSynth";
|
|
11
11
|
import { materializeIfDynamic, type GsapDragCommitCallbacks } from "./gsapDragCommit";
|
|
12
12
|
|
|
@@ -38,7 +38,6 @@ export async function commitWholePropertyOffset(
|
|
|
38
38
|
|
|
39
39
|
const ts = resolveTweenStart(effectiveAnim);
|
|
40
40
|
const td = resolveTweenDuration(effectiveAnim);
|
|
41
|
-
const ease = effectiveAnim.keyframes?.easeEach ?? effectiveAnim.ease;
|
|
42
41
|
const keys = Object.keys(newValues);
|
|
43
42
|
const at = (props: Record<string, number | string>, key: string) =>
|
|
44
43
|
typeof props[key] === "number" ? (props[key] as number) : (PROPERTY_DEFAULTS[key] ?? 0);
|
|
@@ -79,7 +78,7 @@ export async function commitWholePropertyOffset(
|
|
|
79
78
|
position: roundTo3(ts ?? 0),
|
|
80
79
|
duration: roundTo3(td || 1),
|
|
81
80
|
keyframes: shifted,
|
|
82
|
-
|
|
81
|
+
...keyframeEases(effectiveAnim),
|
|
83
82
|
},
|
|
84
83
|
{ label, softReload: true },
|
|
85
84
|
);
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
synthesizeFlatTweenKeyframes,
|
|
15
15
|
type MergeableKeyframe,
|
|
16
16
|
} from "./gsapTweenSynth";
|
|
17
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
17
18
|
|
|
18
19
|
export { resolveSelectorElementIds };
|
|
19
20
|
|
|
@@ -79,7 +80,7 @@ async function requestParsedAnimations(
|
|
|
79
80
|
): Promise<ParsedGsapAnimations | null> {
|
|
80
81
|
if (!isPreviewBooted(projectId) && !(await whenPreviewBooted(projectId))) return null;
|
|
81
82
|
try {
|
|
82
|
-
const res = await
|
|
83
|
+
const res = await studioApiFetch(
|
|
83
84
|
`/api/projects/${encodeURIComponent(projectId)}/gsap-animations/${encodeURIComponent(sourceFile)}`,
|
|
84
85
|
// Always revalidate; an unchanged file answers 304. No per-call timestamp
|
|
85
86
|
// (a deterministic-render no-no).
|
|
@@ -13,57 +13,30 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
13
13
|
import { parseGsapScript } from "@hyperframes/core/gsap-parser";
|
|
14
14
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
15
15
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
16
|
-
import {
|
|
16
|
+
import { mountGroupSiblings, stableSelectionFor } from "./domSelectionTestHarness";
|
|
17
17
|
import { resolveSelectorElementIds } from "./gsapShared";
|
|
18
18
|
import { ensureElementAddressable } from "./gsapScriptCommitHelpers";
|
|
19
19
|
import {
|
|
20
20
|
commitStaticGsapPosition,
|
|
21
21
|
commitStaticGsapRotation,
|
|
22
22
|
commitStaticGsapSize,
|
|
23
|
-
commitKeyframedSizeFromResize,
|
|
24
23
|
commitWholePathOffset,
|
|
25
24
|
findExistingPositionWrite,
|
|
26
25
|
} from "./gsapDragCommit";
|
|
27
26
|
import { promoteSetToKeyframes } from "./useEnableKeyframes";
|
|
27
|
+
import { commitValueAtPlayhead } from "./gsapValueAtPlayhead";
|
|
28
28
|
|
|
29
29
|
afterEach(() => {
|
|
30
30
|
document.body.innerHTML = "";
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
function selectionFor(el: HTMLElement): DomEditSelection {
|
|
34
|
-
const selector = buildStableSelector(el);
|
|
35
|
-
return {
|
|
36
|
-
element: el,
|
|
37
|
-
id: el.id || undefined,
|
|
38
|
-
hfId: el.getAttribute("data-hf-id") || undefined,
|
|
39
|
-
selector,
|
|
40
|
-
selectorIndex: getSelectorIndex(document, el, selector, "index.html", null),
|
|
41
|
-
sourceFile: "index.html",
|
|
42
|
-
dataAttributes: { start: "0", duration: "2" },
|
|
43
|
-
} as unknown as DomEditSelection;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Five class-only siblings, each with an id so attribution is nameable. */
|
|
47
|
-
function mountGroupSiblings(): HTMLElement[] {
|
|
48
|
-
document.body.innerHTML = `
|
|
49
|
-
<div id="scene" class="clip" data-start="0" data-duration="2">
|
|
50
|
-
<div class="group" id="group-0"></div>
|
|
51
|
-
<div class="group" id="group-1"></div>
|
|
52
|
-
<div class="group" id="group-2"></div>
|
|
53
|
-
<div class="group" id="group-3"></div>
|
|
54
|
-
<div class="group" id="group-4"></div>
|
|
55
|
-
</div>
|
|
56
|
-
`;
|
|
57
|
-
return Array.from(document.querySelectorAll<HTMLElement>(".group"));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
33
|
/**
|
|
61
34
|
* The selection production hands these writers: the element HAS an id in the
|
|
62
35
|
* DOM (so resolveSelectorElementIds can name it) but the SELECTION carries none,
|
|
63
36
|
* which is the id-less shape buildStableSelector answers with a bare class.
|
|
64
37
|
*/
|
|
65
38
|
function classOnlySelection(el: HTMLElement): DomEditSelection {
|
|
66
|
-
return { ...
|
|
39
|
+
return { ...stableSelectionFor(el), id: undefined, selector: ".group" } as DomEditSelection;
|
|
67
40
|
}
|
|
68
41
|
|
|
69
42
|
/** The elements a written targetSelector actually attributes the tween to. */
|
|
@@ -94,7 +67,7 @@ describe("ensureElementAddressable — add-animation button", () => {
|
|
|
94
67
|
</div>
|
|
95
68
|
`;
|
|
96
69
|
const el = document.querySelectorAll<HTMLElement>(".group")[1]!;
|
|
97
|
-
const selection =
|
|
70
|
+
const selection = stableSelectionFor(el);
|
|
98
71
|
expect(selection.selector).toBe(".group");
|
|
99
72
|
|
|
100
73
|
const { selector, autoId } = ensureElementAddressable(selection);
|
|
@@ -109,20 +82,20 @@ describe("ensureElementAddressable — add-animation button", () => {
|
|
|
109
82
|
document.body.innerHTML = `<div id="box" class="card"></div>`;
|
|
110
83
|
const el = document.querySelector<HTMLElement>("#box")!;
|
|
111
84
|
|
|
112
|
-
expect(ensureElementAddressable(
|
|
85
|
+
expect(ensureElementAddressable(stableSelectionFor(el)).selector).toBe("#box");
|
|
113
86
|
});
|
|
114
87
|
|
|
115
88
|
it("keeps an already-unique class selector as authored", () => {
|
|
116
89
|
document.body.innerHTML = `<div id="scene"><div class="header"></div></div>`;
|
|
117
90
|
const el = document.querySelector<HTMLElement>(".header")!;
|
|
118
91
|
|
|
119
|
-
expect(ensureElementAddressable(
|
|
92
|
+
expect(ensureElementAddressable(stableSelectionFor(el)).selector).toBe(".header");
|
|
120
93
|
});
|
|
121
94
|
|
|
122
95
|
it("still mints an id when there is no live element to disambiguate against", () => {
|
|
123
96
|
document.body.innerHTML = `<div id="scene"><div></div></div>`;
|
|
124
97
|
const el = document.querySelector<HTMLElement>("#scene > div")!;
|
|
125
|
-
const selection = { ...
|
|
98
|
+
const selection = { ...stableSelectionFor(el), selector: undefined } as DomEditSelection;
|
|
126
99
|
|
|
127
100
|
const { selector, autoId } = ensureElementAddressable(selection);
|
|
128
101
|
|
|
@@ -131,25 +104,28 @@ describe("ensureElementAddressable — add-animation button", () => {
|
|
|
131
104
|
});
|
|
132
105
|
});
|
|
133
106
|
|
|
107
|
+
/** Moves the middle of five class-only siblings with no position yet; returns the selector written. */
|
|
108
|
+
async function positionMiddleSibling(): Promise<string> {
|
|
109
|
+
const groups = mountGroupSiblings(5, true);
|
|
110
|
+
const { mutations, callbacks } = recorder();
|
|
111
|
+
await commitStaticGsapPosition(
|
|
112
|
+
classOnlySelection(groups[2]!),
|
|
113
|
+
{ x: 10, y: 10 },
|
|
114
|
+
{ x: 0, y: 0 },
|
|
115
|
+
".group",
|
|
116
|
+
null,
|
|
117
|
+
callbacks,
|
|
118
|
+
);
|
|
119
|
+
return writtenTargets(mutations)[0]!;
|
|
120
|
+
}
|
|
121
|
+
|
|
134
122
|
describe("gsapDragCommit — new-tween targets", () => {
|
|
135
123
|
it("commitStaticGsapPosition authors the new set against one element", async () => {
|
|
136
|
-
|
|
137
|
-
const { mutations, callbacks } = recorder();
|
|
138
|
-
|
|
139
|
-
await commitStaticGsapPosition(
|
|
140
|
-
classOnlySelection(groups[2]!),
|
|
141
|
-
{ x: 10, y: 10 },
|
|
142
|
-
{ x: 0, y: 0 },
|
|
143
|
-
".group",
|
|
144
|
-
null,
|
|
145
|
-
callbacks,
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
expect(attributedTo(writtenTargets(mutations)[0]!)).toEqual(["group-2"]);
|
|
124
|
+
expect(attributedTo(await positionMiddleSibling())).toEqual(["group-2"]);
|
|
149
125
|
});
|
|
150
126
|
|
|
151
127
|
it("commitStaticGsapRotation authors the new set against one element", async () => {
|
|
152
|
-
const groups = mountGroupSiblings();
|
|
128
|
+
const groups = mountGroupSiblings(5, true);
|
|
153
129
|
const { mutations, callbacks } = recorder();
|
|
154
130
|
|
|
155
131
|
await commitStaticGsapRotation(classOnlySelection(groups[1]!), 42, ".group", null, callbacks);
|
|
@@ -158,7 +134,7 @@ describe("gsapDragCommit — new-tween targets", () => {
|
|
|
158
134
|
});
|
|
159
135
|
|
|
160
136
|
it("commitStaticGsapSize authors the new set against one element", async () => {
|
|
161
|
-
const groups = mountGroupSiblings();
|
|
137
|
+
const groups = mountGroupSiblings(5, true);
|
|
162
138
|
const { mutations, callbacks } = recorder();
|
|
163
139
|
|
|
164
140
|
await commitStaticGsapSize(
|
|
@@ -172,34 +148,34 @@ describe("gsapDragCommit — new-tween targets", () => {
|
|
|
172
148
|
expect(attributedTo(writtenTargets(mutations)[0]!)).toEqual(["group-4"]);
|
|
173
149
|
});
|
|
174
150
|
|
|
175
|
-
it("
|
|
176
|
-
const groups = mountGroupSiblings();
|
|
151
|
+
it("a keyframe edit refuses a tween its siblings share instead of moving them all", async () => {
|
|
152
|
+
const groups = mountGroupSiblings(5, true);
|
|
177
153
|
const { mutations, callbacks } = recorder();
|
|
178
|
-
const
|
|
154
|
+
const sharedTween = {
|
|
179
155
|
id: "t1",
|
|
180
156
|
targetSelector: ".group",
|
|
181
157
|
method: "to",
|
|
182
|
-
properties: {},
|
|
158
|
+
properties: { width: 120 },
|
|
183
159
|
resolvedStart: 0,
|
|
184
160
|
duration: 2,
|
|
185
|
-
|
|
161
|
+
ease: "none",
|
|
186
162
|
} as unknown as GsapAnimation;
|
|
187
163
|
|
|
188
|
-
const
|
|
164
|
+
const outcome = await commitValueAtPlayhead(
|
|
189
165
|
classOnlySelection(groups[3]!),
|
|
190
|
-
|
|
191
|
-
|
|
166
|
+
sharedTween,
|
|
167
|
+
{ width: 80 },
|
|
192
168
|
null,
|
|
193
|
-
animatedTween,
|
|
194
169
|
callbacks,
|
|
170
|
+
{ label: "Resize" },
|
|
195
171
|
);
|
|
196
172
|
|
|
197
|
-
expect(
|
|
198
|
-
expect(
|
|
173
|
+
expect(outcome).toMatchObject({ status: "blocked", detail: "shared-tween" });
|
|
174
|
+
expect(mutations).toEqual([]);
|
|
199
175
|
});
|
|
200
176
|
|
|
201
177
|
it("commitStaticGsapPosition replaces a corrupt keyframed hold against one element", async () => {
|
|
202
|
-
const groups = mountGroupSiblings();
|
|
178
|
+
const groups = mountGroupSiblings(5, true);
|
|
203
179
|
const { mutations, callbacks } = recorder();
|
|
204
180
|
const corruptHold = {
|
|
205
181
|
id: "hold-1",
|
|
@@ -225,7 +201,7 @@ describe("gsapDragCommit — new-tween targets", () => {
|
|
|
225
201
|
|
|
226
202
|
describe("gsapDragCommit — retargeting an existing tween is left alone", () => {
|
|
227
203
|
it("commitWholePathOffset keeps the tween's own group target", async () => {
|
|
228
|
-
const groups = mountGroupSiblings();
|
|
204
|
+
const groups = mountGroupSiblings(5, true);
|
|
229
205
|
const { mutations, callbacks } = recorder();
|
|
230
206
|
const groupTween = {
|
|
231
207
|
id: "t-group",
|
|
@@ -261,7 +237,7 @@ describe("gsapDragCommit — retargeting an existing tween is left alone", () =>
|
|
|
261
237
|
*/
|
|
262
238
|
describe("a re-nudge updates its own previous write instead of stacking a second one", () => {
|
|
263
239
|
it("finds the write the first nudge authored", async () => {
|
|
264
|
-
const groups = mountGroupSiblings();
|
|
240
|
+
const groups = mountGroupSiblings(5, true);
|
|
265
241
|
const selection = classOnlySelection(groups[2]!);
|
|
266
242
|
const first = recorder();
|
|
267
243
|
|
|
@@ -309,7 +285,7 @@ gsap.set(${JSON.stringify(written)}, { x: 10, y: 10 });
|
|
|
309
285
|
*/
|
|
310
286
|
describe("useEnableKeyframes — rewriting an existing tween keeps its group target", () => {
|
|
311
287
|
it("promoteSetToKeyframes leaves a group-authored set aimed at the group", async () => {
|
|
312
|
-
const groups = mountGroupSiblings();
|
|
288
|
+
const groups = mountGroupSiblings(5, true);
|
|
313
289
|
const mutations: Array<Record<string, unknown>> = [];
|
|
314
290
|
const setAnim = {
|
|
315
291
|
id: "set-group",
|
|
@@ -339,18 +315,7 @@ describe("useEnableKeyframes — rewriting an existing tween keeps its group tar
|
|
|
339
315
|
|
|
340
316
|
describe("the written selector survives the real writer and parser", () => {
|
|
341
317
|
it("re-parses to a tween attributed to the one element it targeted", async () => {
|
|
342
|
-
const
|
|
343
|
-
const { mutations, callbacks } = recorder();
|
|
344
|
-
|
|
345
|
-
await commitStaticGsapPosition(
|
|
346
|
-
classOnlySelection(groups[2]!),
|
|
347
|
-
{ x: 10, y: 10 },
|
|
348
|
-
{ x: 0, y: 0 },
|
|
349
|
-
".group",
|
|
350
|
-
null,
|
|
351
|
-
callbacks,
|
|
352
|
-
);
|
|
353
|
-
const written = writtenTargets(mutations)[0]!;
|
|
318
|
+
const written = await positionMiddleSibling();
|
|
354
319
|
|
|
355
320
|
const script = `
|
|
356
321
|
const tl = gsap.timeline({ paused: true });
|
|
@@ -27,7 +27,11 @@ export function savePlainRotation(
|
|
|
27
27
|
? buildClearRotationPatches(element)
|
|
28
28
|
: [];
|
|
29
29
|
if (patches.length) clearStudioRotation(element);
|
|
30
|
-
const drawn = applyCssRotation(
|
|
30
|
+
const drawn = applyCssRotation(
|
|
31
|
+
element,
|
|
32
|
+
next.angle,
|
|
33
|
+
patches.length ? undefined : (next.plain ?? undefined),
|
|
34
|
+
);
|
|
31
35
|
patches.push(...drawn);
|
|
32
36
|
const turn = drawn.at(-1)!;
|
|
33
37
|
return commitPositionPatchToHtml(selection, patches, {
|
|
@@ -593,6 +593,7 @@ describe("a timing edit's soft reload restores what GSAP wrote from the files",
|
|
|
593
593
|
|
|
594
594
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
595
595
|
expect.stringContaining("/files/compositions%2Fsub.html"),
|
|
596
|
+
{ credentials: "omit" },
|
|
596
597
|
);
|
|
597
598
|
expect(reloadPreview).not.toHaveBeenCalled();
|
|
598
599
|
expect(wt.getAttribute("style")).toBe("left: 700px;");
|
|
@@ -28,12 +28,13 @@ import {
|
|
|
28
28
|
serializeStudioFileMutations,
|
|
29
29
|
type StudioProjectFileWriter,
|
|
30
30
|
} from "../utils/studioFileMutationCoordinator";
|
|
31
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
31
32
|
|
|
32
33
|
export async function readFileContent(projectId: string, targetPath: string): Promise<string> {
|
|
33
34
|
if (targetPath.includes("\0") || targetPath.includes("..")) {
|
|
34
35
|
throw new Error(`Unsafe path: ${targetPath}`);
|
|
35
36
|
}
|
|
36
|
-
const response = await
|
|
37
|
+
const response = await studioApiFetch(
|
|
37
38
|
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(targetPath)}`,
|
|
38
39
|
);
|
|
39
40
|
if (!response.ok) {
|