게으른개발너D

[JS] Paint App 2 - handle JS 본문

프로젝트/Side Project

[JS] Paint App 2 - handle JS

lazyhysong 2023. 2. 14. 17:25

Canvas Events

캔버스에 발생하는 이벤트를 세팅해 주려고 한다.

1. mousemove

캔버스 안에서 마우스를 움직였을 때 스크립트에서 감지를 해야 한다.

먼저 캔버스를 불러오자.

const canvas = document.getElementById("jsCanvas");

function onMouseMove(event) {
    console.log(event);
}

if(canvas) {
    canvas.addEventListener("mousemove", onMouseMove);
}

캔버스 안에서 마우스를 움직였을 때 onMouseMove라는 함수가 작동하게 만들었다.

console에서 해당 이벤트를 들여다보면 이렇게 나온다.

마우스를 캔버스 바깥에서 움직이면 아무것도 찍히지 않지만

캔버스 안에서 움직이면 마우스 좌표를 포함한 이벤트 정보들이 엄청나게 찍혀 나온다.

해당 이벤트 정보를 열어서 확인하면 offsetXoffsetY라는 게 있는데

이게 캔버스 안에서의 마우스 위치 정보이다.

참고로 screenX, Y는 스크린 안에서의 마우스 위치 정보이므로 헷갈리면 안 된다.

function onMouseMove(event) {
    const x = event.offsetX;
    const Y = event.offsetY;
}

일단은 요렇게 마우스 위치 정보를 세팅해 두었다.

2. mousedown

마우스를 클릭한 상태로 그림을 그려야 하기 때문에

캔버스 위에서 마우스를 클릭했을 때 이벤트를 받아와야 한다.

const canvas = document.getElementById("jsCanvas");

let painting = false;

function onMouseMove(event) {
    const x = event.offsetX;
    const Y = event.offsetY;
}

function startPainting(event) {
    painting = true
}

if(canvas) {
    canvas.addEventListener("mousemove", onMouseMove);
    canvas.addEventListener("mousedown", startPainting);
}

mousedown 은 마우스를 클릭했을 때 발생되는 이벤트이다.

painting 이라는 변수를 만들어 false이면 페인트칠이 안되게,

true이면 페인트칠이 되도록 하려고 한다.

3. mouseup

마우스 클릭을 놓으면 페인팅이 멈추게 해야 한다.

function stopPainting(event) {
    painting = false;
}

if(canvas) {
    canvas.addEventListener("mousemove", onMouseMove);
    canvas.addEventListener("mousedown", startPainting);
    canvas.addEventListener("mouseup", stopPainting);
}

mouseup은 마우스 클릭을 놓았을 때 발생하는 이벤트이다.

stopPainting 함수를 만들어서 변수 painting이 false가 되도록 작성했다.

4. mouseleave

이제 마우스가 캔버스 바깥으로 나갔을 때 페인팅이 멈추도록 만들어야 한다.

function stopPainting() {
    painting = false;
}

if(canvas) {
    canvas.addEventListener("mousemove", onMouseMove);
    canvas.addEventListener("mousedown", startPainting);
    canvas.addEventListener("mouseup", stopPainting);
    canvas.addEventListener("mouseleave", stopPainting);
}

mouseleave는 canvas에서 마우스가 나갔을 때 발생하는 이벤트.

stopPainting 함수를 이벤트 리스너에 추가해 주었다.

 

 

 


 

2D Context

캔버스 사용 방법은 MDN에서 자세히 알 수 있다.

https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D

 

CanvasRenderingContext2D - Web APIs | MDN

The CanvasRenderingContext2D interface, part of the Canvas API, provides the 2D rendering context for the drawing surface of a <canvas> element. It is used for drawing shapes, text, images, and other objects.

developer.mozilla.org

캔버스는 HTML 요소 중 하나인데, 캔버스에는 context라는 게 있다.

이것으로 우리는 캔버스 안에서 픽셀을 컨트롤할 수 있다.

1. context

먼저 context를 2D로 불러오고 캔버스 사이즈를 정의하자.

const canvas = document.getElementById("jsCanvas");
const ctx = canvas.getContext("2d");

canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;

css로 정의한 사이즈는 화면에서 보여주기 위한 사이즈이고

js에서는 캔버스 안에서 움직이는 이벤트를 감지하기 위해 따로 정의해 줘야 한다.

화면에 따라 캔버스 크리가 달라질 수 있으니

크기는 현재 화면에서 정의한 크기를 그대로 가져오도록 작성했다.

다음은 그림의 기본 색상과 선 굵기를 설정한다.

ctx.strokeStyle = "#2c2c2c";
ctx.lineWidth = 2.5;

맨 처음 HTML 코드로 설정한 값들을 넣어주었다.

2. path

다음은 캔버스 위에서 마우스를 클릭했을 때 선을 그려야 한다.

마우스를 클릭했을 때부터 선이 그려져야 하기 때문에 클릭전부터

마우스 위치를 즉각 알아야 한다.

function onMouseMove(event) {
    const X = event.offsetX;
    const Y = event.offsetY;

    if(!painting) {
        ctx.beginPath();
        ctx.moveTo(X, Y);
    }
}

여기서 path는 선이다.

beginPath를 하고 moveTo에 현재 마우스 위차를 넣으면

마우스를 움직일 때마다 (투명)선을 그리며 마우스를 따라간다.

moveTo로 마우스를 클릭하기 직전 선의 시작점을 알 수 있다.

3. stroke

이제 마우스를 클릭하며 라인을 그려보자.

function onMouseMove(event) {
    const X = event.offsetX;
    const Y = event.offsetY;

    if(!painting) {
        ctx.beginPath();
        ctx.moveTo(X, Y);
    } else {
        ctx.lineTo(X, Y);
        ctx.stroke();
    }
}

변수 painting이 false 일 때 마우스 위치 정보를 계속 불러오면서 라인에 색을 칠해야 한다.

이때 마우스 위치를 계속 잡아주는 것이 lineTo이다.

stroke는 획을 그을 수 있는 이벤트이다.

마우스를 클릭하는 순간 !painting 안에 정의했던 코드는 실행되지 않는다.

대신 moveTo로 잡아놓은 위치는 beginPath로 인해 선의 시작점이 된다.

이 지점부터 line이 새로운 좌표를 그리며 움직이는 것이다.

요렇게 선을 긋는 것을 완성하였다.

 

 

 


Changing Color

1. Changing Color

선 색상을 변경하는 코드를 작성해 보자.

<div class="control_color jsColor" style="background-color: #2c2c2c;"></div>

색상 버튼 코드 9개에 jsColor라는 class를 추가해 주었다.

해당 클래스를 가진 div를 모두 부른다.

const colors = document.getElementsByClassName("jsColor");
...
function handleClickColor(event) {
    console.log(event.target.style);
}
...
Array.from(colors).forEach(color => color.addEventListener("click", handleClickColor));

forEach를 쓰기 위해 colors를 Array로 만든 후

각각의 요소마다 클릭 이벤트 리스너를 달아주었다.

각각의 color 버튼을 클릭하면 콘솔에서 다음과 같이 찍힌다.

우리가 원하는 건 background-color로 지정해 준 색상이기 때문에

색상 버튼을 클릭할 때마다 배경 색상을 불러와

그걸 라인 색 정의한 코드에 overriding 한다.

function handleClickColor(event) {
   const color = event.target.style.backgroundColor;
   ctx.strokeStyle = color;
}

 

2. Changing Active

현재 선택된 색상이 무엇인지 알 수 있도록 클릭한 색상을 표시한다.

클릭한 색상엔 active라는 class를 넣어 css로 형광 노란색 동그라미를 표시했다.

function handleClickColor(event) {
    Array.from(colors).forEach(
        color => color.classList.remove("active")
    );
    
    const color = event.target.style.backgroundColor;
    ctx.strokeStyle = color;
    event.target.classList.add("active");
}
...
Array.from(colors).forEach(
    color => color.addEventListener("click", handleClickColor)
);

함수를 불러올 때마다 forEach로 버튼들을 모두 돌아가면서 active class를 없애고

target 버튼에 active class를 추가한다.

쫌 더 좋은 방법이 있으면 누가 알려주면 좋겠다ㅠㅠ

 

 

 

 


Brush Size

브러시 사이즈 조절을 구현할 것이다.

const range = document.getElementById("jsRange");
...
function handleRangeChange(event) {
    console.log(event);
}
...
if(range) {
    range.addEventListener("input", handleRangeChange);
}

range를 불러오고 난 후

handleRangeChange란 함수를 이벤트 리스너에 추가해 주었다.

참고로 range 변화에 대한 이벤트는 input 으로 걸어줘야 한다.

range를 살짝 움직인 후 event를 콘솔 창에서 보면 다음과 같다.

target을 열면 현재 range가 가리키고 있는 value 값을 알 수 있다.

function handleRangeChange(event) {
    const rangeSize = event.target.value;
    ctx.lineWidth = rangeSize;
}

range value 값을 받아서 라인 두께 값으로 overriding 해줬다.

이제 브러시 두께 값을 조정하며 그림을 그릴 수 있다.

 

 

 

 

 


Filling Mode

1. Changing Mode

컨트롤러의 FILL 버튼을 클릭하면 캔버스를 원하는 색상으로 채우는 filling 모드가 되고

그것을 다시 클릭하면 paint 모드로 변경할 것이다.

const mode = document.getElementById("jsMode");
...
let filling = false;
...
function handleModeChange() {
    if(filling === false) {
        filling = true;
        mode.innerText = "PAINT";
    } else {
        filling = false;
        mode.innerText = "FILL";
    }
}
...
if(mode) {
    mode.addEventListener("click", handleModeChange);
}

버튼을 불러온 후 handleModeChange 함수를 추가했다.

함수 안에는 채우기 모드가 아닐 땐 변수 filling이 false, 채우기 모드일 땐 filling이 true가 되도록 했고,

버튼 안 글자도 채우기와 페인트로 변하도록 했다.

FILL 버튼을 클릭하면 다음과 같이 바뀐다.

2. Filling Mode

filling 모드일 때 캔버스를 색상으로 채우는 걸 구현해 보자.

먼저 MDN을 다시 들여다보자.

아래로 살짝 내려보면 fillRect라는 게 있다.

x, y 좌표 부분에서 지정한 사각형을 그리는 것이다.

ctx.fillRect(50, 50, 100, 120);

요렇게 구현하면 (50, 50) 위치에 가로 100px, 세로 120px 크기의 사각형을 그려낸다.

fill의 색상은 라인 색상을 지정했던 strokeStyle가 아니라

fillStyle을 적용해야 한다.

ctx.fillStyle = "2c2c2c";

색상을 변경할 때마다 fillStyle의 색상도 변경되도록 만들자

function handleClickColor(event) {
    Array.from(colors).forEach(
        color => color.classList.remove("active")
    );
    
    const color = event.target.style.backgroundColor;
    ctx.strokeStyle = color;
    ctx.fillStyle = color;
    event.target.classList.add("active");
}

그리고 filling 모드일 때 캔버스를 클릭하면 색상으로 채우기를 구현.

const CANVAS_SIZE_X = canvas.offsetWidth;
const CANVAS_SIZE_Y = canvas.offsetHeight; 

canvas.width = CANVAS_SIZE_X;
canvas.height = CANVAS_SIZE_Y;
...
function handleClickCanvas() {
    if(filling) {
        ctx.fillRect(0, 0, CANVAS_SIZE_X,  CANVAS_SIZE_Y);
    }
}
...
if(canvas) {
    canvas.addEventListener("mousemove", onMouseMove);
    canvas.addEventListener("mousedown", startPainting);
    canvas.addEventListener("mouseup", stopPainting);
    canvas.addEventListener("mouseleave", stopPainting);
    canvas.addEventListener("click", handleClickCanvas);
}

캔버스를 클릭하면 handleClickCanvas 함수를 호출한다.

filling이 true 일 때, (0, 0) 좌표에서 캔버스 크기만 한 사각형을 만들어 내도록 구현했다.

캔버스 사이즈는 중복되어 사용되므로 변수로 만들었다.

 

 

 

 


Saving the Image

