# 01. HTML-in-Canvas API 정확한 스펙

> 조사 기준일: 2026-08-20
> 1차 출처: WICG 공식 explainer(living document), WHATWG HTML PR, Chrome Platform Status, Chrome for Developers 블로그, blink-dev Intent 스레드
> **이 문서의 모든 시그니처는 WICG explainer의 IDL 블록 원문에서 그대로 옮긴 것이다. 추측한 부분은 명시적으로 "미확인"으로 표시했다.**

---

## 0. 한 줄 요약

`<canvas layoutsubtree>` 안에 실제 HTML을 넣고, `paint` 이벤트 안에서 `ctx.drawElementImage(el, x, y)`(2D) / `gl.texElementImage2D(...)`(WebGL) / `device.queue.copyElementImageToTexture(...)`(WebGPU)를 호출하면, 그 HTML의 **살아있는 렌더링 결과**가 캔버스 픽셀(또는 GPU 텍스처)로 들어온다. 원본 요소는 DOM에 그대로 남아 클릭·포커스·접근성·find-in-page가 계속 동작한다.

## 1. 공식 명칭과 저장소

| 항목 | 값 |
|---|---|
| 기능 이름 | **HTML-in-canvas** (Chrome Platform Status 등록명) |
| 인큐베이션 | W3C WICG |
| Explainer 저장소 | `https://github.com/WICG/html-in-canvas` |
| Explainer 렌더링 | `https://wicg.github.io/html-in-canvas/` (형식 스펙이 아니라 explainer 그 자체) |
| 스펙 PR | `https://github.com/whatwg/html/pull/11588` — "Add HTML-in-Canvas APIs", 2025-08-21 개설, **2026-08 현재 open / 미머지** |
| chromestatus | `https://chromestatus.com/feature/5172548013916160` |
| Chromium 버그 | `https://crbug.com/500967896` (Blink 컴포넌트: `Blink>Canvas`) |
| 저자 | Philip Rogers, Stephen Chenney(Igalia), Chris Harrelson, Philip Jägenstedt, Khushal Sagar, Vladimir Levin, Fernando Serboncini |

### 1.1 이름 변천사 — 영상/구 자료의 이름이 지금과 다른 이유

노마드코더 영상 및 상당수의 블로그가 쓰는 `canvas place element` / `drawElement` / `setHitTestRegions()`는 **모두 옛 이름**이다. WICG 저장소 커밋 로그로 확인한 실제 변천:

| 날짜 | 변경 |
|---|---|
| ~2025-08 이전 | 제안 이름 `canvas place element`, 메서드 `drawElement()`, 히트테스트는 `setHitTestRegions()` |
| 2025-08-22 | 메서드 rename (`drawElement` → `drawHTMLElement`) |
| 2025-09-05 | `drawHTMLElement` → **`drawHTML`** |
| 2025-09-11 | `drawHTML` → **`drawElementImage`** ← **현재 이름** |
| 2025-10-08 | explainer에서 "place element" 표현 전부 제거 (저장소도 `WICG/canvas-place-element` → `WICG/html-in-canvas`. 옛 저장소는 현재 404) |
| 2025-11-08 | **`setHitTestRegions()` 폐기** → "Switch to CSS transforms for hit testing" (반환된 `DOMMatrix`를 `element.style.transform`에 넣는 방식으로 대체) |
| 2026-02-10~25 | `paint` 이벤트 / `onpaint` 설계 확정, 이벤트에서 `time` 인자 제거 |
| 2026-03-17~31 | `captureElementImage()` + `ElementImage` (OffscreenCanvas 지원) 추가 |
| 2026-03-20 | `drawElementImage()` 소스 사각형(sx/sy/swidth/sheight) 오버로드 추가 |
| 2026-04-16 / 06-01 | WebGL/WebGPU IDL 대폭 변경 (**아래 4·5절의 "구 시그니처" 주의사항 참조**) |
| 2026-06-16 | "privacy-preserving painting" → **"read-back-allowed rendering"** 으로 개념 이름 변경 |
| 2026-07-13/14 | 중첩 canvas 허용, `paint` 이벤트가 **역트리 순서**로 발화함을 명시 |

> ⚠️ WHATWG 스펙 PR #11588 본문에는 아직 `setHitTestRegions()`와 `drawable` 속성이 남아 있다. **explainer(=구현 기준)와 스펙 PR이 아직 동기화되지 않은 상태**이므로, 구현 기준은 explainer를 따라야 한다.

