diff options
Diffstat (limited to 'apps/web/src/components/editor/timeline-canvas')
| -rw-r--r-- | apps/web/src/components/editor/timeline-canvas/timeline-canvas-ruler-wrapper.tsx | 24 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline-canvas/timeline-canvas-ruler.tsx | 77 |
2 files changed, 101 insertions, 0 deletions
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 ( + <div + ref={ref} + className={`relative overflow-hidden h-5 w-full select-none cursor-pointer ${className}`} + onMouseDown={onMouseDown} + data-ruler-wrapper + > + {children} + </div> + ); +}); + +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<HTMLCanvasElement>(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 ( + <canvas ref={canvasRef} width={width} height={height} className="block" /> + ); +} |
