@hyperframes/studio 0.8.114 → 0.8.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/dist/assets/{hyperframes-player-aXNJKiIg.js → hyperframes-player-mGYvPVpw.js} +1 -1
  2. package/dist/assets/{index-Bn2RJNTJ.js → index-BWHt0-ir.js} +1 -1
  3. package/dist/assets/{index-Dz7doz9l.js → index-DYNaXPVf.js} +203 -203
  4. package/dist/assets/{index-C9N-DGjC.js → index-Dr3hNpvS.js} +1 -1
  5. package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
  6. package/dist/{chunk-BIUXP3PG.js → chunk-3F47EFCT.js} +142 -277
  7. package/dist/chunk-3F47EFCT.js.map +1 -0
  8. package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
  9. package/dist/chunk-5EKWJZLM.js.map +1 -0
  10. package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
  11. package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-OL7GLSS4.js} +3 -5
  12. package/dist/index.d.ts +146 -40
  13. package/dist/index.html +2 -2
  14. package/dist/index.js +4021 -3591
  15. package/dist/index.js.map +1 -1
  16. package/dist/player.js +2 -2
  17. package/dist/styles.css +1 -1
  18. package/package.json +9 -8
  19. package/src/App.tsx +2 -2
  20. package/src/captions/hooks/useCaptionSync.ts +10 -6
  21. package/src/components/LinkedSelectionToggle.tsx +3 -2
  22. package/src/components/ProjectUnreachableBanner.tsx +3 -3
  23. package/src/components/dock/Dock.tsx +5 -7
  24. package/src/components/editor/CanvasContextMenu.tsx +3 -2
  25. package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
  26. package/src/components/editor/DomEditCropHandles.tsx +7 -4
  27. package/src/components/editor/DomEditOverlay.tsx +5 -1
  28. package/src/components/editor/FileTreeNodes.tsx +13 -10
  29. package/src/components/editor/MotionPathOverlay.tsx +7 -2
  30. package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
  31. package/src/components/editor/SnapToolbar.test.tsx +100 -1
  32. package/src/components/editor/SnapToolbar.tsx +43 -5
  33. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
  34. package/src/components/editor/audioFxTelemetry.ts +2 -3
  35. package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
  36. package/src/components/editor/cropResize.ts +4 -3
  37. package/src/components/editor/domEditingDom.ts +4 -0
  38. package/src/components/editor/domEditingLayers.ts +1 -5
  39. package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
  40. package/src/components/editor/domGroupMoveBy.ts +95 -0
  41. package/src/components/editor/dragUndoPaint.test.ts +5 -5
  42. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
  43. package/src/components/editor/manualEditingAvailability.ts +8 -0
  44. package/src/components/editor/manualEdits.ts +1 -0
  45. package/src/components/editor/manualEditsParsing.ts +7 -0
  46. package/src/components/editor/manualOffsetDrag.ts +5 -6
  47. package/src/components/editor/marqueeCommit.test.tsx +106 -37
  48. package/src/components/editor/marqueeCommit.ts +24 -8
  49. package/src/components/editor/motionPathCommit.test.ts +29 -1
  50. package/src/components/editor/motionPathCommit.ts +21 -9
  51. package/src/components/editor/motionPathSelection.test.ts +6 -31
  52. package/src/components/editor/probeSourceElement.test.ts +45 -0
  53. package/src/components/editor/probeSourceElement.ts +41 -16
  54. package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
  55. package/src/components/editor/propertyPanelFont.tsx +3 -2
  56. package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
  57. package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
  58. package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
  59. package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
  60. package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
  61. package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
  62. package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
  63. package/src/components/editor/propertyPanelFxSection.tsx +30 -28
  64. package/src/components/editor/propertyPanelTypes.ts +6 -3
  65. package/src/components/editor/useColorGradingController.ts +2 -1
  66. package/src/components/editor/useDomEditNudge.test.tsx +37 -0
  67. package/src/components/editor/useDomEditNudge.ts +2 -0
  68. package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
  69. package/src/components/editor/useDomEditZOrder.ts +12 -2
  70. package/src/components/editor/useFxCarveNodes.ts +2 -1
  71. package/src/components/editor/useFxLevelling.ts +2 -1
  72. package/src/components/feedback/projectProvenance.ts +2 -1
  73. package/src/components/nle/NLEContext.test.ts +1 -0
  74. package/src/components/nle/NLEContext.tsx +2 -1
  75. package/src/components/nle/NLEPreview.tsx +12 -10
  76. package/src/components/nle/PreviewPoster.tsx +2 -1
  77. package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
  78. package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
  79. package/src/components/renders/useFfmpegStatus.ts +2 -1
  80. package/src/components/renders/useRenderQueue.ts +5 -4
  81. package/src/components/sidebar/AssetContextMenu.tsx +4 -4
  82. package/src/components/sidebar/AssetsTab.tsx +2 -1
  83. package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
  84. package/src/components/ui/menuStyle.test.ts +61 -0
  85. package/src/components/ui/menuStyle.ts +10 -0
  86. package/src/contexts/DomEditContext.tsx +5 -1
  87. package/src/contexts/TimelineEditContext.tsx +1 -0
  88. package/src/domEditExports.test.tsx +9 -1
  89. package/src/hooks/domEditCommitRunner.ts +1 -1
  90. package/src/hooks/domSelectionTestHarness.ts +23 -0
  91. package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
  92. package/src/hooks/domStyleCommit.ts +8 -1
  93. package/src/hooks/draggedGsapPosition.ts +10 -0
  94. package/src/hooks/elementOffsetStager.ts +5 -4
  95. package/src/hooks/gsapDragCommit.test.ts +75 -164
  96. package/src/hooks/gsapDragCommit.ts +4 -125
  97. package/src/hooks/gsapDragPositionCommit.ts +36 -384
  98. package/src/hooks/gsapEditOutcome.ts +22 -2
  99. package/src/hooks/gsapGestureOutcome.test.ts +161 -0
  100. package/src/hooks/gsapGestureOutcome.ts +64 -0
  101. package/src/hooks/gsapGroupPreflight.ts +34 -0
  102. package/src/hooks/gsapKeyframeCommit.ts +3 -12
  103. package/src/hooks/gsapMutationClient.ts +4 -3
  104. package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
  105. package/src/hooks/gsapParsedTween.test.ts +62 -0
  106. package/src/hooks/gsapParsedTween.ts +243 -0
  107. package/src/hooks/gsapPositionDetection.test.ts +78 -0
  108. package/src/hooks/gsapPositionDetection.ts +52 -33
  109. package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
  110. package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
  111. package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
  112. package/src/hooks/gsapResizeIntercept.test.ts +96 -11
  113. package/src/hooks/gsapResizeIntercept.ts +79 -168
  114. package/src/hooks/gsapResizeSweep.test.ts +16 -33
  115. package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
  116. package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
  117. package/src/hooks/gsapRuntimeBridge.ts +105 -131
  118. package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
  119. package/src/hooks/gsapScriptCommitTypes.ts +13 -0
  120. package/src/hooks/gsapShared.ts +7 -0
  121. package/src/hooks/gsapShared.writeTarget.test.ts +45 -57
  122. package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
  123. package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
  124. package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
  125. package/src/hooks/gsapValueAtPlayhead.ts +292 -0
  126. package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
  127. package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
  128. package/src/hooks/keyframeCacheAstLoad.ts +8 -6
  129. package/src/hooks/newTweenTarget.test.ts +41 -76
  130. package/src/hooks/plainRotation.ts +3 -2
  131. package/src/hooks/previewPromotionSelection.test.tsx +1 -1
  132. package/src/hooks/refreshTimelineRowText.test.ts +65 -0
  133. package/src/hooks/refreshTimelineRowText.ts +29 -0
  134. package/src/hooks/timelineTimingSync.test.ts +1 -0
  135. package/src/hooks/timelineTimingSync.ts +2 -1
  136. package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
  137. package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
  138. package/src/hooks/useAskAgentModal.ts +2 -1
  139. package/src/hooks/useBlockCatalog.ts +2 -1
  140. package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
  141. package/src/hooks/useCaptionDetection.ts +2 -1
  142. package/src/hooks/useCompositionContentLoader.ts +2 -1
  143. package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
  144. package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
  145. package/src/hooks/useDomEditSession.test.tsx +30 -1
  146. package/src/hooks/useDomEditSession.ts +10 -0
  147. package/src/hooks/useDomEditTextCommits.test.tsx +41 -0
  148. package/src/hooks/useDomEditTextCommits.ts +13 -3
  149. package/src/hooks/useDomEditWiring.ts +4 -2
  150. package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
  151. package/src/hooks/useDomGeometryCommit.ts +8 -4
  152. package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
  153. package/src/hooks/useDomGeometryCommits.ts +3 -2
  154. package/src/hooks/useDomSelection.test.ts +26 -0
  155. package/src/hooks/useDomSelection.ts +10 -3
  156. package/src/hooks/useDomSelectionTypes.ts +6 -1
  157. package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
  158. package/src/hooks/useEditHistoryActions.test.tsx +15 -0
  159. package/src/hooks/useEditHistoryActions.ts +16 -5
  160. package/src/hooks/useElementLifecycleOps.ts +2 -1
  161. package/src/hooks/useEnableKeyframes.test.ts +34 -3
  162. package/src/hooks/useEnableKeyframes.ts +7 -4
  163. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +121 -0
  164. package/src/hooks/useExternalFileChangeCoordinator.ts +35 -8
  165. package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
  166. package/src/hooks/useFileManager.ts +14 -10
  167. package/src/hooks/useFileTree.ts +3 -2
  168. package/src/hooks/useFrameCapture.ts +5 -1
  169. package/src/hooks/useFreezeFrame.ts +3 -2
  170. package/src/hooks/useGestureCommit.test.tsx +225 -55
  171. package/src/hooks/useGestureCommit.ts +86 -55
  172. package/src/hooks/useGroupCommits.ts +2 -1
  173. package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
  174. package/src/hooks/useGsapAwareEditing.test.tsx +28 -35
  175. package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
  176. package/src/hooks/useGsapAwareEditing.ts +101 -195
  177. package/src/hooks/useGsapAwareGroupMove.ts +179 -0
  178. package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
  179. package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
  180. package/src/hooks/useGsapKeyframeOps.ts +36 -10
  181. package/src/hooks/useGsapScriptCommits.test.tsx +60 -1
  182. package/src/hooks/useGsapScriptCommits.ts +15 -10
  183. package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
  184. package/src/hooks/useGsapSelectionHandlers.ts +24 -18
  185. package/src/hooks/useGsapTweenCache.ts +18 -12
  186. package/src/hooks/useLintModal.ts +2 -1
  187. package/src/hooks/usePersistentEditHistory.test.ts +277 -1
  188. package/src/hooks/usePersistentEditHistory.ts +47 -8
  189. package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
  190. package/src/hooks/usePreviewInteraction.test.ts +21 -0
  191. package/src/hooks/usePreviewInteraction.ts +2 -0
  192. package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
  193. package/src/hooks/usePreviewPersistence.ts +13 -11
  194. package/src/hooks/useProjectFileWriter.ts +20 -19
  195. package/src/hooks/useRemoveBackground.ts +2 -1
  196. package/src/hooks/useRenderClipContent.test.ts +18 -0
  197. package/src/hooks/useRenderClipContent.ts +4 -5
  198. package/src/hooks/useSdkSession.ts +2 -1
  199. package/src/hooks/useServerConnection.ts +4 -3
  200. package/src/hooks/useStudioSelectionPublisher.ts +2 -1
  201. package/src/hooks/useThumbnailStill.test.ts +100 -0
  202. package/src/hooks/useThumbnailStill.ts +37 -0
  203. package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
  204. package/src/hooks/useTimelineDeleteOps.ts +22 -27
  205. package/src/hooks/useTimelineEditing.test.tsx +1 -1
  206. package/src/index.ts +4 -0
  207. package/src/player/components/AudioWaveform.tsx +3 -2
  208. package/src/player/components/AutomationSelectionMenu.tsx +3 -2
  209. package/src/player/components/ClipContextMenu.test.tsx +73 -0
  210. package/src/player/components/ClipContextMenu.tsx +33 -31
  211. package/src/player/components/CompositionThumbnail.tsx +2 -1
  212. package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
  213. package/src/player/components/OutOfSyncBadge.tsx +5 -5
  214. package/src/player/components/TextClipContent.test.ts +19 -0
  215. package/src/player/components/TextClipContent.tsx +27 -0
  216. package/src/player/components/TrackGapContextMenu.tsx +3 -6
  217. package/src/player/components/clipMenuNormalize.ts +2 -1
  218. package/src/player/components/clipMenuToolItems.test.tsx +23 -0
  219. package/src/player/components/clipMenuToolItems.tsx +13 -9
  220. package/src/player/components/clipPeakMap.ts +2 -1
  221. package/src/player/components/timelineCallbacks.ts +1 -0
  222. package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
  223. package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
  224. package/src/player/components/timelineClipGestureHandlers.ts +20 -2
  225. package/src/player/components/timelineEditing.ts +3 -1
  226. package/src/player/components/timelineLinkSelection.ts +35 -1
  227. package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
  228. package/src/player/components/useTimelineClipDrag.ts +6 -7
  229. package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
  230. package/src/player/hooks/timelinePlayerSync.ts +6 -1
  231. package/src/player/hooks/useTimelinePlayer.ts +5 -1
  232. package/src/player/lib/thumbnailScheduler.test.ts +16 -0
  233. package/src/player/lib/thumbnailScheduler.ts +10 -2
  234. package/src/player/lib/timelineElementHelpers.test.ts +87 -1
  235. package/src/player/lib/timelineElementHelpers.ts +6 -15
  236. package/src/player/lib/timelineText.ts +40 -0
  237. package/src/player/sceneSwap.ts +2 -1
  238. package/src/player/store/timelineElement.ts +12 -0
  239. package/src/styles/theme.css +3 -0
  240. package/src/telemetry/client.ts +2 -1
  241. package/src/utils/blockInstaller.ts +2 -1
  242. package/src/utils/elementGsap.test.ts +9 -0
  243. package/src/utils/elementGsap.ts +7 -0
  244. package/src/utils/gsapSoftReload.ts +2 -3
  245. package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
  246. package/src/utils/gsapUndoRestore.ts +3 -6
  247. package/src/utils/keyframeUsage.ts +79 -0
  248. package/src/utils/previewFeatureUsage.test.ts +32 -0
  249. package/src/utils/previewFeatureUsage.ts +46 -0
  250. package/src/utils/previewPrefetch.ts +2 -1
  251. package/src/utils/razorSplitTransaction.ts +12 -8
  252. package/src/utils/softReloadTargets.ts +1 -3
  253. package/src/utils/studioApiFetch.test.ts +75 -0
  254. package/src/utils/studioApiFetch.ts +13 -0
  255. package/src/utils/studioFileHistory.ts +4 -1
  256. package/src/utils/studioHelpers.ts +7 -1
  257. package/src/utils/studioPendingEdits.test.ts +23 -6
  258. package/src/utils/studioPendingEdits.ts +32 -8
  259. package/src/utils/studioTelemetry.test.ts +107 -0
  260. package/src/utils/studioTelemetry.ts +51 -5
  261. package/src/utils/timelineCompositionInsert.ts +3 -2
  262. package/src/webmcp/StudioAgentTools.tsx +4 -3
  263. package/dist/chunk-BIUXP3PG.js.map +0 -1
  264. package/dist/chunk-K47E2ZJA.js.map +0 -1
  265. /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
  266. /package/dist/{domEditingLayers-V67MS4WP.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,16 +4,12 @@
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
- import type { RuntimeTweenChange } from "./gsapRuntimePatch";
12
+ import type { CommitMutation } from "./gsapScriptCommitTypes";
17
13
  import { isGestureTransactionCommit, runGestureTransaction } from "./gestureTransaction";
18
14
  import { setPatchFromUpdateProperty } from "./gsapDragStaticSetHelpers";
19
15
  import { GsapEditBlockedError } from "./gsapEditOutcome";
@@ -23,25 +19,7 @@ export {
23
19
  findSizeSetAnimation,
24
20
  } from "./gsapDragStaticSetHelpers";
25
21
  export interface GsapDragCommitCallbacks {
26
- commitMutation: (
27
- selection: DomEditSelection,
28
- mutation: Record<string, unknown>,
29
- options: {
30
- label: string;
31
- coalesceKey?: string;
32
- coalesceMs?: number;
33
- softReload?: boolean;
34
- skipReload?: boolean;
35
- beforeReload?: () => void;
36
- /**
37
- * Value-only fast path: when set, `runCommit` patches the changed tween in
38
- * the preview runtime in place (instant, no re-run) and only falls back to
39
- * the soft reload if the patch can't be safely applied. Attached only to
40
- * value-only `set` commits; structural/keyframe commits omit it.
41
- */
42
- instantPatch?: { selector: string; change: RuntimeTweenChange };
43
- },
44
- ) => Promise<void>;
22
+ commitMutation: CommitMutation;
45
23
  fetchAnimations?: () => Promise<GsapAnimation[]>;
46
24
  }
47
25
 
@@ -313,104 +291,6 @@ export async function commitStaticGsapSize(
313
291
  );
314
292
  }
