[React] Canvas API ๊ธฐ๋ณธ ๋ด์ฉ ์ ๋ฆฌ / ๋ฆฌ์กํธ์์ ์ฌ์ฉ๋ฒ
์บ๋ฒ์ค ๊ธฐ๋ณธ ์ธํ for React
๐ก ์บ๋ฒ์ค๋ HTML ์์ ์ค ํ๋๋ก ์คํฌ๋ฆฝํธ ์ธ์ด๋ก ๊ทธ๋ฆผ์ ๊ทธ๋ฆฌ๋๋ฐ ์ฌ์ฉํ๋ค.
canvas๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์ ์บ๋ฒ์ค context์ DOM์ผ๋ก ์ ๊ทผํด์ผ ํ๋ค. React์์ useRef()
๋ฅผ ์ฌ์ฉํด์ Ref ๊ฐ์ฒด๋ฅผ ๋ง๋ค๊ณ ์ ๊ทผํ๊ณ ์ถ์ DOM์ ref ๊ฐ์ผ๋ก ์ค์ ํด์ฃผ๋ฉด ๋๋ค. ๊ทธ๋ผ Ref ๊ฐ์ฒด์ .current
๊ฐ์ ํด๋น DOM์ ๊ฐ๋ฆฌํจ๋ค.
์บ๋ฒ์ค ์๋ฆฌ๋จผํธ ์์ฑ ๋ฐ useRef ์ค์
import React, { useRef, useEffect, useState } from 'react';
import styled from 'styled-components';
const Canvas = () => {
const canvasRef = useRef(null);
// ์๋ต
return <DrawingArea ref={canvasRef} />;
};
/* ----- ์คํ์ผ ์์ญ ----- */
const DrawingArea = styled.canvas`
// ์๋ต
`;
์บ๋ฒ์ค ์ฌ์ด์ฆ ์ค์ ๋ฐ ๋๋ก์ ์ปจํ ์คํธ ์ฐธ์กฐ
canvasRef์ ๊ธฐ๋ณธ๊ฐ์ null
์ด๊ธฐ ๋๋ฌธ์ ์ปดํฌ๋ํธ๊ฐ ๋ง์ดํธ ๋ ํ ์ฝ๋๋ฅผ ์คํํ๋๋ก useEffect ์์๋ค ์ฝ๋๋ฅผ ์์ฑํ๋ค.
์์ง ๊ฒฝํํ์ง ๋ชปํ์ง๋ง ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋๊ณ ๊ฐ๋ useRef ์ฐธ์กฐ๊ฐ์ด null
์ธ ๊ฒฝ์ฐ๊ฐ ์๋ค๊ณ . useState๋ฅผ ํ์ฉํด ๋๋ก์ ์ปจํ
์คํธ๋ฅผ ์ํ๋ก ์ ์ฅํ๋ฉด ์ด๋ฐ ๊ฒฝ์ฐ๋ฅผ ๋ฐฉ์งํ ์ ์๋ค๊ณ ํจ.
const Canvas = () => {
const canvasRef = useRef(null);
const [ctx, setCtx] = useState();
useEffect(() => {
// ์บ๋ฒ์ค ์ฌ์ด์ฆ ์ค์
const canvas = canvasRef.current;
canvas.width = window.innerHeight; // ํน์ ์ํ๋ ์ฌ์ด์ฆ px์์ด ์ซ์๋ง ์
๋ ฅ
canvas.height = window.innerWidth; // ํน์ ์ํ๋ ์ฌ์ด์ฆ px์์ด ์ซ์๋ง ์
๋ ฅ
// ์บ๋ฒ์ค context ์ ๊ทผ ํ ๊ธฐ๋ณธ ์ค์
const context = canvas.getContext('2d'); // "๊ทธ๋ฆฌ๊ธฐ ๋ฉ์๋์ ์์ฑ์ ๊ฐ๋" 2์ฐจ์ ๋๋ก์ ์ปจํ
์คํธ ์ฐธ์กฐ
context.strokeStyle = 'black'; // line ์
context.lineWidth = 3; // line ๊ตต๊ธฐ
context.lineJoin = 'round'; // ์ ์ฐ๊ฒฐ ๋ชจ์(๊ธฐ๋ณธ ๊ฐ miter ์ผ๋ฐ ๋ชจ์)
context.lineCap = 'round'; // ์ ๋ ๋ชจ์
context.save(); // ๋๋ก์ ์ปจํ
์คํธ ์ค์ ์ ์ฅ
setCtx(context); // ์ค์ ํ ๋๋ก์ ์ปจํ
์คํธ๋ฅผ ์ํ๋ก ์ ์ฅ
}, []);
};
๋ง์ฐ์ค ์ด๋ฒคํธ ํธ๋ค๋ฌ ์ค์
๐ก offset ์ขํ๋ ์ด๋ฒคํธ๊ฐ ๊ฑธ๋ ค ์๋ DOM ๊ฐ์ฒด๋ฅผ ๊ธฐ์ค์ผ๋ก ์ขํ๋ฅผ ์ถ๋ ฅํ๋ค. canvas ์์์ ์ด๋ฒคํธ๋ฅผ ๊ฑธ์๋ค๋ฉด ๋ธ๋ผ์ฐ์ ํ๋ฉด์ด ์๋ canvas ์์๊ฐ ๊ธฐ์ค์ด ๋๋ค. offset ์ขํ์ ๊ธฐ๋ณธ๊ฐ์ ์ผ์ชฝ ์๋จ (0, 0)
์บ๋ฒ์ค์์ ๋ง์ฐ์ค๋ก ๊ทธ๋ฆผ์ ๊ทธ๋ฆด ๋ ์ด๋ฒคํธ ๋์์ x(๊ฐ๋ก), y(์ธ๋ก) ์ขํ๋ฅผ ๋ฐํํ๋ offsetX
offsetY
๊ฐ ํ์ํ๋ค. ๋ฆฌ์กํธ์์ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํด ๋ํ๋ ํฉ์ฑ ์ด๋ฒคํธ(Synthetic Event)๋ฅผ ์ ๋ฌํ๋ฉฐ ์ฌ์ฌ์ฉ๋๋ค.
ํ์ง๋ง ์ด ํฉ์ฑ ์ด๋ฒคํธ์์ offset ์ขํ๋ฅผ ์ฌ์ฉํ ์ ์๋ค. ํธ๋ค๋ฌ์์ ์ด๋ฒคํธ ๊ฐ์ฒด๋ฅผ ์ฝ์๋ก ์ฐ์ด๋ณด๋ฉด offsetX, offsetY๋ฅผ ์ฐพ์ ์ ์๋ค. — ์ฐธ๊ณ ๊ธ
๋ฆฌ์กํธ์์ offset ์ขํ๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด event.nativeEvent
๋ฅผ ํตํด ๋ธ๋ผ์ฐ์ ์ ๊ณ ์ ์ด๋ฒคํธ์ ์ ๊ทผํด์ผ ํ๋ค. ๋งค๊ฐ๋ณ์ ๊ตฌ์กฐ๋ถํด๋ฅผ ์ด์ฉํด ({ nativeEvent })
์ด๋ฐ์์ผ๋ก ๋ช
์ํ๋ฉด ์ฌ์ฉํ๊ธฐ ํธํ๋ค.
const Canvas = () => {
const [isDrawing, setIsDrawing] = useState(false); // ๊ทธ๋ฆฌ๊ธฐ ์ํ ์ค์
const [path, setPath2D] = useState(new Path2D()); // path ๊ฐ์ฒด ์์ฑ
const startDrawing = ({ nativeEvent }) => {};
const drawing = ({ nativeEvent }) => {};
const finishDrawing = ({ nativeEvent }) => {};
return (
<DrawingArea
ref={canvasRef}
onMouseDown={startDrawing}
onMouseUp={finishDrawing}
onMouseMove={drawing}
// onMouseLeave={finishDrawing} // ๋ง์ฐ์ค๊ฐ ์ด๋ฒคํธ ์์ญ ๋ฒ์ด๋ฌ์ ๋
/>
);
};
- ์บ๋ฒ์ค ์์์ ๋ง์ฐ์ค๋ฅผ ์์ง์ด๋ฉด ์์์ ์ ๋ง์ฐ์ค๊ฐ ์์นํ ์ขํ๋ก ์ด๋
ctx.moveTo(x, y)
- ๋ง์ฐ์ค๋ฅผ ํด๋ฆญํ๋ฉด ๊ทธ๋ฆฌ๊ธฐ ์ํ๋กค
true
๋ก ๋ณ๊ฒฝํ๊ณ - ํด๋ฆญํ ์ํ์์ ๋ง์ฐ์ค๋ฅผ ๋๋๊ทธํ๋ฉด ์ ์ ๊ทธ๋ฆฐ๋ค.
ctx.lineTo(x, y)
ctx.stroke(x, y)
- ํด๋ฆญ ๋ฒํผ์ ๋ผ๋ฉด ๊ทธ๋ฆฌ๊ธฐ ์ํ๋ฅผ false๋ก ๋ณ๊ฒฝํ๊ณ path๋ฅผ ๋ซ๊ณ , ๋ง์ง๋ง path๊น์ง ์ ์ ๊ทธ๋ฆฐ๋ค.
ctx.closePath(x, y)
ctx.stroke(x, y)
๊ทธ๋ฆฌ๊ธฐ ์์ startDrawing
์บ๋ฒ์ค์์ ๋ง์ฐ์ค๋ฅผ ํด๋ฆญํ๋ฉด ๊ทธ๋ฆฌ๊ธฐ ์ํ๋ฅผ true
๋ก ๋ณ๊ฒฝํ๋ค
// ๋ง์ฐ์ค๋ฅผ ํด๋ฆญํ๋ฉด ์คํ๋๋ ํธ๋ค๋ฌ
const startDrawing = ({ nativeEvent }) => {
const { offsetX, offsetY } = nativeEvent;
setIsDrawing(true); // ๊ทธ๋ฆฌ๊ธฐ ์ํ ๋ณ๊ฒฝ
console.log('start offset: ', offsetX, offsetY);
};
๊ทธ๋ฆฌ๋ ์ค drawing
์บ๋ฒ์ค์์ ๋ง์ฐ์ค๋ฅผ ํด๋ฆญํ์ง ์๊ณ ์์ง์ด๊ธฐ๋ง ํ๋ฉด ๋ง์ฐ์ค๊ฐ ์์นํ ์ขํ๋ก ์์์ ์ ์ด๋ํ๋ค. ๋ง์ฐ์ค๋ฅผ ํด๋ฆญํ ํ(startDrawing
ํธ๋ค๋ฌ ํธ์ถ → ๊ทธ๋ฆฌ๊ธฐ ์ํ true
๋ก ๋ณ๊ฒฝ) ๋๋๊ทธํ๋ฉด ์์์ ๋ถํฐ ํ์ฌ ๋ง์ฐ์ค ์ขํ๊น์ง ์ ์ ๊ธ๋๋ค.
// ๋ง์ฐ์ค๋ฅผ ์บ๋ฒ์ค ์์์ ์์ง์ด๋ฉด(ํน์ ๋๋๊ทธ) ์คํ๋๋ ํธ๋ค๋ฌ
const drawing = ({ nativeEvent }) => {
// offsetX, offsetY๋ ์ด๋ฒคํธ๊ฐ ๊ฑธ๋ ค์๋ DOM ๊ฐ์ฒด๋ฅผ ๊ธฐ์ค์ผ๋ก ์ขํ ์ถ๋ ฅ
const { offsetX, offsetY } = nativeEvent;
if (ctx) {
if (!isDrawing) {
// ํด๋ฆญํ์ง ์์ ์ํ๋ผ๋ฉด
path.moveTo(offsetX, offsetY); // ์์์ ์ค์
} else {
// ํด๋ฆญํ ์ํ๋ผ๋ฉด
path.lineTo(offsetX, offsetY); // ์์์ ์์(ํน์ ํ์ฌ ์ขํ) ์ด์ด์ง ๋ถ๋ถ ์ค์
ctx.stroke(path); // ์ ๊ธ๊ธฐ(ํ๋ฉด ์ถ๋ ฅ)
}
}
};
๊ทธ๋ฆฌ๊ธฐ ์ข ๋ฃ finishDrawing
๋ง์ฐ์ค ํด๋ฆญ(onMouseUp)์ ๋ผ๋ฉด finishDrawing
ํธ๋ค๋ฌ๊ฐ ์คํ๋๊ณ , ๊ทธ๋ฆฌ๊ธฐ ์ํ๋ false
๋ก ๋ณ๊ฒฝ๋๋ค. path๋ ๋ซํ๋ฉด์ ์์์ ๊ณผ ๋์ ์ ์๋์ผ๋ก ์ฐ๊ฒฐํ๊ณ , ๋ค์ ๊ทธ๋ฆด ๋ํ์ ์ํด ์๋ก์ด path ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค.
// ๋ง์ฐ์ค ํด๋ฆญ์ ํด์ ํ๋ฉด ์คํ๋๋ ํธ๋ค๋ฌ
const finishDrawing = () => {
setIsDrawing(false); // ๊ทธ๋ฆฌ๊ธฐ ์ํ ๋ณ๊ฒฝ
path.closePath(); // ์ ์ข
๋ฃ ์ ์ธ(์์์ ๊ณผ ๋์ ์ ์๋์ผ๋ก ์ฐ๊ฒฐํ๋ค)
setPath2D(new Path2D()); // ์๋ก์ด path ๊ฐ์ฒด ์์ฑ
};
(๋ค๊ฐํ)๋ํ ์ ์ฅ ๋ฐ ๋ค์ ๊ทธ๋ฆฌ๊ธฐ
Path2D ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ์ง ์์์ ๋
๋ํ๋ง๋ค ๋ชจ๋ offset ์ขํ๋ฅผ ์ ์ฅํด์ ํ์ ์ ํด๋น ๋ํ์ ์ขํ๋ฅผ ์ด์ฉํด ๋ค์ ๊ทธ๋ฆด ์ ์๋ค. 1๊ฐ ๋ํ์ ๊ทธ๋ฆด ๋ ๋ง๋ค ์ขํ ์ ๋ณด๋ฅผ ์์๋ก ์ ์ฅํ๋ ์ํ๋ ํ์ํ๋ค.
// Canvas.js
const Canvas = () => {
// ์ฌ๋ฌ ๋ํ์ ์ขํ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ ์ํ [[...], [...], [...]]
const [savedPolygon, setSavedPolygon] = useState([]);
// 1๊ฐ ๋ํ์ ๊ทธ๋ฆด ๋ ์ขํ ์ ๋ณด๋ฅผ ์์๋ก ๋ด์๋๋ ์ํ [...]
const [drawingOffset, setDrawingOffset] = useState([]);
const startDrawing = ({ nativeEvent }) => {
const { offsetX, offsetY } = nativeEvent;
// ์๋ต
setDrawingOffset([offsetX, offsetY]);
};
const drawing = ({ nativeEvent }) => {
const { offsetX, offsetY } = nativeEvent;
if (ctx) {
if (!isDrawing) {
// ์๋ต(ํด๋ฆญํ์ง ์์ ์ํ์์ ๋ง์ฐ์ค ๋ฌด๋ธ)
} else {
// ์๋ต(ํด๋ฆญํ ์ํ์์ ๋๋๊ทธ)
setDrawingOffset([...drawingOffset, offsetX, offsetY]);
}
}
};
const finishDrawing = () => {
// ์๋ต
setSavedPolygon([...savedPolygon, drawingOffset]); // ์์ฑํ ๋ํ์ ์ขํ ์ ๋ณด๋ฅผ ๋ฐฐ์ด์ ์ ์ฅ
setDrawingOffset([]); // ์์ ์ขํ ์ ๋ณด ์ํ ์ด๊ธฐํ
};
};
// reDraw.js
export default (ctx, savedPolygon) => {
savedPolygon.forEach((polygon) => {
const copied = polygon.slice();
ctx.beginPath();
ctx.moveTo(copied.shift(), copied.shift());
// shift/pop ๋ฉ์๋๋ ์ ๊ฑฐํ ์์๋ฅผ ๋ฐํํ๋ค.
while (copied.length) {
ctx.lineTo(copied.shift(), copied.shift());
}
ctx.closePath();
ctx.stroke();
});
};
Path2D ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ์ ๋(๋ ๊ฐ๋จํจ)
๊ฐ ๋ํ์ ์ขํ ์ ๋ณด๋ฅผ ์ ์ฅํ ์ํ ๋ฐฐ์ด์ ๋ง๋ ํ ๋ํ์ ์์ฑํ ๋๋ง๋ค ์ด ์ํ์ ์ ์ฅํ๋ค. reDraw
ํจ์์์ ctx
์ ๊ฐ ๋ํ์ path
์ ๋ณด๊ฐ ๋ด๊ธด savedPolygon
๋ฐฐ์ด์ ๋ฐ์์ ํ๋ฉด์ ๊ทธ๋ฆฐ๋ค. Path2D ์ธ์คํด์ค์ ๋ด๊ธด path ์ ๋ณด๋ ctx.stroke(path)
๋ช
๋ น์ด ํ ๋ฒ์ผ๋ก ํด๋น path์ ๋ชจ๋ ์ขํ๋ฅผ ๊ทธ๋ ค์ฃผ๊ธฐ ๋๋ฌธ์ ๋ ๊น๋ํ๋ค.
// Canvas.js
const Canvas = () => {
const [savedPolygon, setSavedPolygon] = useState([]);
// savedPolygon [{ path }, { path }, { path }]
const finishDrawing = () => {
// ์๋ต
setSavedPolygon([...savedPolygon, path]); // ๊ทธ๋ฆฐ ๋ํ ์ํ ๋ฐฐ์ด์ ์ ์ฅ
};
};
// reDraw.js
export default (ctx, savedPolygon) => {
savedPolygon.forEach((path) => ctx.stroke(path));
};
๊ธฐํ ์ฐธ๊ณ ๋ด์ฉ
- ์บ๋ฒ์ค ์ฌ์ด์ฆ ๊ธฐ๋ณธ๊ฐ์ width
300
(px), height150
(px) - ์บ๋ฒ์ค์์ 1๋จ์๋ 1ํฝ์
- ์๋์ฐ ์ฌ์ด์ฆ ๊ด๋ จ (์ฐธ๊ณ ๋งํฌ)
window.innerWidth
window.innerHeight
- ์คํฌ๋กค๋ฐ๊ฐ ์ฐจ์งํ๋ ๊ณต๊ฐ ํฌํจ
document.body.clientWidth/clientHeight
(body)document.documentElement.clientWidth/clientHeight
(html)- ์คํฌ๋กค๋ฐ๊ฐ ์ฐจ์งํ๋ ๊ณต๊ฐ ์ ์ธ
getBoundingClientRect()
๋ฉ์๋๋ก ์๋ฆฌ๋จผํธ์ ์ฌ์ด์ฆ ์ ๋ณด๋ฅผ ์ฐธ์กฐํ ์ ์๋ค.
๊ทธ๋ฆฌ๊ธฐ ๊ด๋ จ
์บ๋ฒ์ค์ ๊ธฐ๋ณธ ์ข์๋จ ์ขํ๋(์์ ) 0, 0์ด๋ค. ๋ธ๋ผ์ฐ์ ๊ฐ ์๋ ์บ๋ฒ์ค ์๋ฆฌ๋จผํธ์ ์์ ์ธ ๊ฒ ์ฃผ์. X์ถ(๊ฐ๋ก)์ ์ค๋ฅธ์ชฝ์ผ๋ก, Y์ถ(์ธ๋ก)์ ์๋์ชฝ์ผ๋ก ์ฆ๊ฐํ๋ ๋ฐ์นด๋ฅดํธ ์ขํ๊ณ๋ฅผ ์ฌ์ฉํ๋ค.
์ Stroke
๐ก `lineTo()`๋ ์ง์ ์ ๊ทธ์ ํ ํ์ฌ ์ขํ๋ฅผ ๋์ ์ผ๋ก ์ฎ๊ธด๋ค. `moveTo()`๋ฅผ ํธ์ถํ์ง ์์๋ `lineTo()`๋ฅผ ๊ณ์ ํธ์ถํ์ฌ ์ด์ด์ง ๋ค๊ฐํ์ ๊ทธ๋ฆด ์ ์๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก ์บ๋ฒ์ค์์ ์ ์ ๊ทธ๋ฆฌ๋ ์์๋ ์๋์ ๊ฐ๋ค. ์ธ๊ฐ์ ์ ๊ทธ๋ฆฐ ํ fill()
์ ์ด์ฉํด ์์ชฝ ์์์ ์ฑ์ธ ์๋ ์๋ค. ์ด๋ fillStyle = 'lightgray'
๋ฑ์ผ๋ก ์์์ ๋ฏธ๋ฆฌ ์ง์ ํด์ค์ผ ํ๋ค. fill()
์ ์ฌ์ฉํ๋ฉด path๊ฐ ๋ซํ์์ง ์์์ ๊ฒฝ์ฐ ์๋์ผ๋ก ๋ซ์ ํ ์ฑ์ด๋ค.
ctx.beginPath(); // ๋น ๊ฒฝ๋ก ์์ฑ
ctx.moveTo(x, y); // ์์์ ์ค์ (์ขํ๋ง ์ด๋)
ctx.lineTo(x, y); // ์์์ (ํ์ฌ ์ขํ)์์ x, y ์ขํ๊น์ง ์ ๊ธ๊ธฐ(์ขํ๋ง ๊ธฐ๋ก)
ctx.closePath(); // path๋ฅผ ๋ซ๋๋ค(์์์ ๊ณผ ๋์ ์ฐ๊ฒฐ)
ctx.stroke(); // ์ธ๊ณฝ์ "๊ทธ๋ฆฌ๊ธฐ"(ํ๋ฉด ์ถ๋ ฅ)
Path2D ์ธ์คํด์ค ๊ฐ์ฒด๋ฅผ ์๋ก ์์ฑํ์ฌ ๊ทธ๋ฆฌ๋ ๋ฐฉ๋ฒ๋ ์๋ค. Path2D๋ ๋ฐ๋ก beginPath()
๋ฉ์๋๋ฅผ ๊ฐ์ง ์๋๊ฑธ ๋ด์ ๋น ๊ฒฝ๋ก๋ฅผ ์๋์ผ๋ก ์์ฑํ ํ์๊ฐ ์๋๋ณด๋ค. Path2D ๊ฐ์ฒด๋ฅผ ์ด์ฉํด ๊ทธ๋ฆฐ ์ธ๊ณฝ์ ์ fill()
์ ์ด์ฉํด ์์ชฝ ์์์ ์ฑ์ธ ์ ์๋ค(์ฐธ๊ณ ๋งํฌ)
const path = new Path2D() // ์๋ก์ด Path ๊ฐ์ฒด ์์ฑ
path.moveTo(x, y) // ์์์ ์ค์ (์ขํ๋ง ์ด๋)
path.lineTo(x, y) // ์์์ (ํ์ฌ ์ขํ)์์ x, y ์ขํ๊น์ง ์ ๊ธ๊ธฐ(์ขํ๋ง ๊ธฐ๋ก)
path.closePath() // path๋ฅผ ๋ซ๋๋ค(์์์ ๊ณผ ๋์ ์ฐ๊ฒฐ)
ctx.stroke(path) // ์ธ๊ณฝ์ "๊ทธ๋ฆฌ๊ธฐ"(ํ๋ฉด ์ถ๋ ฅ)
์ ์ ๊ทธ๋ฆด ๋ ๋ง๋ค ๋งค๋ฒ ์ ๊ฐ์ ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ ๋ฒ๊ฑฐ๋กญ๊ธฐ ๋๋ฌธ์ ๋ณดํต x, y ์ขํ๋ฅผ ๋ฐ์ ์ ์ ๊ทธ๋ฆฌ๋ drawLine ๊ฐ์ ํจ์๋ฅผ ์ ์ธํด์ ์ฌ์ฉํ๋ค(๋๋ก์ ์ปจํ ์คํธ๊ฐ ์ ์ญ์ด ์๋์ด์ ์ ๊ทผํ ์ ์๋ค๋ฉด ctx๋ ๋งค๊ฐ๋ณ์๋ก ๋๊ฒจ์ผ ํ๋ค)
// ์ฑ์์ง์ง ์์ ๋ค๊ฐํ ๊ทธ๋ฆฌ๊ธฐ ์์
ctx.beginPath();
ctx.moveTo(170, 110);
ctx.lineTo(220, 110);
ctx.lineTo(220, 160);
ctx.closePath();
ctx.stroke();
// ์์์ด ์ฑ์์ง ๋ค๊ฐํ ๊ทธ๋ฆฌ๊ธฐ ์์
ctx.fillStyle = 'lightgray'; // ์ฑ์ฐ๊ธฐ ์์ ์ง์
ctx.beginPath();
ctx.moveTo(170, 110);
ctx.lineTo(220, 110);
ctx.lineTo(220, 160);
ctx.closePath();
ctx.fill();
์ฌ๊ฐํ Rect
x, y ์ขํ๋ฅผ ๋๊ธด ๊ณณ ๋ถํฐ width, height์ ํฌ๊ธฐ๋ฅผ ๊ฐ์ง๋ ์ฌ๊ฐํ์ ๊ทธ๋ฆฐ๋ค. clearRect()
๋ ๊ฐ์ ์๋ฆฌ๋ก ์บ๋ฒ์ค์ ๊ทธ๋ฆฐ ์ฌ๊ฐํ์ ์ง์ฐ๋๋ฐ ์บ๋ฒ์ค์ ๊ทธ๋ฆฐ ๋ชจ๋ ๋ํ์ ์ง์ฐ๊ณ ์ถ์ ๋ ์ ์ฉํ๋ค. ์ด๋ x, y๋ 0์ width, height๋ ์บ๋ฒ์ค์ ํฌ๊ธฐ๋ฅผ ๋๊ธฐ๋ฉด ๋๋ค.
ctx.strokeRect(x, y, width, height); // ์ฑ์์ง์ง ์์ ์ฌ๊ฐํ(์ธ๊ณฝ์ ๋ง ์๋)
ctx.fillRect(x, y, width, height); // ์ฑ์์ง ์ฌ๊ฐํ
ctx.clearRect(x, y, width, height); // ์ฌ๊ฐํ ์ญ์
์ Arc
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
arc(x, y, r, st, ed, anticlockwise);
// x, y : ์์ ์ค์ฌ์
// r : ๋ฐ์ง๋ฆ
// st, ed : ์์๊ฐ๊ณผ ๋๊ฐ(3์ ๋ฐฉํฅ์ผ๋ก 0๋ํ์ฌ ์๊ณ ๋ฐฉํฅ์ผ๋ก ์ฆ๊ฐํ๋ ๋ผ๋์ ๊ฐ)
// anticlockwise : ์์ ๊ทธ๋ฆฌ๋ ๋ฐฉํฅ true๋ ๋ฐ์๊ณ, false๋ ์๊ณ(๊ธฐ๋ณธ๊ฐ) ๋ฐฉํฅ
์ ๊ทธ๋ฆฌ๊ธฐ ์์ . ์ ์ ์ฒด๋ฅผ ๊ทธ๋ฆฌ๋ฏ๋ก ์ ๋ฐฉํฅ์ ๋ช ์ํ์ง ์์๋ ๋๋ค(์ด๋ ์ชฝ์ด๋ ์ ์ ์ฒด๊ฐ ๊ทธ๋ ค์ง).
ctx.beginPath();
ctx.arc(200, 200, 100, 0, 2 * Math.PI);
ctx.stroke();
์์ ๊ทธ๋ฆด ๋ ์์/๋ ๊ฐ์ ๋ผ๋์(ํธ๋๋ฒ; ๅผงๅบฆ) ๊ฐ์ด๋ค. ๋ผ๋์์ ๋ฐ์ง๋ฆ๊ณผ ํธ(ๅผง)์ ๊ธธ์ด๊ฐ ๊ฐ์์ง๋ ๊ฐ๋๋ฅผ ์๋ฏธํ๋๋ฐ ์ผ๋ฐ์ ์ผ๋ก ๋ผ๋์๋ณด๋จ ๊ฐ๋°
์ ์ต์ํ๋ฏ๋ก ๊ฐ๋ * Math.PI/180
์ผ๋ก ๋์ ๊ณ์ฐํ ์ ์๋ค.
1 * Math.PI
180๋2 * Math.PI
360๋
๊ณก์ / ํธ
์์ธํ ๋ด์ฉ์ ํ๊ธ ๋งํฌ ์ฐธ๊ณ .
quadraticCurveTo(cpx, cpy, x, y); // ๊ณก์ ๊ทธ๋ฆฌ๊ธฐ
arcTo(x1, y1, x2, y2, radius); // ์ ์ด์ ์ฌ์ด์ ํธ/๊ณก์ ๊ทธ๋ฆฌ๊ธฐ
ํ์ฌ ์ขํ๊ฐ ๋ํ/์ ์์ ์๋์ง ํ์ธ
ctx.isPointInPath([path], x, y)
์ธ์๋ก ์ ๋ฌํ x, y ์บ๋ฒ์ค ์ขํ๊ฐ ๋ํ์ ์์ชฝ ์์ญ์ ์๋์ง ํ์ธํ ํ Boolean ๊ฐ์ ๋ฐํํ๋ค. Path2D ๊ฐ์ฒด๋ฅผ ์ด์ฉํ๋ค๋ฉด ์ฒซ๋ฒ์งธ ์ธ์์ path
๋ฅผ ๋๊ฒจ์ผ ํ๋ค.
์๋๋ ํด๋ฆญํ ์ขํ๊ฐ ๋ํ์ ์์ชฝ์ด๋ผ๋ฉด ํด๋น ๋ํ์ ์ญ์ ํ๋ ์์ .
// onMouseDown ๋ง์ฐ์ค ํด๋ฆญ ํธ๋ค๋ฌ
const deleteHandler = ({ nativeEvent: e }) => {
const x = e.offsetX;
const y = e.offsetY;
// ์ ์ฅํ ๋ํ path๋ฅผ ํ๋์ฉ ๊ฒ์ฌํ๋ค
savedPolygon.forEach((polygon, index) => {
if (ctx.isPointInPath(polygon, x, y)) {
const remaningPolygons = [
...savedPolygon.slice(0, index),
...savedPolygon.slice(index + 1),
];
setSavedPolygon(remaningPolygons);
reDraw(ctx, remaningPolygons, canvasSize);
}
});
};
ctx.isPointInStroke([path], x, y)
์ธ์๋ก ์ ๋ฌํ x, y ์บ๋ฒ์ค ์ขํ๊ฐ Stroke ์์ญ์ ์์ชฝ์ธ์ง ํ์ธํ ํ Boolean ๊ฐ์ ๋ฐํํ๋ค. Path2D ๊ฐ์ฒด๋ฅผ ์ด์ฉํ๋ค๋ฉด ์ฒซ๋ฒ์งธ ์ธ์์ path
๋ฅผ ๋๊ฒจ์ผ ํ๋ค.
์๋๋ ์ขํ๊ฐ ์ ์์ ์๋ค๋ฉด ์ปค์๊ฐ grab์ผ๋ก ๋ฐ๋๋ ์์ .
// onMouseMove ๋ง์ฐ์ค ์ด๋ ํธ๋ค๋ฌ
if (ctx.isPointInStroke(x, y)) {
e.target.style.cursor = 'grab';
} else {
e.target.style.cursor = '';
}
์บ๋ฒ์ค ์ด๋ฏธ์ง ํฌ๋งท ๋ณํ / ๋ค์ด๋ก๋ / ๊ทธ๋ฆฌ๊ธฐ
์ด๋ฏธ์ง ๋ค์ด๋ก๋
์บ๋ฒ์ค์ ๊ทธ๋ฆฐ ๋ด์ฉ๋ค์ canvas.toDataURL(type, option)
์ ์ด์ฉํด ์ํ๋ ํฌ๋งท์ผ๋ก ๋ณํํ ํ ๋ค์ด๋ก๋ํ ์ ์๋ค. type
์๋ ์ด๋ฏธ์ง ํฌ๋งท์ ์
๋ ฅํ๊ณ , option
์๋ 0
๊ณผ 1
์ฌ์ด์ ์ซ์๋ฅผ ์
๋ ฅํ์ฌ ์ด๋ฏธ์ง์ ํ๋ฆฌํฐ๋ฅผ ์ง์ ํ ์ ์๋ค(๋์ ์๋ก ์ข์ ํ๋ฆฌํฐ). option
ํ๋ผ๋ฏธํฐ์ ์๋ฌด๊ฒ๋ ์
๋ ฅํ์ง ์์ผ๋ฉด 0.92
๊ฐ ๊ธฐ๋ณธ๊ฐ
toDataURL('image/png')
๊ธฐ๋ณธ๊ฐ(ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ๋ก ๋ช ์ํ์ง ์์ผ๋ฉด ๊ธฐ๋ณธ๊ฐ)"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAH0AAAB9C..."
toDataURL('image/webp')
toDataURL('image/jpg')
// ๋ค์ด๋ก๋ ๋ฒํผ ํด๋ฆญ์ ์ด๋ฏธ์ง ๋ค์ด๋ก๋
export default (canvasRef) => {
const image = canvasRef.current
.toDataURL('image/png') // ๊ธฐ๋ณธ๊ฐ
.replace('image/png', 'image/octet-stream'); // 2์ง ๋ฐ์ดํฐ ํํ
// window.location.href = image;
const a = document.createElement('a'); // aํ๊ทธ ์์ฑ
a.style.display = 'none'; // (์ ํ)ํ๋ฉด์์ ๋ณด์ด์ง ์๋๋ก display ์ค์
a.href = image; // ์ธ๋ถ ๋ฆฌ์์ค์ URL ํ ๋น
a.download = 'yourImage.png'; // ๋ค์ด๋ก๋ ์งํ ์ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ง์ ํ ํ์ผ๋ช
a.click(); // ํด๋ฆญ ์ด๋ฒคํธ
};
๐ก ๋ง์ฝ ์ด๋ฏธ์ง๊ฐ Blob ๊ฐ์ฒด๋ผ๋ฉด window.URL.createObjectURL()
์ ์ด์ฉํด url(DOM String)๋ก ๋ฐ๊ฟ ์ ์๋ค. ๋ณํํ url์ window ์ฐฝ์ด ์ฌ๋ผ์ง๋ฉด ํจ๊ป ์ฌ๋ผ์ง๋ฏ๋ก ๋ค๋ฅธ window์์ ์ฌ์ฌ์ฉํ ์ ์๋ค. (์ฐธ๊ณ ๋ธ๋ก๊ทธ)
// blob:http://localhost:1234/28ff8746-94eb-4dbe-9d6c-2443b581dd30
const url = window.URL.createObjectURL(blob);
a.href = url;
์ด๋ฏธ์ง ๋ฐ์ดํฐ ๊ทธ๋ฆฌ๊ธฐ
๋ฐ๋๋ก ์ด๋ฏธ์ง DataURL
์ ์บ๋ฒ์ค์ ๊ทธ๋ฆฌ๋ ค๋ฉด onload
์์ฑ์ ์ด์ฉํด์ผ ํ๋ค. ์ด๋ฏธ์ง๋ ํ
์คํธ์ ๋นํด ์ฉ๋์ด ํฌ๋ค. ์ฉ๋์ด ํฌ๋ฏ๋ก ๋ก๋ ์๋๋ ๋๋ฆฌ๋ค. ๋น๋๊ธฐ๋ก ์๋ํ๋ ํน์ฑ ๋๋ฌธ์ ์ด๋ฏธ์ง๋ฅผ ์์ ํ ๋ก๋ํ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ , ํ๋ฉด์ ์ด๋ฏธ์ง ํ๊ทธ๋ฅผ ๋จผ์ ํ์ํ ํ, ์ด๋ฏธ์ง ๋ฐ์ดํฐ๋ ๋ฐ๋ก ์ฝ๋๋ค.
onload
์์ฑ์ ์ฌ์ฉํ๋ฉด DOM ํธ๋ฆฌ๋ฟ๋ง ์๋๋ผ ์ด๋ฏธ์ง, ์คํ์ผ์ํธ ๊ฐ์ ์ธ๋ถ ๋ฆฌ์์ค๋ฅผ ๋ชจ๋ ๋ก๋ํ์ ๋ ํน์ ์์
์ ์ํํ๋๋ก ํ ์ ์๋ค. ์ฆ ์๋ ์ฝ๋๋ ์ด๋ฏธ์ง๊ฐ ๋ชจ๋ ๋ก๋๋ ํ์ ctx.drawImage()
๊ฐ ์คํ๋๋ค. ๋ง์ฝ onload
์์ฑ์ ์ฌ์ฉํ์ง ์์๋ค๋ฉด ctx.drawImage()
๋ฅผ ํธ์ถํ์ ์์ ์ ์ด๋ฏธ์ง๊ฐ ๋ชจ๋ ๋ก๋๋์ง ์์๋ค๋ฉด ํ๋ฉด ์ถ๋ ฅ์ ์คํจํ๋ค. — ์ฐธ๊ณ 1, ์ฐธ๊ณ 2
const image = new Image();
image.onload = function () {
ctx.drawImage(image, 0, 0);
};
image.src = correctAnswer;
// ์ ์ฝ๋๋ ์๋์ ๊ฐ๋ค
// <image src={correctAnswer} onload={() => ctx.drawImage(image, 0, 0)} />
์คํ ์ทจ์ ๊ธฐ๋ฅ / imageData
์บ๋ฒ์ค ์ ์ฒด ํน์ ์ผ๋ถ ํฝ์
์ ๋ณด๋ฅผ imageData ๊ฐ์ฒด๋ก ๋ฐํํ๋ getImageData()
๋ฅผ ์ด์ฉํด ์คํ ์ทจ์ ๊ธฐ๋ฅ์ ๊ตฌํํ ์๋ ์๋ค. ๋ง์ฐ์ค ํด๋ฆญ → ๋๋๊ทธ → ๋ง์ฐ์ค ํด๋ฆญ ํด์
์์
์ ๋ฐ๋ณตํ ๋๋ง๋ค ์บ๋ฒ์ค ์ ์ฒด๋ฅผ imageData๋ก ์ ์ฅํ๊ณ , ์คํ ์ทจ์ ๋ฒํผ์ ๋๋ฅด๋ฉด putImageData()
๋ฅผ ์ด์ฉํด ๋ฐ๋ก ์ด์ imageData ๊ฐ์ฒด๋ฅผ ์บ๋ฒ์ค์ ๋ณต์ํ๋ ๋ฐฉ์.
ctx.createImageData(width, height)
- ๋น imageData ์์ฑ
- width, height : imageData์ ๊ฐ๋ก/์ธ๋ก ํฌ๊ธฐ
ctx.createImageData(imageData)
- ์ฌ๋ณธ imageData ์์ฑ
ctx.getImageData(x, y, width, height)
- x, y : ์ด๋ํ ์ขํ ๊ฑฐ๋ฆฌ
- x, y ๋๋ค 0์ ์ ๋ ฅํ๊ณ ์บ๋ฒ์ค์ width, height๋ฅผ ์ ๋ ฅํ๋ฉด ์บ๋ฒ์ค ์ ์ฒด๋ฅผ ์ ์ฅํ๋ค.
ctx.putImageData(imageData, x, y, [dx], [dy], [dw], [dh])
- x, y : ์ด๋ํ ์ขํ ๊ฑฐ๋ฆฌ
- (์ต์ ) dx, dy : imageData์ x, y ์ขํ
- (์ต์ ) dw, dh : imageData์ ๊ฐ๋ก, ์ธ๋ก ํฌ๊ธฐ
- dx, dy, dw, dh : ์๋ณธ ์ด๋ฏธ์ง์ ์ผ๋ถ๋ฅผ ์๋ผ์ ์ฌ์ฉํ ๋ ์ ์ฉํ๋ค
๐ก ctx.getImageData(x, y, 1, 1).data
๋ฅผ ํตํด ํ์ฌ ์ขํ์ RGBA ์์๊ฐ์ ํ์ธํ ์ ์๋ค.
imageData ๊ฐ์ฒด์ data, width, height ์์ฑ์ ๊ฐ๋๋ค. data ์์ฑ์ ๋์คํฐ(ํฝ์
์ ๋ณด) ์ ๋ณด๊ฐ ๋ด๊ฒจ ์๋ค. 1๊ฐ ํฝ์
์ R
(็บข) G
(็ปฟ) B
(่) A
(Alpha) 4๊ฐ์ ๊ฐ์ ๊ฐ์ง๋ค. RGB ๊ฐ์ ๋ฒ์๋ 0~255์ ์ซ์์ด๋ฉฐ, ํฌ๋ช
๋๋ฅผ ๋ํ๋ด๋A
๋ 0์ ๊ฐ๊น์ธ ์๋ก ํฌ๋ช
ํด์ง๋ค.
const imageData = ctx.createImageData(300, 300);
console.log(imageData);
// {data: Uint8ClampedArray(360000), width: 300, height: 300}
for (let i = 0; i < imageData.data.length; i += 3) {
// ['R','G','B','A','R','G','B','A']
// RGB ARG BAR GBA ์์๋ฅผ ๋ฐ๋ณตํ๋ค
imageData.data[i + 0] = 100; // R/A/B/G...
imageData.data[i + 1] = 0; // G/R/A/B...
imageData.data[i + 2] = 0; // B/G/R/A...
}
ctx.putImageData(imageData, 300, 300);
์บ๋ฒ์ค ์ค์ ๊ฐ ์ ์ฅ/๋ณต์
์บ๋ฒ์ค ์ํ๋ ctx.save()
๋ก ์ ์ฅํ๊ณ , ctx.restore()
๋ก ๋ณต์ํ ์ ์๋ค. ์บ๋ฒ์ค์ ๊ทธ๋ฆฐ ์ /๋ํ์ด๋ ์ด๋ฏธ์ง๋ฅผ ์ ์ฅํ๋๊ฒ ์๋ ์บ๋ฒ์ค์ matrix, strokeStyle ๋ฑ์ ์์ฑ์ ์ ์ฅํ๋ค.
ctx.restore()
๋ ๊ฐ์ฅ ์ต๊ทผ์ ์ ์ฅํ ์บ๋ฒ์ค ์ํ๋ฅผ ๋ณต์ํ๋ค. save()
๋ฅผ 3๋ฒํ๊ณ restore()
๋ฅผ 3๋ฒํ๋ฉด ๋ง์ง๋ง save()
๋ถํฐ ์ฐจ๋ก๋๋ก ๋ณต์ํ๋ค. ๋ง์ง๋ง save()
๊น์ง ๋ชจ๋ ๋ณต์ํ ํ restore()
๋ฅผ ํ ๋ฒ ๋ ํ๋ฉด ์ฒซ๋ฒ์งธ๋ก ์ ์ฅํ save()
๊ฐ์ ๋ถ๋ฌ์จ๋ค(์ฐธ๊ณ ๋งํฌ).
ctx.save(); // 1๋ฒ ์ ์ฅ
ctx.save(); // 2๋ฒ ์ ์ฅ
ctx.save(); // 3๋ฒ ์ ์ฅ
ctx.restore(); // 3๋ฒ ๋ณต๊ตฌ
ctx.restore(); // 2๋ฒ ๋ณต๊ตฌ
ctx.restore(); // 1๋ฒ ๋ณต๊ตฌ
ctx.restore(); // 1๋ฒ ๋ณต๊ตฌ
์บ๋ฒ์ค ๊ทธ๋ฆฌ๊ธฐ ๋นํ์ฑํ
์บ๋ฒ์ค ์๋ฆฌ๋จผํธ CSS์ ์๋ ์์ฑ์ ์ถ๊ฐํ๋ค.
cursor: not-allowed; /* ์์ฒญ๋ ์์
์ ์ํํ์ง ์์์ ๋ํ๋ด๋ ์ปค์ */
pointer-events: none; /* ๋ง์ฐ์ค ํฌ์ธํฐ ์ต์
(ํด๋ฆญ, hover, ์ปค์ ๋๋๊ทธ ๋ฑ) ๋นํ์ฑํ */
๋ ํผ๋ฐ์ค
- MDN Canvas ์บ๋ฒ์ค ํํ ๋ฆฌ์ผ (ํ๊ธ)
- HTML Canvas Reference (์บ๋ฒ์ค ํ์ฉ ๋ ํผ๋ฐ์ค)
- Canvas ๅ่ๆๅ (์ค๋ฌธ)
- ํ๋ณด๋ ์บ๋ฒ์ค Canvas ๊ธฐ์ด ๊ฐ์ด๋ (ํ๊ธ)
- SoEn ์บ๋ฒ์ค Canvas ๊ฐ์ด๋ (ํ๊ธ)
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ๋๋ค ์ซ์, ๋ฐ์ดํฐ ์์ฑ ์์ (0) | 2024.04.25 |
---|---|
[JS] ์บ๋ฒ์ค Canvas ๋ํ ํ๋ ์ถ์ ํ ์์ ์กฐ์ ํ๊ธฐ (0) | 2024.04.24 |
[TS] ํ์ ์คํฌ๋ฆฝํธ - ํ์ ์ถ๋ก / ๋จ์ธ / ๊ฐ๋ (0) | 2024.04.24 |
[TS] ํ์ ์คํฌ๋ฆฝํธ - ์ ๋ค๋ฆญ, ํ๋ก๋ฏธ์ค (0) | 2024.04.24 |
[TS] ํ์ ์คํฌ๋ฆฝํธ - ๊ธฐ์ด ๋ด์ฉ ์ ๋ฆฌ (0) | 2024.04.24 |
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ๋๋ค ์ซ์, ๋ฐ์ดํฐ ์์ฑ ์์
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ๋๋ค ์ซ์, ๋ฐ์ดํฐ ์์ฑ ์์
2024.04.25 -
[JS] ์บ๋ฒ์ค Canvas ๋ํ ํ๋ ์ถ์ ํ ์์ ์กฐ์ ํ๊ธฐ
[JS] ์บ๋ฒ์ค Canvas ๋ํ ํ๋ ์ถ์ ํ ์์ ์กฐ์ ํ๊ธฐ
2024.04.24 -
[TS] ํ์ ์คํฌ๋ฆฝํธ - ํ์ ์ถ๋ก / ๋จ์ธ / ๊ฐ๋
[TS] ํ์ ์คํฌ๋ฆฝํธ - ํ์ ์ถ๋ก / ๋จ์ธ / ๊ฐ๋
2024.04.24 -
[TS] ํ์ ์คํฌ๋ฆฝํธ - ์ ๋ค๋ฆญ, ํ๋ก๋ฏธ์ค
[TS] ํ์ ์คํฌ๋ฆฝํธ - ์ ๋ค๋ฆญ, ํ๋ก๋ฏธ์ค
2024.04.24