벌써 마지막을 향해 달려가고 있는 그림판 클론코딩입니다.

생각보다 크게 어렵지 않기도 하고, 크롬 앱 클론코딩을 통해 VanillaJS에 조금이나마 익숙해져 있으시다면 금방 쫒아갈 수 있는 수업이라고 생각합니다. 

이번 포스팅은 강의의 #2.4~#2.6까지의 내용을 담고 있습니다.

 

목차


    0. 페인트통 기능

    배경을 모두 다른 색으로 바꾸고 싶은데 페인트통 기능이 없다면 제일 두꺼운 브러시로 모두 칠해야 하는 불상사가 발생합니다. 이런 불상사가 일어나지 않게 하기 위해서는 베인트통 기능이 반드시 필요하겠죠?

     

    먼저 이전 포스팅에서 미리 설계했던 Fill버튼 / Paint버튼의 구현이 필요합니다.

    이 부분은 filling이라는 새로운 변수를 하나 만들어서, if문을 통해 filling 모드인지 아닌지 파악하고 그에 따라 버튼의 문구를 Fill, Paint로 바꾸는 작업입니다. 코드를 보시면 굉장히 간단하게 구현할 수 있습니다.

     

    그 다음엔 INITIAL_COLOR이라는 변수를 하나 만들었는데, 굳이 만들지 않아도 상관이 없습니다.

    이제 eventListener를 통해 클릭하면 fillRect를 이용하여 캔버스를 꽉 채워버리는 작업을 하도록 만들어줍니다.

    대신 이 작업을 수행할 때는 filling 변수가 true일때만 수행되게 하는 것이죠.

    false 상태라면 클릭을 하면 아까 설정한 대로 onMouseMove 함수에 의해 Paint작업이 수행됩니다.

     

    // app.js
    const canvas = document.getElementById("jsCanvas"); // id를 가져올때는 getElementById!! Class를 가져올 때는 get
    const ctx = canvas.getContext("2d") // context 불러오기 (mdn 참고)
    const colors = document.getElementsByClassName("jsColor") // jsColors라는 Class를 가진 모든 요소를 가져온다!
    const range = document.getElementById("jsRange");
    const mode = document.getElementById("jsMode")
    
    const INITIAL_COLOR = "black"; // 아래에서 strokeStyle, fillStyle이 중복되므로, 이렇게 변수 하나를 만들어서 값을 할당한다.
    // const CANVAS_SIZE = canvas.offsetWidth; // 어짜피 가로세로 길이 같으니까 가로만 가져온다!
    // 굳이 한번에 CANVAS_SIZE로 안하고 offsetWidth, offsetHeight를 바로 불러와도 된다.
    
    // canvas는 2개의 사이즈를 가져야한다. 우리가 보고 있는 CSS size와 pixel manipulating size를 알아야한다
    // index에서 생성한 canvas는 css로부터 size 정보를 받아오는데, js는 이것을 알 수 없다.
    // 그러므로 여기서 따로 
    // 아래는 pixel을 관리하기 위해서 알아야하는 size를 canvas에 제공한 것이다.
    canvas.width = canvas.offsetWidth; // 강의에서는 700, 700을 넣었지만 실제로는 이렇게 해야 관리가 편하다!
    canvas.height = canvas.offsetHeight;
    
    ctx.strokeStyle = INITIAL_COLOR; // 사용하려는 사람이 검정색으로 시작하도록 설정 - 추후 변경하면 그 색으로 선이 그어질 것!!
    ctx.fillStyle = INITIAL_COLOR;
    ctx.lineWidth = "2.5"; // 아까 만들었던 range를 이용해 선의 굵기를 결정, 초기값 2.5px!
    
    
    
    let painting = false; // 클릭중인지 여부를 나타내는 변수
    let filling = false; // 채우기 상태 여부를 나타내는 변수
    
    
    function stopPainting(){
        painting = false;
    }
    
    function startPainting(){
        painting = true;
    }
    
    function onMouseMove(event){ // 마우스가 캔버스 위에 있는지를 나타내는 함수
        // 여기서 관심있는 부분은 캔버스 내 위치인 offset에 대한 부분!
        // client X Y는 윈도우 전체에서 마우스의 위치
        const x = event.offsetX;
        const y = event.offsetY;
        if(!painting){ // painting(클릭) 상태가 아니라면
            ctx.beginPath(); // path를 만든다 (path를 현 위치로 초기화한다)
            ctx.moveTo(x, y); // path를 x,y로 옮긴다 - 클릭하면 그 path의 최종 지점이 x와 y로 남는다 (beginPath가 있기에 지워도 상관이 없다)
        } else { // painting(클릭) 상태라면
            // CanvasRenderingContext2D.lineTo()는 현재 sub-path의 마지막 점을 특정 좌표와 '직선'으로 연결한다
            ctx.lineTo(x, y); // 실시간으로 계속해서 클릭한 상태로 이동한 좌표를 따라가서 path를 만든다
            // 6,6에서 클릭한 상태로 마우스를 8,8로 가져갔다면 path는 (6,6)~(7,7), (7,7)~(8,8) 이런 식으로 만들어진다.
            ctx.stroke(); // 그렇게 생성된 path를 이어서 선을 만든다. 즉 매우 작은 직선들이 우리 눈에는 부드럽게 보이는 것이다!
        }
    }
    /* 이 부분도 startPainting()으로 대체
    function onMouseDown(event){ // 캔버스 안을 클릭했을 때를 나타내는 함수
        painting = true; // 누르면 true
    }
    */
    
    /* 우리가 만들 로직은 onMouseDown에 들어가면 되기 때문에 이 부분도 생략 후 stopPainting으로 대체
    function onMouseUp(event)
        stopPainting(); // 떼면 false - 다른 문장이 필요하므로 stopPainting()을 가져와서 여기서 사용한다!
    }
    */
    
    /*
    이 부분은 stopPainting()을 만들면서 만들지 않을 수 있다!
    function onMouseLeave(event){
        painting = false;
    }
    */
    
    function handleColorClick(event){ // 색상표를 클릭했을 때 어떤 반응이 나오게 할 것인지 만드는 함수
        // console.log(event.target.style) 찍어보면 우리가 원하는 것은 backgroundColor
        const color = event.target.style.backgroundColor;
        ctx.strokeStyle = color; // strokeStyle을 override! 여기서부터는 클릭한 color로 선이 그려진다!
        ctx.fillStyle = color; // fill모드에서도 똑같이 색이 변할 수 있도록 만든다!
    }
    
    function handleRangeChange(event){ // 조정할 때마다 그에 맞춰서 lineWidth를 변경하는 함수
        const size = event.target.value; // 항상 어떤 값인지 console.log()를 찍어보고 찾아보자!
        ctx.lineWidth = size;
    }
    
    function handleModeClick(){ // Fill 버튼이 클릭이 되었을 때 함수
        if(filling === true){ // fill 상태이면
            filling = false; // filling을 false로 만들기
            mode.innerText = "Fill"; // 버튼 글씨 Fill로 만들기 (Fill 떠있으면 Paint모드)
        } else {
            filling = true; // filling을 true로 만들기
            mode.innerText = "Paint"; // 버튼 글씨 Paint로 만들기 (Paint 떠있으면 Fill모드)
        }
    } 
    
    function handleCanvasClick(){ // Fill 상태일 때 클릭하면 사각형 만드는 함수
        if(filling){ // fill 상태면
            ctx.fillRect(0, 0, canvas.width, canvas.height); // fillRect(시작점x, 시작점y, 너비, 높이) - 사각형 생성
        }
    }
    
    if(canvas){ // 사실상 init와 같은 역할이다 - eventListener들은 한번 작동하면 계속 작동!
        canvas.addEventListener("mousemove", onMouseMove); // 캔버스 위 움직임 감지
        canvas.addEventListener("mousedown", startPainting); // 클릭했을 때 감지
        canvas.addEventListener("mouseup", stopPainting); // 클릭을 멈췄을 때 감지
        canvas.addEventListener("mouseleave", stopPainting); // 클릭하다가 캔버스를 벗어났을 때 감지
        canvas.addEventListener("click", handleCanvasClick); // 캔버스를 클릭했을 때 감지
    }
    
    // Array.from 메소드는 object로부터 array를 만든다!
    // array를 주면 그 array 안에서 forEach로 color를 가질 수 있다!
    // colors의 각각에다가 이벤트리스너를 실행하도록 한다
    Array.from(colors).forEach(color => color.addEventListener("click", handleColorClick));
    
    if(range){ // getElementById로 잘 받아왔는지 확인
        range.addEventListener("input", handleRangeChange); // input에 반응해야 하기 때문!
    }
    
    if(mode){
        mode.addEventListener("click", handleModeClick);
    }

     


    1. 저장!!

    이번에는 canvas에 내장되어있는 download와 save기능을 활용하여 이미지를 저장하는 기능을 만들어보겠습니다.

     

    먼저 한 가지 버그를 고치고 넘어가겠습니다.

    우리가 내장기능을 사용하여 저장할 때, Fill을 한 번도 하지 않은 상태라면 배경이 흰색이 아닌 투명하게 저장이 됩니다. 왜냐하면 우리가 기본 배경색을 설정하지 않았기 때문이죠. 그래서 기본 색을 설정하기 전에 시작되자마자 흰색으로 배경을 채울 수 있도록 만들어줍니다.

     

    아래 코드를 ctx를 설정하는 곳의 맨 위쪽에 삽입합니다!

    // 맨 처음에는 배경색이 없어서 저장하면 투명한 배경으로 나온다.
    // 이걸 방지하기 위해 실행하자마자 흰색으로 캔버스를 칠해준다.
    ctx.fillStyle = "white";
    ctx.fillRect(0, 0, canvas.width, canvas.height);

     

    그 다음에는 오른쪽 단추를 눌러 저장하는 기능을 없애기 위해 eventListener를 사용해봅시다.

    우클릭을 했을 때는 contextmenu입니다. 그리고 함수인 handleCM을 만들어서 event.preventDefault()를 통해 우클릭 기본 설정을 막아줍시다. 그러면 캔버스 위에서는 우클릭이 되지 않습니다! (비슷하게 window에서 우클릭을 막을 수도 있습니다!)

     

    이제 save 버튼을 눌렀을 때의 eventListener를 설정합니다.

    handleSaveClick 함수를 만들고, toDataURL을 이용해 canvas의 이미지를 URL로 변환해주는 기능을 사용합니다.

    이 URL을 받아서 link라는 a태그요소에 넣고, href를 이용해 하이퍼링크를 걸어주고 download를 이용해 다운로드 받았을 때의 이름을 설정해줍니다. 그리고 click을 통해 클릭한 효과를 내주면 끝입니다.

    // app.js
    const canvas = document.getElementById("jsCanvas"); // id를 가져올때는 getElementById!! Class를 가져올 때는 get
    const ctx = canvas.getContext("2d") // context 불러오기 (mdn 참고)
    const colors = document.getElementsByClassName("jsColor") // jsColors라는 Class를 가진 모든 요소를 가져온다!
    const range = document.getElementById("jsRange");
    const mode = document.getElementById("jsMode");
    const saveBtn = document.getElementById("jsSave");
    
    const INITIAL_COLOR = "black"; // 아래에서 strokeStyle, fillStyle이 중복되므로, 이렇게 변수 하나를 만들어서 값을 할당한다.
    // const CANVAS_SIZE = canvas.offsetWidth; // 어짜피 가로세로 길이 같으니까 가로만 가져온다!
    // 굳이 한번에 CANVAS_SIZE로 안하고 offsetWidth, offsetHeight를 바로 불러와도 된다.
    
    // canvas는 2개의 사이즈를 가져야한다. 우리가 보고 있는 CSS size와 pixel manipulating size를 알아야한다
    // index에서 생성한 canvas는 css로부터 size 정보를 받아오는데, js는 이것을 알 수 없다.
    // 그러므로 여기서 따로 
    // 아래는 pixel을 관리하기 위해서 알아야하는 size를 canvas에 제공한 것이다.
    canvas.width = canvas.offsetWidth; // 강의에서는 700, 700을 넣었지만 실제로는 이렇게 해야 관리가 편하다!
    canvas.height = canvas.offsetHeight;
    
    // 맨 처음에는 배경색이 없어서 저장하면 투명한 배경으로 나온다.
    // 이걸 방지하기 위해 실행하자마자 흰색으로 캔버스를 칠해준다.
    ctx.fillStyle = "white";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    ctx.strokeStyle = INITIAL_COLOR; // 사용하려는 사람이 검정색으로 시작하도록 설정 - 추후 변경하면 그 색으로 선이 그어질 것!!
    ctx.fillStyle = INITIAL_COLOR;
    ctx.lineWidth = "2.5"; // 아까 만들었던 range를 이용해 선의 굵기를 결정, 초기값 2.5px!
    
    
    
    let painting = false; // 클릭중인지 여부를 나타내는 변수
    let filling = false; // 채우기 상태 여부를 나타내는 변수
    
    
    function stopPainting(){
        painting = false;
    }
    
    function startPainting(){
        painting = true;
    }
    
    function onMouseMove(event){ // 마우스가 캔버스 위에 있는지를 나타내는 함수
        // 여기서 관심있는 부분은 캔버스 내 위치인 offset에 대한 부분!
        // client X Y는 윈도우 전체에서 마우스의 위치
        const x = event.offsetX;
        const y = event.offsetY;
        if(!painting){ // painting(클릭) 상태가 아니라면
            ctx.beginPath(); // path를 만든다 (path를 현 위치로 초기화한다)
            ctx.moveTo(x, y); // path를 x,y로 옮긴다 - 클릭하면 그 path의 최종 지점이 x와 y로 남는다 (beginPath가 있기에 지워도 상관이 없다)
        } else { // painting(클릭) 상태라면
            // CanvasRenderingContext2D.lineTo()는 현재 sub-path의 마지막 점을 특정 좌표와 '직선'으로 연결한다
            ctx.lineTo(x, y); // 실시간으로 계속해서 클릭한 상태로 이동한 좌표를 따라가서 path를 만든다
            // 6,6에서 클릭한 상태로 마우스를 8,8로 가져갔다면 path는 (6,6)~(7,7), (7,7)~(8,8) 이런 식으로 만들어진다.
            ctx.stroke(); // 그렇게 생성된 path를 이어서 선을 만든다. 즉 매우 작은 직선들이 우리 눈에는 부드럽게 보이는 것이다!
        }
    }
    /* 이 부분도 startPainting()으로 대체
    function onMouseDown(event){ // 캔버스 안을 클릭했을 때를 나타내는 함수
        painting = true; // 누르면 true
    }
    */
    
    /* 우리가 만들 로직은 onMouseDown에 들어가면 되기 때문에 이 부분도 생략 후 stopPainting으로 대체
    function onMouseUp(event)
        stopPainting(); // 떼면 false - 다른 문장이 필요하므로 stopPainting()을 가져와서 여기서 사용한다!
    }
    */
    
    /*
    이 부분은 stopPainting()을 만들면서 만들지 않을 수 있다!
    function onMouseLeave(event){
        painting = false;
    }
    */
    
    function handleColorClick(event){ // 색상표를 클릭했을 때 어떤 반응이 나오게 할 것인지 만드는 함수
        // console.log(event.target.style) 찍어보면 우리가 원하는 것은 backgroundColor
        const color = event.target.style.backgroundColor;
        ctx.strokeStyle = color; // strokeStyle을 override! 여기서부터는 클릭한 color로 선이 그려진다!
        ctx.fillStyle = color; // fill모드에서도 똑같이 색이 변할 수 있도록 만든다!
    }
    
    function handleRangeChange(event){ // 조정할 때마다 그에 맞춰서 lineWidth를 변경하는 함수
        const size = event.target.value; // 항상 어떤 값인지 console.log()를 찍어보고 찾아보자!
        ctx.lineWidth = size;
    }
    
    function handleModeClick(){ // Fill 버튼이 클릭이 되었을 때 함수
        if(filling === true){ // fill 상태이면
            filling = false; // filling을 false로 만들기
            mode.innerText = "Fill"; // 버튼 글씨 Fill로 만들기 (Fill 떠있으면 Paint모드)
        } else {
            filling = true; // filling을 true로 만들기
            mode.innerText = "Paint"; // 버튼 글씨 Paint로 만들기 (Paint 떠있으면 Fill모드)
        }
    } 
    
    function handleCanvasClick(){ // Fill 상태일 때 클릭하면 사각형 만드는 함수
        if(filling){ // fill 상태면
            ctx.fillRect(0, 0, canvas.width, canvas.height); // fillRect(시작점x, 시작점y, 너비, 높이) - 사각형 생성
        }
    }
    
    function handleCM(event){ // 우클릭을 방지하는 함수!
        event.preventDefault(); // 우클릭 기본 설정 방지!
    }
    
    function handleSaveClick(){ // 세이브 버튼을 눌렀을 때 동작하는 함수!
        // canvas의 데이터를 image처럼 얻어야 한다!
        const image = canvas.toDataURL(); // "image/jpeg"를 매개변수로 넣으면 jpeg타입의 이미지를 URL로 변환 (기본 png)
        const link = document.createElement("a"); // 존재하지 않는 임의의 링크 생성
        link.href = image; // href를 통해 url 연결
        link.download = "PaintJS[🎨]"; // download를 받을 때의 파일 이름을 설정
        link.click(); // link를 클릭한 것과 같은 효과를 내기 위해서
    }
    
    if(canvas){ // 사실상 init와 같은 역할이다 - eventListener들은 한번 작동하면 계속 작동!
        canvas.addEventListener("mousemove", onMouseMove); // 캔버스 위 움직임 감지
        canvas.addEventListener("mousedown", startPainting); // 클릭했을 때 감지
        canvas.addEventListener("mouseup", stopPainting); // 클릭을 멈췄을 때 감지
        canvas.addEventListener("mouseleave", stopPainting); // 클릭하다가 캔버스를 벗어났을 때 감지
        canvas.addEventListener("click", handleCanvasClick); // 캔버스를 클릭했을 때 감지
        canvas.addEventListener("contextmenu", handleCM);
    }
    
    // Array.from 메소드는 object로부터 array를 만든다!
    // array를 주면 그 array 안에서 forEach로 color를 가질 수 있다!
    // colors의 각각에다가 이벤트리스너를 실행하도록 한다
    Array.from(colors).forEach(color => color.addEventListener("click", handleColorClick));
    
    if(range){ // getElementById로 잘 받아왔는지 확인
        range.addEventListener("input", handleRangeChange); // input에 반응해야 하기 때문!
    }
    
    if(mode){
        mode.addEventListener("click", handleModeClick); // Fill 버튼이 클릭이 되었을 때
    }
    
    if(saveBtn){ // 
        saveBtn.addEventListener("click", handleSaveClick); // Save 버튼이 클릭이 되었을 때
    }

    다운로드까지 잘 작동합니다!

     


    이것으로 강의를 모두 수강했습니다! 아쉬운 부분들이 몇 가지 있지만 (지우개 기능, 현재 선택한 색깔을 나타내는 기능, fill에서 특정 부분만 채우기, 마우스 클릭한 채로 나갔다 들어와도 그려지게 하기 등등) 이 부분들은 저 혼자서 공부해 나가면서 개선해 볼 생각입니다!

     

    혹시나 이해가 잘 되지 않으시는 부분이 있거나 오류가 있다면 댓글로 남겨주시면 최대한 빠르게 피드백하도록 하겠습니다!! 감사합니다 :)

     

    p.s. 본 클론코딩 자료는 제 github에 모두 올려놓았습니다 :)

     

     

    alittlekitten/NC_paintJS

    Painting Board made with VanillaJS. Contribute to alittlekitten/NC_paintJS development by creating an account on GitHub.

    github.com

     

    반응형
    • 네이버 블로그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기