import { HTMLProps, useRef, useEffect } from "react"; type TimelineProps = { length: number; offset: number; setLength: (n: number) => void; setOffset: (n: number) => void; } & Omit, "ref">; export function TimelineBar({ length: pLength, offset: pOffset, setLength: pSetLength, setOffset: pSetOffset, ...props }: TimelineProps) { const ref = useRef(null); function setupHandler(canvas: HTMLCanvasElement) { const rect = canvas.getBoundingClientRect(); let draggingOffset = false; let draggingLength = false; let offset = pOffset; let length = pLength; function getBodyRect() { const x = canvas.width * offset; const w = Math.max(10, canvas.width * length); return { x, y: 0, w, h: canvas.height, }; } function getDragHandleRect() { const x = canvas.width * (offset + length); const w = 5; return { x, y: 0, w, h: canvas.height, }; } function render() { const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.lineWidth = 1; ctx.strokeStyle = "white"; ctx.strokeRect(0, 0, canvas.width, canvas.height); const drawBody = () => { const { x, y, w, h } = getBodyRect(); ctx.fillStyle = "white"; ctx.fillRect(x, y, w, h); }; const drawHandle = () => { const { x, y, w, h } = getDragHandleRect(); ctx.fillStyle = "#ccc"; ctx.fillRect(x, y, w, h); }; drawBody(); drawHandle(); requestAnimationFrame(render); } function scaleX(x: number) { return (x / rect.width) * canvas.width; } function getEventLocation(event: MouseEvent | TouchEvent): { x: number } { if (event instanceof TouchEvent) { return { x: scaleX(event.touches[0].clientX - rect.x), }; } else { // MouseEvent return { x: scaleX(event.clientX - rect.x), }; } } function xOfBody(x: number) { const { w } = getBodyRect(); return Math.min(1 - length, Math.max(0, (x - w / 2) / canvas.width)); } function xOfHandle(x: number) { const { w } = getDragHandleRect(); return Math.min(1, Math.max(0.1, (x - w / 2) / canvas.width - offset)); } function handleStart(event: MouseEvent | TouchEvent) { event.preventDefault(); const { x } = getEventLocation(event); const body = getBodyRect(); if (x >= body.x && x <= body.x + body.w) { draggingOffset = true; console.debug("dragging offset"); } const handle = getDragHandleRect(); if (x >= handle.x && x <= handle.x + handle.w) { draggingLength = true; console.debug("dragging length"); } } function handleMove(event: MouseEvent | TouchEvent) { event.preventDefault(); const { x } = getEventLocation(event); if (draggingLength) { const newVal = xOfHandle(x); length = newVal; } else if (draggingOffset) { const newVal = xOfBody(x); offset = newVal; } } function handleEnd(event: MouseEvent | TouchEvent) { event.preventDefault(); const { x } = getEventLocation(event); console.debug("drag end"); if (draggingLength) { const newVal = xOfHandle(x); pSetLength(newVal); } else if (draggingOffset) { const newVal = xOfBody(x); pSetOffset(newVal); } draggingLength = false; draggingOffset = false; } // Add mouse event listeners canvas.addEventListener("mousedown", handleStart); canvas.addEventListener("mousemove", handleMove); canvas.addEventListener("mouseup", handleEnd); canvas.addEventListener("mouseleave", handleEnd); // Add touch event listeners canvas.addEventListener("touchstart", handleStart); canvas.addEventListener("touchmove", handleMove); canvas.addEventListener("touchend", handleEnd); requestAnimationFrame(render); } useEffect(() => { if (ref.current) { console.debug("Setup render loop"); setupHandler(ref.current); } }, [ref.current]); return ; }