@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.
Files changed (163) hide show
  1. package/dist/assets/{hyperframes-player-C-9N8nk0.js → hyperframes-player-5fcgSstJ.js} +1 -1
  2. package/dist/assets/{index-CAvZ0AA-.js → index-C5qKdpgl.js} +205 -203
  3. package/dist/assets/{index-BoUM6Xqb.js → index-XJSawwYe.js} +1 -1
  4. package/dist/assets/{index-CVuorIMv.js → index-tg2LXUeu.js} +1 -1
  5. package/dist/{chunk-TXZMMOMA.js → chunk-3F47EFCT.js} +100 -2
  6. package/dist/chunk-3F47EFCT.js.map +1 -0
  7. package/dist/{domEditingLayers-D7VYFJJD.js → domEditingLayers-OL7GLSS4.js} +2 -2
  8. package/dist/index.d.ts +132 -41
  9. package/dist/index.html +1 -1
  10. package/dist/index.js +1184 -1084
  11. package/dist/index.js.map +1 -1
  12. package/package.json +8 -8
  13. package/src/captions/hooks/useCaptionSync.ts +10 -6
  14. package/src/components/ProjectUnreachableBanner.tsx +3 -3
  15. package/src/components/editor/DomEditOverlay.tsx +1 -0
  16. package/src/components/editor/domEditOverlayGestures.ts +1 -0
  17. package/src/components/editor/domEditOverlayStartGesture.ts +2 -2
  18. package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
  19. package/src/components/editor/domGroupMoveBy.ts +95 -0
  20. package/src/components/editor/dragUndoPaint.test.ts +5 -5
  21. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
  22. package/src/components/editor/manualEditingAvailability.ts +8 -0
  23. package/src/components/editor/manualOffsetDrag.ts +8 -9
  24. package/src/components/editor/motionPathSelection.test.ts +6 -31
  25. package/src/components/editor/plainTranslate.ts +5 -3
  26. package/src/components/editor/probeSourceElement.ts +2 -1
  27. package/src/components/editor/propertyPanelFont.tsx +3 -2
  28. package/src/components/editor/propertyPanelHelpers.ts +2 -2
  29. package/src/components/editor/propertyPanelTransformCommit.ts +2 -2
  30. package/src/components/editor/rotationDraft.ts +2 -1
  31. package/src/components/editor/useColorGradingController.ts +2 -1
  32. package/src/components/editor/useDomEditOverlayGestures.ts +2 -2
  33. package/src/components/editor/useFxCarveNodes.ts +2 -1
  34. package/src/components/editor/useFxLevelling.ts +2 -1
  35. package/src/components/feedback/projectProvenance.ts +2 -1
  36. package/src/components/nle/NLEContext.test.ts +1 -0
  37. package/src/components/nle/NLEContext.tsx +2 -1
  38. package/src/components/nle/NLEPreview.tsx +12 -10
  39. package/src/components/nle/PreviewPoster.tsx +2 -1
  40. package/src/components/renders/useFfmpegStatus.ts +2 -1
  41. package/src/components/renders/useRenderQueue.ts +5 -4
  42. package/src/components/sidebar/AssetsTab.tsx +2 -1
  43. package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
  44. package/src/contexts/DomEditContext.tsx +4 -0
  45. package/src/hooks/domSelectionTestHarness.ts +23 -0
  46. package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
  47. package/src/hooks/domStyleCommit.ts +8 -1
  48. package/src/hooks/draggedGsapPosition.ts +10 -0
  49. package/src/hooks/gsapDragCommit.test.ts +75 -164
  50. package/src/hooks/gsapDragCommit.ts +2 -105
  51. package/src/hooks/gsapDragPositionCommit.ts +30 -393
  52. package/src/hooks/gsapEditOutcome.ts +22 -2
  53. package/src/hooks/gsapGroupPreflight.ts +34 -0
  54. package/src/hooks/gsapMutationClient.ts +4 -3
  55. package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
  56. package/src/hooks/gsapParsedTween.test.ts +62 -0
  57. package/src/hooks/gsapParsedTween.ts +243 -0
  58. package/src/hooks/gsapPositionDetection.test.ts +78 -0
  59. package/src/hooks/gsapPositionDetection.ts +52 -33
  60. package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
  61. package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
  62. package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
  63. package/src/hooks/gsapResizeIntercept.test.ts +63 -11
  64. package/src/hooks/gsapResizeIntercept.ts +79 -169
  65. package/src/hooks/gsapResizeSweep.test.ts +16 -33
  66. package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
  67. package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
  68. package/src/hooks/gsapRuntimeBridge.ts +105 -131
  69. package/src/hooks/gsapRuntimeKeyframes.ts +16 -3
  70. package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
  71. package/src/hooks/gsapScriptCommitTypes.ts +9 -1
  72. package/src/hooks/gsapShared.ts +7 -0
  73. package/src/hooks/gsapShared.writeTarget.test.ts +26 -58
  74. package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
  75. package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
  76. package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
  77. package/src/hooks/gsapValueAtPlayhead.ts +292 -0
  78. package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
  79. package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
  80. package/src/hooks/keyframeCacheAstLoad.ts +2 -1
  81. package/src/hooks/newTweenTarget.test.ts +41 -76
  82. package/src/hooks/plainRotation.ts +5 -1
  83. package/src/hooks/timelineTimingSync.test.ts +1 -0
  84. package/src/hooks/timelineTimingSync.ts +2 -1
  85. package/src/hooks/useAskAgentModal.ts +2 -1
  86. package/src/hooks/useBlockCatalog.ts +2 -1
  87. package/src/hooks/useCaptionDetection.ts +2 -1
  88. package/src/hooks/useCompositionContentLoader.ts +2 -1
  89. package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
  90. package/src/hooks/useDomEditPersist.test.tsx +335 -0
  91. package/src/hooks/useDomEditSession.test.tsx +30 -1
  92. package/src/hooks/useDomEditSession.ts +10 -0
  93. package/src/hooks/useDomEditTextCommits.test.tsx +23 -0
  94. package/src/hooks/useDomEditTextCommits.ts +8 -2
  95. package/src/hooks/useDomGeometryCommit.test.tsx +5 -1
  96. package/src/hooks/useDomGeometryCommit.ts +7 -9
  97. package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
  98. package/src/hooks/useEditHistoryActions.test.tsx +15 -0
  99. package/src/hooks/useEditHistoryActions.ts +16 -5
  100. package/src/hooks/useElementLifecycleOps.ts +2 -1
  101. package/src/hooks/useEnableKeyframes.test.ts +32 -1
  102. package/src/hooks/useEnableKeyframes.ts +2 -1
  103. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +19 -0
  104. package/src/hooks/useExternalFileChangeCoordinator.ts +4 -2
  105. package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
  106. package/src/hooks/useFileManager.ts +14 -10
  107. package/src/hooks/useFileTree.ts +3 -2
  108. package/src/hooks/useFrameCapture.ts +5 -1
  109. package/src/hooks/useFreezeFrame.test.ts +39 -3
  110. package/src/hooks/useFreezeFrame.ts +7 -3
  111. package/src/hooks/useGroupCommits.ts +2 -1
  112. package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
  113. package/src/hooks/useGsapAwareEditing.test.tsx +42 -29
  114. package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
  115. package/src/hooks/useGsapAwareEditing.ts +17 -8
  116. package/src/hooks/useGsapAwareGroupMove.ts +18 -18
  117. package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
  118. package/src/hooks/useGsapScriptCommits.test.tsx +72 -2
  119. package/src/hooks/useGsapScriptCommits.ts +21 -17
  120. package/src/hooks/useLintModal.ts +2 -1
  121. package/src/hooks/usePersistentEditHistory.test.ts +302 -3
  122. package/src/hooks/usePersistentEditHistory.ts +50 -17
  123. package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
  124. package/src/hooks/usePreviewPersistence.ts +13 -11
  125. package/src/hooks/useProjectFileWriter.ts +20 -19
  126. package/src/hooks/useRemoveBackground.ts +2 -1
  127. package/src/hooks/useSdkSession.ts +2 -1
  128. package/src/hooks/useServerConnection.ts +4 -3
  129. package/src/hooks/useStudioSelectionPublisher.ts +2 -1
  130. package/src/hooks/useThumbnailStill.test.ts +100 -0
  131. package/src/hooks/useThumbnailStill.ts +37 -0
  132. package/src/hooks/useTimelineDeleteOps.ts +2 -1
  133. package/src/index.ts +4 -0
  134. package/src/player/components/AudioWaveform.tsx +3 -2
  135. package/src/player/components/CompositionThumbnail.tsx +2 -1
  136. package/src/player/components/clipMenuNormalize.ts +2 -1
  137. package/src/player/components/clipPeakMap.ts +2 -1
  138. package/src/player/hooks/useTimelinePlayer.ts +5 -1
  139. package/src/player/lib/thumbnailScheduler.test.ts +16 -0
  140. package/src/player/lib/thumbnailScheduler.ts +10 -2
  141. package/src/player/sceneSwap.ts +2 -1
  142. package/src/telemetry/client.ts +2 -1
  143. package/src/utils/blockInstaller.ts +2 -1
  144. package/src/utils/elementGsap.test.ts +9 -0
  145. package/src/utils/elementGsap.ts +7 -0
  146. package/src/utils/gsapSoftReload.test.ts +39 -0
  147. package/src/utils/gsapSoftReload.ts +9 -14
  148. package/src/utils/gsapUndoRestore.browser.test.ts +94 -41
  149. package/src/utils/gsapUndoRestore.ts +3 -6
  150. package/src/utils/previewPrefetch.ts +2 -1
  151. package/src/utils/razorSplitTransaction.ts +12 -8
  152. package/src/utils/softReloadTargets.ts +5 -6
  153. package/src/utils/studioApiFetch.test.ts +75 -0
  154. package/src/utils/studioApiFetch.ts +13 -0
  155. package/src/utils/studioFileHistory.ts +5 -1
  156. package/src/utils/studioHelpers.ts +7 -1
  157. package/src/utils/studioPendingEdits.test.ts +23 -6
  158. package/src/utils/studioPendingEdits.ts +32 -8
  159. package/src/utils/studioTelemetry.ts +2 -1
  160. package/src/utils/timelineCompositionInsert.ts +3 -2
  161. package/src/webmcp/StudioAgentTools.tsx +2 -1
  162. package/dist/chunk-TXZMMOMA.js.map +0 -1
  163. /package/dist/{domEditingLayers-D7VYFJJD.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
@@ -14,6 +14,7 @@ import {
14
14
  type GsapDragCommitCallbacks,
15
15
  } from "./gsapDragCommit";
16
16
  import { usePlayerStore } from "../player/store/playerStore";
17
+ import { liveTween, previewWith } from "./gsapParsedTween.test-helpers";
17
18
 
18
19
  // Minimal selection whose element has no drag-baseline attributes (origX/Y = 0).
19
20
  const selection = (): DomEditSelection =>
@@ -111,175 +112,86 @@ function recordingCallbacks(): {
111
112
  };
112
113
  }
