Skip to content

Commit

Permalink
Merge pull request #59 from low-earth-orbit/30-text-side-panel
Browse files Browse the repository at this point in the history
Add side panel for editing text
  • Loading branch information
low-earth-orbit authored Sep 30, 2024
2 parents b8795bb + 8b4d95b commit 624fc57
Show file tree
Hide file tree
Showing 14 changed files with 569 additions and 78 deletions.
3 changes: 1 addition & 2 deletions .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@
"semi": true,
"singleQuote": false,
"printWidth": 80,
"endOfLine": "lf",
"insertFinalNewline": true
"endOfLine": "lf"
}
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"prettier.prettierPath": "./node_modules/prettier",
"cSpell.words": ["Konva"]
"cSpell.words": ["Konva", "reduxjs"]
}
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
104 changes: 65 additions & 39 deletions components/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ 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 TextFieldsLayer from "./textFields/TextFieldsLayer";
import TextLayer from "./text/TextLayer";
import { RootState } from "../redux/store";
import {
addCanvasObject,
Expand All @@ -21,10 +21,14 @@ import {
} from "../redux/canvasSlice";
import { SHAPE_DEFAULT_HEIGHT, SHAPE_DEFAULT_WIDTH } from "./shapes/shapeUtils";
import {
getFontStyleStringFromTextStyleArray,
getTextDecorationStringFromTextStyleArray,
TEXT_DEFAULT_HEIGHT,
TEXT_DEFAULT_WIDTH,
} from "./textFields/textFieldUtils";
import SidePanel from "./toolbar/SidePanel";
} from "./text/textUtils";
import ShapePanel from "./toolbar/ShapePanel";
import TextPanel from "./toolbar/TextPanel";
import { setStrokeColor, setStrokeWidth } from "@/redux/shapeSlice";

export interface StageSizeType {
width: number;
Expand All @@ -38,7 +42,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;
Expand All @@ -48,6 +52,10 @@ export interface CanvasObjectType {
text?: string;
fontSize?: number;
fontFamily?: string;
fontStyle?: string;
textDecoration?: string;
align?: string;
lineHeight?: number;
}

export type ObjectType = "ink" | "shape" | "text";
Expand All @@ -65,24 +73,27 @@ export type ToolType =
export type ShapeName = "rectangle" | "oval" | "triangle" | "star";

export default function Canvas() {
const dispatch = useDispatch();
const [stageSize, setStageSize] = useState<StageSizeType>();
const [isDarkMode, setIsDarkMode] = useState(false);

const dispatch = useDispatch();
const { canvasObjects, selectedObjectId, selectedTool } = useSelector(
(state: RootState) => state.canvas,
);
const [isDarkMode, setIsDarkMode] = useState(false);
const { strokeWidth, strokeColor, fillColor } = useSelector(
(state: RootState) => state.shape,
);

const [strokeWidth, setStrokeWidth] = useState<number>(5);
const [strokeColor, setStrokeColor] = useState<string>("#2986cc");
const [fillColor, setFillColor] = useState<string>("#FFFFFF");
const { textSize, textStyle, textColor, textAlignment, lineSpacing } =
useSelector((state: RootState) => state.text);

const [isInProgress, setIsInProgress] = useState(false);

const [newObject, setNewObject] = useState<CanvasObjectType | null>(null); // new text/shape object to be added to the 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(() => {
Expand Down Expand Up @@ -221,9 +232,9 @@ export default function Canvas() {
function updateStyle(property: keyof CanvasObjectType, value: any) {
// Dynamically update state
if (property === "strokeWidth") {
setStrokeWidth(value);
dispatch(setStrokeWidth(value)); // TODO: Ink property should separate from shape/text
} else if (property === "stroke") {
setStrokeColor(value);
dispatch(setStrokeColor(value)); // TODO: Ink property should separate from shape/text
}

// Update object property
Expand All @@ -245,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,
Expand All @@ -253,10 +267,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",
};

Expand All @@ -265,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,
Expand Down Expand Up @@ -309,7 +329,7 @@ export default function Canvas() {

setNewObject(newShape);
dispatch(selectCanvasObject(newShapeId));
setSidePanelVisible(true);
setShapePanelVisible(true);
};

const handleMouseDown = (e: any) => {
Expand Down Expand Up @@ -372,12 +392,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);
}
};

Expand Down Expand Up @@ -444,27 +469,25 @@ export default function Canvas() {
onTouchStart={handleMouseDown}
>
<InkLayer objects={canvasObjects} newObject={newObject} />
<ShapesLayer
<ShapeLayer
objects={canvasObjects}
newObject={newObject}
onChange={updateSelectedObject}
setWidth={setStrokeWidth}
setBorderColor={setStrokeColor}
setFillColor={setFillColor}
selectedObjectId={selectedObjectId}
setSelectedObjectId={(newObjectId) =>
dispatch(selectCanvasObject(newObjectId))
}
setSidePanelVisible={setSidePanelVisible}
setSidePanelVisible={setShapePanelVisible}
/>
<TextFieldsLayer
<TextLayer
objects={canvasObjects}
newObject={newObject}
selectedObjectId={selectedObjectId}
setSelectedObjectId={(newObjectId) =>
dispatch(selectCanvasObject(newObjectId))
}
onChange={updateSelectedObject}
setSidePanelVisible={setTextPanelVisible}
/>
</Stage>
<Toolbar
Expand All @@ -484,18 +507,21 @@ export default function Canvas() {
description="Are you sure you want to clear the canvas? This action cannot be undone."
isDarkMode={isDarkMode}
/>
{isSidePanelVisible && (
<SidePanel
isOpen={isSidePanelVisible}
onClose={() => setSidePanelVisible(false)}
strokeWidth={strokeWidth}
setStrokeWidth={(newWidth) => updateStyle("strokeWidth", newWidth)}
color={strokeColor}
onSelectColor={(newColor) => updateStyle("stroke", newColor)}
fillColor={fillColor}
onSelectFillColor={(newColor) => updateStyle("fill", newColor)}
/>
)}
<ShapePanel
isOpen={isShapePanelVisible}
onClose={() => setShapePanelVisible(false)}
strokeWidth={strokeWidth}
setStrokeWidth={(newWidth) => updateStyle("strokeWidth", newWidth)}
color={strokeColor}
onSelectColor={(newColor) => updateStyle("stroke", newColor)}
fillColor={fillColor}
onSelectFillColor={(newColor) => updateStyle("fill", newColor)}
/>
<TextPanel
isOpen={isTextPanelVisible}
onClose={() => setTextPanelVisible(false)}
selectedObjectId={selectedObjectId}
/>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -14,25 +19,21 @@ type ShapesLayerProps = {
newAttrs: Partial<CanvasObjectType>,
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;
};

export default function ShapesLayer({
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 = [
Expand All @@ -51,9 +52,10 @@ export default function ShapesLayer({
// Show the side panel
setSidePanelVisible(true);

setWidth(shape.strokeWidth as number);
setBorderColor(shape.stroke as string);
setFillColor(shape.fill as string);
// update settings to match selected shape's
dispatch(setStrokeWidth(shape.strokeWidth || 5));
dispatch(setStrokeColor(shape.stroke || "#2986cc"));
dispatch(setFillColor(shape.fill || "#FFFFFF"));

// Update cursor style
const stage = e.target.getStage();
Expand Down
Loading

0 comments on commit 624fc57

Please sign in to comment.