Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

노마드코더 JS로 그림앱 만들기) #3 MEME MAKER - file URL, Image(), dblclick, download, transition 본문

Javascript/노마드코더 - JS로 그림앱 만들기

노마드코더 JS로 그림앱 만들기) #3 MEME MAKER - file URL, Image(), dblclick, download, transition

끈기JK 2022. 10. 11. 16:43

-그림 파일 추가

accept="image/*" : 이미지 파일만 허용한다. video 등도 가능하며 .pdf 등으로 확장자 제한을 걸 수도 있다.

multiple 속성 추가시 여러 파일을 등록할 수 있다.

// index.html 추가
<input type="file" accept="image/*" id="file" />

const fileInput = document.getElementById("file");

function onFileChange(event) {
    console.dir(event.target);
}

fileInput.addEventListener("change", onFileChange);

콘솔 확인은 다음과 같다.

 

브라우저의 자바스크립트는 유저의 파일을 읽을 수 없다. 유저가 선택했을 때 메모리에 있게 된다. 그러면 브라우저가 파일을 활용할 수 있다. 메모리에 있는 파일을 URL을 쳐서 브라우저에서 볼수 있다.

아래처럼 수정하면 콘솔에서 url 확인 가능하다.

event.target.files[0] : 파일이 한 개라 0이다. multiple로 여러 파일 올리면 배열 숫자로 여러 파일 가져올 수 있다.

function onFileChange(event) {
    const file = event.target.files[0];
    const url = URL.createObjectURL(file);  // 브라우저 안에서만 접근 가능한 URL
    console.log(url);
}

 

const image = new Image() : <img> 태그 입력하는 것과 같다.

image.src : <img src=""> 와 같다.

image.onload : 이미지가 로딩 됐을 때 함수 실행

ctx.drawImage(image, 시작x좌표, 시작y좌표, 너비, 높이)

// app.js 추가
const fileInput = document.getElementById("file");

function onFileChange(event) {
    const file = event.target.files[0];
    const url = URL.createObjectURL(file);
    const image = new Image();
    image.src = url;
    image.onload = function () {
        ctx.drawImage(image, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    };
    fileInput = null;
}

fileInput.addEventListener("change", onFileChange);

 


 

-텍스트 추가하기

텍스트 입력 공간 추가

// index.html
    <input type="text" placeholder="Write and then double click" id="text" />

더블클릭시 콘솔에 위치 정보가 표시된다.

function onDoubleClick(event) {
    console.log(event.offsetX, event.offsetY);
}

canvas.addEventListener("dblclick", onDoubleClick);

ctx.lineCap = "round" : 선의 끝 부분을 둥글게 처리한다.

ctx.save() : 현재 context 상태를 저장한다.

ctx.restore() : 저장된 context 상태를 불러온다.

ctx.font = 크기, 폰트를 지정한다.

ctx.fillText : text를 입력된 좌표에 나타낸다. 글자 내부를 채운 글씨가 나타난다. stroke로 하면 내부가 빈 채로 나타난다.

// app.js 추가
const textInput = document.getElementById("text");

ctx.lineCap = "round";

function onDoubleClick(event) {
    ctx.save();
    if (text !== "") {
        const text = textInput.value;
        ctx.lineWidth = 1;
        ctx.font = "68px serif";
        ctx.fillText(text, event.offsetX, event.offsetY);
        ctx.restore();
    }
}

canvas.addEventListener("dblclick", onDoubleClick);

 


 

-그림 저장

// index.html 추가
<button id="save">Save Image</button>

canvas.toDataURL() : 캔버스의 이미지를 URL로 변환하는 메서드. 그림을 문자로 바꿔준다.

const saveBtn = document.getElementById("save");

function onSaveClick() {
    console.log(canvas.toDataURL());
}

saveBtn.addEventListener("click", onSaveClick);

document.createElement("a") : <a> 태그 입력과 같다.

a.href : <a href="">과 같다.

a.download : <a download="myDrawing.png">와 같다. 입력시 다운로드 서비스를 제공한다.

a.click() : 링크를 클릭하는 함수

const saveBtn = document.getElementById("save");

function onSaveClick() {
    const url = canvas.toDataURL();
    const a = document.createElement("a");
    a.href = url;
    a.download = "myDrawing.png";
    a.click();
}

saveBtn.addEventListener("click", onSaveClick);

 


 

■ CSS 적용

Reset 관련사이트 : https://meyerweb.com/eric/tools/css/reset/

 

<label for="">에 다른 태그의 id를 입력하면 해당 id의 역할을 할 수 있다. <label> 태그로 감싸지 않아도 작동된다.

        <label for="file">
            Add Photo
            <input type="file" accept="image/*" id="file" />
        </label>

 

 

-전체 코드

<!-- 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">
    <title>Meme Maker</title>
    <link rel="stylesheet" href="style.css" />
</head>

<body>
    <div class="color-options">
        <input type="color" name="" id="color" />
        <div class="color-option" style="background-color: #1abc9c;" data-color="#1abc9c"></div>
        <div class="color-option" style="background-color: #2ecc71;" data-color="#2ecc71"></div>
        <div class="color-option" style="background-color: #3498db;" data-color="#3498db"></div>
        <div class="color-option" style="background-color: #9b59b6;" data-color="#9b59b6"></div>
        <div class="color-option" style="background-color: #34495e;" data-color="#34495e"></div>
        <div class="color-option" style="background-color: #f1c40f;" data-color="#f1c40f"></div>
        <div class="color-option" style="background-color: #e67e22;" data-color="#e67e22"></div>
        <div class="color-option" style="background-color: #ecf0f1;" data-color="#ecf0f1"></div>
        <div class="color-option" style="background-color: #95a5a6;" data-color="#95a5a6"></div>
    </div>
    <canvas></canvas>
    <div class="btns">
        <input id="line-width" type="range" min="1" max="10" value="5" step="0.1" />
        <button id="mode-btn">💧Fill</button>
        <button id="destroy-btn">🚽Destory</button>
        <button id="erase-btn">❌Erase</button>
        <label for="file">
            📂Add Photo
            <input type="file" accept="image/*" id="file" />
        </label>
        <input type="text" id="text" placeholder="Add text here... :)" />
        <button id="save">💾Save Image</button>
    </div>
    <script src="app.js"></script>

</body>

</html>

*gap : 행과 열 사이 간격 설정

*transition 단축 문법 : <property> <duration> <timing-function> <delay>

transition-property : 트랜지션을 적용해야 하는 CSS 속성의 이름 혹은 이름들을 명시
transition-duration : 트랜지션이 일어나는 지속 시간
transition-timing-function : 속성의 중간값을 계산하는 방법을 정의하는 함수
transition-delay : 속성이 변한 시점과 트랜지션이 실제로 시작하는 사이에 기다리는 시간

*all: unset : 모든 CSS 속성 제거

*padding 순서 : 시계방향. 1개(전체) 2개(상하, 우좌), 3개(상, 우좌, 하), 4개(상, 우, 하, 좌)

/* style.css */
@import "reset.css";

body {
    display: flex;
    gap: 20px;
    justify-content: space-between;
    align-items: flex-start;
    background-color: gainsboro;
    padding: 20px;
}

canvas {
    width: 800px;
    height: 800px;
    background-color: white;
    border-radius: 10px;
}

body {
    display: flex;
    justify-content: center;
    align-items: center;
}

.btns {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.color-options {
    display: flex;
    flex-direction: column;
    gap: 20px;
}

.color-option {
    width: 50px;
    height: 50px;
    border-radius: 50%;
    cursor: pointer;
    border: 5px solid white;
    transition: transform ease-in-out .1s;
}

.color-option:hover {
    transform: scale(1.2);
}

input#color {
    background-color: white;
}

button,
label {
    all: unset;
    padding: 10px 0px;
    text-align: center;
    background-color: royalblue;
    color: white;
    font-weight: 500;
    cursor: pointer;
    border-radius: 10px;
    transition: opacity linear .1s;
}

button:hover {
    opacity: 0.85;
}

input#text {
    all: unset;
    padding: 10px 0px;
    border-radius: 10px;

}

