Recently, How to Create the Drawing Interaction on DEV's Offline Page by Ali Spittel showed up in my feed and it looked quite cool. This got me wondering if I could create the same thing as a React component using Hooks and typescript. Well, the fact that I am writing this post means I was able to recreate it. So let's see how I did it.
드로잉 인터랙션을 만드는 방법이 등장했는데 꽤 멋져 보였다. 이것은 내가 Hooks와 typescript를 사용하여 React 컴포넌트와 같은 것을 만들 수 있는지 궁금하게 만들었다.
If you are interested in the final product, you can check out the Github repository. There is also a sandbox you can play with at the end of this post.
This post assumes you already know how to work with TypeScript and hooks.
Creating the Component
The first thing we need to do is to create a Canvas component. The canvas needs to take up some space which we will want any parent component to be able to override so we will add width and height as props.
But, We want to add a sensible default so that We don't have to add these props every time we want to use this component. We will add some defaultProps to set these values to window.innerWidth and window.innerHeight respectively.
우리가 가장 먼저 해야 할 일은 캔버스 컴포넌트를 만드는 것이다. 캔버스가 공간을 차지해야 하는데, 상위 컴포넌트가 "오버라이드" 가능하기 때문에 width과 height를 props로 추가할 것이다.
하지만 우리는 이 컴포넌트를 사용할 때마다 이러한 props를 추가할 필요가 없도록 sensible default를 추가하고 싶습니다.
이러한 값을 각각 window.innerWidth 및 window.innerHeight로 설정하는 defaultProps를 추가합니다.
interface CanvasProps {
width: number;
height: number;
}
const Canvas = ({ width, height }: CanvasProps) => {
return <canvas height={height} width={width} />;
};
Canvas.defaultProps = {
width: window.innerWidth,
height: window.innerHeight,
};
export default Canvas;
TypeScript
복사
Lets Draw
Since we need to modify the canvas element, we will need to add a ref to it. We can do this by using useRef hook and modifying our canvas element to set the ref.
캔버스 요소를 수정해야 하므로 ref를 추가해야 합니다. useRef를 사용하여 ref를 설정한 캔버스 요소를 수정한다.
const canvasRef = useRef<HTMLCanvasElement>(null);
return <canvas ref={canvasRef} height={height} width={width} />;
TypeScript
복사
Set state
We need to keep track of some variables
•
the mouse position.
•
whether we are painting or not.
We can do this by adding the useState hook.
We will also create a Coordinate type to help with keeping track of mouse positions.
몇 가지 변수를 계속 추적해야 합니다.
•
마우스 위치.
•
우리가 그림을 그리는지 여부.
useState hook을 추가하면 됩니다.
또한 마우스 위치를 추적하는 데 도움이 되는 좌표 타입을 만들 것입니다.
type Coordinate = {
x: number;
y: number;
};
const Canvas = ({ width, height }: CanvasProps) => {
const [isPainting, setIsPainting] = useState(false);
const [mousePosition, setMousePosition] = useState<Coordinate | undefined>(undefined);
// … other stuff here
TypeScript
복사
Start drawing when the mouse is pressed.
We will add the event listener in the useEffect hook. If we have a valid reference to the canvas, we add an event listener to the mouseDown event. We also remove the event listener when we unmount.
useEffect hook에 이벤트 리스너를 추가하겠습니다.
캔버스에 대한 유효한 참조가 있는 경우 mouseDown 이벤트에 이벤트 리스너를 추가합니다.
또한 마운트 해제 시 이벤트 리스너를 제거합니다.
useEffect(() => {
if (!canvasRef.current) {
return;
}
const canvas: HTMLCanvasElement = canvasRef.current;
canvas.addEventListener(‘mousedown’, startPaint);
return () => {
canvas.removeEventListener(‘mousedown’, startPaint);
};
}, [startPaint]);
TypeScript
복사
startPaint needs to get the current coordinates of the mouse and set isPainting to true. We will also wrap it in a useCallback hook so that we can use it inside the useEffect hook.
startPaint는 마우스의 현재 좌표를 가져와 isPainting을 true로 설정해야 합니다.
또한 useEffect hook 안에서 사용할 수 있도록 useCallback hook으로 포장하겠습니다.
const startPaint = useCallback((event: MouseEvent) => {
const coordinates = getCoordinates(event);
if (coordinates) {
setIsPainting(true);
setMousePosition(coordinates);
}
}, []);
// …other stuff here
const getCoordinates = (event: MouseEvent): Coordinate | undefined => {
if (!canvasRef.current) {
return;
}
const canvas: HTMLCanvasElement = canvasRef.current;
return [event.pageX - canvas.offsetLeft, event.pageY - canvas.offsetTop];
};
TypeScript
복사
Draw the line on mouse move
Similar to the mouseDown event listener we will use the useEffect hook to add the mousemove event.
mouseDown 이벤트 리스너와 마찬가지로 useEffect hook을 사용하여 마우스 이동 이벤트를 추가합니다.
useEffect(() => {
if (!canvasRef.current) {
return;
}
const canvas: HTMLCanvasElement = canvasRef.current;
canvas.addEventListener(‘mousemove’, paint);
return () => {
canvas.removeEventListener(‘mousemove’, paint);
};
}, [paint]);
TypeScript
복사
paint needs to
•
Check if we are painting.
•
Get the new mouse coordinates.
•
Draw a line from the old coordinates to the new one by getting the rendering context from the canvas.
•
Update the old coordinates.
•
우리가 페인트칠을 하고 있는지 확인하세요.
•
새 마우스 좌표를 가져옵니다.
•
캔버스에서 렌더링 컨텍스트를 가져와 이전 좌표에서 새 좌표까지 선을 그립니다.
•
이전 좌표를 업데이트합니다.
const paint = useCallback((event: MouseEvent) => {
if (isPainting) {
const newMousePosition = getCoordinates(event);
if (mousePosition && newMousePosition) {
drawLine(mousePosition, newMousePosition);
setMousePosition(newMousePosition);
}
}
}, [isPainting, mousePosition]);
// …other stuff here
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();
}
};
TypeScript
복사
Stop drawing on mouse release // 마우스 놓을 때 그리기 중지
We want to stop drawing when either the user releases the mouse or they move the mouse out of the canvas area
사용자가 마우스를 놓거나 마우스를 캔버스 영역 밖으로 이동할 때 그리기를 중지하려고 합니다.
useEffect(() => {
if (!canvasRef.current) {
return;
}
const canvas: HTMLCanvasElement = canvasRef.current;
canvas.addEventListener(‘mouseup’, exitPaint);
canvas.addEventListener(‘mouseleave’, exitPaint);
return () => {
canvas.removeEventListener(‘mouseup’, exitPaint);
canvas.removeEventListener(‘mouseleave’, exitPaint);
};
}, [exitPaint]);
TypeScript
복사
In exitPaint we just set the isPainting to false
const exitPaint = useCallback(() => {
setIsPainting(false);
}, []);
TypeScript
복사
And, we have a React component that we can reuse. You can see the final code in either the Github repository. Play with the sandbox below.
Sandbox
완료