From b266678fe4bb8c4053688fd037693fa2f930ac14 Mon Sep 17 00:00:00 2001 From: gantiro Date: Tue, 15 Jul 2025 22:51:38 +0700 Subject: Optimize timeline performance --- .../timeline-canvas-ruler-wrapper.tsx | 24 +++++++ .../timeline-canvas/timeline-canvas-ruler.tsx | 77 ++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 apps/web/src/components/editor/timeline-canvas/timeline-canvas-ruler-wrapper.tsx create mode 100644 apps/web/src/components/editor/timeline-canvas/timeline-canvas-ruler.tsx (limited to 'apps/web/src/components/editor/timeline-canvas') diff --git a/apps/web/src/components/editor/timeline-canvas/timeline-canvas-ruler-wrapper.tsx b/apps/web/src/components/editor/timeline-canvas/timeline-canvas-ruler-wrapper.tsx new file mode 100644 index 00000000..c0efc19e --- /dev/null +++ b/apps/web/src/components/editor/timeline-canvas/timeline-canvas-ruler-wrapper.tsx @@ -0,0 +1,24 @@ +import { TimelineCanvasRulerWrapperProps } from "@/types/timeline"; +import React, { forwardRef } from "react"; + +/** + * Wrapper div around the canvas-based timeline ruler. + * Captures mouse events for scrubbing and ensures pointer events are properly enabled. + */ +const TimelineCanvasRulerWrapper = forwardRef< + HTMLDivElement, + TimelineCanvasRulerWrapperProps +>(({ children, onMouseDown, className = "" }, ref) => { + return ( +
+ {children} +
+ ); +}); + +export default TimelineCanvasRulerWrapper; diff --git a/apps/web/src/components/editor/timeline-canvas/timeline-canvas-ruler.tsx b/apps/web/src/components/editor/timeline-canvas/timeline-canvas-ruler.tsx new file mode 100644 index 00000000..d004c465 --- /dev/null +++ b/apps/web/src/components/editor/timeline-canvas/timeline-canvas-ruler.tsx @@ -0,0 +1,77 @@ +import { TimelineCanvasRulerProps } from "@/types/timeline"; +import React, { useRef, useEffect } from "react"; + +/** + * TimelineCanvasRuler renders the timeline ticks and labels using Canvas. + * Should be wrapped by TimelineCanvasRulerWrapper for interaction handling. + */ +export default function TimelineCanvasRuler({ + zoomLevel, + duration, + width, + height = 20, +}: TimelineCanvasRulerProps) { + const canvasRef = useRef(null); + + useEffect(() => { + const ctx = canvasRef.current?.getContext("2d"); + if (!ctx) return; + + ctx.clearRect(0, 0, width, height); + + const pixelsPerSecond = 50 * zoomLevel; + + const getTimeInterval = () => { + if (pixelsPerSecond >= 200) return 0.1; + if (pixelsPerSecond >= 100) return 0.5; + if (pixelsPerSecond >= 50) return 1; + if (pixelsPerSecond >= 25) return 2; + if (pixelsPerSecond >= 12) return 5; + if (pixelsPerSecond >= 6) return 10; + return 30; + }; + + const mainInterval = getTimeInterval(); + const tickPerMain = 5; + const subInterval = mainInterval / tickPerMain; + + const totalTicks = Math.ceil(duration / subInterval) + 1; + + for (let i = 0; i < totalTicks; i++) { + const time = i * subInterval; + const x = time * pixelsPerSecond; + + const isMain = i % tickPerMain === 0; + + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, isMain ? height * 0.3 : height * 0.1); + ctx.strokeStyle = isMain ? "#999" : "#ccc"; + ctx.lineWidth = isMain ? 1 : 0.5; + ctx.stroke(); + + // Label + if (isMain) { + ctx.fillStyle = "#666"; + ctx.font = "10px sans-serif"; + const mins = Math.floor(time / 60); + const secs = Math.floor(time % 60); + let label = ""; + let xTranslate = 10; + if (mainInterval < 1) { + label = `${time.toFixed(1)}s`; + } else if (mins > 0) { + label = `${mins}:${secs.toString().padStart(2, "0")}`; + } else { + label = `${secs}s`; + xTranslate = 5; + } + ctx.fillText(label, x - xTranslate, height - 4); + } + } + }, [zoomLevel, duration, width, height]); + + return ( + + ); +} -- cgit v1.3.1