input#file {
    display: none;
}
// app.js
const saveBtn = document.getElementById("save");
const textInput = document.getElementById("text");
const fileInput = document.getElementById("file");
const eraseBtn = document.getElementById("erase-btn");
const destoryBtn = document.getElementById("destroy-btn");
const modeBtn = document.getElementById("mode-btn");
const colorOptions = Array.from(document.getElementsByClassName("color-option"));
const color = document.getElementById("color");
const lineWidth = document.getElementById("line-width");
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 800;

canvas.width = 800;
canvas.height = 800;
ctx.lineCap = "round";
ctx.lineWidth = lineWidth.value;
let isPainting = false;
let isFilling = false;

function onMove(event) {
    if (isPainting) {
        ctx.lineTo(event.offsetX, event.offsetY);
        ctx.stroke();
        return;
    }
    ctx.beginPath();
    ctx.moveTo(event.offsetX, event.offsetY);
}
function startPainting() {
    isPainting = true;
}
function cancelPainting() {
    isPainting = false;
}

function onLineWidthChange(event) {
    ctx.lineWidth = event.target.value;
}

function onColorChange(event) {
    ctx.strokeStyle = event.target.value;
    ctx.fillStyle = event.target.value;
}

function onColorClick(event) {
    const colorValue = event.target.dataset.color;
    ctx.strokeStyle = colorValue;
    ctx.fillStyle = colorValue;
    color.value = colorValue;
}

function onModeClick() {
    if (isFilling) {
        isFilling = false;
        modeBtn.innerText = "Fill";
    } else {
        isFilling = true;
        modeBtn.innerText = "Draw";
    }
}

function onCanvasClick() {
    if (isFilling) {
        ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    }
}

function onDestroyClick() {
    const confirm = window.confirm("Are you sure destroy all image?");
    if (confirm) {
        ctx.fillStyle = "white";
        ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
    }
}

function onEraserClick() {
    ctx.strokeStyle = "white";
    isFilling = false;
    modeBtn.innerText = "Fill";
}