마지막으로 캔버스로 그린 그림을 이미지로 저장하기!

캔버스에 오른쪽 마우스를 클릭하면 이렇게 나온다.

다른 이름으로 저장을 클릭하면

캔버스에 그렸던 이미지를 저장할 수 있다.

캔버스는 픽셀을 다루는 거라서 기본적으로 이미지를 만들어낸다.

먼저 캔버스에서 우 클릭을 막을 것이다.

function handleCT(event) {
    event.preventDefault();
}
...
if(canvas) {
    canvas.addEventListener("mousemove", onMouseMove);
    canvas.addEventListener("mousedown", startPainting);
    canvas.addEventListener("mouseup", stopPainting);
    canvas.addEventListener("mouseleave", stopPainting);
    canvas.addEventListener("click", handleClickCanvas);
    canvas.addEventListener("contextmenu", handleCT);
}

우 클릭 이벤트는 contextmenu로 지정할 수 있다.

이제 SAVE 버튼을 클릭했을 때 이미지를 저장해 보자.

https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL

 

HTMLCanvasElement.toDataURL() - Web APIs | MDN

The HTMLCanvasElement.toDataURL() method returns a data URL containing a representation of the image in the format specified by the type parameter.

developer.mozilla.org

MDN에 date url을 보면 파장할 파일의 형식을 지정할 수 있다.

default 값은 png이다.

const saveBtn = document.getElementById("jsSave");
...
function handleClickSave() {
    const url = canvas.toDataURL();
    const image = document.createElement("a");
    image.href = url;
    image.download = "paintJS[🎨]";
    image.click();
}
...
if(saveBtn) {
    saveBtn.addEventListener("click", handleClickSave);
}
 

SAVE 버튼을 불러온 후 handleClickSave 함수를 추가했다.

함수 안엔 canvas의 date url을 만들어 a 태그 안에 담아냈다.

download element로 저장할 이미지의 이름을 지정할 수 있다.

이렇게 다 만든 후 a 태그를 가상으로 클릭하여 바로 다운로드 되도록 구현했다.

SAVE를 클릭하면 paintJS[🎨]란 이름으로 즉시 다운로드 된다.

간단한 페인트 앱 완성!

 

 

 


index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="style.css"/>
    <title>PaintJS</title>
</head>
<body>
    <canvas id="jsCanvas" class="canvas"></canvas>
    <div class="controls">
        <div class="controls_range">
            <input type="range" id="jsRange" min="0.1" max="10.0" value="2.5" step="0.5" />
        </div>
        <div class="controls_btns">
            <button id="jsMode">Fill</button>
            <button id="jsSave">Save</button>
        </div>
        <div id="jsColors" class="controls_colors">
            <div class="control_color jsColor active" style="background-color: #2c2c2c;"></div>
            <div class="control_color jsColor" style="background-color: #fff;"></div>
            <div class="control_color jsColor" style="background-color: #ff0077;"></div>
            <div class="control_color jsColor" style="background-color: #ff4da0;"></div>
            <div class="control_color jsColor" style="background-color: #ff8bae;"></div>
            <div class="control_color jsColor" style="background-color: #ffd1dc;"></div>
            <div class="control_color jsColor" style="background-color: #c1b0ff;"></div>
            <div class="control_color jsColor" style="background-color: #b43ccc;"></div>
            <div class="control_color jsColor" style="background-color: #db69fd;"></div>
        </div>
    </div>
    <script src="app.js"></script>
</body>
</html>

style.css

@import "reset.css";

body {
    background-color: #f6f9fc;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 
        Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding-top: 30px;
}

