@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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
9
9
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
10
|
-
import { getIframeGsap, queryIframeElement } from "./gsapShared";
|
|
10
|
+
import { getIframeGsap, KEYFRAME_PCT_MATCH, queryIframeElement } from "./gsapShared";
|
|
11
11
|
import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeCompiler";
|
|
12
12
|
|
|
13
13
|
// fallow-ignore-next-line complexity
|
|
@@ -47,52 +47,71 @@ export function findGsapPositionAnimation(
|
|
|
47
47
|
if (animations.length === 0) return null;
|
|
48
48
|
const currentTime = usePlayerStore.getState().currentTime;
|
|
49
49
|
|
|
50
|
-
const scored = animations
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
else if (a.targetSelector.includes(",")) score -= 5;
|
|
58
|
-
const pos = a.resolvedStart ?? (typeof a.position === "number" ? a.position : 0);
|
|
59
|
-
const dur = a.duration ?? 0;
|
|
60
|
-
if (currentTime >= pos - 0.05 && currentTime <= pos + dur + 0.05) score += 50;
|
|
61
|
-
else
|
|
62
|
-
score -= Math.round(
|
|
63
|
-
Math.min(Math.abs(currentTime - pos), Math.abs(currentTime - pos - dur)) * 5,
|
|
64
|
-
);
|
|
65
|
-
return { anim: a, score };
|
|
66
|
-
});
|
|
50
|
+
const scored = animations.filter(animHasPosition).map((a) => {
|
|
51
|
+
let score = 0;
|
|
52
|
+
if (a.keyframes) score += 5;
|
|
53
|
+
if (selector && a.targetSelector === selector) score += 8;
|
|
54
|
+
else if (a.targetSelector.includes(",")) score -= 5;
|
|
55
|
+
return { anim: a, score: score + playheadProximity(a, currentTime) };
|
|
56
|
+
});
|
|
67
57
|
scored.sort((a, b) => b.score - a.score);
|
|
68
|
-
return scored[0]?.anim ??
|
|
58
|
+
return scored[0]?.anim ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function playheadProximity(a: GsapAnimation, currentTime: number): number {
|
|
62
|
+
const pos = a.resolvedStart ?? (typeof a.position === "number" ? a.position : 0);
|
|
63
|
+
const dur = a.duration ?? 0;
|
|
64
|
+
if (currentTime >= pos - 0.05 && currentTime <= pos + dur + 0.05) return 50;
|
|
65
|
+
return -Math.round(Math.min(Math.abs(currentTime - pos), Math.abs(currentTime - pos - dur)) * 5);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Whether the file states `anim`'s value at `time`; a tween's implicit start is GSAP's current value. */
|
|
69
|
+
function statesValueAt(anim: GsapAnimation, time: number): boolean {
|
|
70
|
+
const duration = resolveTweenDuration(anim);
|
|
71
|
+
if (!(duration > 0)) return false;
|
|
72
|
+
const pct = ((time - (resolveTweenStart(anim) ?? 0)) / duration) * 100;
|
|
73
|
+
const near = (keyframe: number) => Math.abs(keyframe - pct) <= KEYFRAME_PCT_MATCH;
|
|
74
|
+
if (anim.keyframes) return anim.keyframes.keyframes.some((kf) => near(kf.percentage));
|
|
75
|
+
if (anim.method === "from") return near(0);
|
|
76
|
+
return near(100) || (anim.method === "fromTo" && near(0));
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
/**
|
|
72
80
|
* From a set of candidate tweens, pick the one whose time range is closest to
|
|
73
81
|
* the current playhead. A tween that *contains* the playhead wins outright;
|
|
74
82
|
* otherwise the nearest endpoint wins. This ensures a drag at t=6s edits (or
|
|
75
|
-
* extends) the 4s tween, not the 1.5s one.
|
|
76
|
-
*
|
|
83
|
+
* extends) the 4s tween, not the 1.5s one. Ties: the one whose file states a value at the
|
|
84
|
+
* playhead (the later one, which renders, if both do), then most keyframes.
|
|
77
85
|
*/
|
|
78
86
|
// fallow-ignore-next-line complexity
|
|
79
87
|
export function pickClosestToPlayhead(anims: GsapAnimation[]): GsapAnimation | null {
|
|
80
88
|
if (anims.length <= 1) return anims[0] ?? null;
|
|
81
89
|
const ct = usePlayerStore.getState().currentTime;
|
|
82
90
|
return anims.reduce((best, a) => {
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const dist = ct >= s && ct <= e ? 0 : Math.min(Math.abs(ct - s), Math.abs(ct - e));
|
|
86
|
-
const bestS = resolveTweenStart(best) ?? 0;
|
|
87
|
-
const bestE = bestS + resolveTweenDuration(best);
|
|
88
|
-
const bestDist =
|
|
89
|
-
ct >= bestS && ct <= bestE ? 0 : Math.min(Math.abs(ct - bestS), Math.abs(ct - bestE));
|
|
91
|
+
const dist = playheadDistance(a, ct);
|
|
92
|
+
const bestDist = playheadDistance(best, ct);
|
|
90
93
|
if (dist < bestDist) return a;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
95
|
-
return a;
|
|
94
|
+
const stated = dist === 0 && bestDist === 0 ? statedValueWinner(a, best, ct) : null;
|
|
95
|
+
if (stated) return stated;
|
|
96
|
+
if (dist === bestDist && keyframeCount(a) > keyframeCount(best)) return a;
|
|
96
97
|
return best;
|
|
97
98
|
});
|
|
98
99
|
}
|
|
100
|
+
|
|
101
|
+
function playheadDistance(anim: GsapAnimation, time: number): number {
|
|
102
|
+
const s = resolveTweenStart(anim) ?? 0;
|
|
103
|
+
const e = s + resolveTweenDuration(anim);
|
|
104
|
+
return time >= s && time <= e ? 0 : Math.min(Math.abs(time - s), Math.abs(time - e));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function statedValueWinner(a: GsapAnimation, b: GsapAnimation, time: number) {
|
|
108
|
+
const [mine, theirs] = [statesValueAt(a, time), statesValueAt(b, time)];
|
|
109
|
+
if (mine !== theirs) return mine ? a : b;
|
|
110
|
+
const [s, bS] = [resolveTweenStart(a) ?? 0, resolveTweenStart(b) ?? 0];
|
|
111
|
+
if (mine && s !== bS) return s > bS ? a : b;
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function keyframeCount(anim: GsapAnimation): number {
|
|
116
|
+
return anim.keyframes?.keyframes.length ?? 0;
|
|
117
|
+
}
|
|
@@ -148,17 +148,18 @@ function positionAnimation(
|
|
|
148
148
|
} as unknown as GsapAnimation;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
/** Every x/y a run wrote, in commit order. */
|
|
151
|
+
/** Every x/y a run wrote AT THE PLAYHEAD (percentage 0 here), in commit order. */
|
|
152
152
|
function persistedPositions(calls: unknown[][]): Array<{ x?: number; y?: number }> {
|
|
153
153
|
const written: Array<{ x?: number; y?: number }> = [];
|
|
154
154
|
for (const call of calls) {
|
|
155
155
|
const mutation = call[1] as {
|
|
156
156
|
properties?: Record<string, number>;
|
|
157
|
-
keyframes?: Array<{ properties: Record<string, number> }>;
|
|
157
|
+
keyframes?: Array<{ percentage: number; properties: Record<string, number> }>;
|
|
158
158
|
x?: number;
|
|
159
159
|
y?: number;
|
|
160
160
|
};
|
|
161
|
-
const
|
|
161
|
+
const atPlayhead = (mutation.keyframes ?? []).filter((k) => k.percentage === 0);
|
|
162
|
+
const sources = [mutation.properties, ...atPlayhead.map((k) => k.properties)];
|
|
162
163
|
if (mutation.x != null || mutation.y != null) written.push({ x: mutation.x, y: mutation.y });
|
|
163
164
|
for (const source of sources) {
|
|
164
165
|
if (source && (source.x != null || source.y != null))
|
|
@@ -23,17 +23,13 @@
|
|
|
23
23
|
* the way through. A sweep across rotations is what tells us it is.
|
|
24
24
|
*/
|
|
25
25
|
import { afterEach, expect, it, vi } from "vitest";
|
|
26
|
-
import { classifyTweenPropertyGroup } from "@hyperframes/core/gsap-parser";
|
|
27
26
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
28
27
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
29
28
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
29
|
+
import { elTween, resetGsapEditState } from "./gsapParsedTween.test-helpers";
|
|
30
30
|
import { tryGsapResizeIntercept } from "./gsapResizeIntercept";
|
|
31
31
|
|
|
32
|
-
afterEach(
|
|
33
|
-
vi.restoreAllMocks();
|
|
34
|
-
usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null });
|
|
35
|
-
document.body.innerHTML = "";
|
|
36
|
-
});
|
|
32
|
+
afterEach(resetGsapEditState);
|
|
37
33
|
|
|
38
34
|
const LAYOUT = { left: 120, top: 520 };
|
|
39
35
|
|
|
@@ -57,20 +53,6 @@ function renderRect(pose: Pose, rotationDeg: number) {
|
|
|
57
53
|
|
|
58
54
|
type Props = Record<string, number>;
|
|
59
55
|
|
|
60
|
-
function tween(id: string, properties: Props, duration: number): GsapAnimation {
|
|
61
|
-
return {
|
|
62
|
-
id,
|
|
63
|
-
targetSelector: "#el",
|
|
64
|
-
propertyGroup: classifyTweenPropertyGroup(properties),
|
|
65
|
-
method: "to",
|
|
66
|
-
properties,
|
|
67
|
-
position: 0,
|
|
68
|
-
resolvedStart: 0,
|
|
69
|
-
duration,
|
|
70
|
-
...(duration === 0 ? { extras: { immediateRender: "__raw:true" } } : {}),
|
|
71
|
-
} as unknown as GsapAnimation;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
56
|
interface Case {
|
|
75
57
|
name: string;
|
|
76
58
|
/** The element's untransformed CSS box. */
|
|
@@ -99,18 +81,24 @@ const ROTATIONS = [0, -8, 45, -47, 90, 180];
|
|
|
99
81
|
* the size routes here however it looks from the animation list.
|
|
100
82
|
*/
|
|
101
83
|
const ROUTES = {
|
|
102
|
-
"scale tween": {
|
|
84
|
+
"scale tween": {
|
|
85
|
+
animations: () => [elTween("#el-scale", { scale: 1 }, 2, "none")],
|
|
86
|
+
settles: true,
|
|
87
|
+
},
|
|
103
88
|
"scale longhands": {
|
|
104
|
-
animations: () => [
|
|
89
|
+
animations: () => [elTween("#el-scale", { scaleX: 1, scaleY: 1 }, 2, "none")],
|
|
105
90
|
settles: true,
|
|
106
91
|
},
|
|
107
|
-
"scale instant hold": {
|
|
92
|
+
"scale instant hold": {
|
|
93
|
+
animations: () => [elTween("#el-scale", { scale: 1 }, 0, "none")],
|
|
94
|
+
settles: false,
|
|
95
|
+
},
|
|
108
96
|
"size tween": {
|
|
109
|
-
animations: () => [
|
|
97
|
+
animations: () => [elTween("#el-size", { width: 630, height: 408 }, 2, "none")],
|
|
110
98
|
settles: false,
|
|
111
99
|
},
|
|
112
100
|
"size instant hold": {
|
|
113
|
-
animations: () => [
|
|
101
|
+
animations: () => [elTween("#el-size", { width: 630, height: 408 }, 0, "none")],
|
|
114
102
|
settles: false,
|
|
115
103
|
},
|
|
116
104
|
} as const;
|
|
@@ -3,12 +3,21 @@ import { afterEach, expect, it, vi } from "vitest";
|
|
|
3
3
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
4
4
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
5
5
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
6
|
+
import { observeGsapGesture } from "./gsapGestureOutcome";
|
|
7
|
+
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
8
|
+
import type { CommitMutation } from "./gsapScriptCommitTypes";
|
|
6
9
|
import { computeCurrentPercentage } from "./gsapDragCommit";
|
|
7
|
-
import { tryGsapResizeIntercept } from "./gsapResizeIntercept";
|
|
10
|
+
import { commitSizeAtPlayhead, tryGsapResizeIntercept } from "./gsapResizeIntercept";
|
|
11
|
+
import { liveTween, previewWith, tween } from "./gsapParsedTween.test-helpers";
|
|
12
|
+
|
|
13
|
+
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
8
14
|
|
|
9
15
|
afterEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
10
17
|
vi.restoreAllMocks();
|
|
11
18
|
usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null });
|
|
19
|
+
// One #clip per test: leftovers would make every tween on it look shared.
|
|
20
|
+
document.body.innerHTML = "";
|
|
12
21
|
});
|
|
13
22
|
|
|
14
23
|
/**
|
|
@@ -36,10 +45,18 @@ function fakeIframe(el: HTMLElement, gsapValues: Record<string, number>) {
|
|
|
36
45
|
// The element's OPACITY intro tween lives on the timeline: unfiltered
|
|
37
46
|
// capture would pick `opacity` up via the other-tween sweep.
|
|
38
47
|
const opacityIntro = { targets: () => [el], vars: { opacity: 0, duration: 0.8 } };
|
|
48
|
+
const scaleIntro = liveTween(
|
|
49
|
+
el,
|
|
50
|
+
{ start: 0.2, duration: 0.8, vars: { scale: 0.9 }, ends: { scaleX: [0.9, 1] } },
|
|
51
|
+
{ from: true },
|
|
52
|
+
);
|
|
39
53
|
return {
|
|
40
54
|
contentWindow: {
|
|
41
|
-
__timelines: { main: { getChildren: () => [opacityIntro] } },
|
|
42
|
-
gsap: {
|
|
55
|
+
__timelines: { main: { getChildren: () => [opacityIntro, scaleIntro] } },
|
|
56
|
+
gsap: {
|
|
57
|
+
getProperty: (_el: Element, prop: string) => gsapValues[prop] ?? 0,
|
|
58
|
+
defaults: () => ({ ease: "power1.out" }),
|
|
59
|
+
},
|
|
43
60
|
},
|
|
44
61
|
contentDocument: document,
|
|
45
62
|
} as unknown as HTMLIFrameElement;
|
|
@@ -289,24 +306,26 @@ async function runResize(
|
|
|
289
306
|
return committed;
|
|
290
307
|
}
|
|
291
308
|
|
|
292
|
-
it("scale-route resize
|
|
309
|
+
it("scale-route resize keyframes scale through the group filter, not width/height", async () => {
|
|
293
310
|
const el = makeGradedElement();
|
|
294
311
|
const iframe = fakeIframe(el, { scale: 1, scaleX: 1, scaleY: 1, opacity: 0, rotation: 0 });
|
|
295
312
|
// uniform: 800/640 === 450/360
|
|
296
313
|
const committed = await runResize(el, iframe, { width: 800, height: 450 });
|
|
297
314
|
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
// Group filter: the opacity intro tween must NOT leak into the
|
|
302
|
-
expect(
|
|
303
|
-
expect(
|
|
315
|
+
const keyframes = committed.find((m) => m.type === "replace-with-keyframes")?.keyframes as
|
|
316
|
+
| Array<{ properties: Record<string, number> }>
|
|
317
|
+
| undefined;
|
|
318
|
+
// Group filter: the opacity intro tween must NOT leak into the scale keyframes.
|
|
319
|
+
expect(keyframes?.some((k) => "opacity" in k.properties)).toBe(false);
|
|
320
|
+
expect(keyframes?.some((k) => "scale" in k.properties)).toBe(true);
|
|
304
321
|
|
|
305
322
|
// Every committed property is scale-group — the resize never writes
|
|
306
323
|
// width/height for a scale-driven element (the double-apply bug class).
|
|
307
324
|
const allProps = committed.flatMap((m) => [
|
|
308
325
|
...Object.keys((m.properties as Record<string, unknown>) ?? {}),
|
|
309
|
-
...
|
|
326
|
+
...((m.keyframes as Array<{ properties: object }>) ?? []).flatMap((k) =>
|
|
327
|
+
Object.keys(k.properties),
|
|
328
|
+
),
|
|
310
329
|
]);
|
|
311
330
|
expect(allProps).not.toContain("width");
|
|
312
331
|
expect(allProps).not.toContain("height");
|
|
@@ -553,3 +572,69 @@ it("hands the size to the element's CSS when its only tween is a fade", async ()
|
|
|
553
572
|
expect(handled).toEqual({ status: "element-size" });
|
|
554
573
|
expect(commitMutation).not.toHaveBeenCalled();
|
|
555
574
|
});
|
|
575
|
+
|
|
576
|
+
it.each([
|
|
577
|
+
["stays put", { x: 0, y: 0 }, { width: 300 }, undefined],
|
|
578
|
+
["moves", { x: -40, y: 0 }, { width: 300, x: 10, y: 0 }, true],
|
|
579
|
+
])(
|
|
580
|
+
"a size written at the playhead carries the anchor only when the box %s",
|
|
581
|
+
async (_, offset, written, owns) => {
|
|
582
|
+
const selection = titleSelection();
|
|
583
|
+
const el = selection.element;
|
|
584
|
+
el.setAttribute("data-hf-drag-gsap-base-x", "50");
|
|
585
|
+
el.setAttribute("data-hf-drag-gsap-base-y", "0");
|
|
586
|
+
const slide = tween({
|
|
587
|
+
id: "#title-to-0",
|
|
588
|
+
targetSelector: "#title",
|
|
589
|
+
propertyGroup: undefined,
|
|
590
|
+
method: "to",
|
|
591
|
+
properties: { x: 100, width: 320 },
|
|
592
|
+
resolvedStart: 0,
|
|
593
|
+
duration: 2,
|
|
594
|
+
ease: "none",
|
|
595
|
+
});
|
|
596
|
+
usePlayerStore.setState({ currentTime: 1 });
|
|
597
|
+
const commitMutation = vi.fn();
|
|
598
|
+
const live = liveTween(el, { start: 0, duration: 2, vars: slide.properties });
|
|
599
|
+
|
|
600
|
+
const outcome = await commitSizeAtPlayhead(
|
|
601
|
+
selection,
|
|
602
|
+
slide,
|
|
603
|
+
{ width: 300 },
|
|
604
|
+
previewWith(el, [live]),
|
|
605
|
+
offset,
|
|
606
|
+
{ commitMutation },
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
expect(outcome.ownsDragOffset).toBe(owns);
|
|
610
|
+
const mutation = commitMutation.mock.calls[0]![1];
|
|
611
|
+
expect(mutation.keyframes[0].properties).toEqual(written);
|
|
612
|
+
},
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
it.each([
|
|
616
|
+
[5, 1],
|
|
617
|
+
[0.5, 1],
|
|
618
|
+
[0.2, 0],
|
|
619
|
+
])("counts scale resize insertion at %ss", async (time, count) => {
|
|
620
|
+
const el = makeGradedElement();
|
|
621
|
+
const iframe = fakeIframe(el, { scale: 1, scaleX: 1, scaleY: 1 });
|
|
622
|
+
usePlayerStore.setState({ currentTime: time, activeKeyframePct: null });
|
|
623
|
+
const writer: CommitMutation = async (_selection, _mutation, options) => {
|
|
624
|
+
options.onResult?.({ ok: true, changed: true });
|
|
625
|
+
};
|
|
626
|
+
const outcome = observeGsapGesture(writer);
|
|
627
|
+
const result = await tryGsapResizeIntercept(
|
|
628
|
+
{ id: "clip", selector: "#clip", element: el } as DomEditSelection,
|
|
629
|
+
{ width: 700, height: 400 },
|
|
630
|
+
[keyframedScaleFixture()],
|
|
631
|
+
iframe,
|
|
632
|
+
outcome.commit!,
|
|
633
|
+
);
|
|
634
|
+
expect(result.status).toBe("persisted");
|
|
635
|
+
outcome.finish();
|
|
636
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(count);
|
|
637
|
+
if (count) expect(trackStudioEvent).toHaveBeenCalledWith("keyframe", { action: "add" });
|
|
638
|
+
el.remove();
|
|
639
|
+
document.querySelector("#__hf_color_grading_clip")?.remove();
|
|
640
|
+
});
|