라이브 세션에서
처음으로 Canvas를 배웠다
캔버스 API
https://developer.mozilla.org/ko/docs/Web/API/Canvas_API
Canvas API - Web API | MDN
Canvas API는 JavaScript와 HTML <canvas> 엘리먼트를 통해 그래픽을 그리기위한 수단을 제공합니다. 무엇보다도 애니메이션, 게임 그래픽, 데이터 시각화, 사진 조작 및 실시간 비디오 처리를 위해 사용
developer.mozilla.org
Canvas API는 JavaScript와 HTML <canvas> 엘리먼트를 통해 그래픽을 그리기위한 수단을 제공합니다.
무엇보다도 애니메이션, 게임 그래픽, 데이터 시각화, 사진 조작 및 실시간 비디오 처리를 위해 사용됩니다.
Canvas API는 주로 2D 그래픽에 중점을 두고 있습니다.
WebGL API 또한 <canvas> 엘리먼트를 사용하며, 하드웨어 가속 2D 및 3D 그래픽을 그립니다.
MDN Canvas API를 보면 위와 같이 설명합니다.
간단히 말하자면 Canvas API를 사용하여 canvas 엘리먼트를 조작하는 것이라고 이해하시면 됩니다.
작성 코드
import React, { useEffect, useRef, useState } from 'react';
import App from './App';
const DEFAULT_CANVAS_WIDTH = 600;
const DEFAULT_CANVAS_HEIGHT = 300;
const DEFAULT_PEN_SIZE = 5;
function App() {
// 상태관리
// 펜 사이즈
const [penSize, setPenSize] = useState<number>(DEFAULT_PEN_SIZE);
// 지우개 on/off
const [eraserActived, setEraserActive] = useState<boolean>(false);
// 색상 인덱스
const [colorIndex, setColorIndex] = useState<number>(0);
// HTML <canvas> 요소에 대한 참조를 저장
const canvasRef = useRef<HTMLCanvasElement>(null);
// 색상 리스트
const colorList = ['#000', '#f00', '#ff8c00', '#ff0', '#008000', '#00f', '#4b0082', '#800080'];
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
// 2D 그래픽 컨텍스트는 <canvas>에서 실제 그림을 그릴 수 있게 해주는 객체
const ctx = canvas.getContext('2d');
if (!ctx) return;
// 현재 화면의 픽셀 비율
const dpr = window.devicePixelRatio;
canvas.style.width = DEFAULT_CANVAS_WIDTH + 'px';
canvas.style.height = DEFAULT_CANVAS_HEIGHT + 'px';
canvas.width = DEFAULT_CANVAS_WIDTH * dpr;
canvas.height = DEFAULT_CANVAS_HEIGHT * dpr;
ctx.scale(dpr, dpr);
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT);
ctx.fillStyle = '#000';
let drawing = false;
const drawArc = (x: number, y: number) => {
ctx.beginPath();
ctx.arc(x, y, penSize / 2, 0, Math.PI * 2);
ctx.fill();
};
const startDrawing = (e: MouseEvent) => {
drawArc(e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop);
ctx.beginPath();
ctx.moveTo(e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop);
drawing = true;
};
const draw = (e: MouseEvent) => {
if (!drawing) return;
ctx.lineTo(e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.moveTo(e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop);
};
const stopDrawing = () => {
if (!drawing) return;
ctx.closePath();
drawing = false;
};
canvas.addEventListener('mousedown', startDrawing);
window.addEventListener('mousemove', draw);
window.addEventListener('mouseup', stopDrawing);
return () => {
window.removeEventListener('mousemove', draw);
window.removeEventListener('mouseup', stopDrawing);
};
}, [penSize]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.lineWidth = penSize;
}, [penSize]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = penSize;
if (eraserActived) {
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#fff';
} else {
const currentColor = colorList[colorIndex];
ctx.fillStyle = currentColor;
ctx.strokeStyle = currentColor;
}
}, [eraserActived, colorIndex, penSize]);
const handleChangePenSize = (e: React.ChangeEvent<HTMLInputElement>) => {
setPenSize(parseInt(e.target.value));
};
const handleChangeEraser = (e: React.ChangeEvent<HTMLInputElement>) => {
setEraserActive(e.target.checked);
};
const handleChangeColor = (index: number) => {
setEraserActive(false);
setColorIndex(index);
};
return (
<div>
<div className='menu'>
<div className='menu-item'>
<strong className='menu-title'>펜 굵기 {penSize}</strong>
<div className='menu-content'>
<input type='range' min={1} max={30} value={penSize} onChange={handleChangePenSize} />
</div>
</div>
<div className='menu-item'>
<strong className='menu-title'>지우개</strong>
<div className='menu-content'>
<input
type='checkbox'
id='eraser-input'
checked={eraserActived}
className='eraser-input'
onChange={handleChangeEraser}
/>
<label htmlFor='eraser-input' className='eraser-label'></label>
</div>
</div>
<div className='menu-item'>
<strong className='menu-title'>색상</strong>
<div className='menu-content'>
<ul className='color-list'>
{colorList.map((item, index) => (
<li key={item} className='color-item'>
<button
type='button'
className='color-button'
onClick={() => handleChangeColor(index)}
>
<div
className='color-background'
style={{ background: colorList[index] }}
></div>
</button>
</li>
))}
</ul>
</div>
</div>
</div>
<div className='canvas-area'>
<canvas ref={canvasRef} />
</div>
</div>
);
}
export default App;
실행 결과
'✍️ 스파르타 TIL' 카테고리의 다른 글
[TIL] sparta 59일차 - 첨부파일 이미지 미리보기 (0) | 2023.12.27 |
---|---|
[TIL] sparta 58일차 - Next.js 사용해보기 (0) | 2023.12.26 |
모듈 컴포넌트 차이 (0) | 2023.12.21 |
[TIL] sparta 55일차 - React-Query 심화 (0) | 2023.12.20 |
[TIL] sparta 52일차 - 클래스, 객체 (0) | 2023.12.15 |