feat(skill): route fixes, delivery report, composition diversity, platform refresh
- route table: extension route for products with an existing visual language, feedback row, derived state tokens stay local - completion: blocking 0 plus reported unverified; hard-gate surface is the touched scope; delivery report starts with what and how to open - design.md created at step 0 with a skeleton, fingerprint line and change log; product-surface rubric reading; direction delegation states the chosen card and reason - layout: hero families, nav forms, footer forms; art-direction page convergence 6 axes; genre sections; image treatments; placeholder conventions; multi-screen order; Tailwind theme mapping - platform: scroll-driven examples, anchor positioning, canvas OT, three/R3F pins, shadcn style prefixes, Safari 26 notes, subgrid - design-gate: four result states, exit codes, cwd module resolution, --url/--out-dir/--strict, oklch contrast, dead selector word match - Korean font budget split from the Latin baseline
This commit is contained in:
parent
9bcb45cbd3
commit
88c11682b1
51 changed files with 5834 additions and 2259 deletions
|
|
@ -2,6 +2,18 @@
|
|||
|
||||
Chrome의 HTML-in-Canvas API로 **실제 HTML 요소를 캔버스 픽셀/GPU 텍스처로 그린다.** 요소는 DOM에 남아 클릭·포커스·접근성이 유지된다. 구현 직전에 읽는 문서다.
|
||||
|
||||
## 이 문서를 읽는 법
|
||||
|
||||
| 상황 | 읽을 곳 |
|
||||
|---|---|
|
||||
| **쓸지 아직 안 정했다** | **§1만.** 세 관문 중 하나라도 걸리면 "쓰지 않는다"로 끝난다 |
|
||||
| 지금 브라우저에서 되는지 | §2 — 읽는 시점에 다시 확인 |
|
||||
| 어떤 경로로 구현하나 | §3 → §7 (b) 권장 기본 경로 |
|
||||
| API 이름·시그니처 | §4 (낡은 이름 목록 포함) |
|
||||
| 깨지지 않게 지킬 것 | §5 필수 계약 3가지, §6 그리지 않는 것 |
|
||||
| 복사해 시작할 코드 | §7 (a)~(c). CDN을 쓰면 (b)의 공급망 규칙을 먼저 |
|
||||
| 구현을 마무리할 때 | §8 접근성 자기 점검, §9 흔한 실패. 납품 판정은 [preflight.md](preflight.md) X5 |
|
||||
|
||||
## 1. 발동 조건 게이트
|
||||
|
||||
**기본값은 "쓰지 않는다".** 세 관문을 전부 통과할 때만 진행한다.
|
||||
|
|
@ -37,15 +49,16 @@ Chrome의 HTML-in-Canvas API로 **실제 HTML 요소를 캔버스 픽셀/GPU 텍
|
|||
|
||||
## 2. 가용성 — 읽는 시점에 반드시 확인
|
||||
|
||||
| 항목 | 값 |
|
||||
| 항목 | 값 (chromestatus 확인 2026-10-06) |
|
||||
|---|---|
|
||||
| 플래그 | `chrome://flags/#canvas-draw-element` → Enabled |
|
||||
| 권장 브라우저 | Chrome Canary 149+ |
|
||||
| Origin Trial | Chrome 148 ~ **154**. **2026년 10월 초 만료 예정** |
|
||||
| Stable 기본 활성화 | **없음.** chromestatus 상태는 `In development` |
|
||||
| Firefox / Safari | 구현 없음, 입장 미표명 |
|
||||
| Origin Trial | Chrome 148 ~ **160**(두 차례 연장) |
|
||||
| Stable 기본 활성화 | **아직 없음.** 상태 표시는 `In development`다. Chrome **157** 출하 절차(Intent to Ship)가 올라가 있고 M157 안정판 예정일은 2026-11-03이다. 출하 전에는 일정이 밀리거나 취소될 수 있다 |
|
||||
| Safari | 입장 Positive, 초기 구현 착수. 출하 버전 없음 |
|
||||
| Firefox | 구현 없음, 입장 미표명 |
|
||||
|
||||
**OT 만료 후에는 플래그 전용으로 되돌아간다.** 일반 사용자에게는 아무것도 보이지 않고 **폴백이 곧 실제 결과물이 된다.** 2026-10 이후에 읽고 있다면 `chromestatus.com/feature/5172548013916160`에서 연장/출시 여부를 먼저 확인하라.
|
||||
**출하 전까지는 OT 토큰이나 플래그 없이는 아무것도 보이지 않는다. 출하된 뒤에도 Safari·Firefox 사용자에게는 폴백이 곧 실제 결과물이다.** 관문 3은 출하와 무관하게 유지한다 — 엔진 하나만 지원하는 기능을 일반 사용자 대상 프로덕션의 구조로 쓰지 않는다. 읽는 시점에 `chromestatus.com/feature/5172548013916160`과 `chrome://version`으로 출하 여부를 다시 확인한다.
|
||||
|
||||
## 3. 권장 경로 — 폴리필 우선
|
||||
|
||||
|
|
@ -92,7 +105,7 @@ el.style.transform = t.toString(); // 이 줄이 없으면 클릭이 전부
|
|||
|
||||
### ② WebGL/WebGPU는 try/catch로 신·구 시그니처를 모두 지원한다
|
||||
|
||||
2026년 상반기에 두 시그니처가 모두 바뀌었다. **Chrome 공식 블로그 예제가 이미 구버전이다.** WICG 공식 데모조차 양쪽을 지원한다.
|
||||
2026년 상반기에 두 시그니처가 모두 바뀌었다. **Chrome 공식 블로그 예제가 이미 구버전이다.** WICG 공식 데모조차 양쪽을 지원한다. 출하판에서 구 시그니처가 사라졌는지 확인한 뒤에만 `catch` 분기를 지운다.
|
||||
|
||||
```js
|
||||
try { gl.texElementImage2D(gl.TEXTURE_2D, gl.RGBA8, el); } // 현재
|
||||
|
|
@ -184,13 +197,22 @@ catch (e) { device.queue.copyElementImageToTexture(el, width, height, { texture
|
|||
|
||||
3D 씬 안에 상호작용 UI를 넣는 경우.
|
||||
|
||||
**공급망 규칙.** 운영 배포는 npm으로 설치해 번들하거나 파일을 self-host하는 것이 기본이다. 프로토타입에서 CDN을 쓰더라도 **모든 URL에 정확한 버전을 고정**한다 — 버전 없는 URL은 패키지의 새 버전이 나오는 순간 사용자 사이트에서 그대로 실행된다. 그리고 import map의 `integrity` 필드로 SRI 해시를 단다. `integrity`를 모르는 브라우저는 그 필드를 무시하므로, 무결성이 꼭 필요하면 self-host가 가장 확실하다. 해시는 고정한 URL의 실제 파일로 계산한다: `curl -sL <URL> | openssl dgst -sha384 -binary | openssl base64 -A`.
|
||||
|
||||
```html
|
||||
<!-- 아래 <버전>·<해시>는 채택 시점에 확인해 채운다. 비워 둔 채 배포하지 않는다 -->
|
||||
<script type="importmap">
|
||||
{ "imports": {
|
||||
{
|
||||
"imports": {
|
||||
"three": "https://unpkg.com/three@0.184.0/build/three.module.js",
|
||||
"three/addons/": "https://unpkg.com/three@0.184.0/examples/jsm/",
|
||||
"three-html-render/polyfill": "https://cdn.jsdelivr.net/npm/three-html-render/dist/polyfill.mjs"
|
||||
}}
|
||||
"three-html-render/polyfill": "https://cdn.jsdelivr.net/npm/three-html-render@<버전>/dist/polyfill.mjs"
|
||||
},
|
||||
"integrity": {
|
||||
"https://unpkg.com/three@0.184.0/build/three.module.js": "sha384-<해시>",
|
||||
"https://cdn.jsdelivr.net/npm/three-html-render@<버전>/dist/polyfill.mjs": "sha384-<해시>"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
|
|
@ -204,7 +226,7 @@ if (!('requestPaint' in HTMLCanvasElement.prototype)) {
|
|||
}
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(devicePixelRatio);
|
||||
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); // three.md §8 DPR 클램프
|
||||
renderer.setSize(innerWidth, innerHeight);
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
|
|
@ -217,8 +239,13 @@ scene.background = new THREE.Color(0x0b0b10);
|
|||
const element = document.createElement('div');
|
||||
element.style.cssText = 'width:600px;padding:30px;background:#12121b;color:#eaeaf2;'
|
||||
+ 'font:28px/1.5 system-ui,sans-serif;text-align:center';
|
||||
element.innerHTML = '진짜 HTML 입니다. 선택·복사·검색이 됩니다.'
|
||||
+ '<br><input type="text" placeholder="입력해 보세요"> <button>보내기</button>';
|
||||
// innerHTML 문자열 대신 DOM API로 만든다 — 나중에 CMS·사용자 값이 들어와도 마크업으로 해석되지 않는다
|
||||
const note = document.createElement('p');
|
||||
note.textContent = '진짜 HTML 입니다. 선택·복사·검색이 됩니다.';
|
||||
const input = Object.assign(document.createElement('input'), { type: 'text', placeholder: '입력해 보세요' });
|
||||
input.setAttribute('aria-label', '메시지');
|
||||
const send = Object.assign(document.createElement('button'), { type: 'button', textContent: '보내기' });
|
||||
element.append(note, input, ' ', send);
|
||||
|
||||
const material = new THREE.MeshBasicMaterial(); // 조명 없이 텍스트 가독성 유지
|
||||
material.map = new THREE.HTMLTexture(element); // ← 핵심 한 줄
|
||||
|
|
@ -320,17 +347,17 @@ HIC.mount(document.getElementById('canvas'), {
|
|||
});
|
||||
```
|
||||
|
||||
## 8. 접근성 체크리스트 — 캔버스를 얹은 뒤 매번 확인한다
|
||||
## 8. 접근성 체크리스트 — 캔버스를 얹은 뒤 매번 확인한다 (구현 중 자기 점검 — 납품 판정은 preflight.md X5)
|
||||
|
||||
- [ ] **캔버스를 지워도 콘텐츠가 보이는가.** DevTools에서 `<canvas>`를 삭제해도 페이지가 읽히고 동작해야 한다.
|
||||
- [ ] **Tab 순서가 시각 순서와 일치하는가.** 캔버스 자식의 DOM 순서가 곧 탭 순서다.
|
||||
- [ ] **포커스 링이 보이고 위치가 맞는가.** 셰이더가 얇은 아웃라인을 뭉갠다. `:focus-visible`에 두꺼운 링을 주고, 키보드로 이동하며 링 위치가 그려진 픽셀과 맞는지 눈으로 확인한다.
|
||||
- [ ] **`prefers-reduced-motion: reduce`에서 모션을 끈다.** 왜곡·회전·진동은 정지 상태로 폴백한다.
|
||||
- [ ] **왜곡 중에는 클릭이 어긋난다.** 큰 전환 동안 `pointer-events: none`을 걸거나 왜곡량을 작게 유지한다.
|
||||
- [ ] **한글 입력을 캔버스 안에서 요구하지 않는다.** IME 조합 상태가 그려지지 않는다(§6).
|
||||
- [ ] **텍스트 대비를 셰이더 적용 후에 측정한다.** 블렌딩·색수차가 대비를 떨어뜨린다.
|
||||
- [ ] **히트테스트가 필요 없는 장식 요소는 `inert`.** WICG 공식 예제도 그렇게 한다.
|
||||
- [ ] **스크린리더로 한 번 통과시킨다.** 자식은 접근성 트리에 그대로 노출되므로, 이상하면 HTML 구조가 이상한 것이다.
|
||||
납품 판정은 [preflight.md](preflight.md) X5가 하고, 감소 모션은 A7, 포커스 표시는 A2, Tab 순서는 A1이 판정한다. 여기에는 캔버스에서만 생기는 점검을 둔다.
|
||||
|
||||
- **캔버스를 지워도 콘텐츠가 보이는가.** DevTools에서 `<canvas>`를 삭제해도 페이지가 읽히고 동작해야 한다.
|
||||
- **포커스 링 위치가 그려진 픽셀과 맞는가.** 셰이더가 얇은 아웃라인을 뭉갠다. `:focus-visible`에 두꺼운 링을 주고, 키보드로 이동하며 링 위치를 눈으로 확인한다. 캔버스 자식의 DOM 순서가 곧 탭 순서다.
|
||||
- **왜곡 중에는 클릭이 어긋난다.** 큰 전환 동안 `pointer-events: none`을 걸거나 왜곡량을 작게 유지한다.
|
||||
- **한글 입력을 캔버스 안에서 요구하지 않는다.** IME 조합 상태가 그려지지 않는다(§6).
|
||||
- **텍스트 대비를 셰이더 적용 후에 측정한다.** 블렌딩·색수차가 대비를 떨어뜨린다.
|
||||
- **히트테스트가 필요 없는 장식 요소는 `inert`.** WICG 공식 예제도 그렇게 한다.
|
||||
- **스크린리더로 한 번 통과시킨다.** 자식은 접근성 트리에 그대로 노출되므로, 이상하면 HTML 구조가 이상한 것이다.
|
||||
|
||||
## 9. 흔한 실패와 원인
|
||||
|
||||
|
|
@ -347,4 +374,4 @@ HIC.mount(document.getElementById('canvas'), {
|
|||
| 3D 텍스트가 뭉개진다 | mipmap 대신 `gl.LINEAR` 필터를 쓴다 |
|
||||
| iframe/이미지가 검게 나온다, 캔버스가 콘텐츠 높이만큼 안 자란다 | 각각 cross-origin(§6), `<canvas>`는 div가 아니므로 크기 명시 |
|
||||
|
||||
> 근거: research/canvas/01-api-spec.md, 02-availability.md (조사일 2026-08-20)
|
||||
> 근거: designpaca 저장소의 HTML-in-Canvas 조사 노트(API 명세·가용성, 조사일 2026-08-20). 설치본에는 포함되지 않으며, 이 문서의 결론만으로 작업할 수 있게 썼다.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue