Skip to content

Commit c2f6b7d

Browse files
committed
fix(studio): remove keyframe dragging from the timeline
Dragging timeline keyframe diamonds was unreliable — clip<->tween percentage remapping, an optimistic-hold workaround, and an intermittent no-op/revert when the GSAP session lagged the drag (its own comments document the flakiness). Remove the drag interaction entirely: diamonds still display, click-to-seek, and offer the context menu (add/remove/ease) — keyframe timing is edited via the playhead + panel, which are deterministic. Deletes the keyframe-move plan module + its wiring through TimelineClipDiamonds -> TimelineCanvas -> Timeline -> TimelineEditContext.
1 parent 7771520 commit c2f6b7d

8 files changed

Lines changed: 4 additions & 448 deletions

File tree

packages/studio/src/components/StudioPreviewArea.tsx

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ import { useDomEditActionsContext, useDomEditSelectionContext } from "../context
2121
import { TimelineEditProvider } from "../contexts/TimelineEditContext";
2222
import type { BlockPreviewInfo } from "./sidebar/BlocksTab";
2323
import { readStudioUiPreferences } from "../utils/studioUiPreferences";
24-
import { fetchParsedAnimations } from "../hooks/useGsapTweenCache";
25-
import { pickKeyframeTween, computeKeyframeMovePlan } from "./editor/keyframeMove";
2624
import type { GestureRecordingState } from "./editor/GestureRecordControl";
2725