---

## 2. `layoutsubtree` 속성

**철자: 전부 소문자 `layoutsubtree`** (HTML 속성). IDL 반사 프로퍼티는 camelCase `layoutSubtree`.

```html
<canvas id="canvas" style="width:400px; height:200px" layoutsubtree>
  <form id="form_element">
    <label for="name">name:</label>
    <input id="name">
  </form>
</canvas>
```

- `boolean` 타입 → **불리언 속성**. 존재하기만 하면 true다. `layoutsubtree="true"`도 되고(WICG 예제가 이 형태를 쓴다) `layoutsubtree=""`도 된다. `layoutsubtree="false"`라고 써도 **true로 취급**되니 끄려면 속성 자체를 제거해야 한다.
- 효과 (explainer 원문 기준):
  - 캔버스 자손이 **레이아웃에 참여**하고 **히트테스트에 참여**한다.
  - `<canvas>`의 **직계 자식**은 stacking context를 생성하고, 모든 자손의 containing block이 되며, **paint containment**를 갖는다.
  - 자식은 "보이는 것처럼" 동작하지만, `drawElementImage()`로 명시적으로 그려지기 전까지 **사용자에게는 보이지 않는다**.
- 이 속성이 없으면 캔버스 자식은 종전대로 fallback 콘텐츠일 뿐이고, `drawElementImage()`는 예외를 던진다.

---

## 3. 2D 컨텍스트 — `drawElementImage()`

### 3.1 IDL (explainer 원문 그대로)

```webidl
interface mixin CanvasDrawElementImage {
  DOMMatrix drawElementImage((Element or ElementImage) element,
                             unrestricted double dx, unrestricted double dy);

  DOMMatrix drawElementImage((Element or ElementImage) element,
                             unrestricted double dx, unrestricted double dy,
                             unrestricted double dwidth, unrestricted double dheight);

  DOMMatrix drawElementImage((Element or ElementImage) element,
                             unrestricted double sx, unrestricted double sy,
                             unrestricted double swidth, unrestricted double sheight,
                             unrestricted double dx, unrestricted double dy);

  DOMMatrix drawElementImage((Element or ElementImage) element,
                             unrestricted double sx, unrestricted double sy,
                             unrestricted double swidth, unrestricted double sheight,
                             unrestricted double dx, unrestricted double dy,
                             unrestricted double dwidth, unrestricted double dheight);
};

CanvasRenderingContext2D includes CanvasDrawElementImage;
OffscreenCanvasRenderingContext2D includes CanvasDrawElementImage;
```

오버로드는 인자 개수 **3 / 5 / 7 / 9개** 네 가지다 — `(el, dx, dy)`, `(el, dx, dy, dw, dh)`, `(el, sx, sy, sw, sh, dx, dy)`, `(el, sx, sy, sw, sh, dx, dy, dw, dh)`. 기존 `CanvasRenderingContext2D.drawImage()`와 정확히 같은 모양이고, 첫 인자만 이미지 소스 대신 `Element`(또는 `ElementImage`)로 바뀐 것이다.

### 3.2 반환값

**`DOMMatrix`** — 이 행렬을 `element.style.transform = returned.toString()` 으로 적용하면, DOM 상의 요소 위치가 캔버스에 그려진 위치와 일치한다. 히트테스트·포커스 링·IntersectionObserver·접근성 좌표가 이 DOM 위치를 쓰기 때문에 **이 동기화를 하지 않으면 클릭이 엉뚱한 곳에 떨어진다.**

### 3.3 요구사항·제약 (explainer 원문 항목)

- 최근 렌더링 업데이트 시점에 `<canvas>`에 `layoutsubtree`가 지정되어 있어야 한다.
- `element`는 최근 렌더링 업데이트 시점에 `<canvas>`의 **직계 자식(direct child)** 이어야 한다.
- `element`가 **박스를 생성**해야 한다 (즉 `display:none` 이면 안 된다).
- **변환(Transforms)**: 캔버스의 현재 변환 행렬(CTM)은 그리기에 적용된다. 반면 **소스 `element`에 걸린 CSS transform은 그리기에서 무시된다.** (다만 히트테스트/접근성에는 계속 영향을 준다 — 그래서 3.2의 동기화가 성립한다.)
- **클리핑**: 넘치는 콘텐츠(layout overflow, ink overflow 모두)는 요소의 **border box로 클리핑**된다.
- **크기**: `width`/`height` 인자는 캔버스 좌표계의 목적지 사각형이다. 생략하면 **캔버스 밖에 있을 때와 같은 화면상 크기·비율**이 되도록 자동 사이징된다.
  → 실전 의미: `canvas.width`를 device pixel로 잡으면 x/y 좌표도 device pixel 단위여야 한다 (`x * devicePixelRatio`).