function onFileChange(event) {
    const file = event.target.files[0];
    const url = URL.createObjectURL(file);
    const image = new Image();
    image.src = url;
    image.onload = function () {
        ctx.drawImage(image, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    };
    fileInput = null;
}

function onDoubleClick(event) {
    ctx.save();
    if (text !== "") {
        const text = textInput.value;
        ctx.lineWidth = 1;
        ctx.font = "68px serif";
        ctx.fillText(text, event.offsetX, event.offsetY);
        ctx.restore();
    }
}

function onSaveClick() {
    const url = canvas.toDataURL();
    const a = document.createElement("a");
    a.href = url;
    a.download = "myDrawing.png";
    a.click();
}

canvas.addEventListener("dblclick", onDoubleClick);
canvas.addEventListener("mousemove", onMove);
canvas.addEventListener("mousedown", startPainting);
canvas.addEventListener("mouseup", cancelPainting);
canvas.addEventListener("mouseleave", cancelPainting);
canvas.addEventListener("click", onCanvasClick);

lineWidth.addEventListener("change", onLineWidthChange);
color.addEventListener("change", onColorChange);

colorOptions.forEach(color => color.addEventListener("click", onColorClick));

modeBtn.addEventListener("click", onModeClick);
destoryBtn.addEventListener("click", onDestroyClick);
eraseBtn.addEventListener("click", onEraserClick);
fileInput.addEventListener("change", onFileChange);
saveBtn.addEventListener("click", onSaveClick);

 


 

챌린지1. 텍스트 크기, 폰트 종류 변경 만들기

 

챌린지2. 텍스트를 fill, stroke 중 선택

 

챌린지3. 선을 그려서 안을 채우는 옵션 선택(안함)

 

 

최종 코드

// 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">
    <title>Meme Maker</title>
    <link rel="stylesheet" href="style.css" />
</head>

<body>
    <div class="color-options">
        <div class="color-option" style="background-color: #1abc9c;" data-color="#1abc9c"></div>
        <div class="color-option" style="background-color: #2ecc71;" data-color="#2ecc71"></div>
        <div class="color-option" style="background-color: #3498db;" data-color="#3498db"></div>
        <div class="color-option" style="background-color: #9b59b6;" data-color="#9b59b6"></div>
        <div class="color-option" style="background-color: #34495e;" data-color="#34495e"></div>
        <div class="color-option" style="background-color: #f1c40f;" data-color="#f1c40f"></div>
        <div class="color-option" style="background-color: #e67e22;" data-color="#e67e22"></div>
        <div class="color-option" style="background-color: #ecf0f1;" data-color="#ecf0f1"></div>
        <div class="color-option" style="background-color: #95a5a6;" data-color="#95a5a6"></div>
    </div>
    <canvas></canvas>
    <div id="btns">
        <input type="range" id="line-width" min="1" max="20" value="10" step="1">
        <input type="color" name="" id="color" />
        <button id="mode-btn">Fill</button>
        <button id="destroy-btn">Destroy</button>
        <button id="erase-btn">Erase</button>
        <label for="file">
            Add photo
            <input type="file" accept="image/*" id="file" />
        </label>

        <!-- font size 버튼들 div로 묶음. 5개중 1개 선택하기 위해 radio 방식으로 변경. radio 버튼 hidden 처리, label로 감싸고 for로 라디오 버튼 지정 -->
        <div class="font-sizes">
            <label for="font-size-1" class="font-size-label" style="font-size: 15px">1
                <input type="radio" name="font-size" id="font-size-1" class="font-size" data-fontsize="15px"
                    hidden /></label>
            <label for="font-size-2" class="font-size-label" style="font-size: 25px">2
                <input type="radio" name="font-size" id="font-size-2" class="font-size" data-fontsize="25px"
                    hidden /></label>
            <label for="font-size-3" class="font-size-label" style="font-size: 35px">3
                <input type="radio" name="font-size" id="font-size-3" class="font-size" data-fontsize="35px" hidden
                    checked /></label>
            <label for="font-size-4" class="font-size-label" style="font-size: 45px">4
                <input type="radio" name="font-size" id="font-size-4" class="font-size" data-fontsize="45px"
                    hidden /></label>
            <label for="font-size-5" class="font-size-label" style="font-size: 55px">5
                <input type="radio" name="font-size" id="font-size-5" class="font-size" data-fontsize="55px"
                    hidden /></label>
        </div>

        <!-- font-family, fill-stroke 버튼들 div로 묶음. 위와 동일-->
        <div class="font-family-fill-stroke">
            <label for="font-family-1" class="font-family-label" style="font-family: serif;">serif
                <input type="radio" name="font-family" id="font-family-1" class="font-family" data-fontfamily="serif"
                    hidden checked /></label>
            <label for="font-family-2" class="font-family-label"
                style="font-family: sans-serif; margin-right: 20px">sans-serif
                <input type="radio" name="font-family" id="font-family-2" class="font-family"
                    data-fontfamily="sans-serif" hidden /></label>

            <label for="fill-stroke-1" class="fill-stroke-label">Fill
                <input type="radio" name="fill-stroke" id="fill-stroke-1" class="fill-stroke" data-fillstroke="fill"
                    hidden /></label>
            <label for="fill-stroke-2" class="fill-stroke-label">Stroke
                <input type="radio" name="fill-stroke" id="fill-stroke-2" class="fill-stroke" data-fillstroke="stroke"
                    hidden /></label>
        </div>

        <input type="text" placeholder="Add text.." id="text" style="background-color: white;" />
        <button id="save">Save Image</button>
    </div>
    <script src="app.js"></script>
</body>

</html>
// style.css
@import "reset.css";

canvas {
    width: 600px;
    height: 600px;
    background-color: white;
    border-radius: 35px;
}

body {
    display: flex;
    gap: 20px;
    justify-content: center;
    align-items: center;
    background-color: darkkhaki;
    padding: 20px;
}

.color-option {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    cursor: pointer;
    border: 2px solid white;
    transition: transform ease-in-out .1s;
}

.color-options {
    display: flex;
    flex-direction: column;
    gap: 10px
}

.color-option:hover {
    transform: scale(1.3);
}

input#color {
    background-color: white;
}