113
114
 
114
- describe("commitGsapPositionFromDrag — flat tween", () => {
115
- beforeEach(() => {
116
- usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null });
117
- });
118
-
119
- it("extends the existing tween (never spawns a parallel one) when dragged OUTSIDE its range", async () => {
120
- // fallow-ignore-next-line code-duplication
121
- usePlayerStore.setState({ currentTime: 6 }); // outside [1.2, 3.4]
122
- const { types, callbacks } = recordingCallbacks();
123
-
124
- await commitGsapPositionFromDrag(
125
- selection(),
126
- flatTween(),
127
- { x: -100, y: 0 },
128
- { x: 0, y: 0 },
129
- null,
130
- "#puck-a",
131
- callbacks,
132
- );
133
-
134
- expect(types).toContain("convert-to-keyframes");
135
- expect(types).toContain("replace-with-keyframes"); // the extend
136
- expect(types).not.toContain("add-with-keyframes"); // regression: no parallel tween
137
- });
138
-
139
- it("adds a keyframe at the playhead when dragged INSIDE its range", async () => {
140
- // fallow-ignore-next-line code-duplication
141
- usePlayerStore.setState({ currentTime: 2 }); // inside [1.2, 3.4]
142
- // fallow-ignore-next-line code-duplication
143
- const { types, callbacks } = recordingCallbacks();
144
-
145
- await commitGsapPositionFromDrag(
146
- selection(),
147
- flatTween(),
148
- { x: -100, y: 0 },
149
- { x: 0, y: 0 },
150
- null,
151
- "#puck-a",
152
- callbacks,
153
- );
154
-
155
- expect(types).toContain("add-keyframe");
156
- expect(types).not.toContain("add-with-keyframes");
157
- });
115
+ describe("commitGsapPositionFromDrag — at a selected keyframe or past the tween", () => {
116
+ beforeEach(() => usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null }));
117
+ const puck = () => {
118
+ const element = document.createElement("div");
119
+ element.id = "puck-a";
120
+ return { id: "puck-a", selector: "#puck-a", element } as unknown as DomEditSelection;
121
+ };
122
+ const preview = (
123
+ sel: DomEditSelection,
124
+ anim: GsapAnimation,
125
+ ends: Record<string, [number, number]>,
126
+ from = false,
127
+ ) =>
128
+ previewWith(sel.element, [
129
+ liveTween(
130
+ sel.element,
131
+ { start: anim.resolvedStart!, duration: anim.duration!, vars: anim.properties, ends },
132
+ { from },
133
+ ),
134
+ ]);
158
135
 
