summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/timeline-canvas
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components/editor/timeline-canvas')
-rw-r--r--apps/web/src/components/editor/timeline-canvas/timeline-canvas-ruler-wrapper.tsx24
-rw-r--r--apps/web/src/components/editor/timeline-canvas/timeline-canvas-ruler.tsx77
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" />
+ );
+}