본문 바로가기

JS Ecosystem

(canvas) 마우스로 사각형 여러개 그리기 (feat.forEach)

 

JS나 React에서 canvas 태그를 활용하여 하나의 캔버스 태그에 여러개의 사각형을 그리는 방법을 설명하겠다.

마우스로 사각형을 그리는 주제를 검색하면 자료가 많이 있지만 한번 그린 사각형에 또 다른 사각형을 추가할 수 있는 방법을 설명한 자료는 찾기가 어려워서 공유하고자 한다.

 

canvas에서 마우스로 사각형을 한번만 그리는 것을 안다는 가정하에

 

결론을 먼저 말하자면

'현재 내가 그리고 있는 rectagle의 정보를 useState를 활용하여  elements 변수에 배열 형태로 저장해주고 다음 사각형을 그릴 때 저장된 elements와 현재 그리고 있는 rectagle의 정보를 forEach로 순회하며 strokeRect 해주는 것이 포인트이다.'

 

아래는 마우스가 이동할 때 호출되는 drawing 함수이고 본 내용의 핵심 코드이다.

const drawing = (e) => {
    if (ctx) {
      if (isDrawing) {
        ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);

        const gapX = e.clientX - canvasRef.current.offsetLeft - pos[0];
        const gapY = e.clientY - canvasRef.current.offsetTop - pos[1];

        const updateElements = [...elements, [pos[0], pos[1], gapX, gapY]];
        setPrev([pos[0], pos[1], gapX, gapY]);

        // 사각형 렌더링
        updateElements.forEach((element) =>
          ctx.strokeRect(element[0], element[1], element[2], element[3]),
        );
      }
    }
  };

 

Return 되는 부분을 먼저 살펴보고 자세한 코드 설명으로 들어가겠다.

 return (
    <TagPageContainer>
      <canvas
        ref={canvasRef}
        onMouseDown={startDrawing}
        onMouseMove={drawing}
        onMouseUp={finishDrawing}
      />
    </TagPageContainer>
  );
}

const TagPageContainer = styled.div`
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  margin-top: 4rem;

  canvas {
    border: 1px solid red;
  }
`;

onMouseDown : 마우스의 버튼을 클릭했을 때 작동. 그림 그리기의 시작을 구분한다.

onMouseMove: 마우스가 canvas 영역에서 움직일 때 작동. 본 함수에서는 마우스의 좌표와 이동 거리를 측정 실제 그림을 그려준다.

onMouseDown: 클릭했던 마우스 버튼을 뗀 경우 작동. 그림 그리기의 종료를 구분한다.

 

1. 변수와 상태 선언 부분

  const canvasRef = useRef(null); // canvas 영역
  const [ctx, setCtx] = useState(); // getContext('2d') 객체
  const [prev, setPrev] = useState([]); // 최종적으로 그린 사각형을 담는 변수
  const [elements, setElements] = useState([]); // 지금까지 그린 사각형이 담겨있는 변수
  const [isDrawing, setIsDrawing] = useState(false); // drawing 하는지 마는지 여부
  const [pos, setPos] = useState([]); // 시작점 좌표

 

2. useEffect 부분

  useEffect(() => {
    const canvas = canvasRef.current;
    canvas.width = window.innerWidth * 0.5;
    canvas.height = window.innerHeight;
    const context = canvas.getContext('2d');
    context.strokeStyle = '#F7819F';
    context.lineWidth = 2.5;
    setCtx(context);
  }, []);

canvas로 그림을 그릴 때 주의해야할 점은 캔바스 영역의 크기를 설정해야 한다는 것이다. 마치 도화지를 준비하는 것과 같다.

의존성 배열 요소에 아무것도 없기에 렌더링 될때 1회만 작동한다.

 

3. mouseDown 핸들러, 마우스를 버튼을 누를 때 작동

  const startDrawing = (e) => {
    setIsDrawing(true);
    const startX = e.clientX - canvasRef.current.offsetLeft; // 시작점의 X 좌표
    const startY = e.clientY - canvasRef.current.offsetTop; // 시작점의 Y 좌표
    setPos([startX, startY]);
  };

 여기서는 시작점의 좌표를 구해서 pos 에 저장한다. 

 

