Canvas의 Background와 Flickering에 대한 삽질 기록

2019. 2. 20. 16:15Programming/JavaScript

반응형

캔버스 위에 이미지를 표시해주고, 그 위에 여러개의 박스와 라벨을 그려주는 UI를 구현해야 했다. 사용자가 캔버스 내에 사각형 형태의 영역을 지정할 수 있고, 이 영역의 외부는 어둡게 표시를 해줘야 했다.

Sample
설명하는 것보다는 구현된 내용을 보여주는게 빠를 듯 하다. 간단하게 설명하면 위와 같은 UI를 만들어야했는데, 네모난 영역의 모서리는 이동 및 크기의 변경이 가능했다.

네모난 영역의 외부를 어둡게 표시하는 것 자체는 큰 문제는 아니었다. fillRect를 이용해서 외부의 영역을 네 개의 구역으로 나눠, (0, 0, 0)에 0.5의 알파값을 주고 그려주면 됐다.

context.strokeStyle="rgba(0, 0, 0, 0.5)";
context.fillStyle="rgba(0, 0, 0, 0.5)";

context.fillRect(0, 0, 캔버스의 넓이, 영역의 좌측 상단 y좌표); context.fillRect(0, 영역의 좌측 상단 y좌표, 영역의 좌측 상단 x좌표, 영역의 높이); context.fillRect(영역의 좌측 상단 x좌표+영역의 넓이, 영역의 좌측 상단 y좌표, 영역의 우측 하단 y좌표, 영역의 높이); context.fillRect(0, 영역의 우측 하단 y좌표, 캔버스의 넓이, height-영역의 높이-영역의 좌측 상단 y좌표);

위와같이 영역의 외부에 네 개의 fillRect를 그려줬더니, 잘 동작하는 것처럼 보였다. 하지만 마우스로 영역을 조금 움직이다보니, 외부에 그려진 fillRect들 사이로 1px정도의 틈이 보이는 경우가 있었다. 그래서 고민하다가, 다음의 방법을 생각해냈다.

  1. 받아온 이미지를 캔버스의 BackgroundImage로 설정한다.
  2. 캔버스 사이즈만큼 rgba(0, 0, 0, 0.5)의 fillRect를 그려준다.
  3. clearRect를 이용해서 영역 내부의 fillRect를 지워준다.
  4. 영역을 그려준다.

결과를 보니 꽤 그럴싸했다. 마우스 이벤트를 이용해서 영역의 위치나 크기를 변경하더라도, 영역 외부에 그려진 fillRect에 틈이 생기는 일은 없었다. 그렇게 UI의 세부사항을 구현하고난 뒤에, 다음과 같은 문제가 발생했다. 캔버스에 표시되는 UI는 1초마다 갱신되고 있었는데, Chrome으로 접속할 경우에 Flickering현상이 나타나고 있었다. 세부사항을 구현하는 동안에는 IE로 결과화면을 보고 있어서, 알아차리지 못하고 있던 내용이었다.

여러가지 방법을 찾아보다가 backgroundImage를 포기하고, Image를 생성한 후, onload 이벤트를 이용해서 canvas에 그려주는 방법을 사용하기로 했다. backgroundImage를 변경하는 코드를 제거한 뒤, 캔버스 위에 데이터를 그려주는 로직을 모두 이미지의 onload 내부로 이동시켰다. onload내부에서 이미지를 캔버스에 그리는 시점은, 이미지가 모두 로드된 시점이기 때문에 flickering이 발생하지 않는 듯 했다.

var image = new Image(width, height);
image.onload = function(){
    context.drawImage(this, 0, 0, canvas_width, canvas_height);

    ...
}
image.src = image_url;

clearRect remove image
backgroundImage에 표시되던 이미지를 직접 캔버스에 그리게되면, drawImage의 영향을 받는다. backgroundImage에 이미지를 표시해주거나, clearRect를 사용하지 않거나, 혹은 지워진 부분만큼 이미지를 다시 그려주면 된다. 물론 나는 실수로 backgroundImage를 설정해주긴 했지만, 지워진 이미지를 다시 그리는 것보다 코드가 깔끔할 것 같아 굳이 수정하지는 않았다.

3번의 clearRect로 영역 내부의 fillRect를 지우는 과정에서, Background를 지정하지 않았기 때문에 영역 내부가 까맣게 보였다. 이를 해결하고자 영역 외부를 다시 네 개의 영역으로 나눠서, 네 개의 fillRect를 써야하나 고민하고 있을 무렵이었다. Ctrl+z를 잘못 눌렀는지 Ctrl+v를 잘못 눌렀는지, image.onload 내에 backgroundImage를 설정하는 코드가 들어갔다. 그런데 정상적으로 동작하는게 아닌가? = ㅅ=…?

심지어 mousemove 이벤트가 발생할 때마다 영역을 다시 그려줘야 하는데, 캔버스 위에 그려진 내용을 지우고 다시 그리지 않으면, 영역이 계속해서 중첩되어 그려지기 때문에 몹시 이상해진다. 그래서 들어가있는 clearRect(0, 0, canvas_width, canvas_height)가 drawImage이후에 동작하고 있었다. 바꿔말하면, 캔버스 위에 새로 그려진 Image는 이미 지워진 셈이다.

한참을 찾아봤지만 drawImage에 대해 정리된 내용은 찾지 못했고, 찜찜한 코드를 그냥 내버려둘 수는 없었다. 그래서 Image.onload가 호출됐을 때 이미지를 함수 외부의 변수에 백업해놓고, mousemove가 발생했을 시 다시 그리게끔 처리를 해줬다.

다음은 이런 증상(?)이 발생하는 내용을 추측해본 결과다. 리서치는 해봤지만 뾰족한 답안은 나오지 않아서, 근거 없이 생각한 내용만 정리해두기로 했다. 나중에 찾으면 내용을 추가해야겠다.

  • Image.onload가 발생했을 때 해당 이미지는 모두 로드된 상태이므로, canvas의 backgroundImage에 해당 이미지의 URL을 지정하면 캐쉬가 동작한다.
  • Image.onload가 발생했을 때 이미지는 모두 로드된 상태로, canvas 위에 이미지를 그려줄 때는 flickering이 발생하지 않는다. 따라서 canvas에 backgroundImage를 교체할 때 실제로는 flickering이 발생하지만, flickering이 발생하는 동안 위에 backgroundImage가 표시되므로 사용자의 눈에는 flickering이 발생하지 않는 것처럼 보인다.

일단은 두번째의 가능성이 제일 높은 것 같지만, 자세한 내용은 근거를 찾은 뒤에 추가하도록 해야겠다.

Image.onload가 발생했을 때 이미지는 모두 로드 된 상태이다. 따라서 Image.onload 내에서 backgroundImage를 교체하게 되면, 이미 로드가 완료된 이미지를 교체하게 되므로 Flickering이 발생하지 않는다. Chrome에서 해당 내용을 테스트할 때 인터넷 속도에 제한을 걸어놓고 테스트해보면 확연하게 알 수 있는데, backgroundImage를 교체하는 시점에 기존의 backgroundImage를 삭제한 후, 새로운 이미지가 로드되는 대로 backgroundImage에 그려지는 것을 확인할 수 있다.

이 내용을 우연히 영국 업체의 Invoice로 다시 확인하게 됐는데, 미리 코드를 수정해놔서 크게 걱정할 일은 없을 듯 하다. 해피엔딩이구만. :)




반응형