Skip to content

Commit

Permalink
πŸ‘‘πŸ™‡πŸ»β€β™€οΈ ↝ [SSM-79 SSM-80]: Annotation tool now has shapes
Browse files Browse the repository at this point in the history
  • Loading branch information
Gizmotronn committed Jan 2, 2025
1 parent 2213242 commit e3f2531
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 15 deletions.
55 changes: 45 additions & 10 deletions components/Projects/(classifications)/Annotating/Annotator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ import { ImageUploader } from './ImageUploader';
import { DrawingCanvas } from './DrawingCanvas';
import { DrawingControls } from './DrawingControls';
import { downloadAnnotatedImage } from './DrawingUtils';
import type { Point, Line } from '@/types/Annotation';
import type { Point, Line, Shape, DrawingMode } from '@/types/Annotation';
import { useToast } from "@/hooks/toast";

export function ImageAnnotator() {
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [isDrawing, setIsDrawing] = useState(false);
const [currentLine, setCurrentLine] = useState<Line>({ points: [], color: '#ff0000', width: 2 });
const [currentShape, setCurrentShape] = useState<Shape | null>(null);
const [lines, setLines] = useState<Line[]>([]);
const [shapes, setShapes] = useState<Shape[]>([]);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [strokeColor, setStrokeColor] = useState('#ff0000');
const [strokeWidth, setStrokeWidth] = useState(2);
const [drawingMode, setDrawingMode] = useState<DrawingMode>('freehand');
const [isDownloading, setIsDownloading] = useState(false);

const svgRef = useRef<SVGSVGElement>(null);
Expand All @@ -28,7 +31,9 @@ export function ImageAnnotator() {
const url = URL.createObjectURL(file);
setImageUrl(url);
setLines([]);
setShapes([]);
setCurrentLine({ points: [], color: strokeColor, width: strokeWidth });
setCurrentShape(null);
};

const handleImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
Expand All @@ -41,27 +46,52 @@ export function ImageAnnotator() {

const handleMouseDown = (point: Point) => {
setIsDrawing(true);
setCurrentLine({
points: [point],
color: strokeColor,
width: strokeWidth
});
if (drawingMode === 'freehand') {
setCurrentLine({
points: [point],
color: strokeColor,
width: strokeWidth
});
} else {
setCurrentShape({
type: drawingMode,
startPoint: point,
endPoint: point,
color: strokeColor,
width: strokeWidth
});
}
};

const handleMouseMove = (point: Point) => {
if (isDrawing) {
if (!isDrawing) return;

if (drawingMode === 'freehand') {
setCurrentLine(prev => ({
...prev,
points: [...prev.points, point]
}));
} else if (currentShape) {
setCurrentShape(prev => ({
...prev!,
endPoint: point
}));
}
};

const handleMouseUp = () => {
if (isDrawing && currentLine.points.length > 0) {
setLines(prev => [...prev, currentLine]);
setCurrentLine({ points: [], color: strokeColor, width: strokeWidth });
if (!isDrawing) return;

if (drawingMode === 'freehand') {
if (currentLine.points.length > 0) {
setLines(prev => [...prev, currentLine]);
setCurrentLine({ points: [], color: strokeColor, width: strokeWidth });
}
} else if (currentShape) {
setShapes(prev => [...prev, currentShape]);
setCurrentShape(null);
}

setIsDrawing(false);
};

Expand Down Expand Up @@ -114,8 +144,10 @@ export function ImageAnnotator() {
<DrawingControls
strokeColor={strokeColor}
strokeWidth={strokeWidth}
drawingMode={drawingMode}
onColorChange={setStrokeColor}
onWidthChange={setStrokeWidth}
onModeChange={setDrawingMode}
/>
<div className="relative border rounded-lg overflow-hidden">
<img
Expand All @@ -130,7 +162,10 @@ export function ImageAnnotator() {
ref={svgRef}
isDrawing={isDrawing}
currentLine={currentLine}
currentShape={currentShape}
lines={lines}
shapes={shapes}
drawingMode={drawingMode}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
Expand Down
37 changes: 35 additions & 2 deletions components/Projects/(classifications)/Annotating/DrawingCanvas.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
"use client";

import { forwardRef } from 'react';
import type { Point, Line } from '@/types/Annotation';
import type { Point, Line, Shape, DrawingMode } from '@/types/Annotation';
import { createLineGenerator, getMousePosition } from './DrawingUtils';
import { createShapePath } from '@/types/Annotation/Shapes';

interface DrawingCanvasProps {
isDrawing: boolean;
currentLine: Line;
currentShape: Shape | null;
lines: Line[];
shapes: Shape[];
drawingMode: DrawingMode;
onMouseDown: (point: Point) => void;
onMouseMove: (point: Point) => void;
onMouseUp: () => void;
Expand All @@ -18,7 +22,10 @@ interface DrawingCanvasProps {
export const DrawingCanvas = forwardRef<SVGSVGElement, DrawingCanvasProps>(({
isDrawing,
currentLine,
currentShape,
lines,
shapes,
drawingMode,
onMouseDown,
onMouseMove,
onMouseUp,
Expand Down Expand Up @@ -53,16 +60,19 @@ export const DrawingCanvas = forwardRef<SVGSVGElement, DrawingCanvasProps>(({
onMouseUp={onMouseUp}
onMouseLeave={onMouseUp}
>
{/* Completed lines */}
{lines.map((line, i) => (
<path
key={i}
key={`line-${i}`}
d={lineGenerator(line.points) || ''}
fill="none"
stroke={line.color}
strokeWidth={line.width}
vectorEffect="non-scaling-stroke"
/>
))}

{/* Current line */}
{currentLine.points.length > 0 && (
<path
d={lineGenerator(currentLine.points) || ''}
Expand All @@ -72,6 +82,29 @@ export const DrawingCanvas = forwardRef<SVGSVGElement, DrawingCanvasProps>(({
vectorEffect="non-scaling-stroke"
/>
)}

{/* Completed shapes */}
{shapes.map((shape, i) => (
<path
key={`shape-${i}`}
d={createShapePath(shape)}
fill="none"
stroke={shape.color}
strokeWidth={shape.width}
vectorEffect="non-scaling-stroke"
/>
))}

{/* Current shape */}
{currentShape && (
<path
d={createShapePath(currentShape)}
fill="none"
stroke={currentShape.color}
strokeWidth={currentShape.width}
vectorEffect="non-scaling-stroke"
/>
)}
</svg>
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
"use client";

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import type { DrawingControls } from '@/types/Annotation';
import type { DrawingControls, DrawingMode } from '@/types/Annotation';
import { Pencil, Square, Circle } from 'lucide-react';

export function DrawingControls({
strokeColor,
strokeWidth,
drawingMode,
onColorChange,
onWidthChange
onWidthChange,
onModeChange
}: DrawingControls) {
const tools: { mode: DrawingMode; icon: React.ReactNode; label: string }[] = [
{ mode: 'freehand', icon: <Pencil className="h-4 w-4" />, label: 'Freehand' },
{ mode: 'rectangle', icon: <Square className="h-4 w-4" />, label: 'Rectangle' },
{ mode: 'circle', icon: <Circle className="h-4 w-4" />, label: 'Circle' },
];

return (
<div className="flex items-center gap-4 p-4 bg-white rounded-lg shadow-sm">
<div className="flex items-center gap-2">
Expand All @@ -35,6 +45,23 @@ export function DrawingControls({
/>
<span className="text-sm text-gray-500">{strokeWidth}px</span>
</div>
<div className="flex items-center gap-2 border-l pl-4">
<Label className="sr-only">Drawing Mode:</Label>
<div className="flex gap-2">
{tools.map(({ mode, icon, label }) => (
<Button
key={mode}
variant={drawingMode === mode ? "default" : "outline"}
size="sm"
onClick={() => onModeChange(mode)}
className="gap-2"
>
{icon}
<span className="hidden sm:inline">{label}</span>
</Button>
))}
</div>
</div>
</div>
);
};
}
14 changes: 14 additions & 0 deletions types/Annotation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ export interface Point {
y: number;
};

export type DrawingMode = 'freehand' | 'rectangle' | 'circle';

export interface Shape {
type: DrawingMode;
startPoint: Point;
endPoint: Point;
color: string;
width: number;
};

export interface Line {
points: Point[];
color: string;
Expand All @@ -12,12 +22,16 @@ export interface Line {
export interface DrawingState {
isDrawing: boolean;
currentLine: Line;
currentShape: Shape | null;
lines: Line[];
shapes: Shape[];
};

export interface DrawingControls {
strokeColor: string;
strokeWidth: number;
drawingMode: DrawingMode;
onColorChange: (color: string) => void;
onWidthChange: (width: number) => void;
onModeChange: (mode: DrawingMode) => void;
};
22 changes: 22 additions & 0 deletions types/Annotation/Shapes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Point, Shape } from '../Annotation';

export const createShapePath = (shape: Shape): string => {
const { type, startPoint, endPoint } = shape;

switch (type) {
case 'rectangle': {
const width = endPoint.x - startPoint.x;
const height = endPoint.y - startPoint.y;
return `M ${startPoint.x},${startPoint.y} h ${width} v ${height} h ${-width} Z`;
}
case 'circle': {
const rx = Math.abs(endPoint.x - startPoint.x) / 2;
const ry = Math.abs(endPoint.y - startPoint.y) / 2;
const cx = startPoint.x + (endPoint.x - startPoint.x) / 2;
const cy = startPoint.y + (endPoint.y - startPoint.y) / 2;
return `M ${cx - rx},${cy} a ${rx},${ry} 0 1,0 ${rx * 2},0 a ${rx},${ry} 0 1,0 ${-rx * 2},0`;
}
default:
return '';
}
};

0 comments on commit e3f2531

Please sign in to comment.