@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.
- package/dist/assets/{hyperframes-player-aXNJKiIg.js → hyperframes-player-mGYvPVpw.js} +1 -1
- package/dist/assets/{index-Bn2RJNTJ.js → index-BWHt0-ir.js} +1 -1
- package/dist/assets/{index-Dz7doz9l.js → index-DYNaXPVf.js} +203 -203
- package/dist/assets/{index-C9N-DGjC.js → index-Dr3hNpvS.js} +1 -1
- package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
- package/dist/{chunk-BIUXP3PG.js → chunk-3F47EFCT.js} +142 -277
- package/dist/chunk-3F47EFCT.js.map +1 -0
- package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
- package/dist/chunk-5EKWJZLM.js.map +1 -0
- package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
- package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-OL7GLSS4.js} +3 -5
- package/dist/index.d.ts +146 -40
- package/dist/index.html +2 -2
- package/dist/index.js +4021 -3591
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +9 -8
- package/src/App.tsx +2 -2
- package/src/captions/hooks/useCaptionSync.ts +10 -6
- package/src/components/LinkedSelectionToggle.tsx +3 -2
- package/src/components/ProjectUnreachableBanner.tsx +3 -3
- package/src/components/dock/Dock.tsx +5 -7
- package/src/components/editor/CanvasContextMenu.tsx +3 -2
- package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
- package/src/components/editor/DomEditCropHandles.tsx +7 -4
- package/src/components/editor/DomEditOverlay.tsx +5 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -10
- package/src/components/editor/MotionPathOverlay.tsx +7 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
- package/src/components/editor/SnapToolbar.test.tsx +100 -1
- package/src/components/editor/SnapToolbar.tsx +43 -5
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
- package/src/components/editor/audioFxTelemetry.ts +2 -3
- package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
- package/src/components/editor/cropResize.ts +4 -3
- package/src/components/editor/domEditingDom.ts +4 -0
- package/src/components/editor/domEditingLayers.ts +1 -5
- 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/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsParsing.ts +7 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -6
- package/src/components/editor/marqueeCommit.test.tsx +106 -37
- package/src/components/editor/marqueeCommit.ts +24 -8
- package/src/components/editor/motionPathCommit.test.ts +29 -1
- package/src/components/editor/motionPathCommit.ts +21 -9
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/probeSourceElement.test.ts +45 -0
- package/src/components/editor/probeSourceElement.ts +41 -16
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
- package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
- package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
- package/src/components/editor/propertyPanelFxSection.tsx +30 -28
- package/src/components/editor/propertyPanelTypes.ts +6 -3
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useDomEditNudge.test.tsx +37 -0
- package/src/components/editor/useDomEditNudge.ts +2 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
- package/src/components/editor/useDomEditZOrder.ts +12 -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/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
- package/src/components/renders/useFfmpegStatus.ts +2 -1
- package/src/components/renders/useRenderQueue.ts +5 -4
- package/src/components/sidebar/AssetContextMenu.tsx +4 -4
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
- package/src/components/ui/menuStyle.test.ts +61 -0
- package/src/components/ui/menuStyle.ts +10 -0
- package/src/contexts/DomEditContext.tsx +5 -1
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/domEditExports.test.tsx +9 -1
- package/src/hooks/domEditCommitRunner.ts +1 -1
- 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/elementOffsetStager.ts +5 -4
- package/src/hooks/gsapDragCommit.test.ts +75 -164
- package/src/hooks/gsapDragCommit.ts +4 -125
- package/src/hooks/gsapDragPositionCommit.ts +36 -384
- package/src/hooks/gsapEditOutcome.ts +22 -2
- package/src/hooks/gsapGestureOutcome.test.ts +161 -0
- package/src/hooks/gsapGestureOutcome.ts +64 -0
- package/src/hooks/gsapGroupPreflight.ts +34 -0
- package/src/hooks/gsapKeyframeCommit.ts +3 -12
- 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 +96 -11
- package/src/hooks/gsapResizeIntercept.ts +79 -168
- 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/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +13 -0
- package/src/hooks/gsapShared.ts +7 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +45 -57
- 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 +8 -6
- package/src/hooks/newTweenTarget.test.ts +41 -76
- package/src/hooks/plainRotation.ts +3 -2
- package/src/hooks/previewPromotionSelection.test.tsx +1 -1
- package/src/hooks/refreshTimelineRowText.test.ts +65 -0
- package/src/hooks/refreshTimelineRowText.ts +29 -0
- package/src/hooks/timelineTimingSync.test.ts +1 -0
- package/src/hooks/timelineTimingSync.ts +2 -1
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
- package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useBlockCatalog.ts +2 -1
- package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
- package/src/hooks/useCaptionDetection.ts +2 -1
- package/src/hooks/useCompositionContentLoader.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
- package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
- package/src/hooks/useDomEditSession.test.tsx +30 -1
- package/src/hooks/useDomEditSession.ts +10 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +41 -0
- package/src/hooks/useDomEditTextCommits.ts +13 -3
- package/src/hooks/useDomEditWiring.ts +4 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
- package/src/hooks/useDomGeometryCommit.ts +8 -4
- package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
- package/src/hooks/useDomGeometryCommits.ts +3 -2
- package/src/hooks/useDomSelection.test.ts +26 -0
- package/src/hooks/useDomSelection.ts +10 -3
- package/src/hooks/useDomSelectionTypes.ts +6 -1
- 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 +34 -3
- package/src/hooks/useEnableKeyframes.ts +7 -4
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +121 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +35 -8
- 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.ts +3 -2
- package/src/hooks/useGestureCommit.test.tsx +225 -55
- package/src/hooks/useGestureCommit.ts +86 -55
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +28 -35
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +101 -195
- package/src/hooks/useGsapAwareGroupMove.ts +179 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
- package/src/hooks/useGsapKeyframeOps.ts +36 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +60 -1
- package/src/hooks/useGsapScriptCommits.ts +15 -10
- package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
- package/src/hooks/useGsapSelectionHandlers.ts +24 -18
- package/src/hooks/useGsapTweenCache.ts +18 -12
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +277 -1
- package/src/hooks/usePersistentEditHistory.ts +47 -8
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
- package/src/hooks/usePreviewInteraction.test.ts +21 -0
- package/src/hooks/usePreviewInteraction.ts +2 -0
- 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/useRenderClipContent.test.ts +18 -0
- package/src/hooks/useRenderClipContent.ts +4 -5
- 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.test.tsx +30 -22
- package/src/hooks/useTimelineDeleteOps.ts +22 -27
- package/src/hooks/useTimelineEditing.test.tsx +1 -1
- package/src/index.ts +4 -0
- package/src/player/components/AudioWaveform.tsx +3 -2
- package/src/player/components/AutomationSelectionMenu.tsx +3 -2
- package/src/player/components/ClipContextMenu.test.tsx +73 -0
- package/src/player/components/ClipContextMenu.tsx +33 -31
- package/src/player/components/CompositionThumbnail.tsx +2 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
- package/src/player/components/OutOfSyncBadge.tsx +5 -5
- package/src/player/components/TextClipContent.test.ts +19 -0
- package/src/player/components/TextClipContent.tsx +27 -0
- package/src/player/components/TrackGapContextMenu.tsx +3 -6
- package/src/player/components/clipMenuNormalize.ts +2 -1
- package/src/player/components/clipMenuToolItems.test.tsx +23 -0
- package/src/player/components/clipMenuToolItems.tsx +13 -9
- package/src/player/components/clipPeakMap.ts +2 -1
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
- package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
- package/src/player/components/timelineClipGestureHandlers.ts +20 -2
- package/src/player/components/timelineEditing.ts +3 -1
- package/src/player/components/timelineLinkSelection.ts +35 -1
- package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
- package/src/player/components/useTimelineClipDrag.ts +6 -7
- package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
- package/src/player/hooks/timelinePlayerSync.ts +6 -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/lib/timelineElementHelpers.test.ts +87 -1
- package/src/player/lib/timelineElementHelpers.ts +6 -15
- package/src/player/lib/timelineText.ts +40 -0
- package/src/player/sceneSwap.ts +2 -1
- package/src/player/store/timelineElement.ts +12 -0
- package/src/styles/theme.css +3 -0
- 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.ts +2 -3
- package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
- package/src/utils/gsapUndoRestore.ts +3 -6
- package/src/utils/keyframeUsage.ts +79 -0
- package/src/utils/previewFeatureUsage.test.ts +32 -0
- package/src/utils/previewFeatureUsage.ts +46 -0
- package/src/utils/previewPrefetch.ts +2 -1
- package/src/utils/razorSplitTransaction.ts +12 -8
- package/src/utils/softReloadTargets.ts +1 -3
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +4 -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.test.ts +107 -0
- package/src/utils/studioTelemetry.ts +51 -5
- package/src/utils/timelineCompositionInsert.ts +3 -2
- package/src/webmcp/StudioAgentTools.tsx +4 -3
- package/dist/chunk-BIUXP3PG.js.map +0 -1
- package/dist/chunk-K47E2ZJA.js.map +0 -1
- /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
- /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
|
+
});
|