@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
@@ -0,0 +1,83 @@
1
+ // @vitest-environment happy-dom
2
+ // Real Chrome with the built runtime: its seek, not a plain GSAP seek, decides what a saved edit shows.
3
+ import { readFileSync } from "node:fs";
4
+ import { createRequire } from "node:module";
5
+ import { gsap } from "gsap";
6
+ import { parseGsapScriptAcorn } from "@hyperframes/parsers/gsap-parser-acorn";
7
+ import { replaceTweenWithKeyframesInScript } from "@hyperframes/parsers/gsap-writer-acorn";
8
+ import puppeteer from "puppeteer-core";
9
+ import { expect, it, vi } from "vitest";
10
+ import { findSystemChrome } from "../../vite.browser";
11
+ import type { DomEditSelection } from "../components/editor/domEditingTypes";
12
+ import { usePlayerStore } from "../player/store/playerStore";
13
+ import { planValueEdit } from "./gsapValueAtPlayhead";
14
+
15
+ const require = createRequire(import.meta.url);
16
+ vi.setConfig({ testTimeout: 60_000 });
17
+
18
+ const SRC = `var tl = gsap.timeline({ paused: true });
19
+ tl.from("#target", { x: -60, duration: 2, ease: "none" }, 0);
20
+ tl.fromTo("#target", { x: 0 }, { x: 60, duration: 1, ease: "none" }, 2);
21
+ window.__timelines["main"] = tl;`;
22
+
23
+ /** Plans and writes a drag of `#target` to `x` at `at`, as the move gesture saves it. */
24
+ function writeDrag(x: number, at: number): string {
25
+ const box = Object.assign(document.body.appendChild(document.createElement("div")), {
26
+ id: "target",
27
+ });
28
+ const win = { __timelines: {} as Record<string, gsap.core.Timeline> };
29
+ new Function("gsap", "window", SRC)(gsap, win);
30
+ const timeline = win.__timelines.main!;
31
+ timeline.progress(0.0001, true).seek(at);
32
+ usePlayerStore.setState({ currentTime: at, activeKeyframePct: null });
33
+ const iframe = { contentWindow: { ...win, gsap } } as unknown as HTMLIFrameElement;
34
+ const anim = parseGsapScriptAcorn(SRC).animations[1]!;
35
+ const selection = { id: "target", selector: "#target", element: box } as DomEditSelection;
36
+ const plan = planValueEdit(selection, anim, { x }, iframe);
37
+ timeline.kill();
38
+ if (!plan.ok) throw new Error(`the drag was refused: ${JSON.stringify(plan)}`);
39
+ return replaceTweenWithKeyframesInScript(SRC, anim.id, plan.mutation)!;
40
+ }
41
+
42
+ const page = (script: string) => `<!doctype html><html><body>
43
+ <div id="root" data-composition-id="main" data-start="0" data-duration="4" data-width="1920" data-height="1080">
44
+ <div id="target" class="clip" data-start="0" data-duration="4" data-track-index="1"></div>
45
+ </div>
46
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
47
+ <script>window.__timelines = window.__timelines || {};\n${script}</script>
48
+ </body></html>`;
49
+
50
+ it("a drag on a fromTo's start shows the dropped x there once the runtime seeks", async () => {
51
+ const written = writeDrag(90, 2);
52
+ const executablePath = findSystemChrome();
53
+ if (!executablePath) throw new Error("no Chrome found: set HYPERFRAMES_BROWSER_PATH");
54
+ const browser = await puppeteer.launch({
55
+ executablePath,
56
+ headless: true,
57
+ args: ["--no-sandbox"],
58
+ });
59
+ try {
60
+ const tab = await browser.newPage();
61
+ await tab.setRequestInterception(true);
62
+ // Only the GSAP script is served; anything else is refused, never fetched.
63
+ tab.on("request", (request) =>
64
+ request.url().endsWith("/gsap.min.js")
65
+ ? request.respond({ body: readFileSync(require.resolve("gsap/dist/gsap.min.js"), "utf8") })
66
+ : request.abort("blockedbyclient"),
67
+ );
68
+ await tab.setContent(page(written), { waitUntil: "load" });
69
+ await tab.evaluate(readFileSync(require.resolve("@hyperframes/core/runtime"), "utf8"));
70
+ await tab.waitForFunction(() => "__player" in window);
71
+ const shown = await tab.evaluate(() => {
72
+ const w = window as unknown as {
73
+ __player: { seek(t: number): void };
74
+ gsap: { getProperty(target: string, prop: string): number };
75
+ };
76
+ w.__player.seek(2);
77
+ return w.gsap.getProperty("#target", "x");
78
+ });
79
+ expect(shown).toBeCloseTo(90, 1);
80
+ } finally {
81
+ await browser.close();
82
+ }
83
+ });
@@ -0,0 +1,179 @@
1
+ // @vitest-environment happy-dom
2
+ import { gsap } from "gsap";
3
+ import { parseGsapScriptAcorn } from "@hyperframes/parsers/gsap-parser-acorn";
4
+ import { replaceTweenWithKeyframesInScript } from "@hyperframes/parsers/gsap-writer-acorn";
5
+ import { afterEach, expect, it } from "vitest";
6
+ import type { DomEditSelection } from "../components/editor/domEditingTypes";
7
+ import { usePlayerStore } from "../player/store/playerStore";
8
+ import { findParsedTween, parsedImplicitEndValue, parsedTweenEase } from "./gsapParsedTween";
9
+ import { planValueEdit } from "./gsapValueAtPlayhead";
10
+
11
+ /** Runs a composition script as the preview does: a paused timeline, bound, then seeked to `at`. */
12
+ function play(script: string, at: number) {
13
+ const win = { __timelines: {} as Record<string, gsap.core.Timeline> };
14
+ new Function("gsap", "window", script)(gsap, win);
15
+ const timeline = win.__timelines.t!;
16
+ timeline.progress(0.0001, true).seek(at);
17
+ return { timeline, iframe: { contentWindow: { ...win, gsap } } as unknown as HTMLIFrameElement };
18
+ }
19
+
20
+ /** Drags `#x` to `x` at `at`, writes the plan into the script, and replays the written file there. */
21
+ function dragAndReplay(script: string, x: number, at: number) {
22
+ const box = document.body.appendChild(document.createElement("div"));
23
+ box.id = "x";
24
+ const { timeline, iframe } = play(script, at);
25
+ usePlayerStore.setState({ currentTime: at, activeKeyframePct: null });
26
+ const anim = parseGsapScriptAcorn(script).animations[0]!;
27
+ const selection = { id: "x", selector: "#x", element: box } as DomEditSelection;
28
+ const plan = planValueEdit(selection, anim, { x }, iframe);
29
+ timeline.kill();
30
+ if (!plan.ok) return { plan };
31
+ const written = replaceTweenWithKeyframesInScript(script, anim.id, plan.mutation)!;
32
+ const replay = play(written, at);
33
+ const shown = gsap.getProperty(box, "x");
34
+ replay.timeline.kill();
35
+ return { plan, written, shown };
36
+ }
37
+
38
+ const script = (vars: string) =>
39
+ `var tl = gsap.timeline({ paused: true });\ntl.to("#x", { ${vars} }, 0);\nwindow.__timelines["t"] = tl;`;
40
+
41
+ afterEach(() => {
42
+ document.body.replaceChildren();
43
+ usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null });
44
+ });
45
+
46
+ it("edits a delayed tween with no authored ease, landing the value at the playhead", () => {
47
+ const { plan, shown } = dragAndReplay(script("duration: 1, delay: 0.5, x: 100"), 200, 2);
48
+ expect(plan.ok).toBe(true);
49
+ expect(shown).toBe(200);
50
+ });
51
+
52
+ it("writes a delayed linear tween so GSAP shows the new value at the playhead, not later", () => {
53
+ const { shown } = dragAndReplay(script("duration: 1, delay: 0.5, x: 100, ease: 'none'"), 200, 2);
54
+ expect(shown).toBe(200);
55
+ });
56
+
57
+ it("keeps GSAP's default ease, by name, for a tween that authors none", () => {
58
+ const { plan } = dragAndReplay(script("duration: 1, x: 100"), 60, 0.5);
59
+ expect(plan.ok && plan.mutation.easeEach).toBe("power1.out");
60
+ });
61
+
62
+ it("reads a tween the playhead has not reached without redrawing a sibling's live value", () => {
63
+ const [x, y] = ["x", "y"].map((id) =>
64
+ Object.assign(document.body.appendChild(document.createElement("div")), { id }),
65
+ );
66
+ const src = `var tl = gsap.timeline({ paused: true });
67
+ tl.to("#y", { x: 100, duration: 1, ease: "none" }, 0);
68
+ tl.to("#x", { x: 300, duration: 1 }, 2);
69
+ window.__timelines["t"] = tl;`;
70
+ const { timeline, iframe } = play(src, 0.5);
71
+ gsap.set(y!, { x: 77 });
72
+
73
+ const tween = findParsedTween(iframe, x!, parseGsapScriptAcorn(src).animations[1]!);
74
+
75
+ expect(parsedImplicitEndValue(tween)("x", "start")).toBe(0);
76
+ expect(gsap.getProperty(y!, "x")).toBe(77);
77
+ expect(timeline.time()).toBe(0.5);
78
+ timeline.kill();
79
+ });
80
+
81
+ it("keeps a sibling's live attribute while reading an unplayed tween beside the runtime's filler", () => {
82
+ const [x, y] = ["x", "y"].map((id) =>
83
+ Object.assign(document.body.appendChild(document.createElement("div")), { id }),
84
+ );
85
+ y!.setAttribute("data-value", "0");
86
+ const src = `var tl = gsap.timeline({ paused: true });
87
+ tl.to({}, { duration: 4, data: "hf-runtime-filler" }, 0);
88
+ tl.to("#y", { attr: { "data-value": 100 }, duration: 1, ease: "none" }, 0);
89
+ tl.to("#x", { x: 300, duration: 1 }, 2);
90
+ window.__timelines["t"] = tl;`;
91
+ const { timeline, iframe } = play(src, 0.5);
92
+ y!.setAttribute("data-value", "77");
93
+
94
+ const tween = findParsedTween(iframe, x!, parseGsapScriptAcorn(src).animations.at(-1)!);
95
+
96
+ expect(parsedImplicitEndValue(tween)("x", "start")).toBe(0);
97
+ expect(y!.getAttribute("data-value")).toBe("77");
98
+ timeline.kill();
99
+ });
100
+
101
+ it("reads a later tween's start from the earlier tween on the same layer, as playback does", () => {
102
+ const x = Object.assign(document.body.appendChild(document.createElement("div")), { id: "x" });
103
+ const src = `var tl = gsap.timeline({ paused: true });
104
+ tl.to("#x", { x: 50, duration: 4, ease: "none" }, 0);
105
+ tl.to("#x", { x: 100, duration: 1, ease: "none", overwrite: "auto" }, 2);
106
+ window.__timelines["t"] = tl;`;
107
+ const { timeline, iframe } = play(src, 0.5);
108
+
109
+ const tween = findParsedTween(iframe, x, parseGsapScriptAcorn(src).animations[1]!);
110
+
111
+ expect(parsedImplicitEndValue(tween)("x", "start")).toBe(25);
112
+ timeline.seek(2.5);
113
+ expect(gsap.getProperty(x, "x")).toBe(62.5);
114
+ timeline.kill();
115
+ });
116
+
117
+ it("saves a dragged keyframe tween with its authored start, not the drag's live value", () => {
118
+ const box = Object.assign(document.body.appendChild(document.createElement("div")), { id: "x" });
119
+ const src = script(
120
+ "keyframes: { '0%': { opacity: 0 }, '100%': { opacity: 1, x: 300 } }, duration: 1, ease: 'none'",
121
+ ).replace("}, 0);", "}, 2);");
122
+ const { timeline, iframe } = play(src, 0.5);
123
+ usePlayerStore.setState({ currentTime: 0.5, activeKeyframePct: null });
124
+ gsap.set(box, { x: 77 });
125
+ const anim = parseGsapScriptAcorn(src).animations[0]!;
126
+ const selection = { id: "x", selector: "#x", element: box } as DomEditSelection;
127
+
128
+ const plan = planValueEdit(selection, anim, { x: 77 }, iframe);
129
+ timeline.kill();
130
+
131
+ expect(plan.ok).toBe(true);
132
+ if (!plan.ok) return;
133
+ const written = replaceTweenWithKeyframesInScript(src, anim.id, plan.mutation)!;
134
+ const at = (time: number) => {
135
+ const replay = play(written, time);
136
+ const x = gsap.getProperty(box, "x");
137
+ replay.timeline.kill();
138
+ return x;
139
+ };
140
+ expect(at(0.5)).toBe(77);
141
+ expect(at(2)).toBe(0);
142
+ expect(at(3)).toBe(300);
143
+ });
144
+
145
+ it("keeps a layer's other transform values while reading an unplayed tween", () => {
146
+ const box = Object.assign(document.body.appendChild(document.createElement("div")), { id: "x" });
147
+ const src = script(
148
+ "keyframes: { '0%': { opacity: 0 }, '100%': { opacity: 1, x: 300 } }, duration: 1",
149
+ ).replace("}, 0);", "}, 2);");
150
+ const { timeline, iframe } = play(src, 0.5);
151
+ gsap.set(box, {
152
+ x: 77,
153
+ skewX: 23,
154
+ rotationX: 19,
155
+ xPercent: 12,
156
+ transformOrigin: "20px 30px 40px",
157
+ });
158
+
159
+ const tween = findParsedTween(iframe, box, parseGsapScriptAcorn(src).animations[0]!);
160
+ timeline.kill();
161
+
162
+ expect(parsedImplicitEndValue(tween)("x", "start")).toBe(0);
163
+ const values = ["x", "skewX", "rotationX", "xPercent", "transformOrigin"];
164
+ expect(values.map((p) => gsap.getProperty(box, p))).toEqual([77, 23, 19, 12, "20px 30px 40px"]);
165
+ });
166
+
167
+ it("names only an ease GSAP built in, and refuses a custom function rather than guess", () => {
168
+ const custom = { vars: { ease: (p: number) => p * p } };
169
+ const iframe = { contentWindow: { gsap } } as unknown as HTMLIFrameElement;
170
+ expect(parsedTweenEase(iframe, { vars: { ease: gsap.parseEase("expo.in") } })).toBe("expo.in");
171
+ expect(parsedTweenEase(iframe, custom)).toBeNull();
172
+ });
173
+
174
+ it("writes an edit into a looping tween and keeps its repeat and yoyo, as main did", () => {
175
+ const vars = "duration: 2, x: 100, ease: 'none', repeat: 1, yoyo: true";
176
+ const { plan, written } = dragAndReplay(script(vars), 40, 1);
177
+ expect(plan.ok).toBe(true);
178
+ expect(written).toMatch(/repeat: 1[\s\S]*yoyo: true/);
179
+ });