React를 활용해서 canvas를 그림판처럼 활용해보는 예제입니다.
01. CRA를 이용해 프로젝트 생성
create-react-app canvas-example --typescript
--typescript를 붙여서 React + Typescript 트프로젝트를 생성합니다.
02. canvas element 추가
import React, { useRef } from 'react';
import './App.css';
interface CanvasProps {
width: number;
height: number;
}
function App({ width, height }: CanvasProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
return (
<div className="App">
<canvas ref={canvasRef} height={height} width={width} className="canvas"/>
</div>
);
}
App.defaultProps = {
width: 800,
height: 600
};
export default App;
위와 같이 작성해 줍니다.
React Hooks 중 하나인 useRef를 활용합니다. 이는 <canvas>와 같은 DOM노드에 쉽게 접근케 해줍니다.
그리고 width, height를 설정해줬습니다.
간단하게 css도 작성해봅시다.
.App {
text-align: center;
}
.canvas {
border-radius: 15px;
background: lightgrey;
}
03. 마우스 포인터 상태 관리
canvas에 마우스로 선을 그을 때, 두가지 상태관리가 필요합니다. 색깔이나 굵기를 조작하려면 더 필요할 수 있지만..
- mouse down시, 마우스 포인터(x, y)
- isPainting
import React, { useRef, useState } from 'react';
interface CanvasProps {
width: number;
height: number;
}
interface Coordinate {
x: number;
y: number;
};
function App({ width, height }: CanvasProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
// 상태관리
const [mousePosition, setMousePosition] = useState<Coordinate | undefined>(undefined);
const [isPainting, setIsPainting] = useState(false);
...(중략)
위와 같이 useState를 활용해서 두개의 상태를 선언해줍니다.
04. drawLine
import React, { useRef, useState, useCallback } from 'react';
interface CanvasProps {
width: number;
height: number;
}
interface Coordinate {
x: number;
y: number;
};
function App({ width, height }: CanvasProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
// 상태관리
const [mousePosition, setMousePosition] = useState<Coordinate | undefined>(undefined);
const [isPainting, setIsPainting] = useState(false);
// 좌표 얻는 함수
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
};
};
// canvas에 선을 긋는 함수
const drawLine = (originalMousePosition: Coordinate, newMousePosition: Coordinate) => {
if (!canvasRef.current) {
return;
}
const canvas: HTMLCanvasElement = canvasRef.current;
const context = canvas.getContext('2d');
if (context) {
context.strokeStyle = "red"; // 선 색깔
context.lineJoin = 'round'; // 선 끄트머리(?)
context.lineWidth = 5; // 선 굵기
context.beginPath();
context.moveTo(originalMousePosition.x, originalMousePosition.y);
context.lineTo(newMousePosition.x, newMousePosition.y);
context.closePath();
context.stroke();
}
};
const startPaint = useCallback((event: MouseEvent) => {
const coordinates = getCoordinates(event);
if (coordinates) {
setIsPainting(true);
setMousePosition(coordinates);
}
}, []);
const paint = useCallback(
(event: MouseEvent) => {
event.preventDefault(); // drag 방지
event.stopPropagation(); // drag 방지
if (isPainting) {
const newMousePosition = getCoordinates(event);
if (mousePosition && newMousePosition) {
drawLine(mousePosition, newMousePosition);
setMousePosition(newMousePosition);
}
}
},
[isPainting, mousePosition]
);
const exitPaint = useCallback(() => {
setIsPainting(false);
}, []);
...(중략)
선을 그리는데 필요한 함수를 작성해줍니다.
- getCoordinates(event) : MouseEvent로 부터 좌표를 얻음
- drawLine(originPosition, newPosition) : 선을 그음
- startPaint, paint, exitPaint : MouseEvent Listener에 사용될 callback 함수
05. EventListener 등록
컴포넌트가 마운트 되면 마우스 이벤트 리스너를 등록해줍시다.
import React, { useRef, useState, useCallback, useEffect } from 'react';
interface CanvasProps {
width: number;
height: number;
}
interface Coordinate {
x: number;
y: number;
};
function App({ width, height }: CanvasProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [mousePosition, setMousePosition] = useState<Coordinate | undefined>(undefined);
const [isPainting, setIsPainting] = useState(false);
...(중략)
useEffect(() => {
if (!canvasRef.current) {
return;
}
const canvas: HTMLCanvasElement = canvasRef.current;
canvas.addEventListener('mousedown', startPaint);
canvas.addEventListener('mousemove', paint);
canvas.addEventListener('mouseup', exitPaint);
canvas.addEventListener('mouseleave', exitPaint);
return () => {
// Unmount 시 이벤트 리스터 제거
canvas.removeEventListener('mousedown', startPaint);
canvas.removeEventListener('mousemove', paint);
canvas.removeEventListener('mouseup', exitPaint);
canvas.removeEventListener('mouseleave', exitPaint);
};
}, [startPaint, paint, exitPaint]);
...(중략)
06. 전체 코드
import React, { useRef, useState, useCallback, useEffect } from 'react';
import './App.css';
interface CanvasProps {
width: number;
height: number;
}
interface Coordinate {
x: number;
y: number;
};
function App({ width, height }: CanvasProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [mousePosition, setMousePosition] = useState<Coordinate | undefined>(undefined);
const [isPainting, setIsPainting] = useState(false);
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
};
};
const drawLine = (originalMousePosition: Coordinate, newMousePosition: Coordinate) => {
if (!canvasRef.current) {
return;
}
const canvas: HTMLCanvasElement = canvasRef.current;
const context = canvas.getContext('2d');
if (context) {
context.strokeStyle = "red";
context.lineJoin = 'round';
context.lineWidth = 5;
context.beginPath();
context.moveTo(originalMousePosition.x, originalMousePosition.y);
context.lineTo(newMousePosition.x, newMousePosition.y);
context.closePath();
context.stroke();
}
};
const startPaint = useCallback((event: MouseEvent) => {
const coordinates = getCoordinates(event);
if (coordinates) {
setIsPainting(true);
setMousePosition(coordinates);
}
}, []);
const paint = useCallback(
(event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
if (isPainting) {
const newMousePosition = getCoordinates(event);
if (mousePosition && newMousePosition) {
drawLine(mousePosition, newMousePosition);
setMousePosition(newMousePosition);
}
}
},
[isPainting, mousePosition]
);
const exitPaint = useCallback(() => {
setIsPainting(false);
}, []);
useEffect(() => {
if (!canvasRef.current) {
return;
}
const canvas: HTMLCanvasElement = canvasRef.current;
canvas.addEventListener('mousedown', startPaint);
canvas.addEventListener('mousemove', paint);
canvas.addEventListener('mouseup', exitPaint);
canvas.addEventListener('mouseleave', exitPaint);
return () => {
canvas.removeEventListener('mousedown', startPaint);
canvas.removeEventListener('mousemove', paint);
canvas.removeEventListener('mouseup', exitPaint);
canvas.removeEventListener('mouseleave', exitPaint);
};
}, [startPaint, paint, exitPaint]);
return (
<div className="App">
<canvas ref={canvasRef} height={height} width={width} className="canvas"/>
</div>
);
}
App.defaultProps = {
width: 800,
height: 600
};
export default App;
전체코드는 위와 같습니다.
07. 실행 화면
08. 지우기(Clear) 기능
const clearCanvas = () => {
if (!canvasRef.current) {
return;
}
const canvas: HTMLCanvasElement = canvasRef.current;
canvas.getContext('2d')!!.clearRect(0, 0, canvas.width, canvas.height);
}
지우기 버튼을 추가하고 싶다며 위 함수를 코드에 넣어주고 onClick 이벤트에 바인딩 해주면 됩니닷!
09. 모바일에서도?
모바일 환경에서 canvas에 그림 그리는 것도 됩니다.
단, 위 코드로는 canvas만 떡하니 보이고 선이 안그려질겁니다.
아래 이벤트에 대한 처리를 해주면 모바일에서도 canvas에 선을 그을수 있습니다
- touchstart
- touchmove
- touchend
위 전체 코드에서 touch 이벤트에 대한 callback을 만들어 주면 됩니다. 예를들면 아래와 같이 말입니다.
...(중략)
const startTouch = useCallback((event: TouchEvent) => { // MouseEvent인터페이스를 TouchEvent로
event.preventDefault();
if (!canvasRef.current) {
return;
}
const canvas: HTMLCanvasElement = canvasRef.current;
var touch = event.touches[0]; // event로 부터 touch 좌표를 얻어낼수 있습니다.
var mouseEvent = new MouseEvent("mousedown", {
clientX: touch.clientX,
clientY: touch.clientY
});
canvas.dispatchEvent(mouseEvent); // 앞서 만든 마우스 이벤트를 디스패치해줍니다
}, []);
github.com/kjcaway/sketchq/blob/master/src/containers/room/CanvasContainer.tsx
위 git 주소에 canvas를 활용한 응용 그림판 코드가 있습니다.
'개발 이야기 > React' 카테고리의 다른 글
React 에서 활성화된 focus 해제(blur) 방법. [feat. Typescript] (1) | 2020.12.03 |
---|---|
React 에서 WebSocket 활용하기 (feat. Typescript) (0) | 2020.09.16 |
React 메뉴 활성/비활성 상태 관리하기 (0) | 2020.02.05 |
React 이미지파일 미리보기 (2) | 2019.12.24 |
React 프로젝트 Font 적용하기 (0) | 2019.11.16 |