designpaca/research/canvas/01-api-spec.md
Yun Chan 8808c672dc designpaca 초기 구현 — 스킬 · 설치 CLI · 배포 파이프라인
웹 디자인 파이프라인 스킬과 이를 5개 에이전트에 설치하는 CLI 를 담은 모노레포.

스킬 (packages/skill)
- SKILL.md 261줄 + 참조 문서 16개 3,349줄. progressive disclosure 로
  본문은 절차와 인덱스만, 지식은 references/ 로 분리
- 0~6단계 파이프라인. 규모에 따라 전체·연장·국소 세 경로로 분기
- 하드 게이트 12개는 grep·카운트로 검증 가능한 것만. 취향 판단은 제외
- 미학 프리셋 5종, AI 슬롭 지문 목록, 한글 조판 규칙,
  SVG 필터·three.js·인터랙티브 모션·HTML-in-Canvas 실전 지침

설치 CLI (packages/cli, packages/core)
- npx designpaca 온보딩 TUI. Claude Code · Codex · Cursor · Windsurf · AGENTS.md
- 매니페스트에 설치 시점 해시를 기록해 사용자가 고친 파일은 update 가 건너뛴다
- 타깃별로 본문의 references/ 경로를 실제 설치 위치로 재작성
- AGENTS.md 는 항상 로드되므로 본문 대신 303자 포인터만 주입
- Windsurf 는 12,000자 상한 초과 시 설치를 차단

배포 (build/ci, .forgejo/workflows)
- 태그 v* → 검사·테스트·빌드 → npmjs 배포 + Forgejo 레지스트리 미러
  → draft 릴리스 → Cloudflare Pages. 재실행 멱등

근거 (research/)
- 약 250개 웹 소스 조사 결과와 도그푸딩 검증 2건. 스킬의 모든 수치는 여기서 나온다

테스트 22개 통과 (core 16 · cli 6)
2026-08-20 10:48:00 +09:00

23 KiB

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 (drawElementdrawHTMLElement)
2025-09-05 drawHTMLElementdrawHTML
2025-09-11 drawHTMLdrawElementImage현재 이름
2025-10-08 explainer에서 "place element" 표현 전부 제거 (저장소도 WICG/canvas-place-elementWICG/html-in-canvas. 옛 저장소는 현재 404)
2025-11-08 setHitTestRegions() 폐기 → "Switch to CSS transforms for hit testing" (반환된 DOMMatrixelement.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.

<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 원문 그대로)

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 원문)

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 = {});
};

호출:

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로 신·구 양쪽을 지원한다:
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 경로에서 텍스처 크기를 지정할 방법이 없다. configwidth/height가 추가된 배경이지만 논의는 진행 중.

5. WebGPU — copyElementImageToTexture()

5.1 현재 IDL (explainer 원문)

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 원문)

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

[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-renderingIntersectionObserver 단계가 실행된 직후. 설계 문서상으로는 "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()

void requestPaint();

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


7. OffscreenCanvas / Worker — captureElementImage() & ElementImage

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 컨텍스트용 동기화 헬퍼

DOMMatrix getElementTransform((Element or ElementImage) element, DOMMatrix drawTransform);
  • HTMLCanvasElementOffscreenCanvas에 있다. (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 블로그 원문 코드)

// 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가 명시.