diff --git a/client/src/components/Canvas.jsx b/client/src/components/Canvas.jsx
index 10b2e93..62abed2 100644
--- a/client/src/components/Canvas.jsx
+++ b/client/src/components/Canvas.jsx
@@ -15,6 +15,7 @@ const SHAPE_TYPE = {
PEN: "pen",
CIRCLE: "circle",
ERASER: "eraser",
+ IMAGE: 'image',
};
export const Canvas = () => {
@@ -52,35 +53,32 @@ export const Canvas = () => {
const [hoveredHandle, setHoveredHandle] = useState(null); // { id, dir }
const handleLogout = async () => {
- try {
- const token = localStorage.getItem("token");
- if (!token) {
- toast.error("You are not logged in.");
- return;
- }
+ // placeholder
+ };
- const res = await fetch("http://localhost:3000/api/auth/logout", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ token }),
- });
+const handleImageUpload = (file) => {
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ const img = new Image();
+ img.onload = () => {
+ const newShape = {
+ id: Date.now().toString(),
+ type: SHAPE_TYPE.IMAGE,
+ image: img,
+ start: { x: 100, y: 100 },
+ end: { x: 100 + img.width, y: 100 + img.height },
+ width: img.width,
+ height: img.height,
+ };
+ setShapes((prev) => [...prev, newShape]);
+ };
+ img.src = event.target.result;
+ };
+ reader.readAsDataURL(file);
+};
- if (!res.ok) {
- const data = await res.json().catch(() => ({}));
- toast.error(data.message || "Logout failed");
- return;
- }
- // Clear auth state and redirect
- localStorage.removeItem("token");
- setIsLoggedIn(false);
- toast.success("Logged out successfully");
- window.location.href = "/";
- } catch (err) {
- console.error("Logout error:", err);
- toast.error("Logout failed. Please try again.");
- }
- };
// --- Helpers ---
const getWorldPoint = (e) => {
@@ -136,6 +134,14 @@ export const Canvas = () => {
const maxY = Math.max(...shape.path.map(p => p.y));
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
}
+ if (shape.type === SHAPE_TYPE.IMAGE) {
+ const minX = Math.min(shape.start.x, shape.end.x);
+ const maxX = Math.max(shape.start.x, shape.end.x);
+ const minY = Math.min(shape.start.y, shape.end.y);
+ const maxY = Math.max(shape.start.y, shape.end.y);
+ return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
+}
+
return null;
}, []);
@@ -192,7 +198,7 @@ export const Canvas = () => {
if (isSelected) {
// draw purple glow under the stroke for visibility
ctx.save();
- ctx.lineWidth = shape.width + 4;
+ ctx.lineWidth = shape.type === SHAPE_TYPE.IMAGE ? 4 : shape.width + 4;
ctx.strokeStyle = "rgba(76,29,149,1)";
switch (shape.type) {
case SHAPE_TYPE.LINE:
@@ -245,7 +251,7 @@ export const Canvas = () => {
// then draw the actual shape on top
ctx.strokeStyle = shape.color;
- ctx.lineWidth = shape.width;
+ ctx.lineWidth = shape.type === SHAPE_TYPE.IMAGE ? 1 : shape.width;
}
switch (shape.type) {
@@ -517,6 +523,15 @@ export const Canvas = () => {
ctx.globalAlpha = 1;
}
break;
+ case SHAPE_TYPE.IMAGE: {
+ const { image, start, end } = shape;
+ if (image) {
+ const width = end.x - start.x;
+ const height = end.y - start.y;
+ ctx.drawImage(image, start.x, start.y, width, height);
+ }
+ break;
+ }
default:
break;
}
@@ -547,12 +562,15 @@ export const Canvas = () => {
const ctx = canvas?.getContext("2d");
if (!ctx) return;
- // clear and reset transform
- ctx.setTransform(1, 0, 0, 1, 0, 0);
- ctx.clearRect(0, 0, canvas.width, canvas.height);
+ // clear and reset transform
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.globalCompositeOperation = "source-over";
+ ctx.globalAlpha = 1;
+ ctx.shadowBlur = 0;
- // apply pan & zoom
- ctx.setTransform(1, 0, 0, 1, offset.x, offset.y);
+ // apply pan & zoom
+ ctx.setTransform(1, 0, 0, 1, offset.x, offset.y);
ctx.scale(scale, scale);
// draw non-selected shapes first
@@ -622,6 +640,7 @@ export const Canvas = () => {
if (handle && hitShape && hitShape.id === selectedShapeId) {
// begin resize
const origShape = JSON.parse(JSON.stringify(hitShape));
+ if (hitShape.type === SHAPE_TYPE.IMAGE) origShape.image = hitShape.image;
const origBBox = getShapeBBox(origShape);
manipulationMode.current = { mode: 'resize', dir: handle.dir, origShape, origBBox };
setIsDrawing(true);
@@ -629,11 +648,21 @@ export const Canvas = () => {
}
if (hitShape) {
+ try {
+ const _ctx = canvasRef.current?.getContext('2d');
+ if (_ctx) {
+ _ctx.globalCompositeOperation = 'source-over';
+ _ctx.globalAlpha = 1;
+ _ctx.shadowBlur = 0;
+ }
+ } catch (err) {
+ console.debug('[canvas] composite reset failed', err);
+ }
setSelectedShapeId(hitShape.id);
setIsDrawing(true);
- manipulationMode.current = { mode: "move" };
+ manipulationMode.current = { mode: "pending-move" };
setActiveColor(hitShape.color);
- setStrokeWidth(hitShape.width);
+ if (hitShape.type === SHAPE_TYPE.PEN) setStrokeWidth(hitShape.width);
} else {
setSelectedShapeId(null);
setIsDrawing(false);
@@ -665,7 +694,7 @@ export const Canvas = () => {
// creation tools
- if (Object.values(SHAPE_TYPE).includes(activeTool) || activeTool.startsWith('brush-')) {
+if ((Object.values(SHAPE_TYPE).includes(activeTool) || activeTool.startsWith('brush-')) && activeTool !== SHAPE_TYPE.IMAGE) {
setSelectedShapeId(null);
setIsDrawing(true);
manipulationMode.current = { mode: "create" };
@@ -686,6 +715,15 @@ export const Canvas = () => {
newShape.brush = brushType || "solid";
newShape._seed = Math.floor(Math.random() * 0xffffffff);
}
+ if (activeTool === SHAPE_TYPE.IMAGE) {
+ const hitShape = shapes.slice().reverse().find((shape) => isPointInShape(worldPoint, shape));
+ if (hitShape && hitShape.type === SHAPE_TYPE.IMAGE) {
+ // Just select, don't draw a new one
+ setSelectedShapeId(hitShape.id);
+ setIsDrawing(false);
+ return;
+ }
+}
if (activeTool === SHAPE_TYPE.CIRCLE) {
newShape.radius = 0;
}
@@ -702,8 +740,13 @@ export const Canvas = () => {
if (!shape) return false;
const bbox = getShapeBBox(shape);
if (!bbox) return false;
- const tol = shape.width + 6;
- return (point.x >= bbox.minX - tol && point.x <= bbox.maxX + tol && point.y >= bbox.minY - tol && point.y <= bbox.maxY + tol);
+ const tol = shape.type === SHAPE_TYPE.IMAGE ? 8 : (shape.width || 0) + 6;
+ return (
+ point.x >= bbox.minX - tol &&
+ point.x <= bbox.maxX + tol &&
+ point.y >= bbox.minY - tol &&
+ point.y <= bbox.maxY + tol
+ );
};
const draw = (e) => {
@@ -722,6 +765,24 @@ export const Canvas = () => {
}
if (!isDrawing) return;
+ if (
+ activeTool === 'select' &&
+ selectedShapeId &&
+ manipulationMode.current &&
+ manipulationMode.current.mode === 'pending-move'
+ ) {
+ const dx0 = worldPoint.x - pointerStart.current.x;
+ const dy0 = worldPoint.y - pointerStart.current.y;
+ const distSq0 = dx0 * dx0 + dy0 * dy0;
+ const threshold = 4 * 4; // squared threshold in world coords
+ if (distSq0 > threshold) {
+ // begin move: set pointerStart so subsequent deltas work from here
+ manipulationMode.current.mode = 'move';
+ pointerStart.current = worldPoint;
+ } else {
+ return;
+ }
+ }
// MOVE
if (activeTool === 'select' && selectedShapeId && manipulationMode.current && manipulationMode.current.mode === 'move') {
@@ -753,8 +814,9 @@ export const Canvas = () => {
const shapeIndex = shapes.findIndex((s) => s.id === selectedShapeId);
if (shapeIndex === -1) return;
- const newShapes = [...shapes];
- const sh = JSON.parse(JSON.stringify(origShape));
+ const newShapes = [...shapes];
+ const sh = JSON.parse(JSON.stringify(origShape));
+ if (origShape && origShape.type === SHAPE_TYPE.IMAGE) sh.image = origShape.image;
// We'll compute a new bounding box keeping the opposite corner fixed depending on dir
let { minX, minY, maxX, maxY } = origBBox;
@@ -873,14 +935,15 @@ export const Canvas = () => {
setIsPointerDown(false);
if (!isDrawing) return;
setIsDrawing(false);
+ const prevMode = manipulationMode.current?.mode;
newShapeId.current = null;
manipulationMode.current = null;
- if (manipulationMode.current?.mode === "erase") {
- const ctx = canvasRef.current.getContext("2d");
- ctx.globalCompositeOperation = "source-over";
-}
+ if (prevMode === "erase") {
+ const ctx = canvasRef.current?.getContext("2d");
+ if (ctx) ctx.globalCompositeOperation = "source-over";
+ }
- };
+};
// delete
const handleDeleteSelectedShape = useCallback(() => {
@@ -1053,6 +1116,7 @@ export const Canvas = () => {
onToolChange={handleToolChange}
onClear={handleClear}
onExport={handleExport}
+ onImageUpload={handleImageUpload}
/>
{joined ? (
diff --git a/client/src/components/Toolbar.jsx b/client/src/components/Toolbar.jsx
index b910c03..dcbd3da 100644
--- a/client/src/components/Toolbar.jsx
+++ b/client/src/components/Toolbar.jsx
@@ -11,13 +11,23 @@ import {
FileType,
Brush,
SquareDashed, // New import for the area select tool
+ ImagePlus,
} from "lucide-react";
import { cn } from "../lib/utils";
import { Button } from "./ui/Button";
import { Separator } from "./ui/Separator";
import { DropdownMenu, DropdownMenuItem } from "./ui/DropdownMenu";
+import { useRef } from "react";
+
+export const Toolbar = ({
+ activeTool,
+ onToolChange,
+ onClear,
+ onExport,
+ onImageUpload,
+}) => {
+ const imageInputRef = useRef(null);
-export const Toolbar = ({ activeTool, onToolChange, onClear, onExport }) => {
const tools = [
{ type: "select", icon: MousePointer2 },
{ type: "area-select", icon: SquareDashed }, // New Area Select Tool
@@ -30,7 +40,14 @@ export const Toolbar = ({ activeTool, onToolChange, onClear, onExport }) => {
const handleExport = (format) => {
onExport(format);
- }
+ };
+
+ const handleImageSelect = (e) => {
+ const file = e.target.files?.[0];
+ if (file && onImageUpload) {
+ onImageUpload(file);
+ }
+ };
const brushTypes = [
{
@@ -123,6 +140,25 @@ export const Toolbar = ({ activeTool, onToolChange, onClear, onExport }) => {
+
+
+
+
+