142 lines
5.2 KiB
JavaScript
142 lines
5.2 KiB
JavaScript
// 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 (
|
|
<canvas
|
|
ref={canvasRef}
|
|
className={`drawing-overlay ${active ? "is-active" : ""} ${erase ? "is-erase" : ""} ${className}`.trim()}
|
|
aria-hidden="true"
|
|
onPointerDown={handlePointerDown}
|
|
onPointerMove={handlePointerMove}
|
|
onPointerUp={handlePointerUp}
|
|
onPointerCancel={handlePointerUp}
|
|
/>
|
|
);
|
|
}
|