From b266678fe4bb8c4053688fd037693fa2f930ac14 Mon Sep 17 00:00:00 2001 From: gantiro Date: Tue, 15 Jul 2025 22:51:38 +0700 Subject: Optimize timeline performance --- apps/web/src/components/editor/timeline.tsx | 245 ++++++++++------------------ 1 file changed, 90 insertions(+), 155 deletions(-) (limited to 'apps/web/src/components/editor/timeline.tsx') diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index e83b64b9..7b7455e0 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -15,8 +15,9 @@ import { Video, Music, TypeIcon, - Magnet, Lock, + ZoomIn, + ZoomOut, } from "lucide-react"; import { Tooltip, @@ -54,6 +55,9 @@ import { getTotalTracksHeight, TIMELINE_CONSTANTS, } from "@/constants/timeline-constants"; +import { Slider } from "../ui/slider"; +import TimelineCanvasRuler from "./timeline-canvas/timeline-canvas-ruler"; +import TimelineCanvasRulerWrapper from "./timeline-canvas/timeline-canvas-ruler-wrapper"; export function Timeline() { // Timeline shows all tracks (video, audio, effects) and their elements. @@ -92,10 +96,13 @@ export function Timeline() { const [isInTimeline, setIsInTimeline] = useState(false); // Timeline zoom functionality - const { zoomLevel, setZoomLevel, handleWheel } = useTimelineZoom({ - containerRef: timelineRef, - isInTimeline, - }); + const { + zoomLevel, + zoomStep, + handleChangeZoomLevel, + handleChangeZoomStep, + handleWheel, + } = useTimelineZoom(); // Old marquee selection removed - using new SelectionBox component instead @@ -108,7 +115,6 @@ export function Timeline() { // Scroll synchronization and auto-scroll to playhead const rulerScrollRef = useRef(null); - const tracksScrollRef = useRef(null); const trackLabelsRef = useRef(null); const playheadRef = useRef(null); const trackLabelsScrollRef = useRef(null); @@ -125,8 +131,6 @@ export function Timeline() { seek, rulerRef, rulerScrollRef, - tracksScrollRef, - playheadRef, }); // Selection box functionality @@ -196,14 +200,14 @@ export function Timeline() { clearSelectedElements(); // Determine if we're clicking in ruler or tracks area - const isRulerClick = (e.target as HTMLElement).closest( - "[data-ruler-area]" + const isTracksAreaClick = (e.target as HTMLElement).closest( + "[data-tracks-area]" ); let mouseX: number; let scrollLeft = 0; - if (isRulerClick) { + if (isTracksAreaClick) { // Calculate based on ruler position const rulerContent = rulerScrollRef.current?.querySelector( "[data-radix-scroll-area-viewport]" @@ -213,14 +217,7 @@ export function Timeline() { mouseX = e.clientX - rect.left; scrollLeft = rulerContent.scrollLeft; } else { - // Calculate based on tracks content position - const tracksContent = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; - if (!tracksContent) return; - const rect = tracksContent.getBoundingClientRect(); - mouseX = e.clientX - rect.left; - scrollLeft = tracksContent.scrollLeft; + return; } const rawTime = Math.max( @@ -244,7 +241,6 @@ export function Timeline() { zoomLevel, seek, rulerScrollRef, - tracksScrollRef, clearSelectedElements, isSelecting, justFinishedSelecting, @@ -616,14 +612,11 @@ export function Timeline() { const rulerViewport = rulerScrollRef.current?.querySelector( "[data-radix-scroll-area-viewport]" ) as HTMLElement; - const tracksViewport = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; const trackLabelsViewport = trackLabelsScrollRef.current?.querySelector( "[data-radix-scroll-area-viewport]" ) as HTMLElement; - if (!rulerViewport || !tracksViewport) return; + if (!rulerViewport) return; // Horizontal scroll synchronization between ruler and tracks const handleRulerScroll = () => { @@ -631,7 +624,6 @@ export function Timeline() { if (isUpdatingRef.current || now - lastRulerSync.current < 16) return; lastRulerSync.current = now; isUpdatingRef.current = true; - tracksViewport.scrollLeft = rulerViewport.scrollLeft; isUpdatingRef.current = false; }; const handleTracksScroll = () => { @@ -639,12 +631,10 @@ export function Timeline() { if (isUpdatingRef.current || now - lastTracksSync.current < 16) return; lastTracksSync.current = now; isUpdatingRef.current = true; - rulerViewport.scrollLeft = tracksViewport.scrollLeft; isUpdatingRef.current = false; }; rulerViewport.addEventListener("scroll", handleRulerScroll); - tracksViewport.addEventListener("scroll", handleTracksScroll); // Vertical scroll synchronization between track labels and tracks content if (trackLabelsViewport) { @@ -654,7 +644,6 @@ export function Timeline() { return; lastVerticalSync.current = now; isUpdatingRef.current = true; - tracksViewport.scrollTop = trackLabelsViewport.scrollTop; isUpdatingRef.current = false; }; const handleTracksVerticalScroll = () => { @@ -663,30 +652,22 @@ export function Timeline() { 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 () => { rulerViewport.removeEventListener("scroll", handleRulerScroll); - tracksViewport.removeEventListener("scroll", handleTracksScroll); }; }, []); @@ -846,6 +827,39 @@ export function Timeline() { Delete element (Delete) + + + + + Zoom Out + + handleChangeZoomStep(value[0])} + /> + + + + + Zoom In +
@@ -875,7 +889,6 @@ export function Timeline() { seek={seek} rulerRef={rulerRef} rulerScrollRef={rulerScrollRef} - tracksScrollRef={tracksScrollRef} trackLabelsRef={trackLabelsRef} timelineRef={timelineRef} playheadRef={playheadRef} @@ -893,114 +906,38 @@ export function Timeline() { /> {/* Timeline Header with Ruler */}
- {/* Track Labels Header */} -
- {/* Empty space */} - - . - -
+
- {/* Timeline Ruler */} -
+
-
- {/* Time markers */} - {(() => { - // Calculate appropriate time interval based on zoom level - const getTimeInterval = (zoom: number) => { - const pixelsPerSecond = - TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoom; - if (pixelsPerSecond >= 200) return 0.1; // Every 0.1s when very zoomed in - if (pixelsPerSecond >= 100) return 0.5; // Every 0.5s when zoomed in - if (pixelsPerSecond >= 50) return 1; // Every 1s at normal zoom - if (pixelsPerSecond >= 25) return 2; // Every 2s when zoomed out - if (pixelsPerSecond >= 12) return 5; // Every 5s when more zoomed out - if (pixelsPerSecond >= 6) return 10; // Every 10s when very zoomed out - return 30; // Every 30s when extremely zoomed out - }; - - const interval = getTimeInterval(zoomLevel); - const markerCount = Math.ceil(duration / interval) + 1; - - return Array.from({ length: markerCount }, (_, i) => { - const time = i * interval; - if (time > duration) return null; - - const isMainMarker = - time % (interval >= 1 ? Math.max(1, interval) : 1) === 0; - - return ( -
- - {(() => { - const formatTime = (seconds: number) => { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = seconds % 60; - - if (hours > 0) { - return `${hours}:${minutes.toString().padStart(2, "0")}:${Math.floor(secs).toString().padStart(2, "0")}`; - } else if (minutes > 0) { - return `${minutes}:${Math.floor(secs).toString().padStart(2, "0")}`; - } else if (interval >= 1) { - return `${Math.floor(secs)}s`; - } else { - return `${secs.toFixed(1)}s`; - } - }; - return formatTime(time); - })()} - -
- ); - }).filter(Boolean); - })()} -
+ +
{/* Tracks Area */} -
- {/* Track Labels */} - {tracks.length > 0 && ( -
- -
+ +
+ {/* Track Labels */} + {tracks.length > 0 && ( +
+
{tracks.map((track) => (
))}
- -
- )} +
+ )} - {/* Timeline Tracks Content */} -
- - + {/* Timeline Tracks Content */} +
+
)}
- +
-
+
); -- cgit v1.3.1