improve notepad features and add drawing canvas
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
e73d754082
commit
de1b8a5638
20 changed files with 1732 additions and 141 deletions
142
website/src/features/toolboxes/modules/DrawingOverlay.jsx
Normal file
142
website/src/features/toolboxes/modules/DrawingOverlay.jsx
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
// 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue