// Rôle : fournit un calque canvas réutilisable pour les annotations dessinées. import { useEffect, useRef, useState } from "react"; function getCanvasPoint(event, canvas, coordinateMode = "pixel") { const rect = canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; if (coordinateMode === "percent") return { x: rect.width ? (x / rect.width) * 100 : 0, y: rect.height ? (y / rect.height) * 100 : 0 }; return { x, y }; } function pointDistance(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); } function toCanvasPoint(point, size, coordinateMode) { if (coordinateMode === "percent") return { x: (point.x / 100) * size.width, y: (point.y / 100) * size.height }; return point; } function strokeHitsPoint(stroke, point, size, coordinateMode) { const canvasPoint = toCanvasPoint(point, size, coordinateMode); const threshold = Math.max(10, (Number(stroke.width) || 4) * 2.2); return stroke.points.some((strokePoint) => pointDistance(toCanvasPoint(strokePoint, size, coordinateMode), canvasPoint) <= threshold); } function drawStroke(context, stroke, size, coordinateMode) { if (!stroke.points.length) return; context.strokeStyle = stroke.color; context.lineWidth = stroke.width; context.lineCap = "round"; context.lineJoin = "round"; context.beginPath(); stroke.points.forEach((point, index) => { const canvasPoint = toCanvasPoint(point, size, coordinateMode); if (index === 0) context.moveTo(canvasPoint.x, canvasPoint.y); else context.lineTo(canvasPoint.x, canvasPoint.y); }); context.stroke(); } export function DrawingOverlay({ active, erase, strokes, color, width, onChange, getSurfaceSize, createStrokeId, coordinateMode = "pixel", className = "" }) { const canvasRef = useRef(null); const drawingRef = useRef(null); const [size, setSize] = useState({ width: 0, height: 0 }); useEffect(() => { function syncSize() { const nextSize = getSurfaceSize?.() || { width: 0, height: 0 }; setSize({ width: Math.max(0, Math.ceil(nextSize.width || 0)), height: Math.max(0, Math.ceil(nextSize.height || 0)) }); } syncSize(); const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(syncSize) : null; const canvas = canvasRef.current; if (observer && canvas?.parentElement) observer.observe(canvas.parentElement); window.addEventListener("resize", syncSize); return () => { observer?.disconnect(); window.removeEventListener("resize", syncSize); }; }, [getSurfaceSize]); useEffect(() => { const canvas = canvasRef.current; if (!canvas || !size.width || !size.height) return; const ratio = window.devicePixelRatio || 1; canvas.width = Math.max(1, Math.floor(size.width * ratio)); canvas.height = Math.max(1, Math.floor(size.height * ratio)); canvas.style.width = `${size.width}px`; canvas.style.height = `${size.height}px`; const context = canvas.getContext("2d"); context.setTransform(ratio, 0, 0, ratio, 0, 0); context.clearRect(0, 0, size.width, size.height); strokes.forEach((stroke) => drawStroke(context, stroke, size, coordinateMode)); }, [coordinateMode, size, strokes]); function commitStroke(points) { if (points.length < 2) return; onChange([ ...strokes, { id: createStrokeId?.() || `stroke_${Date.now().toString(36)}`, color, width, points } ]); } function handlePointerDown(event) { if (!active || event.button > 0) return; event.preventDefault(); const canvas = canvasRef.current; const point = getCanvasPoint(event, canvas, coordinateMode); if (erase) { const nextStrokes = strokes.filter((stroke) => !strokeHitsPoint(stroke, point, size, coordinateMode)); if (nextStrokes.length !== strokes.length) onChange(nextStrokes); return; } canvas.setPointerCapture?.(event.pointerId); drawingRef.current = { pointerId: event.pointerId, points: [point] }; } function handlePointerMove(event) { const drawing = drawingRef.current; if (!drawing || drawing.pointerId !== event.pointerId) return; event.preventDefault(); const point = getCanvasPoint(event, canvasRef.current, coordinateMode); const lastPoint = drawing.points[drawing.points.length - 1]; if (pointDistance(toCanvasPoint(lastPoint, size, coordinateMode), toCanvasPoint(point, size, coordinateMode)) < 1.5) return; drawing.points.push(point); const canvas = canvasRef.current; const context = canvas.getContext("2d"); drawStroke(context, { color, width, points: [lastPoint, point] }, size, coordinateMode); } function handlePointerUp(event) { const drawing = drawingRef.current; if (!drawing || drawing.pointerId !== event.pointerId) return; event.preventDefault(); canvasRef.current.releasePointerCapture?.(event.pointerId); drawingRef.current = null; commitStroke(drawing.points); } return (