button,
label {
    all: unset;
    padding: 10px 2px;
    text-align: center;
    background-color: deeppink;
    color: white;
    font-weight: 700;
    cursor: pointer;
    border-radius: 7px;
    transition: opacity linear .1s;
}

button:hover {
    opacity: 0.65;
}


#btns {
    display: flex;
    flex-direction: column;
    gap: 5px
}

input#text {
    all: unset;
    padding: 10px 0px;
    border-radius: 5px;
}

input#file {
    display: none;
}

.font-sizes {
    display: flex;
    justify-content: space-between;
    gap: 5px;
}

.font-size-label {
    width: 40px;
    height: 30px;
    text-align: center;
    font-weight: 500;
    font-family: serif;

}

.font-size:hover {
    opacity: 1;
}

.font-family-fill-stroke {
    display: flex;
    justify-content: space-between;
}

.font-size-label {
    display: flex;
    align-items: center;
    justify-content: center;
}

/* 선택시 색상 효과 주기 위한 스타일 */
.font-size-label-sel,
.font-family-label-sel,
.fill-stroke-label-sel {
    background-color: cornflowerblue;
}
// app.js
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const lineWidth = document.getElementById("line-width");
const color = document.getElementById("color");
const colorOptions = Array.from(document.getElementsByClassName("color-option"));
const modeBtn = document.getElementById("mode-btn");
const destroyBtn = document.getElementById("destroy-btn");
const eraseBtn = document.getElementById("erase-btn")
const fileInput = document.getElementById("file");
const textInput = document.getElementById("text");
const saveBtn = document.getElementById("save");
const fontSizes = Array.from(document.getElementsByClassName("font-size"));
const fontFamilies = Array.from(document.getElementsByClassName("font-family"))
const fillStrokes = Array.from(document.getElementsByClassName("fill-stroke"));

canvas.width = 600;
canvas.height = 600;
ctx.lineWidth = lineWidth.value;
ctx.lineCap = "round";
let isPainting = false;
let isFilling = false;

// 초기값 지정
let currentFontSize = "35px";
let currentFontFamily = "serif";
let currentFillStroke = "fill"

const CANVAS_WIDTH = 600;
const CANVAS_HEIGHT = 600;

// 초기 fontSize 3 색 지정
fontSizes.forEach(fontSizeId => {
    const fontSizeIdParent = fontSizeId.parentElement;
    if (fontSizeId.checked === true) {
        fontSizeIdParent.classList.add("font-size-label-sel");
    } else {
        fontSizeIdParent.classList.remove("font-size-label-sel");
    }
});

// 초기 fontFamily 색 지정
fontFamilies[0].parentElement.classList.add("font-family-label-sel");

// 초기 fill 지정
fillStrokes[0].parentElement.classList.add("fill-stroke-label-sel")

/* 함수 */
function onMove(event) {
    if (isPainting) {
        ctx.lineTo(event.offsetX, event.offsetY);
        ctx.stroke();
        return;
    }
    ctx.beginPath();
    ctx.moveTo(event.offsetX, event.offsetY);
}

function startPainting() {
    isPainting = true;
}

function cancelPainting() {
    isPainting = false;
}

function onLineWidthChange(event) {
    ctx.lineWidth = event.target.value;
}

function onColorChange(event) {
    ctx.storkeStyle = event.target.value;
    ctx.fillStyle = event.target.value;
}

function onColorClick(event) {
    const colorValue = event.target.dataset.color;
    ctx.strokeStyle = colorValue;
    ctx.fillStyle = colorValue;
    color.value = colorValue;
}

function onModeClick() {
    if (isFilling) {
        isFilling = false;
        modeBtn.innerText = "Fill";
    } else {
        isFilling = true;
        modeBtn.innerText = "Draw";
    }
}

function onCanvasClick() {
    if (isFilling) {
        ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    }
}

function onDestroyClick() {
    ctx.fillStyle = "white";
    ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
}

function onEraserClick() {
    ctx.strokeStyle = "white";
    isFilling = false;
    modeBtn.innerText = "Fill";
}

