fix: canvas dragging for keyframed positions

This commit is contained in:
Maze Winther 2026-05-16 05:27:20 +02:00
parent d6622dc6b3
commit 0d35499356
1 changed files with 134 additions and 21 deletions

View File

@ -19,6 +19,17 @@ import {
} from "@/preview/preview-snap";
import type { TCanvasSize } from "@/project/types";
import type { Transform } from "@/rendering";
import { resolveTransformAtTime } from "@/rendering/animation-values";
import {
getElementLocalTime,
hasKeyframesForPath,
upsertElementKeyframe,
} from "@/animation";
import type {
AnimationPropertyPath,
ElementAnimations,
} from "@/animation/types";
import { mediaTime, type MediaTime } from "@/wasm";
import { isVisualElement } from "@/timeline/element-utils";
import type {
ElementRef,
@ -50,7 +61,12 @@ interface PendingGesture extends CapturedPointerState {
interface DragElementSnapshot {
readonly trackId: string;
readonly elementId: string;
readonly initialTransform: Transform;
readonly initialBaseTransform: Transform;
readonly initialResolvedTransform: Transform;
readonly animations: ElementAnimations | undefined;
readonly localTime: MediaTime;
readonly hasAnimatedPositionX: boolean;
readonly hasAnimatedPositionY: boolean;
}
interface DraggingGesture extends CapturedPointerState {
@ -204,8 +220,10 @@ function movedPastDragThreshold({
function toDragElementSnapshots({
elementsWithTracks,
currentTime,
}: {
elementsWithTracks: Array<{ track: TimelineTrack; element: TimelineElement }>;
currentTime: number;
}): DragElementSnapshot[] {
const isVisualTrackedElement = (value: {
track: TimelineTrack;
@ -215,11 +233,107 @@ function toDragElementSnapshots({
return elementsWithTracks
.filter(isVisualTrackedElement)
.map(({ track, element }) => ({
trackId: track.id,
elementId: element.id,
initialTransform: element.transform,
}));
.map(({ track, element }) => {
const localTime = mediaTime({
ticks: getElementLocalTime({
timelineTime: currentTime,
elementStartTime: element.startTime,
elementDuration: element.duration,
}),
});
return {
trackId: track.id,
elementId: element.id,
initialBaseTransform: element.transform,
initialResolvedTransform: resolveTransformAtTime({
baseTransform: element.transform,
animations: element.animations,
localTime,
}),
animations: element.animations,
localTime,
hasAnimatedPositionX: hasKeyframesForPath({
animations: element.animations,
propertyPath: "transform.positionX",
}),
hasAnimatedPositionY: hasKeyframesForPath({
animations: element.animations,
propertyPath: "transform.positionY",
}),
};
});
}
function upsertPositionKeyframe({
animations,
propertyPath,
localTime,
value,
}: {
animations: ElementAnimations | undefined;
propertyPath: AnimationPropertyPath;
localTime: MediaTime;
value: number;
}): ElementAnimations | undefined {
return upsertElementKeyframe({
animations,
propertyPath,
time: localTime,
value,
});
}
function buildDragPositionUpdates({
snapshot,
position,
}: {
snapshot: DragElementSnapshot;
position: Point;
}): Partial<TimelineElement> {
const hasAnimatedPosition =
snapshot.hasAnimatedPositionX || snapshot.hasAnimatedPositionY;
const hasStaticPosition =
!snapshot.hasAnimatedPositionX || !snapshot.hasAnimatedPositionY;
let animations = snapshot.animations;
const updates: Partial<TimelineElement> = {};
if (snapshot.hasAnimatedPositionX) {
animations = upsertPositionKeyframe({
animations,
propertyPath: "transform.positionX",
localTime: snapshot.localTime,
value: position.x,
});
}
if (snapshot.hasAnimatedPositionY) {
animations = upsertPositionKeyframe({
animations,
propertyPath: "transform.positionY",
localTime: snapshot.localTime,
value: position.y,
});
}
if (hasAnimatedPosition) {
updates.animations = animations;
}
if (hasStaticPosition) {
updates.transform = {
...snapshot.initialBaseTransform,
position: {
x: snapshot.hasAnimatedPositionX
? snapshot.initialBaseTransform.position.x
: position.x,
y: snapshot.hasAnimatedPositionY
? snapshot.initialBaseTransform.position.y
: position.y,
},
};
}
return updates;
}
export class PreviewInteractionController {
@ -489,6 +603,7 @@ export class PreviewInteractionController {
elementsWithTracks: this.deps.timeline.getElementsWithTracks({
elements: dragSelection,
}),
currentTime: this.deps.scene.getCurrentTime(),
});
if (draggableElements.length === 0) {
@ -536,8 +651,8 @@ export class PreviewInteractionController {
const deltaY = currentPos.y - drag.origin.y;
const proposedPosition = {
x: firstElement.initialTransform.position.x + deltaX,
y: firstElement.initialTransform.position.y + deltaY,
x: firstElement.initialResolvedTransform.position.x + deltaX,
y: firstElement.initialResolvedTransform.position.y + deltaY,
};
const shouldSnap = !this.deps.input.isShiftHeld();
@ -560,23 +675,21 @@ export class PreviewInteractionController {
this.deps.preview.onSnapLinesChange?.(activeLines);
const deltaSnappedX =
snappedPosition.x - firstElement.initialTransform.position.x;
snappedPosition.x - firstElement.initialResolvedTransform.position.x;
const deltaSnappedY =
snappedPosition.y - firstElement.initialTransform.position.y;
snappedPosition.y - firstElement.initialResolvedTransform.position.y;
this.deps.timeline.previewElements(
drag.elements.map(({ trackId, elementId, initialTransform }) => ({
trackId,
elementId,
updates: {
transform: {
...initialTransform,
position: {
x: initialTransform.position.x + deltaSnappedX,
y: initialTransform.position.y + deltaSnappedY,
},
drag.elements.map((snapshot) => ({
trackId: snapshot.trackId,
elementId: snapshot.elementId,
updates: buildDragPositionUpdates({
snapshot,
position: {
x: snapshot.initialResolvedTransform.position.x + deltaSnappedX,
y: snapshot.initialResolvedTransform.position.y + deltaSnappedY,
},
},
}),
})),
);
}