.canvas {
    width: 500px;
    height: 500px;
    background-color: #fff;
    border-radius: 15px;
    box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(50, 50, 93, 0.11);
}
.controls {
    margin-top: 50px;
    display: flex;
    flex-direction: column;
    align-items: center;
}
.controls .controls_range {
    margin-bottom: 15px;
}
.controls .controls_btns {
    margin-bottom: 15px;
}
.controls_btns button {
    all: unset;
    cursor: pointer;
    background-color: #fff;
    padding: 5px 0px;
    width: 50px;
    font-size: 14px;
    border-radius: 10px;
    text-align: center;
    box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(50, 50, 93, 0.11);
    text-transform: uppercase;
    font-weight: 600;
    color: rgba(0,0,0,0.8);
}
.controls_btns button:active {
    transform: scale(0.98);
}
.controls .controls_colors {
    display: flex;
    border-radius: 10px;
    box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(50, 50, 93, 0.11);
}
.controls_colors .control_color {
    width: 30px;
    height: 30px;
    cursor: pointer;
}
.controls_colors .control_color:first-child {
    border-radius: 10px 0 0 10px;
}
.controls_colors .control_color:last-child {
    border-radius: 0 10px 10px 0;
}
.control_color.active {
    position: relative;
}
.control_color.active:after {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 70%;
    height: 70%;
    border-radius: 50%;
    border: 2px solid rgb(255, 253, 147);
}

app.js

const colors = document.getElementsByClassName("jsColor");
const canvas = document.getElementById("jsCanvas");
const range = document.getElementById("jsRange");
const mode = document.getElementById("jsMode");
const saveBtn = document.getElementById("jsSave");
const ctx = canvas.getContext("2d");

const CANVAS_SIZE_X = canvas.offsetWidth;
const CANVAS_SIZE_Y = canvas.offsetHeight; 

canvas.width = CANVAS_SIZE_X;
canvas.height = CANVAS_SIZE_Y;

let painting = false;
let filling = false;

ctx.strokeStyle = "#2c2c2c";
ctx.lineWidth = 2.5;
ctx.fillStyle = "2c2c2c";

function onMouseMove(event) {
    const X = event.offsetX;
    const Y = event.offsetY;

    if(!painting) {
        ctx.beginPath();
        ctx.moveTo(X, Y);
    } else {
        ctx.lineTo(X, Y);
        ctx.stroke();
    }
}

function startPainting(event) {
    painting = true
}

function stopPainting() {
    painting = false;
}

function handleClickColor(event) {
    Array.from(colors).forEach(
        color => color.classList.remove("active")
    );
    
    const color = event.target.style.backgroundColor;
    ctx.strokeStyle = color;
    ctx.fillStyle = color;
    event.target.classList.add("active");
}

function handleRangeChange(event) {
    const rangeSize = event.target.value;
    ctx.lineWidth = rangeSize;
}

function handleModeChange() {
    if(filling === false) {
        filling = true;
        mode.innerText = "PAINT";
    } else {
        filling = false;
        mode.innerText = "FILL";
    }
}

function handleClickCanvas() {
    if(filling) {
        ctx.fillRect(0, 0, CANVAS_SIZE_X,  CANVAS_SIZE_Y);
    }
}

function handleCT(event) {
    event.preventDefault();
}

function handleClickSave() {
    const url = canvas.toDataURL();
    const image = document.createElement("a");
    image.href = url;
    image.download = "paintJS[🎨]";
    image.click();
}

if(canvas) {
    canvas.addEventListener("mousemove", onMouseMove);
    canvas.addEventListener("mousedown", startPainting);
    canvas.addEventListener("mouseup", stopPainting);
    canvas.addEventListener("mouseleave", stopPainting);
    canvas.addEventListener("click", handleClickCanvas);
    canvas.addEventListener("contextmenu", handleCT);
}

Array.from(colors).forEach(
    color => color.addEventListener("click", handleClickColor)
);

if(range) {
    range.addEventListener("input", handleRangeChange);
}

if(mode) {
    mode.addEventListener("click", handleModeChange);
}

if(saveBtn) {
    saveBtn.addEventListener("click", handleClickSave);
}

 


 

https://eee0930.github.io/paintjs 

 

PaintJS

Fill Save Clear

eee0930.github.io

 

'프로젝트 > Side Project' 카테고리의 다른 글

[JS] Analog Clock  (0) 2023.03.24
[JS] JavaScript Drum Kit  (0) 2023.03.24
[JS] Paint App 1 - style  (0) 2023.02.14
[JS] Momentum App 6 - Style  (0) 2023.02.13
[JS] Momentum App 5 - Weather  (0) 2023.02.13
Comments