159
- it("MODIFIES the selected keyframe (no extend) when one is selected, even past the tween end", async () => {
160
- // User clicked the 100% diamond (activeKeyframePct=100), playhead drifted past
161
- // the end. Expect: convert + add-keyframe AT 100% — not replace-with-keyframes.
136
+ it("changes the selected keyframe without extending, even with the playhead past the end", async () => {
162
137
  usePlayerStore.setState({ currentTime: 6, activeKeyframePct: 100 }); // outside [1.2, 3.4]
163
- // fallow-ignore-next-line code-duplication
164
- const { types, mutations, callbacks } = recordingCallbacks();
165
-
138
+ const { mutations, callbacks } = recordingCallbacks();
139
+ const sel = puck();
166
140
  await commitGsapPositionFromDrag(
167
- selection(),
141
+ sel,
168
142
  flatTween(),
169
143
  { x: -100, y: 0 },
170
144
  { x: 0, y: 0 },
171
- null,
172
- "#puck-a",
145
+ preview(sel, flatTween(), { x: [0, -260] }),
173
146
  callbacks,
174
147
  );
175
-
176
- expect(types).toContain("add-keyframe");
177
- expect(types).not.toContain("replace-with-keyframes"); // not extended
178
- const addKf = mutations.find((m) => m.type === "add-keyframe");
179
- expect(addKf?.percentage).toBe(100); // modified the selected endpoint
180
- // consumed: cleared so the next free drag doesn't keep modifying
148
+ expect(mutations).toEqual([
149
+ expect.objectContaining({
150
+ type: "replace-with-keyframes",
151
+ position: 1.2,
152
+ duration: 2.2,
153
+ easeEach: "power1.out",
154
+ keyframes: [{ percentage: 100, properties: { x: -100, y: 0 } }],
155
+ }),
156
+ ]);
181
157
  expect(usePlayerStore.getState().activeKeyframePct).toBeNull();
182
- // parked the playhead on the edited keyframe (1.2 start + 100% * 2.2 dur),
183
- // so the edit is visible instead of rendering the base pose
184
158
  expect(usePlayerStore.getState().requestedSeekTime).toBe(3.4);
185
159
  });
186
- });
187
160
 
