Skip to content

Commit

Permalink
Merge pull request #16 from low-earth-orbit/rectangle
Browse files Browse the repository at this point in the history
Add rectangle feature
  • Loading branch information
low-earth-orbit authored Aug 26, 2024
2 parents 7eac29b + cdf79a7 commit 409fbe9
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 72 deletions.
167 changes: 111 additions & 56 deletions components/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,68 @@
"use client";

import React, { useEffect, useRef, useState } from "react";
import { Stage, Layer, Line } from "react-konva";
import Toolbar from "./Toolbar";
import FreeDrawLayer from "./FreeDrawLayer";
import { v4 as uuid } from "uuid";
import { Stage } from "react-konva";
import ShapesLayer from "./ShapesLayer";

interface LineType {
export interface LineType {
tool: string;
points: number[];
color: string;
stroke: string;
strokeWidth: number;
}

export interface ShapeType {
id: string;
// shapeType: string;
x: number;
y: number;
width: number;
height: number;
stroke: string;
strokeWidth: number;
}

export interface StageSizeType {
width: number;
height: number;
}

export default function Canvas() {
const [tool, setTool] = useState<string>("pen");
const [lines, setLines] = useState<LineType[]>([]);
const [shapes, setShapes] = useState<ShapeType[]>([]);

const [color, setColor] = useState<string>("#0000FF");
const [strokeWidth, setStrokeWidth] = useState<number>(5);

const isDrawing = useRef<boolean>(false);
const [stageSize, setStageSize] = useState<{
width: number;
height: number;
}>();
const [stageSize, setStageSize] = useState<StageSizeType>();

const handleMouseDown = (e: any) => {
isDrawing.current = true;
const pos = e.target.getStage().getPointerPosition();
setLines([
...lines,
const isFreeDrawing = useRef<boolean>(false);

const [selectedShapeId, setSelectedShapeId] = useState<string>("");

const addRectangle = () => {
setShapes([
...shapes,
{
tool,
points: [pos.x, pos.y],
color: color,
id: uuid(),
x: stageSize ? stageSize.width / 2 - 100 : 0,
y: stageSize ? stageSize.height / 2 - 50 : 0,
width: 200,
height: 100,
stroke: color,
strokeWidth: strokeWidth,
},
]);
};

const handleMouseMove = (e: any) => {
// no drawing - skipping
if (!isDrawing.current) {
return;
}
const stage = e.target.getStage();
const point = stage.getPointerPosition();
let lastLine = lines[lines.length - 1];
// add point
lastLine.points = lastLine.points.concat([point.x, point.y]);

// replace last
lines.splice(lines.length - 1, 1, lastLine);
setLines(lines.concat());
};

const handleMouseUp = () => {
isDrawing.current = false;
};
function resetCanvas() {
setLines([]);
setShapes([]);
}

useEffect(() => {
const handleResize = () => {
Expand All @@ -79,40 +86,88 @@ export default function Canvas() {
return null;
}

const handleMouseDown = (e: any) => {
if (selectedShapeId === "") {
isFreeDrawing.current = true;
const pos = e.target.getStage().getPointerPosition();
setLines([
...lines,
{
tool,
points: [pos.x, pos.y],
stroke: color,
strokeWidth: strokeWidth,
},
]);
} else {
// deselect shapes when clicked on empty area
const clickedOnEmpty = e.target === e.target.getStage();
if (clickedOnEmpty) {
setSelectedShapeId("");
}
}
};

const handleMouseMove = (e: any) => {
// no drawing - skipping
if (!isFreeDrawing.current || selectedShapeId !== "") {
return;
}
const stage = e.target.getStage();
const point = stage.getPointerPosition();
let lastLine = lines[lines.length - 1];
// add point
lastLine.points = lastLine.points.concat([point.x, point.y]);

// replace last
lines.splice(lines.length - 1, 1, lastLine);
setLines(lines.concat());
};

const handleMouseUp = () => {
isFreeDrawing.current = false;
};

return (
<div>
<>
<Stage
width={stageSize.width}
height={stageSize.height}
width={window.innerWidth}
height={window.innerHeight}
onMouseDown={handleMouseDown}
onMousemove={handleMouseMove}
onMouseup={handleMouseUp}
onTouchStart={handleMouseDown}
>
<Layer>
{lines.map((line, i) => (
<Line
key={i}
points={line.points}
stroke={line.color}
strokeWidth={line.strokeWidth}
tension={0.5}
lineCap="round"
lineJoin="round"
globalCompositeOperation={
line.tool === "eraser" ? "destination-out" : "source-over"
}
/>
))}
</Layer>
<ShapesLayer
shapes={shapes}
setShapes={setShapes}
tool={tool}
color={color}
strokeWidth={strokeWidth}
stageSize={stageSize}
isFreeDrawing={isFreeDrawing}
selectedShapeId={selectedShapeId}
setSelectedShapeId={setSelectedShapeId}
/>
<FreeDrawLayer
lines={lines}
setLines={setLines}
tool={tool}
color={color}
strokeWidth={strokeWidth}
stageSize={stageSize}
isFreeDrawing={isFreeDrawing}
/>
</Stage>
<Toolbar
selectTool={setTool}
color={color}
selectColor={setColor}
resetCanvas={() => setLines([])}
resetCanvas={resetCanvas}
strokeWidth={strokeWidth}
setStrokeWidth={setStrokeWidth}
handleAddRectangle={addRectangle}
/>
</div>
</>
);
}
34 changes: 34 additions & 0 deletions components/FreeDrawLayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { MutableRefObject, useRef } from "react";
import { Stage, Layer, Line, Rect } from "react-konva";
import { LineType, StageSizeType } from "./Canvas";

type FreeDrawLayerProps = {
lines: LineType[];
setLines: (newLines: LineType[]) => void;
tool: string;
color: string;
strokeWidth: number;
stageSize: StageSizeType;
isFreeDrawing: MutableRefObject<boolean>;
};

export default function FreeDrawLayer({ lines }: FreeDrawLayerProps) {
return (
<Layer>
{lines.map((line, i) => (
<Line
key={i}
points={line.points}
stroke={line.stroke}
strokeWidth={line.strokeWidth}
tension={0.5}
lineCap="round"
lineJoin="round"
globalCompositeOperation={
line.tool === "eraser" ? "destination-out" : "source-over"
}
/>
))}
</Layer>
);
}
60 changes: 60 additions & 0 deletions components/ShapesLayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { MutableRefObject, useState } from "react";
import { Stage, Layer } from "react-konva";
import { ShapeType, StageSizeType } from "./Canvas";
import Rectangle from "./shapes/Rectangle";

type ShapesLayerProps = {
shapes: ShapeType[];
setShapes: (newShapes: ShapeType[]) => void;
tool: string;
color: string;
strokeWidth: number;
stageSize: StageSizeType;
isFreeDrawing: MutableRefObject<boolean>;
selectedShapeId: string;
setSelectedShapeId: (id: string) => void;
};

export default function ShapesLayer({
tool,
shapes,
setShapes,
color,
strokeWidth,
stageSize,
isFreeDrawing,
selectedShapeId,
setSelectedShapeId,
}: ShapesLayerProps) {
return (
<Layer>
{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 (
<Rectangle
key={i}
shapeProps={shapeProps}
isSelected={shape.id === selectedShapeId}
onSelect={() => {
setSelectedShapeId(shape.id);
}}
onChange={(newAttrs: ShapeType) => {
const newShapes = shapes.slice(); // deep copy
newShapes[i] = newAttrs;
setShapes(newShapes);
}}
/>
);
})}
</Layer>
);
}
Loading

0 comments on commit 409fbe9

Please sign in to comment.