https://developer.mozilla.org/ko/docs/Web/HTML/Reference/Elements/canvas
웹에서 그래픽이나 Animation 을 구현하기 위해 사용하는 HTML tag.
이 안에서 어떤 것을 그릴지는 JavaScript 코드로 구현해야한다.
canvas 태그에 아무런 넓이, 높이 값을 주지 않으면 300 * 150의 기본 사이즈로 제공된다.
이를 CSS로도, canvas 요소가 가진 자체적인 width, height 속성 두가지를 통해 직접 크기를 바꿀 수 있다.
CSS 로 바꾸는 경우 렌더링 과정에서 CSS 크기에 맞추게 되기 때문에 최종 그래픽이 바뀔 수 있다는
MDN문서 내용을 보고 우리 프로젝트에선 html 속성을 활용하기로 했다.
우리는 canvas 태그를 많은 사용자들이 픽셀을 찍고, 뺐는 핵심 기능을 구현하는데 활용하기로 했다.
또한 컴포넌트 기반으로 개발을 진행하고, 많은 레퍼런스가 있는 React를 사용하여,
React 활용경험이 없는 팀원들도 레퍼런스를 참고하여 최대한 러닝 커브를 줄여
빠르게 기능 개발을 하기 위한 것이 선택의 이유.
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D
const canvas = document.getElementById("my-house");
const ctx = canvas.getContext("2d");
우리는 단순히 픽셀을 찍는 영역이라 3D 렌더링이 필요 없다고 판단하여 굳이 WebGL을 쓰지 않고
2D Context를 활용한다.
우리가 통제해야할 요소가 어떤 컴포넌트가 아니라, canvas라는 HTML tag 이자 DOM 요소이다.
따라서 useRef 와 useEffect를 활용하고, useEffect 안에서 canvas 2D context에서
제공하는 메서드들을 활용하여 캔버스를 그리도록 하였다.
const renderCanvasRef = useRef<HTMLCanvasElement>(null);
<canvas
ref={renderCanvasRef}
className='pointer-events-none absolute top-0 left-0'
/>
const draw = useCallback(() => {
const src = sourceCanvasRef.current;
if (!src) return;
const canvas = renderCanvasRef.current;
const ctx = canvas?.getContext('2d');
if (ctx && canvas) {
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.translate(viewPosRef.current.x, viewPosRef.current.y);
ctx.scale(scaleRef.current, scaleRef.current);
ctx.fillStyle = INITIAL_BACKGROUND_COLOR;
ctx.fillRect(0, 0, canvasSize.width, canvasSize.height);
ctx.strokeStyle = 'rgba(0,192,0,0.9)';
ctx.lineWidth = 0.1;
ctx.strokeRect(0, 0, canvasSize.width, canvasSize.width);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(src, 0, 0);
ctx.restore();
}
}, [])
draw 함수에 사용된 메서드 요약ctx.save(): 상태 저장ctx.clearRect(): 영역 지우기ctx.translate(): 원점 이동 (Pan)ctx.scale(): 크기 조절 (Zoom)ctx.fillStyle: 채우기 스타일 설정ctx.fillRect(): 채운 사각형 그리기ctx.strokeStyle: 선 스타일 설정ctx.lineWidth: 선 두께 설정ctx.strokeRect(): 선 사각형 그리기ctx.imageSmoothingEnabled = false: 안티에일리어싱 비활성화 (픽셀 선명화)ctx.drawImage(): 이미지 그리기ctx.restore(): 저장한 상태 복원원래는 하나의 canvas 만 활용하려고 했으나
생각보다 이벤트 핸들링, 커서 미리보기, Optimistic UI 등 캔버스에 그려야할 것들이 많아서
canvas를 Layering 하기로 하였다.
(사용자 눈)
|
V
+---------------------------------+
| interactionCanvas <-- 마우스 이벤트 감지, 커서 그리기
---------------------------------|
| PreviewCanvas | <-- 미리보기, Optimistic UI
+---------------------------------+
| renderCanvasRef (배경 그림) | <-- 실제 보여지는 모든 그림
+---------------------------------+
(메모리 어딘가에...)
+---------------------------------+
| sourceCanvasRef (원본 스케치북) | <-- 512x512 픽셀 데이터 저장
+---------------------------------+
이런식으로 각각의 레이어가 담당하는 기능들을 분리해서 달아놓았다.
책임을 좀 분산해야 특정 기능이 고장났을때 디버깅하기도 쉽고, 추후 기능을 추가할때도
확장성 측면에서 장점이 있다고 보았다.