From 21d135cb9ecddc660cbaf85c6b693e0404f87fc0 Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:21:25 -0300 Subject: [PATCH 1/7] Move shapes layer on top of free draw layer --- components/Canvas.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/components/Canvas.tsx b/components/Canvas.tsx index df2df71..f77b539 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -138,25 +138,25 @@ export default function Canvas() { onMouseup={handleMouseUp} onTouchStart={handleMouseDown} > - - Date: Tue, 27 Aug 2024 00:00:46 -0300 Subject: [PATCH 2/7] Add ellipse shape --- components/Canvas.tsx | 17 +++- components/ShapesLayer.tsx | 69 ++++++++----- components/Toolbar.tsx | 16 +++- components/shapes/EllipseShape.tsx | 96 +++++++++++++++++++ .../{Rectangle.tsx => RectangleShape.tsx} | 37 ++++--- 5 files changed, 187 insertions(+), 48 deletions(-) create mode 100644 components/shapes/EllipseShape.tsx rename components/shapes/{Rectangle.tsx => RectangleShape.tsx} (65%) diff --git a/components/Canvas.tsx b/components/Canvas.tsx index f77b539..9e26211 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -15,12 +15,15 @@ export interface LineType { } export interface ShapeType { + shapeName: string; id: string; - // shapeType: string; x: number; y: number; width: number; height: number; + radiusX: number; + radiusY: number; + fill?: string; stroke: string; strokeWidth: number; } @@ -44,19 +47,25 @@ export default function Canvas() { const [selectedShapeId, setSelectedShapeId] = useState(""); - const addRectangle = () => { + const addShape = (shapeName: string) => { + const newShapeId = uuid(); setShapes([ ...shapes, { - id: uuid(), + shapeName: shapeName, + id: newShapeId, x: stageSize ? stageSize.width / 2 - 100 : 0, y: stageSize ? stageSize.height / 2 - 50 : 0, width: 200, height: 100, + radiusX: 50, + radiusY: 50, stroke: color, strokeWidth: strokeWidth, + fill: undefined, // TODO }, ]); + setSelectedShapeId(newShapeId); }; function resetCanvas() { @@ -166,7 +175,7 @@ export default function Canvas() { resetCanvas={resetCanvas} strokeWidth={strokeWidth} setStrokeWidth={setStrokeWidth} - handleAddRectangle={addRectangle} + handleAddShape={addShape} /> ); diff --git a/components/ShapesLayer.tsx b/components/ShapesLayer.tsx index f5ae31b..8224278 100644 --- a/components/ShapesLayer.tsx +++ b/components/ShapesLayer.tsx @@ -1,7 +1,8 @@ import { MutableRefObject, useState } from "react"; import { Stage, Layer } from "react-konva"; import { ShapeType, StageSizeType } from "./Canvas"; -import Rectangle from "./shapes/Rectangle"; +import RectangleShape from "./shapes/RectangleShape"; +import EllipseShape from "./shapes/EllipseShape"; type ShapesLayerProps = { shapes: ShapeType[]; @@ -26,34 +27,50 @@ export default function ShapesLayer({ selectedShapeId, setSelectedShapeId, }: ShapesLayerProps) { + + function onShapeChange(newAttrs: Partial, i: number) { + console.log(newAttrs); + const newShapes = shapes.slice(); // Create a shallow copy of the shapes array + newShapes[i] = { ...newShapes[i], ...newAttrs }; // Update the specific shape with new attributes + setShapes(newShapes); + } + return ( {shapes.map((shape, i) => { - const shapeProps = { - id: shape.id, - x: shape.x, - y: shape.y, - width: shape.width, - height: shape.height, - strokeWidth: shape.strokeWidth, - stroke: shape.stroke, - }; - - return ( - { - setSelectedShapeId(shape.id); - }} - onChange={(newAttrs: ShapeType) => { - const newShapes = shapes.slice(); // deep copy - newShapes[i] = newAttrs; - setShapes(newShapes); - }} - /> - ); + switch (shape.shapeName) { + case "rectangle": + return ( + { + setSelectedShapeId(shape.id); + }} + onChange={(newAttrs: Partial) => + onShapeChange(newAttrs, i) + } + /> + ); + case "ellipse": + return ( + { + setSelectedShapeId(shape.id); + }} + onChange={(newAttrs: Partial) => + onShapeChange(newAttrs, i) + } + /> + ); + default: + console.warn(`Unknown shape: ${shape.shapeName}`); + return null; + } })} ); diff --git a/components/Toolbar.tsx b/components/Toolbar.tsx index a06a026..b337706 100644 --- a/components/Toolbar.tsx +++ b/components/Toolbar.tsx @@ -15,6 +15,7 @@ import { HexColorPicker } from "react-colorful"; import { useState } from "react"; import LineWeightRoundedIcon from "@mui/icons-material/LineWeightRounded"; import CropSquareRoundedIcon from "@mui/icons-material/CropSquareRounded"; +import CircleOutlinedIcon from "@mui/icons-material/CircleOutlined"; function LineWeightSliderValueLabel(props: SliderValueLabelProps) { const { children, value } = props; @@ -33,7 +34,7 @@ type ToolbarProps = { selectColor: (newColor: string) => void; strokeWidth: number; setStrokeWidth: (newWidth: number) => void; - handleAddRectangle: () => void; + handleAddShape: (shapeName: string) => void; }; function Toolbar({ @@ -43,7 +44,7 @@ function Toolbar({ selectColor, strokeWidth, setStrokeWidth, - handleAddRectangle, + handleAddShape, }: ToolbarProps) { // color picker const [colorPickerAnchorEl, setColorPickerAnchorEl] = @@ -94,9 +95,18 @@ function Toolbar({ {/* shapes */} - + handleAddShape("rectangle")} + > + handleAddShape("ellipse")} + > + + {/* line weight */} ; + isSelected: boolean; + onSelect: () => void; + onChange: (newAttrs: Partial) => void; +}; + +export default function EllipseShape({ + shapeProps, + isSelected, + onSelect, + onChange, +}: EllipseShapeProps) { + const shapeRef = useRef(null); + const trRef = useRef(null); + + useEffect(() => { + if (isSelected && trRef.current && shapeRef.current) { + // we need to attach transformer manually + trRef.current.nodes([shapeRef.current]); + trRef.current.getLayer()?.batchDraw(); + } + }, [isSelected]); + + return ( + <> + { + onChange({ + ...shapeProps, + x: e.target.x(), + y: e.target.y(), + }); + }} + onTransformEnd={(e) => { + // transformer is changing scale of the node + // and NOT its width or height + // but in the store we have only width and height + // to match the data better we will reset scale on transform end + const node = shapeRef.current; + if (node) { + const scaleX = node.scaleX(); + const scaleY = node.scaleY(); + + // we will reset it back + node.scaleX(1); + node.scaleY(1); + + onChange({ + ...shapeProps, + x: node.x(), + y: node.y(), + // set minimal value + radiusX: Math.max( + 5, + shapeProps.strokeWidth!, + (shapeProps.radiusX ?? node.width() / 2) * scaleX + ), + radiusY: Math.max( + 5, + shapeProps.strokeWidth!, + (shapeProps.radiusY ?? node.height() / 2) * scaleY + ), + }); + } + }} + /> + {isSelected && ( + { + // limit resize + if (Math.abs(newBox.width) < 5 || Math.abs(newBox.height) < 5) { + return oldBox; + } + return newBox; + }} + /> + )} + + ); +} diff --git a/components/shapes/Rectangle.tsx b/components/shapes/RectangleShape.tsx similarity index 65% rename from components/shapes/Rectangle.tsx rename to components/shapes/RectangleShape.tsx index f191722..5320cec 100644 --- a/components/shapes/Rectangle.tsx +++ b/components/shapes/RectangleShape.tsx @@ -1,30 +1,29 @@ -import React, { useEffect } from "react"; -import { createRoot } from "react-dom/client"; -import { Stage, Layer, Rect, Transformer } from "react-konva"; +import React, { useEffect, useRef } from "react"; +import { Rect, Transformer } from "react-konva"; import { ShapeType } from "../Canvas"; import { Node } from "konva/lib/Node"; import Konva from "konva"; -type RectangleProps = { - shapeProps: ShapeType; +type RectangleShapeProps = { + shapeProps: Partial; isSelected: boolean; onSelect: () => void; - onChange: (newAttrs: ShapeType) => void; + onChange: (newAttrs: Partial) => void; }; -export default function Rectangle({ +export default function RectangleShape({ shapeProps, isSelected, onSelect, onChange, -}: RectangleProps) { - const shapeRef = React.useRef(null); - const trRef = React.useRef(null); +}: RectangleShapeProps) { + const shapeRef = useRef(null); + const trRef = useRef(null); useEffect(() => { - if (isSelected && trRef.current) { + if (isSelected && trRef.current && shapeRef.current) { // we need to attach transformer manually - trRef.current.nodes([shapeRef.current as unknown as Node]); + trRef.current.nodes([shapeRef.current]); trRef.current.getLayer()?.batchDraw(); } }, [isSelected]); @@ -49,7 +48,7 @@ export default function Rectangle({ // and NOT its width or height // but in the store we have only width and height // to match the data better we will reset scale on transform end - const node: Node = shapeRef.current as unknown as Node; + const node = shapeRef.current; if (node) { const scaleX = node.scaleX(); const scaleY = node.scaleY(); @@ -62,8 +61,16 @@ export default function Rectangle({ x: node.x(), y: node.y(), // set minimal value - width: Math.max(5, node.width() * scaleX), - height: Math.max(node.height() * scaleY), + width: Math.max( + 5, + shapeProps.strokeWidth! * 2, + (shapeProps.width ?? node.width()) * scaleX + ), + height: Math.max( + 5, + shapeProps.strokeWidth! * 2, + (shapeProps.height ?? node.height()) * scaleY + ), }); } }} From a0e51b0ab557de1e42300dbc2e892f5de10d8d06 Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Tue, 27 Aug 2024 02:12:01 -0300 Subject: [PATCH 3/7] Add line shape --- components/Canvas.tsx | 88 ++++++++++++++++++------- components/ShapesLayer.tsx | 72 ++++++++------------- components/Toolbar.tsx | 11 +++- components/shapes/EllipseShape.tsx | 31 ++++++--- components/shapes/LineShape.tsx | 96 ++++++++++++++++++++++++++++ components/shapes/RectangleShape.tsx | 27 +++++--- 6 files changed, 236 insertions(+), 89 deletions(-) create mode 100644 components/shapes/LineShape.tsx diff --git a/components/Canvas.tsx b/components/Canvas.tsx index 9e26211..d340ff4 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -17,12 +17,13 @@ export interface LineType { export interface ShapeType { shapeName: string; id: string; - x: number; - y: number; - width: number; - height: number; - radiusX: number; - radiusY: number; + x?: number; + y?: number; + width?: number; + height?: number; + radiusX?: number; + radiusY?: number; + points?: number[]; fill?: string; stroke: string; strokeWidth: number; @@ -49,22 +50,65 @@ export default function Canvas() { const addShape = (shapeName: string) => { const newShapeId = uuid(); - setShapes([ - ...shapes, - { - shapeName: shapeName, - id: newShapeId, - x: stageSize ? stageSize.width / 2 - 100 : 0, - y: stageSize ? stageSize.height / 2 - 50 : 0, - width: 200, - height: 100, - radiusX: 50, - radiusY: 50, - stroke: color, - strokeWidth: strokeWidth, - fill: undefined, // TODO - }, - ]); + + switch (shapeName) { + case "rectangle": + setShapes([ + ...shapes, + { + shapeName: shapeName, + id: newShapeId, + x: stageSize ? stageSize.width / 2 - 100 : 0, + y: stageSize ? stageSize.height / 2 - 50 : 0, + width: 200, + height: 100, + stroke: color, + strokeWidth: strokeWidth, + }, + ]); + break; + + case "ellipse": + console.log("inside ellipse case"); + console.log("shapes = ", shapes); + setShapes([ + ...shapes, + { + shapeName: shapeName, + id: newShapeId, + x: stageSize ? stageSize.width / 2 : 0, + y: stageSize ? stageSize.height / 2 : 0, + radiusX: 100, + radiusY: 100, + stroke: color, + strokeWidth: strokeWidth, + }, + ]); + break; + + case "line": + setShapes([ + ...shapes, + { + shapeName: shapeName, + id: newShapeId, + points: [ + stageSize ? stageSize.width / 2 - 50: 0, + stageSize ? stageSize.height / 2 : 0, + stageSize ? stageSize.width / 2 + 50 : 0, + stageSize ? stageSize.height / 2 : 0, + ], + stroke: color, + strokeWidth: strokeWidth, + }, + ]); + break; + + default: + console.warn(`Unknown shapeName: ${shapeName}`); + break; + } + setSelectedShapeId(newShapeId); }; diff --git a/components/ShapesLayer.tsx b/components/ShapesLayer.tsx index 8224278..6cdf9b3 100644 --- a/components/ShapesLayer.tsx +++ b/components/ShapesLayer.tsx @@ -1,8 +1,9 @@ -import { MutableRefObject, useState } from "react"; -import { Stage, Layer } from "react-konva"; +import { MutableRefObject } from "react"; +import { Layer } from "react-konva"; import { ShapeType, StageSizeType } from "./Canvas"; import RectangleShape from "./shapes/RectangleShape"; import EllipseShape from "./shapes/EllipseShape"; +import LineShape from "./shapes/LineShape"; type ShapesLayerProps = { shapes: ShapeType[]; @@ -17,17 +18,11 @@ type ShapesLayerProps = { }; export default function ShapesLayer({ - tool, shapes, setShapes, - color, - strokeWidth, - stageSize, - isFreeDrawing, selectedShapeId, setSelectedShapeId, }: ShapesLayerProps) { - function onShapeChange(newAttrs: Partial, i: number) { console.log(newAttrs); const newShapes = shapes.slice(); // Create a shallow copy of the shapes array @@ -35,43 +30,26 @@ export default function ShapesLayer({ setShapes(newShapes); } - return ( - - {shapes.map((shape, i) => { - switch (shape.shapeName) { - case "rectangle": - return ( - { - setSelectedShapeId(shape.id); - }} - onChange={(newAttrs: Partial) => - onShapeChange(newAttrs, i) - } - /> - ); - case "ellipse": - return ( - { - setSelectedShapeId(shape.id); - }} - onChange={(newAttrs: Partial) => - onShapeChange(newAttrs, i) - } - /> - ); - default: - console.warn(`Unknown shape: ${shape.shapeName}`); - return null; - } - })} - - ); + const renderShape = (shape: ShapeType, i: number) => { + const commonProps = { + shapeProps: shape, + isSelected: shape.id === selectedShapeId, + onSelect: () => setSelectedShapeId(shape.id), + onChange: (newAttrs: Partial) => onShapeChange(newAttrs, i), + }; + + switch (shape.shapeName) { + case "rectangle": + return ; + case "ellipse": + return ; + case "line": + return ; + default: + console.warn(`Unknown shape: ${shape.shapeName}`); + return null; + } + }; + + return {shapes.map((shape, i) => renderShape(shape, i))}; } diff --git a/components/Toolbar.tsx b/components/Toolbar.tsx index b337706..735b6cb 100644 --- a/components/Toolbar.tsx +++ b/components/Toolbar.tsx @@ -16,6 +16,7 @@ import { useState } from "react"; import LineWeightRoundedIcon from "@mui/icons-material/LineWeightRounded"; import CropSquareRoundedIcon from "@mui/icons-material/CropSquareRounded"; import CircleOutlinedIcon from "@mui/icons-material/CircleOutlined"; +import HorizontalRuleRoundedIcon from "@mui/icons-material/HorizontalRuleRounded"; function LineWeightSliderValueLabel(props: SliderValueLabelProps) { const { children, value } = props; @@ -96,17 +97,23 @@ function Toolbar({ {/* shapes */} handleAddShape("rectangle")} > handleAddShape("ellipse")} > + handleAddShape("line")} + > + + {/* line weight */} { onChange({ - ...shapeProps, + ...selectedProps, x: e.target.x(), y: e.target.y(), }); @@ -60,19 +73,17 @@ export default function EllipseShape({ node.scaleY(1); onChange({ - ...shapeProps, + ...selectedProps, x: node.x(), y: node.y(), // set minimal value radiusX: Math.max( 5, - shapeProps.strokeWidth!, - (shapeProps.radiusX ?? node.width() / 2) * scaleX + (selectedProps.radiusX ?? node.width() / 2) * scaleX ), radiusY: Math.max( 5, - shapeProps.strokeWidth!, - (shapeProps.radiusY ?? node.height() / 2) * scaleY + (selectedProps.radiusY ?? node.height() / 2) * scaleY ), }); } diff --git a/components/shapes/LineShape.tsx b/components/shapes/LineShape.tsx new file mode 100644 index 0000000..4121552 --- /dev/null +++ b/components/shapes/LineShape.tsx @@ -0,0 +1,96 @@ +import React, { useEffect, useRef } from "react"; +import { Line, Transformer } from "react-konva"; +import { ShapeType } from "../Canvas"; +import Konva from "konva"; + +type LineShapeProps = { + shapeProps: Partial; + isSelected: boolean; + onSelect: () => void; + onChange: (newAttrs: Partial) => void; +}; + +export default function LineShape({ + shapeProps, + isSelected, + onSelect, + onChange, +}: LineShapeProps) { + const shapeRef = useRef(null); + const trRef = useRef(null); + + useEffect(() => { + if (isSelected && trRef.current && shapeRef.current) { + // we need to attach transformer manually + trRef.current.nodes([shapeRef.current]); + trRef.current.getLayer()?.batchDraw(); + } + }, [isSelected]); + + const { shapeName, id, points, stroke, strokeWidth } = shapeProps; + + const selectedProps = { + shapeName, + id, + points, + stroke, + strokeWidth, + }; + + return ( + <> + { + onChange({ + ...selectedProps, + x: e.target.x(), + y: e.target.y(), + }); + }} + onTransformEnd={(e) => { + const node = shapeRef.current; + if (node) { + const scaleX = node.scaleX(); + const scaleY = node.scaleY(); + + // Reset scale to 1 + node.scaleX(1); + node.scaleY(1); + + const newPoints = node + .points() + .map((point, index) => + index % 2 === 0 ? point * scaleX : point * scaleY + ); + + onChange({ + ...selectedProps, + x: node.x(), + y: node.y(), + points: newPoints, + }); + } + }} + /> + {isSelected && ( + { + // limit resize + if (Math.abs(newBox.width) < 5 || Math.abs(newBox.height) < 5) { + return oldBox; + } + return newBox; + }} + /> + )} + + ); +} diff --git a/components/shapes/RectangleShape.tsx b/components/shapes/RectangleShape.tsx index 5320cec..96cf583 100644 --- a/components/shapes/RectangleShape.tsx +++ b/components/shapes/RectangleShape.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef } from "react"; import { Rect, Transformer } from "react-konva"; import { ShapeType } from "../Canvas"; -import { Node } from "konva/lib/Node"; import Konva from "konva"; type RectangleShapeProps = { @@ -28,17 +27,31 @@ export default function RectangleShape({ } }, [isSelected]); + const { shapeName, id, x, y, width, height, stroke, strokeWidth } = + shapeProps; + + const selectedProps = { + shapeName, + id, + x, + y, + width, + height, + stroke, + strokeWidth, + }; + return ( <> { onChange({ - ...shapeProps, + ...selectedProps, x: e.target.x(), y: e.target.y(), }); @@ -57,19 +70,17 @@ export default function RectangleShape({ node.scaleX(1); node.scaleY(1); onChange({ - ...shapeProps, + ...selectedProps, x: node.x(), y: node.y(), // set minimal value width: Math.max( 5, - shapeProps.strokeWidth! * 2, - (shapeProps.width ?? node.width()) * scaleX + (selectedProps.width ?? node.width()) * scaleX ), height: Math.max( 5, - shapeProps.strokeWidth! * 2, - (shapeProps.height ?? node.height()) * scaleY + (selectedProps.height ?? node.height()) * scaleY ), }); } From 0364c5c9fcd50787e866d3cf256289e6708fb64c Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Tue, 27 Aug 2024 02:26:17 -0300 Subject: [PATCH 4/7] Set round edges and line anchor points --- components/shapes/LineShape.tsx | 3 ++- components/shapes/RectangleShape.tsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/components/shapes/LineShape.tsx b/components/shapes/LineShape.tsx index 4121552..d109aa3 100644 --- a/components/shapes/LineShape.tsx +++ b/components/shapes/LineShape.tsx @@ -45,6 +45,7 @@ export default function LineShape({ ref={shapeRef} {...selectedProps} draggable + lineCap="round" // round ends onDragEnd={(e) => { onChange({ ...selectedProps, @@ -80,7 +81,7 @@ export default function LineShape({ {isSelected && ( { // limit resize diff --git a/components/shapes/RectangleShape.tsx b/components/shapes/RectangleShape.tsx index 96cf583..b961f02 100644 --- a/components/shapes/RectangleShape.tsx +++ b/components/shapes/RectangleShape.tsx @@ -49,6 +49,7 @@ export default function RectangleShape({ ref={shapeRef} {...selectedProps} draggable + lineJoin="round" // round corners onDragEnd={(e) => { onChange({ ...selectedProps, From e49e70e39edfe81e303d1a708c0e713722b3b129 Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:56:54 -0300 Subject: [PATCH 5/7] Group shape buttons into popover --- README.md | 2 +- components/Toolbar.tsx | 82 +++++++++++++++++++++++++++++++----------- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index dc06ee3..7063e8e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A whiteboard app made with Konva library. -Latest deployed version: [Click Me](https://whiteboard.leohong.dev) +Demo: [Click Me](https://whiteboard.leohong.dev) ## Tech diff --git a/components/Toolbar.tsx b/components/Toolbar.tsx index 735b6cb..38cb3e6 100644 --- a/components/Toolbar.tsx +++ b/components/Toolbar.tsx @@ -17,6 +17,7 @@ import LineWeightRoundedIcon from "@mui/icons-material/LineWeightRounded"; import CropSquareRoundedIcon from "@mui/icons-material/CropSquareRounded"; import CircleOutlinedIcon from "@mui/icons-material/CircleOutlined"; import HorizontalRuleRoundedIcon from "@mui/icons-material/HorizontalRuleRounded"; +import ShapeLineOutlinedIcon from "@mui/icons-material/ShapeLineOutlined"; function LineWeightSliderValueLabel(props: SliderValueLabelProps) { const { children, value } = props; @@ -51,6 +52,8 @@ function Toolbar({ const [colorPickerAnchorEl, setColorPickerAnchorEl] = useState(null); + const isColorPickerAnchorElOpen = Boolean(colorPickerAnchorEl); + const handleClickColorPickerButton = ( event: React.MouseEvent ) => { @@ -61,12 +64,28 @@ function Toolbar({ setColorPickerAnchorEl(null); }; - const isColorPickerAnchorElOpen = Boolean(colorPickerAnchorEl); + // shapes + const [shapesAnchorEl, setShapesAnchorEl] = + useState(null); + + const isShapesAnchorElOpen = Boolean(shapesAnchorEl); + + const handleClickShapesButton = ( + event: React.MouseEvent + ) => { + setShapesAnchorEl(event.currentTarget); + }; + + const handleCloseShapesPopover = () => { + setShapesAnchorEl(null); + }; - // line weight + // stroke width const [lineWeightAnchorEl, setLineWeightAnchorEl] = useState(null); + const isLineWeightSliderAnchorElOpen = Boolean(lineWeightAnchorEl); + const handleClickLineWeightButton = ( event: React.MouseEvent ) => { @@ -80,9 +99,6 @@ function Toolbar({ const handleChangeStrokeWidth = (value: number) => { setStrokeWidth(value); }; - - const isLineWeightSliderAnchorElOpen = Boolean(lineWeightAnchorEl); - return (
handleAddShape("rectangle")} - > - - - handleAddShape("ellipse")} + aria-label="open to select a shape" + onClick={handleClickShapesButton} > - - - handleAddShape("line")} - > - + {/* line weight */} @@ -142,6 +146,7 @@ function Toolbar({ + {/* colorPickerPopover */} + {/* lineWeightPopover */} + + {/* shapesPopover */} + + {/* shapes */} + handleAddShape("rectangle")} + > + + + handleAddShape("ellipse")} + > + + + handleAddShape("line")} + > + + +
); } From ecd4afb677624b6ec02b83cd15168c12f47e6923 Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:06:22 -0300 Subject: [PATCH 6/7] Close shapes popover upon selecting a shape --- components/Toolbar.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/components/Toolbar.tsx b/components/Toolbar.tsx index 38cb3e6..b2d2263 100644 --- a/components/Toolbar.tsx +++ b/components/Toolbar.tsx @@ -212,19 +212,28 @@ function Toolbar({ {/* shapes */} handleAddShape("rectangle")} + onClick={() => { + handleAddShape("rectangle"); + handleCloseShapesPopover(); + }} > handleAddShape("ellipse")} + onClick={() => { + handleAddShape("ellipse"); + handleCloseShapesPopover(); + }} > handleAddShape("line")} + onClick={() => { + handleAddShape("line"); + handleCloseShapesPopover(); + }} > From 64d057112ca7881d7c82151fe3847421b33da250 Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:22:46 -0300 Subject: [PATCH 7/7] Enable change properties of selected shape --- components/Canvas.tsx | 44 +++++++++++++++++++++++++++++++----------- components/Toolbar.tsx | 1 + 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/components/Canvas.tsx b/components/Canvas.tsx index d340ff4..c9ced6b 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -39,7 +39,7 @@ export default function Canvas() { const [lines, setLines] = useState([]); const [shapes, setShapes] = useState([]); - const [color, setColor] = useState("#0000FF"); + const [strokeColor, setStrokeColor] = useState("#0000FF"); const [strokeWidth, setStrokeWidth] = useState(5); const [stageSize, setStageSize] = useState(); @@ -48,6 +48,26 @@ export default function Canvas() { const [selectedShapeId, setSelectedShapeId] = useState(""); + function updateShapeProperty(property: keyof ShapeType, value: any) { + // Dynamically update state + if (property === "strokeWidth") { + setStrokeWidth(value); + } else if (property === "stroke") { + setStrokeColor(value); + } + + // Update shape property + if (selectedShapeId !== "") { + setShapes((prevShapes) => { + return prevShapes.map((shape) => + shape.id === selectedShapeId + ? { ...shape, [property]: value } // Update the selected shape property + : shape + ); + }); + } + } + const addShape = (shapeName: string) => { const newShapeId = uuid(); @@ -62,7 +82,7 @@ export default function Canvas() { y: stageSize ? stageSize.height / 2 - 50 : 0, width: 200, height: 100, - stroke: color, + stroke: strokeColor, strokeWidth: strokeWidth, }, ]); @@ -80,7 +100,7 @@ export default function Canvas() { y: stageSize ? stageSize.height / 2 : 0, radiusX: 100, radiusY: 100, - stroke: color, + stroke: strokeColor, strokeWidth: strokeWidth, }, ]); @@ -93,12 +113,12 @@ export default function Canvas() { shapeName: shapeName, id: newShapeId, points: [ - stageSize ? stageSize.width / 2 - 50: 0, + stageSize ? stageSize.width / 2 - 50 : 0, stageSize ? stageSize.height / 2 : 0, stageSize ? stageSize.width / 2 + 50 : 0, stageSize ? stageSize.height / 2 : 0, ], - stroke: color, + stroke: strokeColor, strokeWidth: strokeWidth, }, ]); @@ -148,7 +168,7 @@ export default function Canvas() { { tool, points: [pos.x, pos.y], - stroke: color, + stroke: strokeColor, strokeWidth: strokeWidth, }, ]); @@ -195,7 +215,7 @@ export default function Canvas() { lines={lines} setLines={setLines} tool={tool} - color={color} + color={strokeColor} strokeWidth={strokeWidth} stageSize={stageSize} isFreeDrawing={isFreeDrawing} @@ -204,7 +224,7 @@ export default function Canvas() { shapes={shapes} setShapes={setShapes} tool={tool} - color={color} + color={strokeColor} strokeWidth={strokeWidth} stageSize={stageSize} isFreeDrawing={isFreeDrawing} @@ -214,11 +234,13 @@ export default function Canvas() { updateShapeProperty("stroke", newColor)} resetCanvas={resetCanvas} strokeWidth={strokeWidth} - setStrokeWidth={setStrokeWidth} + setStrokeWidth={(newWidth) => + updateShapeProperty("strokeWidth", newWidth) + } handleAddShape={addShape} /> diff --git a/components/Toolbar.tsx b/components/Toolbar.tsx index b2d2263..a0afe5f 100644 --- a/components/Toolbar.tsx +++ b/components/Toolbar.tsx @@ -99,6 +99,7 @@ function Toolbar({ const handleChangeStrokeWidth = (value: number) => { setStrokeWidth(value); }; + return (