mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
add transform + few fixes
feat: transform feat: batch command fix: slider undo fix: timeline zoom perf refactor: make updateElement support multiple
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
interface UseScrollPositionReturn {
|
||||
scrollLeft: number;
|
||||
viewportWidth: number;
|
||||
}
|
||||
|
||||
export function useScrollPosition({
|
||||
scrollRef,
|
||||
}: {
|
||||
scrollRef: React.RefObject<HTMLElement | null>;
|
||||
}): UseScrollPositionReturn {
|
||||
const [scrollLeft, setScrollLeft] = useState(0);
|
||||
const [viewportWidth, setViewportWidth] = useState(0);
|
||||
const rafIdRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollElement = scrollRef.current;
|
||||
if (!scrollElement) return;
|
||||
|
||||
const updatePosition = () => {
|
||||
if (rafIdRef.current !== null) {
|
||||
cancelAnimationFrame(rafIdRef.current);
|
||||
}
|
||||
|
||||
rafIdRef.current = requestAnimationFrame(() => {
|
||||
setScrollLeft(scrollElement.scrollLeft);
|
||||
setViewportWidth(scrollElement.clientWidth);
|
||||
rafIdRef.current = null;
|
||||
});
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updatePosition();
|
||||
});
|
||||
|
||||
updatePosition();
|
||||
|
||||
scrollElement.addEventListener("scroll", updatePosition, { passive: true });
|
||||
resizeObserver.observe(scrollElement);
|
||||
|
||||
return () => {
|
||||
scrollElement.removeEventListener("scroll", updatePosition);
|
||||
resizeObserver.disconnect();
|
||||
if (rafIdRef.current !== null) {
|
||||
cancelAnimationFrame(rafIdRef.current);
|
||||
}
|
||||
};
|
||||
}, [scrollRef]);
|
||||
|
||||
return { scrollLeft, viewportWidth };
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useEditor } from "@/hooks/use-editor";
|
||||
import { useSyncExternalStore } from "react";
|
||||
import type { Transform, TimelineTrack } from "@/types/timeline";
|
||||
|
||||
interface DragState {
|
||||
startX: number;
|
||||
startY: number;
|
||||
tracksSnapshot: TimelineTrack[];
|
||||
elements: Array<{
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
initialTransform: Transform;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function usePreviewInteraction({
|
||||
canvasRef,
|
||||
}: {
|
||||
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
||||
}) {
|
||||
const editor = useEditor();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const dragStateRef = useRef<DragState | null>(null);
|
||||
|
||||
const selectedElements = useSyncExternalStore(
|
||||
(listener) => editor.selection.subscribe(listener),
|
||||
() => editor.selection.getSelectedElements(),
|
||||
);
|
||||
|
||||
const getCanvasCoordinates = useCallback(
|
||||
({ clientX, clientY }: { clientX: number; clientY: number }) => {
|
||||
if (!canvasRef.current) return { x: 0, y: 0 };
|
||||
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const logicalWidth = canvasRef.current.width;
|
||||
const logicalHeight = canvasRef.current.height;
|
||||
const scaleX = logicalWidth / rect.width;
|
||||
const scaleY = logicalHeight / rect.height;
|
||||
|
||||
const canvasX = (clientX - rect.left) * scaleX;
|
||||
const canvasY = (clientY - rect.top) * scaleY;
|
||||
|
||||
return { x: canvasX, y: canvasY };
|
||||
},
|
||||
[canvasRef],
|
||||
);
|
||||
|
||||
const handlePointerDown = useCallback(
|
||||
(event: React.PointerEvent) => {
|
||||
if (selectedElements.length === 0) return;
|
||||
|
||||
const elementsWithTracks = editor.timeline.getElementsWithTracks({
|
||||
elements: selectedElements,
|
||||
});
|
||||
|
||||
const draggableElements = elementsWithTracks.filter(
|
||||
({ element }) =>
|
||||
element.type === "video" ||
|
||||
element.type === "image" ||
|
||||
element.type === "text" ||
|
||||
element.type === "sticker",
|
||||
);
|
||||
|
||||
if (draggableElements.length === 0) return;
|
||||
|
||||
const startPos = getCanvasCoordinates({
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
});
|
||||
|
||||
dragStateRef.current = {
|
||||
startX: startPos.x,
|
||||
startY: startPos.y,
|
||||
tracksSnapshot: editor.timeline.getTracks(),
|
||||
elements: draggableElements.map(({ track, element }) => ({
|
||||
trackId: track.id,
|
||||
elementId: element.id,
|
||||
initialTransform: (element as { transform: Transform }).transform,
|
||||
})),
|
||||
};
|
||||
|
||||
setIsDragging(true);
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
},
|
||||
[selectedElements, editor, getCanvasCoordinates],
|
||||
);
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(event: React.PointerEvent) => {
|
||||
if (!dragStateRef.current || !isDragging) return;
|
||||
|
||||
const currentPos = getCanvasCoordinates({
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
});
|
||||
|
||||
const deltaX = currentPos.x - dragStateRef.current.startX;
|
||||
const deltaY = currentPos.y - dragStateRef.current.startY;
|
||||
|
||||
for (const { trackId, elementId, initialTransform } of dragStateRef
|
||||
.current.elements) {
|
||||
const newPosition = {
|
||||
x: initialTransform.position.x + deltaX,
|
||||
y: initialTransform.position.y + deltaY,
|
||||
};
|
||||
|
||||
editor.timeline.updateElements({
|
||||
updates: [
|
||||
{
|
||||
trackId,
|
||||
elementId,
|
||||
updates: {
|
||||
transform: {
|
||||
...initialTransform,
|
||||
position: newPosition,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pushHistory: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
[isDragging, getCanvasCoordinates, editor],
|
||||
);
|
||||
|
||||
const handlePointerUp = useCallback(
|
||||
(event: React.PointerEvent) => {
|
||||
if (!dragStateRef.current || !isDragging) return;
|
||||
|
||||
const currentPos = getCanvasCoordinates({
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
});
|
||||
|
||||
const deltaX = currentPos.x - dragStateRef.current.startX;
|
||||
const deltaY = currentPos.y - dragStateRef.current.startY;
|
||||
|
||||
// revert to pre-drag state so the command captures the correct undo snapshot
|
||||
editor.timeline.updateTracks(dragStateRef.current.tracksSnapshot);
|
||||
|
||||
const updates = dragStateRef.current.elements.map(
|
||||
({ trackId, elementId, initialTransform }) => {
|
||||
const newPosition = {
|
||||
x: initialTransform.position.x + deltaX,
|
||||
y: initialTransform.position.y + deltaY,
|
||||
};
|
||||
|
||||
return {
|
||||
trackId,
|
||||
elementId,
|
||||
updates: {
|
||||
transform: {
|
||||
...initialTransform,
|
||||
position: newPosition,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
editor.timeline.updateElements({ updates });
|
||||
|
||||
dragStateRef.current = null;
|
||||
setIsDragging(false);
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
},
|
||||
[isDragging, getCanvasCoordinates, editor],
|
||||
);
|
||||
|
||||
return {
|
||||
onPointerDown: handlePointerDown,
|
||||
onPointerMove: handlePointerMove,
|
||||
onPointerUp: handlePointerUp,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user