글모음

[웹개발] Canvas API 다루면서 생긴 문제들 - globalCompositeOperation, resizing 본문

프로그래밍 공부/기타

[웹개발] Canvas API 다루면서 생긴 문제들 - globalCompositeOperation, resizing

Nova_61 2023. 3. 6. 20:03
728x90
반응형

현재 React drop-zone과 Canvas API로 이미지를 받아오는 페이지를 작업중이다.

React drop-zone으로 이미지 파일을 받고, Canvas를 통해 사용자가 이미지를 수정하도록 작동하려고 하는데 생각처럼 돌아가지 않았다.

 

[ Canvas를 다루는 중에 생긴 문제점들 ]

1. 빈 Canvas 생성 : Canvas 위에서 drawing을 하고 난 뒤에, 이미지를 다시 첨부해도 Canvas 위에 이미지가 제대로 올라가지 않음

2. 마우스 커서가 맞지 않음 : Canvas의 크기를 변경시에 Canvas 위에서 마우스 커서보다 더 위에 drawing이 됨

3. Canvas의 이미지 비율이 깨짐

 

[ 1. 빈 캔버스 생성 ] - globalCompositeOperation 속성

빈 캔버스가 계속 생성되는 문제는 비교적 간단했다. 

Canvas에 그림을 다시 불러오는 과정에서 globalCompositeOperation 속성이 비어있는 화면으로 나오게된것.

globalCompositeOperation 속성은 Canvas에 그림을 그릴 때, 새로운 그림을 기존 그림 위에 덮을 때 적용되는 그리기 규칙을 정의한다. destination-out 규칙은 기존 그림과 겹치는 부분을 지우는 효과를 줄 수 있다. 새로 그린 그림과 기존 그림이 겹치는 부분에서, 새로 그린 그림이 지워지는 효과를 준다. 즉, destination-out 규칙을 적용하면 기존 그림 위에 있는 부분을 지우고, 투명한 배경을 만들 수 있다.

 

      context.globalCompositeOperation = "source-over;

이 라인 하나만으로 문제를 해결했다.

 

Canvas에 새로 그림을 그리기 전에(drawImage 부분) context.globalCompositeOperation 속성에 "source-over" 값을 설정해서 Canvas를 초기화해준다.

  const handleImageResetClick = () => {
    const canvas = canvasRef.current;
    const context = canvas!.getContext("2d");
    const img = new Image();
    img.onload = () => {
      if (canvas && context) {
        context.globalCompositeOperation = "source-over"; // 초기화 추가
        context.drawImage(img, 0, 0, canvas.width, canvas.height);
        setIsEditing(false);
      }
    };
    img.src = image!.src;
  };

 

source-over 말고 Canvas에 기존의 그림과 새로운 그림에 대한 여러가지 속성값들이 있다.

globalCompositeOperation 속성값들 : 

  1. source-over: 기본값으로, 새로운 요소를 캔버스에 그릴 때 이전에 그려진 요소 위에 덮여집니다.
  2. source-in: 새로운 요소와 이전에 그려진 요소 중 겹치는 부분만 보이고, 나머지는 투명해집니다.
  3. source-out: 새로운 요소와 이전에 그려진 요소 중 겹치지 않는 부분만 보이고, 나머지는 투명해집니다.
  4. source-atop: 새로운 요소는 이전에 그려진 요소 위에 그려지지만, 겹치지 않는 부분은 새로운 요소의 색상을 사용하고, 겹치는 부분은 이전에 그려진 요소의 색상을 사용합니다.
  5. destination-over: 새로운 요소를 이전에 그려진 요소 아래에 그립니다.
  6. destination-in: 이전에 그려진 요소와 겹치는 부분만 보이고, 나머지는 투명해집니다.
  7. destination-out: 이전에 그려진 요소와 겹치지 않는 부분만 보이고, 나머지는 투명해집니다.
  8. destination-atop: 이전에 그려진 요소는 새로운 요소 위에 그려지지만, 겹치지 않는 부분은 이전에 그려진 요소의 색상을 사용하고, 겹치는 부분은 새로운 요소의 색상을 사용합니다.
  9. lighter: 새로운 요소의 색상과 이전에 그려진 요소의 색상을 합산하여 보여줍니다.
  10. darker: 새로운 요소의 색상과 이전에 그려진 요소의 색상 중 더 어두운 색상을 보여줍니다.
  11. copy: 새로운 요소를 그리면 기존의 캔버스 내용은 모두 삭제됩니다.
  12. xor: 새로운 요소와 이전에 그려진 요소 중 겹치지 않는 부분만 보이고, 겹치는 부분은 투명해집니다.

