html5 canvas 뜯어먹기

html5 canvas 뜯어먹기

html5 canvas의 기본 구조부터 작동 원리까지

·

6 min read

21개 프로젝트로 완성하는 인터랙티브 웹 개발 with Three.js & Canvas 강의 part.2를 참고하여 작성한 포스팅입니다.


HTML Canvas란?

HTML5 캔버스(Canvas)는 웹 페이지에서 그래픽을 그리기 위한 HTML 요소다. 캔버스는 JavaScript를 사용하여 동적으로 그래픽을 생성하고 조작할 수 있는 강력한 도구로, 다양한 웹 기반 그래픽 애플리케이션을 만들 수 있도록 지원한다. 2D 그래픽, 애니메이션, 인터렉션, 차트 및 그래프, 게임 개발, 이미지 처리, 그래픽 편집기, 그림판 등 다양한 활용 범위가 있다.


Canvas의 기본 구조

<canvas>
</canvas>
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

const canvasWidth = 500;
const canvasHeight = 500;

canvas.style.width = `${canvasWidth}px`;
canvas.style.height = `${canvasHeight}px`;
canvas.width = canvasWidth;
canvas.height = canvasHeight;

ctx : getContext

HTMLCanvasElement.getContext(contextType) 메소드는 캔버스의 드로잉 컨텍스트를 반환한다. getContext(”2d”); 로 설정하면 2D 렌더링 컨텍스트를 가지게 된다. 바로 캔버스에 그림을 그릴 수 있게 된 것이다.

캔버스에 사각형 그려보기

ctx를 생성했다면, fillRect(x, y, width, height) 함수를 사용해 캔버스 위에 사각형을 그려볼 수 있다.

캔버스 사이즈에 대한 이해

캔버스의 기본 사이즈는 300*150(px)로 지정 되어있다. 정사각형 형태의 캔버스를 만들기 위해 검은색 정사각형이 그려져있는 캔버스를 아래와 같이 height를 300px로 늘려주면 어떻게 될까?

canvas.style.width = "300px";
canvas.style.height = "300px";

예상과는 다르게 정직하게 캔버스 내에 그려진 정사각형까지 같이 2배로 늘어난 모습을 관찰할 수 있다. 이유는 기본 css를 사용해서 캔버스의 height를 강제로 2배 늘린 꼴이 되기 때문이다. 캔버스 자체의 사이즈도 일치시켜 주어야 예상과 동일하게 화면이 출력된다.

canvas.width = 300;
canvas.height = 300;

반대로 canvas 사이즈를 강제로 줄여본다면?

canvas.width = 100;
canvas.height = 100;

기존 캔버스 고유의 크기가 css의 사이즈에 맞춰 늘어나게 되면서 픽셀이 깨져 살짝 화질이 흐려지는 모습을 발견 할 수 있다. 그래서 캔버스의 사이즈를 설정할 때는, 캔버스 요소의 사이즈와 캔버스 자체의 사이즈를 일치해 사용하는 것이 좋다.

dpr : devicePixelRatio

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio;

const canvasWidth = 500;
const canvasHeight = 500;

canvas.style.width = `${canvasWidth}px`;
canvas.style.height = `${canvasHeight}px`;
canvas.width = canvasWidth * dpr;
canvas.height = canvasHeight * dpr;

ctx.scale(dpr, dpr);

window 인터페이스의 devicePixelRatio 는 현재 디스플레이 장치에 대한 '물리적 픽셀' 해상도와 'CSS 픽셀' 해상도의 비율을 반환한다. 쉽게 말하면, 브라우저에 단일 CSS 픽셀을 그리기 위해 화면의 실제 픽셀 중 몇 개를 사용해야 하는지 알려주는 것이다. (dpr = 디바이스 픽셀 / css 픽셀)

애플 레티나 디스플레이의 경우 dpr 값이 2가 나오는 것을 관찰할 수 있다. (선명하다는 뜻)

dpr이 높을수록 이미지가 선명해지는데, 위의 예시처럼 애플 레티나 디스플레이의 경우 1개의 CSS 픽셀을 표현하기 위해 4개의 물리적 픽셀을 사용한다. 때문에 0.5px 단위의 border 등을 표현하기에 용이한 것이다.

제 모니터는 dpr 값이 1.5가 나옵니다

dpr : 다양한 디바이스에서 일관된 시각적 경험 제공하기

const dpr = window.devicePixelRatio;

canvas.width = canvasWidth * dpr;
canvas.height = canvasHeight * dpr;

ctx.scale(dpr, dpr);

그래서 dpr을 활용해 요소의 크기를 디바이스의 픽셀 비율에 따라 조정하면서, 동일한 그래픽이 고해상도 디스플레이와 일반 디스플레이에서 모두 잘 보일 수 있게 된다. 아래 그림으로 코드를 이해해보자.

일반적인 디바이스에서 보여지는 모습을 간략하게 나타낸 예제다. dpr이 1의 값을 나타내기 때문에 별다른 변화를 일으키지 않는다.

dpr이 1보다 큰 값을 가지는 경우에는 기존에 생각하고 있던 화면과 다른 비율의 모습이 그려지게 된다. 먼저 canvas 요소의 width, heightdpr을 곱해서 논리적 픽셀에 픽셀 비율을 곱해 물리적 픽셀에 맞게 조정된 너비를 설정한다.

다음으로는 scale에 각 dpr을 곱해주면서 캔버스 내 그래픽 컨텍스트에 대한 스케일을 설정한다. scale() 함수는 그래픽을 그릴 때 픽셀 비율을 고려하여 좌표 시스템을 조정하는 데 사용된다.

