๐Ÿ’ซ ๋ณต๊ถŒ ๊ธ๊ธฐ ์• ๋‹ˆ๋ฉ”์ด์…˜

Aromahyangยท2022๋…„ 12์›” 19์ผ
0

Frontend

๋ชฉ๋ก ๋ณด๊ธฐ
2/5
post-thumbnail

์–ด๋ฆด ๋•Œ ๋ณต๊ถŒ์„ ๊ธ์–ด๋ณด์ง„ ์•Š์•˜์ง€๋งŒ ๋ฌธํ™”์ƒํ’ˆ๊ถŒ PIN ๋ฒˆํ˜ธ ์•Œ๋ ค๊ณ  ๋งŽ์ด ๊ธ์–ด๋ดค๋‹ค.
ํ† ์Šค์˜ ๋ณต๊ถŒ ๊ธ๊ธฐ ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‹ˆ ์˜›๋‚  ์ƒ๊ฐ์ด ๋‚ฌ๊ณ , ๋งˆ์นจ ์ด๋ฅผ ๊ตฌํ˜„ํ•ด๋ณผ ๊ธฐํšŒ๊ฐ€ ์ƒ๊ฒจ์„œ ์ƒˆ๋กœ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜์— ๋„์ „ํ•ด๋ณด๊ฒŒ ๋˜์—ˆ๋‹ค.
๊ตฌํ˜„ํ•˜๋Š” ๊ณผ์ •์—์„œ ๋ฐฐ์šด ์ ๊ณผ ๋ฌธ์ œ ํ•ด๊ฒฐ์— ๋Œ€ํ•ด ์ ์–ด๋ณด์•˜๋‹ค.

Canvas API ํ™œ์šฉ

์‚ฌ์šฉ์ž์˜ ๋ฐ˜์‘(๋งˆ์šฐ์Šค/ํ„ฐ์น˜)์— ๋”ฐ๋ผ ๋‹น์ฒจ ๊ฒฐ๊ณผ๊ฐ€ ๋ณด์—ฌ์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— Canvas API๋ฅผ ํ™œ์šฉํ•˜์˜€๋‹ค.

canvas ์ดˆ๊ธฐํ™”

๋จผ์ € ๋ณต๊ถŒ ๊ธ๊ธฐ ์ „ ๋ชจ์Šต์„ ๊ทธ๋ ค์ค€๋‹ค. (์Šคํƒ€์ผ ์ฝ”๋“œ๋Š” ์ƒ๋žตํ•˜์˜€๋‹ค.)

<!-- 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)์˜ ์ดํ•ด

profile
ํ•œ ์šฐ๋ฌผ๋งŒ ํŒŒ๋Š” ์‚ฌ๋žŒ

2๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2024๋…„ 9์›” 2์ผ

์•ˆ๋…•ํ•˜์„ธ์š” ๋•๋ถ„์— ์Šคํฌ๋ž˜์น˜ ๋ณต๊ถŒ ๊ธฐ๋Šฅ์„ ์ด์šฉํ•ด์„œ ์„œ๋น„์Šคํ•˜๋‚˜๋ฅผ ๋งŒ๋“ค์—ˆ์–ด์š”! ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!
https://saramjh.github.io/scratchLottery/

1๊ฐœ์˜ ๋‹ต๊ธ€