@hyperframes/studio 0.7.55 → 0.7.56
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/{index-BXaqaVKt.js → index-BWnOxAiH.js} +1 -1
- package/dist/assets/{index-CPetwHFV.js → index-C4csZims.js} +1 -1
- package/dist/assets/index-CmVCjZjp.js +423 -0
- package/dist/assets/index-D-GyYi2d.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +146 -144
- package/dist/index.html +2 -2
- package/dist/index.js +40414 -37680
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +158 -126
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +43 -1
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/nle/AssetPreviewOverlay.tsx +147 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +240 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.tsx +326 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +39 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +12 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +34 -1
- package/src/hooks/timelineEditingHelpers.ts +22 -48
- package/src/hooks/timelineMoveAdapter.test.ts +77 -0
- package/src/hooks/timelineMoveAdapter.ts +37 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +154 -1
- package/src/hooks/useDomEditCommits.ts +100 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +258 -0
- package/src/hooks/useElementLifecycleOps.ts +40 -71
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +148 -18
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineEditing.test.tsx +20 -29
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +18 -4
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayheadIndicator.tsx +50 -5
- package/src/player/components/Timeline.test.ts +102 -19
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +207 -521
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +943 -0
- package/src/player/components/timelineClipDragCommit.ts +401 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +42 -2
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +120 -61
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineStackingSync.test.tsx +67 -0
- package/src/player/components/useTimelineStackingSync.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.ts +48 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/index.ts +3 -2
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +12 -81
- package/src/player/lib/timelineElementHelpers.ts +54 -5
- package/src/player/lib/timelineIframeHelpers.ts +2 -0
- package/src/player/store/playerStore.ts +62 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.ts +62 -0
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -3,6 +3,7 @@ import { usePlayerStore, liveTime, type TimelineElement } from "../store/playerS
|
|
|
3
3
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
4
4
|
import { usePlaybackKeyboard } from "./usePlaybackKeyboard";
|
|
5
5
|
import { useTimelineSyncCallbacks } from "./useTimelineSyncCallbacks";
|
|
6
|
+
import { useTimelinePlayerLoop } from "./useTimelinePlayerLoop";
|
|
6
7
|
|
|
7
8
|
export type { ClipManifestClip } from "../lib/playbackTypes";
|
|
8
9
|
export { createStaticSeekPlaybackAdapter } from "../lib/playbackAdapter";
|
|
@@ -15,7 +16,6 @@ export {
|
|
|
15
16
|
parseTimelineFromDOM,
|
|
16
17
|
readTimelineDurationFromDocument,
|
|
17
18
|
resolveStandaloneRootCompositionSrc,
|
|
18
|
-
resolveIframe,
|
|
19
19
|
} from "../lib/timelineDOM";
|
|
20
20
|
export {
|
|
21
21
|
shouldIgnorePlaybackShortcutEvent,
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
mergeTimelineElementsPreservingDowngrades,
|
|
37
37
|
parseTimelineFromDOM,
|
|
38
38
|
} from "../lib/timelineDOM";
|
|
39
|
+
import { normalizeToZones } from "../components/timelineZones";
|
|
39
40
|
import {
|
|
40
41
|
setPreviewMediaMuted,
|
|
41
42
|
setPreviewPlaybackRate,
|
|
@@ -90,12 +91,17 @@ export function useTimelinePlayer() {
|
|
|
90
91
|
// applyCachedSourceDurations re-applies the cached probe duration: re-derived
|
|
91
92
|
// elements (e.g. after a clip move) can arrive without sourceDuration, which
|
|
92
93
|
// otherwise makes trimmed waveforms lose their window.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
// Enforced CapCut zoning (overlay → main → audio): normalize track indices
|
|
95
|
+
// on every discovery. Idempotent — already-zoned input is returned as-is, so
|
|
96
|
+
// drops persist zoned indices and reloads re-zone to the same (no drift).
|
|
97
|
+
const mergedElements = normalizeToZones(
|
|
98
|
+
applyCachedSourceDurations(
|
|
99
|
+
mergeTimelineElementsPreservingDowngrades(
|
|
100
|
+
state.elements,
|
|
101
|
+
elements,
|
|
102
|
+
state.duration,
|
|
103
|
+
resolvedDuration,
|
|
104
|
+
),
|
|
99
105
|
),
|
|
100
106
|
);
|
|
101
107
|
|
|
@@ -207,49 +213,14 @@ export function useTimelinePlayer() {
|
|
|
207
213
|
}
|
|
208
214
|
}, []);
|
|
209
215
|
|
|
210
|
-
const stopReverseLoop =
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const adapter = getAdapter();
|
|
218
|
-
if (adapter) {
|
|
219
|
-
const rawTime = adapter.getTime();
|
|
220
|
-
const dur = adapter.getDuration();
|
|
221
|
-
const time = dur > 0 ? Math.min(rawTime, dur) : rawTime;
|
|
222
|
-
liveTime.notify(time); // direct DOM updates, no React re-render
|
|
223
|
-
const { inPoint, outPoint } = usePlayerStore.getState();
|
|
224
|
-
const rawLoopEnd = outPoint !== null ? Math.min(outPoint, dur) : dur;
|
|
225
|
-
const rawLoopStart = inPoint !== null ? inPoint : 0;
|
|
226
|
-
const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : dur;
|
|
227
|
-
const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0;
|
|
228
|
-
if (time >= loopEnd) {
|
|
229
|
-
if (usePlayerStore.getState().loopEnabled && dur > 0) {
|
|
230
|
-
// keepPlaying skips the adapter's implicit pause; play() below is then a no-op.
|
|
231
|
-
adapter.seek(loopStart, { keepPlaying: true });
|
|
232
|
-
liveTime.notify(loopStart);
|
|
233
|
-
adapter.play();
|
|
234
|
-
setIsPlaying(true);
|
|
235
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (adapter.isPlaying()) adapter.pause();
|
|
239
|
-
setCurrentTime(time); // sync Zustand once at end
|
|
240
|
-
setIsPlaying(false);
|
|
241
|
-
cancelAnimationFrame(rafRef.current);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
246
|
-
};
|
|
247
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
248
|
-
}, [getAdapter, setCurrentTime, setIsPlaying]);
|
|
216
|
+
const { startRAFLoop, stopRAFLoop, stopReverseLoop } = useTimelinePlayerLoop({
|
|
217
|
+
rafRef,
|
|
218
|
+
reverseRafRef,
|
|
219
|
+
getAdapter,
|
|
220
|
+
setCurrentTime,
|
|
221
|
+
setIsPlaying,
|
|
222
|
+
});
|
|
249
223
|
|
|
250
|
-
const stopRAFLoop = useCallback(() => {
|
|
251
|
-
cancelAnimationFrame(rafRef.current);
|
|
252
|
-
}, []);
|
|
253
224
|
const applyPlaybackRate = useCallback((rate: number) => {
|
|
254
225
|
const iframe = iframeRef.current;
|
|
255
226
|
if (!iframe) return;
|
|
@@ -463,10 +434,24 @@ export function useTimelinePlayer() {
|
|
|
463
434
|
applyPreviewAudioState,
|
|
464
435
|
});
|
|
465
436
|
const saveSeekPosition = useCallback(() => {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
437
|
+
// Never DEGRADE the saved position. Overlapping reloads (e.g. an external
|
|
438
|
+
// file drop = upload reload + insert reload back-to-back) call this while
|
|
439
|
+
// the iframe from the FIRST reload is mid-teardown: getAdapter() can still
|
|
440
|
+
// return that dying document's adapter, whose getTime() reads 0 — and the
|
|
441
|
+
// store's currentTime can lag the visual playhead. Overwriting the
|
|
442
|
+
// still-unconsumed pendingSeek with either value is exactly how the
|
|
443
|
+
// playhead used to end up at 0 after a Finder drop (verified live via a
|
|
444
|
+
// currentTime write-trace). So: while a refresh is already in flight and a
|
|
445
|
+
// save exists, keep it; otherwise trust the live adapter, then the store.
|
|
446
|
+
const refreshInFlight = isRefreshingRef.current && pendingSeekRef.current != null;
|
|
447
|
+
if (!refreshInFlight) {
|
|
448
|
+
const adapter = getAdapter();
|
|
449
|
+
if (adapter) {
|
|
450
|
+
pendingSeekRef.current = adapter.getTime();
|
|
451
|
+
} else if (pendingSeekRef.current == null) {
|
|
452
|
+
pendingSeekRef.current = usePlayerStore.getState().currentTime ?? 0;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
470
455
|
isRefreshingRef.current = true;
|
|
471
456
|
stopRAFLoop();
|
|
472
457
|
stopReverseLoop();
|
|
@@ -476,6 +461,16 @@ export function useTimelinePlayer() {
|
|
|
476
461
|
const iframe = iframeRef.current;
|
|
477
462
|
if (!iframe) return;
|
|
478
463
|
saveSeekPosition();
|
|
464
|
+
// Hide the iframe across the full reload so the user never sees the reloading
|
|
465
|
+
// document's RAW DOM (every clip stacked and visible) in the window between the
|
|
466
|
+
// new document parsing and the runtime initializing + seeking. initializeAdapter
|
|
467
|
+
// reveals it again right after its restore seek renders the correct frame.
|
|
468
|
+
// Tradeoff: this shows the parent stage background (a brief "freeze"/blank, on
|
|
469
|
+
// the order of the reload time ~100-300ms) INSTEAD of the all-clips flash. A
|
|
470
|
+
// blank is far less jarring than a burst of every asset appearing at once.
|
|
471
|
+
// Only the FULL-reload edits (drops/inserts) hit this — timing edits now take
|
|
472
|
+
// the soft-reload path and never touch refreshPlayer.
|
|
473
|
+
iframe.style.visibility = "hidden";
|
|
479
474
|
const src = iframe.src;
|
|
480
475
|
const url = new URL(src, window.location.origin);
|
|
481
476
|
url.searchParams.set("_t", String(Date.now()));
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The forward playback loop for the timeline player.
|
|
3
|
+
*
|
|
4
|
+
* Owns the three requestAnimationFrame lifecycle callbacks that drive (and stop)
|
|
5
|
+
* playback:
|
|
6
|
+
* - startRAFLoop — the forward tick: advance liveTime, honour in/out loop
|
|
7
|
+
* points + loopEnabled, and pause + sync the store at the end.
|
|
8
|
+
* - stopRAFLoop — cancel the forward tick.
|
|
9
|
+
* - stopReverseLoop — cancel the reverse-shuttle tick (owned by the parent hook).
|
|
10
|
+
*
|
|
11
|
+
* Called unconditionally at the top level of useTimelinePlayer so its useCallback
|
|
12
|
+
* hooks run in a stable order; every dependency is passed in as an argument.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback } from "react";
|
|
16
|
+
import { liveTime, usePlayerStore } from "../store/playerStore";
|
|
17
|
+
import type { PlaybackAdapter } from "../lib/playbackTypes";
|
|
18
|
+
|
|
19
|
+
interface UseTimelinePlayerLoopParams {
|
|
20
|
+
rafRef: React.MutableRefObject<number>;
|
|
21
|
+
reverseRafRef: React.MutableRefObject<number>;
|
|
22
|
+
getAdapter: () => PlaybackAdapter | null;
|
|
23
|
+
setCurrentTime: (v: number) => void;
|
|
24
|
+
setIsPlaying: (v: boolean) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface UseTimelinePlayerLoopResult {
|
|
28
|
+
startRAFLoop: () => void;
|
|
29
|
+
stopRAFLoop: () => void;
|
|
30
|
+
stopReverseLoop: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useTimelinePlayerLoop({
|
|
34
|
+
rafRef,
|
|
35
|
+
reverseRafRef,
|
|
36
|
+
getAdapter,
|
|
37
|
+
setCurrentTime,
|
|
38
|
+
setIsPlaying,
|
|
39
|
+
}: UseTimelinePlayerLoopParams): UseTimelinePlayerLoopResult {
|
|
40
|
+
const stopReverseLoop = useCallback(() => {
|
|
41
|
+
cancelAnimationFrame(reverseRafRef.current);
|
|
42
|
+
}, [reverseRafRef]);
|
|
43
|
+
|
|
44
|
+
const startRAFLoop = useCallback(() => {
|
|
45
|
+
// fallow-ignore-next-line complexity
|
|
46
|
+
const tick = () => {
|
|
47
|
+
const adapter = getAdapter();
|
|
48
|
+
if (adapter) {
|
|
49
|
+
const rawTime = adapter.getTime();
|
|
50
|
+
const dur = adapter.getDuration();
|
|
51
|
+
const time = dur > 0 ? Math.min(rawTime, dur) : rawTime;
|
|
52
|
+
liveTime.notify(time); // direct DOM updates, no React re-render
|
|
53
|
+
const { inPoint, outPoint } = usePlayerStore.getState();
|
|
54
|
+
const rawLoopEnd = outPoint !== null ? Math.min(outPoint, dur) : dur;
|
|
55
|
+
const rawLoopStart = inPoint !== null ? inPoint : 0;
|
|
56
|
+
const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : dur;
|
|
57
|
+
const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0;
|
|
58
|
+
if (time >= loopEnd) {
|
|
59
|
+
if (usePlayerStore.getState().loopEnabled && dur > 0) {
|
|
60
|
+
// keepPlaying skips the adapter's implicit pause; play() below is then a no-op.
|
|
61
|
+
adapter.seek(loopStart, { keepPlaying: true });
|
|
62
|
+
liveTime.notify(loopStart);
|
|
63
|
+
adapter.play();
|
|
64
|
+
setIsPlaying(true);
|
|
65
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (adapter.isPlaying()) adapter.pause();
|
|
69
|
+
setCurrentTime(time); // sync Zustand once at end
|
|
70
|
+
setIsPlaying(false);
|
|
71
|
+
cancelAnimationFrame(rafRef.current);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
76
|
+
};
|
|
77
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
78
|
+
}, [rafRef, getAdapter, setCurrentTime, setIsPlaying]);
|
|
79
|
+
|
|
80
|
+
const stopRAFLoop = useCallback(() => {
|
|
81
|
+
cancelAnimationFrame(rafRef.current);
|
|
82
|
+
}, [rafRef]);
|
|
83
|
+
|
|
84
|
+
return { startRAFLoop, stopRAFLoop, stopReverseLoop };
|
|
85
|
+
}
|
package/src/player/index.ts
CHANGED
|
@@ -6,11 +6,12 @@ export { VideoThumbnail } from "./components/VideoThumbnail";
|
|
|
6
6
|
export { CompositionThumbnail } from "./components/CompositionThumbnail";
|
|
7
7
|
|
|
8
8
|
// Hooks
|
|
9
|
-
export { useTimelinePlayer
|
|
9
|
+
export { useTimelinePlayer } from "./hooks/useTimelinePlayer";
|
|
10
|
+
export { resolveIframe } from "./lib/timelineDOM";
|
|
10
11
|
|
|
11
12
|
// Store
|
|
12
13
|
export { usePlayerStore, liveTime } from "./store/playerStore";
|
|
13
|
-
export type {
|
|
14
|
+
export type { SelectElementOptions, TimelineElement } from "./store/playerStore";
|
|
14
15
|
|
|
15
16
|
// Utils
|
|
16
17
|
export { formatTime } from "./lib/time";
|
|
@@ -3,26 +3,26 @@ import { formatFrameTime, frameToSeconds, secondsToFrame, stepFrameTime, formatT
|
|
|
3
3
|
|
|
4
4
|
describe("formatTime", () => {
|
|
5
5
|
it("formats zero seconds", () => {
|
|
6
|
-
expect(formatTime(0)).toBe("
|
|
6
|
+
expect(formatTime(0)).toBe("00:00");
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// fallow-ignore-next-line code-duplication
|
|
10
10
|
it("formats seconds less than a minute", () => {
|
|
11
|
-
expect(formatTime(5)).toBe("
|
|
12
|
-
expect(formatTime(30)).toBe("
|
|
13
|
-
expect(formatTime(59)).toBe("
|
|
11
|
+
expect(formatTime(5)).toBe("00:05");
|
|
12
|
+
expect(formatTime(30)).toBe("00:30");
|
|
13
|
+
expect(formatTime(59)).toBe("00:59");
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
it("formats exact minutes", () => {
|
|
17
|
-
expect(formatTime(60)).toBe("
|
|
18
|
-
expect(formatTime(120)).toBe("
|
|
17
|
+
expect(formatTime(60)).toBe("01:00");
|
|
18
|
+
expect(formatTime(120)).toBe("02:00");
|
|
19
19
|
expect(formatTime(600)).toBe("10:00");
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it("formats minutes and seconds", () => {
|
|
23
|
-
expect(formatTime(65)).toBe("
|
|
24
|
-
expect(formatTime(90)).toBe("
|
|
25
|
-
expect(formatTime(125)).toBe("
|
|
23
|
+
expect(formatTime(65)).toBe("01:05");
|
|
24
|
+
expect(formatTime(90)).toBe("01:30");
|
|
25
|
+
expect(formatTime(125)).toBe("02:05");
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
it("formats large values (over an hour)", () => {
|
|
@@ -32,28 +32,28 @@ describe("formatTime", () => {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
it("floors fractional seconds", () => {
|
|
35
|
-
expect(formatTime(0.9)).toBe("
|
|
36
|
-
expect(formatTime(1.5)).toBe("
|
|
37
|
-
expect(formatTime(59.99)).toBe("
|
|
38
|
-
expect(formatTime(60.5)).toBe("
|
|
35
|
+
expect(formatTime(0.9)).toBe("00:00");
|
|
36
|
+
expect(formatTime(1.5)).toBe("00:01");
|
|
37
|
+
expect(formatTime(59.99)).toBe("00:59");
|
|
38
|
+
expect(formatTime(60.5)).toBe("01:00");
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
it("pads single-digit seconds with leading
|
|
42
|
-
expect(formatTime(1)).toBe("
|
|
43
|
-
expect(formatTime(61)).toBe("
|
|
41
|
+
it("pads single-digit minutes and seconds with leading zeros", () => {
|
|
42
|
+
expect(formatTime(1)).toBe("00:01");
|
|
43
|
+
expect(formatTime(61)).toBe("01:01");
|
|
44
44
|
expect(formatTime(609)).toBe("10:09");
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
it("guards against negative values", () => {
|
|
48
|
-
expect(formatTime(-1)).toBe("
|
|
48
|
+
expect(formatTime(-1)).toBe("00:00");
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
it("guards against NaN", () => {
|
|
52
|
-
expect(formatTime(NaN)).toBe("
|
|
52
|
+
expect(formatTime(NaN)).toBe("00:00");
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
it("guards against Infinity", () => {
|
|
56
|
-
expect(formatTime(Infinity)).toBe("
|
|
56
|
+
expect(formatTime(Infinity)).toBe("00:00");
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
|
package/src/player/lib/time.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
export const STUDIO_PREVIEW_FPS = 30;
|
|
2
2
|
|
|
3
3
|
export function formatTime(time: number): string {
|
|
4
|
-
if (!Number.isFinite(time) || time < 0) return "
|
|
4
|
+
if (!Number.isFinite(time) || time < 0) return "00:00";
|
|
5
5
|
const mins = Math.floor(time / 60);
|
|
6
6
|
const secs = Math.floor(time % 60);
|
|
7
|
-
|
|
7
|
+
// Zero-pad minutes as well as seconds so every readout is a stable-width
|
|
8
|
+
// MM:SS (e.g. "00:44", not "0:44"). Minutes past 99 keep their extra digits
|
|
9
|
+
// ("120:00"); an hours-style H:MM:SS grouping is applied by callers that need
|
|
10
|
+
// it (see formatTimelineTickLabel), never here.
|
|
11
|
+
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
export function secondsToFrame(time: number, fps = STUDIO_PREVIEW_FPS): number {
|
|
@@ -4,8 +4,13 @@ import {
|
|
|
4
4
|
createTimelineElementFromManifestClip,
|
|
5
5
|
parseTimelineFromDOM,
|
|
6
6
|
createImplicitTimelineLayersFromDOM,
|
|
7
|
-
|
|
7
|
+
mergeTimelineElementsPreservingDowngrades,
|
|
8
8
|
} from "./timelineDOM";
|
|
9
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
10
|
+
|
|
11
|
+
function el(id: string, extra: Partial<TimelineElement> = {}): TimelineElement {
|
|
12
|
+
return { id, tag: "img", start: 0, duration: 5, track: 0, ...extra };
|
|
13
|
+
}
|
|
9
14
|
|
|
10
15
|
function makeDoc(html: string): Document {
|
|
11
16
|
const d = document.implementation.createHTMLDocument();
|
|
@@ -13,29 +18,6 @@ function makeDoc(html: string): Document {
|
|
|
13
18
|
return d;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
function makeLiveDoc(html: string): Document {
|
|
17
|
-
document.head.innerHTML = "";
|
|
18
|
-
document.body.innerHTML = html;
|
|
19
|
-
return document;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function mockComputedZIndex(doc: Document, zIndexById: ReadonlyMap<string, string>): void {
|
|
23
|
-
const win = doc.defaultView;
|
|
24
|
-
if (!win) throw new Error("Expected document window");
|
|
25
|
-
const original = win.getComputedStyle.bind(win);
|
|
26
|
-
Object.defineProperty(win, "getComputedStyle", {
|
|
27
|
-
configurable: true,
|
|
28
|
-
value: (element: Element, pseudoElt?: string | null) => {
|
|
29
|
-
const style = original(element, pseudoElt);
|
|
30
|
-
const zIndex = zIndexById.get(element.id);
|
|
31
|
-
if (zIndex != null) {
|
|
32
|
-
Object.defineProperty(style, "zIndex", { configurable: true, value: zIndex });
|
|
33
|
-
}
|
|
34
|
-
return style;
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
21
|
describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
|
|
40
22
|
it("harvests hfId from a data-start element that has data-hf-id", () => {
|
|
41
23
|
const doc = makeDoc(`
|
|
@@ -126,58 +108,6 @@ describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
|
|
|
126
108
|
|
|
127
109
|
expect(element.hidden).toBe(true);
|
|
128
110
|
});
|
|
129
|
-
|
|
130
|
-
it("captures the effective z-index from the live element, not the runtime inline-only value", () => {
|
|
131
|
-
// The runtime reports inline-only z-index (0 for CSS-rule authored z-index),
|
|
132
|
-
// which must NOT override the live element's effective z-index — otherwise
|
|
133
|
-
// the timeline collapses every CSS-styled clip to a z=0 tie and mis-orders.
|
|
134
|
-
const doc = makeDoc(`
|
|
135
|
-
<div data-composition-id="root">
|
|
136
|
-
<div id="hero" class="clip" data-start="0" data-duration="5" style="z-index: 30"></div>
|
|
137
|
-
</div>
|
|
138
|
-
`);
|
|
139
|
-
const hostEl = doc.getElementById("hero");
|
|
140
|
-
|
|
141
|
-
const element = createTimelineElementFromManifestClip({
|
|
142
|
-
clip: {
|
|
143
|
-
id: "hero",
|
|
144
|
-
label: "Hero",
|
|
145
|
-
kind: "element",
|
|
146
|
-
tagName: "div",
|
|
147
|
-
start: 0,
|
|
148
|
-
duration: 5,
|
|
149
|
-
track: 0,
|
|
150
|
-
zIndex: 0,
|
|
151
|
-
compositionId: null,
|
|
152
|
-
parentCompositionId: null,
|
|
153
|
-
compositionSrc: null,
|
|
154
|
-
assetUrl: null,
|
|
155
|
-
},
|
|
156
|
-
fallbackIndex: 0,
|
|
157
|
-
doc,
|
|
158
|
-
hostEl,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
expect(element.zIndex).toBe(30);
|
|
162
|
-
expect(element.hasExplicitZIndex).toBe(true);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("marks parsed inline, CSS-rule, and auto z-index authorship accurately", () => {
|
|
166
|
-
const doc = makeLiveDoc(`
|
|
167
|
-
<div data-composition-id="root">
|
|
168
|
-
<div id="inline" class="clip" data-start="0" data-duration="2" style="z-index: 3"></div>
|
|
169
|
-
<div id="rule" class="clip" data-start="0" data-duration="2"></div>
|
|
170
|
-
<div id="auto" class="clip" data-start="0" data-duration="2"></div>
|
|
171
|
-
</div>
|
|
172
|
-
`);
|
|
173
|
-
mockComputedZIndex(doc, new Map([["rule", "12"]]));
|
|
174
|
-
|
|
175
|
-
const elements = parseTimelineFromDOM(doc, 10);
|
|
176
|
-
|
|
177
|
-
expect(elements.find((el) => el.id === "inline")?.hasExplicitZIndex).toBe(true);
|
|
178
|
-
expect(elements.find((el) => el.id === "rule")?.hasExplicitZIndex).toBe(true);
|
|
179
|
-
expect(elements.find((el) => el.id === "auto")?.hasExplicitZIndex).toBe(false);
|
|
180
|
-
});
|
|
181
111
|
});
|
|
182
112
|
|
|
183
113
|
describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => {
|
|
@@ -211,31 +141,28 @@ describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => {
|
|
|
211
141
|
|
|
212
142
|
expect(layers).toEqual([]);
|
|
213
143
|
});
|
|
144
|
+
});
|
|
214
145
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
mockComputedZIndex(doc, new Map([["layer", "8"]]));
|
|
222
|
-
|
|
223
|
-
const layers = createImplicitTimelineLayersFromDOM(doc, 10);
|
|
224
|
-
|
|
225
|
-
expect(layers[0]?.zIndex).toBe(8);
|
|
226
|
-
expect(layers[0]?.hasExplicitZIndex).toBe(true);
|
|
146
|
+
describe("mergeTimelineElementsPreservingDowngrades — genuine removal vs transient downgrade", () => {
|
|
147
|
+
it("drops a removed TOP-LEVEL element (undo of a split) instead of ghosting it", () => {
|
|
148
|
+
const current = [el("a"), el("a-split")]; // post-split store: original + clone
|
|
149
|
+
const next = [el("a")]; // fresh scan of the reverted file: clone gone
|
|
150
|
+
const merged = mergeTimelineElementsPreservingDowngrades(current, next, 30, 30);
|
|
151
|
+
expect(merged.map((e) => e.id)).toEqual(["a"]);
|
|
227
152
|
});
|
|
228
|
-
});
|
|
229
153
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
iframeSrc: "/preview/comp/index.html",
|
|
237
|
-
});
|
|
154
|
+
it("still preserves an enriched sub-composition child a bare re-scan drops", () => {
|
|
155
|
+
const current = [el("a"), el("sub-child", { compositionSrc: "sub.html" })];
|
|
156
|
+
const next = [el("a")]; // bare DOM scan misses the enriched sub-comp child
|
|
157
|
+
const merged = mergeTimelineElementsPreservingDowngrades(current, next, 30, 30);
|
|
158
|
+
expect(merged.map((e) => e.id).sort()).toEqual(["a", "sub-child"]);
|
|
159
|
+
});
|
|
238
160
|
|
|
239
|
-
|
|
161
|
+
it("trusts the fresh scan fully when it is not shorter", () => {
|
|
162
|
+
const current = [el("a"), el("b", { compositionSrc: "sub.html" })];
|
|
163
|
+
const next = [el("a"), el("c")];
|
|
164
|
+
expect(
|
|
165
|
+
mergeTimelineElementsPreservingDowngrades(current, next, 30, 30).map((e) => e.id),
|
|
166
|
+
).toEqual(["a", "c"]);
|
|
240
167
|
});
|
|
241
168
|
});
|