devicePixelContentBox로 완벽한 픽셀 렌더링

캔버스에는 실제로 몇 개의 픽셀이 있나요?

Chrome 84부터 ResizeObserver실제 픽셀로 요소의 크기를 측정하는 devicePixelContentBox라는 새로운 상자 측정을 지원합니다. 이를 통해 특히 고밀도 화면의 맥락에서 픽셀이 정확한 그래픽을 렌더링할 수 있습니다.

브라우저 지원

  • Chrome: 84.
  • Edge: 84.
  • Firefox: 93
  • Safari: 지원되지 않음

소스

em, %, vh와 같은 추상적인 길이 단위를 사용하는 경우가 많지만 결국 픽셀로 귀결됩니다. CSS에서 요소의 크기나 위치를 지정할 때마다 브라우저의 레이아웃 엔진은 결국 이 값을 픽셀 (px)로 변환합니다. 이러한 픽셀은 'CSS 픽셀'이며, 많은 기록이 있으며 화면에 있는 픽셀과는 약한 관계만 있습니다.

오랫동안 모든 사용자의 화면 픽셀 밀도를 96DPI ('인치당 도트 수')로 추정하는 것이 상당히 합리적이었습니다. 즉, 모든 모니터의 픽셀 밀도는 센티미터당 약 38개였습니다. 시간이 지남에 따라 모니터가 커지거나 작아지거나 동일한 표면적에 더 많은 픽셀이 포함되기 시작했습니다. 웹의 많은 콘텐츠가 글꼴 크기를 비롯한 크기를 px에서 정의한다는 사실과 결합하면 이러한 고밀도 ('HiDPI') 화면에서 읽을 수 없는 텍스트가 표시됩니다. 이에 대한 대책으로 브라우저는 모니터의 실제 픽셀 밀도를 숨기고 대신 사용자에게 96DPI 디스플레이가 있는 것처럼 가장합니다. CSS의 px 단위는 이 가상 96DPI 디스플레이의 1픽셀 크기를 나타내므로 'CSS 픽셀'이라는 이름이 붙었습니다. 이 단위는 측정 및 위치 지정용으로만 사용됩니다. 실제 렌더링이 실행되기 전에 실제 픽셀로 변환됩니다.

이 가상 디스플레이에서 사용자의 실제 디스플레이로 이동하려면 어떻게 해야 하나요? devicePixelRatio를 입력합니다. 이 전역 값은 단일 CSS 픽셀을 형성하는 데 필요한 실제 픽셀 수를 나타냅니다. devicePixelRatio (dPR)이 1이면 약 96DPI의 모니터를 사용하고 있는 것입니다. 레티나 화면을 사용하는 경우 dPR은 2일 수 있습니다. 휴대전화에서는 2, 3, 2.65와 같이 더 높고 이상한 dPR 값이 발생하는 경우가 많습니다. 이 값은 정확하지만 모니터의 실제 DPI 값을 파생할 수는 없습니다. dPR이 2이면 CSS 픽셀 1개가 물리적 픽셀 2개에 정확히 매핑됩니다.

너비는 3,440픽셀이고 디스플레이 영역의 너비는 79cm입니다. 따라서 해상도는 110DPI가 됩니다. 96에 근접하지만 정확하지는 않습니다. 또한 대부분의 디스플레이에서 <div style="width: 1cm; height: 1cm">이 정확히 1cm로 측정되지 않는 이유도 이 때문입니다.

마지막으로 dPR은 브라우저의 확대/축소 기능의 영향을 받을 수도 있습니다. 확대하면 브라우저에서 보고된 dPR이 증가하여 모든 항목이 더 크게 렌더링됩니다. 확대/축소하는 동안 DevTools 콘솔에서 devicePixelRatio를 선택하면 소수점 이하 자릿수가 표시됩니다.

확대/축소로 인해 다양한 소수점 devicePixelRatio을 보여주는 DevTools

