HTML-in-Canvas API 오리진 트라이얼 소개

Thomas Nattestad
Thomas Nattestad

수년 동안 웹 개발자는 웹에서 복잡하고 상호작용성이 높은 시각적 애플리케이션을 빌드할 때 어려운 아키텍처 선택을 해야 했습니다. 풍부한 시맨틱 기능을 위해 DOM을 사용해야 할까요 아니면 하위 수준 그래픽 성능을 위해 <canvas> 요소에 직접 렌더링해야 할까요?

이제 오리진 트라이얼에서 사용할 수 있는 새로운 실험용 HTML-in-Canvas API를 사용하면 선택하지 않아도 됩니다. 이 API를 사용하면 UI를 상호작용 가능하고 액세스 가능하며 즐겨 사용하는 브라우저 기능에 연결된 상태로 유지하면서 DOM 콘텐츠를 2D 캔버스 또는 WebGL/WebGPU 텍스처에 직접 그릴 수 있습니다. HTML을 하위 수준 그래픽 처리와 결합하면 이전에는 불가능했던 환경을 만들 수 있습니다.

DOM과 캔버스

이 새로운 API의 기능을 이해하려면 DOM과 Canvas의 상대적인 강점을 살펴보는 것이 좋습니다.

DOM은 웹 UI의 기본 요소입니다. 시맨틱으로 이해된 콘텐츠를 사용하여 풍부한 인터페이스를 만드는 텍스트 레이아웃 솔루션을 기본적으로 제공합니다. 이를 통해 사용자는 웹페이지에서 텍스트를 강조 표시하여 복사하거나 이미지를 마우스 오른쪽 버튼으로 클릭하여 저장하는 등 당연하게 생각하는 일반적인 작업을 원활하게 실행할 수 있습니다. DOM은 접근성 도구, 번역, 페이지 내 검색, 리더 모드, 확장 프로그램, 어두운 모드, 브라우저 확대/축소, 자동 완성과 같은 필수 브라우저 기능과도 통합됩니다.

반면 캔버스 (및 WebGL/WebGPU)를 사용하면 고급 2D 및 3D 그래픽을 위해 픽셀 그리드에 대한 하위 수준 액세스가 가능합니다. 게임과 복잡한 웹 앱 (예: Google Docs 또는 Figma)에는 이러한 고성능의 하위 수준 액세스가 필요합니다. 캔버스는 기본적으로 픽셀 그리드이므로 반응형 텍스트와 같은 기능을 지원하려면 복잡한 맞춤 UI 로직이 필요하여 번들 크기가 크게 증가했습니다. 중요한 점은 UI가 정적 캔버스 픽셀 그리드 내에 갇히면 DOM에 통합된 모든 강력한 브라우저 기능이 완전히 중단된다는 것입니다.

DOM을 Canvas로 가져올 때의 이점

HTML-in-Canvas API는 두 가지 장점을 모두 제공하는 브리지입니다. <canvas> 요소 내에 HTML을 배치하고 변환을 동기화하면 콘텐츠가 완전히 대화형으로 유지되고 모든 브라우저 통합이 자동으로 작동합니다.

<canvas> 요소 내에서 DOM이 UI를 처리하도록 하면 다음과 같은 이점이 있습니다.

  • 텍스트 레이아웃 및 서식: CSS 스타일이 적용된 여러 줄 또는 양방향 텍스트를 비롯한 텍스트 레이아웃 및 서식이 간소화되었습니다.
  • 양식 컨트롤: 광범위한 맞춤설정 옵션을 갖춘 표현력이 풍부하고 사용하기 쉬운 양식 컨트롤
  • 텍스트 선택, 복사/붙여넣기, 마우스 오른쪽 버튼 클릭: 사용자는 3D 장면 내에서 텍스트를 강조 표시하거나 컨텍스트 메뉴를 기본적으로 마우스 오른쪽 버튼으로 클릭할 수 있습니다.
  • 접근성: 캔버스 내에 렌더링된 콘텐츠가 접근성 트리에 노출됩니다. 접근성 시스템은 일반 HTML처럼 UI를 파싱하고 스크린 리더와 같은 시스템에 노출할 수 있습니다.
  • 페이지 내 검색: 사용자는 페이지 내 검색 (Ctrl/Cmd+F)을 사용하여 텍스트를 검색할 수 있으며 브라우저에서 WebGL 텍스처 내에서 직접 강조 표시합니다.
  • 색인 가능 및 AI 에이전트 인터페이스 가능: 웹 크롤러와 AI 에이전트가 2D 및 3D 장면으로 렌더링된 텍스트를 원활하게 색인하고 읽을 수 있습니다.
  • 확장 프로그램 통합: 브라우저 확장 프로그램이 기본적으로 작동합니다. 예를 들어 텍스트 대체 확장 프로그램은 3D 메시에 렌더링된 텍스트를 자동으로 업데이트합니다.
  • DevTools 통합: Chrome DevTools에서 직접 WebGL/WebGPU UI 요소를 포함한 캔버스 콘텐츠를 검사할 수 있습니다. 인스펙터에서 CSS 스타일을 조정하면 3D 텍스처가 즉시 업데이트됩니다.

