From 8a553256b2679d05c2927f5bfd586fb7e14dafa1 Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:57:06 -0300 Subject: [PATCH 01/10] Add text panel --- components/Canvas.tsx | 68 ++++-- components/textFields/TextFieldsLayer.tsx | 5 + .../toolbar/{SidePanel.tsx => ShapePanel.tsx} | 2 +- components/toolbar/TextPanel.tsx | 220 ++++++++++++++++++ 4 files changed, 275 insertions(+), 20 deletions(-) rename components/toolbar/{SidePanel.tsx => ShapePanel.tsx} (99%) create mode 100644 components/toolbar/TextPanel.tsx diff --git a/components/Canvas.tsx b/components/Canvas.tsx index a7e28ec..a6378bf 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -24,7 +24,8 @@ import { TEXT_DEFAULT_HEIGHT, TEXT_DEFAULT_WIDTH, } from "./textFields/textFieldUtils"; -import SidePanel from "./toolbar/SidePanel"; +import ShapePanel from "./toolbar/ShapePanel"; +import TextPanel from "./toolbar/TextPanel"; export interface StageSizeType { width: number; @@ -82,7 +83,8 @@ export default function Canvas() { const [isConfirmationModalOpen, setConfirmationModalOpen] = useState(false); // confirmation modal for delete button - clear canvas - const [isSidePanelVisible, setSidePanelVisible] = useState(false); + const [isShapePanelVisible, setShapePanelVisible] = useState(false); + const [isTextPanelVisible, setTextPanelVisible] = useState(false); // Dark mode listener useEffect(() => { @@ -309,7 +311,7 @@ export default function Canvas() { setNewObject(newShape); dispatch(selectCanvasObject(newShapeId)); - setSidePanelVisible(true); + setShapePanelVisible(true); }; const handleMouseDown = (e: any) => { @@ -372,12 +374,17 @@ export default function Canvas() { dispatch(selectCanvasObject("")); } + // side panel if ( e.target === e.target.getStage() || - e.target.attrs.name?.includes("ink") || - e.target.attrs.name?.includes("text") + e.target.attrs.name?.includes("ink") ) { - setSidePanelVisible(false); + setShapePanelVisible(false); + setTextPanelVisible(false); + } else if (e.target.attrs.name?.includes("text")) { + setShapePanelVisible(false); + } else if (e.target.attrs.name?.includes("shape")) { + setTextPanelVisible(false); } }; @@ -455,7 +462,7 @@ export default function Canvas() { setSelectedObjectId={(newObjectId) => dispatch(selectCanvasObject(newObjectId)) } - setSidePanelVisible={setSidePanelVisible} + setSidePanelVisible={setShapePanelVisible} /> - {isSidePanelVisible && ( - setSidePanelVisible(false)} - strokeWidth={strokeWidth} - setStrokeWidth={(newWidth) => updateStyle("strokeWidth", newWidth)} - color={strokeColor} - onSelectColor={(newColor) => updateStyle("stroke", newColor)} - fillColor={fillColor} - onSelectFillColor={(newColor) => updateStyle("fill", newColor)} - /> - )} + setShapePanelVisible(false)} + strokeWidth={strokeWidth} + setStrokeWidth={(newWidth) => updateStyle("strokeWidth", newWidth)} + color={strokeColor} + onSelectColor={(newColor) => updateStyle("stroke", newColor)} + fillColor={fillColor} + onSelectFillColor={(newColor) => updateStyle("fill", newColor)} + /> + setTextPanelVisible(false)} + textSize={0} + setTextSize={function (newSize: number): void { + throw new Error("Function not implemented."); + }} + textStyle={[]} + setTextStyle={function (newStyle: string[]): void { + throw new Error("Function not implemented."); + }} + textColor={""} + onSelectTextColor={function (newColor: string): void { + throw new Error("Function not implemented."); + }} + textAlign={""} + setTextAlign={function (newAlign: string): void { + throw new Error("Function not implemented."); + }} + lineHeight={0} + setLineHeight={function (newHeight: number): void { + throw new Error("Function not implemented."); + }} + /> ); } diff --git a/components/textFields/TextFieldsLayer.tsx b/components/textFields/TextFieldsLayer.tsx index e873f04..be7fb2a 100644 --- a/components/textFields/TextFieldsLayer.tsx +++ b/components/textFields/TextFieldsLayer.tsx @@ -13,6 +13,7 @@ type Props = { newAttrs: Partial, selectedObjectId: string, ) => void; + setSidePanelVisible: (isVisible: boolean) => void; }; export default function TextFieldsLayer({ @@ -21,6 +22,7 @@ export default function TextFieldsLayer({ selectedObjectId, setSelectedObjectId, onChange, + setSidePanelVisible, }: Props) { const { selectedTool } = useSelector((state: RootState) => state.canvas); @@ -40,6 +42,9 @@ export default function TextFieldsLayer({ if (selectedTool === "select") { setSelectedObjectId(text.id); + // Open side panel + setSidePanelVisible(true); + // Update cursor style const stage = e.target.getStage(); if (stage) { diff --git a/components/toolbar/SidePanel.tsx b/components/toolbar/ShapePanel.tsx similarity index 99% rename from components/toolbar/SidePanel.tsx rename to components/toolbar/ShapePanel.tsx index f79413a..00b5e73 100644 --- a/components/toolbar/SidePanel.tsx +++ b/components/toolbar/ShapePanel.tsx @@ -26,7 +26,7 @@ type Props = { onSelectFillColor: (newColor: string) => void; }; -export default function SidePanel({ +export default function ShapePanel({ onClose, isOpen, strokeWidth, diff --git a/components/toolbar/TextPanel.tsx b/components/toolbar/TextPanel.tsx new file mode 100644 index 0000000..b821ef0 --- /dev/null +++ b/components/toolbar/TextPanel.tsx @@ -0,0 +1,220 @@ +import * as React from "react"; +import { + Box, + Drawer, + Typography, + Button, + Dialog, + DialogTitle, + DialogContent, + Divider, + IconButton, + TextField, + Slider, + ToggleButtonGroup, + ToggleButton, +} from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import { HexColorPicker } from "react-colorful"; +import FormatBoldIcon from "@mui/icons-material/FormatBold"; +import FormatItalicIcon from "@mui/icons-material/FormatItalic"; +import FormatUnderlinedIcon from "@mui/icons-material/FormatUnderlined"; +import FormatAlignLeftIcon from "@mui/icons-material/FormatAlignLeft"; +import FormatAlignCenterIcon from "@mui/icons-material/FormatAlignCenter"; +import FormatAlignRightIcon from "@mui/icons-material/FormatAlignRight"; + +type Props = { + onClose: () => void; + isOpen: boolean; + textSize: number; + setTextSize: (newSize: number) => void; + textStyle: string[]; + setTextStyle: (newStyle: string[]) => void; + textColor: string; + onSelectTextColor: (newColor: string) => void; + textAlign: string; + setTextAlign: (newAlign: string) => void; + lineHeight: number; + setLineHeight: (newHeight: number) => void; +}; + +export default function TextPanel({ + onClose, + isOpen, + textSize, + setTextSize, + textStyle, + setTextStyle, + textColor, + onSelectTextColor, + textAlign, + setTextAlign, + lineHeight, + setLineHeight, +}: Props) { + const [colorPickerOpen, setColorPickerOpen] = React.useState(false); + + const handleColorPickerOpen = () => { + setColorPickerOpen(true); + }; + + const handleColorChange = (color: string) => { + onSelectTextColor(color); + }; + + const handleTextStyleChange = ( + event: React.MouseEvent, + newStyles: string[], + ) => { + setTextStyle(newStyles); + }; + + const handleTextAlignChange = ( + event: React.MouseEvent, + newAlign: string, + ) => { + setTextAlign(newAlign); + }; + + const drawer = ( + <> + + Edit text + + + + + + {/* Text Size Section */} + + + Size + setTextSize(Number(e.target.value))} + InputProps={{ inputProps: { min: 1 } }} + sx={{ width: 80, mt: 1 }} + /> + + + {/* Text Style Section */} + + + Style + + + + + + + + + + + + + + {/* Text Color Section */} + + + Color + + + + + + + {/* Text Alignment Section */} + + + Alignment + + + + + + + + + + + + + + {/* Line Spacing Section */} + + + Line spacing + setLineHeight(Number(e.target.value))} + InputProps={{ inputProps: { min: 1 } }} + sx={{ width: 80, mt: 1 }} + /> + + + {/* Color Picker Dialog */} + setColorPickerOpen(false)}> + Text Color + + + + + + ); + + return ( + + {drawer} + + ); +} From 6278766f478034439d7dcd047c1c5c91450b3e45 Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Sat, 28 Sep 2024 23:46:56 -0300 Subject: [PATCH 02/10] Rename --- components/Canvas.tsx | 9 +++------ components/{textFields => text}/TextField.tsx | 2 +- .../TextFieldsLayer.tsx => text/TextLayer.tsx} | 2 +- .../{textFields/textFieldUtils.ts => text/textUtils.ts} | 0 4 files changed, 5 insertions(+), 8 deletions(-) rename components/{textFields => text}/TextField.tsx (99%) rename components/{textFields/TextFieldsLayer.tsx => text/TextLayer.tsx} (97%) rename components/{textFields/textFieldUtils.ts => text/textUtils.ts} (100%) diff --git a/components/Canvas.tsx b/components/Canvas.tsx index a6378bf..6a5e009 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -6,7 +6,7 @@ import Toolbar from "./toolbar/Toolbar"; import InkLayer from "./ink/InkLayer"; import ShapesLayer from "./shapes/ShapesLayer"; import ConfirmationDialog from "./ConfirmationDialog"; -import TextFieldsLayer from "./textFields/TextFieldsLayer"; +import TextLayer from "./text/TextLayer"; import { RootState } from "../redux/store"; import { addCanvasObject, @@ -20,10 +20,7 @@ import { updateSelectedTool, } from "../redux/canvasSlice"; import { SHAPE_DEFAULT_HEIGHT, SHAPE_DEFAULT_WIDTH } from "./shapes/shapeUtils"; -import { - TEXT_DEFAULT_HEIGHT, - TEXT_DEFAULT_WIDTH, -} from "./textFields/textFieldUtils"; +import { TEXT_DEFAULT_HEIGHT, TEXT_DEFAULT_WIDTH } from "./text/textUtils"; import ShapePanel from "./toolbar/ShapePanel"; import TextPanel from "./toolbar/TextPanel"; @@ -464,7 +461,7 @@ export default function Canvas() { } setSidePanelVisible={setShapePanelVisible} /> - ; diff --git a/components/textFields/TextFieldsLayer.tsx b/components/text/TextLayer.tsx similarity index 97% rename from components/textFields/TextFieldsLayer.tsx rename to components/text/TextLayer.tsx index be7fb2a..3107381 100644 --- a/components/textFields/TextFieldsLayer.tsx +++ b/components/text/TextLayer.tsx @@ -16,7 +16,7 @@ type Props = { setSidePanelVisible: (isVisible: boolean) => void; }; -export default function TextFieldsLayer({ +export default function TextLayer({ objects, newObject, selectedObjectId, diff --git a/components/textFields/textFieldUtils.ts b/components/text/textUtils.ts similarity index 100% rename from components/textFields/textFieldUtils.ts rename to components/text/textUtils.ts From 202b662e741f06303cb15b8ec975b3d1efed4716 Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Sun, 29 Sep 2024 01:00:09 -0300 Subject: [PATCH 03/10] Adjust textarea move up amount --- components/text/TextField.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/text/TextField.tsx b/components/text/TextField.tsx index d6c3184..13e47dc 100644 --- a/components/text/TextField.tsx +++ b/components/text/TextField.tsx @@ -85,7 +85,7 @@ export default function TextField({ document.body.appendChild(textarea); // adjust the styles to match - textarea.id = `textarea-${node.id()}`; + textarea.id = `text-${node.id()}-textarea`; textarea.value = node.text(); textarea.style.position = "absolute"; textarea.style.top = `${areaPosition.y}px`; @@ -117,7 +117,7 @@ export default function TextField({ // slightly move textarea up // because it jumps a bit - const moveUpPx = Math.round(node.fontSize() / 20); + const moveUpPx = 2; transform += `translateY(-${moveUpPx}px)`; textarea.style.transform = transform; From bad872ee0e0ab20ecb77d20a4459da5e33a85021 Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Sun, 29 Sep 2024 01:53:24 -0300 Subject: [PATCH 04/10] Redux refactor for text and shape configurations --- .vscode/settings.json | 2 +- components/Canvas.tsx | 45 +++++++++--------------- components/toolbar/TextPanel.tsx | 57 ++++++++++++++---------------- redux/shapeSlice.ts | 33 ++++++++++++++++++ redux/store.ts | 4 +++ redux/textSlice.ts | 59 ++++++++++++++++++++++++++++++++ 6 files changed, 139 insertions(+), 61 deletions(-) create mode 100644 redux/shapeSlice.ts create mode 100644 redux/textSlice.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index b319c44..cca127c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { "prettier.prettierPath": "./node_modules/prettier", - "cSpell.words": ["Konva"] + "cSpell.words": ["Konva", "reduxjs"] } diff --git a/components/Canvas.tsx b/components/Canvas.tsx index 6a5e009..40d8d67 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -23,6 +23,11 @@ import { SHAPE_DEFAULT_HEIGHT, SHAPE_DEFAULT_WIDTH } from "./shapes/shapeUtils"; import { TEXT_DEFAULT_HEIGHT, TEXT_DEFAULT_WIDTH } from "./text/textUtils"; import ShapePanel from "./toolbar/ShapePanel"; import TextPanel from "./toolbar/TextPanel"; +import { + setFillColor, + setStrokeColor, + setStrokeWidth, +} from "@/redux/shapeSlice"; export interface StageSizeType { width: number; @@ -63,19 +68,20 @@ export type ToolType = export type ShapeName = "rectangle" | "oval" | "triangle" | "star"; export default function Canvas() { - const dispatch = useDispatch(); const [stageSize, setStageSize] = useState(); + const [isDarkMode, setIsDarkMode] = useState(false); + + const dispatch = useDispatch(); const { canvasObjects, selectedObjectId, selectedTool } = useSelector( (state: RootState) => state.canvas, ); - const [isDarkMode, setIsDarkMode] = useState(false); - - const [strokeWidth, setStrokeWidth] = useState(5); - const [strokeColor, setStrokeColor] = useState("#2986cc"); - const [fillColor, setFillColor] = useState("#FFFFFF"); + const { strokeWidth, strokeColor, fillColor } = useSelector( + (state: RootState) => state.shape, + ); + const { textSize, textStyle, textColor, textAlignment, lineSpacing } = + useSelector((state: RootState) => state.text); const [isInProgress, setIsInProgress] = useState(false); - const [newObject, setNewObject] = useState(null); // new text/shape object to be added to the canvas const [isConfirmationModalOpen, setConfirmationModalOpen] = useState(false); // confirmation modal for delete button - clear canvas @@ -220,9 +226,9 @@ export default function Canvas() { function updateStyle(property: keyof CanvasObjectType, value: any) { // Dynamically update state if (property === "strokeWidth") { - setStrokeWidth(value); + dispatch(setStrokeWidth(value)); } else if (property === "stroke") { - setStrokeColor(value); + dispatch(setStrokeColor(value)); } // Update object property @@ -502,26 +508,7 @@ export default function Canvas() { setTextPanelVisible(false)} - textSize={0} - setTextSize={function (newSize: number): void { - throw new Error("Function not implemented."); - }} - textStyle={[]} - setTextStyle={function (newStyle: string[]): void { - throw new Error("Function not implemented."); - }} - textColor={""} - onSelectTextColor={function (newColor: string): void { - throw new Error("Function not implemented."); - }} - textAlign={""} - setTextAlign={function (newAlign: string): void { - throw new Error("Function not implemented."); - }} - lineHeight={0} - setLineHeight={function (newHeight: number): void { - throw new Error("Function not implemented."); - }} + selectedObjectId={selectedObjectId} /> ); diff --git a/components/toolbar/TextPanel.tsx b/components/toolbar/TextPanel.tsx index b821ef0..889d6e6 100644 --- a/components/toolbar/TextPanel.tsx +++ b/components/toolbar/TextPanel.tsx @@ -22,35 +22,26 @@ import FormatUnderlinedIcon from "@mui/icons-material/FormatUnderlined"; import FormatAlignLeftIcon from "@mui/icons-material/FormatAlignLeft"; import FormatAlignCenterIcon from "@mui/icons-material/FormatAlignCenter"; import FormatAlignRightIcon from "@mui/icons-material/FormatAlignRight"; +import { useDispatch, useSelector } from "react-redux"; +import { RootState } from "@/redux/store"; +import { + setLineSpacing, + setTextAlignment, + setTextColor, + setTextSize, + toggleTextStyle, +} from "@/redux/textSlice"; type Props = { onClose: () => void; isOpen: boolean; - textSize: number; - setTextSize: (newSize: number) => void; - textStyle: string[]; - setTextStyle: (newStyle: string[]) => void; - textColor: string; - onSelectTextColor: (newColor: string) => void; - textAlign: string; - setTextAlign: (newAlign: string) => void; - lineHeight: number; - setLineHeight: (newHeight: number) => void; + selectedObjectId: string; }; export default function TextPanel({ onClose, isOpen, - textSize, - setTextSize, - textStyle, - setTextStyle, - textColor, - onSelectTextColor, - textAlign, - setTextAlign, - lineHeight, - setLineHeight, + selectedObjectId, }: Props) { const [colorPickerOpen, setColorPickerOpen] = React.useState(false); @@ -58,22 +49,26 @@ export default function TextPanel({ setColorPickerOpen(true); }; - const handleColorChange = (color: string) => { - onSelectTextColor(color); - }; + const dispatch = useDispatch(); + const { textSize, textStyle, textColor, textAlignment, lineSpacing } = + useSelector((state: RootState) => state.text); const handleTextStyleChange = ( event: React.MouseEvent, - newStyles: string[], + newStyle: string, ) => { - setTextStyle(newStyles); + dispatch(toggleTextStyle(newStyle)); + }; + + const handleColorChange = (color: string) => { + dispatch(setTextColor(color)); }; const handleTextAlignChange = ( event: React.MouseEvent, - newAlign: string, + newValue: "left" | "center" | "right", ) => { - setTextAlign(newAlign); + dispatch(setTextAlignment(newValue)); }; const drawer = ( @@ -99,7 +94,7 @@ export default function TextPanel({ setTextSize(Number(e.target.value))} + onChange={(e) => dispatch(setTextSize(Number(e.target.value)))} InputProps={{ inputProps: { min: 1 } }} sx={{ width: 80, mt: 1 }} /> @@ -157,7 +152,7 @@ export default function TextPanel({ Alignment Line spacing setLineHeight(Number(e.target.value))} + value={lineSpacing} + onChange={(e) => dispatch(setLineSpacing(Number(e.target.value)))} InputProps={{ inputProps: { min: 1 } }} sx={{ width: 80, mt: 1 }} /> diff --git a/redux/shapeSlice.ts b/redux/shapeSlice.ts new file mode 100644 index 0000000..9f1e915 --- /dev/null +++ b/redux/shapeSlice.ts @@ -0,0 +1,33 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +interface ShapeState { + strokeWidth: number; + strokeColor: string; + fillColor: string; +} + +const initialState: ShapeState = { + strokeWidth: 5, + strokeColor: "#2986cc", + fillColor: "#FFFFFF", +}; + +const shapeSlice = createSlice({ + name: "shape", + initialState, + reducers: { + setStrokeWidth(state, action: PayloadAction) { + state.strokeWidth = action.payload; + }, + setStrokeColor(state, action: PayloadAction) { + state.strokeColor = action.payload; + }, + setFillColor(state, action: PayloadAction) { + state.fillColor = action.payload; + }, + }, +}); + +export const { setStrokeWidth, setStrokeColor, setFillColor } = + shapeSlice.actions; +export default shapeSlice.reducer; diff --git a/redux/store.ts b/redux/store.ts index a740402..25f3a70 100644 --- a/redux/store.ts +++ b/redux/store.ts @@ -1,9 +1,13 @@ import { configureStore } from "@reduxjs/toolkit"; import canvasReducer from "./canvasSlice"; +import shapeReducer from "./shapeSlice"; +import textReducer from "./textSlice"; const store = configureStore({ reducer: { canvas: canvasReducer, + shape: shapeReducer, + text: textReducer, }, }); diff --git a/redux/textSlice.ts b/redux/textSlice.ts new file mode 100644 index 0000000..d425d3a --- /dev/null +++ b/redux/textSlice.ts @@ -0,0 +1,59 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +interface TextState { + textSize: number; + textStyle: string[]; + textColor: string; + textAlignment: "left" | "center" | "right"; + lineSpacing: number; +} + +const initialState: TextState = { + textSize: 16, + textStyle: [], + textColor: "#000000", + textAlignment: "left", + lineSpacing: 1.5, +}; + +const textSlice = createSlice({ + name: "text", + initialState, + reducers: { + setTextSize: (state, action: PayloadAction) => { + state.textSize = action.payload; + }, + toggleTextStyle: (state, action: PayloadAction) => { + const style = action.payload; + if (state.textStyle.includes(style)) { + state.textStyle = state.textStyle.filter((s) => s !== style); + } else { + state.textStyle.push(style); + } + }, + setTextColor: (state, action: PayloadAction) => { + state.textColor = action.payload; + }, + setTextAlignment: ( + state, + action: PayloadAction<"left" | "center" | "right">, + ) => { + state.textAlignment = action.payload; + }, + setLineSpacing: (state, action: PayloadAction) => { + state.lineSpacing = action.payload; + }, + }, +}); + +// Export actions +export const { + setTextSize, + toggleTextStyle, + setTextColor, + setTextAlignment, + setLineSpacing, +} = textSlice.actions; + +// Export reducer +export default textSlice.reducer; From 72a0d7c71d823d6db78667a739cd1a22778aa48c Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Sun, 29 Sep 2024 23:11:15 -0300 Subject: [PATCH 05/10] Implement event handlers --- components/Canvas.tsx | 8 ++-- components/text/TextField.tsx | 38 +++++++++++----- components/toolbar/TextPanel.tsx | 77 +++++++++++++++++++++++++++++--- redux/textSlice.ts | 11 ++--- 4 files changed, 106 insertions(+), 28 deletions(-) diff --git a/components/Canvas.tsx b/components/Canvas.tsx index 40d8d67..eb18b91 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -41,7 +41,7 @@ export interface CanvasObjectType { shapeName?: ShapeName; stroke?: string; // stroke color strokeWidth?: number; - fill?: string; + fill?: string; // shape fill color / text color points?: number[]; x?: number; y?: number; @@ -51,6 +51,10 @@ export interface CanvasObjectType { text?: string; fontSize?: number; fontFamily?: string; + fontStyle?: string; + textDecoration?: string; + align?: string; + lineHeight?: number; } export type ObjectType = "ink" | "shape" | "text"; @@ -78,8 +82,6 @@ export default function Canvas() { const { strokeWidth, strokeColor, fillColor } = useSelector( (state: RootState) => state.shape, ); - const { textSize, textStyle, textColor, textAlignment, lineSpacing } = - useSelector((state: RootState) => state.text); const [isInProgress, setIsInProgress] = useState(false); const [newObject, setNewObject] = useState(null); // new text/shape object to be added to the canvas diff --git a/components/text/TextField.tsx b/components/text/TextField.tsx index 13e47dc..89fae09 100644 --- a/components/text/TextField.tsx +++ b/components/text/TextField.tsx @@ -31,31 +31,35 @@ export default function TextField({ const { type, id, + text, x, y, width, height, + rotation, fill, - text, + lineHeight, fontSize, - fontFamily, - rotation, + align, + fontStyle, + textDecoration, } = objectProps; const selectedProps = { type, id, + text, x, y, width, height, + rotation, fill, - text, - lineHeight: 1.5, + lineHeight, fontSize, - fontFamily, - fontStyle: "normal", - rotation, + align, + fontStyle, + textDecoration, }; const handleTransform = () => { @@ -85,6 +89,21 @@ export default function TextField({ document.body.appendChild(textarea); // adjust the styles to match + if (node.fontStyle().includes("bold")) { + textarea.style.fontWeight = "bold"; + } + if (node.fontStyle().includes("italic")) { + textarea.style.fontStyle = "italic"; + } + if (node.textDecoration().includes("underline")) { + const offset = node.fontSize() * 0.19; + const thickness = node.fontSize() * 0.07; + + textarea.style.textDecoration = "underline"; + textarea.style.textUnderlineOffset = `${offset}px`; + textarea.style.textDecorationThickness = `${thickness}px`; + } + textarea.id = `text-${node.id()}-textarea`; textarea.value = node.text(); textarea.style.position = "absolute"; @@ -102,7 +121,6 @@ export default function TextField({ textarea.style.resize = "none"; textarea.style.lineHeight = `${node.lineHeight()}`; textarea.style.fontFamily = node.fontFamily(); - textarea.style.fontStyle = node.fontStyle(); textarea.style.textAlign = node.align(); textarea.style.color = node.fill() as string; textarea.style.transformOrigin = "left top"; @@ -117,7 +135,7 @@ export default function TextField({ // slightly move textarea up // because it jumps a bit - const moveUpPx = 2; + const moveUpPx = node.fontSize() / 14.5; transform += `translateY(-${moveUpPx}px)`; textarea.style.transform = transform; diff --git a/components/toolbar/TextPanel.tsx b/components/toolbar/TextPanel.tsx index 889d6e6..283f52d 100644 --- a/components/toolbar/TextPanel.tsx +++ b/components/toolbar/TextPanel.tsx @@ -29,8 +29,9 @@ import { setTextAlignment, setTextColor, setTextSize, - toggleTextStyle, + setTextStyle, } from "@/redux/textSlice"; +import { updateCanvasObject } from "@/redux/canvasSlice"; type Props = { onClose: () => void; @@ -55,13 +56,47 @@ export default function TextPanel({ const handleTextStyleChange = ( event: React.MouseEvent, - newStyle: string, + newStyle: string[], ) => { - dispatch(toggleTextStyle(newStyle)); + console.log("newStyle = ", newStyle); + dispatch(setTextStyle(newStyle)); + + // fontStyle (bold / italic) + let newFontStyle = ""; + if (newStyle.includes("bold")) { + newFontStyle += "bold"; + } + if (newStyle.includes("italic")) { + newFontStyle += " italic"; + } + dispatch( + updateCanvasObject({ + id: selectedObjectId, + updates: { fontStyle: newFontStyle }, + }), + ); + + // textDecoration (underline) + let newTextDecoration = ""; + if (newStyle.includes("underline")) { + newTextDecoration += "underline"; + } + dispatch( + updateCanvasObject({ + id: selectedObjectId, + updates: { textDecoration: newTextDecoration }, + }), + ); }; const handleColorChange = (color: string) => { dispatch(setTextColor(color)); + dispatch( + updateCanvasObject({ + id: selectedObjectId, + updates: { fill: color }, + }), + ); }; const handleTextAlignChange = ( @@ -69,6 +104,34 @@ export default function TextPanel({ newValue: "left" | "center" | "right", ) => { dispatch(setTextAlignment(newValue)); + dispatch( + updateCanvasObject({ + id: selectedObjectId, + updates: { align: newValue }, + }), + ); + }; + + const handleLineSpacingChange = (e: any) => { + const value = Number(e.target.value); + dispatch(setLineSpacing(value)); + dispatch( + updateCanvasObject({ + id: selectedObjectId, + updates: { lineHeight: value }, + }), + ); + }; + + const handleTextSizeChange = (e: any) => { + const value = Number(e.target.value); + dispatch(setTextSize(value)); + dispatch( + updateCanvasObject({ + id: selectedObjectId, + updates: { fontSize: value }, + }), + ); }; const drawer = ( @@ -94,8 +157,8 @@ export default function TextPanel({ dispatch(setTextSize(Number(e.target.value)))} - InputProps={{ inputProps: { min: 1 } }} + onChange={handleTextSizeChange} + InputProps={{ inputProps: { min: 8, max: 100, step: 1 } }} sx={{ width: 80, mt: 1 }} /> @@ -177,8 +240,8 @@ export default function TextPanel({ dispatch(setLineSpacing(Number(e.target.value)))} - InputProps={{ inputProps: { min: 1 } }} + onChange={handleLineSpacingChange} + InputProps={{ inputProps: { min: 1, max: 3, step: 0.5 } }} sx={{ width: 80, mt: 1 }} /> diff --git a/redux/textSlice.ts b/redux/textSlice.ts index d425d3a..ccdec6f 100644 --- a/redux/textSlice.ts +++ b/redux/textSlice.ts @@ -23,13 +23,8 @@ const textSlice = createSlice({ setTextSize: (state, action: PayloadAction) => { state.textSize = action.payload; }, - toggleTextStyle: (state, action: PayloadAction) => { - const style = action.payload; - if (state.textStyle.includes(style)) { - state.textStyle = state.textStyle.filter((s) => s !== style); - } else { - state.textStyle.push(style); - } + setTextStyle: (state, action: PayloadAction) => { + state.textStyle = action.payload; }, setTextColor: (state, action: PayloadAction) => { state.textColor = action.payload; @@ -49,7 +44,7 @@ const textSlice = createSlice({ // Export actions export const { setTextSize, - toggleTextStyle, + setTextStyle, setTextColor, setTextAlignment, setLineSpacing, From 5db6d2808543b7fe24ec61f071463f75fae9bedc Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Sun, 29 Sep 2024 23:17:51 -0300 Subject: [PATCH 06/10] Rename --- .prettierrc | 3 +-- components/Canvas.tsx | 4 ++-- components/shapes/{ShapesLayer.tsx => ShapeLayer.tsx} | 3 ++- components/text/TextLayer.tsx | 2 ++ 4 files changed, 7 insertions(+), 5 deletions(-) rename components/shapes/{ShapesLayer.tsx => ShapeLayer.tsx} (96%) diff --git a/.prettierrc b/.prettierrc index b893548..8756cd8 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,6 +3,5 @@ "semi": true, "singleQuote": false, "printWidth": 80, - "endOfLine": "lf", - "insertFinalNewline": true + "endOfLine": "lf" } diff --git a/components/Canvas.tsx b/components/Canvas.tsx index eb18b91..d09222e 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -4,7 +4,7 @@ import { v4 as uuid } from "uuid"; import { Stage } from "react-konva"; import Toolbar from "./toolbar/Toolbar"; import InkLayer from "./ink/InkLayer"; -import ShapesLayer from "./shapes/ShapesLayer"; +import ShapeLayer from "./shapes/ShapeLayer"; import ConfirmationDialog from "./ConfirmationDialog"; import TextLayer from "./text/TextLayer"; import { RootState } from "../redux/store"; @@ -456,7 +456,7 @@ export default function Canvas() { onTouchStart={handleMouseDown} > - void; }; -export default function ShapesLayer({ +export default function ShapeLayer({ objects, newObject, onChange, @@ -51,6 +51,7 @@ export default function ShapesLayer({ // Show the side panel setSidePanelVisible(true); + // update settings to match selected shape's setWidth(shape.strokeWidth as number); setBorderColor(shape.stroke as string); setFillColor(shape.fill as string); diff --git a/components/text/TextLayer.tsx b/components/text/TextLayer.tsx index 3107381..f05aa18 100644 --- a/components/text/TextLayer.tsx +++ b/components/text/TextLayer.tsx @@ -45,6 +45,8 @@ export default function TextLayer({ // Open side panel setSidePanelVisible(true); + // update settings to match selected text's + // Update cursor style const stage = e.target.getStage(); if (stage) { From 1d23d2e624222b86f0652f3d5e5f061be06221b9 Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Sun, 29 Sep 2024 23:25:03 -0300 Subject: [PATCH 07/10] Update readme --- README.md | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 349d05c..d511009 100644 --- a/README.md +++ b/README.md @@ -21,21 +21,35 @@ Although there isn’t a strict timeline, the project is divided into three phas ## Implemented Features -- Canvas objects +- Supported canvas objects - - Draw + - Freehand inking - Text fields - Shapes - Rectangle - Oval - Triangle - Star - - Adjustable shape borders (width & color) -- Canvas +- Edit objects + + - Drag & move, resize, rotate objects + - Edit shape + - Border width + - Color + - Fill + - Edit text + - Double click to edit content + - Size + - Style (bold, italic, underline) + - Color + - Alignment + - Line spacing + +- Canvas operations - Select object - - History (undo & redo) - Delete an object or clear the entire canvas + - History (undo & redo) - Keyboard shortcuts for delete/undo/redo actions - Persistent canvas data stored in browser's local storage From 3fc4638782c0714d3a97068dac8cb0d5b5e99020 Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Mon, 30 Sep 2024 00:09:35 -0300 Subject: [PATCH 08/10] Update text configs upon selecting a text --- components/Canvas.tsx | 19 +++++++++++---- components/text/TextLayer.tsx | 25 ++++++++++++++++++- components/text/textUtils.ts | 40 +++++++++++++++++++++++++++++++ components/toolbar/ShapePanel.tsx | 2 +- components/toolbar/TextPanel.tsx | 24 ++++++++----------- redux/textSlice.ts | 9 +++---- 6 files changed, 93 insertions(+), 26 deletions(-) diff --git a/components/Canvas.tsx b/components/Canvas.tsx index d09222e..87a4445 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -20,7 +20,12 @@ import { updateSelectedTool, } from "../redux/canvasSlice"; import { SHAPE_DEFAULT_HEIGHT, SHAPE_DEFAULT_WIDTH } from "./shapes/shapeUtils"; -import { TEXT_DEFAULT_HEIGHT, TEXT_DEFAULT_WIDTH } from "./text/textUtils"; +import { + getFontStyleStringFromTextStyleArray, + getTextDecorationStringFromTextStyleArray, + TEXT_DEFAULT_HEIGHT, + TEXT_DEFAULT_WIDTH, +} from "./text/textUtils"; import ShapePanel from "./toolbar/ShapePanel"; import TextPanel from "./toolbar/TextPanel"; import { @@ -83,6 +88,9 @@ export default function Canvas() { (state: RootState) => state.shape, ); + const { textSize, textStyle, textColor, textAlignment, lineSpacing } = + useSelector((state: RootState) => state.text); + const [isInProgress, setIsInProgress] = useState(false); const [newObject, setNewObject] = useState(null); // new text/shape object to be added to the canvas @@ -260,10 +268,13 @@ export default function Canvas() { y: y, width: TEXT_DEFAULT_WIDTH, height: TEXT_DEFAULT_HEIGHT, - fill: strokeColor, // use strokeColor for fill for now - // strokeWidth not applied to text field for now + fill: textColor, text: "Double click to edit.", - fontSize: 28, + fontSize: textSize, + align: textAlignment, + lineHeight: lineSpacing, + fontStyle: getFontStyleStringFromTextStyleArray(textStyle), + textDecoration: getTextDecorationStringFromTextStyleArray(textStyle), fontFamily: "Arial", }; diff --git a/components/text/TextLayer.tsx b/components/text/TextLayer.tsx index f05aa18..e382ba4 100644 --- a/components/text/TextLayer.tsx +++ b/components/text/TextLayer.tsx @@ -2,7 +2,16 @@ import { Layer } from "react-konva"; import { CanvasObjectType } from "../Canvas"; import TextField from "./TextField"; import { RootState } from "@/redux/store"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; +import { updateCanvasObject } from "@/redux/canvasSlice"; +import { + setLineSpacing, + setTextAlignment, + setTextColor, + setTextSize, + setTextStyle, +} from "@/redux/textSlice"; +import { convertTextPropertiesToTextStyleArray } from "./textUtils"; type Props = { objects: CanvasObjectType[]; @@ -24,6 +33,8 @@ export default function TextLayer({ onChange, setSidePanelVisible, }: Props) { + const dispatch = useDispatch(); + const { selectedTool } = useSelector((state: RootState) => state.canvas); const texts = [ @@ -46,6 +57,18 @@ export default function TextLayer({ setSidePanelVisible(true); // update settings to match selected text's + dispatch(setTextSize(text.fontSize || 28)); + dispatch(setTextColor(text.fill || "#000")); + dispatch(setTextAlignment(text.align || "left")); + dispatch(setLineSpacing(text.lineHeight || 1.5)); + dispatch( + setTextStyle( + convertTextPropertiesToTextStyleArray( + text.fontStyle, + text.textDecoration, + ), + ), + ); // Update cursor style const stage = e.target.getStage(); diff --git a/components/text/textUtils.ts b/components/text/textUtils.ts index 980c377..547e30f 100644 --- a/components/text/textUtils.ts +++ b/components/text/textUtils.ts @@ -2,3 +2,43 @@ export const TEXT_DEFAULT_WIDTH = 200; export const TEXT_DEFAULT_HEIGHT = 50; export const TEXT_MIN_WIDTH = 5; export const TEXT_MIN_HEIGHT = 5; + +export function convertTextPropertiesToTextStyleArray( + fontStyle: string | undefined, + textDecoration: string | undefined, +): string[] { + let textStyle = []; + if (fontStyle?.includes("bold")) { + textStyle.push("bold"); + } + if (fontStyle?.includes("italic")) { + textStyle.push("italic"); + } + if (textDecoration?.includes("underline")) { + textStyle.push("underline"); + } + return textStyle; +} + +export function getFontStyleStringFromTextStyleArray( + textStyle: string[], +): string { + let fontStyle = ""; + if (textStyle.includes("bold")) { + fontStyle += "bold"; + } + if (textStyle.includes("italic")) { + fontStyle += " italic"; + } + return fontStyle; +} + +export function getTextDecorationStringFromTextStyleArray( + textStyle: string[], +): string { + let textDecoration = ""; + if (textStyle.includes("underline")) { + textDecoration += "underline"; + } + return textDecoration; +} diff --git a/components/toolbar/ShapePanel.tsx b/components/toolbar/ShapePanel.tsx index 00b5e73..ca58c36 100644 --- a/components/toolbar/ShapePanel.tsx +++ b/components/toolbar/ShapePanel.tsx @@ -89,7 +89,7 @@ export default function ShapePanel({ }} aria-label="Border width" onChange={(_, value) => setStrokeWidth(value as number)} - sx={{ flex: 1 }} + sx={{ flex: 1, mr: 2 }} /> diff --git a/components/toolbar/TextPanel.tsx b/components/toolbar/TextPanel.tsx index 283f52d..be5a721 100644 --- a/components/toolbar/TextPanel.tsx +++ b/components/toolbar/TextPanel.tsx @@ -32,6 +32,10 @@ import { setTextStyle, } from "@/redux/textSlice"; import { updateCanvasObject } from "@/redux/canvasSlice"; +import { + getFontStyleStringFromTextStyleArray, + getTextDecorationStringFromTextStyleArray, +} from "../text/textUtils"; type Props = { onClose: () => void; @@ -58,33 +62,25 @@ export default function TextPanel({ event: React.MouseEvent, newStyle: string[], ) => { - console.log("newStyle = ", newStyle); + // update textStyle in redux store dispatch(setTextStyle(newStyle)); + // update selected object // fontStyle (bold / italic) - let newFontStyle = ""; - if (newStyle.includes("bold")) { - newFontStyle += "bold"; - } - if (newStyle.includes("italic")) { - newFontStyle += " italic"; - } dispatch( updateCanvasObject({ id: selectedObjectId, - updates: { fontStyle: newFontStyle }, + updates: { fontStyle: getFontStyleStringFromTextStyleArray(newStyle) }, }), ); // textDecoration (underline) - let newTextDecoration = ""; - if (newStyle.includes("underline")) { - newTextDecoration += "underline"; - } dispatch( updateCanvasObject({ id: selectedObjectId, - updates: { textDecoration: newTextDecoration }, + updates: { + textDecoration: getTextDecorationStringFromTextStyleArray(newStyle), + }, }), ); }; diff --git a/redux/textSlice.ts b/redux/textSlice.ts index ccdec6f..d927ac5 100644 --- a/redux/textSlice.ts +++ b/redux/textSlice.ts @@ -4,12 +4,12 @@ interface TextState { textSize: number; textStyle: string[]; textColor: string; - textAlignment: "left" | "center" | "right"; + textAlignment: string; lineSpacing: number; } const initialState: TextState = { - textSize: 16, + textSize: 28, textStyle: [], textColor: "#000000", textAlignment: "left", @@ -29,10 +29,7 @@ const textSlice = createSlice({ setTextColor: (state, action: PayloadAction) => { state.textColor = action.payload; }, - setTextAlignment: ( - state, - action: PayloadAction<"left" | "center" | "right">, - ) => { + setTextAlignment: (state, action: PayloadAction) => { state.textAlignment = action.payload; }, setLineSpacing: (state, action: PayloadAction) => { From 28664afe1963329b06be61442aaa1b6774f3301d Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Mon, 30 Sep 2024 00:27:22 -0300 Subject: [PATCH 09/10] Fix shape property update on select --- components/Canvas.tsx | 13 +++---------- components/shapes/ShapeLayer.tsx | 21 +++++++++++---------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/components/Canvas.tsx b/components/Canvas.tsx index 87a4445..65839bc 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -28,11 +28,7 @@ import { } from "./text/textUtils"; import ShapePanel from "./toolbar/ShapePanel"; import TextPanel from "./toolbar/TextPanel"; -import { - setFillColor, - setStrokeColor, - setStrokeWidth, -} from "@/redux/shapeSlice"; +import { setStrokeColor, setStrokeWidth } from "@/redux/shapeSlice"; export interface StageSizeType { width: number; @@ -236,9 +232,9 @@ export default function Canvas() { function updateStyle(property: keyof CanvasObjectType, value: any) { // Dynamically update state if (property === "strokeWidth") { - dispatch(setStrokeWidth(value)); + dispatch(setStrokeWidth(value)); // TODO: Ink property should separate from shape/text } else if (property === "stroke") { - dispatch(setStrokeColor(value)); + dispatch(setStrokeColor(value)); // TODO: Ink property should separate from shape/text } // Update object property @@ -471,9 +467,6 @@ export default function Canvas() { objects={canvasObjects} newObject={newObject} onChange={updateSelectedObject} - setWidth={setStrokeWidth} - setBorderColor={setStrokeColor} - setFillColor={setFillColor} selectedObjectId={selectedObjectId} setSelectedObjectId={(newObjectId) => dispatch(selectCanvasObject(newObjectId)) diff --git a/components/shapes/ShapeLayer.tsx b/components/shapes/ShapeLayer.tsx index c420364..d856bec 100644 --- a/components/shapes/ShapeLayer.tsx +++ b/components/shapes/ShapeLayer.tsx @@ -4,8 +4,13 @@ import RectangleShape from "./RectangleShape"; import OvalShape from "./OvalShape"; import TriangleShape from "./TriangleShape"; import StarShape from "./StarShape"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { RootState } from "@/redux/store"; +import { + setFillColor, + setStrokeColor, + setStrokeWidth, +} from "@/redux/shapeSlice"; type ShapesLayerProps = { objects: CanvasObjectType[]; @@ -14,9 +19,6 @@ type ShapesLayerProps = { newAttrs: Partial, selectedObjectId: string, ) => void; - setWidth: (newWidth: number) => void; - setBorderColor: (newColor: string) => void; - setFillColor: (newColor: string) => void; selectedObjectId: string; setSelectedObjectId: (id: string) => void; setSidePanelVisible: (isVisible: boolean) => void; @@ -26,13 +28,12 @@ export default function ShapeLayer({ objects, newObject, onChange, - setWidth, - setBorderColor, - setFillColor, selectedObjectId, setSelectedObjectId, setSidePanelVisible, }: ShapesLayerProps) { + const dispatch = useDispatch(); + const { selectedTool } = useSelector((state: RootState) => state.canvas); const shapes = [ @@ -52,9 +53,9 @@ export default function ShapeLayer({ setSidePanelVisible(true); // update settings to match selected shape's - setWidth(shape.strokeWidth as number); - setBorderColor(shape.stroke as string); - setFillColor(shape.fill as string); + dispatch(setStrokeWidth(shape.strokeWidth || 5)); + dispatch(setStrokeColor(shape.stroke || "#2986cc")); + dispatch(setFillColor(shape.fill || "#FFFFFF")); // Update cursor style const stage = e.target.getStage(); From 8b4d95bbfb9e1a41f6e877757ee4bee60ae302c7 Mon Sep 17 00:00:00 2001 From: Leo Hong <5917188+low-earth-orbit@users.noreply.github.com> Date: Mon, 30 Sep 2024 00:31:27 -0300 Subject: [PATCH 10/10] Adjust panel visibility upon adding text/shape --- components/Canvas.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/Canvas.tsx b/components/Canvas.tsx index 65839bc..2aea8a9 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -256,6 +256,9 @@ export default function Canvas() { } const addTextField = (x: number, y: number) => { + setShapePanelVisible(false); + setTextPanelVisible(true); + const newObjectId = uuid(); let newObject: CanvasObjectType = { id: newObjectId, @@ -279,6 +282,9 @@ export default function Canvas() { }; const addShape = (shapeName: ShapeName, x: number, y: number) => { + setTextPanelVisible(false); + setShapePanelVisible(true); + const newShapeId = uuid(); const baseShape = { id: newShapeId,