<canvas> 요소를 믹스에 추가해 보겠습니다. widthheight 속성을 사용하여 캔버스의 픽셀 수를 지정할 수 있습니다. 따라서 <canvas width=40 height=30>는 40x30픽셀의 캔버스입니다. 하지만 그렇다고 해서 40x30픽셀로 표시된다는 의미는 아닙니다. 기본적으로 캔버스는 widthheight 속성을 사용하여 고유 크기를 정의하지만, 익숙한 모든 CSS 속성을 사용하여 캔버스의 크기를 임의로 조절할 수 있습니다. 지금까지 알아본 내용을 토대로 생각해 보면 모든 시나리오에 이 방법이 이상적이지 않을 수 있습니다. 캔버스의 한 픽셀이 여러 실제 픽셀을 덮을 수도 있고 실제 픽셀의 일부만 덮을 수도 있습니다. 이로 인해 불쾌감을 주는 시각적 아티팩트가 발생할 수 있습니다.

요약하면 캔버스 요소는 그릴 수 있는 영역을 정의하는 지정된 크기를 갖습니다. 캔버스 픽셀 수는 CSS 픽셀로 지정된 캔버스의 디스플레이 크기와는 완전히 무관합니다. CSS 픽셀 수는 실제 픽셀 수와 다릅니다.

Pixel Perfect

경우에 따라 캔버스 픽셀을 실제 픽셀에 정확하게 매핑하는 것이 좋습니다. 이러한 매핑이 이루어지면 '픽셀 정밀'이라고 합니다. 특히 하위 픽셀 렌더링을 사용하거나 밝기가 번갈아 표시되는 선이 촘촘하게 정렬된 그래픽을 표시할 때 텍스트를 읽기 쉽게 렌더링하려면 픽셀 정밀 렌더링이 중요합니다.

웹에서 최대한 픽셀 완벽 캔버스에 가까운 결과를 얻기 위해 다음과 같은 접근 방식이 주로 사용되었습니다.

<style>
 
/* … styles that affect the canvas' size … */
</style>
<canvas id="myCanvas"></canvas>
<script>
 
const cvs = document.querySelector('#myCanvas');
 
// Get the canvas' size in CSS pixels
 
const rectangle = cvs.getBoundingClientRect();
 
// Convert it to real pixels. Ish.
  cvs
.width = rectangle.width * devicePixelRatio;
  cvs
.height = rectangle.height * devicePixelRatio;
 
// Start drawing…
</script>

눈치가 빠른 독자는 dPR이 정수 값이 아닌 경우 어떻게 되는지 궁금할 수 있습니다. 좋은 질문입니다. 이 문제의 핵심이 바로 여기에 있습니다. 또한 비율, vh 또는 기타 간접 값을 사용하여 요소의 위치나 크기를 지정하면 이러한 값이 소수점 CSS 픽셀 값으로 확인될 수 있습니다. margin-left: 33%가 있는 요소는 다음과 같은 직사각형으로 끝날 수 있습니다.

getBoundingClientRect() 호출의 결과로 소수점 이하 자릿수의 픽셀 값을 보여주는 DevTools

CSS 픽셀은 순전히 가상적이므로 이론적으로는 픽셀의 일부를 사용하는 것이 좋지만 브라우저는 실제 픽셀에 대한 매핑을 어떻게 파악하나요? 분수 실제 픽셀은 존재하지 않기 때문입니다.

픽셀 고정

요소를 실제 픽셀과 정렬하는 단위 변환 프로세스의 부분을 '픽셀 스냅'이라고 하며, 이 부분은 분수 픽셀 값을 정수, 실제 픽셀 값으로 스냅합니다. 정확한 작동 방식은 브라우저마다 다릅니다. dPR이 1인 디스플레이에 너비가 791.984px인 요소가 있는 경우 한 브라우저는 요소를 792px 실제 픽셀로 렌더링할 수 있고 다른 브라우저는 791px로 렌더링할 수 있습니다. 1픽셀만 어긋나지만 1픽셀이라도 픽셀 단위로 완벽해야 하는 렌더링에는 해로울 수 있습니다. 이로 인해 흐릿해지거나 모아레 효과와 같은 더 눈에 띄는 아티팩트가 발생할 수 있습니다.

