Skip to content

Commit f792e0c

Browse files
authored
Merge pull request #52 from Shreyanshi210205/circle_tool
feat: add support for circle shapes in canvas drawing
2 parents f3a3026 + 1530c90 commit f792e0c

File tree

1 file changed

+182
-125
lines changed

1 file changed

+182
-125
lines changed

client/src/components/Canvas.jsx

Lines changed: 182 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const SHAPE_TYPE = {
1313
RECTANGLE: "rectangle",
1414
LINE: "line",
1515
PEN: "pen",
16+
CIRCLE: "circle",
1617
};
1718

1819
export const Canvas = () => {
@@ -80,24 +81,37 @@ export const Canvas = () => {
8081
};
8182

8283
// Compute bounding box for a shape (world coords)
83-
const getShapeBBox = useCallback((shape) => {
84-
if (!shape) return null;
85-
if (shape.type === SHAPE_TYPE.RECTANGLE || shape.type === SHAPE_TYPE.LINE) {
86-
const minX = Math.min(shape.start.x, shape.end.x);
87-
const maxX = Math.max(shape.start.x, shape.end.x);
88-
const minY = Math.min(shape.start.y, shape.end.y);
89-
const maxY = Math.max(shape.start.y, shape.end.y);
90-
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
91-
}
92-
if (shape.type === SHAPE_TYPE.PEN && shape.path && shape.path.length) {
93-
const minX = Math.min(...shape.path.map(p => p.x));
94-
const maxX = Math.max(...shape.path.map(p => p.x));
95-
const minY = Math.min(...shape.path.map(p => p.y));
96-
const maxY = Math.max(...shape.path.map(p => p.y));
97-
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
98-
}
99-
return null;
100-
}, []);
84+
const getShapeBBox = useCallback((shape) => {
85+
if (!shape) return null;
86+
87+
if (shape.type === SHAPE_TYPE.RECTANGLE || shape.type === SHAPE_TYPE.LINE) {
88+
const minX = Math.min(shape.start.x, shape.end.x);
89+
const maxX = Math.max(shape.start.x, shape.end.x);
90+
const minY = Math.min(shape.start.y, shape.end.y);
91+
const maxY = Math.max(shape.start.y, shape.end.y);
92+
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
93+
}
94+
95+
if (shape.type === SHAPE_TYPE.CIRCLE) {
96+
const r = Math.max(shape.radius || 0, 0);
97+
const minX = shape.start.x - r;
98+
const maxX = shape.start.x + r;
99+
const minY = shape.start.y - r;
100+
const maxY = shape.start.y + r;
101+
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
102+
}
103+
104+
if (shape.type === SHAPE_TYPE.PEN && shape.path && shape.path.length) {
105+
const minX = Math.min(...shape.path.map(p => p.x));
106+
const maxX = Math.max(...shape.path.map(p => p.x));
107+
const minY = Math.min(...shape.path.map(p => p.y));
108+
const maxY = Math.max(...shape.path.map(p => p.y));
109+
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
110+
}
111+
112+
return null;
113+
}, []);
114+
101115

102116
// Returns array of handle objects: { x, y, dir }
103117
const getHandlesForShape = useCallback(
@@ -137,132 +151,151 @@ export const Canvas = () => {
137151
};
138152

139153
// --- Drawing Utilities ---
140-
const drawShape = useCallback((ctx, shape, isSelected = false) => {
141-
ctx.save();
142-
ctx.beginPath();
143-
ctx.strokeStyle = shape.color;
144-
ctx.lineWidth = shape.width;
145-
ctx.lineCap = "round";
146-
ctx.lineJoin = "round";
147-
ctx.setLineDash([]);
148-
ctx.globalAlpha = 1;
149-
150-
if (isSelected) {
154+
const drawShape = useCallback((ctx, shape, isSelected = false) => {
155+
ctx.save();
156+
ctx.beginPath();
157+
ctx.strokeStyle = shape.color;
158+
ctx.lineWidth = shape.width;
159+
ctx.lineCap = "round";
160+
ctx.lineJoin = "round";
161+
ctx.setLineDash([]);
162+
ctx.globalAlpha = 1;
163+
164+
if (isSelected) {
151165
// draw purple glow under the stroke for visibility
152-
ctx.save();
153-
ctx.lineWidth = shape.width + 4;
154-
ctx.strokeStyle = "rgba(76,29,149,1)";
155-
switch (shape.type) {
156-
case SHAPE_TYPE.LINE:
157-
ctx.beginPath();
158-
ctx.moveTo(shape.start.x, shape.start.y);
159-
ctx.lineTo(shape.end.x, shape.end.y);
160-
ctx.stroke();
161-
break;
162-
case SHAPE_TYPE.RECTANGLE:
163-
ctx.strokeRect(shape.start.x, shape.start.y, shape.end.x - shape.start.x, shape.end.y - shape.start.y);
164-
break;
165-
case SHAPE_TYPE.PEN:
166-
if (shape.path && shape.path.length > 1) {
167-
ctx.beginPath();
168-
ctx.moveTo(shape.path[0].x, shape.path[0].y);
169-
shape.path.forEach((p) => ctx.lineTo(p.x, p.y));
170-
ctx.stroke();
171-
}
172-
break;
173-
default:
174-
break;
175-
}
176-
ctx.restore();
177-
178-
// then draw the actual shape on top
179-
ctx.strokeStyle = shape.color;
180-
ctx.lineWidth = shape.width;
181-
}
182-
166+
ctx.save();
167+
ctx.lineWidth = shape.width + 4;
168+
ctx.strokeStyle = "rgba(76,29,149,1)";
183169
switch (shape.type) {
184170
case SHAPE_TYPE.LINE:
185171
ctx.beginPath();
186172
ctx.moveTo(shape.start.x, shape.start.y);
187173
ctx.lineTo(shape.end.x, shape.end.y);
188174
ctx.stroke();
189175
break;
190-
case SHAPE_TYPE.RECTANGLE: {
191-
const x = shape.start.x;
192-
const y = shape.start.y;
193-
const width = shape.end.x - shape.start.x;
194-
const height = shape.end.y - shape.start.y;
176+
case SHAPE_TYPE.RECTANGLE:
177+
ctx.strokeRect(
178+
shape.start.x,
179+
shape.start.y,
180+
shape.end.x - shape.start.x,
181+
shape.end.y - shape.start.y
182+
);
183+
break;
184+
case SHAPE_TYPE.CIRCLE:
195185
ctx.beginPath();
196-
ctx.strokeRect(x, y, width, height);
186+
ctx.arc(shape.start.x, shape.start.y, shape.radius || 0, 0, Math.PI * 2);
187+
ctx.stroke();
197188
break;
198-
}
199189
case SHAPE_TYPE.PEN:
200190
if (shape.path && shape.path.length > 1) {
201-
const brush = shape.brush || "solid";
202-
203-
const drawPath = (offsetJitter = 0) => {
204-
ctx.beginPath();
205-
ctx.moveTo(
206-
shape.path[0].x + (Math.random() - 0.5) * offsetJitter,
207-
shape.path[0].y + (Math.random() - 0.5) * offsetJitter
208-
);
209-
shape.path.forEach((p) =>
210-
ctx.lineTo(
211-
p.x + (Math.random() - 0.5) * offsetJitter,
212-
p.y + (Math.random() - 0.5) * offsetJitter
213-
)
214-
);
215-
ctx.stroke();
216-
};
217-
218-
// Reset any brush-specific state first
219-
ctx.setLineDash([]);
220-
ctx.shadowBlur = 0;
221-
ctx.globalAlpha = 1;
191+
ctx.beginPath();
192+
ctx.moveTo(shape.path[0].x, shape.path[0].y);
193+
shape.path.forEach((p) => ctx.lineTo(p.x, p.y));
194+
ctx.stroke();
195+
}
196+
break;
197+
default:
198+
break;
199+
}
200+
ctx.restore();
222201

223-
if (brush === "dashed") {
224-
const base = Math.max(4, shape.width * 3);
225-
const dash = Math.round(base);
226-
const gap = Math.round(base * 0.6);
227-
ctx.setLineDash([dash, gap]);
228-
ctx.lineWidth = shape.width;
229-
ctx.strokeStyle = shape.color;
230-
drawPath(0);
231-
ctx.setLineDash([]);
232-
} else if (brush === 'paint') {
233-
ctx.lineCap = "round";
234-
ctx.lineJoin = "round";
202+
// then draw the actual shape on top
203+
ctx.strokeStyle = shape.color;
204+
ctx.lineWidth = shape.width;
205+
}
235206

236-
const baseWidth = Math.max(shape.width, 1.5); // Ensure a minimum body
237-
const layers = 8;
207+
switch (shape.type) {
208+
case SHAPE_TYPE.LINE:
209+
ctx.beginPath();
210+
ctx.moveTo(shape.start.x, shape.start.y);
211+
ctx.lineTo(shape.end.x, shape.end.y);
212+
ctx.stroke();
213+
break;
238214

239-
for (let i = 0; i < layers; i++) {
240-
const opacity = 0.18 + Math.random() * 0.12;
241-
const color = tinycolor(shape.color)
242-
.brighten((Math.random() - 0.5) * 2.5)
243-
.setAlpha(opacity)
244-
.toRgbString();
215+
case SHAPE_TYPE.RECTANGLE: {
216+
const x = shape.start.x;
217+
const y = shape.start.y;
218+
const width = shape.end.x - shape.start.x;
219+
const height = shape.end.y - shape.start.y;
220+
ctx.beginPath();
221+
ctx.strokeRect(x, y, width, height);
222+
break;
223+
}
245224

246-
ctx.strokeStyle = color;
247-
ctx.globalAlpha = 0.9;
248-
const widthFactor = baseWidth < 4 ? 3.8 : 2.2;
249-
ctx.lineWidth = baseWidth * (widthFactor + i * 0.2);
225+
case SHAPE_TYPE.CIRCLE: {
226+
const r = Math.max(shape.radius || 0, 0);
227+
ctx.beginPath();
228+
ctx.arc(shape.start.x, shape.start.y, r, 0, Math.PI * 2);
229+
ctx.stroke();
230+
break;
231+
}
250232

251-
drawPath(0);
252-
}
233+
case SHAPE_TYPE.PEN:
234+
if (shape.path && shape.path.length > 1) {
235+
const brush = shape.brush || "solid";
253236

254-
ctx.globalAlpha = 0.25;
255-
ctx.lineWidth = baseWidth * (baseWidth < 4 ? 4.8 : 3.2);
256-
ctx.strokeStyle = tinycolor(shape.color)
257-
.lighten(3)
258-
.setAlpha(0.25)
237+
const drawPath = (offsetJitter = 0) => {
238+
ctx.beginPath();
239+
ctx.moveTo(
240+
shape.path[0].x + (Math.random() - 0.5) * offsetJitter,
241+
shape.path[0].y + (Math.random() - 0.5) * offsetJitter
242+
);
243+
shape.path.forEach((p) =>
244+
ctx.lineTo(
245+
p.x + (Math.random() - 0.5) * offsetJitter,
246+
p.y + (Math.random() - 0.5) * offsetJitter
247+
)
248+
);
249+
ctx.stroke();
250+
};
251+
252+
// Reset brush states
253+
ctx.setLineDash([]);
254+
ctx.shadowBlur = 0;
255+
ctx.globalAlpha = 1;
256+
257+
if (brush === "dashed") {
258+
const base = Math.max(4, shape.width * 3);
259+
const dash = Math.round(base);
260+
const gap = Math.round(base * 0.6);
261+
ctx.setLineDash([dash, gap]);
262+
ctx.lineWidth = shape.width;
263+
ctx.strokeStyle = shape.color;
264+
drawPath(0);
265+
ctx.setLineDash([]);
266+
} else if (brush === "paint") {
267+
ctx.lineCap = "round";
268+
ctx.lineJoin = "round";
269+
const baseWidth = Math.max(shape.width, 1.5);
270+
const layers = 8;
271+
272+
for (let i = 0; i < layers; i++) {
273+
const opacity = 0.18 + Math.random() * 0.12;
274+
const color = tinycolor(shape.color)
275+
.brighten((Math.random() - 0.5) * 2.5)
276+
.setAlpha(opacity)
259277
.toRgbString();
260-
drawPath(0);
261278

262-
ctx.globalAlpha = 0.95;
263-
ctx.lineWidth = baseWidth * (baseWidth < 4 ? 3.4 : 2.4);
264-
ctx.strokeStyle = shape.color;
279+
ctx.strokeStyle = color;
280+
ctx.globalAlpha = 0.9;
281+
const widthFactor = baseWidth < 4 ? 3.8 : 2.2;
282+
ctx.lineWidth = baseWidth * (widthFactor + i * 0.2);
283+
265284
drawPath(0);
285+
}
286+
287+
ctx.globalAlpha = 0.25;
288+
ctx.lineWidth = baseWidth * (baseWidth < 4 ? 4.8 : 3.2);
289+
ctx.strokeStyle = tinycolor(shape.color)
290+
.lighten(3)
291+
.setAlpha(0.25)
292+
.toRgbString();
293+
drawPath(0);
294+
295+
ctx.globalAlpha = 0.95;
296+
ctx.lineWidth = baseWidth * (baseWidth < 4 ? 3.4 : 2.4);
297+
ctx.strokeStyle = shape.color;
298+
drawPath(0);
266299

267300
ctx.globalAlpha = 1;
268301
ctx.lineWidth = shape.width;
@@ -593,6 +626,9 @@ export const Canvas = () => {
593626
newShape.brush = brushType || "solid";
594627
newShape._seed = Math.floor(Math.random() * 0xffffffff);
595628
}
629+
if (activeTool === SHAPE_TYPE.CIRCLE) {
630+
newShape.radius = 0;
631+
}
596632
newShapeId.current = newShape.id;
597633
setShapes((prev) => [...prev, newShape]);
598634
} else {
@@ -625,7 +661,7 @@ export const Canvas = () => {
625661
return;
626662
}
627663

628-
if (!isDrawing) return;
664+
if (!isDrawing) return;
629665

630666
// MOVE
631667
if (activeTool === 'select' && selectedShapeId && manipulationMode.current && manipulationMode.current.mode === 'move') {
@@ -709,7 +745,19 @@ export const Canvas = () => {
709745
sh.start = { x: nx, y: ny };
710746
sh.end = { x: nx + nw, y: ny + nh };
711747
}
712-
748+
if (sh.type === SHAPE_TYPE.CIRCLE) {
749+
// Resize based on bounding box change
750+
const cx = (minX + maxX) / 2;
751+
const cy = (minY + maxY) / 2;
752+
const newRadius = Math.max(
753+
Math.abs(maxX - minX),
754+
Math.abs(maxY - minY)
755+
) / 2;
756+
757+
sh.start = { x: cx, y: cy };
758+
sh.radius = newRadius;
759+
sh.end = { x: cx + newRadius, y: cy }; // optional
760+
}
713761
newShapes[shapeIndex] = sh;
714762
setShapes(newShapes);
715763
return;
@@ -732,6 +780,15 @@ export const Canvas = () => {
732780
} else if (cur.type === SHAPE_TYPE.LINE || cur.type === SHAPE_TYPE.RECTANGLE) {
733781
cur.end = worldPoint;
734782
}
783+
else if (cur.type === SHAPE_TYPE.CIRCLE) {
784+
// Circle creation: start = center, drag defines radius
785+
const dx = worldPoint.x - cur.start.x;
786+
const dy = worldPoint.y - cur.start.y;
787+
const r = Math.sqrt(dx * dx + dy * dy);
788+
cur.radius = r;
789+
cur.end = worldPoint; // optional for reference
790+
}
791+
735792
return newShapes;
736793
});
737794
}

0 commit comments

Comments
 (0)