대략적인 사용 사례

이 API는 여러 도메인에서 엄청난 잠재력을 발휘합니다.

  • 대형 캔버스 기반 애플리케이션: Google Docs, Miro, Figma와 같은 대형 웹 앱이 이제 복잡한 애플리케이션 UI 구성요소를 캔버스 기반 작업공간에 기본적으로 렌더링하여 접근성을 개선하고 번들 무게를 줄일 수 있습니다.
  • 3D 장면 및 게임: 마케팅 사이트, 몰입형 WebXR 환경, 웹 게임에서 이제 완전히 상호작용 가능한 웹 UI를 3D 장면에 배치할 수 있습니다. 예를 들어 실제 DOM 텍스트를 사용하는 3D 책이나 복사 및 붙여넣기를 기본적으로 지원하는 게임 내 터미널이 있습니다.

API 사용 방법

API 사용은 캔버스 설정, 캔버스에 렌더링, 브라우저가 화면에서 요소가 실제로 있는 위치를 알 수 있도록 CSS 변환 업데이트의 세 단계로 이루어집니다.

기본 요건

HTML-in-Canvas API는 Chrome 148~150에서 오리진 트라이얼로 제공됩니다. 사이트에서 테스트하려면 chrome://flags/#canvas-draw-element 플래그가 사용 설정된 Chrome Canary 149 이상을 사용하세요. 다른 사용자가 API를 사용할 수 있도록 하려면 오리진 트라이얼에 등록하세요.

1단계: 기본 캔버스 설정

먼저 <canvas> 태그에 layoutsubtree 속성을 추가합니다. 이렇게 하면 브라우저가 캔버스 내에 중첩된 콘텐츠를 인식하여 캔버스 내에 표시할 준비를 하고 접근성 트리에 노출합니다.

<canvas id="canvas" style="width: 200px; height: 200px;" layoutsubtree>
  <div id="form_element">
    <label for="name">Name:</label> <input id="name" type="text">
  </div>
</canvas>

캔버스 그리드 크기 조정

렌더링된 콘텐츠가 흐려지지 않도록 캔버스 그리드를 기기 배율에 맞게 조정해야 합니다.

const observer = new ResizeObserver(([entry]) => {
  const dpc = entry.devicePixelContentBoxSize;
  canvas.width = dpc ? dpc[0].inlineSize : Math.round(entry.contentRect.width * window.devicePixelRatio);
  canvas.height = dpc ? dpc[0].blockSize : Math.round(entry.contentRect.height * window.devicePixelRatio);
});

const supportsDevicePixelContentBox =
  typeof ResizeObserverEntry !== 'undefined' &&
  'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;
const options = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};
observer.observe(canvas, options);

2단계: 렌더링

2D 컨텍스트의 경우 drawElementImage 메서드를 사용합니다. 텍스트 강조 표시 또는 사용자 입력과 같이 요소가 다시 그려질 때마다 트리거되는 paint 이벤트 내에서 이를 실행합니다. 상호작용이 계속 작동하도록 반환 값으로 요소의 CSS 변환을 업데이트하는 것이 중요합니다.

const ctx = document.getElementById('canvas').getContext('2d');
const form_element = document.getElementById('form_element');
const canvas = document.getElementById('canvas');

canvas.onpaint = () => {
  ctx.reset();

  // Draw the form element at x:0, y:0
  let transform = ctx.drawElementImage(form_element, 0, 0);

  // Use the transform returned later on...
};

WebGL로 렌더링