### 3.4 스냅샷 타이밍 (중요)

- 캔버스 모든 자식의 렌더링 스냅샷은 **`paint` 이벤트 직전**에 기록된다.
- `paint` 이벤트 **안에서** 호출하면 → **현재 프레임**의 모습으로 그려진다.
- `paint` 이벤트 **밖에서** 호출하면 → **직전 프레임**의 스냅샷이 쓰인다.
- 최초 스냅샷이 기록되기 전에 호출하면 **예외**가 발생한다 (Chromium 구현에서는 `InvalidStateError`).

---

## 4. WebGL — `texElementImage2D()`

### 4.1 현재 IDL (explainer 원문)

```webidl
dictionary WebGLCopyElementImageConfig {
  GLfloat sx;
  GLfloat sy;
  GLfloat swidth;
  GLfloat sheight;
  GLsizei width;
  GLsizei height;
};

partial interface WebGLRenderingContext {
  void texElementImage2D(GLenum target, GLenum internalformat,
                         (Element or ElementImage) element,
                         optional WebGLCopyElementImageConfig config = {});
};
```

호출:
```js
gl.texElementImage2D(gl.TEXTURE_2D, gl.RGBA8, element);
```

### 4.2 ⚠️ 시그니처가 2026-04~06에 바뀌었다

- **구 시그니처**: `texElementImage2D(target, level, internalformat, format, type, element)` — `texImage2D`와 똑같은 6인자.
- **신 시그니처**: `texElementImage2D(target, internalformat, element, config?)` — `level`, `format`, `type`이 사라졌다.
- Chrome for Developers 블로그(2026-05-19 최종 수정)의 코드 예제는 **아직 구 시그니처**(`gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, form_element)`)를 보여준다. 반면 WICG 공식 예제 `Examples/webGL.html`은 try/catch로 신·구 양쪽을 지원한다:

```js
try {
  gl.texElementImage2D(gl.TEXTURE_2D, gl.RGBA8, draw_element);      // 신
} catch (e) {
  gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,          // 구
                       gl.UNSIGNED_BYTE, draw_element);
}
```
→ **실전 코드에서는 이 try/catch 패턴을 그대로 쓰는 게 안전하다.**

### 4.3 알려진 미해결 이슈

- Jake Archibald가 blink-dev Intent 스레드에서 지적: **WebGL 경로에서 텍스처 크기를 지정할 방법이 없다.** `config`의 `width`/`height`가 추가된 배경이지만 논의는 진행 중.

---

## 5. WebGPU — `copyElementImageToTexture()`

### 5.1 현재 IDL (explainer 원문)

```webidl
dictionary GPUCopyElementImageDestination {
  required GPUImageCopyTextureTagged destination;
  GPUIntegerCoordinate width;
  GPUIntegerCoordinate height;
};

dictionary GPUCopyElementImageSource {
  required (Element or ElementImage) source;
  float sx;
  float sy;
  float swidth;
  float sheight;
};

partial interface GPUQueue {
  void copyElementImageToTexture(GPUCopyElementImageSource source,
                                 GPUCopyElementImageDestination destination);
}
```

### 5.2 실제 호출 형태 (WICG 젤리 슬라이더 데모 `Examples/webgpu-jelly-slider/src/index.ts` 원문)

```js
canvas.onpaint = () => {
  const sourceDict = { source: valueElement };
  const destDict = {
    destination: { texture: valueRawTexture },
    width: width,
    height: height
  };
  try {
    device.queue.copyElementImageToTexture(sourceDict, destDict);           // 신
  } catch (e) {
    device.queue.copyElementImageToTexture(valueElement, width, height,     // 구
                                           { texture: valueRawTexture });
    console.log('Note: using old copyElementImageToTexture API');
  }
  // ... transform 동기화
};
```

> ⚠️ Chrome 블로그는 `device.queue.copyElementImageToTexture(valueElement, { texture: targetTexture })` 라는 **또 다른(간략화된/구) 형태**를 보여준다. IDL과 WICG 데모 소스가 서로 일치하므로 **위 dictionary 2개 형태가 현재 기준**이다.

`copyExternalImageToTexture()`의 DOM 요소 버전이라고 생각하면 된다.

