improve notepad features and add drawing canvas
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s

This commit is contained in:
Shinuwa 2026-07-29 13:51:42 +02:00
parent e73d754082
commit de1b8a5638
20 changed files with 1732 additions and 141 deletions

View 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}
/>
);
}