# 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개면 n−1개 구간이 만들어진다.
- `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인가 → 사각 아티팩트
