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)
This commit is contained in:
commit
8808c672dc
135 changed files with 38838 additions and 0 deletions
397
research/canvas/01-api-spec.md
Normal file
397
research/canvas/01-api-spec.md
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
# 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가 명시.
|
||||
Loading…
Add table
Add a link
Reference in a new issue