Drawing rectangles on the canvas using react hooks

Viewed 861

I have a React+Typescript web application and I am trying to draw rectangles on the canvas. I want to use React hooks and avoid classes if possible.

Expected result is to be able to draw as many rectangles as is needed within the canvas when mouse is pressed down and is moving. Also I would like to extend this later on to allow dragging, resizing and different shapes.

Currently I am able to draw rectangles however as mouse moves context keep adding rectangles as oppose updating size of the original one.

Drawing result

I saw few questions related to drawing rectangles however I didn't see any of them using React hooks, that's why I decided to create question.

This is the code which am using at the moment and which gives me result from above.

import React, { useEffect, useRef, useState } from 'react';

interface CanvasProps {
    width: number;
    height: number;
}
type Coordinate = {
    x: number;
    y: number;
};

type Rectangle = {
    start: Coordinate;
    completed: boolean;
    end?: Coordinate;
};

const Canvas = ({ width, height }: CanvasProps) => {
    const canvasRef = useRef<HTMLCanvasElement>(null);
    const [currentMousePosition, setCurrentMousePosition] = useState<Coordinate>({ x: 0, y: 0 });
    const [isMouseDown, setMouseDown] = useState(false);
    const [activeDrawing, setActiveDrawing] = useState<Rectangle>();

    const onMouseMove = (event: MouseEvent) => {
        setCurrentMousePosition({
            x: event.clientX,
            y: event.clientY,
        });
    };
    const triggerMouseDown = (event: MouseEvent) => {
        if (!canvasRef.current) return;
        let boundingRect = canvasRef.current.getBoundingClientRect();
        if (!isMouseDown) {
            setMouseDown(true);
            //check is there active drawing
            if (!activeDrawing)
                setActiveDrawing({
                    start: {
                        x: event.clientX - boundingRect.left,
                        y: event.clientY - boundingRect.top,
                    },
                    completed: false,
                });
        }

        setCurrentMousePosition({
            x: event.clientX - boundingRect.left,
            y: event.clientY - boundingRect.top,
        });
    };
    const triggerMouseUp = () => {
        setMouseDown(false);
        setActiveDrawing(undefined);
        draw();
    };

    const draw = () => {
        const canvas: HTMLCanvasElement = canvasRef.current;
        const context = canvas.getContext('2d');
        if (context && activeDrawing?.start) {
            context.putImageData(
                context.getImageData(0, 0, context.canvas.clientWidth, context.canvas.clientHeight),
                0,
                0
            );
            context.save();
            const startX =
                currentMousePosition.x < activeDrawing.start.x ? currentMousePosition.x : activeDrawing.start.x;
            const startY =
                currentMousePosition.y < activeDrawing.start.y ? currentMousePosition.y : activeDrawing.start.y;
            const widthX = Math.abs(activeDrawing.start.x - currentMousePosition.x);
            const widthY = Math.abs(activeDrawing.start.y - currentMousePosition.y);
            // context.beginPath();
            console.info('Current Mouse Position', currentMousePosition);
            console.info('Active drawing', activeDrawing);
            context.rect(startX, startY, widthX, widthY);
            context.stroke();
            context.restore();
        }
    };
    useEffect(() => {
        if (!isMouseDown) return;
        if (!canvasRef.current) {
            return;
        }
        draw();
    }, [isMouseDown, currentMousePosition]);
    useEffect(() => {
        if (!canvasRef.current) {
            return;
        }
        const canvas: HTMLCanvasElement = canvasRef.current;
        canvas.addEventListener('mouseup', triggerMouseUp);
        canvas.addEventListener('mousedown', triggerMouseDown);
        canvas.addEventListener('mousemove', onMouseMove);
        return () => {
            canvas.removeEventListener('mouseup', triggerMouseUp);
            canvas.removeEventListener('mouseleave', triggerMouseDown);
            canvas.removeEventListener('mousemove', onMouseMove);
        };
    }, []);

    const getCoordinates = (event: MouseEvent): Coordinate | undefined => {
        if (!canvasRef.current) {
            return;
        }

        const canvas: HTMLCanvasElement = canvasRef.current;
        return { x: event.pageX - canvas.offsetLeft, y: event.pageY - canvas.offsetTop };
    };

    return <canvas ref={canvasRef} height={height} width={width} />;
};

Canvas.defaultProps = {
    width: window.innerWidth,
    height: window.innerHeight,
};

export default Canvas;

There is also codesandbox where you can see code and actual result.

1 Answers

In your draw() function, try adding

context.clearRect(0, 0, canvas.width, canvas.height);

to clear any unwanted rectangles from the canvas. After that you need to use

context.beginPath();

as stated in the docs: Make sure to call beginPath() before starting to draw new items after calling clearRect().

Full draw() function:

    const draw = () => {
        const canvas: HTMLCanvasElement = canvasRef.current;
        const context = canvas.getContext('2d');
        if (context && activeDrawing?.start) {
            context.putImageData(
                context.getImageData(0, 0, context.canvas.clientWidth, context.canvas.clientHeight),
                0,
                0
            );
            context.save();

            context.clearRect(0, 0, canvas.width, canvas.height);
            context.beginPath();

            const startX =
                currentMousePosition.x < activeDrawing.start.x ? currentMousePosition.x : activeDrawing.start.x;
            const startY =
                currentMousePosition.y < activeDrawing.start.y ? currentMousePosition.y : activeDrawing.start.y;
            const widthX = Math.abs(activeDrawing.start.x - currentMousePosition.x);
            const widthY = Math.abs(activeDrawing.start.y - currentMousePosition.y);
            // context.beginPath();
            console.info('Current Mouse Position', currentMousePosition);
            console.info('Active drawing', activeDrawing);
            context.rect(startX, startY, widthX, widthY);
            context.stroke();
            context.restore();
        }
    };
Related