184 lines
5.8 KiB
TypeScript
184 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
interface Point {
|
|
norm_x: number;
|
|
norm_y: number;
|
|
}
|
|
|
|
interface Impact {
|
|
id: number;
|
|
label: string;
|
|
score: number;
|
|
coords: Point;
|
|
}
|
|
|
|
interface PhotoOverlayProps {
|
|
imageUrl: string;
|
|
impacts: Impact[];
|
|
targetCorners: Point[];
|
|
isEditing?: boolean;
|
|
onImpactsChange?: (impacts: Impact[]) => void;
|
|
}
|
|
|
|
export default function PhotoOverlay({
|
|
imageUrl,
|
|
impacts,
|
|
targetCorners,
|
|
isEditing = false,
|
|
onImpactsChange
|
|
}: PhotoOverlayProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
const updateDimensions = () => {
|
|
if (containerRef.current) {
|
|
setDimensions({
|
|
width: containerRef.current.offsetWidth,
|
|
height: containerRef.current.offsetHeight,
|
|
});
|
|
}
|
|
};
|
|
|
|
updateDimensions();
|
|
window.addEventListener("resize", updateDimensions);
|
|
return () => window.removeEventListener("resize", updateDimensions);
|
|
}, []);
|
|
|
|
const handleImageClick = (e: React.MouseEvent) => {
|
|
if (!isEditing || !onImpactsChange || dragIndex !== null) return;
|
|
|
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
if (!rect) return;
|
|
|
|
// Calculer les coordonnées normalisées
|
|
const x = (e.clientX - rect.left) / dimensions.width;
|
|
const y = (e.clientY - rect.top) / dimensions.height;
|
|
|
|
// Créer un nouvel impact
|
|
const newImpact: Impact = {
|
|
id: impacts.length > 0 ? Math.max(...impacts.map(i => i.id)) + 1 : 1,
|
|
label: "bullet_hole",
|
|
score: 10,
|
|
coords: { norm_x: x, norm_y: y }
|
|
};
|
|
|
|
onImpactsChange([...impacts, newImpact]);
|
|
};
|
|
|
|
const handleMouseMove = (e: React.MouseEvent) => {
|
|
if (dragIndex === null || !onImpactsChange) return;
|
|
|
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
if (!rect) return;
|
|
|
|
const x = (e.clientX - rect.left) / dimensions.width;
|
|
const y = (e.clientY - rect.top) / dimensions.height;
|
|
|
|
const newImpacts = [...impacts];
|
|
newImpacts[dragIndex] = {
|
|
...newImpacts[dragIndex],
|
|
coords: { norm_x: x, norm_y: y }
|
|
};
|
|
|
|
onImpactsChange(newImpacts);
|
|
};
|
|
|
|
const deleteImpact = (e: React.MouseEvent, index: number) => {
|
|
e.stopPropagation();
|
|
if (!isEditing || !onImpactsChange) return;
|
|
const newImpacts = impacts.filter((_, i) => i !== index);
|
|
onImpactsChange(newImpacts);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={`relative w-full flex items-center justify-center bg-slate-900 rounded-2xl overflow-hidden border border-slate-800 shadow-2xl min-h-[400px] ${isEditing ? 'cursor-crosshair' : ''}`}
|
|
onClick={handleImageClick}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseUp={() => setDragIndex(null)}
|
|
onMouseLeave={() => setDragIndex(null)}
|
|
>
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={imageUrl}
|
|
alt="Target Analysis"
|
|
className="max-w-full max-h-[80vh] object-contain select-none"
|
|
draggable={false}
|
|
onLoad={(e) => {
|
|
const img = e.currentTarget;
|
|
setDimensions({ width: img.clientWidth, height: img.clientHeight });
|
|
}}
|
|
/>
|
|
|
|
{dimensions.width > 0 && (
|
|
<svg
|
|
className="absolute pointer-events-none"
|
|
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
|
|
style={{ width: dimensions.width, height: dimensions.height }}
|
|
>
|
|
{/* Draw Target Box if corners exist */}
|
|
{targetCorners && targetCorners.length === 4 && (
|
|
<polygon
|
|
points={targetCorners.map(c => `${c.norm_x * dimensions.width},${c.norm_y * dimensions.height}`).join(' ')}
|
|
fill="rgba(79, 70, 229, 0.15)"
|
|
stroke="#6366f1"
|
|
strokeWidth="2"
|
|
strokeDasharray="4 2"
|
|
/>
|
|
)}
|
|
|
|
{/* Draw Impacts */}
|
|
{impacts.map((impact, index) => (
|
|
<g key={impact.id}>
|
|
{/* Glow effect */}
|
|
<circle
|
|
cx={impact.coords.norm_x * dimensions.width}
|
|
cy={impact.coords.norm_y * dimensions.height}
|
|
r={isEditing ? "12" : "8"}
|
|
fill={isEditing ? "rgba(244, 63, 94, 0.2)" : "rgba(244, 63, 94, 0.3)"}
|
|
className={isEditing ? 'pointer-events-auto cursor-move' : ''}
|
|
onMouseDown={(e) => {
|
|
if (!isEditing) return;
|
|
e.stopPropagation();
|
|
setDragIndex(index);
|
|
}}
|
|
onContextMenu={(e) => deleteImpact(e, index)}
|
|
/>
|
|
{/* Main circle */}
|
|
<circle
|
|
cx={impact.coords.norm_x * dimensions.width}
|
|
cy={impact.coords.norm_y * dimensions.height}
|
|
r="5"
|
|
fill="#f43f5e"
|
|
stroke="white"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{/* Score text */}
|
|
<text
|
|
x={impact.coords.norm_x * dimensions.width}
|
|
y={impact.coords.norm_y * dimensions.height - 10}
|
|
className="text-[12px] font-bold fill-white text-center select-none"
|
|
textAnchor="middle"
|
|
style={{ textShadow: '0 0 4px rgba(0,0,0,0.9), 1px 1px 0 rgba(0,0,0,0.9)' }}
|
|
>
|
|
{impact.score}
|
|
</text>
|
|
</g>
|
|
))}
|
|
</svg>
|
|
)}
|
|
|
|
{isEditing && (
|
|
<div className="absolute bottom-4 right-4 bg-slate-950/80 backdrop-blur-md px-3 py-1.5 rounded-full text-[10px] font-bold text-indigo-400 border border-indigo-500/30">
|
|
MODE ÉDITION : CLIC POUR AJOUTER • DRAG POUR DÉPLACER • CLIC DROIT POUR SUPPRIMER
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|