---

## 6. `paint` 이벤트

### 6.1 IDL

```webidl
[Exposed=Window]
interface PaintEvent : Event {
  constructor(DOMString type, optional PaintEventInit eventInitDict);
  readonly attribute FrozenArray<Element> changedElements;
};

dictionary PaintEventInit : EventInit {
  sequence<Element> changedElements = [];
};
```

`canvas.onpaint = fn` 또는 `canvas.addEventListener('paint', fn)` 둘 다 가능.

### 6.2 동작 규칙 (explainer 원문 기준)

- 캔버스 자식들의 **렌더링이 변했을 때** 발화한다 (포커스, 호버, 입력, CSS 애니메이션 등).
- 발화 시점: [update-the-rendering](https://html.spec.whatwg.org/#update-the-rendering) 중 **IntersectionObserver 단계가 실행된 직후**. 설계 문서상으로는 "Paint 단계 직후, 루프 없이 프레임당 1회"(explainer의 Option C).
- 이벤트는 **바뀐 자식들의 목록**(`changedElements`)을 담는다.
- 캔버스 자식의 **CSS transform 변경은 렌더링에서 무시**되므로, transform만 바꿔서는 다음 프레임에 `paint`가 발화하지 않는다. (→ 3.2의 동기화 코드가 무한 루프를 만들지 않는 이유)
- `paint` 안에서 한 **캔버스 드로잉 명령은 현재 프레임에 반영**되지만, `paint` 안에서 한 **DOM 변경은 다음 프레임부터** 반영된다.
- `<canvas>`가 여러 개면 `paint`는 **역트리 순서(reverse tree order)** 로 발화한다 → 자손이 조상보다 먼저 발화한다 (중첩 canvas 지원, 2026-07 추가).

### 6.3 `requestPaint()`

```webidl
void requestPaint();
```

자식이 하나도 안 바뀌어도 `paint`를 **한 번** 강제로 발화시킨다. explainer 표현으로 "`requestAnimationFrame()`과 유사". 매 프레임 갱신이 필요한 앱은 `onpaint` 핸들러 끝에서 `requestPaint()`를 다시 호출해 루프를 만든다. 또한 **최초 1회 호출해서 렌더 파이프라인을 킥스타트**해야 한다(첫 스냅샷 확보).

---

## 7. OffscreenCanvas / Worker — `captureElementImage()` & `ElementImage`

```webidl
partial interface HTMLCanvasElement {
  [CEReactions, Reflect] attribute boolean layoutSubtree;
  attribute EventHandler onpaint;
  void requestPaint();
  ElementImage captureElementImage(Element element);
  DOMMatrix getElementTransform((Element or ElementImage) element, DOMMatrix drawTransform);
};

partial interface OffscreenCanvas {
  DOMMatrix getElementTransform((Element or ElementImage) element, DOMMatrix drawTransform);
};

[Exposed=(Window,Worker), Transferable]
interface ElementImage {
  readonly attribute double width;
  readonly attribute double height;
  undefined close();
};
```

- `canvas.captureElementImage(element)` → 요소 렌더링의 **전송 가능한(Transferable) 스냅샷**.
- `postMessage(msg, [elementImage])`로 워커에 넘기고, 워커의 `OffscreenCanvasRenderingContext2D.drawElementImage(elementImage, x, y)`로 그린다.
- 워커에서 계산된 transform은 `postMessage`로 메인 스레드에 돌려보내 `element.style.transform`에 적용해야 한다. 위치가 동적이면 메인 스레드에서 미리 계산해 `ElementImage` 전송과 동시에 적용하는 편이 낫다.

---

## 8. `getElementTransform()` — 3D 컨텍스트용 동기화 헬퍼

```webidl
DOMMatrix getElementTransform((Element or ElementImage) element, DOMMatrix drawTransform);
```

- **`HTMLCanvasElement`와 `OffscreenCanvas`에 있다.** (Chrome 블로그 본문 산문이 `element.getElementTransform()`이라고 쓴 곳이 있는데, **블로그 자체의 코드 예제와 IDL 모두 `canvas.getElementTransform(el, matrix)`** 이므로 산문 쪽이 오기다.)
- WebGL/WebGPU에서는 요소의 최종 화면 위치를 셰이더가 결정하므로 `drawElementImage()`처럼 자동으로 알 수 없다. 그래서 개발자가 MVP 행렬을 스크린 스페이스 행렬로 변환해 넘겨주면, 이 메서드가 `style.transform`에 넣을 `DOMMatrix`를 돌려준다.

### 8.1 변환 공식 (explainer 원문)

```
T_origin⁻¹ · S_css→grid⁻¹ · T_draw · S_css→grid · T_origin
```
- `T_draw`: 캔버스 그리드 좌표계에서 요소를 그린 변환. `drawElementImage`의 경우 `CTM · T_(x,y) · S_(destScale)`.
- `T_origin`: 요소의 계산된 `transform-origin` 평행이동 행렬.
- `S_css→grid`: CSS 픽셀 → 캔버스 그리드 픽셀 스케일 행렬.

### 8.2 WebGL에서 screenSpaceTransform 만드는 절차 (Chrome 블로그 원문 코드)

```js
// 1. WebGL MVP → DOMMatrix
const mvpDOM = new DOMMatrix(Array.from(htmlElementMVP));

// 2. HTML 요소 정규화 (px → 1x1 단위 사각형, Y축 뒤집기)
const width  = targetHTMLElement.offsetWidth;
const height = targetHTMLElement.offsetHeight;
const cssToUnitSpace = new DOMMatrix()
  .scale(1 / width, -1 / height, 1)
  .translate(-width / 2, -height / 2);

// 3. 클립 공간 → 캔버스 뷰포트
const clipToCanvasViewport = new DOMMatrix()
  .translate(canvas.width / 2, canvas.height / 2)
  .scale(canvas.width / 2, -canvas.height / 2, 1);

// 4. 합성: (Clip→Pixels) * MVP * (px→unit)
const screenSpaceTransform = clipToCanvasViewport.multiply(mvpDOM).multiply(cssToUnitSpace);

// 5. 적용
const computedTransform = canvas.getElementTransform(targetHTMLElement, screenSpaceTransform);
if (computedTransform) targetHTMLElement.style.transform = computedTransform.toString();
```

---

## 9. 접근성·히트테스트가 유지되는 원리

핵심은 **"같은 요소가 두 곳에 존재"가 아니라 "요소는 DOM에 한 번만 존재하고, 캔버스에는 그 픽셀 복사본이 있다"** 는 것이다.

- 히트테스트, 포커스, 키보드 탭 순서, 텍스트 선택, 복사/붙여넣기, 우클릭 컨텍스트 메뉴, find-in-page, 번역, 리더 모드, 확장 프로그램, 브라우저 줌, 자동완성 — **전부 DOM 쪽이 처리한다.** 캔버스는 픽셀만 담당한다.
- `layoutsubtree`가 자식들을 **접근성 트리에 노출**시킨다. 기존 canvas fallback 콘텐츠와 달리, 그려진 내용과 접근성 트리가 **구조적으로 일치함이 보장**된다 (explainer가 명시한 주요 동기 중 하나).
- 단 **DOM 위치와 그려진 위치가 어긋나면 전부 어긋난다.** → `drawElementImage()`의 반환 `DOMMatrix`(또는 `getElementTransform()`)를 매 프레임 `style.transform`에 반영하는 것이 이 API의 필수 계약이다.
- DevTools에서 캔버스 안 HTML을 그대로 인스펙트/스타일 수정할 수 있고, 수정 즉시 텍스처에 반영된다.

---

## 10. 보안·프라이버시: "Read-back-allowed rendering"

(구 명칭 "privacy-preserving painting", 2026-06-16 개칭)

캔버스 픽셀은 `getImageData()`로 읽을 수 있고, WebGL/WebGPU에서는 항상 읽을 수 있다. 따라서 **저자 코드가 원래 볼 수 없던 정보는 애초에 그려지지 않는다.** 페인팅(픽셀 읽기·타이밍 공격)과 무효화(`onpaint` 발화 여부) **양쪽 모두**에서 민감 정보를 배제한다.

### 그려지지 않는(=민감) 정보

- **cross-origin 데이터**: `<iframe>`·`<img>` 등 embedded content의 교차 출처 콘텐츠, `url()` 참조(`background-image`, `clip-path`), 교차 출처로 오염(tainted)된 `<canvas>`, SVG의 `<use>`/`<pattern>`/`<feImage>`.
  → **same-origin iframe은 그려진다.** 그 안의 cross-origin 콘텐츠만 안 그려진다.
- 시스템 색상 / 테마 / 사용자 환경설정
- 맞춤법·문법 검사 밑줄 마커
- **방문한 링크 정보(`:visited`)** — 히스토리 스니핑 방지
- JS로 접근 불가한 대기 중 폼 자동완성 정보
- 서브픽셀 텍스트 안티에일리어싱
- 캡션/자막 선택 및 외형에 대한 사용자 설정
- IME 팝업 및 IME 고유 텍스트 서식

### 민감하지 않다고 판정된(=그려지는) 새 정보

- find-in-page 검색어 하이라이트, text-fragment(URL 프래그먼트) 마커
- 스크롤바 및 폼 컨트롤 외형 (Blink/WebKit에서 이미 `foreignObject`로 탐지 가능)
- 캐럿 깜빡임 속도
- `forced-colors` (이미 미디어 쿼리 + 시스템 색상으로 JS에서 알 수 있음)

> blink-dev Intent에 명시된 잔여 리스크: "이 API는 그라디언트 픽셀, 폼 컨트롤 렌더링 등 **소량의 새 정보를 노출**하며 이는 상호운용성 리스크를 만든다." Mozilla의 우려도 주로 이 핑거프린팅 지점이다.

---

## 11. 기타 확인된 제약

| 제약 | 내용 | 출처 |
|---|---|---|
| cross-origin iframe | 지원 안 함 (위 10절) | Chrome 블로그 "Limitations" |
| 메인 스레드 스크롤 | 캔버스 내부 콘텐츠는 JS로 그려지므로 **스크롤·애니메이션이 JS와 독립적으로 갱신될 수 없다.** 컴포지터 스레드 스크롤의 이점을 잃는다. 캔버스 안에 스크롤 콘텐츠를 넣을지, 캔버스 전체를 스크롤시킬지 신중히 판단하라. | Chrome 블로그 "Limitations" |
| 중첩 canvas | explainer는 자손을 조상 캔버스에 그리는 것을 허용하지만, **Chromium Canary 구현은 가장 가까운 canvas 조상으로 제한**한다 (스펙 PR #11588에서 미해결 논의 중) | whatwg/html#11588 |
| ElementImage 교차 캔버스 | `ElementImage`를 만든 캔버스 외의 캔버스에 그리는 것을 제한할지 논의 중 (접근성·래스터화 복잡도 vs 오래된 스냅샷 문제) | whatwg/html#11588 |
| GC 압박 | dictionary 기반 API 설계 때문에 애니메이션 중 잦은 GC가 발생한다는 리뷰 지적 | whatwg/html#11588 |
| 크기/리사이즈 | 커뮤니티 공통 지적: "이 API에서 크기와 리사이즈가 유일하게 덜 익은 부분". `<canvas>`는 div처럼 `width:100%`가 기본이 아니고 콘텐츠에 따라 높이가 자라지도 않는다. | Frontend Masters (Amit Sheen, 2026-04-21) |
| 첫 스냅샷 전 호출 | `InvalidStateError` throw. 반드시 `onpaint` 안에서 그리고, `requestPaint()`로 킥스타트 | explainer + Matt Rothenberg |
| 캔버스 자식 크기 | 캔버스 자식의 크기가 캔버스 CSS 크기와 불일치하면 텍스처가 늘어나고 좌표가 페이지 아래로 갈수록 누적 오차 | Matt Rothenberg 실전 노트 |
| 반투명 배경 | 반투명 input 배경은 셰이더 효과가 비쳐 나오므로 불투명 색을 쓸 것 | Matt Rothenberg 실전 노트 |
| WebGL Y축 | 텍스처는 top-down, WebGL은 bottom-up → UV에서 Y 뒤집기 필요 | Matt Rothenberg 실전 노트 |
| 전체 화면 후처리 | 페이지 전체에 후처리를 걸면 오히려 접근성 이점을 훼손할 수 있다 | Codrops |

---

## 12. 향후 방향 (explainer "Future considerations")

**Auto-updating canvas 모드**: `drawElementImage`가 "최신 렌더링을 가리키는 플레이스홀더"를 기록하고, 캔버스가 커맨드 버퍼를 보관해 스크롤/애니메이션 갱신마다 자동 재생하는 모드. 스크립트를 블로킹하지 않고 컴포지터 스레드 스크롤·애니메이션과 완벽히 동기화되는 효과를 가능하게 한다. 2D 컨텍스트에는 실현 가능, WebGPU도 소폭 API 추가로 가능할 것으로 보고 있다. **WebGL은 `getError()` 등 플러시가 필요한 API 때문에 이 모델이 근본적으로 불가**하다고 explainer가 명시.
