ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Fun with HTML5 Canvas 🎨
    Web/JS30 2020. 11. 1. 18:40
    반응형

    HTML5의 기능인 Canvas를 다뤄보는 예제이다.

    솔직히 처음 배웠을 때는 어디다 쓰나 했는데, 많은 분들이 Canvas로 재미있는 것들을 하시더라.

    생각보다 활용도가 높을수도 있을 것 같다는 생각이 든다.

     

    1. HTML Canvas element 위에다가 직접 무언가를 그릴 순 없다. 

    무언가를 그리기 위해서는 Canvas 위에 'Context'를 만들어서, 그 위에다가 그림을 그려야 한다.

    <canvas id="draw" width="800" height="800"></canvas>
    const canvas = document.getElementById('draw');
    const ctx = canvas.getContext('2d');
    // context는 2d도, 3d도 가능하다.

     

    2. 기본 세팅 코드

    // 브라우저의 크기만큼 가로, 세로를 설정
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    
    // 색 지정
    ctx.strokeStyle = '#BADA55';
    
    // 선이 다른 선과 만나는 부분을 둥글게 할건지, 각지게 할건지
    ctx.lineJoin = 'round';
    
    // 선이 끝나는 부분을 둥글게 할건지, 각지게 할건지
    ctx.lineCap = 'round'; 
    
    // 마우스를 누르고 있는지 아닌지를 나타내는 flag
    let isDrawing = false;
    
    // 선은 항상 시작점과 끝점이 필요하다. 걔네들을 나타낼 변수.
    let lastX = 0;
    let lastY = 0;

     

    3. 그림을 그리는 역할을 하는 draw 함수

    function draw(e) {
        if(!isDrawing) return; // 마우스가 클릭된 상태가 아닐 때는 함수를 실행시키지 않음.
    
        ctx.beginPath();
        // start from
        ctx.moveTo(lastX, lastY);
        // go to
        ctx.lineTo(e.offsetX, e.offsetY);
        // 화면에 표시해라!
        ctx.stroke();
    }

    라인을 그리기 위해서는 ctx.beginPath()로, 경로를 열어줘야 한다.

    일단 ctx.moveTo()로 점을 옮겨주고 ctx.lineTo()로 선을 그어주면 되는데,

    여기서 주의. 둘을 구별할 수 있어야 한다.

     

    moveTo()는 말 그대로 점을 '옮겨주는' 역할이다.

    여기서는 처음에 점을 (lastX, lastY)로 옮겨줬기 때문에 라인의 시작점이 (lastX, lastY)가 된다.

     

    그리고 lineTo(e.offsetX, e.offsetY)를 해주게 되면, 라인의 시작점인 (lastX, lastY)로부터 현재 마우스의 위치인 (e.offsetX, e.offsetY)까지 라인을 그리라는 뜻이 된다.

     

    참고로, 마지막에 ctx.stroke()를 해주지 않으면 그린 선이 화면에 표시되지 않으니 주의할 것.

     

    4. 이벤트 리스너 붙여주기

    canvas.addEventListener('mousemove', draw);
    canvas.addEventListener('mousedown', () => isDrawing = true);
    canvas.addEventListener('mouseup', () => isDrawing = false);
    // 마우스가 화면을 벗어나면 중단
    canvas.addEventListener('mouseout', () => isDrawing = false); 

    마우스가 이동할 때마다 draw 함수를 실행시켜준다.

    그리고, 마우스가 눌러지면 isDrawing을 true로, 마우스를 떼면 isDrawing을 false로 만들어준다.

     

    이제 Canvas에 마우스로 선을 그려보자.

    그런데, 뭔가 이상하다. 라인의 시작점이 전부 (0, 0)이다.

    선을 그릴때마다 lastX와 lastY를 갱신해주지 않았기 때문

    시작점이 맨 처음에 설정한 (0, 0)으로 고정되어있어서 저런 모양이 나타나는 것.

     

    5. 개선된 코드

    function draw(e) {
      if(!isDrawing) return; 
    
      ctx.beginPath();
      ctx.moveTo(lastX, lastY);
      ctx.lineTo(e.offsetX, e.offsetY);
      ctx.stroke();
    
      [lastX, lastY] = [e.offsetX, e.offsetY];
    }

    선을 그리고 마지막에 lastX와 lastY를 옮겨줬다.

    이렇게 하면 시작점이 계속해서 이동하면서 부드러운 선이 그려지게 될 것이다.

    하지만 여전히 문제가 있다.

    맨 처음라인의 시작점은 여전히 (0, 0)이며,

    마우스를 뗐다가 다시 클릭할 경우, 이전에 마우스를 뗐던 지점에서 선이 연결된다.

     

    이를 해결하기 위해서는 mousedown 이벤트의 콜백함수를 고쳐줘야 한다.

    캔버스에 그릴 때를 생각해보면, 무조건 mousedown이 mousemove보다 먼저 일어나게 되어있다.

    (일단 마우스를 클릭해야 그림이 시작되니까)

     

    그래서 mousemove를 하기 전에 mousedown 단계에서, 현재 마우스의 위치로 X, Y좌표를 옮겨주면 된다.

    그러면 이전과 이어지지 않고, 현재 마우스의 위치에서 새로 선을 시작할 수 있다.

    canvas.addEventListener('mousedown', (e) => {
        isDrawing = true;
    
        // 현재 마우스의 위치로 lastX, lastY를 옮겨준다.
        [lastX, lastY] = [e.offsetX, e.offsetY];
    });

    6. 라인의 색을 지정해주기 위해선 ctx.strokeStyle을 고쳐주면 된다.

    HSL 컬러를 사용해서, 라인이 그려질 때마다 Hue 값을 1씩 더해주도록 draw함수를 고쳐보자.

    let hue = 0;
    
    function draw(e) {
      if(!isDrawing) return; 
    
      // HSL = Hue, Saturation, Lightness
      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++;
      if(hue >= 360) {
        hue = 0;
      }
    }

    마지막에 hue를 조작하는 부분이 추가되었다.

    hue값은 0 ~ 360의 범위를 가지기 때문에, 360을 넘는 순간 0으로 다시 초기화해줬다.

    7. 선의 굵기는 ctx.lineWidth로 조절해주면 된다.

    라인의 굵기가 1부터 100까지 천천히 증가하다가, 100이 되는 순간 다시 100부터 1까지 천천히 감소하도록 만들어보자.

    let direction = true;
    
    function draw(e) {
      if(!isDrawing) return; 
    
      // HSL = Hue, Saturation, Lightness
      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++;
      if(hue >= 360) {
        hue = 0;
      }
    
      if(ctx.lineWidth >= 100 || ctx.lineWidth <= 1) {
        direction = !direction;
      }
    
      if(direction) {
        ctx.lineWidth++;
      } else {
        ctx.lineWidth--;
      }
    }

    direction 이라는 변수를 새로 만들어서 direction이 true일 때는 lineWidth가 1씩 증가하도록 하고

    direction이 false일 때는 lineWidth가 1씩 감소하도록 만들어 주었다.

    참고로, 선이 겹쳐지는 부분의 색이 더해지는 모습을 볼 수 있는데,

    이는 'GlobalCompositeOperation'을 이용한 것이다.

    색이 더해지는 것 말고도 점점 더 밝아지거나 색끼리 빼는 등의 여러 연산이 가능하니

    공식 문서를 참고해보도록 하자.

    ctx.globalCompositeOperation = 'multiply';

     

     

    8. 참고로, lineCap과 lineJoin을 round로 해주지 않으면 다음과 같은 현상이 벌어지니, 주의하자.

    라인은 여러 개의 점으로 이루어지기 때문에 점이 계속해서 찍히는 모습인 것 같은데,

    lineWidth를 100으로 했기 때문에 100만큼 길이의 선이 계속해서 찍히게 되어 이런 형태가 된 것 같다.

    부드러운 선을 그리기 위해서는 반드시 lineJoinlineCap을 'round'로 해줘야 한다는 사실을 잊지 말자.

     

    9. 전체 코드

    const canvas = document.getElementById('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 = 100;
    ctx.globalCompositeOperation = 'multiply';
    
    let isDrawing = false;
    let lastX = 0;
    let lastY = 0;
    let hue = 0;
    let direction = true;
    
    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 = e.offsetX;
        // lastY = e.offsetY;
        [lastX, lastY] = [e.offsetX, e.offsetY];
    
        hue++;
        if(hue >= 360) {
            hue = 0;
        }
    
        if(ctx.lineWidth >= 100 || ctx.lineWidth <= 1) {
            direction = !direction;
        }
    
        if(direction) {
            ctx.lineWidth++;
        } else {
            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); 
    반응형

    'Web > JS30' 카테고리의 다른 글

    Hold Shift to Check Multiple Checkboxes 📦  (0) 2020.11.09
    14 Must Know Dev Tools Tricks 💪  (0) 2020.11.04
    Array Cardio Day 2 🎈  (0) 2020.10.30
    Type Ahead ⌨  (0) 2020.09.25
    Flex Panel Gallery 🎫  (0) 2020.09.21

    댓글

Designed by Tistory.