+ {/* Left Toolbar */}
+
+
setActiveTool('move')}
+ title="Move (V)"
+ >✥
+
+
+
+
setActiveTool('select')}
+ title="Rectangle Select (R)"
+ >▢
+
setActiveTool('ellipse')}
+ title="Ellipse Select (E)"
+ >○
+
setActiveTool('lasso')}
+ title="Free Select (F)"
+ >✎
+
setActiveTool('magic')}
+ title="Smart Select - SAM (W)"
+ >✨
+
setActiveTool('colorPick')}
+ title="Color Select (U)"
+ >◉
+
+
+
+
setActiveTool('brush')}
+ title="Brush (B)"
+ >🖌
+
setActiveTool('bucket')}
+ title="Bucket Fill (G)"
+ >◧
+
setActiveTool('eraser')}
+ title="Eraser (Shift+E)"
+ >◫
+
+
+
+
setActiveTool('eyedropper')}
+ title="Color Picker (O)"
+ >💧
+
setActiveTool('zoom')}
+ title="Zoom (Z)"
+ >🔍
+
setActiveTool('pan')}
+ title="Pan (H)"
+ >✋
+
+
+ {/* Canvas Area */}
+
+
+
+ {isProcessing && (
+
+ )}
+
+
+
+ −
+ {zoom}%
+ +
+ ⟲
+
Selection: {selection ? `${selection.bbox?.width || 0}×${selection.bbox?.height || 0}` : 'None'}
+
Tool: {TOOLS[activeTool]?.name || activeTool}
+
+
-
-
-
-
{
- setCurrentImageUrl(projectsApi.getCurrentImageUrl(project.id));
- }}
- isProcessing={isProcessing}
- setIsProcessing={setIsProcessing}
- setError={setError}
- activeToolMode={advancedToolMode}
- setActiveToolMode={setAdvancedToolMode}
- />
-
-
-
- {
- // Reload image after applying eye
- setCurrentImageUrl(projectsApi.getCurrentImageUrl(project.id));
- await loadEdits(project.id);
- }}
- isProcessing={isProcessing}
- />
-
-
-
+ {/* Right Sidebar */}
+
+ {/* Tool Options Panel */}
+
+
togglePanel('toolOptions')}>
+
Tool Options
+ {collapsedPanels.toolOptions ? '▶' : '▼'}
+
+
+
+
Feather
+
+ setFeather(parseInt(e.target.value))}
+ />
+ {feather}px
+
- )}
+
+ {/* AI Edit Panel */}
+
+
togglePanel('aiEdit')}>
+
AI Edit
+ {collapsedPanels.aiEdit ? '▶' : '▼'}
+
+
+
+
Mode
+
+ setMode('A')}
+ >Patch Only
+ setMode('B')}
+ >With Context
+
+
+
+ Prompt
+
+
+ {isProcessing ? 'Processing...' : 'Apply AI Edit'}
+
+
+
+
+ {/* Quick Actions Panel */}
+
+
togglePanel('actions')}>
+
Quick Actions
+ {collapsedPanels.actions ? '▶' : '▼'}
+
+
+ Remove Background
+
+
+
+ {/* Eyes Panel */}
+
+
togglePanel('eyes')}>
+
Eye Catalog
+ {collapsedPanels.eyes ? '▶' : '▼'}
+
+
+ {eyes.length > 0 ? (
+ <>
+
+ {eyes.map(eye => (
+
setSelectedEye(eye)}
+ >
+
+
+ ))}
+
+
Apply Eye to Selection
+ >
+ ) : (
+
No eyes in catalog
+ )}
+
+
+
+ {/* Layers Panel */}
+
+
togglePanel('layers')}>
+
Layers
+ {collapsedPanels.layers ? '▶' : '▼'}
+
+
+
+
+ 👁
+ Background
+
+ {layers.map((layer) => (
+
+ 👁
+ {layer.name}
+
+ ))}
+
+
+
+
+ {/* History Panel */}
+
+
togglePanel('history')}>
+
History
+ {collapsedPanels.history ? '▶' : '▼'}
+
+
+
+
+ Original
+
+ {edits.filter(e => e.status === 'completed').map((edit, i) => (
+
handleRevert(edit.id)}
+ >
+ {edit.prompt?.substring(0, 30) || `Edit ${i + 1}`}
+
+ ))}
+
+
+
+
);
diff --git a/frontend/src/components/ImageCanvas.css b/frontend/src/components/ImageCanvas.css
index d3164e0..f088433 100644
--- a/frontend/src/components/ImageCanvas.css
+++ b/frontend/src/components/ImageCanvas.css
@@ -1,47 +1,50 @@
+/* ImageCanvas fills the entire canvas wrapper */
.canvas-container {
- position: relative;
- width: 100%;
- height: 100%;
- min-height: calc(100vh - 180px);
- background-color: #2a2a2a;
- border: 2px solid #444;
- border-radius: 8px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
overflow: hidden;
- display: flex;
- align-items: center;
- justify-content: center;
}
.canvas-container canvas {
display: block;
}
+/* Selection hint */
+.selection-hint {
+ position: absolute;
+ bottom: 40px;
+ left: 50%;
+ transform: translateX(-50%);
+ background-color: rgba(0, 0, 0, 0.8);
+ color: #0088ff;
+ padding: 6px 12px;
+ border-radius: 4px;
+ font-size: 11px;
+ z-index: 10;
+ white-space: nowrap;
+ pointer-events: none;
+}
+
+/* Clear selection button */
.clear-selection-btn {
position: absolute;
- top: 10px;
- right: 10px;
- background-color: #ff4444;
+ top: 8px;
+ right: 8px;
+ background-color: #cc3333;
color: white;
- padding: 8px 16px;
+ padding: 6px 12px;
+ font-size: 11px;
+ border: none;
+ border-radius: 3px;
+ cursor: pointer;
z-index: 10;
}
.clear-selection-btn:hover {
- background-color: #cc0000;
-}
-
-.selection-hint {
- position: absolute;
- bottom: 10px;
- left: 50%;
- transform: translateX(-50%);
- background-color: rgba(0, 0, 0, 0.8);
- color: #00ff00;
- padding: 8px 16px;
- border-radius: 4px;
- font-size: 12px;
- z-index: 10;
- white-space: nowrap;
+ background-color: #dd4444;
}
/* Zoom controls */
diff --git a/frontend/src/components/ImageCanvas.jsx b/frontend/src/components/ImageCanvas.jsx
index e1a061e..c11d84b 100644
--- a/frontend/src/components/ImageCanvas.jsx
+++ b/frontend/src/components/ImageCanvas.jsx
@@ -1,23 +1,20 @@
-import React, { useEffect, useRef, useState, useCallback } from 'react';
+import React, { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react';
import { fabric } from 'fabric';
import './ImageCanvas.css';
-const ImageCanvas = ({
+const ImageCanvas = forwardRef(({
imageUrl,
onSelectionChange,
selectionMode,
- advancedToolMode,
- onAdvancedToolClick,
- zoom = 1,
- onZoomChange,
- externalSelection, // { polygon: [[x,y],...], bbox: {x,y,width,height} }
-}) => {
+ activeTool,
+ zoom = 100,
+ onSmartSelect,
+ isProcessing
+}, ref) => {
const canvasRef = useRef(null);
const fabricCanvasRef = useRef(null);
const [currentSelection, setCurrentSelection] = useState(null);
- const [isDrawing, setIsDrawing] = useState(false);
- const [isTransformMode, setIsTransformMode] = useState(false);
- const [currentZoom, setCurrentZoom] = useState(zoom);
+ const currentSelectionRef = useRef(null);
const lassoPoints = useRef([]);
const onZoomChangeRef = useRef(onZoomChange);
@@ -26,17 +23,28 @@ const ImageCanvas = ({
onZoomChangeRef.current = onZoomChange;
}, [onZoomChange]);
+ // Expose methods to parent
+ useImperativeHandle(ref, () => ({
+ getCanvas: () => fabricCanvasRef.current,
+ clearSelection: () => clearSelection(),
+ }));
+
+ // Update selection ref when state changes
+ useEffect(() => {
+ currentSelectionRef.current = currentSelection;
+ }, [currentSelection]);
+
+ // Initialize canvas
useEffect(() => {
if (!canvasRef.current) return;
- // Initialize Fabric.js canvas
const canvas = new fabric.Canvas(canvasRef.current, {
selection: false,
- backgroundColor: '#2a2a2a',
+ backgroundColor: 'transparent',
+ preserveObjectStacking: true,
});
fabricCanvasRef.current = canvas;
- // Handle window resize
const handleResize = () => {
const container = canvasRef.current?.parentElement;
if (container) {
@@ -45,19 +53,9 @@ const ImageCanvas = ({
canvas.setWidth(width);
canvas.setHeight(height);
- // Re-center and rescale the image if it exists
- const bgImage = canvas.backgroundImage;
- if (bgImage) {
- // Allow scaling up to fill the canvas
- const scale = Math.min(
- (width - 40) / bgImage.width,
- (height - 40) / bgImage.height
- );
- bgImage.scale(scale);
- bgImage.set({
- left: (width - bgImage.width * scale) / 2,
- top: (height - bgImage.height * scale) / 2,
- });
+ // Re-center image if it exists
+ if (imageRef.current) {
+ centerImage(canvas, imageRef.current, zoom / 100);
}
canvas.renderAll();
}
@@ -102,6 +100,39 @@ const ImageCanvas = ({
};
}, []); // Empty dependency array - only run once on mount
+ // Center and scale image
+ const centerImage = (canvas, img, zoomFactor) => {
+ if (!img) return;
+
+ const padding = 40;
+ const availableWidth = canvas.width - padding;
+ const availableHeight = canvas.height - padding;
+
+ // Calculate base scale to fit
+ const fitScale = Math.min(
+ availableWidth / img.width,
+ availableHeight / img.height
+ );
+
+ baseScaleRef.current = fitScale;
+ const scale = fitScale * zoomFactor;
+
+ img.scale(scale);
+ img.set({
+ left: (canvas.width - img.width * scale) / 2,
+ top: (canvas.height - img.height * scale) / 2,
+ });
+ };
+
+ // Apply zoom changes
+ useEffect(() => {
+ const canvas = fabricCanvasRef.current;
+ if (!canvas || !imageRef.current) return;
+
+ centerImage(canvas, imageRef.current, zoom / 100);
+ canvas.renderAll();
+ }, [zoom]);
+
// Load image when URL changes
useEffect(() => {
if (!fabricCanvasRef.current || !imageUrl) return;
@@ -143,186 +174,155 @@ const ImageCanvas = ({
top: ((canvas.height || 600) - img.height * scale) / 2,
selectable: false,
evented: false,
+ hoverCursor: 'default',
});
+ imageRef.current = img;
canvas.add(img);
canvas.sendToBack(img);
- canvas.renderAll();
- // Store image reference
- canvas.backgroundImage = img;
+ centerImage(canvas, img, zoom / 100);
+ canvas.renderAll();
}, { crossOrigin: 'anonymous' });
}, [imageUrl]);
- // Handle advanced tool mode clicks (smart-select, color-select)
- useEffect(() => {
- if (!fabricCanvasRef.current || !advancedToolMode) return;
-
- const canvas = fabricCanvasRef.current;
- const bgImage = canvas.backgroundImage;
-
- const handleAdvancedClick = async (e) => {
- if (!bgImage || !onAdvancedToolClick) return;
-
- const pointer = canvas.getPointer(e.e);
-
- // Convert canvas coordinates to image coordinates
- const imgScale = bgImage.scaleX;
- const imgLeft = bgImage.left;
- const imgTop = bgImage.top;
-
- const imgX = Math.round((pointer.x - imgLeft) / imgScale);
- const imgY = Math.round((pointer.y - imgTop) / imgScale);
-
- // Check if click is within image bounds
- if (imgX < 0 || imgY < 0 || imgX > bgImage.width || imgY > bgImage.height) {
- return;
- }
-
- if (advancedToolMode === 'color-select') {
- // Get pixel color at click position
- const ctx = canvas.getContext('2d');
- const canvasX = pointer.x * canvas.getZoom();
- const canvasY = pointer.y * canvas.getZoom();
-
- // For color picking, we need to get the color from the image
- // Create a temporary canvas to read pixel color
- const tempCanvas = document.createElement('canvas');
- tempCanvas.width = bgImage.width;
- tempCanvas.height = bgImage.height;
- const tempCtx = tempCanvas.getContext('2d');
-
- // Draw the image element to temp canvas
- const imgElement = bgImage.getElement();
- tempCtx.drawImage(imgElement, 0, 0);
-
- const pixelData = tempCtx.getImageData(imgX, imgY, 1, 1).data;
- const color = { r: pixelData[0], g: pixelData[1], b: pixelData[2] };
-
- onAdvancedToolClick(imgX, imgY, color);
- } else {
- onAdvancedToolClick(imgX, imgY, null);
- }
- };
-
- canvas.on('mouse:down', handleAdvancedClick);
-
- return () => {
- canvas.off('mouse:down', handleAdvancedClick);
- };
- }, [advancedToolMode, onAdvancedToolClick]);
-
- // Handle external selection (from smart-select or color-select)
- useEffect(() => {
- if (!fabricCanvasRef.current || !externalSelection?.polygon?.length) return;
-
- const canvas = fabricCanvasRef.current;
- const bgImage = canvas.backgroundImage;
-
- if (!bgImage) return;
-
- // Clear previous selection
- if (currentSelection) {
- canvas.remove(currentSelection);
- }
-
- // Convert image coordinates to canvas coordinates
- const imgScale = bgImage.scaleX;
- const imgLeft = bgImage.left;
- const imgTop = bgImage.top;
-
- const canvasPoints = externalSelection.polygon.map(([x, y]) => ({
- x: x * imgScale + imgLeft,
- y: y * imgScale + imgTop,
- }));
-
- // Create polygon selection
- const polygon = new fabric.Polygon(canvasPoints, {
- fill: 'rgba(255, 255, 255, 0.3)',
- stroke: '#00ff00',
- strokeWidth: 2,
- selectable: true,
- hasControls: true,
- hasBorders: true,
- lockRotation: false,
- cornerColor: '#00ff00',
- cornerSize: 10,
- transparentCorners: false,
- borderColor: '#00ff00',
- borderScaleFactor: 2,
- });
-
- canvas.add(polygon);
- canvas.setActiveObject(polygon);
- setCurrentSelection(polygon);
- lassoPoints.current = canvasPoints;
-
- // Notify parent of selection
- onSelectionChange({
- type: 'polygon',
- bbox: externalSelection.bbox,
- selectionData: { points: externalSelection.polygon },
- });
-
- canvas.renderAll();
- }, [externalSelection]);
-
- // Handle selection mode changes
+ // Handle tool/mode changes
useEffect(() => {
if (!fabricCanvasRef.current) return;
const canvas = fabricCanvasRef.current;
- // Don't set up selection handlers if in advanced tool mode
- if (advancedToolMode) return;
-
- // Clear previous selection when changing modes
- if (currentSelection) {
- canvas.remove(currentSelection);
- setCurrentSelection(null);
- onSelectionChange(null);
- }
-
- // Reset transform mode
- setIsTransformMode(false);
-
- // Set up event handlers based on mode
+ // Remove all event handlers
canvas.off('mouse:down');
canvas.off('mouse:move');
canvas.off('mouse:up');
canvas.off('object:modified');
+ canvas.off('object:moving');
+ canvas.off('object:scaling');
+ // Set up handlers based on selection mode
if (selectionMode === 'rectangle') {
setupRectangleMode(canvas);
} else if (selectionMode === 'ellipse') {
setupEllipseMode(canvas);
} else if (selectionMode === 'lasso') {
setupLassoMode(canvas);
+ } else if (selectionMode === 'smart') {
+ setupSmartSelectMode(canvas);
+ } else if (selectionMode === 'color') {
+ setupColorSelectMode(canvas);
+ } else if (activeTool === 'move') {
+ setupMoveMode(canvas);
+ } else if (activeTool === 'pan') {
+ setupPanMode(canvas);
}
- }, [selectionMode, advancedToolMode]);
+ }, [selectionMode, activeTool, onSmartSelect]);
- const setupRectangleMode = (canvas) => {
- let rect, isDown, startX, startY;
+ const setupMoveMode = (canvas) => {
+ // In move mode, allow selecting and moving selection objects
+ const sel = currentSelectionRef.current;
+ if (sel) {
+ sel.set({ selectable: true, evented: true });
+ canvas.setActiveObject(sel);
+ }
+
+ canvas.on('object:modified', (e) => {
+ if (e.target && e.target === currentSelectionRef.current) {
+ updateTransformedSelection(e.target);
+ }
+ });
+ };
+
+ const setupPanMode = (canvas) => {
+ let isPanning = false;
+ let lastPosX, lastPosY;
canvas.on('mouse:down', (e) => {
- // If clicking on existing selection, enable transform mode
- if (e.target && e.target === currentSelection) {
- setIsTransformMode(true);
+ isPanning = true;
+ lastPosX = e.e.clientX;
+ lastPosY = e.e.clientY;
+ canvas.setCursor('grabbing');
+ });
+
+ canvas.on('mouse:move', (e) => {
+ if (!isPanning) return;
+
+ const deltaX = e.e.clientX - lastPosX;
+ const deltaY = e.e.clientY - lastPosY;
+
+ canvas.relativePan({ x: deltaX, y: deltaY });
+
+ lastPosX = e.e.clientX;
+ lastPosY = e.e.clientY;
+ });
+
+ canvas.on('mouse:up', () => {
+ isPanning = false;
+ canvas.setCursor('grab');
+ });
+
+ canvas.setCursor('grab');
+ };
+
+ const setupSmartSelectMode = (canvas) => {
+ canvas.on('mouse:down', (e) => {
+ if (isProcessing) return;
+
+ const pointer = canvas.getPointer(e.e);
+ const img = imageRef.current;
+
+ if (!img) return;
+
+ // Convert to image coordinates
+ const imgScale = img.scaleX;
+ const imgLeft = img.left;
+ const imgTop = img.top;
+
+ const x = Math.round((pointer.x - imgLeft) / imgScale);
+ const y = Math.round((pointer.y - imgTop) / imgScale);
+
+ // Check if click is within image bounds
+ if (x >= 0 && x < img.width && y >= 0 && y < img.height) {
+ onSmartSelect?.(x, y);
+ }
+ });
+
+ canvas.setCursor('crosshair');
+ };
+
+ const setupColorSelectMode = (canvas) => {
+ canvas.on('mouse:down', (e) => {
+ if (isProcessing) return;
+
+ // TODO: Get pixel color at click position
+ const pointer = canvas.getPointer(e.e);
+ console.log('Color select at:', pointer);
+ });
+
+ canvas.setCursor('crosshair');
+ };
+
+ const setupRectangleMode = (canvas) => {
+ let rect = null;
+ let isDown = false;
+ let startX, startY;
+
+ canvas.on('mouse:down', (e) => {
+ // Check if clicking on existing selection
+ const sel = currentSelectionRef.current;
+ if (e.target && e.target === sel) {
+ // Allow moving/transforming
return;
}
- // If in transform mode and clicking elsewhere, exit transform mode
- if (isTransformMode) {
- setIsTransformMode(false);
- }
-
- // Clear previous selection if exists
- if (currentSelection) {
- canvas.remove(currentSelection);
+ // Clear previous selection
+ if (sel) {
+ canvas.remove(sel);
setCurrentSelection(null);
}
isDown = true;
+ isDrawingRef.current = true;
const pointer = canvas.getPointer(e.e);
startX = pointer.x;
startY = pointer.y;
@@ -332,26 +332,24 @@ const ImageCanvas = ({
top: startY,
width: 0,
height: 0,
- fill: 'rgba(255, 255, 255, 0.3)',
- stroke: '#00ff00',
+ fill: 'rgba(0, 136, 255, 0.2)',
+ stroke: '#0088ff',
strokeWidth: 2,
+ strokeDashArray: [5, 5],
selectable: true,
hasControls: true,
hasBorders: true,
- lockRotation: false,
- cornerColor: '#00ff00',
- cornerSize: 10,
+ cornerColor: '#0088ff',
+ cornerSize: 8,
transparentCorners: false,
- borderColor: '#00ff00',
- borderScaleFactor: 2,
+ borderColor: '#0088ff',
});
canvas.add(rect);
- setCurrentSelection(rect);
});
canvas.on('mouse:move', (e) => {
- if (!isDown || isTransformMode) return;
+ if (!isDown || !rect) return;
const pointer = canvas.getPointer(e.e);
const width = pointer.x - startX;
@@ -368,39 +366,40 @@ const ImageCanvas = ({
});
canvas.on('mouse:up', () => {
- if (isDown && !isTransformMode) {
+ if (isDown && rect && rect.width > 5 && rect.height > 5) {
isDown = false;
+ isDrawingRef.current = false;
+ setCurrentSelection(rect);
canvas.setActiveObject(rect);
updateSelection(rect, 'rectangle');
+ } else if (isDown && rect) {
+ // Selection too small, remove it
+ canvas.remove(rect);
+ isDown = false;
+ isDrawingRef.current = false;
}
});
- // Update selection when object is modified (moved, scaled, rotated)
canvas.on('object:modified', (e) => {
- if (e.target && e.target === currentSelection) {
- updateTransformedSelection(e.target, 'rectangle');
+ if (e.target === currentSelectionRef.current) {
+ updateTransformedSelection(e.target);
}
});
};
const setupEllipseMode = (canvas) => {
- let ellipse, isDown, startX, startY;
+ let ellipse = null;
+ let isDown = false;
+ let startX, startY;
canvas.on('mouse:down', (e) => {
- // If clicking on existing selection, enable transform mode
- if (e.target && e.target === currentSelection) {
- setIsTransformMode(true);
+ const sel = currentSelectionRef.current;
+ if (e.target && e.target === sel) {
return;
}
- // If in transform mode and clicking elsewhere, exit transform mode
- if (isTransformMode) {
- setIsTransformMode(false);
- }
-
- // Clear previous selection if exists
- if (currentSelection) {
- canvas.remove(currentSelection);
+ if (sel) {
+ canvas.remove(sel);
setCurrentSelection(null);
}
@@ -414,26 +413,24 @@ const ImageCanvas = ({
top: startY,
rx: 0,
ry: 0,
- fill: 'rgba(255, 255, 255, 0.3)',
- stroke: '#00ff00',
+ fill: 'rgba(0, 136, 255, 0.2)',
+ stroke: '#0088ff',
strokeWidth: 2,
+ strokeDashArray: [5, 5],
selectable: true,
hasControls: true,
hasBorders: true,
- lockRotation: false,
- cornerColor: '#00ff00',
- cornerSize: 10,
+ cornerColor: '#0088ff',
+ cornerSize: 8,
transparentCorners: false,
- borderColor: '#00ff00',
- borderScaleFactor: 2,
+ borderColor: '#0088ff',
});
canvas.add(ellipse);
- setCurrentSelection(ellipse);
});
canvas.on('mouse:move', (e) => {
- if (!isDown || isTransformMode) return;
+ if (!isDown || !ellipse) return;
const pointer = canvas.getPointer(e.e);
const rx = Math.abs(pointer.x - startX) / 2;
@@ -442,58 +439,55 @@ const ImageCanvas = ({
ellipse.set({
rx: rx,
ry: ry,
- left: startX < pointer.x ? startX : pointer.x,
- top: startY < pointer.y ? startY : pointer.y,
+ left: Math.min(startX, pointer.x),
+ top: Math.min(startY, pointer.y),
});
canvas.renderAll();
});
canvas.on('mouse:up', () => {
- if (isDown && !isTransformMode) {
+ if (isDown && ellipse && ellipse.rx > 5 && ellipse.ry > 5) {
isDown = false;
+ setCurrentSelection(ellipse);
canvas.setActiveObject(ellipse);
updateSelection(ellipse, 'ellipse');
+ } else if (isDown && ellipse) {
+ canvas.remove(ellipse);
+ isDown = false;
}
});
- // Update selection when object is modified (moved, scaled, rotated)
canvas.on('object:modified', (e) => {
- if (e.target && e.target === currentSelection) {
- updateTransformedSelection(e.target, 'ellipse');
+ if (e.target === currentSelectionRef.current) {
+ updateTransformedSelection(e.target);
}
});
};
const setupLassoMode = (canvas) => {
- let polygon, points = [], drawingLine;
+ let points = [];
+ let drawingLine = null;
+ let polygon = null;
canvas.on('mouse:down', (e) => {
- // If clicking on existing selection, enable transform mode
- if (e.target && e.target === currentSelection) {
- setIsTransformMode(true);
+ const sel = currentSelectionRef.current;
+ if (e.target && e.target === sel) {
return;
}
- // If in transform mode and clicking elsewhere, exit transform mode
- if (isTransformMode) {
- setIsTransformMode(false);
- }
-
- // Clear previous selection if exists
- if (currentSelection) {
- canvas.remove(currentSelection);
+ if (sel) {
+ canvas.remove(sel);
setCurrentSelection(null);
}
- setIsDrawing(true);
+ isDrawingRef.current = true;
const pointer = canvas.getPointer(e.e);
points = [{ x: pointer.x, y: pointer.y }];
- // Create a temporary line for visual feedback while drawing
drawingLine = new fabric.Polyline(points, {
fill: 'transparent',
- stroke: '#00ff00',
+ stroke: '#0088ff',
strokeWidth: 2,
selectable: false,
evented: false,
@@ -503,16 +497,15 @@ const ImageCanvas = ({
});
canvas.on('mouse:move', (e) => {
- if (!isDrawing || isTransformMode) return;
+ if (!isDrawingRef.current) return;
const pointer = canvas.getPointer(e.e);
points.push({ x: pointer.x, y: pointer.y });
- // Remove old line and create new one with updated points
canvas.remove(drawingLine);
drawingLine = new fabric.Polyline([...points], {
fill: 'transparent',
- stroke: '#00ff00',
+ stroke: '#0088ff',
strokeWidth: 2,
selectable: false,
evented: false,
@@ -522,59 +515,50 @@ const ImageCanvas = ({
});
canvas.on('mouse:up', () => {
- if (isDrawing && !isTransformMode && points.length > 2) {
- setIsDrawing(false);
+ if (isDrawingRef.current && points.length > 5) {
+ isDrawingRef.current = false;
lassoPoints.current = [...points];
- // Remove drawing line
canvas.remove(drawingLine);
- // Create final polygon with fill
polygon = new fabric.Polygon(points, {
- fill: 'rgba(255, 255, 255, 0.3)',
- stroke: '#00ff00',
+ fill: 'rgba(0, 136, 255, 0.2)',
+ stroke: '#0088ff',
strokeWidth: 2,
+ strokeDashArray: [5, 5],
selectable: true,
hasControls: true,
hasBorders: true,
- lockRotation: false,
- cornerColor: '#00ff00',
- cornerSize: 10,
+ cornerColor: '#0088ff',
+ cornerSize: 8,
transparentCorners: false,
- borderColor: '#00ff00',
- borderScaleFactor: 2,
+ borderColor: '#0088ff',
});
canvas.add(polygon);
canvas.setActiveObject(polygon);
setCurrentSelection(polygon);
updateSelection(polygon, 'lasso');
- } else if (isDrawing) {
- setIsDrawing(false);
+ } else if (isDrawingRef.current) {
+ isDrawingRef.current = false;
canvas.remove(drawingLine);
}
});
- // Update selection when object is modified (moved, scaled, rotated)
canvas.on('object:modified', (e) => {
- if (e.target && e.target === currentSelection) {
- updateTransformedSelection(e.target, 'lasso');
+ if (e.target === currentSelectionRef.current) {
+ updateTransformedSelection(e.target);
}
});
};
const updateSelection = (selection, type) => {
- if (!selection || !fabricCanvasRef.current) return;
+ if (!selection || !imageRef.current) return;
- const canvas = fabricCanvasRef.current;
- const bgImage = canvas.backgroundImage;
-
- if (!bgImage) return;
-
- // Calculate bounding box in original image coordinates
- const imgScale = bgImage.scaleX;
- const imgLeft = bgImage.left;
- const imgTop = bgImage.top;
+ const img = imageRef.current;
+ const imgScale = img.scaleX;
+ const imgLeft = img.left;
+ const imgTop = img.top;
let bbox, selectionData = null;
@@ -601,39 +585,32 @@ const ImageCanvas = ({
height: Math.round(bounds.height / imgScale),
};
- // Convert lasso points to relative coordinates within bbox
const relativePoints = lassoPoints.current.map(p => [
- Math.round((p.x - bounds.left) / imgScale),
- Math.round((p.y - bounds.top) / imgScale),
+ Math.round((p.x - imgLeft) / imgScale) - bbox.x,
+ Math.round((p.y - imgTop) / imgScale) - bbox.y,
]);
selectionData = { points: relativePoints };
}
- onSelectionChange({
+ onSelectionChange?.({
type,
bbox,
selectionData,
});
};
- // Update selection after transformation (move, scale, rotate)
- const updateTransformedSelection = (selection, type) => {
- if (!selection || !fabricCanvasRef.current) return;
+ const updateTransformedSelection = (selection) => {
+ if (!selection || !imageRef.current) return;
- const canvas = fabricCanvasRef.current;
- const bgImage = canvas.backgroundImage;
+ const img = imageRef.current;
+ const imgScale = img.scaleX;
+ const imgLeft = img.left;
+ const imgTop = img.top;
- if (!bgImage) return;
-
- const imgScale = bgImage.scaleX;
- const imgLeft = bgImage.left;
- const imgTop = bgImage.top;
-
- // Get the transformed bounding rect (accounts for scale and rotation)
const bounds = selection.getBoundingRect(true);
- let bbox = {
+ const bbox = {
x: Math.round((bounds.left - imgLeft) / imgScale),
y: Math.round((bounds.top - imgTop) / imgScale),
width: Math.round(bounds.width / imgScale),
@@ -641,8 +618,8 @@ const ImageCanvas = ({
};
let selectionData = null;
+ const type = selection.type === 'polygon' ? 'lasso' : (selection.type === 'ellipse' ? 'ellipse' : 'rectangle');
- // For lasso, we need to transform the points based on the object's transformation
if (type === 'lasso' && lassoPoints.current.length > 0) {
const matrix = selection.calcTransformMatrix();
const transformedPoints = lassoPoints.current.map(p => {
@@ -651,14 +628,14 @@ const ImageCanvas = ({
matrix
);
return [
- Math.round((transformed.x - bounds.left) / imgScale),
- Math.round((transformed.y - bounds.top) / imgScale),
+ Math.round((transformed.x - imgLeft) / imgScale) - bbox.x,
+ Math.round((transformed.y - imgTop) / imgScale) - bbox.y,
];
});
selectionData = { points: transformedPoints };
}
- onSelectionChange({
+ onSelectionChange?.({
type,
bbox,
selectionData,
@@ -666,10 +643,12 @@ const ImageCanvas = ({
};
const clearSelection = () => {
- if (currentSelection && fabricCanvasRef.current) {
- fabricCanvasRef.current.remove(currentSelection);
+ const canvas = fabricCanvasRef.current;
+ const sel = currentSelectionRef.current;
+ if (sel && canvas) {
+ canvas.remove(sel);
setCurrentSelection(null);
- onSelectionChange(null);
+ onSelectionChange?.(null);
}
};
@@ -705,36 +684,15 @@ const ImageCanvas = ({
return (
-
- {/* Zoom controls */}
-
- −
- {Math.round(currentZoom * 100)}%
- +
- ⟲
-
-
- {/* Advanced tool mode indicator */}
- {advancedToolMode && (
-
- {advancedToolMode === 'smart-select' && 'Click on an object to select it'}
- {advancedToolMode === 'color-select' && 'Click on a color to select similar pixels'}
- {advancedToolMode === 'object-remove' && 'Click on an object to remove it'}
-
- )}
-
- {currentSelection && !advancedToolMode && (
- <>
-
- Click selection to move/resize/rotate
-
-
- Clear Selection
-
- >
+ {currentSelection && (
+
+ Clear
+
)}
);
-};
+});
+
+ImageCanvas.displayName = 'ImageCanvas';
export default ImageCanvas;