designpaca/research/svg/01-primitives-reference.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

865 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 01. SVG 필터 프리미티브 전수 레퍼런스
> designpaca 스킬용 실전 레퍼런스. 모든 기본값·값 범위는 W3C SVG 1.1 / Filter Effects Module Level 1 스펙과 MDN 기준.
> "검증됨" 표시는 Chromium 151(Playwright)에서 실제 렌더링으로 확인한 항목이다. 그 외 엔진(Firefox / WebKit)은 별도 표기.
---
## 0. 실행 모델부터 이해하기
SVG 필터는 **이미지 처리 파이프라인**이다. CSS `filter: blur(4px)`처럼 "효과 하나"가 아니라, 노드 그래프를 직접 배선하는 도구다.
```
SourceGraphic ─┬─→ [feGaussianBlur] → result="blur" ─┐
│ ├─→ [feBlend] → 최종 출력
└──────────────────────────────────────┘
```
### 0.1 배선 규칙 (`in` / `in2` / `result`)
| 규칙 | 내용 |
|---|---|
| `in` 생략 시 | **첫 번째** 프리미티브면 `SourceGraphic`, 그 외에는 **직전 프리미티브의 출력** |
| `in2` | 입력 2개가 필요한 프리미티브(`feBlend`, `feComposite`, `feDisplacementMap`, `feTile`은 예외적으로 `in`만)에서 두 번째 입력 |
| `result` | 이 프리미티브의 출력에 이름을 붙여 뒤에서 재사용. **이름을 안 붙이면 직전 것만 참조 가능** |
| 출력 개수 | 프리미티브당 항상 **1개** |
> **함정 1**: `result`를 붙여 놓고도 다음 프리미티브에 `in`을 안 쓰면, 어차피 직전 출력이 들어가서 "왜 배선이 무시되지?"가 된다. 분기(branch)를 만들 때는 **양쪽 모두 `in`을 명시**하라.
### 0.2 소스 키워드
| 키워드 | 의미 | 실전 사용 가능? |
|---|---|---|
| `SourceGraphic` | 필터가 적용된 원본 그래픽 (RGBA 전체) | ✅ |
| `SourceAlpha` | 원본의 **알파 채널만** (RGB는 전부 0=검정) | ✅ 그림자·아웃라인·범프맵의 기본 재료 |
| `BackgroundImage` | 요소 뒤 배경 | ❌ **어떤 브라우저도 구현 안 함.** `backdrop-filter`로 대체 |
| `BackgroundAlpha` | 배경의 알파 | ❌ 미구현 |
| `FillPaint` | 대상의 `fill` 페인트로 무한 평면 채움 | ❌ 사실상 신뢰 불가 (엔진별 해석 불일치) |
| `StrokePaint` | 대상의 `stroke` 페인트로 무한 평면 채움 | ❌ 사실상 신뢰 불가 |
> **함정 2**: 튜토리얼에서 `BackgroundImage`를 봤다면 2010년대 초 문서다. 유리/글래스 효과는 `backdrop-filter: url(#id)`로 간다(§03 참고).
### 0.3 필터 영역 (filter region) — 가장 흔한 버그의 원인
```xml
<!-- 기본값 -->
<filter id="f" x="-10%" y="-10%" width="120%" height="120%"
filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse">
```
필터 영역 밖은 **잘려 나간다**. 기본값은 바운딩 박스보다 사방 10%씩만 넓으므로, `stdDeviation`이 크거나 `feOffset`/`feMorphology`로 밀어내면 잘린 사각형이 그대로 보인다.
**경험칙**
| 상황 | 권장 영역 |
|---|---|
| 블러 `stdDeviation` ≤ 5 | 기본값(120%)으로 충분 |
| 블러 6~15, 글로우, 아웃라인 | `x="-30%" y="-30%" width="160%" height="160%"` |
| 다중 드롭섀도(네온), 큰 변위 | `x="-40%" y="-40%" width="180%" height="180%"` |
| 노이즈/그레인 오버레이처럼 **영역을 넓히면 안 되는** 경우 | `x="0%" y="0%" width="100%" height="100%"` (성능도 이득) |
> 필터 영역을 넓히면 **처리 픽셀 수가 제곱으로 증가**한다. 180% × 180% = 원본의 3.24배. 필요한 만큼만 넓혀라. (검증됨 — §02 레시피 18번 비교 데모)
### 0.4 좌표계: `filterUnits` vs `primitiveUnits`
| 속성 | 기본값 | 영향 대상 |
|---|---|---|
| `filterUnits` | `objectBoundingBox` | `<filter>``x/y/width/height` |
| `primitiveUnits` | `userSpaceOnUse` | 각 프리미티브의 `x/y/width/height` **그리고 `stdDeviation`, `radius`, `dx/dy`, `scale` 같은 길이값** |
- `objectBoundingBox` → 0~1 또는 % 로 해석. `0.05` = 바운딩 박스 크기의 5%.
- `userSpaceOnUse` → 현재 사용자 좌표계의 절대 길이(대개 px).
```xml
<!-- 요소 크기에 비례하는 블러 (반응형 컴포넌트에 유용) -->
<filter id="relBlur" primitiveUnits="objectBoundingBox">
<feGaussianBlur stdDeviation="0.05"/> <!-- 박스 폭의 5% -->
</filter>
```
> 검증됨: 90×90px 박스에 `primitiveUnits="objectBoundingBox"` + `stdDeviation="0.05"` → 약 4.5px 블러.
> **함정 3**: `primitiveUnits="objectBoundingBox"`로 바꾸면 `stdDeviation="10"`이 "박스 10배 크기 블러"가 되어 화면이 통째로 사라진다. 단위를 바꾸면 **모든 수치를 다시 계산**해야 한다.
> **함정 4 (HTML 요소)**: `filter: url(#f)`를 HTML 요소에 걸면 바운딩 박스는 **border box** 기준이다. `objectBoundingBox`를 쓰는 필터를 SVG용으로 만들어놓고 HTML에 재활용하면 크기가 달라진다.
### 0.5 색공간: `color-interpolation-filters`
**SVG 필터의 기본 연산 색공간은 `linearRGB`다.** CSS의 `filter: blur()` 같은 단축 함수는 `sRGB`다. 이 불일치가 "같은 값인데 왜 다르게 보이지?"의 정체다.
검증된 실측 (Chromium, `#e11d48` 사각형에 `feColorMatrix type="saturate" values="0"`):
| 설정 | 결과 |
|---|---|
| 기본(linearRGB) | 밝은 회색 (≈ `#808080`) |
| `color-interpolation-filters="sRGB"` | 어두운 회색 (≈ `#4a4a4a`, 계산값 R=G=B=74) |
같은 조건의 `feGaussianBlur stdDeviation="10"`:
- linearRGB → 헤일로가 **밝고 넓게** 번짐(물리적으로 정확한 빛의 합성)
- sRGB → 헤일로가 **좁고 진함**(포토샵/CSS와 동일한 룩)
**실전 지침**
```xml
<!-- 디자인 툴/CSS와 색을 맞추고 싶으면 항상 filter에 sRGB를 명시 -->
<filter id="f" color-interpolation-filters="sRGB"> ... </filter>
```
| 케이스 | 권장 |
|---|---|
| 듀오톤, 그라디언트 맵, 브랜드 컬러 정확도 | **반드시 `sRGB`** |
| 알파 대비 트릭(gooey), 합성 위주 | `sRGB` (예측 가능) |
| 조명(`feDiffuseLighting`/`feSpecularLighting`)에서 물리적 자연스러움 | linearRGB 기본이 더 그럴듯할 때도 있음 — 둘 다 보고 고른다 |
| feTurbulence 노이즈 자체 | 어느 쪽이든 무방, 단 후속 대비 조정 결과가 달라짐 |
> **함정 5**: `color-interpolation-filters`는 개별 프리미티브에도 걸 수 있다. 체인 중간에서 색공간이 바뀌면 브라우저가 변환을 삽입한다 → 비용 발생 + 예측 어려움. **`<filter>` 한 곳에만 지정**하는 것이 안전하다.
### 0.6 필터 붙이는 법
```html
<!-- 1. SVG 요소에 (프레젠테이션 어트리뷰트) -->
<circle filter="url(#f)" .../>
<!-- 2. HTML 요소에 (CSS) -->
<div style="filter: url(#f)"></div>
<!-- 3. 배경에 (Chromium 계열만) -->
<div style="backdrop-filter: url(#f)"></div>
```
필터 정의를 담는 SVG 컨테이너 패턴:
```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<defs>
<filter id="myFilter"> ... </filter>
</defs>
</svg>
```
> 검증됨: Chromium에서는 `<svg style="display:none">` 안에 정의한 필터도 정상 동작했다. 하지만 과거 엔진 이슈 보고가 있고 Firefox/WebKit은 이 세션에서 미검증이므로, **`width=0 height=0 + position:absolute` 패턴을 기본값으로 쓴다.**
> 검증됨: 존재하지 않는 필터를 참조하면(`filter:url(#없음)`) Chromium은 **필터를 무시하고 원본을 그대로 렌더**한다(CSS·SVG 어트리뷰트 양쪽 모두). 현행 Filter Effects L1과 일치. 다만 구 SVG 1.1은 "요소를 렌더하지 않음"으로 정의했었으므로, 레거시 엔진에서 요소가 사라질 수 있다 — **ID 오타는 치명적일 수 있다.**
---
## 1. 소스 생성 프리미티브
입력 없이 새 이미지를 만들어내는 것들. 필터 그래프의 "재료".
---
### 1.1 `<feTurbulence>` — 절차적 노이즈
가장 강력하고, 가장 비싼 프리미티브. Perlin 노이즈를 생성한다.
| 속성 | 기본값 | 범위/타입 | 의미 |
|---|---|---|---|
| `type` | `turbulence` | `turbulence` \| `fractalNoise` | `turbulence`=결·물결·대리석, `fractalNoise`=구름·그레인 |
| `baseFrequency` | `0` | 0 이상 실수, `x y` 2개 가능 | **노이즈의 촘촘함.** 작을수록 큰 무늬 |
| `numOctaves` | `1` | 양의 정수 | 겹칠 주파수 층 수. 디테일↑ 비용↑ |
| `seed` | `0` | 정수(소수는 0 방향으로 절삭) | 난수 시드 |
| `stitchTiles` | `noStitch` | `noStitch` \| `stitch` | 타일 경계 이음매 제거 |
**`baseFrequency` 실전 값 지도**
| 값 | 결과 | 용도 |
|---|---|---|
| `0.002 ~ 0.01` | 거대한 유기적 덩어리 | 액체 배경, 오로라, 마블 |
| `0.01 ~ 0.03` | 큰 파형 | 손그림 왜곡, 물결 텍스트 |
| `0.03 ~ 0.08` | 중간 결 | 종이 질감, 잉크 번짐, 러프 엣지 |
| `0.1 ~ 0.3` | 촘촘한 결 | 천·직물, 얕은 디스토션 |
| `0.6 ~ 0.95` | 픽셀 단위 그레인 | **필름 그레인 / 노이즈 오버레이** |
| `> 1.0` | 에일리어싱 발생, 무의미 | ❌ |
**2값 형식**: `baseFrequency="0.008 0.05"` → x축은 큰 파형, y축은 촘촘 → **가로로 늘어진 결**. 물결·글리치 스캔라인에 필수.
**`numOctaves` 실전**: 1=밋밋, 2~3=대부분의 경우 최적, 4~5=거친 종이, **5 초과는 시각적 이득이 거의 없고 비용만 늘어난다.**
**함정**
1. **`baseFrequency`의 기본값은 `0`** = 아무것도 안 나온다. 반드시 지정하라.
2. **알파 채널도 노이즈다.** feTurbulence 출력은 RGB뿐 아니라 A도 랜덤이라 그냥 보면 반투명 컬러 스노우다. 불투명 그레이 노이즈가 필요하면 `feColorMatrix`로 알파를 1로 고정하라:
```xml
<feColorMatrix type="matrix" values="
0.33 0.33 0.33 0 0
0.33 0.33 0.33 0 0
0.33 0.33 0.33 0 0
0 0 0 0 1"/> <!-- 마지막 행: A = 1 상수 -->
```
3. **특정 seed 값에서 사각형 아티팩트**가 나온다. 보고된 값: `514, 1977, 2337, 4777, 8032, 9615` 등. 스펙 참조 구현의 정수 나눗셈 버그라 Chrome·Firefox·Batik이 동일하게 재현한다. → **seed는 1~50 같은 작은 값에서 눈으로 확인하고 고정하라.**
4. **CPU 비용이 가장 크다.** `baseFrequency`를 애니메이션하면 매 프레임 전체 노이즈를 재생성한다. §03 참고.
5. `stitchTiles="stitch"`는 **타일링해서 배경으로 쓸 때만** 의미가 있다. `feTile`이나 CSS `background-repeat`와 조합할 때 켜라.
---
### 1.2 `<feFlood>` — 단색 채우기
| 속성 | 기본값 | 의미 |
|---|---|---|
| `flood-color` | `black` | 채울 색. **CSS 프로퍼티이기도 하다 → CSS/SMIL로 애니메이션 가능** |
| `flood-opacity` | `1` | 0~1 |
필터 **서브영역 전체**를 색으로 채운다. 단독으로는 쓸모없고 `feComposite operator="in"`으로 마스킹해서 쓴다.
```xml
<!-- 알파 모양대로 특정 색 칠하기 = 실루엣 -->
<feFlood flood-color="#22d3ee" result="col"/>
<feComposite in="col" in2="SourceAlpha" operator="in"/>
```
> **함정 6**: `feFlood`는 서브영역 전체를 채우므로, 서브영역이 필터 영역(기본 120%)이면 색이 사방으로 넘친다. `operator="in"` 마스킹을 **반드시** 뒤에 붙이거나 `x/y/width/height`로 서브영역을 제한하라.
> **활용**: `flood-color`가 CSS 프로퍼티라는 점이 핵심이다. `baseFrequency`·`stdDeviation` 같은 어트리뷰트는 CSS로 애니메이션할 수 없지만, **`flood-color`, `flood-opacity`, `lighting-color`는 CSS 트랜지션/애니메이션이 가능하다.** (검증됨: SMIL `<animate attributeName="flood-color">` 정상 동작)
---
### 1.3 `<feImage>` — 외부 이미지 끌어오기
| 속성 | 기본값 | 의미 |
|---|---|---|
| `href` | — | 이미지 URL. `xlink:href`는 레거시 |
| `preserveAspectRatio` | `xMidYMid meet` | 서브영역에 맞추는 방식. **변위맵으로 쓸 땐 `none` 권장** |
| `crossorigin` | — | CORS |
Liquid Glass 같은 정밀한 굴절에서 **미리 계산된 변위맵**을 넣는 통로.
**참조 방식별 지원 현황**
| 방식 | Chromium | WebKit | Firefox |
|---|---|---|---|
| 외부 파일 `href="map.png"` | ✅ | 부분적 | ✅ |
| 외부 SVG 파일 `href="map.svg"` | ✅ (래스터화) | ❌ 보고됨 | ✅ |
| 같은 문서 fragment `href="#id"` | ❌ | ✅ | ❌ |
| **data URI** `href="data:image/svg+xml;..."` | ✅ | ✅ | ✅ |
> **함정 7**: fragment 참조는 Safari 전용, 외부 SVG는 Safari 미지원 — **크로스브라우저로는 data URI가 유일한 안전 경로다.** (검증됨: data URI로 SVG 변위맵 생성 → Chromium 정상)
> **함정 8**: `feImage`는 `x/y/width/height`를 **명시하지 않으면 필터 영역에 맞춰진다.** `backdrop-filter`에서 쓸 때 요소 크기와 맵 크기가 어긋나면 굴절이 엉뚱한 위치에 생긴다. 픽셀 값으로 정확히 지정하라.
> **함정 9**: data URI 안의 SVG는 **완전히 URL 인코딩**해야 한다. 최소한 `#` → `%23`, `<` → `%3C`, `>` → `%3E`, `"` → `'`. `#`을 인코딩 안 하면 fragment로 잘려서 조용히 실패한다.
---
### 1.4 `<feTile>` — 서브영역 타일링
`in`의 **필터 서브영역**을 하나의 타일로 삼아, 자기 서브영역을 가득 채울 때까지 x/y로 반복한다.
```xml
<filter id="tiled" primitiveUnits="userSpaceOnUse"
x="0%" y="0%" width="100%" height="100%">
<!-- 40×40 영역만 노이즈 생성 = 타일 원본 -->
<feTurbulence type="turbulence" baseFrequency="0.12" numOctaves="2" seed="5"
x="0" y="0" width="40" height="40" result="tile"/>
<feTile in="tile" x="0" y="0" width="220" height="140"/>
</filter>
```
> 검증됨 (Chromium). 작은 타일만 계산하고 복사하므로 **큰 면적 노이즈보다 훨씬 싸다** — 성능 최적화 기법으로도 유용.
> **함정 10**: `feTile`은 `in`에 **서브영역이 명시되어 있어야** 동작한다. 앞 프리미티브에 `x/y/width/height`가 없으면 타일 크기가 필터 영역 전체가 되어 반복이 일어나지 않는다.
---
## 2. 기하/변형 프리미티브
---
### 2.1 `<feGaussianBlur>`
| 속성 | 기본값 | 의미 |
|---|---|---|
| `stdDeviation` | `0` | 가우시안 표준편차. `x y` 2값 가능 |
| `edgeMode` | `none` | `duplicate` \| `wrap` \| `none` |
- `stdDeviation="0"` = 효과 없음(기본값이므로 반드시 지정).
- **2값 = 방향성 블러**: `stdDeviation="15 0"` → 가로 모션 블러. 세로 결/속도감 표현에 필수.
- 음수는 에러(필터 전체 무시).
- CSS `blur(6px)` ≈ `feGaussianBlur stdDeviation="6"` (단, CSS는 sRGB).
> **함정 11**: `edgeMode`는 **Safari에서만 지원**된다(Chrome·Firefox 미지원). `edgeMode="duplicate"`로 가장자리 하드컷을 만드는 트릭은 크로스브라우저 불가 — `feComposite operator="in"`으로 원본 알파에 다시 클리핑하는 방식으로 대체하라.
> **함정 12**: 큰 블러(≈100px 이상)는 엔진이 상한을 걸거나 극단적으로 느려진다. 큰 소프트 글로우는 블러 대신 **radialGradient**로 흉내내는 것이 압도적으로 싸다.
---
### 2.2 `<feOffset>`
| 속성 | 기본값 | 의미 |
|---|---|---|
| `dx`, `dy` | `0` | 이동량 (primitiveUnits 기준) |
가장 싼 프리미티브. 그림자·색분리(RGB split)의 기본 부품.
> **함정 13**: `feOffset`으로 민 결과는 필터 영역 밖으로 나가면 잘린다. `dx="20"`이면 영역도 그만큼 넓혀라.
---
### 2.3 `<feDropShadow>` — 단축 그림자
| 속성 | 기본값 |
|---|---|
| `dx` | `2` |
| `dy` | `2` |
| `stdDeviation` | `2` |
| `flood-color` | `black` |
| `flood-opacity` | `1` |
내부적으로 `feGaussianBlur + feOffset + feFlood + feComposite + feMerge`와 동등. 훨씬 짧고 최적화도 잘 된다.
**체이닝으로 다중 글로우** (검증됨 — 네온 텍스트):
```xml
<filter id="neon" x="-40%" y="-40%" width="180%" height="180%"
color-interpolation-filters="sRGB">
<feDropShadow dx="0" dy="0" stdDeviation="2" flood-color="#22d3ee" flood-opacity="1" result="s1"/>
<feDropShadow in="s1" dx="0" dy="0" stdDeviation="6" flood-color="#7c3aed" flood-opacity="0.9" result="s2"/>
<feDropShadow in="s2" dx="0" dy="0" stdDeviation="14" flood-color="#ec4899" flood-opacity="0.7"/>
</filter>
```
> `feDropShadow`는 **결과에 원본을 포함**하므로 `feMerge`가 필요 없다. 체인으로 연결하면 그림자가 누적된다.
---
### 2.4 `<feMorphology>` — 팽창/침식
| 속성 | 기본값 | 의미 |
|---|---|---|
| `operator` | `erode` | `erode`(축소/가늘게) \| `dilate`(팽창/굵게) |
| `radius` | `0` | 반경. `x y` 2값 가능. 음수는 에러 |
**핵심 용도**: 텍스트 아웃라인. `stroke`는 선이 글자 안쪽으로 반 들어가 글자를 얇게 만들지만, `feMorphology dilate`는 **바깥으로만 키운다.**
```xml
<!-- 컬러 아웃라인 (검증됨) -->
<filter id="outline" x="-20%" y="-20%" width="140%" height="140%"
color-interpolation-filters="sRGB">
<feMorphology in="SourceAlpha" operator="dilate" radius="4" result="thick"/>
<feFlood flood-color="#22d3ee" result="col"/>
<feComposite in="col" in2="thick" operator="in" result="ring"/>
<feMerge>
<feMergeNode in="ring"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
```
```xml
<!-- 속 빈(knockout) 아웃라인 (검증됨) -->
<filter id="knockout" x="-25%" y="-25%" width="150%" height="150%"
color-interpolation-filters="sRGB">
<feMorphology in="SourceAlpha" operator="dilate" radius="3" result="thick"/>
<feComposite in="thick" in2="SourceAlpha" operator="out" result="ring"/>
<feFlood flood-color="#f472b6" result="col"/>
<feComposite in="col" in2="ring" operator="in"/>
</filter>
```
> **함정 14**: `feMorphology`의 커널은 **사각형**이다. `radius`가 커지면 둥근 모서리가 각지게 뭉개진다. radius 8 이상은 결과를 반드시 눈으로 확인하라. 부드러운 확장이 필요하면 `feGaussianBlur` + `feComponentTransfer`(알파 대비)로 대체.
> **함정 15**: `radius` 기본값이 `0`이라 지정 안 하면 아무 일도 안 일어난다.
---
### 2.5 `<feDisplacementMap>` — 변위 (유기적 디자인의 심장)
| 속성 | 기본값 | 의미 |
|---|---|---|
| `in` | — | 왜곡될 이미지 |
| `in2` | — | 변위맵 |
| `scale` | `0` | 최대 이동량(px). 음수 가능(방향 반전) |
| `xChannelSelector` | **`A`** | `R`\|`G`\|`B`\|`A` |
| `yChannelSelector` | **`A`** | `R`\|`G`\|`B`\|`A` |
**공식**
```
P'(x,y) ← P( x + scale × (XC(x,y) 0.5),
y + scale × (YC(x,y) 0.5) )
```
- 채널 값 **128(=0.5)** → 변위 0 (중립)
- 0 → `scale/2`px 이동, 255 → `+scale/2`px 이동
- 즉 `scale`은 **총 이동 폭**이고 한쪽 방향 최대치는 `scale/2`
> **함정 16 (가장 자주 틀림)**: `xChannelSelector`/`yChannelSelector`의 **기본값은 `A`(알파)**다. 지정하지 않으면 알파 채널로 왜곡한다. feTurbulence를 맵으로 쓸 때 알파도 노이즈라 "되긴 되는데 왜 이렇게 나오지?"가 된다. **항상 `R`/`G`를 명시하라.** (검증됨: 기본값과 R/G 명시의 결과가 눈에 띄게 다름)
> **함정 17**: 변위 후에는 **안티에일리어싱이 깨진다.** 가장자리가 계단처럼 보이면 `feGaussianBlur stdDeviation="0.5"`를 뒤에 살짝 넣거나, 텍스트라면 `scale`을 6 이하로 낮춰라.
> **함정 18**: 변위는 **필터 영역 안에서만** 일어난다. 영역 밖 픽셀은 존재하지 않으므로, 큰 `scale`은 가장자리를 투명하게 빨아들인다. `scale`의 절반 이상만큼 영역을 넓혀라.
**두 가지 맵 만드는 법**
```xml
<!-- (a) 절차적: feTurbulence — 유기적, 손그림, 물결 -->
<feTurbulence type="fractalNoise" baseFrequency="0.03" numOctaves="4" seed="12" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="9"
xChannelSelector="R" yChannelSelector="G"/>
```
```xml
<!-- (b) 결정론적: feImage + 그라디언트 — 렌즈, 굴절, 유리 -->
<!-- R채널 = x변위, G채널 = y변위. 중립은 128. -->
<feImage preserveAspectRatio="none" x="0" y="0" width="220" height="88" result="map"
href="data:image/svg+xml;charset=utf-8,%3Csvg .../%3E"/>
<feDisplacementMap in="SourceGraphic" in2="map" scale="-60"
xChannelSelector="R" yChannelSelector="G"/>
```
(b)의 맵 SVG 원문 (인코딩 전) — **가장자리에만 변위가 몰리는 렌즈 프로파일**:
```xml
<svg xmlns="http://www.w3.org/2000/svg" width="220" height="88">
<defs>
<linearGradient id="rx" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="rgb(0,0,0)"/>
<stop offset="0.28" stop-color="rgb(128,0,0)"/>
<stop offset="0.72" stop-color="rgb(128,0,0)"/>
<stop offset="1" stop-color="rgb(255,0,0)"/>
</linearGradient>
<linearGradient id="gy" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="rgb(0,0,0)"/>
<stop offset="0.28" stop-color="rgb(0,128,0)"/>
<stop offset="0.72" stop-color="rgb(0,128,0)"/>
<stop offset="1" stop-color="rgb(0,255,0)"/>
</linearGradient>
</defs>
<rect width="220" height="88" rx="44" fill="rgb(128,128,128)"/>
<rect width="220" height="88" rx="44" fill="url(#rx)" style="mix-blend-mode:screen"/>
<rect width="220" height="88" rx="44" fill="url(#gy)" style="mix-blend-mode:screen"/>
</svg>
```
> 검증됨: `mix-blend-mode: screen`이 `feImage`가 래스터화하는 SVG 내부에서 정상 동작한다. R채널만 있는 레이어와 G채널만 있는 레이어를 screen으로 합치면 채널이 서로 간섭 없이 합쳐진다(= 채널별 가산). 중앙 28~72% 구간을 128로 평평하게 두는 것이 "가장자리에서만 굴절"의 핵심.
---
### 2.6 `<feConvolveMatrix>` — 컨볼루션 커널
| 속성 | 기본값 | 의미 |
|---|---|---|
| `order` | `3` | 커널 크기 (`3` = 3×3) |
| `kernelMatrix` | — | `order²`개의 값 (필수) |
| `divisor` | 커널 합(0이면 1) | 결과를 나눌 값 |
| `bias` | `0` | 더할 상수 |
| `targetX`/`targetY` | `floor(order/2)` | 커널 중심 위치 |
| `edgeMode` | `duplicate` | `duplicate`\|`wrap`\|`none` |
| `preserveAlpha` | `false` | true면 알파를 건드리지 않음 |
```xml
<!-- 샤픈: 변위 후 흐릿해진 엣지 복구 -->
<feConvolveMatrix order="3" preserveAlpha="true"
kernelMatrix="0 -1 0 -1 5 -1 0 -1 0"/>
<!-- 엠보스 -->
<feConvolveMatrix order="3" preserveAlpha="true" bias="0.5"
kernelMatrix="-2 -1 0 -1 1 1 0 1 2"/>
<!-- 엣지 검출(윤곽선 추출) -->
<feConvolveMatrix order="3" preserveAlpha="true"
kernelMatrix="0 1 0 1 -4 1 0 1 0"/>
```
> **함정 19**: `preserveAlpha="false"`(기본값)면 알파에도 커널이 적용되어 형태가 무너진다. 색만 만지고 싶으면 `true`로.
> **함정 20**: 큰 `order`(5×5 이상)는 픽셀당 연산이 제곱으로 늘어 매우 비싸다. 실시간 애니메이션에는 쓰지 마라.
---
## 3. 색 조작 프리미티브
---
### 3.1 `<feColorMatrix>`
| 속성 | 기본값 | 의미 |
|---|---|---|
| `type` | `matrix` | `matrix`\|`saturate`\|`hueRotate`\|`luminanceToAlpha` |
| `values` | type별 항등 | `matrix`=20개 값, `saturate`=0~1(초과도 허용), `hueRotate`=각도 |
**5×4 행렬 레이아웃** (열: R G B A 상수 / 행: R' G' B' A')
```
R' = r1·R + r2·G + r3·B + r4·A + r5
G' = g1·R + g2·G + g3·B + g4·A + g5
B' = b1·R + b2·G + b3·B + b4·A + b5
A' = a1·R + a2·G + a3·B + a4·A + a5
```
모든 값은 **0~1 정규화 기준**이고 결과는 0~1로 clamp된다. 5번째 열은 **상수 오프셋**(1이 곱해짐).
**항등 행렬** (변화 없음, 커스텀의 출발점):
```
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 1 0
```
**자주 쓰는 행렬 사전**
```xml
<!-- 그레이스케일 (Rec.709 휘도) -->
values="0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0 0 0 1 0"
<!-- 노이즈 → 불투명 그레이 (알파 노이즈 제거) -->
values="0.33 0.33 0.33 0 0
0.33 0.33 0.33 0 0
0.33 0.33 0.33 0 0
0 0 0 0 1"
<!-- 단색 실루엣 (#22d3ee = 34,211,238 → /255) -->
values="0 0 0 0 0.133
0 0 0 0 0.827
0 0 0 0 0.933
0 0 0 1 0"
<!-- 알파 대비 극대화 = gooey/threshold 핵심 -->
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 18 -7"
<!-- 채널 분리 (R만) -->
values="1 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 1 0"
<!-- 색 반전 -->
values="-1 0 0 0 1
0 -1 0 0 1
0 0 -1 0 1
0 0 0 1 0"
```
**알파 대비 트릭의 수학** — `values="... 0 0 0 M -O"` 이면 `A' = M·A O`:
- 임계점: `A = O / M`
- 전이 폭: `1 / M`
- 예) `M=18, O=7` → 임계 0.389, 폭 0.056 → A가 0.389~0.444 사이에서 0→1로 급전환.
- `M`을 키우면 더 날카롭게, `O/M`을 키우면 형태가 더 수축한다.
**`type` 단축형**
| type | values | 결과 |
|---|---|---|
| `saturate` | `0` | 흑백 |
| `saturate` | `0.5` | 채도 절반 |
| `saturate` | `3` | 과채도 (1 초과 허용) |
| `hueRotate` | `180` | 색상환 180° 회전 |
| `luminanceToAlpha` | (없음) | RGB→0, A=휘도 → **밝기를 마스크로 변환** |
> **함정 21**: `luminanceToAlpha`는 결과의 RGB가 전부 0(검정)이다. 마스크로만 쓰고, 색이 필요하면 `feFlood`+`feComposite in`으로 다시 칠하라.
> **함정 22**: 행렬 값은 **0~1 스케일**이다. `#ff8000`을 넣겠다고 `255 128 0`을 쓰면 전부 clamp되어 흰색이 된다. 반드시 255로 나눠라.
> **함정 23**: `type="saturate"`의 결과는 색공간에 따라 극적으로 다르다(§0.5). 브랜드 흑백 톤을 맞추려면 `sRGB`를 명시하라.
---
### 3.2 `<feComponentTransfer>` + `<feFuncR/G/B/A>`
채널별 **전달 함수**를 적용한다. 톤 커브 도구라고 생각하면 된다.
| type | 파라미터 | 공식 |
|---|---|---|
| `identity` | — | `C' = C` |
| `table` | `tableValues="v0 v1 ... vn"` | n개 값을 균등 구간으로 **선형 보간** |
| `discrete` | `tableValues="v0 v1 ... vn"` | 균등 구간별 **계단 함수**(보간 없음) |
| `linear` | `slope`(1), `intercept`(0) | `C' = slope·C + intercept` |
| `gamma` | `amplitude`(1), `exponent`(1), `offset`(0) | `C' = amplitude·C^exponent + offset` |
**table의 동작**: 값이 n개면 n1개 구간이 만들어진다.
- `tableValues="0 1"` → 항등(0→0, 1→1)
- `tableValues="0.05 0.98"` → 그림자를 0.05로, 하이라이트를 0.98로 리매핑 → **듀오톤의 원리**
- `tableValues="0 0.5 1"` → 3점 톤 커브
**discrete의 동작**: `tableValues="0 0.25 0.5 0.75 1"` → 5단계 포스터화. (검증됨)
**듀오톤 = 그레이스케일 + 채널별 2점 table** (검증됨)
```xml
<filter id="duotone" color-interpolation-filters="sRGB">
<feColorMatrix type="matrix" values="
0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0 0 0 1 0"/>
<feComponentTransfer>
<!-- 그림자색 #0D1A59, 하이라이트색 #FA591A -->
<feFuncR type="table" tableValues="0.05 0.98"/>
<feFuncG type="table" tableValues="0.10 0.35"/>
<feFuncB type="table" tableValues="0.35 0.10"/>
</feComponentTransfer>
</filter>
```
**hex → tableValues 변환 공식**
```
shadowColor #RRGGBB, highlightColor #RRGGBB
feFuncR tableValues = "R_shadow/255 R_highlight/255"
feFuncG tableValues = "G_shadow/255 G_highlight/255"
feFuncB tableValues = "B_shadow/255 B_highlight/255"
```
3색(트라이톤)이면 값 3개, 4색이면 4개를 넣으면 된다.
> **함정 24**: **`color-interpolation-filters="sRGB"`를 안 넣으면 듀오톤 색이 전부 어긋난다.** 이 프리미티브에서는 필수라고 봐도 된다.
> **함정 25**: 같은 채널의 `feFunc*`를 여러 개 쓰면 **마지막 것만** 적용된다.
> **함정 26**: 계산은 **비프리멀티플라이드(non-premultiplied)** 값으로 이뤄진다. 반투명 영역에서 색이 튀면 이 때문이다.
---
## 4. 합성 프리미티브
---
### 4.1 `<feBlend>`
| 속성 | 기본값 |
|---|---|
| `mode` | `normal` |
지원 모드 (CSS 블렌드 모드와 동일):
`normal, multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, hue, saturation, color, luminosity`
> `in`이 **위**, `in2`가 **아래** 레이어다. 포토샵 레이어 순서와 반대로 헷갈리기 쉽다.
---
### 4.2 `<feComposite>`
| 속성 | 기본값 |
|---|---|
| `operator` | `over` |
| `k1, k2, k3, k4` | `0` |
**Porter-Duff 연산자**
| operator | 결과 |
|---|---|
| `over` | `in`을 `in2` 위에 (기본) |
| `in` | **`in2`의 알파로 `in`을 자른다** ← 마스킹의 핵심 |
| `out` | `in2`가 없는 곳의 `in`만 남긴다 ← 속 빈 아웃라인 |
| `atop` | `in2` 영역 안에서만 `in`을 얹는다 |
| `xor` | 겹치지 않는 부분만 |
| `lighter` | 가산 합성 |
| `arithmetic` | 아래 공식 |
**arithmetic 공식**
```
result = k1·i1·i2 + k2·i1 + k3·i2 + k4
```
| 목적 | k1 | k2 | k3 | k4 |
|---|---|---|---|---|
| 단순 덧셈 (조명 합성) | 0 | 1 | 1 | 0 |
| 곱셈 (multiply) | 1 | 0 | 0 | 0 |
| `in`만 통과 | 0 | 1 | 0 | 0 |
| 50:50 블렌드 | 0 | 0.5 | 0.5 | 0 |
| 노이즈 가산 (±0.25) | 0 | 1 | 0.5 | 0.25 |
> **함정 27**: `arithmetic`은 **프리멀티플라이드 알파 값**에 대해 계산된다. 반투명 입력에서 예상 밖 결과가 나오면 `feFlood`+`in`으로 불투명화 후 처리하라. Firefox에는 `arithmetic` 관련 시각 아티팩트 버그 리포트도 있다.
> **함정 28**: 조명 프리미티브 결과를 원본과 합칠 때는 관례적으로 `operator="arithmetic" k1=0 k2=1 k3=1 k4=0`(가산)을 쓴다. 그냥 `over`로 얹으면 조명이 원본을 덮어버린다.
---
### 4.3 `<feMerge>` / `<feMergeNode>`
여러 레이어를 순서대로 `over` 합성한다. **뒤에 오는 `feMergeNode`가 위**에 쌓인다.
```xml
<feMerge>
<feMergeNode in="glow"/> <!-- 맨 아래 -->
<feMergeNode in="ring"/>
<feMergeNode in="SourceGraphic"/> <!-- 맨 위 -->
</feMerge>
```
> `feComposite operator="over"`를 여러 번 체인하는 것과 같지만 훨씬 읽기 쉽다.
---
## 5. 조명 프리미티브
알파 채널을 **높이맵(범프맵)** 으로 해석해 3D 조명을 계산한다. `feTurbulence`와 조합하면 종이·가죽·금속 질감이 나온다.
### 5.1 `<feDiffuseLighting>` (난반사 — 무광)
| 속성 | 기본값 | 의미 |
|---|---|---|
| `surfaceScale` | `1` | 알파=1일 때의 표면 높이. 클수록 굴곡 심함 |
| `diffuseConstant` | `1` | 반사 계수(kd). 0 이상 |
| `lighting-color` | `white` | 광원 색. **CSS 프로퍼티 → 애니메이션 가능** |
| `kernelUnitLength` | (없음) | 노멀 계산 샘플 간격. 대개 생략 |
결과는 **불투명한 RGBA**다(알파=1). 그래서 반드시 원본 알파로 다시 잘라야 한다.
### 5.2 `<feSpecularLighting>` (정반사 — 광택)
| 속성 | 기본값 | 범위 |
|---|---|---|
| `surfaceScale` | `1` | — |
| `specularConstant` | `1` | ks, 0 이상 |
| `specularExponent` | `1` | **1 ~ 128**. 클수록 하이라이트가 작고 날카로움 |
| `lighting-color` | `white` | — |
결과는 **정반사 성분만 담긴 RGBA**로, 알파가 0이 아닌 영역이 하이라이트다. 원본에 **가산**해야 한다.
### 5.3 광원 요소 (정확히 하나를 자식으로)
```xml
<feDistantLight azimuth="45" elevation="60"/>
```
| 속성 | 기본값 | 의미 |
|---|---|---|
| `azimuth` | `0` | xy평면 방향각 0~360° |
| `elevation` | `0` | 고도각 0~90°. 낮을수록 그림자가 길고 대비 강함 |
```xml
<fePointLight x="60" y="20" z="120"/>
```
| 속성 | 기본값 |
|---|---|
| `x`, `y`, `z` | `0` |
```xml
<feSpotLight x="55" y="15" z="90"
pointsAtX="110" pointsAtY="80" pointsAtZ="0"
specularExponent="6" limitingConeAngle="42"/>
```
| 속성 | 기본값 | 의미 |
|---|---|---|
| `x/y/z` | `0` | 광원 위치 |
| `pointsAtX/Y/Z` | `0` | 조준점 |
| `specularExponent` | `1` | 중심부 집중도 |
| `limitingConeAngle` | (없음=제한 없음) | 원뿔 반각(도). 넘으면 빛 없음 |
### 5.4 검증된 조합 패턴
```xml
<!-- (A) 종이 질감: 노이즈를 높이맵으로 -->
<filter id="paper" x="0%" y="0%" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="5" seed="3" result="noise"/>
<feDiffuseLighting in="noise" lighting-color="#e8e0d0" surfaceScale="2" result="lit">
<feDistantLight azimuth="45" elevation="60"/>
</feDiffuseLighting>
<feComposite in="lit" in2="SourceAlpha" operator="in"/>
</filter>
```
```xml
<!-- (B) 광택 버튼: 알파 블러를 범프맵으로 -->
<filter id="specular" x="-20%" y="-20%" width="140%" height="140%"
color-interpolation-filters="sRGB">
<feGaussianBlur in="SourceAlpha" stdDeviation="6" result="bump"/>
<feSpecularLighting in="bump" surfaceScale="6" specularConstant="1"
specularExponent="25" lighting-color="#ffffff" result="spec">
<fePointLight x="60" y="20" z="120"/>
</feSpecularLighting>
<feComposite in="spec" in2="SourceAlpha" operator="in" result="specClip"/>
<feComposite in="SourceGraphic" in2="specClip" operator="arithmetic"
k1="0" k2="1" k3="1" k4="0"/>
</filter>
```
```xml
<!-- (C) 스포트라이트: 조명을 screen으로 얹기 -->
<filter id="spot" x="-20%" y="-20%" width="140%" height="140%"
color-interpolation-filters="sRGB">
<feGaussianBlur in="SourceAlpha" stdDeviation="5" result="bump"/>
<feDiffuseLighting in="bump" surfaceScale="5" diffuseConstant="1"
lighting-color="#ffd88a" result="light">
<feSpotLight x="55" y="15" z="90" pointsAtX="110" pointsAtY="80" pointsAtZ="0"
specularExponent="6" limitingConeAngle="42"/>
</feDiffuseLighting>
<feComposite in="light" in2="SourceAlpha" operator="in" result="lightClipped"/>
<feBlend in="SourceGraphic" in2="lightClipped" mode="screen"/>
</filter>
```
> 검증됨: (A)(B)(C) 모두 Chromium에서 의도대로 렌더. (C)에서 `mode="multiply"`로 바꾸면 음영/비네트 룩이 된다.
> **함정 29**: 조명 결과를 그냥 출력하면 **필터 영역 전체가 불투명 사각형**으로 칠해진다. `feComposite operator="in"` + `SourceAlpha` 클리핑을 잊지 마라.
> **함정 30**: `surfaceScale`이 크면 (20~50) 스투코/구겨진 플라스틱 같은 과장된 질감이 된다. 종이 느낌은 1~3이 적당.
> **함정 31**: `kernelUnitLength`는 엔진마다 해석이 달라 결과가 갈린다. 크로스브라우저가 중요하면 **쓰지 마라.**
---
## 6. CSS 단축 필터 ↔ 프리미티브 대응표
| CSS 함수 | 등가 프리미티브 | 비고 |
|---|---|---|
| `blur(<len>)` | `feGaussianBlur stdDeviation="<len>"` | CSS는 sRGB |
| `brightness(a)` | `feComponentTransfer` + `type="linear" slope="a"` | |
| `contrast(a)` | `feComponentTransfer` + `type="linear" slope="a" intercept="-(0.5a)+0.5"` | |
| `grayscale(a)` | `feColorMatrix type="matrix"` (휘도 행렬 보간) | |
| `sepia(a)` | `feColorMatrix type="matrix"` | |
| `saturate(a)` | `feColorMatrix type="saturate" values="a"` | |
| `hue-rotate(deg)` | `feColorMatrix type="hueRotate" values="deg"` | |
| `invert(a)` | `feComponentTransfer type="table" tableValues="a 1-a"` | |
| `opacity(a)` | `feComponentTransfer feFuncA type="table" tableValues="0 a"` | |
| `drop-shadow(...)` | `feDropShadow` | |
**결정적 차이**: CSS 단축 함수는 스펙상 **sRGB에서 동작**하고, SVG `<filter>`는 **linearRGB가 기본**이다. 같은 결과를 원하면 `color-interpolation-filters="sRGB"`를 명시하라.
**언제 CSS 단축을 쓰나**: 단순 blur/그림자/채도만 필요하면 CSS 단축이 **더 빠르고 더 잘 가속되며 Safari 호환성이 좋다.** SVG 필터는 CSS로 표현 불가능한 것(노이즈, 변위, 채널 분리, 알파 대비, 조명)에만 써라.
---
## 7. 애니메이션 가능성 매트릭스
| 대상 | SMIL `<animate>` | CSS 애니메이션 | JS (`setAttribute`) |
|---|---|---|---|
| `baseFrequency`, `stdDeviation`, `scale`, `radius`, `dx/dy`, `k1~k4`, `values` | ✅ | ❌ (CSS 프로퍼티가 아님) | ✅ |
| `flood-color`, `flood-opacity`, `lighting-color` | ✅ | ✅ **CSS 프로퍼티다** | ✅ |
| `filter` 속성 자체 (`url(#a)` → `url(#b)`) | — | ❌ 보간 불가, 즉시 전환만 | — |
| CSS 단축 필터 (`blur(2px)` → `blur(8px)`) | — | ✅ 보간됨 | — |
| 필터 걸린 요소의 `transform`/`opacity` | — | ✅ (단, 필터 재계산 유발 가능) | — |
**핵심**: `feTurbulence`의 `baseFrequency`를 CSS로 애니메이션하려는 시도는 **작동하지 않는다.** SMIL이나 JS를 써야 한다. 대신 `flood-color`/`lighting-color`는 CSS 트랜지션으로 부드럽게 다룰 수 있어, 호버 인터랙션에 매우 유용하다.
```xml
<!-- SMIL: baseFrequency 애니메이션 (검증됨) -->
<feTurbulence type="turbulence" baseFrequency="0.008 0.04" numOctaves="2" seed="2" result="n">
<animate attributeName="baseFrequency" dur="8s"
values="0.008 0.04;0.012 0.055;0.008 0.04" repeatCount="indefinite"/>
</feTurbulence>
```
```xml
<!-- SMIL: flood-color 애니메이션 (검증됨) -->
<feFlood flood-color="#22d3ee" result="col">
<animate attributeName="flood-color"
values="#22d3ee;#a855f7;#f43f5e;#22d3ee" dur="4s" repeatCount="indefinite"/>
</feFlood>
```
```js
// JS: seed 순환으로 squigglevision (SMIL보다 제어가 쉽다)
const t = document.querySelector('#squiggle feTurbulence');
let i = 0;
const seeds = [1, 2, 3, 4];
setInterval(() => { t.setAttribute('seed', seeds[i++ % seeds.length]); }, 120);
```
---
## 8. 작성 전 체크리스트
작성한 필터가 이상하면 위에서부터 확인하라.
1. `id` 오타 / `url(#id)`의 `#` 누락
2. `baseFrequency`, `stdDeviation`, `radius`, `scale` — **기본값이 0이라 지정 안 하면 무효과**
3. `feDisplacementMap`의 `xChannelSelector`/`yChannelSelector` — **기본값 A**
4. 필터 영역이 좁아 잘리는가 → `x/y/width/height` 확대
5. 색이 이상한가 → `color-interpolation-filters="sRGB"`
6. `feFlood`/조명 결과가 사각형으로 넘치는가 → `feComposite operator="in"` 누락
7. 분기했는데 무시되는가 → `in`을 양쪽 다 명시했는가
8. `feColorMatrix` 값을 0~255로 썼는가 → 0~1로
9. 텍스트가 뭉개지는가 → 필터 컨테이너 안에 텍스트를 넣지 마라(§03)
10. `feTurbulence` seed가 514/1977/2337/4777/8032/9615인가 → 사각 아티팩트