188
- describe("commitGsapPositionFromDrag — keyframed tween backfill", () => {
189
- beforeEach(() => {
190
- usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null });
191
- });
192
-
193
- const keyframedTween = (): GsapAnimation =>
194
- ({
195
- id: "#puck-a-kf",
196
- targetSelector: "#puck-a",
197
- method: "to",
198
- resolvedStart: 1.2,
199
- duration: 2.2,
200
- keyframes: {
201
- keyframes: [
202
- { percentage: 0, properties: { x: 0 } },
203
- { percentage: 100, properties: { x: -260 } },
204
- ],
205
- },
206
- }) as unknown as GsapAnimation;
207
-
208
- it("passes backfillDefaults so a newly-introduced prop doesn't move the other keyframes", async () => {
209
- // Drag the 0% keyframe DOWN (introduces y on an x-only tween). The add-keyframe
210
- // must carry backfillDefaults at the element's base so 100% gets y:0, not y:780.
211
- usePlayerStore.setState({ currentTime: 1.2, activeKeyframePct: 0 });
212
- const { mutations, callbacks } = recordingCallbacks();
213
-
214
- await commitGsapPositionFromDrag(
215
- selection(),
216
- keyframedTween(),
217
- { x: 0, y: 780 }, // studioOffset: dragged straight down
218
- { x: 0, y: 0 }, // gsapPos → base falls back to {0,0} (selection has no base attrs)
219
- null,
220
- "#puck-a",
221
- callbacks,
222
- );
223
-
224
- const addKf = mutations.find((m) => m.type === "add-keyframe");
225
- expect(addKf).toBeDefined();
226
- expect(addKf?.percentage).toBe(0); // edited the selected 0% keyframe
227
- expect(addKf?.properties).toMatchObject({ y: 780 });
228
- expect(addKf?.backfillDefaults).toEqual({ x: 0, y: 0 }); // base → 100% gets y:0
229
- });
230
- });
231
-
232
- describe("commitGsapPositionFromDrag — from() tween dragged outside its range", () => {
233
- beforeEach(() => usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null }));
234
-
235
- const fromTween = (): GsapAnimation =>
236
- ({
237
- id: "#title-from-400",
161
+ it("extends a from() tween to the playhead, keeping its parsed end as the old end", async () => {
162
+ usePlayerStore.setState({ currentTime: 2.2 }); // outside [0.4, 1.3]
163
+ const fromTween = {
164
+ id: "#title-from-400-position",
238
165
  targetSelector: "#title",
239
166
  method: "from",
167
+ propertyGroup: "position",
240
168
  resolvedStart: 0.4,
241
169
  duration: 0.9,
170
+ ease: "none",
242
171
  properties: { y: 70 },
243
- }) as unknown as GsapAnimation;
244
-
245
- it("REPLACES the split position from() tween (no parallel tween → no drop jump)", async () => {
246
- usePlayerStore.setState({ currentTime: 2.13 }); // outside [0.4, 1.3]
247
- const types: string[] = [];
248
- const mutations: Array<Record<string, unknown>> = [];
249
- const callbacks: GsapDragCommitCallbacks = {
250
- commitMutation: async (_s, m) => {
251
- types.push(m.type as string);
252
- mutations.push(m);
253
- },
254
- // After split-into-property-groups, the position group is a from() tween (no keyframes).
255
- fetchAnimations: async () => [
256
- {
257
- id: "#title-from-400-position",
258
- targetSelector: "#title",
259
- method: "from",
260
- propertyGroup: "position",
261
- resolvedStart: 0.4,
262
- duration: 0.9,
263
- properties: { y: 70 },
264
- } as unknown as GsapAnimation,
265
- ],
266
- };
267
-
172
+ } as unknown as GsapAnimation;
173
+ const { types, mutations, callbacks } = recordingCallbacks();
174
+ const sel = puck();
268
175
  await commitGsapPositionFromDrag(
269
- selection(),
270
- fromTween(),
176
+ sel,
177
+ fromTween,
271
178
  { x: 0, y: -333 },
272
179
  { x: 0, y: 0 },
273
- null,
274
- "#title",
180
+ preview(sel, fromTween, { y: [70, 0] }, true),
275
181
  callbacks,
276
182
  );
277
-
278
- expect(types).toContain("split-into-property-groups");
279
- expect(types).toContain("replace-with-keyframes");
280
- expect(types).not.toContain("add-with-keyframes"); // regression: no parallel tween
281
- const replace = mutations.find((m) => m.type === "replace-with-keyframes");
282
- expect(replace?.animationId).toBe("#title-from-400-position"); // replaces the split from()
183
+ expect(types).toEqual(["replace-with-keyframes"]);
184
+ expect(mutations[0]).toMatchObject({
185
+ animationId: "#title-from-400-position",
186
+ position: 0.4,
187
+ duration: 1.8,
188
+ easeEach: "none",
189
+ keyframes: [
190
+ { percentage: 0, properties: { y: 70, x: 0 } },
191
+ { percentage: 50, properties: { y: 0, x: 0 } },
192
+ { percentage: 100, properties: { y: -333, x: 0 } },
193
+ ],
194
+ });
283
195
  });
284
196
  });
