This commit is contained in:
Maze Winther
2025-07-30 11:38:21 +02:00
parent deda0e366f
commit 004fa0c12b
4 changed files with 223 additions and 123 deletions
@@ -211,7 +211,7 @@ export function PreviewPanel() {
setDragState({ setDragState({
isDragging: true, isDragging: true,
elementId: element.id, elementId: element.id,
trackId: trackId, trackId,
startX: e.clientX, startX: e.clientX,
startY: e.clientY, startY: e.clientY,
initialElementX: element.x, initialElementX: element.x,
@@ -355,8 +355,22 @@ export function PreviewPanel() {
handleTextMouseDown(e, element, elementData.track.id) handleTextMouseDown(e, element, elementData.track.id)
} }
style={{ style={{
left: `${50 + ((dragState.isDragging && dragState.elementId === element.id ? dragState.currentX : element.x) / canvasSize.width) * 100}%`, left: `${
top: `${50 + ((dragState.isDragging && dragState.elementId === element.id ? dragState.currentY : element.y) / canvasSize.height) * 100}%`, 50 +
((dragState.isDragging && dragState.elementId === element.id
? dragState.currentX
: element.x) /
canvasSize.width) *
100
}%`,
top: `${
50 +
((dragState.isDragging && dragState.elementId === element.id
? dragState.currentY
: element.y) /
canvasSize.height) *
100
}%`,
transform: `translate(-50%, -50%) rotate(${element.rotation}deg) scale(${scaleRatio})`, transform: `translate(-50%, -50%) rotate(${element.rotation}deg) scale(${scaleRatio})`,
opacity: element.opacity, opacity: element.opacity,
zIndex: 100 + index, // Text elements on top zIndex: 100 + index, // Text elements on top
@@ -552,7 +552,7 @@ export function Timeline() {
{/* Timeline Ruler */} {/* Timeline Ruler */}
<div <div
className="flex-1 relative overflow-hidden h-4" className="flex-1 relative overflow-hidden h-10"
onWheel={(e) => { onWheel={(e) => {
// Check if this is horizontal scrolling - if so, don't handle it here // Check if this is horizontal scrolling - if so, don't handle it here
if (e.shiftKey || Math.abs(e.deltaX) > Math.abs(e.deltaY)) { if (e.shiftKey || Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
@@ -567,7 +567,7 @@ export function Timeline() {
<ScrollArea className="w-full" ref={rulerScrollRef}> <ScrollArea className="w-full" ref={rulerScrollRef}>
<div <div
ref={rulerRef} ref={rulerRef}
className="relative h-4 select-none cursor-default" className="relative h-10 select-none cursor-default"
style={{ style={{
width: `${dynamicTimelineWidth}px`, width: `${dynamicTimelineWidth}px`,
}} }}
@@ -601,13 +601,17 @@ export function Timeline() {
return ( return (
<div <div
key={i} key={i}
className={`absolute top-0 bottom-0 ${ className={`absolute top-0 h-4 ${
isMainMarker isMainMarker
? "border-l border-muted-foreground/40" ? "border-l border-muted-foreground/40"
: "border-l border-muted-foreground/20" : "border-l border-muted-foreground/20"
}`} }`}
style={{ style={{
left: `${time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`, left: `${
time *
TIMELINE_CONSTANTS.PIXELS_PER_SECOND *
zoomLevel
}px`,
}} }}
> >
<span <span
@@ -624,10 +628,16 @@ export function Timeline() {
const secs = seconds % 60; const secs = seconds % 60;
if (hours > 0) { if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, "0")}:${Math.floor(secs).toString().padStart(2, "0")}`; return `${hours}:${minutes
.toString()
.padStart(2, "0")}:${Math.floor(secs)
.toString()
.padStart(2, "0")}`;
} }
if (minutes > 0) { if (minutes > 0) {
return `${minutes}:${Math.floor(secs).toString().padStart(2, "0")}`; return `${minutes}:${Math.floor(secs)
.toString()
.padStart(2, "0")}`;
} }
if (interval >= 1) { if (interval >= 1) {
return `${Math.floor(secs)}s`; return `${Math.floor(secs)}s`;
@@ -710,7 +720,10 @@ export function Timeline() {
<div <div
className="relative flex-1" className="relative flex-1"
style={{ style={{
height: `${Math.max(200, Math.min(800, getTotalTracksHeight(tracks)))}px`, height: `${Math.max(
200,
Math.min(800, getTotalTracksHeight(tracks))
)}px`,
width: `${dynamicTimelineWidth}px`, width: `${dynamicTimelineWidth}px`,
}} }}
> >
@@ -724,7 +737,10 @@ export function Timeline() {
<div <div
className="absolute left-0 right-0 border-b border-muted/30 py-[0.05rem]" className="absolute left-0 right-0 border-b border-muted/30 py-[0.05rem]"
style={{ style={{
top: `${getCumulativeHeightBefore(tracks, index)}px`, top: `${getCumulativeHeightBefore(
tracks,
index
)}px`,
height: `${getTrackHeight(track.type)}px`, height: `${getTrackHeight(track.type)}px`,
}} }}
onClick={(e) => { onClick={(e) => {
@@ -1092,7 +1108,9 @@ function TimelineToolbar({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant="text" size="icon" onClick={toggleRippleEditing}> <Button variant="text" size="icon" onClick={toggleRippleEditing}>
<Link <Link
className={`h-4 w-4 ${rippleEditingEnabled ? "text-primary" : ""}`} className={`h-4 w-4 ${
rippleEditingEnabled ? "text-primary" : ""
}`}
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@@ -62,7 +62,7 @@ export function Handlebars({ children }: HandlebarsProps) {
whileDrag={{ scale: 1.1, cursor: "grabbing" }} whileDrag={{ scale: 1.1, cursor: "grabbing" }}
transition={{ type: "spring", stiffness: 400, damping: 30 }} transition={{ type: "spring", stiffness: 400, damping: 30 }}
> >
<div className="w-2 h-8 rounded-full bg-yellow-500"></div> <div className="w-2 h-8 rounded-full bg-yellow-500" />
</motion.div> </motion.div>
<motion.div <motion.div
@@ -81,7 +81,7 @@ export function Handlebars({ children }: HandlebarsProps) {
whileDrag={{ scale: 1.1, cursor: "grabbing" }} whileDrag={{ scale: 1.1, cursor: "grabbing" }}
transition={{ type: "spring", stiffness: 400, damping: 30 }} transition={{ type: "spring", stiffness: 400, damping: 30 }}
> >
<div className="w-2 h-8 rounded-full bg-yellow-500"></div> <div className="w-2 h-8 rounded-full bg-yellow-500" />
</motion.div> </motion.div>
</div> </div>
+177 -109
View File
@@ -26,136 +26,204 @@ export function useEditorActions() {
const { activeProject } = useProjectStore(); const { activeProject } = useProjectStore();
// Playback actions // Playback actions
useActionHandler("toggle-play", () => { useActionHandler(
toggle(); "toggle-play",
}, undefined); () => {
useActionHandler("stop-playback", () => {
if (isPlaying) {
toggle(); toggle();
} },
seek(0); undefined
}, undefined); );
useActionHandler("seek-forward", (args) => { useActionHandler(
const seconds = args?.seconds ?? 1; "stop-playback",
seek(Math.min(duration, currentTime + seconds)); () => {
}, undefined); if (isPlaying) {
toggle();
}
seek(0);
},
undefined
);
useActionHandler("seek-backward", (args) => { useActionHandler(
const seconds = args?.seconds ?? 1; "seek-forward",
seek(Math.max(0, currentTime - seconds)); (args) => {
}, undefined); const seconds = args?.seconds ?? 1;
seek(Math.min(duration, currentTime + seconds));
},
undefined
);
useActionHandler("frame-step-forward", () => { useActionHandler(
const projectFps = activeProject?.fps || 30; "seek-backward",
seek(Math.min(duration, currentTime + 1 / projectFps)); (args) => {
}, undefined); const seconds = args?.seconds ?? 1;
seek(Math.max(0, currentTime - seconds));
},
undefined
);
useActionHandler("frame-step-backward", () => { useActionHandler(
const projectFps = activeProject?.fps || 30; "frame-step-forward",
seek(Math.max(0, currentTime - 1 / projectFps)); () => {
}, undefined); const projectFps = activeProject?.fps || 30;
seek(Math.min(duration, currentTime + 1 / projectFps));
},
undefined
);
useActionHandler("jump-forward", (args) => { useActionHandler(
const seconds = args?.seconds ?? 5; "frame-step-backward",
seek(Math.min(duration, currentTime + seconds)); () => {
}, undefined); const projectFps = activeProject?.fps || 30;
seek(Math.max(0, currentTime - 1 / projectFps));
},
undefined
);
useActionHandler("jump-backward", (args) => { useActionHandler(
const seconds = args?.seconds ?? 5; "jump-forward",
seek(Math.max(0, currentTime - seconds)); (args) => {
}, undefined); const seconds = args?.seconds ?? 5;
seek(Math.min(duration, currentTime + seconds));
},
undefined
);
useActionHandler("goto-start", () => { useActionHandler(
seek(0); "jump-backward",
}, undefined); (args) => {
const seconds = args?.seconds ?? 5;
seek(Math.max(0, currentTime - seconds));
},
undefined
);
useActionHandler("goto-end", () => { useActionHandler(
seek(duration); "goto-start",
}, undefined); () => {
seek(0);
},
undefined
);
useActionHandler(
"goto-end",
() => {
seek(duration);
},
undefined
);
// Timeline editing actions // Timeline editing actions
useActionHandler("split-element", () => { useActionHandler(
if (selectedElements.length !== 1) { "split-element",
toast.error("Select exactly one element to split"); () => {
return; if (selectedElements.length !== 1) {
} toast.error("Select exactly one element to split");
return;
const { trackId, elementId } = selectedElements[0];
const track = tracks.find((t: any) => t.id === trackId);
const element = track?.elements.find((el: any) => el.id === elementId);
if (element) {
const effectiveStart = element.startTime;
const effectiveEnd =
element.startTime +
(element.duration - element.trimStart - element.trimEnd);
if (currentTime > effectiveStart && currentTime < effectiveEnd) {
splitElement(trackId, elementId, currentTime);
} else {
toast.error("Playhead must be within selected element");
} }
}
}, undefined);
useActionHandler("delete-selected", () => { const { trackId, elementId } = selectedElements[0];
if (selectedElements.length === 0) { const track = tracks.find((t: any) => t.id === trackId);
return; const element = track?.elements.find((el: any) => el.id === elementId);
}
selectedElements.forEach( if (element) {
({ trackId, elementId }: { trackId: string; elementId: string }) => { const effectiveStart = element.startTime;
removeElementFromTrack(trackId, elementId); const effectiveEnd =
element.startTime +
(element.duration - element.trimStart - element.trimEnd);
if (currentTime > effectiveStart && currentTime < effectiveEnd) {
splitElement(trackId, elementId, currentTime);
} else {
toast.error("Playhead must be within selected element");
}
} }
); },
clearSelectedElements(); undefined
}, undefined); );
useActionHandler("select-all", () => { useActionHandler(
const allElements = tracks.flatMap((track: any) => "delete-selected",
track.elements.map((element: any) => ({ () => {
trackId: track.id, if (selectedElements.length === 0) {
elementId: element.id, return;
})) }
); selectedElements.forEach(
setSelectedElements(allElements); ({ trackId, elementId }: { trackId: string; elementId: string }) => {
}, undefined); removeElementFromTrack(trackId, elementId);
}
);
clearSelectedElements();
},
undefined
);
useActionHandler("duplicate-selected", () => { useActionHandler(
if (selectedElements.length !== 1) { "select-all",
toast.error("Select exactly one element to duplicate"); () => {
return; const allElements = tracks.flatMap((track: any) =>
} track.elements.map((element: any) => ({
trackId: track.id,
elementId: element.id,
}))
);
setSelectedElements(allElements);
},
undefined
);
const { trackId, elementId } = selectedElements[0]; useActionHandler(
const track = tracks.find((t: any) => t.id === trackId); "duplicate-selected",
const element = track?.elements.find((el: any) => el.id === elementId); () => {
if (selectedElements.length !== 1) {
toast.error("Select exactly one element to duplicate");
return;
}
if (element) { const { trackId, elementId } = selectedElements[0];
const newStartTime = const track = tracks.find((t: any) => t.id === trackId);
element.startTime + const element = track?.elements.find((el: any) => el.id === elementId);
(element.duration - element.trimStart - element.trimEnd) +
0.1;
const { id, ...elementWithoutId } = element;
addElementToTrack(trackId, { if (element) {
...elementWithoutId, const newStartTime =
startTime: newStartTime, element.startTime +
}); (element.duration - element.trimStart - element.trimEnd) +
} 0.1;
}, undefined); const { id, ...elementWithoutId } = element;
useActionHandler("toggle-snapping", () => { addElementToTrack(trackId, {
toggleSnapping(); ...elementWithoutId,
}, undefined); startTime: newStartTime,
});
}
},
undefined
);
useActionHandler(
"toggle-snapping",
() => {
toggleSnapping();
},
undefined
);
// History actions // History actions
useActionHandler("undo", () => { useActionHandler(
undo(); "undo",
}, undefined); () => {
undo();
},
undefined
);
useActionHandler("redo", () => { useActionHandler(
redo(); "redo",
}, undefined); () => {
redo();
},
undefined
);
} }