WebGL의 경우 texElementImage2D를 사용합니다. texImage2D와 유사하게 작동하지만 DOM 요소를 소스로 사용합니다.

canvas.onpaint = () => {
  if (gl.texElementImage2D) {
    gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, form_element);
  }
};

WebGPU로 렌더링

WebGPU는 copyExternalImageToTexture과 유사하게 기기 대기열에서 copyElementImageToTexture 메서드를 사용합니다.

canvas.onpaint = () => {
  root.device.queue.copyElementImageToTexture(
    valueElement,
    { texture: targetTexture }
  );
};

3단계: CSS 변환 업데이트

캔버스에 요소를 렌더링했으므로 이제 요소가 있는 위치를 브라우저에 업데이트해야 합니다. 이렇게 하면 캔버스와 DOM의 레이아웃 간에 공간 동기화가 이루어집니다. 브라우저가 이벤트 영역(예: 사용자가 클릭하거나 마우스를 가져간 위치)을 요소가 렌더링되는 위치와 올바르게 매핑할 수 있도록 하는 것이 중요합니다.

2D 컨텍스트의 경우 렌더링 호출에서 반환된 변환을 .style.transform property에 적용합니다.

const ctx = document.getElementById('canvas').getContext('2d');
const form_element = document.getElementById('form_element');
const canvas = document.getElementById('canvas');

canvas.onpaint = () => {
  ctx.reset();
  // Draw the form element at x:0, y:0
  let transform = ctx.drawElementImage(form_element, 0, 0);

  // Sync the DOM location with the drawn location
  form_element.style.transform = transform.toString();
};

WebGL 또는 WebGPU를 사용하면 요소의 화면 위치가 셰이더 코드에서 출력 텍스처를 사용하는 방식에 따라 달라지며 캔버스 렌더링 컨텍스트에서 추론할 수 없습니다. 하지만 셰이더 프로그램에서 일반적인 모델 뷰 프로젝션을 사용하여 텍스처를 그리는 경우 새 편의 함수 element.getElementTransform()를 사용하여 drawElementImage()의 반환 값과 동일한 방식으로 사용할 수 있는 변환을 계산할 수 있습니다. 이를 위해 다음을 실행해야 합니다.

  • WebGL MVP MatrixDOM Matrix로 변환
  • HTML 요소를 정규화합니다. HTML 요소는 픽셀 단위로 크기가 지정됩니다 (예: 너비 200px). 하지만 WebGL은 일반적으로 객체를 0~1 범위의 '단위 정사각형'으로 취급합니다. 정규화하지 않으면 200px 버튼이 200배 더 크게 표시됩니다.
  • 캔버스 표시 영역에 매핑합니다. 이 단계는 '재조정' 단계입니다. 이러한 단위 공간 수학을 다시 늘려 화면에 있는 <canvas> 요소의 실제 픽셀 크기와 일치시킵니다. 또한 WebGL에서는 위가 양수이지만 CSS에서는 아래가 양수이므로 Y축이 반전됩니다.
  • 최종 변환을 계산합니다. 행렬을 순서대로 곱합니다. Viewport * MVP * Normalization. 이를 하나의 최종 변환으로 결합하면 브라우저가 3D 그림과 정렬하기 위해 HTML 요소 레이어가 정확히 어디에 있어야 하는지 알려주는 '지도'가 생성됩니다.
  • HTML 요소에 변환을 적용합니다. 이렇게 하면 HTML 요소 레이어가 렌더링된 픽셀 바로 위에 배치됩니다. 이렇게 하면 사용자가 버튼을 클릭하거나 텍스트를 선택할 때 실제 HTML 요소를 누르게 됩니다.
if (canvas.getElementTransform) {
  // 1. Convert WebGL MVP Matrix to DOM Matrix
  const mvpDOM = new DOMMatrix(Array.from(htmlElementMVP));

  // 2. Normalize the HTML element (pixels -> 1x1 unit square)
  const width = targetHTMLElement.offsetWidth;
  const height = targetHTMLElement.offsetHeight;

  const cssToUnitSpace = new DOMMatrix()
    .scale(1 / width, -1 / height, 1) // Shrink to unit size and flip Y
    .translate(-width / 2, -height / 2); // Center the element

  // 3. Map to the canvas viewport
  const clipToCanvasViewport = new DOMMatrix()
    .translate(canvas.width / 2, canvas.height / 2) // Move origin to center
    .scale(canvas.width / 2, -canvas.height / 2, 1); // Stretch to canvas dimensions

  // 4. Multiply: (Clip -> Pixels) * (MVP) * (pixels -> unit square)
  const screenSpaceTransform = clipToCanvasViewport
      .multiply(mvpDOM)
      .multiply(cssToUnitSpace);

  // 5. Apply to the transform
  const computedTransform = canvas.getElementTransform(targetHTMLElement, screenSpaceTransform);
  if (computedTransform) {
    targetHTMLElement.style.transform = computedTransform.toString();
  }
}

