์ด๋ฆด ๋ ๋ณต๊ถ์ ๊ธ์ด๋ณด์ง ์์์ง๋ง ๋ฌธํ์ํ๊ถ PIN ๋ฒํธ ์๋ ค๊ณ ๋ง์ด ๊ธ์ด๋ดค๋ค.
ํ ์ค์ ๋ณต๊ถ ๊ธ๊ธฐ ์ด๋ฒคํธ๋ฅผ ๋ณด๋ ์๋ ์๊ฐ์ด ๋ฌ๊ณ , ๋ง์นจ ์ด๋ฅผ ๊ตฌํํด๋ณผ ๊ธฐํ๊ฐ ์๊ฒจ์ ์๋ก์ด ์ ๋๋ฉ์ด์
์ ๋์ ํด๋ณด๊ฒ ๋์๋ค.
๊ตฌํํ๋ ๊ณผ์ ์์ ๋ฐฐ์ด ์ ๊ณผ ๋ฌธ์ ํด๊ฒฐ์ ๋ํด ์ ์ด๋ณด์๋ค.
์ฌ์ฉ์์ ๋ฐ์(๋ง์ฐ์ค/ํฐ์น)์ ๋ฐ๋ผ ๋น์ฒจ ๊ฒฐ๊ณผ๊ฐ ๋ณด์ฌ์ผ ํ๊ธฐ ๋๋ฌธ์ Canvas API๋ฅผ ํ์ฉํ์๋ค.
๋จผ์ ๋ณต๊ถ ๊ธ๊ธฐ ์ ๋ชจ์ต์ ๊ทธ๋ ค์ค๋ค. (์คํ์ผ ์ฝ๋๋ ์๋ตํ์๋ค.)
<!-- html -->
<div class="wrapper">
<div class="result">๋น์ฒจ ๊ฒฐ๊ณผ์
๋๋ค.</div>
<canvas id="canvans" />
</div>
// javascript
const $canvas = document.getElementById("canvas");
const context = $canvas.getContext("2d");
const WIDTH = 400;
const HEIGHT = 200;
const dpr = window.devicePixelRatio;
const initCanvas = () => {
$canvas.style.width = `${WIDTH}px`;
$canvas.style.height = `${HEIGHT}px`;
$canvas.width = WIDTH * dpr;
$canvas.height = HEIGHT * dpr;
context.scale(dpr, dpr);
// ํ์ ๋ฐฐ๊ฒฝ์ผ๋ก ๋ฎ๊ธฐ
context.strokeStyle = "#999";
context.fillStyle = "#999";
context.beginPath();
context.roundRect(0, 0, WIDTH, HEIGHT, 8);
context.stroke();
context.fill();
// ์๋ด ๋ฌธ๊ตฌ ์ถ๊ฐ
context.font = "20px sans-serif";
context.fillStyle = "#000";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("์ฌ๊ธฐ๋ฅผ ๊ธ์ด๋ณด์ธ์", WIDTH / 2, HEIGHT / 2);
};
initCanvas();
ํ๋ฉด์ ๋ณด์ฌ์ก์ผ๋ฉด ํ๋ ์บ๋ฒ์ค์ ํฌ๊ธฐ๋ 400x200์ธ๋ฐ, ์บ๋ฒ์ค์ ๊ฐ๋ก, ์ธ๋ก ํฝ์
์๋ ๋ณด์ฌ์ฃผ๊ณ ์ ํ๋ ๊ธธ์ด์ window.devicePixelRatio
๋ฅผ ๊ณฑํ ๊ฐ์ ์คฌ๋ค.
์บ๋ฒ์ค์์ ์ฌ์ฉ๋๋ ํฝ์
์ ํฌ๊ธฐ์ css ํฝ์
ํฌ๊ธฐ๊ฐ 1:1๋ก ์ผ์นํ์ง ์์ ์ ์๊ธฐ ๋๋ฌธ์ devicePixelRatio
๋ก ๊ฐ ํฝ์
์ด ์๋ก ๊ฐ๋๋ก ์กฐ์ ํด์ฃผ๋ ๊ฒ์ด๋ค.
๋ง์ฝ ์ด๋ฐ ์ฐจ์ด๋ฅผ ํด์ํ์ง ์์ผ๋ฉด Retina ๋์คํ๋ ์ด์ ๊ฐ์ ํน์ ํ๋ฉด์์ ์บ๋ฒ์ค์ ๊ธ์ ๋ฑ์ด ํ๋ฆฟํ๊ฒ ๋ณด์ผ ์ ์๋ค.
devicePixelRatio
๋ ํฝ์
์ ๋ํด์ ์ข ๋ ์์๋ณด๊ณ ์ถ๋ค๋ฉด ์ด ๊ธ์ ํ ๋ฒ ์ฝ์ด๋ณด๋ ๊ฒ๋ ์ข๋ค.
๋ง์ฐ์ค๊ฐ ๋๋ฌ์ก์ ๋(ํฐ์น๋ฅผ ์์ํ ๋), ๋ง์ฐ์ค๊ฐ ์์ง์ผ ๋(ํฐ์นํ์ฌ ์์ง์ผ ๋), ๋ง์ฐ์ค๋ฅผ ๋์ ๋(ํฐ์น๋ฅผ ๊ทธ๋งํ ๋)์ ๋ํ ๋์์ ์์ฑํ๋ค.
์ javascript ์ฝ๋์ ์ถ๊ฐ๋ก ์๋ ์ฝ๋๋ฅผ ์์ฑํ์๋ค.
// javascript
const ERASE_RADIUS = 30;
const { top: canvasTop, left: canvasLeft } = $canvas.getBoundingClientRect();
let isDrawing = false;
const handleDrawingStart = () => {
if (!isDrawing) {
isDrawing = true;
}
};
const handleDrawing = (event) => {
if (isDrawing) {
const { offsetX, offsetY } = event;
context.save();
context.globalCompositeOperation = "destination-out";
context.beginPath();
context.arc(offsetX, offsetY, ERASE_RADIUS, 0, 2 * Math.PI, false);
context.fill();
context.closePath();
context.restore();
}
};
const handleDrawingEnd = () => {
if (isDrawing) {
isDrawing = false;
}
};
$canvas.addEventListener("mousedown", handleDrawingStart);
$canvas.addEventListener("mousemove", handleDrawing);
$canvas.addEventListener("mouseup", handleDrawingEnd);
๋ง์ฐ์ค๊ฐ ๋๋ฌ์ง๊ณ ๋ ๋(ํฐ์น๋ฅผ ์์ํ๊ณ ๋๋ผ ๋)์๋ ํฌ๋ช
ํ ์์ ๊ทธ๋ฆด์ง ๋ง์ง๋ฅผ ์ ์ดํ๋ค.
๋ง์ฐ์ค๋ฅผ ์์ง์ด๋ฉด์(ํฐ์นํ ์ฑ๋ก ์์ง์ด๋ฉด์) ํฌ๋ช
ํ ์์ ๊ทธ๋ฆด ์ ์๋ ์ํ๋ผ๋ฉด ํฌ๋ช
ํ ์์ ๊ทธ๋ฆฌ๋๋ฐ, context.save()
์ context.restore()
, context.globalCompositeOperation
์ ์ด์ฉํ์๋ค.
context.save()
๋ ์บ๋ฒ์ค์ ์ปจํ
์คํธ์ ์ง์ ๋ ์์ฑ ๊ฐ์ ์ ์ฅํ๋ค.
context.restore()
๋ ๊ฐ์ฅ ์ต๊ทผ์ ์ปจํ
์คํธ์ ์ง์ ๋ ์์ฑ ๊ฐ์ ๋ถ๋ฌ์จ๋ค.
๋ณดํต ๋ฐ๋ณต์ ์ธ ๋๋ก์ ์์
์ ํ ๋๋ ํน์ ์์ฑ๋ค์ ์ ์ฅํด์ผ ํ ๋ save
, restore
๋ฉ์๋๋ฅผ ์ฐ๋ ๊ฒ ๊ฐ๋ค.
save
, restore
ํจ์ผ๋ก์จ ๋
๋ฆฝ์ ์ธ ์บ๋ฒ์ค ์ปจํ
์คํธ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๊ธฐ ๋๋ฌธ์ธ ๊ฑฐ ๊ฐ๋ค.
context.globalCompositeOperation
์ ์บ๋ฒ์ค์ ๊ทธ๋ ค์ง ๋ํ์ด ๊ฒน์ณค์ ๋ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง๋ฅผ ์ ํ ์ ์๋ค.
context.globalCompositeOperation = 'destination-out';
์ ๋ํ์ด ๊ฒน์น ๊ฒฝ์ฐ ์ฒ์ ๊ทธ๋ ค์ง ๋ํ์์ ๊ฒน์น์ง ์๋ ๋ถ๋ถ๋ง ๊ทธ๋ฆฐ๋ค.
์ฆ, ์ฒ์ ๊ทธ๋ ค์ง ๋ํ๊ณผ ๊ฒน์น๋ ๋ถ๋ถ์ ํฌ๋ช
ํ๊ฒ ์ฒ๋ฆฌ๋๋ค.
๋ง์ฝ ๊ธ์ ์ ์๋ ์์ญ์ ๋ช %๋ฅผ ๊ธ์์ ๋ ์๋์ผ๋ก ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ฃผ๋ฉด ์ด๋จ๊น?
์ด ๋ธ๋ก๊ทธ๋ฅผ ์ฐธ๊ณ ํ์ฌ ์๊ณ์น์ ๋๋ฌํ๋ฉด ๊ธ์ง ์์ ์์ญ๋ ๋ชจ๋ ์ฌ๋ผ์ง๋๋ก ํ์๋ค.
๋ฐฉ๋ฒ์ ์๋์ ๊ฐ๋ค.
1. ํฌ๋ช
ํ ์๋ค ์ฌ์ด์ ๊ฐ๊ฒฉ์ ์ฃผ์์ ๋, ์บ๋ฒ์ค์ ๊ทธ๋ฆด ์ ์๋ ํฌ๋ช
ํ ์์ ์ต๋ ๊ฐ์๋ฅผ ๊ตฌํ๋ค.
2. ํฌ๋ช
ํ ์์ ๊ทธ๋ฆด ๋ ์ด๋ฏธ ๊ทธ๋ ค์ง ํฌ๋ช
ํ ์๋ค๊ณผ์ ๊ฐ๊ฒฉ์ด ์์์ ๊ฐ๊ฒฉ๋ณด๋ค ํด ๊ฒฝ์ฐ ์๋ก์ด ๋ฐฐ์ด์ ์ถ๊ฐํ๋ค.
3. ์๋ก์ด ๋ฐฐ์ด์ ์์์ ๊ฐ์๊ฐ 1์์ ๊ตฌํ ์ต๋ ๊ฐ์๋ฅผ ๋์ผ๋ฉด ํฌ๋ช
ํ ์์ ๊ทธ๋ง ๊ทธ๋ฆฌ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ค๋ค.
// javascript
const ERASE_DISTANCE = ERASE_RADIUS / 2; // ์ง์์ง ์์ญ(ํฌ๋ช
ํ ์)๊ฐ ์์ ๊ฐ๊ฒฉ
let thresholdOfEraseCount = 0;
const initCanvas = () => {
// ์ initCanvas ํจ์์ ์ถ๊ฐ
// ์๋์ผ๋ก ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ค ์๊ณ์น ์ค์
const col = Math.ceil(WIDTH / (ERASE_RADIUS * 2 + ERASE_DISTANCE));
const row = Math.ceil(HEIGHT / (ERASE_RADIUS * 2 + ERASE_DISTANCE));
thresholdOfEraseCount = col * row;
// ํฌ๋ช
ํ ์์ด ์ต๋๋ก ๊ทธ๋ ค์ง ๊ฒฝ์ฐ๋ฅผ ์บ๋ฒ์ค์ ํํ
for (let i = 0; i < col; i++) {
for (let j = 0; j < row; j++) {
context.save();
context.beginPath();
context.arc(
ERASE_RADIUS + i * (ERASE_RADIUS * 2 + ERASE_DISTANCE),
ERASE_RADIUS + j * (ERASE_RADIUS * 2 + ERASE_DISTANCE),
ERASE_RADIUS,
0,
2 * Math.PI,
false
);
context.fill();
context.closePath();
context.restore();
}
}
};
400x200 ํฌ๊ธฐ์ ์บ๋ฒ์ค์ ํฌ๋ช
ํ ์ ์ฌ์ด์ ๊ฐ๊ฒฉ์ ๋ฐ์ง๋ฆ(15)๋งํผ ์ค ๊ฒฝ์ฐ ์ต๋ 18๊ฐ๊ฐ ๊ทธ๋ ค์ง๋ค.
2, 3๋ฒ ๊ณผ์ ์ ์๋ ์ฝ๋๋ก ์์ฑํด๋ณด์๋ค.
ํฌ๋ช
ํ ์ ์ฌ์ด์ ๊ฐ๊ฒฉ์ ์ค์ฌ์ ๊ณผ ๋ง์ฐ์ค(ํฐ์น) ์์น๋ก ๊ตฌํ์๋ค.
// javascript
let isRevealed = false;
const drawTransparentCircle = (x, y) => {
context.save();
context.globalCompositeOperation = "destination-out";
context.beginPath();
context.arc(x, y, ERASE_RADIUS, 0, 2 * Math.PI, false);
context.fill();
context.closePath();
context.restore();
const checkList = erasedList.filter(({ x: posX, y: posY }) => {
const distX = posX - x;
const distY = posY - y;
return (
Math.sqrt(distX * distX + distY * distY) < ERASE_RADIUS + ERASE_DISTANCE
);
});
if (!checkList.length) {
erasedList.push({ x, y });
}
};
const handleDrawing = (x, y) => {
if (isDrawing) {
if (erasedList.length < thresholdOfEraseCount) {
drawTransparentCircle(x, y);
} else {
if (!isRevealed) {
context.clearRect(0, 0, WIDTH, HEIGHT);
isRevealed = true;
}
}
}
};
HTML Canvas ๋ณต๊ถ ๊ธ๊ธฐ ํจ๊ณผ(scratch, lottery)
[CSS] DPR(Device-pixel-ratio)์ ์ดํด
์๋ ํ์ธ์ ๋๋ถ์ ์คํฌ๋์น ๋ณต๊ถ ๊ธฐ๋ฅ์ ์ด์ฉํด์ ์๋น์คํ๋๋ฅผ ๋ง๋ค์์ด์! ๊ฐ์ฌํฉ๋๋ค!
https://saramjh.github.io/scratchLottery/