285
197
 
@@ -581,32 +493,31 @@ describe("commitStaticGsapRotation — instantPatch (value-only set)", () => {
581
493
  });
582
494
  });
583
495
 
584
- describe("commitGsapPositionFromDrag — keyframe/structural commits omit instantPatch", () => {
496
+ describe("commitGsapPositionFromDrag — keyframe commits omit instantPatch", () => {
585
497
  beforeEach(() => usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null }));
586
498
 
587
- it("a structural keyframe drag (convert-to-keyframes → add-keyframe) sets no instantPatch", async () => {
588
- usePlayerStore.setState({ currentTime: 2 }); // inside [1.2, 3.4] → convert + add-keyframe
499
+ it("a keyframe drag relies on the soft reload, not the instant patch", async () => {
500
+ usePlayerStore.setState({ currentTime: 2 }); // inside [1.2, 3.4]
589
501
  const { commits, callbacks } = optionRecordingCallbacks();
590
-
502
+ const element = document.createElement("div");
503
+ const sel = { id: "puck-a", selector: "#puck-a", element } as unknown as DomEditSelection;
504
+ const live = liveTween(element, {
505
+ start: 1.2,
506
+ duration: 2.2,
507
+ vars: { x: -260 },
508
+ ends: { x: [0, -260] },
509
+ });
591
510
  await commitGsapPositionFromDrag(
592
- selection(),
511
+ sel,
593
512
  flatTween(),
594
513
  { x: -100, y: 0 },
595
514
  { x: 0, y: 0 },
596
- null,
597
- "#puck-a",
515
+ previewWith(element, [live]),
598
516
  callbacks,
599
517
  );
600
-
601
- // The keyframe path is structural here (convert + add-keyframe) and must rely
602
- // on the soft reload — none of its commits opt into the instant patch.
603
- expect(commits.length).toBeGreaterThan(0);
604
- for (const c of commits) {
605
- expect(c.options.instantPatch).toBeUndefined();
606
- }
607
- const types = commits.map((c) => c.mutation.type);
608
- expect(types).toContain("convert-to-keyframes");
609
- expect(types).toContain("add-keyframe");
518
+ expect(commits.map((c) => c.mutation.type)).toEqual(["replace-with-keyframes"]);
519
+ expect(commits[0]!.options.instantPatch).toBeUndefined();
520
+ expect(commits[0]!.options.softReload).toBe(true);
610
521
  });
611
522
  });
612
523
 
@@ -4,14 +4,10 @@
4
4
  */
5
5
  import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
6
6
  import type { DomEditSelection } from "../components/editor/domEditingTypes";
7
- import {
8
- STUDIO_ORIGINAL_WIDTH_ATTR,
9
- STUDIO_ORIGINAL_HEIGHT_ATTR,
10
- } from "../components/editor/manualEditsTypes";
11
7
  import { usePlayerStore } from "../player/store/playerStore";
12
8
  import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeCompiler";
13
9
  import { roundTo3, roundToLayoutPx } from "../utils/rounding";
14
- import { computeElementPercentage, writeTargetSelector } from "./gsapShared";
10
+ import { computeElementPercentage, keyframeEases, writeTargetSelector } from "./gsapShared";
15
11
  import { computeDraggedGsapPosition } from "./draggedGsapPosition";
16
12
  import type { CommitMutation } from "./gsapScriptCommitTypes";
17
13
  import { isGestureTransactionCommit, runGestureTransaction } from "./gestureTransaction";
@@ -295,104 +291,6 @@ export async function commitStaticGsapSize(
295
291
  );
296
292
  }
297
293
 
298
- /** Rounded `n` when it's a positive finite number, else `fallback`. */
299
- function positiveOr(n: number, fallback: number): number {
300
- return Number.isFinite(n) && n > 0 ? roundToLayoutPx(n) : fallback;
301
- }
302
-
303
- /**
304
- * Prior size for a keyframed resize: the existing global set's value, else the
305
- * element's pre-resize size (the draft saved it on the element before mutating
306
- * el.style.width/height). Falls back to the new size when neither is available.
307
- */
308
- function resolvePriorSize(
309
- sizeSet: GsapAnimation | null,
310
- el: Element | null | undefined,
311
- fallbackW: number,
312
- fallbackH: number,
313
- ): { width: number; height: number } {
314
- if (sizeSet) {
315
- return {
316
- width: positiveOr(Number(sizeSet.properties.width), fallbackW),
317
- height: positiveOr(Number(sizeSet.properties.height), fallbackH),
318
- };
319
- }
320
- const ow = Number.parseFloat(el?.getAttribute(STUDIO_ORIGINAL_WIDTH_ATTR) ?? "");
321
- const oh = Number.parseFloat(el?.getAttribute(STUDIO_ORIGINAL_HEIGHT_ATTR) ?? "");
322
- return { width: positiveOr(ow, fallbackW), height: positiveOr(oh, fallbackH) };
323
- }
324
-
325
- /**
326
- * Resize an *animated* element by keyframing its size at the current playhead,
327
- * instead of a global `gsap.set` hold. Builds a width/height keyframe tween
328
- * aligned to the element's existing animation: every base keyframe keeps the
329
- * prior size, only the keyframe nearest the playhead gets the new size — so
330
- * resizing one keyframe leaves the others unchanged. Replaces any prior global
331
- * size set. Returns false when there's no usable range (caller falls back to the
332
- * static set).
333
- */
334
- export async function commitKeyframedSizeFromResize(
335
- selection: DomEditSelection,
336
- size: { width: number; height: number },
337
- selector: string,
338
- sizeSet: GsapAnimation | null,
339
- animatedTween: GsapAnimation,
340
- callbacks: GsapDragCommitCallbacks,
341
- ): Promise<boolean> {
342
- const ts = resolveTweenStart(animatedTween) ?? 0;
343
- const td = resolveTweenDuration(animatedTween);
344
- if (!(td > 0)) return false;
345
-
346
- const newW = roundToLayoutPx(size.width);
347
- const newH = roundToLayoutPx(size.height);
348
- const prior = resolvePriorSize(sizeSet, selection.element, newW, newH);
349
-
350
- const ct = usePlayerStore.getState().currentTime;
351
- const pct = Math.max(0, Math.min(100, Math.round(((ct - ts) / td) * 1000) / 10));
352
-
353
- // Base keyframe percentages from the animated tween (flat tween → 0 & 100),
354
- // plus the endpoints and the playhead. Each keeps the prior size except the
355
- // keyframe at the playhead, which gets the new size.
356
- const pcts = new Set<number>(
357
- animatedTween.keyframes?.keyframes.map((k) => k.percentage) ?? [0, 100],
358
- );
359
- pcts.add(0);
360
- pcts.add(100);
361
- pcts.add(pct);
362
- const keyframes = Array.from(pcts)
363
- .sort((a, b) => a - b)
364
- .map((p) => ({
365
- percentage: p,
366
- properties: Math.abs(p - pct) < 0.05 ? { width: newW, height: newH } : { ...prior },
367
- }));
368
-
369
- // Add the size keyframe tween FIRST, then delete the old global hold. The gesture
370
- // transport applies both in one ordered batch; a plain commit fallback keeps the
371
- // same recoverable ordering. Only the transaction's result triggers the reload.
372
- const addLabel = `Resize (size keyframe ${pct.toFixed(0)}%)`;
373
- const target = newTweenTarget(selection);
374
- if (!target) return false;
375
- await callbacks.commitMutation(
376
- selection,
377
- {
378
- type: "add-with-keyframes",
379
- targetSelector: target,
380
- position: roundTo3(ts),
381
- duration: roundTo3(td),
382
- keyframes,
383
- },
384
- sizeSet ? { label: addLabel, skipReload: true } : { label: addLabel, softReload: true },
385
- );
386
- if (sizeSet) {
387
- await callbacks.commitMutation(
388
- selection,
389
- { type: "delete", animationId: sizeSet.id },
390
- { label: "Resize layer", softReload: true },
391
- );
392
- }
393
- return true;
394
- }
395
-
396
294
  // ── Whole-path offset (plain drag on animated element) ──────────────────
397
295
 
398
296
  /**
@@ -438,7 +336,6 @@ export async function commitWholePathOffset(
438
336
 
439
337
  const ts = resolveTweenStart(effectiveAnim);
440
338
  const td = resolveTweenDuration(effectiveAnim);
441
- const ease = effectiveAnim.keyframes?.easeEach ?? effectiveAnim.ease;
442
339
 
443
340
  let kfs = effectiveAnim.keyframes?.keyframes ?? [];
444
341
  if (kfs.length === 0) {
@@ -475,7 +372,7 @@ export async function commitWholePathOffset(
475
372
  position: roundTo3(ts ?? 0),
476
373
  duration: roundTo3(td || 1),
477
374
  keyframes: shifted,
478
- ease,
375
+ ...keyframeEases(effectiveAnim),
479
376
  },
480
377
  { label: "Move animation path", softReload: true, beforeReload: restoreOffset },
481
378
  );