라이브러리 및 프레임워크 지원

인기 있는 라이브러리 중 일부는 이미 Canvas의 HTML 기능을 지원하고 있습니다.

Three.js

매트릭스를 수동으로 업데이트하는 것은 지루할 수 있으므로 프레임워크가 이미 참여하고 있습니다. Three.js에는 새로운 THREE.HTMLTexture를 사용하는 실험적 지원이 있습니다.

const material = new THREE.MeshBasicMaterial();
material.map = new THREE.HTMLTexture(uiElement); // Pass the DOM element

const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

PlayCanvas

PlayCanvas는 텍스처 API를 사용하여 HTML-in-Canvas도 지원합니다.

// Wait for the 'paint' event to set the source
canvas.addEventListener('paint', () => {
    htmlTexture.setSource(htmlElement);
}, { once: true });
canvas.requestPaint();

// Keep up to date
canvas.addEventListener('paint', onPaintUpload);

const material = new pc.StandardMaterial();
material.diffuseMap = htmlTexture;
material.update();

데모

데모를 사용해 보기 전에 환경이 올바르게 구성되어 있는지 확인하세요.

API 사용의 참조가 되는 여러 데모가 있습니다. 이미 커뮤니티에서 번역 가능한 3D 책부터 유리 셰이더를 통해 굴절되는 UI 요소에 이르기까지 다양한 창의적인 솔루션을 선보이고 있습니다.

  • 3D 책: 페이지에 HTML 레이아웃을 사용하는 WebGL 렌더링 3D 책입니다. 사용자는 CSS를 사용하여 글꼴을 바꿀 수 있습니다. DOM 기반이므로 내장 번역이 즉시 작동하며 AI 에이전트가 덜 복잡하게 텍스트를 추출할 수 있습니다.
  • 양방향 3D UI: 기본 3D 모델을 기반으로 빛을 굴절시키면서도 표준 HTML <input type="range"> 단계 속성에 응답하는 WebGPU 젤리 슬라이더
  • 애니메이션 텍스처: 맞춤 애니메이션 루프가 필요 없이 DOM을 사용하여 애니메이션 SVG 연필을 WebGL 텍스처로 직접 렌더링하는 동적 3D 광고판입니다.
  • 굴절 오버레이: 움직이는 3D 커서로 인해 왜곡되지만 페이지 내 찾기를 사용하여 완전히 선택하고 검색할 수 있는 대화형 서체 레이어입니다.

커뮤니티에서 만든 데모 모음을 확인하세요. HTML-in-Canvas 데모를 이 컬렉션에 포함하려면 pull 요청을 만들어 추가하세요.

제한사항

강력한 API이지만 몇 가지 의도적인 제한사항이 있습니다.

  • 크로스 오리진 콘텐츠: 보안 및 개인 정보 보호를 위해 API는 크로스 오리진 iframe 콘텐츠와 함께 작동하지 않습니다.
  • 기본 스레드 스크롤: HTML-in-canvas는 JavaScript로 그려지므로 캔버스 외부에서와 같이 스크롤과 애니메이션이 JavaScript와 독립적으로 업데이트될 수 없습니다. 개발자는 스크롤 콘텐츠를 캔버스 내부에 배치하는 것과 전체 캔버스를 스크롤하는 것의 성능 특성을 신중하게 고려해야 합니다.

의견

HTML-in-Canvas API를 실험하고 있다면 의견을 보내주세요. 실험 단계에 있는 동안 사이트에서 기능을 사용 설정하는 오리진 트라이얼에 가입하여 API 설계를 개선하는 데 도움을 줄 수 있습니다. 문제를 신고하여 의견을 제공할 수도 있습니다.

리소스