위 이미지는 색상이 다른 픽셀의 래스터입니다. 아래 이미지는 위와 동일하지만, 이중 선형 크기 조정을 사용하여 너비와 높이가 1픽셀씩 줄었습니다. 이때 나타나는 패턴을 모아레 효과라고 합니다.
(이미지에 크기 조정이 적용되지 않은 상태로 보려면 새 탭에서 이 이미지를 열어야 할 수도 있습니다.)

devicePixelContentBox

devicePixelContentBox는 기기 픽셀 (즉, 실제 픽셀) 단위로 요소의 콘텐츠 상자를 제공합니다. ResizeObserver의 일부입니다. Safari 13.1부터 ResizeObserver가 모든 주요 브라우저에서 지원되지만 devicePixelContentBox 속성은 현재 Chrome 84 이상에서만 사용할 수 있습니다.

ResizeObserver: 요소의 document.onresize와 유사에 설명된 대로 ResizeObserver의 콜백 함수는 페인트 전과 레이아웃 후에 호출됩니다. 즉, 콜백의 entries 매개변수에는 페인팅되기 직전의 모든 관찰된 요소의 크기가 포함됩니다. 위에 설명된 캔버스 문제의 맥락에서 이 기회를 사용하여 캔버스의 픽셀 수를 조정하여 캔버스 픽셀과 실제 픽셀 간에 정확한 일대일 매핑을 실행할 수 있습니다.

const observer = new ResizeObserver((entries) => {
 
const entry = entries.find((entry) => entry.target === canvas);
  canvas
.width = entry.devicePixelContentBoxSize[0].inlineSize;
  canvas
.height = entry.devicePixelContentBoxSize[0].blockSize;

 
/* … render to canvas … */
});
observer
.observe(canvas, {box: ['device-pixel-content-box']});

observer.observe()의 옵션 객체에 있는 box 속성을 사용하면 관찰할 크기를 정의할 수 있습니다. 따라서 각 ResizeObserverEntry는 항상 borderBoxSize, contentBoxSize, devicePixelContentBoxSize를 제공하지만 (브라우저에서 지원하는 경우) 콜백은 관찰된 박스 측정항목 중 하나라도 변경되는 경우에만 호출됩니다.

이 새 속성을 사용하면 캔버스의 크기와 위치를 애니메이션 처리할 수도 있고 (실질적으로 소수점 이하 픽셀 값을 보장) 렌더링에 모아레 효과가 표시되지 않습니다. getBoundingClientRect()를 사용하는 접근 방식에서 모아레 효과가 어떻게 표시되고 새 ResizeObserver 속성을 사용하여 이를 방지하는 방법을 알아보려면 Chrome 84 이상에서 데모를 살펴보세요.

기능 감지

사용자의 브라우저에서 devicePixelContentBox를 지원하는지 확인하려면 요소를 관찰하고 ResizeObserverEntry에 속성이 있는지 확인하면 됩니다.

function hasDevicePixelContentBox() {
 
return new Promise((resolve) => {
   
const ro = new ResizeObserver((entries) => {
      resolve
(entries.every((entry) => 'devicePixelContentBoxSize' in entry));
      ro
.disconnect();
   
});
    ro
.observe(document.body, {box: ['device-pixel-content-box']});
 
}).catch(() => false);
}

if (!(await hasDevicePixelContentBox())) {
 
// The browser does NOT support devicePixelContentBox
}

결론

픽셀은 웹에서 매우 복잡한 주제이며 지금까지는 요소가 사용자 화면에서 차지하는 실제 픽셀 수를 정확하게 알 수 있는 방법이 없었습니다. ResizeObserverEntry의 새로운 devicePixelContentBox 속성은 이 정보를 제공하며 <canvas>로 픽셀 완벽 렌더링을 할 수 있도록 합니다. devicePixelContentBox은 Chrome 84 이상에서 지원됩니다.