315
293
 
316
- /** Rounded `n` when it's a positive finite number, else `fallback`. */
317
- function positiveOr(n: number, fallback: number): number {
318
- return Number.isFinite(n) && n > 0 ? roundToLayoutPx(n) : fallback;
319
- }
320
-
321
- /**
322
- * Prior size for a keyframed resize: the existing global set's value, else the
323
- * element's pre-resize size (the draft saved it on the element before mutating
324
- * el.style.width/height). Falls back to the new size when neither is available.
325
- */
326
- function resolvePriorSize(
327
- sizeSet: GsapAnimation | null,
328
- el: Element | null | undefined,
329
- fallbackW: number,
330
- fallbackH: number,
331
- ): { width: number; height: number } {
332
- if (sizeSet) {
333
- return {
334
- width: positiveOr(Number(sizeSet.properties.width), fallbackW),
335
- height: positiveOr(Number(sizeSet.properties.height), fallbackH),
336
- };
337
- }
338
- const ow = Number.parseFloat(el?.getAttribute(STUDIO_ORIGINAL_WIDTH_ATTR) ?? "");
339
- const oh = Number.parseFloat(el?.getAttribute(STUDIO_ORIGINAL_HEIGHT_ATTR) ?? "");
340
- return { width: positiveOr(ow, fallbackW), height: positiveOr(oh, fallbackH) };
341
- }
342
-
343
- /**
344
- * Resize an *animated* element by keyframing its size at the current playhead,
345
- * instead of a global `gsap.set` hold. Builds a width/height keyframe tween
346
- * aligned to the element's existing animation: every base keyframe keeps the
347
- * prior size, only the keyframe nearest the playhead gets the new size — so
348
- * resizing one keyframe leaves the others unchanged. Replaces any prior global
349
- * size set. Returns false when there's no usable range (caller falls back to the
350
- * static set).
351
- */
352
- export async function commitKeyframedSizeFromResize(
353
- selection: DomEditSelection,
354
- size: { width: number; height: number },
355
- selector: string,
356
- sizeSet: GsapAnimation | null,
357
- animatedTween: GsapAnimation,
358
- callbacks: GsapDragCommitCallbacks,
359
- ): Promise<boolean> {
360
- const ts = resolveTweenStart(animatedTween) ?? 0;
361
- const td = resolveTweenDuration(animatedTween);
362
- if (!(td > 0)) return false;
363
-
364
- const newW = roundToLayoutPx(size.width);
365
- const newH = roundToLayoutPx(size.height);
366
- const prior = resolvePriorSize(sizeSet, selection.element, newW, newH);
367
-
368
- const ct = usePlayerStore.getState().currentTime;
369
- const pct = Math.max(0, Math.min(100, Math.round(((ct - ts) / td) * 1000) / 10));
370
-
371
- // Base keyframe percentages from the animated tween (flat tween → 0 & 100),
372
- // plus the endpoints and the playhead. Each keeps the prior size except the
373
- // keyframe at the playhead, which gets the new size.
374
- const pcts = new Set<number>(
375
- animatedTween.keyframes?.keyframes.map((k) => k.percentage) ?? [0, 100],
376
- );
377
- pcts.add(0);
378
- pcts.add(100);
379
- pcts.add(pct);
380
- const keyframes = Array.from(pcts)
381
- .sort((a, b) => a - b)
382
- .map((p) => ({
383
- percentage: p,
384
- properties: Math.abs(p - pct) < 0.05 ? { width: newW, height: newH } : { ...prior },
385
- }));
386
-
387
- // Add the size keyframe tween FIRST, then delete the old global hold. The gesture
388
- // transport applies both in one ordered batch; a plain commit fallback keeps the
389
- // same recoverable ordering. Only the transaction's result triggers the reload.
390
- const addLabel = `Resize (size keyframe ${pct.toFixed(0)}%)`;
391
- const target = newTweenTarget(selection);
392
- if (!target) return false;
393
- await callbacks.commitMutation(
394
- selection,
395
- {
396
- type: "add-with-keyframes",
397
- targetSelector: target,
398
- position: roundTo3(ts),
399
- duration: roundTo3(td),
400
- keyframes,
401
- },
402
- sizeSet ? { label: addLabel, skipReload: true } : { label: addLabel, softReload: true },
403
- );
404
- if (sizeSet) {
405
- await callbacks.commitMutation(
406
- selection,
407
- { type: "delete", animationId: sizeSet.id },
408
- { label: "Resize layer", softReload: true },
409
- );
410
- }
411
- return true;
412
- }
413
-
414
294
  // ── Whole-path offset (plain drag on animated element) ──────────────────
415
295
 
416
296
  /**
@@ -456,7 +336,6 @@ export async function commitWholePathOffset(
456
336
 
457
337
  const ts = resolveTweenStart(effectiveAnim);
458
338
  const td = resolveTweenDuration(effectiveAnim);
459
- const ease = effectiveAnim.keyframes?.easeEach ?? effectiveAnim.ease;
460
339
 
461
340
  let kfs = effectiveAnim.keyframes?.keyframes ?? [];
462
341
  if (kfs.length === 0) {
@@ -493,7 +372,7 @@ export async function commitWholePathOffset(
493
372
  position: roundTo3(ts ?? 0),
494
373
  duration: roundTo3(td || 1),
495
374
  keyframes: shifted,
496
- ease,
375
+ ...keyframeEases(effectiveAnim),
497
376
  },
498
377
  { label: "Move animation path", softReload: true, beforeReload: restoreOffset },
499
378
  );