mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: timeline changes
This commit is contained in:
@@ -129,7 +129,6 @@ export function Timeline() {
|
|||||||
useTimelineScrollSync({
|
useTimelineScrollSync({
|
||||||
rulerScrollRef,
|
rulerScrollRef,
|
||||||
tracksScrollRef,
|
tracksScrollRef,
|
||||||
trackLabelsScrollRef,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wheel event handling
|
// Wheel event handling
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
export { Timeline } from "../timeline";
|
|
||||||
export { TimelineToolbar } from "./timeline-toolbar";
|
|
||||||
export { TimelineContent } from "./timeline-content";
|
|
||||||
export { TimelineRuler } from "./timeline-ruler";
|
|
||||||
export { TimelineTracksArea } from "./timeline-tracks-area";
|
|
||||||
export { TrackIcon } from "./track-icon";
|
|
||||||
export { useTimelineDragHandlers } from "./timeline-drag-handlers";
|
|
||||||
export { useTimelineActionHandlers } from "./timeline-action-handlers";
|
|
||||||
export { useTimelineScrollSync } from "./timeline-scroll-sync";
|
|
||||||
export { useTimelineContentClick } from "./timeline-content-click";
|
|
||||||
export { useTimelineWheelHandler } from "./timeline-wheel-handler";
|
|
||||||
@@ -59,27 +59,39 @@ export function TimelinePlayhead({
|
|||||||
const leftPosition = playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
const leftPosition = playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
ref={playheadRef}
|
{/* Playhead line container */}
|
||||||
className="absolute pointer-events-auto z-[95]"
|
|
||||||
style={{
|
|
||||||
left: `${trackLabelsWidth + leftPosition}px`,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: "2px", // Slightly wider for better click target
|
|
||||||
}}
|
|
||||||
onMouseDown={handlePlayheadMouseDown}
|
|
||||||
>
|
|
||||||
{/* The playhead line spanning full height */}
|
|
||||||
<div
|
<div
|
||||||
className={`absolute left-0 w-0.5 cursor-col-resize h-full ${isSnappingToPlayhead ? "bg-primary" : "bg-foreground"}`}
|
ref={playheadRef}
|
||||||
/>
|
className="absolute pointer-events-auto z-[95]"
|
||||||
|
style={{
|
||||||
|
left: `${trackLabelsWidth + leftPosition}px`,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: "2px", // Slightly wider for better click target
|
||||||
|
}}
|
||||||
|
onMouseDown={handlePlayheadMouseDown}
|
||||||
|
>
|
||||||
|
{/* The playhead line spanning full height */}
|
||||||
|
<div
|
||||||
|
className={`absolute left-0 w-0.5 cursor-col-resize h-full ${isSnappingToPlayhead ? "bg-primary" : "bg-foreground"}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Playhead dot indicator at the top (in ruler area) */}
|
{/* Playhead dot indicator - separate container with highest z-index */}
|
||||||
<div
|
<div
|
||||||
className={`sticky top-1 left-1 -translate-x-[40%] transform w-3 h-3 rounded-full shadow-sm ${isSnappingToPlayhead ? "bg-primary border-primary" : "bg-foreground border-foreground"}`}
|
className="absolute pointer-events-none z-[100]"
|
||||||
/>
|
style={{
|
||||||
</div>
|
left: `${trackLabelsWidth + leftPosition}px`,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`sticky top-1 left-1 -translate-x-[40%] transform w-3 h-3 rounded-full shadow-sm ${isSnappingToPlayhead ? "bg-primary border-primary" : "bg-foreground border-foreground"}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export function TimelineRuler({
|
|||||||
markers.push(
|
markers.push(
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={`absolute top-0 bottom-0 z-[97] ${
|
className={`absolute top-0 bottom-0 z-[99] ${
|
||||||
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"
|
||||||
@@ -91,7 +91,7 @@ export function TimelineRuler({
|
|||||||
style={{ left: `${leftPosition}px` }}
|
style={{ left: `${leftPosition}px` }}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`absolute top-1 left-1 text-[0.6rem] z-[97] ${
|
className={`absolute top-1 left-1 text-[0.6rem] z-[99] ${
|
||||||
isMainMarker
|
isMainMarker
|
||||||
? "text-muted-foreground font-medium"
|
? "text-muted-foreground font-medium"
|
||||||
: "text-muted-foreground/70"
|
: "text-muted-foreground/70"
|
||||||
@@ -108,14 +108,14 @@ export function TimelineRuler({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="sticky top-0 bg-card/[0.99] border-b border-muted/30 z-[97]"
|
className="sticky top-0 bg-card/[0.99] border-b border-muted/30 z-[99]"
|
||||||
onMouseDown={handleSelectionMouseDown}
|
onMouseDown={handleSelectionMouseDown}
|
||||||
onClick={handleTimelineContentClick}
|
onClick={handleTimelineContentClick}
|
||||||
data-ruler-area
|
data-ruler-area
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={rulerRef}
|
ref={rulerRef}
|
||||||
className="relative h-5 select-none cursor-default pb-1 z-[97]"
|
className="relative h-5 select-none cursor-default pb-1 z-[99]"
|
||||||
onMouseDown={handleRulerMouseDown}
|
onMouseDown={handleRulerMouseDown}
|
||||||
>
|
>
|
||||||
{rulerMarkers}
|
{rulerMarkers}
|
||||||
|
|||||||
@@ -3,20 +3,16 @@ import { useEffect, useRef } from "react";
|
|||||||
export interface TimelineScrollSyncProps {
|
export interface TimelineScrollSyncProps {
|
||||||
rulerScrollRef: React.RefObject<HTMLDivElement>;
|
rulerScrollRef: React.RefObject<HTMLDivElement>;
|
||||||
tracksScrollRef: React.RefObject<HTMLDivElement>;
|
tracksScrollRef: React.RefObject<HTMLDivElement>;
|
||||||
trackLabelsScrollRef: React.RefObject<HTMLDivElement>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTimelineScrollSync({
|
export function useTimelineScrollSync({
|
||||||
rulerScrollRef,
|
rulerScrollRef,
|
||||||
tracksScrollRef,
|
tracksScrollRef,
|
||||||
trackLabelsScrollRef,
|
|
||||||
}: TimelineScrollSyncProps) {
|
}: TimelineScrollSyncProps) {
|
||||||
const isUpdatingRef = useRef(false);
|
const isUpdatingRef = useRef(false);
|
||||||
const lastRulerSync = useRef(0);
|
const lastSyncRef = useRef(0);
|
||||||
const lastTracksSync = useRef(0);
|
|
||||||
const lastVerticalSync = useRef(0);
|
|
||||||
|
|
||||||
// --- Scroll synchronization effect ---
|
// --- Horizontal scroll synchronization between ruler and tracks ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const rulerViewport = rulerScrollRef.current?.querySelector(
|
const rulerViewport = rulerScrollRef.current?.querySelector(
|
||||||
"[data-radix-scroll-area-viewport]"
|
"[data-radix-scroll-area-viewport]"
|
||||||
@@ -24,76 +20,38 @@ export function useTimelineScrollSync({
|
|||||||
const tracksViewport = tracksScrollRef.current?.querySelector(
|
const tracksViewport = tracksScrollRef.current?.querySelector(
|
||||||
"[data-radix-scroll-area-viewport]"
|
"[data-radix-scroll-area-viewport]"
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
const trackLabelsViewport = trackLabelsScrollRef.current?.querySelector(
|
|
||||||
"[data-radix-scroll-area-viewport]"
|
|
||||||
) as HTMLElement;
|
|
||||||
|
|
||||||
if (!rulerViewport || !tracksViewport) return;
|
if (!rulerViewport || !tracksViewport) return;
|
||||||
|
|
||||||
// Horizontal scroll synchronization between ruler and tracks
|
// Throttled scroll handlers for better performance
|
||||||
const handleRulerScroll = () => {
|
const handleRulerScroll = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (isUpdatingRef.current || now - lastRulerSync.current < 16) return;
|
if (isUpdatingRef.current || now - lastSyncRef.current < 16) return; // 60fps throttling
|
||||||
lastRulerSync.current = now;
|
lastSyncRef.current = now;
|
||||||
|
|
||||||
isUpdatingRef.current = true;
|
isUpdatingRef.current = true;
|
||||||
tracksViewport.scrollLeft = rulerViewport.scrollLeft;
|
tracksViewport.scrollLeft = rulerViewport.scrollLeft;
|
||||||
isUpdatingRef.current = false;
|
isUpdatingRef.current = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTracksScroll = () => {
|
const handleTracksScroll = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (isUpdatingRef.current || now - lastTracksSync.current < 16) return;
|
if (isUpdatingRef.current || now - lastSyncRef.current < 16) return; // 60fps throttling
|
||||||
lastTracksSync.current = now;
|
lastSyncRef.current = now;
|
||||||
|
|
||||||
isUpdatingRef.current = true;
|
isUpdatingRef.current = true;
|
||||||
rulerViewport.scrollLeft = tracksViewport.scrollLeft;
|
rulerViewport.scrollLeft = tracksViewport.scrollLeft;
|
||||||
isUpdatingRef.current = false;
|
isUpdatingRef.current = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
rulerViewport.addEventListener("scroll", handleRulerScroll);
|
rulerViewport.addEventListener("scroll", handleRulerScroll, { passive: true });
|
||||||
tracksViewport.addEventListener("scroll", handleTracksScroll);
|
tracksViewport.addEventListener("scroll", handleTracksScroll, { passive: true });
|
||||||
|
|
||||||
// Vertical scroll synchronization between track labels and tracks content
|
|
||||||
if (trackLabelsViewport) {
|
|
||||||
const handleTrackLabelsScroll = () => {
|
|
||||||
const now = Date.now();
|
|
||||||
if (isUpdatingRef.current || now - lastVerticalSync.current < 16)
|
|
||||||
return;
|
|
||||||
lastVerticalSync.current = now;
|
|
||||||
isUpdatingRef.current = true;
|
|
||||||
tracksViewport.scrollTop = trackLabelsViewport.scrollTop;
|
|
||||||
isUpdatingRef.current = false;
|
|
||||||
};
|
|
||||||
const handleTracksVerticalScroll = () => {
|
|
||||||
const now = Date.now();
|
|
||||||
if (isUpdatingRef.current || now - lastVerticalSync.current < 16)
|
|
||||||
return;
|
|
||||||
lastVerticalSync.current = now;
|
|
||||||
isUpdatingRef.current = true;
|
|
||||||
trackLabelsViewport.scrollTop = tracksViewport.scrollTop;
|
|
||||||
isUpdatingRef.current = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
trackLabelsViewport.addEventListener("scroll", handleTrackLabelsScroll);
|
|
||||||
tracksViewport.addEventListener("scroll", handleTracksVerticalScroll);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
rulerViewport.removeEventListener("scroll", handleRulerScroll);
|
|
||||||
tracksViewport.removeEventListener("scroll", handleTracksScroll);
|
|
||||||
trackLabelsViewport.removeEventListener(
|
|
||||||
"scroll",
|
|
||||||
handleTrackLabelsScroll
|
|
||||||
);
|
|
||||||
tracksViewport.removeEventListener(
|
|
||||||
"scroll",
|
|
||||||
handleTracksVerticalScroll
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
rulerViewport.removeEventListener("scroll", handleRulerScroll);
|
rulerViewport.removeEventListener("scroll", handleRulerScroll);
|
||||||
tracksViewport.removeEventListener("scroll", handleTracksScroll);
|
tracksViewport.removeEventListener("scroll", handleTracksScroll);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [rulerScrollRef, tracksScrollRef]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function TimelineTracksArea({
|
|||||||
<Fragment key={track.id}>
|
<Fragment key={track.id}>
|
||||||
{/* Left Column (Sticky Track Labels) */}
|
{/* Left Column (Sticky Track Labels) */}
|
||||||
<div
|
<div
|
||||||
className="sticky left-0 flex items-center border-b border-panel border-inset group bg-card/[0.99] z-[96]"
|
className="sticky left-0 flex items-center border-b border-panel border-inset group bg-card/[0.99] z-[98]"
|
||||||
style={{ height: `${getTrackHeight(track.type)}px` }}
|
style={{ height: `${getTrackHeight(track.type)}px` }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 px-2">
|
<div className="flex items-center gap-2 px-2">
|
||||||
|
|||||||
@@ -11,17 +11,14 @@ export function useTimelineWheelHandler({
|
|||||||
isInTimeline,
|
isInTimeline,
|
||||||
handleWheel,
|
handleWheel,
|
||||||
}: TimelineWheelHandlerProps) {
|
}: TimelineWheelHandlerProps) {
|
||||||
// Add wheel event listeners with passive: false to allow preventDefault
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timelineContainer = timelineRef.current;
|
const timelineContainer = timelineRef.current;
|
||||||
if (!timelineContainer || !isInTimeline) return;
|
if (!timelineContainer || !isInTimeline) return;
|
||||||
|
|
||||||
const handleWheelCapture = (e: WheelEvent) => {
|
const handleWheelCapture = (e: WheelEvent) => {
|
||||||
// Call the existing handleWheel function
|
|
||||||
handleWheel(e as any);
|
handleWheel(e as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add wheel event listener with passive: false to allow preventDefault
|
|
||||||
timelineContainer.addEventListener("wheel", handleWheelCapture, { passive: false });
|
timelineContainer.addEventListener("wheel", handleWheelCapture, { passive: false });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user