CSS pixel, device pixel, backing store
CSS에서 width: 800px인 Canvas를 만들면 화면에는 800 CSS pixel 너비의 요소가 생깁니다. 그런데 GPU가 실제로 그리는 픽셀 버퍼의 크기도 꼭 800일까요?
아닙니다. 여기서 devicePixelRatio가 등장합니다.
Canvas에는 두 크기가 있다
Canvas에는 CSS 크기와 backing store 크기가 따로 있습니다.
<canvas style="width: 800px; height: 600px"></canvas>
canvas.width = 1600;
canvas.height = 1200;
겉으로 보이는 크기는 800 x 600 CSS pixel입니다. 하지만 내부 픽셀 버퍼는 1600 x 1200일 수 있습니다. 고해상도 디스플레이에서 선명하게 보이려면 보통 이렇게 맞춥니다.
backingWidth = cssWidth * devicePixelRatio
backingHeight = cssHeight * devicePixelRatio
devicePixelRatio가 2라면 CSS pixel 하나를 물리 pixel 2 x 2개 정도로 표현합니다.
흐릿한 Canvas의 흔한 원인
Canvas가 흐릿해 보일 때는 대개 CSS 크기만 키우고 backing store 크기를 맞추지 않은 경우입니다.
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
2D Canvas에서는 이 뒤에 context scale을 걸기도 합니다.
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
WebGL/WebGPU에서는 viewport와 projection matrix를 이 backing store 크기에 맞춰야 합니다. CSS에서 보던 좌표와 GPU가 보는 픽셀 크기를 섞으면 포인터 위치와 렌더링 위치가 어긋납니다.
editor model은 CSS pixel 기준으로 둔다
그렇다고 편집기 모델의 모든 값을 device pixel로 저장하면 안 됩니다. 도형의 x, y, width, height는 CSS pixel 또는 world unit 기준으로 유지하는 편이 좋습니다.
const node = {
x: 100,
y: 80,
width: 240,
height: 120
};
이 값은 사용자의 문서 좌표입니다. 사용자가 어떤 모니터에서 열었는지와 무관해야 합니다. devicePixelRatio는 렌더링 단계에서만 반영합니다.
world unit -> screen CSS pixel -> device pixel
이 선을 넘기면 같은 파일을 다른 디스플레이에서 열었을 때 좌표가 달라지는 이상한 모델이 됩니다.
resize도 처리해야 한다
Canvas는 화면 크기가 바뀔 때 backing store도 다시 맞춰야 합니다. 브라우저 창이 리사이즈되거나, sidebar가 열려 canvas 영역이 바뀌거나, devicePixelRatio가 바뀔 수도 있습니다.
처음 구현에서는 매 프레임 rect를 확인하는 단순한 방식으로 시작해도 됩니다.
function resizeCanvasToDisplaySize(canvas) {
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const width = Math.round(rect.width * dpr);
const height = Math.round(rect.height * dpr);
if (canvas.width === width && canvas.height === height) {
return false;
}
canvas.width = width;
canvas.height = height;
return true;
}
나중에는 ResizeObserver로 바꿀 수 있습니다. 중요한 것은 CSS 크기와 backing store 크기가 별개라는 사실을 코드에 드러내는 것입니다.
오늘의 핵심
편집기 모델은 사용자가 이해하는 좌표계에 남겨둡니다. 렌더러는 그 모델을 현재 화면과 디스플레이에 맞게 픽셀로 변환합니다.
model coordinate는 안정적이어야 한다.
backing store는 디스플레이에 맞춰 바뀔 수 있다.
이 구분은 WebGL/WebGPU에서 더 중요해집니다. viewport, projection matrix, pointer mapping, screenshot export가 모두 이 선 위에 서 있기 때문입니다.