4. mouseMove 핸들러. 마우스를 버튼을 누른 상태에서 움직일 때 작동

  const drawing = (e) => {
    if (ctx) {
      if (isDrawing) {
        // 사각형을 그리면서 생기는 잔상을 계속해서 지워준다.
        ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);

        const gapX = e.clientX - canvasRef.current.offsetLeft - pos[0];
        const gapY = e.clientY - canvasRef.current.offsetTop - pos[1];

        const updateElements = [...elements, [pos[0], pos[1], gapX, gapY]];
        setPrev([pos[0], pos[1], gapX, gapY]);

        // 사각형 렌더링
        updateElements.forEach((element) =>
          ctx.strokeRect(element[0], element[1], element[2], element[3]),
        );
      }
    }
  };

ctx.clearRect 가 없다면 아래와 같이 마우스 이동하면서 생기는 사각형이 잔상처럼 남는다. 그래서 그릴 때 마다 지워져가야 하는 것이다.

const updateElements = [...elements, [pos[0], pos[1], gapX, gapY]];

updateElements는 기존에 그려진 사각형에 현재 그려진 사각형의 정보가 더해졌다.

 

updateElements.forEach((element) =>
  ctx.strokeRect(element[0], element[1], element[2], element[3]),
);

forEach 순회하며 ctx에 strokeRect 해주고 있다.

 

5. mouseUp 핸들러. 마우스 버튼을 뗐을 때 작동

const finishDrawing = () => {
    setElements([...elements, prev]);
    setIsDrawing(false);
};

마지막에 element를 상태함수 setElements로 업데이트 해준다. (중요)

그래야 4번 mouseMove에서 참조하여 기존에 그려진 사각형을 다시 그려줄 수 있다.

 

canvas 활용에 도움이 되었으면 좋겠다.

 

전체 코드

import React, { useEffect, useState, useRef } from 'react';
import styled from 'styled-components';
import image from '../assets/fashion-unsplash.jpeg';

export default function TagPage() {
  const canvasRef = useRef(null); // canvas 영역
  const [ctx, setCtx] = useState(); // getContext('2d') 객체
  const [prev, setPrev] = useState([]); // 최종적으로 그린 사각형을 담는 변수
  const [elements, setElements] = useState([]); // 지금까지 그린 사각형이 담겨있는 변수
  const [isDrawing, setIsDrawing] = useState(false); // drawing 하는지 마는지 여부
  const [pos, setPos] = useState([]); // 시작점 좌표

  useEffect(() => {
    const canvas = canvasRef.current;
    canvas.width = window.innerWidth * 0.5;
    canvas.height = window.innerHeight;
    const context = canvas.getContext('2d');
    context.strokeStyle = '#F7819F';
    context.lineWidth = 2.5;
    setCtx(context);
  }, []);

  const startDrawing = (e) => {
    setIsDrawing(true);
    const startX = e.clientX - canvasRef.current.offsetLeft; // 시작점의 X 좌표
    const startY = e.clientY - canvasRef.current.offsetTop; // 시작점의 Y 좌표
    setPos([startX, startY]);
  };

  const drawing = (e) => {
    if (ctx) {
      if (isDrawing) {
        // 사각형을 그리면서 생기는 잔상을 계속해서 지워준다.
        // ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);

        const gapX = e.clientX - canvasRef.current.offsetLeft - pos[0];
        const gapY = e.clientY - canvasRef.current.offsetTop - pos[1];

        const updateElements = [...elements, [pos[0], pos[1], gapX, gapY]];
        setPrev([pos[0], pos[1], gapX, gapY]);

        // 사각형 렌더링
        updateElements.forEach((element) =>
          ctx.strokeRect(element[0], element[1], element[2], element[3]),
        );
      }
    }
  };

  const finishDrawing = () => {
    setElements([...elements, prev]);
    setIsDrawing(false);
  };

  return (
    <TagPageContainer>
      <canvas
        ref={canvasRef}
        onMouseDown={startDrawing}
        onMouseMove={drawing}
        onMouseUp={finishDrawing}
      />
    </TagPageContainer>
  );
}

const TagPageContainer = styled.div`
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  margin-top: 4rem;

  canvas {
    border: 1px solid red;
  }
`;