마지막으로 canvas 요소의 스타일 크기를 강제로 조정함으로써, 동일한 너비 내에 더 많은 물리적인 픽셀이 존재하게 되어 화질이 향상된다. 이를 통해 고해상도 디스플레이에서 더 선명한 이미지를 얻을 수 있다.

사각형 그리기 예제

원그리기

사각형을 그리는 것과는 달리, 원을 그리기 위해서는 여러 단계를 거쳐야 한다. arc(x, y, radius, startAngle, endAngle, counterclockwise) 함수를 사용하여 원의 형태를 그릴 수 있으며, 아래 예제와 같은 원리로 원이 그려진다.

radian과 degree

자료출처 : https://www.cuemath.com/geometry/radians-to-degrees/ / 1radian은 57.296degree

canvas에서 각도를 나타낼 때는 일반적으로 사용하는 degree 대신 radian을 사용한다. 라디안은 주로 수학적인 편의성과 특히 삼각학수(sin, cos, tan 등)와의 연동성을 위해 사용된다.

ctx.rotate(), ctx.arc() 등 회전 및 변형 관련 메서드에서 각도를 지정할 때 라디안이 사용된다.

// radian to degree
(Math.PI / 180) * deg // deg = 360 원, 180 반원

만약 각도(degrees)를 canvas에서 사용하고자 한다면, 각도를 라디안으로 변환하는 수식을 사용하여 처리할 수 있다.

원 그리기 예제


Canvas 애니메이션의 작동 원리

이미지 출처 : https://blog.schoolspecialty.com/flip-books-101/

canvas에 그려진 그래픽이 애니메이션되는 것처럼 보이는 원리는 매 프레임마다 기존의 장면을 지우고 새로운 장면을 빠르게 업데이트하여 연속으로 재생하면서 움직이는 것처럼 보이게 하는 것이다.

function animate() {
    window.requestAnimationFrame(animate)
    ctx.clearRect(0, 0, canvasWidth, canvasHeight) // clear
    particle.draw() // draw
}

window.requestAnimationFrame(재귀함수) → 1초 마다 모니터 주사율만큼 무한히 실행되는 함수

동작

requestAnimationFrame 은 웹 애니메이션 및 그래픽 처리에 사용되는 웹 브라우저 함수다. 이 함수를 사용하면 브라우저가 리페인트하기 전에 콜백 함수를 호출하여 애니메이션 및 그래픽 작업을 부드럽게 처리할 수 있다. 이 함수는 1초에 현재 모니터의 주사율 만큼 동작을 반복한다. 예를 들어, 144Hz의 게이밍 모니터를 사용하고 있다면 1초에 144번 동작하는 것이다.

때문에 x를 1px씩 이동시키는 애니메이션을 작성했다면, 주사율에 따라 어떤 사용자는 1초에 144px을 이동하게 되고 다른 사용자는 60px을 이동하는 것을 관찰하게 된다. 이렇게 사용자의 환경마다 다른 애니메이션이 제공되어 버린다.

이 때, FPS(frame per second)의 개념을 도입한다면, 모두에게 같은 화면이 보이도록 지원할 수 있다.

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio;
const fps = 60;
const interval = 1000 / fps;
let now, delta;
let then = Date.now();

function render() {
  requestAnimationFrame(render);
  now = Date.now();
  delta = now - then;
  if (delta < interval) return;

  ctx.fillRect(100, 100, 200, 200);

  then = now - (delta % interval);
}

fps를 60으로 설정하게 되면 약 16ms 마다 requestAnimationFrame이 실행된다.

아래는 위의 코드를 fps를 10으로 설정하여 실제 콘솔에 출력한 결과다. 환경에 따라 가끔씩 1정도의 오차가 발생할 수 있지만, 11-44-78-11(111)의 패턴이 반복되는 것을 확인할 수 있다. 이렇게 now와 then의 차이에 따라 다른 환경에서 동일한 빈도로 애니메이션을 재생할 수 있게 된다.

이를 활용해서 fps를 설정할 때는 적어도 60fps 이상을 지원하는 것이 좋다고 한다.

애니메이션 설정해주기

canvas 애니메이션 코드 예제

파티클 애니메이션 예제

canvas 애니메이션 보일러 플레이트

마지막으로 화면 resize가 발생할 때마다 캔버스 사이즈를 재정의할 수 있도록 init()함수를 실행하는 코드를 추가하여 앞으로의 작업을 위한 보일러 플레이트 코드를 구성해 보았다.

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio;
const fps = 60;
const interval = 1000 / fps;
let now, delta;
let then = Date.now();

let canvasWidth, canvasHeight;

function init() {
  canvasWidth = innerWidth;
  canvasHeight = innerHeight;

  canvas.width = canvasWidth * dpr;
  canvas.height = canvasHeight * dpr;
  ctx.scale(dpr, dpr);

  canvas.style.width = canvasWidth + "px";
  canvas.style.height = canvasHeight + "px";
}

function render() {
  requestAnimationFrame(render);
  now = Date.now();
  delta = now - then;
  if (delta < interval) return;

  ctx.fillRect(100, 100, 200, 200);

  then = now - (delta % interval);
}

window.addEventListener("load", () => {
  init();
  render();
});
window.addEventListener("resize", () => {
  init();
});

참고자료

https://developer.mozilla.org/ko/docs/Web/API/HTMLCanvasElement/getContext

https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D

https://developer.mozilla.org/ko/docs/Web/API/Window/devicePixelRatio

https://velog.io/@vnfdusdl/DPRDevice-pixel-ratio%EC%9D%98-%EC%9D%B4%ED%95%B4

https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/scale

https://jae-kwang.github.io/blog/2019/01/28/requestAnimationFrame/

https://www.cuemath.com/geometry/radians-to-degrees/