이러한 globalCompositeOperation 속성값을 조합하여 다양한 시각 효과를 만들 수 있다.

 

[ Canvas란? ]

우선, Canvas는 픽셀 단위로 그림을 그리는 HTML5 엘리먼트이기 때문에 Canvas의 width와 height를 %식으로 고정시켜도 Canvas의 내부는 처음 랜더링 된 상태 그대로 고정된다.

Canvas의 기본 크기는 300x150 픽셀이고, 이미지를 넣게 되면 이 사이즈로 그대로 랜더링 된다.

 

 

(왼) : Canvas 위의 이미지 (오) 원본 이미지

만약 canvas 요소의 너비와 높이를 100%로 지정하면, 부모 요소의 크기에 따라 자동으로 조정된다. 부모 요소의 크기가 변경될 때마다 canvas를 다시 그리지 않으면, canvas 내부의 내용이 늘어난 혹은 줄어든 크기에 맞게 자동으로 조정되지 않으므로, 화면에 깨진 그림이 나타나게 되는것. 상상과는 다르게 처참하게 깨져버렸다...

Canvas가 커지게 되니까, drawing 되는것도 부분도 덩달아서 커져버리고, 마우스 움직이는것과 drawing 되는 부분이 전혀 일치하지 않았다.

canvas의 너비와 높이가 100%이고, 처음 랜더링된 canvas의 요소의 너비와 높이가 300 x 150이였다가 사용자가 창 크기를 바꾼다 

-> canvas 안의 내부 내용은 그대로 300 x 150 이므로 픽셀이 깨지고, drawing할때 마우스 포인터가 제대로 맞지 않는 일이 생긴다.

2번과 3번은 Canvas 내부가 반응형으로 동작하지 못해서 생기는 문제였다. 

 

< 해결방법 >

canvas의 크기가 변경될 때마다 내부의 내용을 다시 그려야 하고, 마우스 커서의 위치도 다시 잡아야한다.

비효율적이라도 canvas의 내부 내용은 반응형이 아니기 때문에 어쩔 수 없다.

window의 resize 이벤트를 감지하고, 이벤트 핸들러에서 canvas의 크기를 다시 설정하고 내부의 내용을 다시 그려주는 작업을 수행할 수 있습니다. 또는, React의 useEffect 훅을 사용하여, Canvas의 크기가 변경될 때마다 자동으로 canvas를 다시 그리도록 할 수도 있다.

 

[ 2.  마우스 커서 ]

이 문제는 Canvas 위에서 일어나는 마우스 이벤트의 위치가 실제 위치와 일치하지 않기 때문이다. 이미지를 canvas의 크기에 맞게 조정하지만, canvas에 그리는 작업은 실제로 이미지가 그려지는 곳이 아니기 때문. Canvas 위에 그림을 그리는 마우스 커서도 고정된 크기의 Canvas 내부에서 동작하기 때문에 크기가 달라질 때마다 새로 설정해줘야함.

300 x 150인 Canvas의 크기가 600 x 300으로 변하더라도 마우스 커서의 시작점은 300 x 150일때 그대로이기 때문에 이상하게 그려졌던것... 

 

나같은 경우에는 handleMouseMove라는 함수를 만들어서 Canvas위에 그림을 그리도록 만들었다.

