@hyperframes/studio 0.6.97 → 0.6.98
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-DgsMQSvV.js +418 -0
- package/dist/assets/index-B62bDCQv.css +1 -0
- package/dist/assets/index-Ce3pBm_I.js +252 -0
- package/dist/assets/{index-HveJ0MuV.js → index-D-ET9M0b.js} +1 -1
- package/dist/assets/index-D-bS9Dxx.js +1 -0
- package/dist/index.html +2 -2
- package/package.json +7 -5
- package/src/App.tsx +182 -177
- package/src/captions/store.ts +11 -11
- package/src/components/StudioHeader.tsx +4 -4
- package/src/components/StudioLeftSidebar.tsx +2 -2
- package/src/components/StudioPreviewArea.tsx +225 -183
- package/src/components/StudioRightPanel.tsx +3 -3
- package/src/components/TimelineToolbar.tsx +25 -0
- package/src/components/editor/DomEditOverlay.tsx +2 -5
- package/src/components/editor/EaseCurveSection.tsx +2 -3
- package/src/components/editor/GestureTrailOverlay.tsx +4 -3
- package/src/components/editor/LayersPanel.tsx +3 -9
- package/src/components/editor/PropertyPanel.tsx +20 -61
- package/src/components/editor/colorValue.ts +3 -1
- package/src/components/editor/domEditOverlayGestures.ts +54 -1
- package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
- package/src/components/editor/gradientValue.ts +3 -3
- package/src/components/editor/keyframeMove.test.ts +101 -0
- package/src/components/editor/keyframeMove.ts +151 -0
- package/src/components/editor/manualEditsDom.ts +0 -12
- package/src/components/editor/propertyPanelHelpers.ts +10 -38
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -5
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -6
- package/src/components/editor/propertyPanelTransformCommit.ts +129 -0
- package/src/components/editor/studioMotionOps.test.ts +1 -1
- package/src/components/editor/studioMotionOps.ts +2 -1
- package/src/components/editor/useDomEditOverlayGestures.ts +1 -46
- package/src/components/nle/NLELayout.tsx +1 -24
- package/src/components/sidebar/BlocksTab.tsx +2 -2
- package/src/contexts/DomEditContext.tsx +134 -31
- package/src/contexts/StudioContext.tsx +90 -40
- package/src/contexts/TimelineEditContext.tsx +47 -0
- package/src/hooks/domEditCommitTypes.ts +14 -0
- package/src/hooks/gsapDragCommit.ts +9 -24
- package/src/hooks/gsapKeyframeCacheHelpers.ts +2 -1
- package/src/hooks/gsapKeyframeCommit.ts +5 -15
- package/src/hooks/gsapRuntimeBridge.ts +18 -52
- package/src/hooks/gsapRuntimeKeyframes.ts +8 -57
- package/src/hooks/gsapRuntimeReaders.ts +19 -26
- package/src/hooks/gsapScriptCommitHelpers.ts +1 -11
- package/src/hooks/gsapScriptCommitTypes.ts +58 -0
- package/src/hooks/gsapShared.ts +157 -0
- package/src/hooks/timelineEditingHelpers.ts +63 -2
- package/src/hooks/useAnimatedPropertyCommit.ts +3 -25
- package/src/hooks/useAppHotkeys.ts +299 -377
- package/src/hooks/useConsoleErrorCapture.ts +33 -5
- package/src/hooks/useDomEditCommits.ts +35 -293
- package/src/hooks/useDomEditPositionPatchCommit.ts +1 -1
- package/src/hooks/useDomEditSession.ts +78 -249
- package/src/hooks/useDomEditTextCommits.ts +1 -1
- package/src/hooks/useDomEditWiring.ts +255 -0
- package/src/hooks/useDomGeometryCommits.ts +181 -0
- package/src/hooks/useDomSelection.ts +10 -27
- package/src/hooks/useEditorSave.ts +82 -0
- package/src/hooks/useElementLifecycleOps.ts +177 -0
- package/src/hooks/useEnableKeyframes.ts +10 -15
- package/src/hooks/useFileManager.ts +32 -114
- package/src/hooks/useFileTree.ts +80 -0
- package/src/hooks/useGestureCommit.ts +7 -5
- package/src/hooks/useGestureRecording.ts +1 -1
- package/src/hooks/useGsapAnimationOps.ts +122 -0
- package/src/hooks/useGsapArcPathOps.ts +61 -0
- package/src/hooks/useGsapAwareEditing.ts +242 -0
- package/src/hooks/useGsapKeyframeOps.ts +167 -0
- package/src/hooks/useGsapPropertyDebounce.ts +135 -0
- package/src/hooks/useGsapScriptCommits.ts +58 -570
- package/src/hooks/useGsapSelectionHandlers.ts +22 -9
- package/src/hooks/useGsapTweenCache.ts +35 -29
- package/src/hooks/useLintModal.ts +7 -0
- package/src/hooks/useMusicBeatAnalysis.ts +152 -0
- package/src/hooks/useRazorSplit.ts +1 -1
- package/src/hooks/useRenderClipContent.ts +46 -21
- package/src/hooks/useTimelineEditing.ts +48 -4
- package/src/player/components/AudioWaveform.tsx +29 -4
- package/src/player/components/BeatStrip.tsx +166 -0
- package/src/player/components/Timeline.tsx +39 -18
- package/src/player/components/TimelineCanvas.tsx +52 -12
- package/src/player/components/TimelineClipDiamonds.tsx +130 -20
- package/src/player/components/TimelinePropertyRows.tsx +8 -2
- package/src/player/components/TimelineRuler.tsx +36 -2
- package/src/player/components/timelineEditing.ts +30 -5
- package/src/player/components/useTimelineClipDrag.ts +155 -4
- package/src/player/components/useTimelinePlayhead.ts +30 -1
- package/src/player/hooks/useTimelinePlayer.ts +47 -45
- package/src/player/lib/mediaProbe.ts +46 -3
- package/src/player/lib/playbackScrub.ts +16 -0
- package/src/player/lib/timelineDOM.ts +10 -2
- package/src/player/lib/timelineIframeHelpers.ts +89 -0
- package/src/player/store/playerStore.ts +92 -33
- package/src/utils/beatEditActions.ts +109 -0
- package/src/utils/beatEditing.ts +136 -0
- package/src/utils/clipboardPayload.ts +3 -2
- package/src/utils/compositionPatterns.ts +2 -0
- package/src/utils/keyframeSelection.test.ts +45 -0
- package/src/utils/keyframeSelection.ts +29 -0
- package/src/utils/rounding.ts +9 -0
- package/src/utils/studioHelpers.ts +5 -2
- package/src/utils/studioUrlState.ts +2 -1
- package/src/utils/timelineAssetDrop.ts +6 -5
- package/src/utils/timelineInspector.ts +15 -100
- package/dist/assets/hyperframes-player-Daj5djxa.js +0 -418
- package/dist/assets/index-B0twsRu0.css +0 -1
- package/dist/assets/index-Cfye9xzo.js +0 -251
- package/src/components/editor/DopesheetStrip.tsx +0 -141
- package/src/components/editor/StaggerControls.tsx +0 -61
- package/src/components/editor/TimelineLayerPanel.test.ts +0 -42
- package/src/components/editor/TimelineLayerPanel.tsx +0 -15
- package/src/components/nle/TimelineEditorNotice.tsx +0 -133
- package/src/hooks/gsapRuntimePreview.ts +0 -19
- package/src/player/components/timelineUtils.ts +0 -211
- package/src/utils/audioBeatDetection.ts +0 -58
- package/src/utils/keyframeSnapping.test.ts +0 -74
- package/src/utils/keyframeSnapping.ts +0 -63
- package/src/utils/timelineInspector.test.ts +0 -79
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
STUDIO_MANUAL_EDITING_DISABLED_TITLE,
|
|
6
6
|
} from "./editor/manualEditingAvailability";
|
|
7
7
|
import { getHistoryShortcutLabel } from "../utils/studioHelpers";
|
|
8
|
-
import {
|
|
8
|
+
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
9
9
|
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
10
|
-
import {
|
|
10
|
+
import { useDomEditActionsContext } from "../contexts/DomEditContext";
|
|
11
11
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
12
12
|
|
|
13
13
|
export interface StudioHeaderProps {
|
|
@@ -150,9 +150,9 @@ export function StudioHeader({
|
|
|
150
150
|
inspectorPanelActive,
|
|
151
151
|
onExport,
|
|
152
152
|
}: StudioHeaderProps) {
|
|
153
|
-
const { projectId, editHistory, handleUndo, handleRedo } =
|
|
153
|
+
const { projectId, editHistory, handleUndo, handleRedo } = useStudioShellContext();
|
|
154
154
|
const { rightCollapsed, setRightCollapsed, setRightPanelTab } = usePanelLayoutContext();
|
|
155
|
-
const { clearDomSelection } =
|
|
155
|
+
const { clearDomSelection } = useDomEditActionsContext();
|
|
156
156
|
|
|
157
157
|
return (
|
|
158
158
|
<div className="flex items-center justify-between h-10 px-3 bg-neutral-900 border-b border-neutral-800 flex-shrink-0">
|
|
@@ -4,7 +4,7 @@ import { LeftSidebar, type LeftSidebarHandle } from "./sidebar/LeftSidebar";
|
|
|
4
4
|
import { MediaPreview } from "./MediaPreview";
|
|
5
5
|
import { isMediaFile } from "../utils/mediaTypes";
|
|
6
6
|
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
7
|
-
import {
|
|
7
|
+
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
8
8
|
import { useFileManagerContext } from "../contexts/FileManagerContext";
|
|
9
9
|
import { getPersistedRenderSettings } from "./renders/renderSettings";
|
|
10
10
|
import type { BlockPreviewInfo } from "./sidebar/BlocksTab";
|
|
@@ -39,7 +39,7 @@ export function StudioLeftSidebar({
|
|
|
39
39
|
handlePanelResizeMove,
|
|
40
40
|
handlePanelResizeEnd,
|
|
41
41
|
} = usePanelLayoutContext();
|
|
42
|
-
const { projectId, renderQueue, waitForPendingDomEditSaves } =
|
|
42
|
+
const { projectId, renderQueue, waitForPendingDomEditSaves } = useStudioShellContext();
|
|
43
43
|
const {
|
|
44
44
|
compositions,
|
|
45
45
|
assets,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, type ReactNode } from "react";
|
|
1
|
+
import { useState, useMemo, type ReactNode } from "react";
|
|
2
2
|
import { NLELayout } from "./nle/NLELayout";
|
|
3
3
|
import { CaptionOverlay } from "../captions/components/CaptionOverlay";
|
|
4
4
|
import { CaptionTimeline } from "../captions/components/CaptionTimeline";
|
|
@@ -13,10 +13,13 @@ import {
|
|
|
13
13
|
STUDIO_PREVIEW_MANUAL_EDITING_ENABLED,
|
|
14
14
|
STUDIO_PREVIEW_SELECTION_ENABLED,
|
|
15
15
|
} from "./editor/manualEditingAvailability";
|
|
16
|
-
import {
|
|
16
|
+
import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
|
|
17
17
|
import { useDomEditContext } from "../contexts/DomEditContext";
|
|
18
|
+
import { TimelineEditProvider } from "../contexts/TimelineEditContext";
|
|
18
19
|
import type { BlockPreviewInfo } from "./sidebar/BlocksTab";
|
|
19
20
|
import { readStudioUiPreferences } from "../utils/studioUiPreferences";
|
|
21
|
+
import { fetchParsedAnimations } from "../hooks/useGsapTweenCache";
|
|
22
|
+
import { pickKeyframeTween, computeKeyframeMovePlan } from "./editor/keyframeMove";
|
|
20
23
|
import type { GestureRecordingState } from "./editor/GestureRecordControl";
|
|
21
24
|
|
|
22
25
|
export interface StudioPreviewAreaProps {
|
|
@@ -91,18 +94,20 @@ export function StudioPreviewArea({
|
|
|
91
94
|
}: StudioPreviewAreaProps) {
|
|
92
95
|
const {
|
|
93
96
|
projectId,
|
|
94
|
-
refreshKey,
|
|
95
97
|
activeCompPath,
|
|
96
98
|
setActiveCompPath,
|
|
97
|
-
captionEditMode,
|
|
98
|
-
compositionLoading,
|
|
99
|
-
isPlaying,
|
|
100
99
|
previewIframeRef,
|
|
101
|
-
refreshPreviewDocumentVersion,
|
|
102
100
|
handlePreviewIframeRef,
|
|
103
101
|
timelineVisible,
|
|
104
102
|
toggleTimelineVisibility,
|
|
105
|
-
} =
|
|
103
|
+
} = useStudioShellContext();
|
|
104
|
+
const {
|
|
105
|
+
refreshKey,
|
|
106
|
+
captionEditMode,
|
|
107
|
+
compositionLoading,
|
|
108
|
+
isPlaying,
|
|
109
|
+
refreshPreviewDocumentVersion,
|
|
110
|
+
} = useStudioPlaybackContext();
|
|
106
111
|
|
|
107
112
|
const {
|
|
108
113
|
domEditHoverSelection,
|
|
@@ -125,6 +130,7 @@ export function StudioPreviewArea({
|
|
|
125
130
|
handleGsapAddKeyframe,
|
|
126
131
|
handleGsapConvertToKeyframes,
|
|
127
132
|
handleGsapDeleteAllForElement,
|
|
133
|
+
buildDomSelectionForTimelineElement,
|
|
128
134
|
} = useDomEditContext();
|
|
129
135
|
|
|
130
136
|
const [snapPrefs, setSnapPrefs] = useState(() => {
|
|
@@ -137,187 +143,223 @@ export function StudioPreviewArea({
|
|
|
137
143
|
};
|
|
138
144
|
});
|
|
139
145
|
|
|
146
|
+
// fallow-ignore-next-line complexity
|
|
147
|
+
const timelineEditCallbacks = useMemo(
|
|
148
|
+
() => ({
|
|
149
|
+
onMoveElement: handleTimelineElementMove,
|
|
150
|
+
onResizeElement: handleTimelineElementResize,
|
|
151
|
+
onBlockedEditAttempt: handleBlockedTimelineEdit,
|
|
152
|
+
onSplitElement: handleTimelineElementSplit,
|
|
153
|
+
onRazorSplit: handleRazorSplit,
|
|
154
|
+
onRazorSplitAll: handleRazorSplitAll,
|
|
155
|
+
onDeleteAllKeyframes: (elId: string) => {
|
|
156
|
+
const rawId = elId.includes("#") ? (elId.split("#").pop() ?? elId) : elId;
|
|
157
|
+
handleGsapDeleteAllForElement(`#${rawId}`);
|
|
158
|
+
},
|
|
159
|
+
onDeleteKeyframe: (_elId: string, pct: number) => {
|
|
160
|
+
const cacheKey = domEditSelection?.id ?? "";
|
|
161
|
+
const cached = usePlayerStore.getState().keyframeCache.get(cacheKey);
|
|
162
|
+
const kf = cached?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.2);
|
|
163
|
+
const group = kf?.propertyGroup;
|
|
164
|
+
const anim =
|
|
165
|
+
(group ? selectedGsapAnimations.find((a) => a.propertyGroup === group) : undefined) ??
|
|
166
|
+
selectedGsapAnimations.find((a) => a.keyframes);
|
|
167
|
+
if (!anim) return;
|
|
168
|
+
handleGsapRemoveKeyframe(anim.id, kf?.tweenPercentage ?? pct);
|
|
169
|
+
},
|
|
170
|
+
onChangeKeyframeEase: (_elId: string, _pct: number, ease: string) => {
|
|
171
|
+
for (const anim of selectedGsapAnimations) {
|
|
172
|
+
if (anim.keyframes) handleGsapUpdateMeta(anim.id, { ease });
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
// fallow-ignore-next-line complexity
|
|
176
|
+
onMoveKeyframe: async (_el: TimelineElement, oldPct: number, newPct: number) => {
|
|
177
|
+
// Resolve the dragged element's selection + parsed animations on demand
|
|
178
|
+
// (both awaited and cached) rather than relying on the async DOM-edit
|
|
179
|
+
// session being loaded for this element — that coupling made the commit
|
|
180
|
+
// intermittently no-op (revert) when dragging before the session caught up.
|
|
181
|
+
if (!projectId) return;
|
|
182
|
+
const sourceFile = _el.sourceFile || activeCompPath || "index.html";
|
|
183
|
+
const [selection, parsed] = await Promise.all([
|
|
184
|
+
buildDomSelectionForTimelineElement(_el),
|
|
185
|
+
fetchParsedAnimations(projectId, sourceFile),
|
|
186
|
+
]);
|
|
187
|
+
if (!selection || !parsed) return;
|
|
188
|
+
|
|
189
|
+
const cached = usePlayerStore.getState().keyframeCache.get(_el.key ?? _el.id);
|
|
190
|
+
const cachedKf = cached?.keyframes.find((k) => Math.abs(k.percentage - oldPct) < 0.2);
|
|
191
|
+
const origAbsTime = _el.start + (oldPct / 100) * _el.duration;
|
|
192
|
+
const anim = pickKeyframeTween(
|
|
193
|
+
parsed.animations,
|
|
194
|
+
_el,
|
|
195
|
+
origAbsTime,
|
|
196
|
+
cachedKf?.propertyGroup,
|
|
197
|
+
);
|
|
198
|
+
if (!anim) return;
|
|
199
|
+
|
|
200
|
+
const plan = computeKeyframeMovePlan(
|
|
201
|
+
anim,
|
|
202
|
+
cachedKf?.tweenPercentage ?? oldPct,
|
|
203
|
+
_el,
|
|
204
|
+
newPct,
|
|
205
|
+
);
|
|
206
|
+
if (plan.meta) handleGsapUpdateMeta(anim.id, plan.meta, selection);
|
|
207
|
+
for (const pct of plan.removes) handleGsapRemoveKeyframe(anim.id, pct, selection);
|
|
208
|
+
for (const add of plan.adds) {
|
|
209
|
+
for (const [prop, val] of Object.entries(add.properties)) {
|
|
210
|
+
handleGsapAddKeyframe(anim.id, add.pct, prop, val, selection);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
onToggleKeyframeAtPlayhead: (el: TimelineElement) => {
|
|
215
|
+
const currentTime = usePlayerStore.getState().currentTime;
|
|
216
|
+
const pct =
|
|
217
|
+
el.duration > 0
|
|
218
|
+
? Math.max(0, Math.min(100, Math.round(((currentTime - el.start) / el.duration) * 100)))
|
|
219
|
+
: 0;
|
|
220
|
+
const anim = selectedGsapAnimations.find((a) => a.keyframes);
|
|
221
|
+
if (anim?.keyframes) {
|
|
222
|
+
const existing = anim.keyframes.keyframes.find((k) => Math.abs(k.percentage - pct) <= 1);
|
|
223
|
+
if (existing) {
|
|
224
|
+
handleGsapRemoveKeyframe(anim.id, existing.percentage);
|
|
225
|
+
} else {
|
|
226
|
+
handleGsapAddKeyframe(anim.id, pct, "x", 0);
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
const flatAnim = selectedGsapAnimations.find((a) => !a.keyframes);
|
|
230
|
+
if (flatAnim) handleGsapConvertToKeyframes(flatAnim.id);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
235
|
+
[
|
|
236
|
+
handleTimelineElementMove,
|
|
237
|
+
handleTimelineElementResize,
|
|
238
|
+
handleBlockedTimelineEdit,
|
|
239
|
+
handleTimelineElementSplit,
|
|
240
|
+
handleRazorSplit,
|
|
241
|
+
handleRazorSplitAll,
|
|
242
|
+
handleGsapDeleteAllForElement,
|
|
243
|
+
domEditSelection?.id,
|
|
244
|
+
selectedGsapAnimations,
|
|
245
|
+
handleGsapRemoveKeyframe,
|
|
246
|
+
handleGsapUpdateMeta,
|
|
247
|
+
handleGsapAddKeyframe,
|
|
248
|
+
handleGsapConvertToKeyframes,
|
|
249
|
+
buildDomSelectionForTimelineElement,
|
|
250
|
+
projectId,
|
|
251
|
+
activeCompPath,
|
|
252
|
+
],
|
|
253
|
+
);
|
|
254
|
+
|
|
140
255
|
return (
|
|
141
256
|
<div className="flex-1 flex flex-col relative min-w-0">
|
|
142
257
|
<div className="flex-1 min-h-0 relative">
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const cacheKey = domEditSelection?.id ?? "";
|
|
167
|
-
const cached = usePlayerStore.getState().keyframeCache.get(cacheKey);
|
|
168
|
-
const kf = cached?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.2);
|
|
169
|
-
const group = kf?.propertyGroup;
|
|
170
|
-
const anim =
|
|
171
|
-
(group ? selectedGsapAnimations.find((a) => a.propertyGroup === group) : undefined) ??
|
|
172
|
-
selectedGsapAnimations.find((a) => a.keyframes);
|
|
173
|
-
if (!anim) return;
|
|
174
|
-
handleGsapRemoveKeyframe(anim.id, kf?.tweenPercentage ?? pct);
|
|
175
|
-
}}
|
|
176
|
-
onChangeKeyframeEase={(_elId, _pct, ease) => {
|
|
177
|
-
for (const anim of selectedGsapAnimations) {
|
|
178
|
-
if (anim.keyframes) handleGsapUpdateMeta(anim.id, { ease });
|
|
179
|
-
}
|
|
180
|
-
}}
|
|
181
|
-
// fallow-ignore-next-line complexity
|
|
182
|
-
onMoveKeyframe={(_el, oldPct, newPct) => {
|
|
183
|
-
const cacheKey = domEditSelection?.id ?? "";
|
|
184
|
-
const cached = usePlayerStore.getState().keyframeCache.get(cacheKey);
|
|
185
|
-
const cachedKf = cached?.keyframes.find((k) => Math.abs(k.percentage - oldPct) < 0.2);
|
|
186
|
-
const group = cachedKf?.propertyGroup;
|
|
187
|
-
const anim =
|
|
188
|
-
(group ? selectedGsapAnimations.find((a) => a.propertyGroup === group) : undefined) ??
|
|
189
|
-
selectedGsapAnimations.find((a) => a.keyframes);
|
|
190
|
-
if (!anim?.keyframes) return;
|
|
191
|
-
const tweenOldPct = cachedKf?.tweenPercentage ?? oldPct;
|
|
192
|
-
const kf = anim.keyframes.keyframes.find(
|
|
193
|
-
(k) => Math.abs(k.percentage - tweenOldPct) < 0.2,
|
|
194
|
-
);
|
|
195
|
-
if (!kf) return;
|
|
196
|
-
const tweenStart = anim.resolvedStart ?? 0;
|
|
197
|
-
const tweenDur = anim.duration ?? 1;
|
|
198
|
-
const newAbsTime = _el.start + (newPct / 100) * _el.duration;
|
|
199
|
-
const tweenNewPct =
|
|
200
|
-
tweenDur > 0
|
|
201
|
-
? Math.max(
|
|
202
|
-
0,
|
|
203
|
-
Math.min(100, Math.round(((newAbsTime - tweenStart) / tweenDur) * 1000) / 10),
|
|
204
|
-
)
|
|
205
|
-
: 0;
|
|
206
|
-
handleGsapRemoveKeyframe(anim.id, tweenOldPct);
|
|
207
|
-
for (const [prop, val] of Object.entries(kf.properties)) {
|
|
208
|
-
handleGsapAddKeyframe(anim.id, tweenNewPct, prop, val);
|
|
209
|
-
}
|
|
210
|
-
}}
|
|
211
|
-
onToggleKeyframeAtPlayhead={(el) => {
|
|
212
|
-
const currentTime = usePlayerStore.getState().currentTime;
|
|
213
|
-
const pct =
|
|
214
|
-
el.duration > 0
|
|
215
|
-
? Math.max(
|
|
216
|
-
0,
|
|
217
|
-
Math.min(100, Math.round(((currentTime - el.start) / el.duration) * 100)),
|
|
218
|
-
)
|
|
219
|
-
: 0;
|
|
220
|
-
const anim = selectedGsapAnimations.find((a) => a.keyframes);
|
|
221
|
-
if (anim?.keyframes) {
|
|
222
|
-
const existing = anim.keyframes.keyframes.find(
|
|
223
|
-
(k) => Math.abs(k.percentage - pct) <= 1,
|
|
224
|
-
);
|
|
225
|
-
if (existing) {
|
|
226
|
-
handleGsapRemoveKeyframe(anim.id, existing.percentage);
|
|
227
|
-
} else {
|
|
228
|
-
handleGsapAddKeyframe(anim.id, pct, "x", 0);
|
|
258
|
+
<TimelineEditProvider value={timelineEditCallbacks}>
|
|
259
|
+
<NLELayout
|
|
260
|
+
projectId={projectId}
|
|
261
|
+
refreshKey={refreshKey}
|
|
262
|
+
activeCompositionPath={activeCompPath}
|
|
263
|
+
timelineToolbar={timelineToolbar}
|
|
264
|
+
renderClipContent={renderClipContent}
|
|
265
|
+
onDeleteElement={handleTimelineElementDelete}
|
|
266
|
+
onAssetDrop={handleTimelineAssetDrop}
|
|
267
|
+
onBlockDrop={handleTimelineBlockDrop}
|
|
268
|
+
onPreviewBlockDrop={handlePreviewBlockDrop}
|
|
269
|
+
onFileDrop={handleTimelineFileDrop}
|
|
270
|
+
onSelectTimelineElement={handleTimelineElementSelect}
|
|
271
|
+
onCompIdToSrcChange={setCompIdToSrc}
|
|
272
|
+
onCompositionLoadingChange={setCompositionLoading}
|
|
273
|
+
onCompositionChange={(compPath) => {
|
|
274
|
+
// Sync activeCompPath when user drills down via timeline double-click
|
|
275
|
+
// or navigates back via breadcrumb — keeps sidebar + thumbnails in sync.
|
|
276
|
+
// Guard against no-op updates to prevent circular refresh cascades
|
|
277
|
+
// between activeCompPath → compositionStack → onCompositionChange.
|
|
278
|
+
if (compPath !== activeCompPath) {
|
|
279
|
+
setActiveCompPath(compPath);
|
|
280
|
+
refreshPreviewDocumentVersion();
|
|
229
281
|
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
282
|
+
}}
|
|
283
|
+
onIframeRef={handlePreviewIframeRef}
|
|
284
|
+
previewOverlay={
|
|
285
|
+
blockPreview ? (
|
|
286
|
+
<div className="absolute inset-0 z-30 bg-black pointer-events-none">
|
|
287
|
+
{blockPreview.videoUrl ? (
|
|
288
|
+
<video
|
|
289
|
+
src={blockPreview.videoUrl}
|
|
290
|
+
autoPlay
|
|
291
|
+
muted
|
|
292
|
+
loop
|
|
293
|
+
playsInline
|
|
294
|
+
className="w-full h-full object-contain"
|
|
295
|
+
/>
|
|
296
|
+
) : blockPreview.posterUrl ? (
|
|
297
|
+
<img
|
|
298
|
+
src={blockPreview.posterUrl}
|
|
299
|
+
alt={blockPreview.title}
|
|
300
|
+
className="w-full h-full object-contain"
|
|
301
|
+
/>
|
|
302
|
+
) : null}
|
|
303
|
+
</div>
|
|
304
|
+
) : captionEditMode ? (
|
|
305
|
+
<CaptionOverlay iframeRef={previewIframeRef} />
|
|
306
|
+
) : STUDIO_INSPECTOR_PANELS_ENABLED ? (
|
|
307
|
+
<>
|
|
308
|
+
<DomEditOverlay
|
|
309
|
+
iframeRef={previewIframeRef}
|
|
310
|
+
activeCompositionPath={activeCompPath}
|
|
311
|
+
hoverSelection={
|
|
312
|
+
STUDIO_PREVIEW_SELECTION_ENABLED &&
|
|
313
|
+
!captionEditMode &&
|
|
314
|
+
!compositionLoading &&
|
|
315
|
+
!isPlaying
|
|
316
|
+
? domEditHoverSelection
|
|
317
|
+
: null
|
|
318
|
+
}
|
|
319
|
+
selection={shouldShowSelectedDomBounds ? domEditSelection : null}
|
|
320
|
+
groupSelections={shouldShowSelectedDomBounds ? domEditGroupSelections : []}
|
|
321
|
+
allowCanvasMovement={
|
|
322
|
+
STUDIO_PREVIEW_MANUAL_EDITING_ENABLED && !isGestureRecording
|
|
323
|
+
}
|
|
324
|
+
onCanvasMouseDown={handlePreviewCanvasMouseDown}
|
|
325
|
+
onCanvasPointerMove={handlePreviewCanvasPointerMove}
|
|
326
|
+
onCanvasPointerLeave={handlePreviewCanvasPointerLeave}
|
|
327
|
+
onSelectionChange={applyDomSelection}
|
|
328
|
+
onBlockedMove={handleBlockedDomMove}
|
|
329
|
+
onManualDragStart={handleDomManualDragStart}
|
|
330
|
+
onPathOffsetCommit={handleDomPathOffsetCommit}
|
|
331
|
+
onGroupPathOffsetCommit={handleDomGroupPathOffsetCommit}
|
|
332
|
+
onBoxSizeCommit={handleDomBoxSizeCommit}
|
|
333
|
+
onRotationCommit={handleDomRotationCommit}
|
|
334
|
+
gridVisible={snapPrefs.gridVisible}
|
|
335
|
+
gridSpacing={snapPrefs.gridSpacing}
|
|
336
|
+
recordingState={recordingState}
|
|
337
|
+
onToggleRecording={onToggleRecording}
|
|
265
338
|
/>
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
selection={shouldShowSelectedDomBounds ? domEditSelection : null}
|
|
284
|
-
groupSelections={shouldShowSelectedDomBounds ? domEditGroupSelections : []}
|
|
285
|
-
allowCanvasMovement={STUDIO_PREVIEW_MANUAL_EDITING_ENABLED && !isGestureRecording}
|
|
286
|
-
onCanvasMouseDown={handlePreviewCanvasMouseDown}
|
|
287
|
-
onCanvasPointerMove={handlePreviewCanvasPointerMove}
|
|
288
|
-
onCanvasPointerLeave={handlePreviewCanvasPointerLeave}
|
|
289
|
-
onSelectionChange={applyDomSelection}
|
|
290
|
-
onBlockedMove={handleBlockedDomMove}
|
|
291
|
-
onManualDragStart={handleDomManualDragStart}
|
|
292
|
-
onPathOffsetCommit={handleDomPathOffsetCommit}
|
|
293
|
-
onGroupPathOffsetCommit={handleDomGroupPathOffsetCommit}
|
|
294
|
-
onBoxSizeCommit={handleDomBoxSizeCommit}
|
|
295
|
-
onRotationCommit={handleDomRotationCommit}
|
|
296
|
-
gridVisible={snapPrefs.gridVisible}
|
|
297
|
-
gridSpacing={snapPrefs.gridSpacing}
|
|
298
|
-
recordingState={recordingState}
|
|
299
|
-
onToggleRecording={onToggleRecording}
|
|
300
|
-
/>
|
|
301
|
-
<SnapToolbar onSnapChange={setSnapPrefs} />
|
|
302
|
-
{gestureOverlay}
|
|
303
|
-
</>
|
|
304
|
-
) : null
|
|
305
|
-
}
|
|
306
|
-
timelineFooter={
|
|
307
|
-
captionEditMode ? (
|
|
308
|
-
<div className="border-t border-neutral-800/30 flex-shrink-0" style={{ height: 60 }}>
|
|
309
|
-
<div className="flex items-center gap-1.5 px-2 py-0.5">
|
|
310
|
-
<span className="text-[9px] font-medium text-neutral-500 uppercase tracking-wider">
|
|
311
|
-
Captions
|
|
312
|
-
</span>
|
|
339
|
+
<SnapToolbar onSnapChange={setSnapPrefs} />
|
|
340
|
+
{gestureOverlay}
|
|
341
|
+
</>
|
|
342
|
+
) : null
|
|
343
|
+
}
|
|
344
|
+
timelineFooter={
|
|
345
|
+
captionEditMode ? (
|
|
346
|
+
<div
|
|
347
|
+
className="border-t border-neutral-800/30 flex-shrink-0"
|
|
348
|
+
style={{ height: 60 }}
|
|
349
|
+
>
|
|
350
|
+
<div className="flex items-center gap-1.5 px-2 py-0.5">
|
|
351
|
+
<span className="text-[9px] font-medium text-neutral-500 uppercase tracking-wider">
|
|
352
|
+
Captions
|
|
353
|
+
</span>
|
|
354
|
+
</div>
|
|
355
|
+
<CaptionTimeline pixelsPerSecond={100} />
|
|
313
356
|
</div>
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
/>
|
|
357
|
+
) : undefined
|
|
358
|
+
}
|
|
359
|
+
timelineVisible={timelineVisible}
|
|
360
|
+
onToggleTimeline={toggleTimelineVisibility}
|
|
361
|
+
/>
|
|
362
|
+
</TimelineEditProvider>
|
|
321
363
|
</div>
|
|
322
364
|
<StudioFeedbackBar />
|
|
323
365
|
</div>
|
|
@@ -8,7 +8,7 @@ import type { RenderJob } from "./renders/useRenderQueue";
|
|
|
8
8
|
import type { BlockParam } from "@hyperframes/core/registry";
|
|
9
9
|
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "./editor/manualEditingAvailability";
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
|
|
12
12
|
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
13
13
|
import { useFileManagerContext } from "../contexts/FileManagerContext";
|
|
14
14
|
import { useDomEditContext } from "../contexts/DomEditContext";
|
|
@@ -47,14 +47,14 @@ export function StudioRightPanel({
|
|
|
47
47
|
} = usePanelLayoutContext();
|
|
48
48
|
|
|
49
49
|
const {
|
|
50
|
-
captionEditMode,
|
|
51
50
|
previewIframeRef,
|
|
52
51
|
projectId,
|
|
53
52
|
activeCompPath,
|
|
54
53
|
compositionDimensions,
|
|
55
54
|
waitForPendingDomEditSaves,
|
|
56
55
|
renderQueue,
|
|
57
|
-
} =
|
|
56
|
+
} = useStudioShellContext();
|
|
57
|
+
const { captionEditMode } = useStudioPlaybackContext();
|
|
58
58
|
|
|
59
59
|
const {
|
|
60
60
|
domEditSelection,
|
|
@@ -16,6 +16,7 @@ import { Scissors } from "../icons/SystemIcons";
|
|
|
16
16
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
17
17
|
import type { DomEditSelection } from "./editor/domEditingTypes";
|
|
18
18
|
import { canSplitElement } from "../utils/timelineElementSplit";
|
|
19
|
+
import { canAddBeatAt, addBeatAtCompositionTime } from "../utils/beatEditActions";
|
|
19
20
|
|
|
20
21
|
interface DomEditSessionSlice extends EnableKeyframesSession {
|
|
21
22
|
domEditSelection: DomEditSelection | null;
|
|
@@ -70,6 +71,9 @@ export function TimelineToolbar({
|
|
|
70
71
|
}: TimelineToolbarProps) {
|
|
71
72
|
const activeTool = usePlayerStore((s) => s.activeTool);
|
|
72
73
|
const setActiveTool = usePlayerStore((s) => s.setActiveTool);
|
|
74
|
+
// Subscribe so the add-beat button reacts to playhead movement and analysis load.
|
|
75
|
+
const currentTime = usePlayerStore((s) => s.currentTime);
|
|
76
|
+
const beatAnalysisReady = usePlayerStore((s) => s.beatAnalysis !== null);
|
|
73
77
|
const { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent } = useTimelineZoom();
|
|
74
78
|
const displayedTimelineZoomPercent = getTimelineZoomPercent(zoomMode, manualZoomPercent);
|
|
75
79
|
const { state: keyframeState, onToggle: onToggleKeyframe } = useKeyframeToggle(domEditSession);
|
|
@@ -178,6 +182,27 @@ export function TimelineToolbar({
|
|
|
178
182
|
</Tooltip>
|
|
179
183
|
);
|
|
180
184
|
})()}
|
|
185
|
+
{beatAnalysisReady &&
|
|
186
|
+
canAddBeatAt(currentTime) &&
|
|
187
|
+
(() => (
|
|
188
|
+
<Tooltip label="Add beat at playhead">
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
onClick={() => addBeatAtCompositionTime(currentTime)}
|
|
192
|
+
className="flex h-7 w-7 items-center justify-center rounded text-neutral-500 transition-colors hover:text-[#22c55e]"
|
|
193
|
+
>
|
|
194
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
|
195
|
+
<path
|
|
196
|
+
d="M21 10C21 12.2091 16.9706 14 12 14M21 10C21 7.79086 16.9706 6 12 6C7.02944 6 3 7.79086 3 10M21 10V16C21 18.2091 16.9706 20 12 20M12 14C7.02944 14 3 12.2091 3 10M12 14V20M3 10V16C3 18.2091 7.02944 20 12 20M7 19.3264V13.3264M17 19.3264V13.3264M12 10L20 4"
|
|
197
|
+
stroke="currentColor"
|
|
198
|
+
strokeWidth="2"
|
|
199
|
+
strokeLinecap="round"
|
|
200
|
+
strokeLinejoin="round"
|
|
201
|
+
/>
|
|
202
|
+
</svg>
|
|
203
|
+
</button>
|
|
204
|
+
</Tooltip>
|
|
205
|
+
))()}
|
|
181
206
|
</div>
|
|
182
207
|
<div className="flex items-center gap-1">
|
|
183
208
|
<Tooltip label="Fit timeline to width">
|
|
@@ -4,6 +4,7 @@ import { type DomEditSelection } from "./domEditing";
|
|
|
4
4
|
import { resolveDomEditGroupOverlayRect } from "./domEditOverlayGeometry";
|
|
5
5
|
import {
|
|
6
6
|
type BlockedMoveState,
|
|
7
|
+
type DomEditGroupPathOffsetCommit,
|
|
7
8
|
type FocusableDomEditOverlay,
|
|
8
9
|
type GestureState,
|
|
9
10
|
type GroupGestureState,
|
|
@@ -27,11 +28,7 @@ export {
|
|
|
27
28
|
resolveDomEditResizeGesture,
|
|
28
29
|
resolveDomEditRotationGesture,
|
|
29
30
|
} from "./domEditOverlayGestures";
|
|
30
|
-
|
|
31
|
-
export interface DomEditGroupPathOffsetCommit {
|
|
32
|
-
selection: DomEditSelection;
|
|
33
|
-
next: { x: number; y: number };
|
|
34
|
-
}
|
|
31
|
+
export type { DomEditGroupPathOffsetCommit } from "./domEditOverlayGestures";
|
|
35
32
|
|
|
36
33
|
interface DomEditOverlayProps {
|
|
37
34
|
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { memo, useCallback, useRef, useState } from "react";
|
|
2
2
|
import { EASE_CURVES, EASE_LABELS, parseCustomEaseFromString } from "./gsapAnimationConstants";
|
|
3
|
+
import { roundToCenti } from "../../utils/rounding";
|
|
3
4
|
|
|
4
5
|
const PRESET_GRID_EASES = [
|
|
5
6
|
"none",
|
|
@@ -75,9 +76,7 @@ const EasePresetGrid = memo(function EasePresetGrid({
|
|
|
75
76
|
);
|
|
76
77
|
});
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
return Math.round(n * 100) / 100;
|
|
80
|
-
}
|
|
79
|
+
const round2 = roundToCenti;
|
|
81
80
|
|
|
82
81
|
export function EaseCurveSection({
|
|
83
82
|
ease,
|
|
@@ -6,7 +6,7 @@ interface GestureTrailOverlayProps {
|
|
|
6
6
|
sampleCount?: number;
|
|
7
7
|
trail?: Array<{ x: number; y: number }>;
|
|
8
8
|
simplifiedPoints?: Map<number, Record<string, number>>;
|
|
9
|
-
canvasRect: { left: number; top: number; width: number; height: number };
|
|
9
|
+
canvasRect: { left: number; top: number; width: number; height: number } | null;
|
|
10
10
|
compositionSize?: { width: number; height: number };
|
|
11
11
|
mode: "recording" | "preview";
|
|
12
12
|
accentColor?: string;
|
|
@@ -23,6 +23,7 @@ export const GestureTrailOverlay = memo(function GestureTrailOverlay({
|
|
|
23
23
|
accentColor = "#3CE6AC",
|
|
24
24
|
}: GestureTrailOverlayProps) {
|
|
25
25
|
const trailPoints = useMemo(() => {
|
|
26
|
+
if (!canvasRect) return "";
|
|
26
27
|
if (trail && trail.length > 1) {
|
|
27
28
|
return trail.map((p) => `${p.x - canvasRect.left},${p.y - canvasRect.top}`).join(" ");
|
|
28
29
|
}
|
|
@@ -32,7 +33,7 @@ export const GestureTrailOverlay = memo(function GestureTrailOverlay({
|
|
|
32
33
|
.map((s) => `${s.properties.x},${s.properties.y}`)
|
|
33
34
|
.join(" ");
|
|
34
35
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
35
|
-
}, [samples, trail, sampleCount, canvasRect
|
|
36
|
+
}, [samples, trail, sampleCount, canvasRect?.left, canvasRect?.top]);
|
|
36
37
|
|
|
37
38
|
const simplifiedPath = useMemo(() => {
|
|
38
39
|
if (!simplifiedPoints || simplifiedPoints.size === 0) return "";
|
|
@@ -58,7 +59,7 @@ export const GestureTrailOverlay = memo(function GestureTrailOverlay({
|
|
|
58
59
|
return pts.sort((a, b) => a.pct - b.pct);
|
|
59
60
|
}, [simplifiedPoints]);
|
|
60
61
|
|
|
61
|
-
if (samples.length < 2 && !simplifiedPoints) return null;
|
|
62
|
+
if (!canvasRect || (samples.length < 2 && !simplifiedPoints)) return null;
|
|
62
63
|
|
|
63
64
|
return (
|
|
64
65
|
<svg
|