function onFileChange(event) {
    const file = event.target.files[0];
    const url = URL.createObjectURL(file);
    const image = new Image();
    image.src = url;
    image.onload = function () {
        ctx.drawImage(image, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    };
    fileInput = null;
}

function onDoubleClick(event) {
    ctx.save();
    if (text !== "") {
        ctx.lineWidth = 1;
        const text = textInput.value;
        ctx.font = currentFontSize + " " + currentFontFamily;  // 폰트 사이즈, 폰트 패밀리 현재 값 지정
        if (currentFillStroke === "fill") {  // fill, stroke 선택에 따른 차이
            ctx.fillText(text, event.offsetX, event.offsetY);
        } else {
            ctx.strokeText(text, event.offsetX, event.offsetY);
        }
    }
    ctx.restore();
}

function onSaveClick() {
    const url = canvas.toDataURL();
    const a = document.createElement("a");
    a.href = url;
    a.download = "myDrawing.png";
    a.click();
}

// 폰트 사이즈 클릭 선택
function onFontsizeClick(event) {
    const fontSizeValue = event.target.dataset.fontsize;
    ctx.font = fontSizeValue + " " + currentFontFamily;
    currentFontSize = fontSizeValue;
    const fontSizeId = document.getElementById(event.target.id);

    // 색상 변경
    fontSizes.forEach(fontSizeId => {
        const fontSizeIdParent = fontSizeId.parentElement;
        if (fontSizeId.checked === true) {
            fontSizeIdParent.classList.add("font-size-label-sel");
        } else {
            fontSizeIdParent.classList.remove("font-size-label-sel");
        }
    });
}

// 폰트 패밀리 클릭 선택
function onFontFamilyClick(event) {
    const fontFamily = event.target.dataset.fontfamily;
    ctx.font = currentFontSize + " " + fontFamily;
    currentFontFamily = fontFamily;
    const fontFamilyId = document.getElementById(event.target.id);

    // 버튼 색상 지정
    fontFamilies.forEach(fontFamilyId => {
        const fontFamilyIdParent = fontFamilyId.parentElement;
        if (fontFamilyId.checked === true) {
            fontFamilyIdParent.classList.add("font-family-label-sel");
        } else {
            fontFamilyIdParent.classList.remove("font-family-label-sel");
        }
    });
}

// fill, stroke 클릭 선택
function onFillStroke(event) {
    const fillStroke = event.target.dataset.fillstroke;
    const fillStrokeId = document.getElementById(event.target.id);

    if (fillStroke === "fill") {
        currentFillStroke = "fill";
    } else {
        currentFillStroke = "stroke";
    }

    // 버튼 색상 지정
    fillStrokes.forEach(fillStrokeId => {
        const fillStrokeIdParent = fillStrokeId.parentElement;
        if (fillStrokeId.checked == true) {
            fillStrokeIdParent.classList.add("fill-stroke-label-sel");
        } else {
            fillStrokeIdParent.classList.remove("fill-stroke-label-sel");
        }
    })
}

canvas.addEventListener("mousemove", onMove);
canvas.addEventListener("mousedown", startPainting);
canvas.addEventListener("mouseup", cancelPainting);
canvas.addEventListener("mouseleave", cancelPainting);
canvas.addEventListener("click", onCanvasClick);
canvas.addEventListener("dblclick", onDoubleClick);

lineWidth.addEventListener("change", onLineWidthChange);
color.addEventListener("change", onColorChange);
colorOptions.forEach(color => color.addEventListener("click", onColorClick));
modeBtn.addEventListener("click", onModeClick);
destroyBtn.addEventListener("click", onDestroyClick);
eraseBtn.addEventListener("click", onEraserClick);
fileInput.addEventListener("change", onFileChange);
saveBtn.addEventListener("click", onSaveClick);

// 폰트 사이즈, 폰트 패밀리, fill, stroke 이벤트 리스너
fontSizes.forEach(fontsize => fontsize.addEventListener("click", onFontsizeClick));
fontFamilies.forEach(fontfamily => fontfamily.addEventListener("click", onFontFamilyClick));
fillStrokes.forEach(fillStroke => fillStroke.addEventListener("click", onFillStroke));