• [JS30] HTML Canvas

    2019. 2. 15.

    by. 나나 (nykim)


    Javascript 30의 Day 8입니다. 오늘은 Canvas를 다뤄볼 차례입니다(두근두근)
    예전에 잠깐 캔버스에 대해 네이버 포스팅을 한 적이 있긴 한데, 저도 다 까먹었네요 허허허. 일부 내용은 그대로 가져왔습니다.
    아무튼 시작해보죠!╰(✧∇✧╰)


    1. 캔버스 만들기

    아묻따 캔버스를 html에 마크업합니다.

    <canvas id="draw" width="800" height="800"></canvas>

    canvas요소는 id값과 width, height 속성을 갖습니다. 저 너비와 높이값은 선택사항이며, 기본값은 300*150이라고 합니다. IE9 이하 브라우저라면 대체 컨텐츠가 필요하며, 닫는 태그를 잊지 않도록 합시다.

    자, 캔버스에 그림을 그리려면 일단 자바스크립트를 써야합니다. 캔버스 요소 아래에 스크립트를 작성합니다.

    <canvas id="draw" width="800" height="800">
    <script>
      const canvas = document.querySelector("#draw");
      const ctx = canvas.getContext("2d");
    </script>

    canva에 id값을 넣어준 이유는 이렇게 스크립트에서 캔버스를 참조해야 하기 때문입니다.
    한편, 저 ctx는 context를 뜻합니다. 캔버스에 그림을 그릴 때, getContext메서드를 호출해 캔버스의 컨텍스트를 가져와 사용해야합니다.

    컨텍스트는 캔버스의 그리기 영역이면서 그리기 메서드를 가지는 객체인데요, 인자로 원하는 컨텍스트의 종류를 전달합니다. 여기선 2d를 썼는데, 그럼 다른 d도 있을까요? 넵, 3d가 있긴 한데 여기선 투디를 써보죠. 이때 대문자 2D가 아닌 것에 주의!
    이제 getContext("2d")를 통해 CanvasRenderingContext2D 타입의 객체를 가지고 그리기 메서드를 사용할 수 있게 됩니다. (*참고)

    마지막으로 캔버스 크기를 윈도 크기와 동일하게 맞추고 다음으로 넘어갑시다.

    const canvas = document.querySelector("#draw");
    const ctx = canvas.getContext("2d");
    
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;


    2. 캔버스에 그리는 방법

    본격적으로 코드 작성에 앞서, 캔버스의 그리기 메서드를 몇 개 알아봅시다.
    아래와 같이 작성하면 뭐가 일어날까요?

    ctx.fillStyle = "#cde937";
    ctx.fillRect(10, 10, 60, 60)

    띠용 뭔가 사각형이 생겼어요! 캔버스는 기본적으로 직사각형을 제공하고, 다른 도형을 만드려면 path가 필요합니다. 우선, 직사각형을 만들 때는 다음 세 가지 함수를 쓸 수 있습니다.

    • fillRect (x, y, width, height)

      색칠된 직사각형을 그립니다.

    • strokeRect (x, y, width, height)

      직사각형 윤곽선을 그립니다.

    • clearRect (x, y, width, height)

      특정 부분을 지운 직사각형을 그립니다.

    여기서 x와 y는 캔버스 좌측 상단 꼭지점에서의 상대적 위치를 가리키며, width와 height는 사각형의 크기입니다. 그럼 위 예제에서 ctx.fillRect(10,10,60,60)은 10px만큼 떨어진 곳에 60px짜리 정사각형을 그리라는 뜻이었군요 :D

    이번엔 사각형이 아니라 선을 그려봅니다.

    • beginPath()

      새로운 path를 만듭니다.

    • closePath()

      path를 닫습니다.

    • stroke()

      윤곽선을 이어 도형을 그립니다.

    • fill()

      path 내부를 채웁니다.

    • moveTo(x, y)

      펜을 지정된 x, y 좌표로 옮깁니다.

    • lineTo(x, y)

      현재의 드로잉 위치에서 x와 y로 설정된 좌표점까지 연결하는 선을 그립니다.

    path는 기본적으로 path를 열고 - path 커맨드로 그리고 - path를 닫는 방식으로 사용합니다.
    따라서 가장 먼저 beginPath()를 사용하여 새 도형을 그릴 준비를 합니다. beginPath() 메서드를 사용하고 나서 쓰는 첫 path 커맨드는 무조건 moveTo()로 여겨지기 때문에, path를 리셋하면 시작 위치를 명확히 설정하는 것이 좋습니다. 그러고 나서 path와 관련된 커맨드를 써서 스스슥 그림을 그리고, 그 다음 path를 닫으면 됩니다.

    ctx.beginPath(); //시-작
    ctx.moveTo(30, 90);
    ctx.lineTo(60, 40);
    ctx.lineTo(90, 90);
    ctx.lineTo(40, 90);
    ctx.fill(); //채우기!

    이번에는 삼각형을 그려 본 모습입니다.
    beginPath()로 시작해서, moveTo와 lineTo 커맨드로 그림을 그리고, fill()로 도형을 채웠습니다. 참고로, fill() 메서드 사용 시 열린 도형은 자동으로 닫히므로 closePath()가 필요 없습니다. (단, stroke()에는 필요해요!)


    3. 캔버스에 그리기 함수 작성

    본격적으로 하려던 걸 시작해봅시다. 우선 선 스타일을 지정해줍니다.

    ctx.strokeStyle = "#BADA55";
    ctx.lineJoin = "round";
    ctx.lineCap = "round";
    ctx.lineWidth = 10;

    strokeStyle은 도형의 윤곽색 색을 설정합니다. 또 lineJoin 속성은 선이 연결되는 지점의 모양을, lineCap선의 끝 부분 모양을 결정합니다. 여기선 round를 써서 둥글게 만들었는데, 다른 속성이 궁금하다면 MDN을 참고하시면 됩니다.

    let isDrawing = false;
    function draw(){}

    저 isDrawing은 flag역할을 할 변수입니다. 캔버스에 그리는 때는, 그냥 마우스를 움직이는 게 아니라 캔버스 위에서 마우스를 꾹 누른 채로 움직이는 순간이잖아요? 그걸 판단하기 위한 변수입니다. 사용자가 마우스를 누르고 있으면(mouse down) 값은 true가 되고, 사용자가 마우스를 떼면(mouse up) 값은 false가 되겠죠

    이어서 draw 함수를 작성하고 canvas에 이벤트를 걸어줍니다.

    let isDrawing = false;
    
    function draw(e){
      if (!isDrawing) return;
      console.log(e);
    }
    
    canvas.addEventListener("mousemove", draw);
    canvas.addEventListener('mousedown', () => isDrawing = true);
    canvas.addEventListener("mouseup", () => isDrawing = false);
    canvas.addEventListener("mouseout", () => isDrawing = false);

    아까 말했듯이, 마우스를 떼거나 화면 밖으로 나갔을 때는 flag인 isDrawing을 false로 바꿔줬습니다. mousedown일 때는 true로 바꿔줬구요. 또 마우스가 움직이는 동안 그림이 그려져야 하므로 mousemove에도 draw 함수를 이벤트리스너로 걸어줬습니다.
    이제 마우스를 꾹 누른채로 이동하면 콘솔에 MouseEvent에 대한 정보가 촤라라라 찍혀 나옵니다.

    여기서 유의깊에 볼 것은 e.offsetX와 e.offsetY입니다. 우리는 마우스 이벤트가 일어난 이 지점에다 그림을 그려야하니까요 :>
    함수를 마저 작성합니다.

    function draw(e) {
      if (!isDrawing) return;
      console.log(e);
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.lineTo(e.offsetX, e.offsetY);
      ctx.stroke();
    }

    우리가 아까 살펴본 메서드들이네요! beginPath()는 새로운 패스를 시작할 때 쓰고, moveTo(x,y)는 해당 지점으로 펜을 옮길 때, lineTo(x,y)는 현재 위치에서 주어진 x,y까지 연결하는 선을 그릴 때, stroke()는 선을 이어 그릴 때 사용하는 거였죠.

    해당 웹문서를 열어서 코드가 잘 작동하는지 확인해 봅시다.

    오! 뭔가 그러져요!! 이상하지만... 그건 아마도 그릴 때마다 movTo가 0,0으로 설정되어 있어서 그런 것 같습니다.
    자, 지금 시점에서 캔버스는 어떻게 그림을 그리고 있을까요? 제가 (120, 120) 지점에서 마우스를 (121, 121) 지점으로 옮겼다고 합시다. 그럼 (0,0) ~ (120,120) 까지 잇는 선이 쫙 그려집니다. 그대로 마우스를 (121,122) 지점으로 옮기면 (0,0)~(121,122)까지 잇는 선이 또 쫙 그려지겠죠.
    그럼 우리가 원하는 형태는 어떻게 그려져야 할까요? 제가 (50, 50) 지점에서 마우스를 (51, 51) 지점으로 옮기면, (50,50)~(51,51)을 잇는 선이 그려져야하지 않을까요? 즉, 기존의 (0,0)은 사용자가 방금 마우스를 움직인 지점이 되어야 합니다.

    그럼 사용자가 방금 마우스를 움직인 지점을 lastX, lastY라는 변수에 넣어 수정해봅시다.

    let isDrawing = false;
    let lastX = 0;
    let lastY = 0;
    
    function draw(e) {
      if (!isDrawing) return;
      console.log(e);
      ctx.beginPath();
      ctx.moveTo(lastX, lastY);
      ctx.lineTo(e.offsetX, e.offsetY);
      ctx.stroke();
          lastX = e.offsetX;
          lastY = e.offsetY;
    }

    단, 아직은 초기 lastX, lastY가 계속 0이므로 이걸 업데이트해야 합니다. mousedown에 걸린 이벤트리스너를 살짝 손봐줍니다.

    canvas.addEventListener('mousedown', (e) => {
      isDrawing = true;
      lastX = e.offsetX;
      lastY = e.offsetY;
    });

    참고로 ES6 문법에서는 아래처럼 작성하는 것도 가능합니다.

    canvas.addEventListener('mousedown', (e) => {
    isDrawing = true;
    [lastX, lastY] = [e.offsetX, e.offsetY];
    });

    4. 선 꾸미기

    이제까지 작성했으면 캔버스에 원하는 대로 그림이 그려집니다(짝짝짝) 이제 여기서 더 나아가, 선 색이 그라디언트로 바뀌게끔 해봅시다.
    여기서는 hsl 색 코드를 쓸 건데요, hsl은 각각 hue, saturation, lightness를 뜻합니다. hue 컬러 휠이라고 볼 수 있으며, 0부터 360까지 들어갑니다. (*참고)
    그럼 이 hue값을 마우스 지점이 바뀔 때마다 1씩 늘어나게 하면 자연스러운 그라디언트가 나오지 않을까요?

    let hue = 0; //변수 선언
    
    function draw(e) {
      if (!isDrawing) return;
      ctx.strokeStyle = `hsl(${hue}, 100%, 50%)`; //색변경
      ctx.beginPath();
      ctx.moveTo(lastX, lastY);
      ctx.lineTo(e.offsetX, e.offsetY);
      ctx.stroke();
      [lastX, lastY] = [e.offsetX, e.offsetY];
    
      hue > 360 ? hue = 0 : hue ++; //hue값 늘리기
    }

    hue값은 0~360이니까, 360을 넘으면 0이 되게끔 설정하는 것도 잊지 맙시다.
    다만 이렇게 한 경우, 처음에는 항상 빨간색이 나오고, 마우스를 뗐다가 그리면 조금 전 색에 이어서 나옵니다.이걸 좀더 나이내믹하게(?!) 바꾸려면 랜덤으로 색을 지정해주면 되겠죠!

    function randomColor() {
      return Math.floor(Math.random() * 361);
    }
    
    let hue = randomColor();
      }

    이번에는 선의 두께도 유동적으로 설정해보죠. 두께가 점점 늘었다가 점점 줄어야하므로, ++과 --둘다 있어야 합니다. 이것 역시 direction이라는 flag 변수를 만들어 설정하겠습니다.

    let direction = true;
    
    function draw(e) {
      ... (상략) ...
    
      if (ctx.lineWidth > 100 || ctx.lineWidth < 10) {
        direction = !direction;
      }
      if (direction) { //ture면 점점 굵어지게
        ctx.lineWidth++;
      } else { //flase면 점점 가늘어지게
        ctx.lineWidth--;
      }
    }

    direction이 ture라면 두께를 점점 굵어지게, false라면 점점 가늘어지게 할 예정입니다. 이때 두께가 너무 굵어지거나(100을 넘을 때), 두께가 너무 가늘어지거나(10 아래 일 때) 하는 둘 중 하나의 상황일때(||) 이 flag 변수를 반전시켜주는 것입니다.
    좀 더 풀어서 설명하면, 두께가 99에서 100이 되면 direction 변수는 true였지만 false가 되고, 따라서 이 시점부터 점점 가늘어집니다. 두께가 11에서 10이 되면 direction 변수는 false였지만 true가 되고, 따라서 이 시점부터 점점 굵어지는 거죠.


    5. 최종 코드

    최종 코드는 아래와 같습니다.

    <canvas id="draw" width="800" height="800"></canvas>
    
    <script>
      const canvas = document.querySelector("#draw");
      const ctx = canvas.getContext("2d");
    
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
    
      ctx.strokeStyle = "#BADA55";
      ctx.lineJoin = "round";
      ctx.lineCap = "round";
      ctx.lineWidth = 10;
    
      let isDrawing = false;
      let lastX = 0;
      let lastY = 0;
      let direction = true;
    
      function randomColor() {
        return Math.floor(Math.random() * 361);
      }
    
      let hue = randomColor();
    
      function draw(e) {
        if (!isDrawing) return;
        // console.log(e);
        ctx.strokeStyle = `hsl(${hue}, 100%, 50%)`;
        ctx.beginPath();
        ctx.moveTo(lastX, lastY);
        ctx.lineTo(e.offsetX, e.offsetY);
        ctx.stroke();
        [lastX, lastY] = [e.offsetX, e.offsetY];
        hue > 360 ? hue = 0 : hue++; //hue값 늘리기
        if (ctx.lineWidth > 100 || ctx.lineWidth < 10) {
          direction = !direction;
        }
        if (direction) { //ture면 점점 굵어지게
          ctx.lineWidth++;
        } else { //flase면 점점 가늘어지게
          ctx.lineWidth--;
        }
        console.log(ctx.lineWidth);
      }
      canvas.addEventListener("mousemove", draw);
      canvas.addEventListener('mousedown', (e) => {
        isDrawing = true;
        [lastX, lastY] = [e.offsetX, e.offsetY];
      });
      canvas.addEventListener("mouseup", () => isDrawing = false);
      canvas.addEventListener("mouseout", () => isDrawing = false);
    </script>

    See the Pen GzYqyE by NY KIM (@nykim_) on CodePen.


    'Study > JavaScript' 카테고리의 다른 글

    [JS30] Hold Shift and Check Checkboxes  (0) 2019.02.27
    [JS30] Dev Tools Tricks  (0) 2019.02.17
    [JS30] HTML Canvas  (0) 2019.02.15
    [JS30] Array 메서드(2)  (0) 2019.02.11
    [JS30] Ajax Type Ahead (Ajax 타이핑)  (1) 2019.02.06
    [JS30] Array 메서드 (1)  (0) 2019.01.07

    댓글 0