2826
export interface StudioPreviewAreaProps {
@@ -182,45 +180,6 @@ export function StudioPreviewArea({
182180
}
183181
},
184182
// fallow-ignore-next-line complexity
185-
onMoveKeyframe: async (_el: TimelineElement, oldPct: number, newPct: number) => {
186-
// Resolve the dragged element's selection + parsed animations on demand
187-
// (both awaited and cached) rather than relying on the async DOM-edit
188-
// session being loaded for this element — that coupling made the commit
189-
// intermittently no-op (revert) when dragging before the session caught up.
190-
if (!projectId) return;
191-
const sourceFile = _el.sourceFile || activeCompPath || "index.html";
192-
const [selection, parsed] = await Promise.all([
193-
buildDomSelectionForTimelineElement(_el),
194-
fetchParsedAnimations(projectId, sourceFile),
195-
]);
196-
if (!selection || !parsed) return;
197-
198-
const cached = usePlayerStore.getState().keyframeCache.get(_el.key ?? _el.id);
199-
const cachedKf = cached?.keyframes.find((k) => Math.abs(k.percentage - oldPct) < 0.2);
200-
const origAbsTime = _el.start + (oldPct / 100) * _el.duration;
201-
const anim = pickKeyframeTween(
202-
parsed.animations,
203-
_el,
204-
origAbsTime,
205-
cachedKf?.propertyGroup,
206-
);
207-
if (!anim) return;
208-
209-
const plan = computeKeyframeMovePlan(
210-
anim,
211-
cachedKf?.tweenPercentage ?? oldPct,
212-
_el,
213-
newPct,
214-
);
215-
if (plan.meta) handleGsapUpdateMeta(anim.id, plan.meta, selection);
216-
for (const pct of plan.removes) handleGsapRemoveKeyframe(anim.id, pct, selection);
217-
for (const add of plan.adds) {
218-
for (const [prop, val] of Object.entries(add.properties)) {
219-
handleGsapAddKeyframe(anim.id, add.pct, prop, val, selection);
220-
}
221-
}
222-
},
223-
// fallow-ignore-next-line complexity
224183
onToggleKeyframeAtPlayhead: (el: TimelineElement) => {
225184
const currentTime = usePlayerStore.getState().currentTime;
226185
const pct =

packages/studio/src/components/editor/keyframeMove.test.ts

Lines changed: 0 additions & 101 deletions
This file was deleted.

packages/studio/src/components/editor/keyframeMove.ts

Lines changed: 0 additions & 151 deletions
This file was deleted.

packages/studio/src/contexts/TimelineEditContext.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ export function TimelineEditProvider({
3939
value.onDeleteKeyframe,
4040
value.onDeleteAllKeyframes,
4141
value.onChangeKeyframeEase,
42-
value.onMoveKeyframe,
4342
value.onToggleKeyframeAtPlayhead,
4443
],
4544
);

packages/studio/src/player/components/Timeline.tsx

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
type KeyframeDiamondContextMenuState,
2121
} from "./KeyframeDiamondContextMenu";
2222
import { useTimelineClipDrag } from "./useTimelineClipDrag";
23-
import { snapKeyframePctToBeat } from "./timelineEditing";
2423
import { ClipContextMenu } from "./ClipContextMenu";
2524
import {
2625
GUTTER,
@@ -87,7 +86,6 @@ export const Timeline = memo(function Timeline({
8786
onDeleteKeyframe,
8887
onDeleteAllKeyframes,
8988
onChangeKeyframeEase,
90-
onMoveKeyframe,
9189
} = useResolvedTimelineEditCallbacks({
9290
onMoveElement: onMoveElementOverride,
9391
onResizeElement: onResizeElementOverride,
@@ -481,19 +479,6 @@ export const Timeline = memo(function Timeline({
481479
onShiftClickKeyframe={(elId, pct) => {
482480
toggleSelectedKeyframe(`${elId}:${pct}`);
483481
}}
484-
onDragKeyframe={(el, oldPct, newPct) => {
485-
onMoveKeyframe?.(el, oldPct, newPct);
486-
}}
487-
onSnapKeyframePct={(el, pct) =>
488-
snapKeyframePctToBeat(el, pct, adjustedBeatAnalysis?.beatTimes, pps)
489-
}
490-
onPickKeyframeElement={(el) => {
491-
const elKey = el.key ?? el.id;
492-
if (selectedElementId !== elKey) {
493-
setSelectedElementId(elKey);
494-
onSelectElement?.(el);
495-
}
496-
}}
497482
onContextMenuKeyframe={(e, elId, pct) => {
498483
const el = expandedElements.find((x) => (x.key ?? x.id) === elId);
499484
if (el) {

packages/studio/src/player/components/TimelineCanvas.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,6 @@ interface TimelineCanvasProps {
9191
currentTime: number;
9292
onClickKeyframe?: (element: TimelineElement, percentage: number) => void;
9393
onShiftClickKeyframe?: (elementId: string, percentage: number) => void;
94-
onDragKeyframe?: (element: TimelineElement, oldPct: number, newPct: number) => void;
95-
/** Snap a keyframe's clip-relative % to the nearest beat (returns unchanged when none in range). */
96-
onSnapKeyframePct?: (element: TimelineElement, pct: number) => number;
97-
/** Select the element when a keyframe drag starts (loads its GSAP session). */
98-
onPickKeyframeElement?: (element: TimelineElement) => void;
9994
onContextMenuKeyframe?: (e: React.MouseEvent, elementId: string, percentage: number) => void;
10095
onContextMenuClip?: (e: React.MouseEvent, element: TimelineElement) => void;
10196
beatAnalysis?: MusicBeatAnalysis | null;
@@ -143,9 +138,6 @@ export const TimelineCanvas = memo(function TimelineCanvas({
143138
currentTime,
144139
onClickKeyframe,
145140
onShiftClickKeyframe,
146-
onDragKeyframe,
147-
onSnapKeyframePct,
148-
onPickKeyframeElement,
149141
onContextMenuKeyframe,
150142
onContextMenuClip,
151143
beatAnalysis,
@@ -446,11 +438,6 @@ export const TimelineCanvas = memo(function TimelineCanvas({
446438
selectedKeyframes={selectedKeyframes}
447439
onClickKeyframe={(pct) => onClickKeyframe?.(previewElement, pct)}
448440
onShiftClickKeyframe={onShiftClickKeyframe}
449-
onDragKeyframe={(oldPct, newPct) =>
450-
onDragKeyframe?.(previewElement, oldPct, newPct)
451-
}
452-
snapPct={(pct) => onSnapKeyframePct?.(previewElement, pct) ?? pct}
453-
onPickForDrag={() => onPickKeyframeElement?.(previewElement)}
454441
onContextMenuKeyframe={onContextMenuKeyframe}
455442
/>
456443
)}

0 commit comments

Comments
 (0)