문제가 된 부분은 handleMouseMove에서 clientX와 clientY를 사용하여 마우스의 위치를 계산하는 부분인데,clientX와 clientY는 뷰포트 상의 마우스 위치를 나타내지만, canvas는 뷰포트와 별도의 공간에 존재하므로, 이 위치를 캔버스 상의 위치로 변환해줘야 문제가 해결된다.

 

  const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
    if (!isEditing || !image) return;
    const canvas = canvasRef.current;
    const context = canvas?.getContext("2d");

    if (context) {
      const { left, top } = canvas!.getBoundingClientRect();
      const x = e.clientX - left;
      const y = e.clientY - top;

      context.beginPath();
      context.arc(x, y, 10, 0, Math.PI * 2, false);
      context.fill();
    }
  };

canvas 요소의 제일 왼쪽과 위쪽의 정보를 getBoundingClientRect함수를 통해 canvas 요소의 상대적인 위치를 계산하고, 이를 더하여 mouseEvent 객체의 clientX와 clientY를 캔버스 상의 위치로 변환해서 위치를 맞춰줘야한다.

 

[ 3.  이미지 비율 깨짐 ] - ratio(비율) 계산

Canvas의 크기를 변경해야 한다면, 먼저 새로운 크기를 정하고, Canvas의 너비와 높이 속성을 새로운 값으로 설정한 후에, Canvas에 그림을 다시 그려주어야 한다. 나는 새로 그려주지 않았기때문에 그림이 찌그러지거나 늘어나거나 하여 비율이 맞지 않은 그림이 캔버스 위에 그려진것..

 

방법은 2가지가 있는데,

1. CSS object-fit 속성 사용하기

canvas {
    width: 100%;
    object-fit: contain;
}

이미지가 보여지는 문제는 해결할 수 있어도 마우스 커서 문제는 해결하지 못함.

 

2. Canvas 다시 그리기

캔버스의 크기를 동적으로 변경해야하는 경우에는 반드시 Canvas의 내용을 지우고 다시 그려야한다.

1. 새로운 이미지 상태를 만들어서 이미지 상태를 저장

2. 이미지 비율 상태도 저장 -> 크기가 변경될때마다 계속 이미지의 비율을 계산하는건 비효율적이므로

3. Canvas의 크기가 변경되면 Canvas 위에 이미 그려진 것들을 지움

4. 변경된 Canvas의 비율과 이미지 비율을 계산하는 resize 함수를 만들고, useEffect에 캔버스 크기를 변경하고, 변경된 크기에 맞게 이미지를 다시 그리도록 해야함.

 

 

[ Ratio - 비율 계산 방법 ]

Resize 함수에서는 캔버스의 현재 크기와 이미지의 비율을 고려하여 이미지를 다시 그려주어야 한다.

캔버스의 현재 크기와 이미지의 원래 크기 비율을 계산하고, 작은 쪽에 맞춰서 이미지를 그리는 것이 좋다.

예를 들어, 캔버스의 가로 크기가 800px, 세로 크기가 600px인 경우, 캔버스의 가로 세로 비율은 4:3. 그리고 이미지의 가로 크기가 1000px, 세로 크기가 500px이라면, 이미지의 가로 세로 비율은 2:1이다. 따라서, 이미지를 캔버스 크기에 맞게 그리기 위해서는 이미지의 가로 크기가 캔버스 가로 크기보다 작으므로 800px, 세로 크기를 400px로 스케일링 해야한다.

캔버스의 가로 세로 비율을 계산하고, 이미지의 비율과 비교하여 더 작은 비율에 맞춰 캔버스의 크기를 조정한다.

 

  1. 캔버스의 크기를 얻는다.
  2. 이미지의 크기를 얻는다.
  3. 이미지와 캔버스의 비율을 비교한다. (Width / Height)
  4. 이미지와 캔버스 중 작은 비율을 선택
  5. 선택한 비율을 사용하여 이미지의 새로운 크기를 계산한다.

이렇게 하면 이미지 비율을 유지하면서 캔버스에 꽉 차게 그릴 수 있다.

참고로 Canvas의 가로, 세로가 고정되어 있지 않다면, useEffect로 달라질 때마다 resize한 뒤, canvas를 다시 그리는 함수를 실행시켜줘야한다. 안그러면 당연히 이미지 비율도 깨지고 그릴 때 마우스 커서의 위치도 맞지 않는다.

 

728x90
반응형
Comments