# 02. SVG 필터 레시피북 — 바로 붙여넣는 완성 코드

> 22개 레시피. 모든 코드는 **생략 없는 완성본**이며, 별도 표기가 없으면 **Chromium 151에서 실제 렌더링 검증 완료**다.
> 성능 등급: **상**=스크롤/애니메이션 중에도 안전 · **중**=정적이면 안전, 애니메이션은 작은 면적만 · **하**=큰 면적·모바일·상시 애니메이션 금물

**공통 전제**: 아래 컨테이너를 문서 어딘가(보통 `</body>` 직전)에 한 번 넣고, 각 레시피의 `<filter>`를 그 안에 모은다.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <defs>
    <!-- 여기에 filter들 -->
  </defs>
</svg>
```

---

## 목차

| # | 레시피 | 성능 | 핵심 프리미티브 |
|---|---|---|---|
| 01 | Gooey / Metaball | 중 | feGaussianBlur + feColorMatrix |
| 02 | 필름 그레인 오버레이 (CSS만) | 상 | feTurbulence (data URI) |
| 03 | 그레이니 그라디언트 | 상 | feTurbulence + CSS filter |
| 04 | 종이 질감 | 중 | feTurbulence + feDiffuseLighting |
| 05 | Liquid Glass (굴절 유리) | 중 | feImage + feDisplacementMap + backdrop-filter |
| 06 | Liquid Glass + 색수차 | 하 | 3× feDisplacementMap |
| 07 | 프로스티드 글래스 (폴백 겸용) | 상 | backdrop-filter + noise |
| 08 | 크로마틱 애버레이션 / RGB Split | 중 | feOffset + feColorMatrix + feBlend |
| 09 | 손그림 러프 엣지 | 중 | feTurbulence + feDisplacementMap |
| 10 | 잉크 번짐 | 중 | 변위 + 블러 + 알파 대비 |
| 11 | Squigglevision (애니메이션 손그림) | 하 | seed 순환 |
| 12 | 물결 텍스트 | 하 | SMIL baseFrequency |
| 13 | 듀오톤 / 그라디언트 맵 | 상 | feColorMatrix + feComponentTransfer |
| 14 | 포스터화 / 트라이톤 | 상 | feComponentTransfer discrete |
| 15 | 디더 / 임계값 | 중 | 노이즈 가산 + discrete |
| 16 | 리소그래프 인쇄 | 중 | mix-blend-mode + grain + 오프셋 |
| 17 | 텍스트 아웃라인 | 상 | feMorphology |
| 18 | 스티커 | 상 | feMorphology + feDropShadow |
| 19 | 네온 글로우 | 중 | 체인 feDropShadow |
| 20 | 스포트라이트 | 중 | feSpotLight + feDiffuseLighting |
| 21 | 광택/엠보스 | 중 | feSpecularLighting |
| 22 | 글리치 밴드 | 하 | feFlood 밴드 + feDisplacementMap |

---

## 01. Gooey / Metaball

**언제 쓰나** — 떠다니는 블롭 배경, 확장되는 FAB 메뉴, 로딩 인디케이터, 유기적 로고 모션. 개별 도형들이 **액체처럼 서로 융합**해야 할 때.

**원리** — 블러로 알파를 번지게 한 뒤, `feColorMatrix`로 알파 대비를 극단적으로 올려 다시 날카로운 경계를 만든다. 두 도형의 번진 알파가 겹치는 지점에서 임계값을 넘으면 하나로 붙는다.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="goo" x="-25%" y="-25%" width="150%" height="150%"
            color-interpolation-filters="sRGB">
      <feGaussianBlur in="SourceGraphic" stdDeviation="9" result="blur"/>
      <feColorMatrix in="blur" type="matrix"
        values="1 0 0 0  0
                0 1 0 0  0
                0 0 1 0  0
                0 0 0 20 -9" result="goo"/>
      <feComposite in="SourceGraphic" in2="goo" operator="atop"/>
    </filter>
  </defs>
</svg>

<div class="goo-menu">
  <span></span><span></span><span></span>
</div>
```

```css
.goo-menu {
  filter: url(#goo);
  display: flex;
  gap: 6px;
  align-items: center;
  /* 필터 영역이 잘리지 않도록 여백 확보 */
  padding: 24px;
}
.goo-menu span {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: #34d399;
  display: block;
}

@media (prefers-reduced-motion: reduce) {
  .goo-menu { filter: none; }
}
```

**파라미터 가이드**

| 값 | 효과 |
|---|---|
| `stdDeviation` 5 | 도형이 아주 가까워야 붙음. 미세한 융합 |
| `stdDeviation` 9~12 | 표준. 목(neck)이 자연스럽게 늘어남 |
| `stdDeviation` 20+ | 멀리서도 붙음. 형태가 뭉개짐 |
| 알파 행렬 `M`(=20) ↑ | 경계가 더 날카로움. 12~30 권장 |
| `O/M` (=9/20=0.45) ↑ | 형태가 더 수축. 0.35~0.5 권장 |

**마무리 방식 선택**

| 코드 | 결과 |
|---|---|
| `<feBlend in="SourceGraphic" in2="goo"/>` | 원본을 위에 얹음. 원본 색이 선명하게 살아남 |
| `<feComposite in="SourceGraphic" in2="goo" operator="atop"/>` | goo 영역 안에서만 원본. **모서리가 뾰족한 도형일 때 필수** |
| (마무리 없음) | goo만. 알파 대비 결과의 색이 그대로 나옴 |

**함정**
- **필터 컨테이너 안에 텍스트를 넣지 마라.** 텍스트가 블러+임계값을 거쳐 판독 불가능한 덩어리가 된다(검증됨). 텍스트는 필터 밖 별도 레이어에 절대배치로 얹어라.
- 컨테이너에 여백이 없으면 블롭이 필터 영역 경계에서 잘린다.

**성능: 중** — 정적이면 문제없음. 블롭이 움직이면 매 프레임 블러 재계산이라 면적이 크면 급격히 무거워진다. 400×400px 이하로 제한하고 `will-change: filter`는 **쓰지 마라**(§03 참고).

---

## 02. 필름 그레인 오버레이 (SVG 없이 CSS만으로)

**언제 쓰나** — 어떤 배경에도 얹는 범용 텍스처. 밋밋한 그라디언트/단색을 아날로그하게 만든다. **가장 자주 쓰게 될 레시피.**

**원리** — SVG를 data URI 배경 이미지로 넣으면 **한 번만 래스터화**되어 캐시된다. 요소에 `filter`를 거는 것과 달리 리페인트마다 재계산되지 않는다.

```css
.grainy {
  position: relative;
  isolation: isolate; /* 블렌드가 바깥으로 새지 않게 */
  background: linear-gradient(135deg, #4f46e5, #ec4899);
}

.grainy::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 1;
  opacity: 0.55;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}
```

**파라미터 가이드**

| 조절 | 방법 |
|---|---|
| 입자 크기 | data URI 안 `baseFrequency`: `0.6`(굵음) ~ `0.95`(미세) |
| 거칠기 | `numOctaves`: 1(부드러움) ~ 4(거침) |
| 강도 | CSS `opacity`: 0.15(은은) ~ 0.7(강함) |
| 룩 | `mix-blend-mode`: `overlay`(대비 유지) · `soft-light`(부드러움) · `multiply`(어둡게) · `screen`(밝게) |

**밝은 배경용 변형** — `overlay`는 밝은 배경에서 약하다. `multiply` + 낮은 opacity로 바꿔라.

```css
.grainy-light::after {
  mix-blend-mode: multiply;
  opacity: 0.25;
}
```

**URL 인코딩 필수 문자**: `#`→`%23`, `<`→`%3C`, `>`→`%3E`, `%`→`%25`, 큰따옴표는 작은따옴표로.

**성능: 상** — 정적 배경 이미지라 리페인트 비용이 사실상 0. **가장 안전한 노이즈 방법.**

---

## 03. 그레이니 그라디언트

**언제 쓰나** — 그라디언트의 밴딩(계단 현상)을 감추면서 동시에 질감을 준다. 히어로 배경, 카드 표면.

```css
.grainy-gradient {
  position: relative;
  isolation: isolate;
  background: radial-gradient(circle at 30% 20%, #7c3aed, transparent 60%),
              radial-gradient(circle at 75% 70%, #06b6d4, transparent 55%),
              #0f172a;
}

.grainy-gradient::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
  filter: contrast(170%) brightness(1000%);
  mix-blend-mode: overlay;
  opacity: 0.18;
}
```

**핵심**: `contrast(170%) brightness(1000%)`가 회색 뭉치를 흑백 점으로 극단화한다. 이 두 값이 그레인의 성격을 결정한다.

| 목표 | 값 |
|---|---|
| 미세한 필름 그레인 | `contrast(120%) brightness(300%)`, opacity 0.12 |
| 거친 인쇄 노이즈 | `contrast(300%) brightness(1500%)`, opacity 0.25 |
| 스노우/TV 노이즈 | `contrast(500%) brightness(2000%)`, opacity 0.4 |

> 검증됨. `opacity: 0.18`은 의도적으로 아주 은은하다 — 어두운 영역에서 밴딩이 사라지는 것으로 확인된다. 그레인이 "보이길" 원하면 0.3 이상으로 올려라.

**성능: 상** — 배경 이미지 + CSS 단축 필터. GPU 가속이 잘 된다.

---

## 04. 종이 질감

**언제 쓰나** — 에디토리얼 레이아웃, 인쇄물 느낌의 카드, 아날로그 브랜딩.

**원리** — `feTurbulence`의 알파 노이즈를 **높이맵**으로 삼아 `feDiffuseLighting`으로 조명한다. 실제 종이 섬유의 미세한 요철을 시뮬레이션한다.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <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>
  </defs>
</svg>

<div class="paper-card">
  <div class="paper-surface"></div>
  <div class="paper-content">
    <h2>Editorial</h2>
    <p>텍스트는 반드시 필터 밖 레이어에 둔다.</p>
  </div>
</div>
```

```css
.paper-card { position: relative; width: 420px; aspect-ratio: 3/2; }
.paper-surface {
  position: absolute; inset: 0;
  background: #e8e0d0;
  border-radius: 6px;
  filter: url(#paper);
}
.paper-content {
  position: relative;   /* 필터 위에 겹침, 필터 영향 없음 */
  padding: 32px;
  color: #2b2b28;
}
```

**파라미터 가이드**

| 목표 | baseFrequency | numOctaves | surfaceScale | elevation |
|---|---|---|---|---|
| 매끈한 카드지 | 0.02 | 3 | 1 | 70 |
| 표준 종이 | 0.04 | 5 | 2 | 60 |
| 거친 수제지 | 0.06 | 5 | 3.5 | 45 |
| 캔버스/천 | 0.08 0.02 | 4 | 3 | 50 |
| 스투코 벽 | 0.05 | 4 | 20 | 40 |

- `elevation`을 낮출수록 그림자가 길어져 **대비가 강해진다.**
- `lighting-color`를 종이 색과 같게 맞추면 자연스럽다. 다르게 하면 조명 색이 입혀진다.
- **`feComposite operator="in"`을 빼면 필터 영역 전체가 종이로 칠해진다.**

**성능: 중** — `numOctaves="5"`는 무겁다. 정적 배경이면 문제없지만, 애니메이션은 금지. 반복 배경이라면 **한 번 만들어 PNG로 굽는 것**이 최선이다.

---

## 05. Liquid Glass (굴절 유리)

**언제 쓰나** — iOS 26 스타일 유리 UI, 플로팅 툴바/독, 모달 헤더. 배경이 **실제로 굴절**되어야 할 때(단순 블러와 차원이 다르다).

**원리** — `backdrop-filter: url(#id)`로 배경 픽셀을 필터에 통과시키고, 미리 만든 변위맵으로 가장자리에서만 굴절시킨다. **중앙은 중립(128), 가장자리는 극단값**인 맵이 렌즈 효과의 전부다.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="lens" x="0%" y="0%" width="100%" height="100%"
            color-interpolation-filters="sRGB">
      <feImage preserveAspectRatio="none" x="0" y="0" width="220" height="88" result="map"
        href="data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='88'%3E%3Cdefs%3E%3ClinearGradient id='rx' x1='0' y1='0' x2='1' y2='0'%3E%3Cstop offset='0' stop-color='rgb(0,0,0)'/%3E%3Cstop offset='0.28' stop-color='rgb(128,0,0)'/%3E%3Cstop offset='0.72' stop-color='rgb(128,0,0)'/%3E%3Cstop offset='1' stop-color='rgb(255,0,0)'/%3E%3C/linearGradient%3E%3ClinearGradient id='gy' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='rgb(0,0,0)'/%3E%3Cstop offset='0.28' stop-color='rgb(0,128,0)'/%3E%3Cstop offset='0.72' stop-color='rgb(0,128,0)'/%3E%3Cstop offset='1' stop-color='rgb(0,255,0)'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='220' height='88' rx='44' fill='rgb(128,128,128)'/%3E%3Crect width='220' height='88' rx='44' fill='url(%23rx)' style='mix-blend-mode:screen'/%3E%3Crect width='220' height='88' rx='44' fill='url(%23gy)' style='mix-blend-mode:screen'/%3E%3C/svg%3E"/>
      <feDisplacementMap in="SourceGraphic" in2="map" scale="-60"
                         xChannelSelector="R" yChannelSelector="G"/>
    </filter>
  </defs>
</svg>

<div class="glass-pill">Liquid Glass</div>
```

```css
.glass-pill {
  /* 맵 크기와 정확히 일치시켜야 한다 */
  width: 220px;
  height: 88px;
  border-radius: 44px;

  display: grid;
  place-items: center;
  color: #fff;
  font-weight: 600;
  letter-spacing: 0.02em;

  backdrop-filter: url(#lens) brightness(1.06) saturate(1.25);
  border: 1px solid rgba(255, 255, 255, 0.28);
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.7),
    inset 0 -1px 1px rgba(255, 255, 255, 0.3),
    0 10px 30px rgba(0, 0, 0, 0.45);
}

/* Chromium 외 브라우저 폴백 */
@supports not (backdrop-filter: url(#lens)) {
  .glass-pill { backdrop-filter: blur(14px) saturate(1.4); }
}
```

**변위맵 원문** (인코딩 전 — 크기를 바꿀 때 이걸 수정해 다시 인코딩)

```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>
```

**파라미터 가이드**

| 조절 | 방법 |
|---|---|
| 굴절 강도 | `scale`: −30(약함) ~ −90(강함). **음수가 볼록 렌즈** |
| 굴절 방향 반전 | `scale` 부호 뒤집기 (오목 렌즈) |
| 굴절이 몰리는 폭 | 그라디언트 stop `0.28`/`0.72`를 `0.15`/`0.85`로 → 가장자리에 더 집중 |
| 유리 두께감 | `box-shadow`의 `inset` 하이라이트 강도 |
| 배경 밝기/채도 | `backdrop-filter`의 `brightness()` `saturate()` |

**브라우저 지원**
- `backdrop-filter: url(#id)`는 **Chromium 계열만 실제로 렌더**한다. Safari·Firefox는 미지원(Firefox에는 기능 요청이 올라와 있음).
- ⚠️ `CSS.supports('backdrop-filter','url(#x)')`는 **값 파싱만** 검사하므로 신뢰할 수 없다(Chromium에서 true 확인, 다른 엔진도 true를 반환할 가능성이 높음). **폴백은 `@supports`가 아니라 "폴백이 그 자체로 괜찮게 보이도록" 설계하라** — 레시피 07을 기본으로 깔고 그 위에 렌즈를 얹는 방식.

**함정**
- `backdrop-filter`의 필터 영역은 요소 크기에 **자동으로 맞춰지지 않는다.** 맵의 `width/height`와 요소의 `width/height`가 어긋나면 굴절이 엉뚱한 곳에 생긴다. 크기가 다른 버튼마다 **별도의 맵**을 만들어라.
- 반응형으로 크기가 계속 변하는 요소에는 부적합. 고정 크기 칩/독/툴바에 쓴다.

**성능: 중** — `feImage`는 한 번 래스터화 후 캐시된다. `scale`만 애니메이션하면 맵 재생성이 없어 비교적 싸다. 반대로 요소 크기를 애니메이션하면 매 프레임 맵 재계산 → 매우 비쌈.

---

## 06. Liquid Glass + 색수차 (프리즘 유리)

**언제 쓰나** — 프리미엄 히어로, 프로덕트 쇼케이스. 유리 가장자리에서 **빛이 무지개로 분산**되는 효과.

**원리** — 같은 변위맵을 R/G/B 각각 **다른 `scale`** 로 3번 적용한 뒤 `screen`으로 합친다. 굴절률이 파장마다 다른 물리 현상을 흉내낸다.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="lensRGB" x="0%" y="0%" width="100%" height="100%"
            color-interpolation-filters="sRGB">
      <feImage preserveAspectRatio="none" x="0" y="0" width="200" height="96" result="map"
        href="data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='96'%3E%3Cdefs%3E%3ClinearGradient id='rx' x1='0' y1='0' x2='1' y2='0'%3E%3Cstop offset='0' stop-color='rgb(0,0,0)'/%3E%3Cstop offset='0.3' stop-color='rgb(128,0,0)'/%3E%3Cstop offset='0.7' stop-color='rgb(128,0,0)'/%3E%3Cstop offset='1' stop-color='rgb(255,0,0)'/%3E%3C/linearGradient%3E%3ClinearGradient id='gy' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='rgb(0,0,0)'/%3E%3Cstop offset='0.3' stop-color='rgb(0,128,0)'/%3E%3Cstop offset='0.7' stop-color='rgb(0,128,0)'/%3E%3Cstop offset='1' stop-color='rgb(0,255,0)'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='200' height='96' rx='48' fill='rgb(128,128,128)'/%3E%3Crect width='200' height='96' rx='48' fill='url(%23rx)' style='mix-blend-mode:screen'/%3E%3Crect width='200' height='96' rx='48' fill='url(%23gy)' style='mix-blend-mode:screen'/%3E%3C/svg%3E"/>

      <feDisplacementMap in="SourceGraphic" in2="map" scale="-56"
                         xChannelSelector="R" yChannelSelector="G" result="dR"/>
      <feColorMatrix in="dR" type="matrix" result="cR"
        values="1 0 0 0 0
                0 0 0 0 0
                0 0 0 0 0
                0 0 0 1 0"/>

      <feDisplacementMap in="SourceGraphic" in2="map" scale="-60"
                         xChannelSelector="R" yChannelSelector="G" result="dG"/>
      <feColorMatrix in="dG" type="matrix" result="cG"
        values="0 0 0 0 0
                0 1 0 0 0
                0 0 0 0 0
                0 0 0 1 0"/>

      <feDisplacementMap in="SourceGraphic" in2="map" scale="-64"
                         xChannelSelector="R" yChannelSelector="G" result="dB"/>
      <feColorMatrix in="dB" type="matrix" result="cB"
        values="0 0 0 0 0
                0 0 0 0 0
                0 0 1 0 0
                0 0 0 1 0"/>

      <feBlend in="cR" in2="cG" mode="screen" result="rg"/>
      <feBlend in="rg" in2="cB" mode="screen"/>
    </filter>
  </defs>
</svg>

<div class="prism-pill"></div>
```

```css
.prism-pill {
  width: 200px;
  height: 96px;
  border-radius: 48px;
  backdrop-filter: url(#lensRGB);
  border: 1px solid rgba(255, 255, 255, 0.4);
  box-shadow:
    inset 0 1px 2px rgba(255, 255, 255, 0.6),
    0 10px 24px rgba(0, 0, 0, 0.5);
}

@supports not (backdrop-filter: url(#lensRGB)) {
  .prism-pill { backdrop-filter: blur(12px) saturate(1.6); }
}
```

**파라미터 가이드** — 3개 `scale`의 **차이**가 색수차의 세기다.

| scale 조합 | 결과 |
|---|---|
| `-58 / -60 / -62` | 아주 미묘한 프리즘. **프로덕션 권장** |
| `-56 / -60 / -64` | 뚜렷한 무지개 테두리 |
| `-50 / -60 / -70` | 과장된 사이키델릭 |

> 검증됨. 단 `-56/-64/-72`처럼 차이를 크게 벌리면 배경이 줄무늬 무지개로 완전히 분해되니 주의.

**성능: 하** — 변위 3회 + 색행렬 3회 + 블렌드 2회 = 프리미티브 8개. 페이지에 **1~2개만** 쓴다. 모바일에서는 레시피 05나 07로 폴백하라.

---

## 07. 프로스티드 글래스 + 노이즈 (범용 폴백)

**언제 쓰나** — 모든 브라우저에서 동작해야 하는 유리 UI. **레시피 05/06의 기본 레이어로 항상 깔아둔다.**

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="frostNoise" x="0%" y="0%" width="100%" height="100%">
      <feTurbulence type="fractalNoise" baseFrequency="0.9"
                    numOctaves="3" stitchTiles="stitch" result="n"/>
      <feColorMatrix in="n" type="saturate" values="0"/>
    </filter>
  </defs>
</svg>

<div class="frost">FROSTED</div>
```

```css
.frost {
  position: relative;
  width: 230px;
  height: 110px;
  border-radius: 16px;
  display: grid;
  place-items: center;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: #fff;

  backdrop-filter: blur(12px) saturate(1.4);
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.3);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}

/* 유리 표면의 미세 결정감 */
.frost::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: #fff;
  filter: url(#frostNoise);
  mix-blend-mode: overlay;
  opacity: 0.35;
}
```

**파라미터 가이드**

| 조절 | 값 |
|---|---|
| 흐림 정도 | `blur()` 6px(얇은 유리) ~ 24px(두꺼운 간유리) |
| 유리 색조 | `background: rgba(...)` 알파 0.06~0.2 |
| 표면 결정감 | `::after`의 `opacity` 0.15~0.5 |
| 결정 크기 | `baseFrequency` 0.6~0.95 |

**성능: 상** — `backdrop-filter: blur()`는 잘 가속된다. 노이즈 오버레이는 정적이므로 캐시된다. 단, `backdrop-filter` 자체가 배경 합성을 강제하므로 **화면 전체에 깔지는 마라.**

---

## 08. 크로마틱 애버레이션 / RGB Split

**언제 쓰나** — 사이버펑크/글리치 타이포, 레트로 CRT, VHS 룩, 호버 시 순간 왜곡.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="chromatic" x="-20%" y="-20%" width="140%" height="140%"
            color-interpolation-filters="sRGB">
      <feOffset in="SourceGraphic" dx="-3" dy="0" result="rShift"/>
      <feColorMatrix in="rShift" type="matrix" result="red"
        values="1 0 0 0 0
                0 0 0 0 0
                0 0 0 0 0
                0 0 0 1 0"/>

      <feColorMatrix in="SourceGraphic" type="matrix" result="green"
        values="0 0 0 0 0
                0 1 0 0 0
                0 0 0 0 0
                0 0 0 1 0"/>

      <feOffset in="SourceGraphic" dx="3" dy="0" result="bShift"/>
      <feColorMatrix in="bShift" type="matrix" result="blue"
        values="0 0 0 0 0
                0 0 0 0 0
                0 0 1 0 0
                0 0 0 1 0"/>

      <feBlend in="red" in2="green" mode="screen" result="rg"/>
      <feBlend in="rg" in2="blue" mode="screen"/>
    </filter>
  </defs>
</svg>

<h1 class="chroma">GLITCH</h1>
```

```css
.chroma {
  font-size: 44px;
  font-weight: 800;
  color: #fff;             /* 흰색 소스여야 RGB 분리가 선명하다 */
  filter: url(#chromatic);
}
```

**렌즈 색수차 변형** (블러 포함, 사진에 적용)

```xml
<filter id="chromaticLens" x="-10%" y="-10%" width="120%" height="120%"
        color-interpolation-filters="sRGB">
  <feColorMatrix in="SourceGraphic" type="matrix"
    values="1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0"/>
  <feOffset dx="2" dy="0"/>
  <feGaussianBlur stdDeviation="2" result="redChannel"/>

  <feColorMatrix in="SourceGraphic" type="matrix"
    values="0 0 0 0 0  0 1 0 0 0  0 0 0 0 0  0 0 0 1 0"/>
  <feGaussianBlur stdDeviation="0.5" result="greenChannel"/>

  <feColorMatrix in="SourceGraphic" type="matrix"
    values="0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0"/>
  <feOffset dx="-2" dy="0"/>
  <feGaussianBlur stdDeviation="2" result="blueChannel"/>

  <feBlend in="redChannel" in2="greenChannel" mode="screen" result="redGreen"/>
  <feBlend in="redGreen" in2="blueChannel" mode="screen"/>
</filter>
```

**파라미터 가이드**

| 조절 | 값 |
|---|---|
| 분리 강도 | `dx`: ±1(미묘) / ±3(표준) / ±8(강한 글리치) |
| 수직 분리 | `dy`도 함께 주면 대각 분리 |
| 초점 흐림 | 각 채널 `stdDeviation` — G는 낮게(0.5), R/B는 높게(2) 두면 렌즈 느낌 |

**왜 `screen`인가** — R/G/B 각각을 분리하면 나머지 채널이 0(검정)이다. `screen`은 가산 합성이라 세 채널을 원래 색으로 되돌린다. `normal`이면 마지막 채널만 보인다.

**함정** — **소스가 유채색이면 결과가 탁해진다.** 흰색/밝은 회색 텍스트에서 가장 잘 나온다.

**성능: 중** — 프리미티브 7개지만 전부 가벼운 연산. 호버 트리거로 짧게 쓰는 것을 권장.

---

## 09. 손그림 러프 엣지

**언제 쓰나** — 딱딱한 사각형/버튼을 손으로 그린 듯 만들기. 일러스트레이션 톤의 브랜드, 노트/스케치 UI.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="rough" x="-10%" y="-10%" width="120%" height="120%">
      <feTurbulence type="fractalNoise" baseFrequency="0.03"
                    numOctaves="4" seed="12" result="n"/>
      <feDisplacementMap in="SourceGraphic" in2="n" scale="9"
                         xChannelSelector="R" yChannelSelector="G"/>
    </filter>
  </defs>
</svg>

<div class="rough-card">Hand drawn</div>
```

```css
.rough-card {
  width: 220px;
  height: 130px;
  background: #f59e0b;
  border-radius: 10px;
  filter: url(#rough);
}
```

**파라미터 가이드**

| 목표 | baseFrequency | numOctaves | scale |
|---|---|---|---|
| 아주 미세한 손떨림 | 0.02 | 2 | 3 |
| 표준 손그림 | 0.03 | 4 | 9 |
| 거친 크레용 | 0.05 | 5 | 14 |
| 찢어진 종이 | 0.08 | 3 | 20 |

- **`seed`를 바꾸면 다른 "손"이 된다.** 여러 요소에 같은 필터를 쓰면 전부 똑같이 흔들려 부자연스럽다. seed만 다른 필터를 3~4개 만들어 돌려 쓰면 살아난다.

```xml
<filter id="rough1"><feTurbulence type="fractalNoise" baseFrequency="0.03" numOctaves="4" seed="1" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="9" xChannelSelector="R" yChannelSelector="G"/></filter>
<filter id="rough2"><feTurbulence type="fractalNoise" baseFrequency="0.03" numOctaves="4" seed="7" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="9" xChannelSelector="R" yChannelSelector="G"/></filter>
<filter id="rough3"><feTurbulence type="fractalNoise" baseFrequency="0.03" numOctaves="4" seed="23" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="9" xChannelSelector="R" yChannelSelector="G"/></filter>
```

**텍스트에 적용할 때** — `scale`을 **4 이하**로. 그 이상은 판독성이 급격히 떨어진다.

**성능: 중** — 정적이면 안전. seed/scale 애니메이션은 §11 참고.

---

## 10. 잉크 번짐 / 마커 필

**언제 쓰나** — 스탬프, 마커 하이라이트, 리소/실크스크린 텍스트, 손글씨 느낌 강조.

**원리** — 변위로 윤곽을 흐트러뜨리고 → 블러로 번지게 하고 → 알파 대비로 다시 단단한 경계를 만든다. 잉크가 종이에 스며든 뒤 마른 결과와 같은 순서다.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="inkbleed" x="-20%" y="-20%" width="140%" height="140%"
            color-interpolation-filters="sRGB">
      <feTurbulence type="fractalNoise" baseFrequency="0.05"
                    numOctaves="4" seed="7" result="n"/>
      <feDisplacementMap in="SourceGraphic" in2="n" scale="4"
                         xChannelSelector="R" yChannelSelector="G" result="d"/>
      <feGaussianBlur in="d" stdDeviation="1.2" result="b"/>
      <feColorMatrix in="b" type="matrix"
        values="1 0 0 0  0
                0 1 0 0  0
                0 0 1 0  0
                0 0 0 14 -6"/>
    </filter>
  </defs>
</svg>

<span class="ink">INK</span>
```

```css
.ink {
  font-size: 40px;
  font-weight: 800;
  color: #fff;
  filter: url(#inkbleed);
}
```

**파라미터 가이드**

| 조절 | 효과 |
|---|---|
| `scale` 2~6 | 윤곽의 불규칙함 |
| `stdDeviation` 0.8~2.5 | 번짐 정도. 크면 글자가 뭉친다 |
| 알파 행렬 `M`(14) | 크면 경계가 날카롭고 "마른 잉크", 작으면 "젖은 잉크" |
| `O/M`(6/14≈0.43) | 크면 글자가 얇아지고, 작으면 두꺼워진다 |
| `baseFrequency` 0.03~0.09 | 번짐 결의 크기 |

**성능: 중** — 텍스트처럼 작은 면적에만.

---

## 11. Squigglevision (애니메이션 손그림)

**언제 쓰나** — 애니메이션 일러스트, 놀이/교육 UI, 브랜드 마스코트. **저프레임 스톱모션 느낌**이 핵심이라 60fps로 돌리면 안 된다.

**원리** — 3~4개의 서로 다른 `seed`를 **낮은 프레임레이트로 순환**시킨다. 매 프레임 새 노이즈를 만드는 게 아니라 몇 장의 "그림"을 번갈아 보여주는 것이라 훨씬 싸고, 룩도 더 정확하다.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="squiggle" x="-10%" y="-10%" width="120%" height="120%">
      <feTurbulence id="squiggleNoise" type="fractalNoise"
                    baseFrequency="0.02" numOctaves="3" seed="1" result="n"/>
      <feDisplacementMap in="SourceGraphic" in2="n" scale="6"
                         xChannelSelector="R" yChannelSelector="G"/>
    </filter>
  </defs>
</svg>

<div class="squiggle-art">
  <svg viewBox="0 0 200 120" width="200">
    <rect x="20" y="20" width="160" height="80" rx="8"
          fill="none" stroke="#0ea5e9" stroke-width="4"/>
    <path d="M40 90 L80 40 L120 80 L160 30" fill="none"
          stroke="#f43f5e" stroke-width="4" stroke-linecap="round"/>
  </svg>
</div>
```

```css
.squiggle-art { filter: url(#squiggle); }

@media (prefers-reduced-motion: reduce) {
  .squiggle-art { filter: none; }
}
```

```js
(function squigglevision() {
  const noise = document.getElementById('squiggleNoise');
  if (!noise) return;

  // 접근성: 감소 모션을 켠 사용자는 애니메이션 없이 정지 상태 유지
  const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
  if (mq.matches) return;

  const seeds = [1, 2, 3, 4];
  let i = 0;
  let timer = null;

  function tick() {
    noise.setAttribute('seed', String(seeds[i++ % seeds.length]));
  }

  function start() {
    if (timer === null) timer = setInterval(tick, 120); // ≈8fps
  }
  function stop() {
    if (timer !== null) { clearInterval(timer); timer = null; }
  }

  // 탭이 백그라운드면 멈춘다 (배터리 보호)
  document.addEventListener('visibilitychange', () => {
    document.hidden ? stop() : start();
  });

  // 화면 밖이면 멈춘다
  const target = document.querySelector('.squiggle-art');
  if (target && 'IntersectionObserver' in window) {
    new IntersectionObserver(entries => {
      entries[0].isIntersecting ? start() : stop();
    }, { threshold: 0 }).observe(target);
  } else {
    start();
  }

  mq.addEventListener('change', e => { e.matches ? stop() : start(); });
})();
```

**파라미터 가이드**

| 조절 | 값 |
|---|---|
| 프레임레이트 | `setInterval` 100~200ms. **80ms 미만은 노이즈 지글거림으로 보인다** |
| 흔들림 크기 | `scale` 3(미묘) ~ 10(과장) |
| 시드 개수 | 3~4개. 많을수록 불규칙하지만 반복 주기가 길어짐 |

**성능: 하** — feTurbulence를 초당 8회 재생성한다. **반드시** IntersectionObserver + visibilitychange로 게이팅하고, 적용 면적을 300×300px 이하로 제한하라. 모바일에서는 아예 끄는 것을 권장(§03).

---

## 12. 물결 텍스트 (SMIL)

**언제 쓰나** — 히어로 타이틀의 미묘한 액체 모션, 수중 테마, 열기 아지랑이.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="wave" x="-15%" y="-40%" width="130%" height="180%">
      <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>
      <feDisplacementMap in="SourceGraphic" in2="n" scale="7"
                         xChannelSelector="R" yChannelSelector="G"/>
    </filter>
  </defs>
</svg>

<h1 class="wave">LIQUID</h1>
```

```css
.wave {
  font-size: 40px;
  font-weight: 800;
  color: #facc15;
  filter: url(#wave);
}

@media (prefers-reduced-motion: reduce) {
  .wave { filter: none; }
}
```

**파라미터 가이드**

| 조절 | 효과 |
|---|---|
| `baseFrequency="0.008 0.04"` | x는 크게, y는 촘촘 → **가로로 흐르는 물결**. 값을 뒤집으면 세로 물결 |
| `scale` 4~8 | 텍스트 판독성 한계는 대략 8 |
| `dur` 6~12s | 느릴수록 고급스럽다. 3s 이하는 어지럽다 |
| `type="turbulence"` | 물결에 적합. `fractalNoise`는 더 부드럽고 뭉근함 |

**대안: `scale`만 애니메이션** — `baseFrequency`를 바꾸면 매 프레임 노이즈를 새로 만든다. `scale`만 애니메이션하면 **노이즈는 캐시되고 변위만 바뀌어 훨씬 싸다.**

```xml
<filter id="waveCheap" x="-15%" y="-40%" width="130%" height="180%">
  <feTurbulence type="turbulence" baseFrequency="0.01 0.05"
                numOctaves="2" seed="2" result="n"/>
  <feDisplacementMap in="SourceGraphic" in2="n" scale="0"
                     xChannelSelector="R" yChannelSelector="G">
    <animate attributeName="scale" dur="6s"
             values="2;9;2" repeatCount="indefinite"/>
  </feDisplacementMap>
</filter>
```
> 룩은 "흐르는" 대신 "숨쉬는" 쪽에 가깝지만 비용이 크게 낮다. 대부분의 경우 이쪽으로 충분하다.

**성능: 하** (`baseFrequency` 애니메이션) / **중** (`scale` 애니메이션)

---

## 13. 듀오톤 / 그라디언트 맵

**언제 쓰나** — 브랜드 컬러로 사진 톤을 통일. 잡다한 스톡 이미지들을 한 화면에 놓아도 일관돼 보이게 만드는 가장 강력한 기법.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="duotone" color-interpolation-filters="sRGB">
      <!-- 1) Rec.709 휘도로 그레이스케일 -->
      <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"/>
      <!-- 2) 그림자 #0D1A59 → 하이라이트 #FA591A 로 리매핑 -->
      <feComponentTransfer>
        <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>
  </defs>
</svg>

<img class="duo" src="photo.jpg" alt="설명">
```

```css
.duo {
  filter: url(#duotone);
  width: 100%;
  height: auto;
}
```

**임의의 두 색으로 만드는 공식**

```
그림자색 #S_R S_G S_B, 하이라이트색 #H_R H_G H_B  (각 0~255)

feFuncR tableValues = "S_R/255  H_R/255"
feFuncG tableValues = "S_G/255  H_G/255"
feFuncB tableValues = "S_B/255  H_B/255"
```

```js
// 헬퍼: hex 2개 → 듀오톤 필터 문자열
function duotoneFilter(id, shadowHex, highlightHex) {
  const toRGB = h => {
    const n = parseInt(h.replace('#', ''), 16);
    return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
  };
  const s = toRGB(shadowHex);
  const h = toRGB(highlightHex);
  const f = i => `${(s[i] / 255).toFixed(4)} ${(h[i] / 255).toFixed(4)}`;
  return `
<filter id="${id}" 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>
    <feFuncR type="table" tableValues="${f(0)}"/>
    <feFuncG type="table" tableValues="${f(1)}"/>
    <feFuncB type="table" tableValues="${f(2)}"/>
  </feComponentTransfer>
</filter>`;
}
// duotoneFilter('brandDuo', '#12103A', '#FF6B35')
```

**트라이톤** — `tableValues`에 값을 3개 넣는다 (그림자 / 중간톤 / 하이라이트).

```xml
<feComponentTransfer>
  <feFuncR type="table" tableValues="0.05 0.55 0.98"/>
  <feFuncG type="table" tableValues="0.10 0.20 0.35"/>
  <feFuncB type="table" tableValues="0.35 0.45 0.10"/>
</feComponentTransfer>
```

**함정**
- **`color-interpolation-filters="sRGB"`가 없으면 색이 전부 어긋난다.** 이 레시피에서는 필수.
- 원본 대비가 낮은 사진은 듀오톤에서 뭉개진다. 앞에 `feComponentTransfer type="linear" slope="1.3" intercept="-0.15"`로 대비를 올려주면 좋다.

**성능: 상** — 픽셀당 단순 산술 2회. 이미지에 적용해도 부담 없다.

---

## 14. 포스터화 / 컬러 밴딩

**언제 쓰나** — 스크린프린트, 레트로 게임, 벡터 일러스트 느낌. 그라디언트를 계단으로 만든다.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="posterize" color-interpolation-filters="sRGB">
      <feComponentTransfer>
        <feFuncR type="discrete" tableValues="0 0.25 0.5 0.75 1"/>
        <feFuncG type="discrete" tableValues="0 0.25 0.5 0.75 1"/>
        <feFuncB type="discrete" tableValues="0 0.25 0.5 0.75 1"/>
      </feComponentTransfer>
    </filter>
  </defs>
</svg>
```

```css
.poster { filter: url(#posterize); }
```

**파라미터 가이드**

| tableValues | 단계 수 | 룩 |
|---|---|---|
| `0 1` | 2 | 하드 임계값(흑백 스텐실) |
| `0 0.5 1` | 3 | 강한 실크스크린 |
| `0 0.25 0.5 0.75 1` | 5 | 표준 포스터화 |
| `0 0.14 0.28 0.42 0.57 0.71 0.85 1` | 8 | 미묘한 밴딩 |

**채널별로 다른 단계 수**를 주면 색이 이동하며 인쇄 오차 느낌이 난다:
```xml
<feFuncR type="discrete" tableValues="0 0.5 1"/>
<feFuncG type="discrete" tableValues="0 0.33 0.66 1"/>
<feFuncB type="discrete" tableValues="0 1"/>
```

**듀오톤과 결합** — 레시피 13의 그레이스케일 뒤에 `discrete`를 넣으면 **2색 스크린프린트**가 된다.

**성능: 상**

---

## 15. 디더 / 임계값 (하프톤 대체)

**언제 쓰나** — 1비트 룩, 레트로 맥/게임보이, 리소 하프톤 근사. 그라디언트를 점 패턴으로 분해한다.

**원리** — 그레이 노이즈를 소스에 **가산**한 뒤 2단계 `discrete`로 자른다. 노이즈가 임계선 주변을 흩뜨려 점 패턴이 생긴다.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="dither" x="0%" y="0%" width="100%" height="100%"
            color-interpolation-filters="sRGB">
      <feTurbulence type="fractalNoise" baseFrequency="0.75"
                    numOctaves="1" seed="9" result="n"/>
      <!-- 노이즈를 불투명 그레이로 (알파 노이즈 제거) -->
      <feColorMatrix in="n" type="matrix" result="gray"
        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"/>
      <!-- 소스 + (노이즈 × 0.5 − 0.25) -->
      <feComposite in="SourceGraphic" in2="gray" operator="arithmetic"
                   k1="0" k2="1" k3="0.5" k4="-0.25" result="mixed"/>
      <feComponentTransfer in="mixed">
        <feFuncR type="discrete" tableValues="0 1"/>
        <feFuncG type="discrete" tableValues="0 1"/>
        <feFuncB type="discrete" tableValues="0 1"/>
      </feComponentTransfer>
    </filter>
  </defs>
</svg>
```

```css
.dither {
  width: 220px;
  height: 150px;
  background: linear-gradient(90deg, #000, #fff);
  filter: url(#dither);
}
```

**파라미터 가이드**

| 조절 | 효과 |
|---|---|
| `k3` (0.5) | 노이즈 진폭 = 디더 밴드의 **폭**. 0.3(좁음) ~ 0.8(넓음) |
| `k4` (−0.25) | 항상 `−k3/2`로 두어 노이즈를 0 중심으로 만든다 |
| `baseFrequency` 0.5~0.9 | 점 크기 |
| `tableValues="0 0.5 1"` | 3단계 디더 |

**색 디더** — 임계 후 `feColorMatrix`로 2색 매핑하면 리소 잉크 느낌이 난다.

**성능: 중** — feTurbulence + 산술 합성. 정적이면 안전, 애니메이션은 피하라.

---

## 16. 리소그래프 인쇄

**언제 쓰나** — 잡지/포스터 감성, 인디 브랜드, 제한 팔레트 아트디렉션.

**리소의 3요소**: ① 제한된 스팟 컬러 ② `multiply`로 겹치는 잉크 ③ 판 어긋남(misregistration) + 종이 그레인.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="risoGrain" x="0%" y="0%" width="100%" height="100%"
            color-interpolation-filters="sRGB">
      <feTurbulence type="fractalNoise" baseFrequency="0.7"
                    numOctaves="2" stitchTiles="stitch" seed="11" result="n"/>
      <!-- RGB는 흰색 상수, 알파만 노이즈 → 흰 바탕에 랜덤 투명도 -->
      <feColorMatrix in="n" type="matrix"
        values="0   0   0   0 1
                0   0   0   0 1
                0   0   0   0 1
                0.4 0.4 0.4 0 -0.1"/>
    </filter>
  </defs>
</svg>

<div class="riso">
  <i class="ink-a"></i>
  <i class="ink-b"></i>
</div>
```

```css
.riso {
  --misreg-x: 2px;
  --misreg-y: 1.5px;

  position: relative;
  width: 230px;
  height: 160px;
  background: #f2ece0;      /* 종이색 */
  border-radius: 6px;
  overflow: hidden;
  isolation: isolate;
}

.riso i {
  position: absolute;
  display: block;
  width: 110px;
  height: 110px;
  top: 25px;
  border-radius: 50%;
  mix-blend-mode: multiply;  /* 잉크가 겹치면 어두워진다 */
}

.riso .ink-a {
  left: 20px;
  background: #ff4f39;       /* Riso Fluorescent Pink 근사 */
  transform: translate(calc(var(--misreg-x) * -1), calc(var(--misreg-y) * -1));
}

.riso .ink-b {
  left: 90px;
  background: #2f6bff;       /* Riso Blue 근사 */
  transform: translate(var(--misreg-x), var(--misreg-y));
}

/* 종이 그레인 */
.riso::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: #fff;
  filter: url(#risoGrain);
  mix-blend-mode: multiply;
  opacity: 0.5;
}
```

**파라미터 가이드**

| 조절 | 값 |
|---|---|
| 판 어긋남 | `--misreg-x/y` 0.5~4px. 실제 리소는 최대 3mm까지 어긋난다 |
| 잉크 팔레트 | 형광핑크 `#FF48B0`, 블루 `#0078BF`, 옐로 `#FFE800`, 그린 `#00A95C` |
| 종이색 | `#F2ECE0`, `#EFE6D4`, `#FBF7F0` |
| 그레인 강도 | `::after`의 `opacity` 0.25~0.6 |
| 그레인 굵기 | `baseFrequency` 0.5(굵음) ~ 0.9(미세) |

**함정** — `mix-blend-mode`가 부모 밖으로 새지 않도록 컨테이너에 **`isolation: isolate`**를 반드시 걸어라.

**성능: 중** — 잉크 레이어는 순수 CSS라 가볍고, 그레인 필터가 유일한 비용이다. 그레인을 레시피 02(배경 data URI) 방식으로 바꾸면 **상**으로 올라간다.

---

## 17. 텍스트 아웃라인 (feMorphology)

**언제 쓰나** — 스티커 타이포, 코믹/포스터, 배경 위 텍스트 가독성 확보. CSS `-webkit-text-stroke`와 달리 **글자 두께를 깎지 않는다.**

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <!-- (A) 컬러 아웃라인 -->
    <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>

    <!-- (B) 속 빈 아웃라인 -->
    <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>
  </defs>
</svg>

<h1 class="outlined">OUTLINE</h1>
<h1 class="hollow">HOLLOW</h1>
```

```css
.outlined { font-size: 44px; font-weight: 900; color: #111; filter: url(#outline); }
.hollow   { font-size: 52px; font-weight: 900; color: #f472b6; filter: url(#knockout); }
```

**파라미터 가이드**

| 조절 | 값 |
|---|---|
| 선 두께 | `radius` 1~8. 8 초과는 모서리가 각지게 뭉개진다 |
| 이중 아웃라인 | `feMorphology`를 두 번 체인 (radius 6 → 3), 색 두 개 |
| 굵은 글씨 | `operator="dilate"`를 `SourceGraphic`에 걸면 원본 색 그대로 굵어진다 |

**이중 아웃라인 전체 코드**

```xml
<filter id="doubleOutline" x="-30%" y="-30%" width="160%" height="160%"
        color-interpolation-filters="sRGB">
  <feMorphology in="SourceAlpha" operator="dilate" radius="8" result="outer"/>
  <feFlood flood-color="#0f172a" result="outerCol"/>
  <feComposite in="outerCol" in2="outer" operator="in" result="outerRing"/>

  <feMorphology in="SourceAlpha" operator="dilate" radius="4" result="inner"/>
  <feFlood flood-color="#fbbf24" result="innerCol"/>
  <feComposite in="innerCol" in2="inner" operator="in" result="innerRing"/>

  <feMerge>
    <feMergeNode in="outerRing"/>
    <feMergeNode in="innerRing"/>
    <feMergeNode in="SourceGraphic"/>
  </feMerge>
</filter>
```

**성능: 상** — 텍스트는 선택·검색·스크린리더 접근이 그대로 유지된다.

---

## 18. 스티커

**언제 쓰나** — 배지, 이모지 스타일 라벨, 컷아웃 요소. 흰 테두리 + 아래로 떨어지는 그림자.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="sticker" x="-30%" y="-30%" width="160%" height="160%"
            color-interpolation-filters="sRGB">
      <feMorphology in="SourceAlpha" operator="dilate" radius="8" result="outer"/>
      <feFlood flood-color="#ffffff" result="white"/>
      <feComposite in="white" in2="outer" operator="in" result="whiteRing"/>
      <feDropShadow in="whiteRing" dx="0" dy="4" stdDeviation="4"
                    flood-color="#000000" flood-opacity="0.35" result="shadowed"/>
      <feMerge>
        <feMergeNode in="shadowed"/>
        <feMergeNode in="SourceGraphic"/>
      </feMerge>
    </filter>
  </defs>
</svg>

<span class="sticker">STICKER</span>
```

```css
.sticker {
  font-size: 44px;
  font-weight: 900;
  color: #111;
  filter: url(#sticker);
}
```

**파라미터 가이드**

| 조절 | 값 |
|---|---|
| 테두리 두께 | `radius` 4(얇음) ~ 14(두꺼움) |
| 떠 있는 높이 | `dy` 2~8, `stdDeviation` 2~8 |
| 그림자 세기 | `flood-opacity` 0.2~0.5 |
| 테두리 색 | `flood-color` — 흰색 대신 배경보다 밝은 색도 좋다 |

**이미지에도 적용 가능** — 투명 PNG 컷아웃에 그대로 걸면 스티커가 된다.

**성능: 상**

---

## 19. 네온 글로우

**언제 쓰나** — 다크 UI의 강조, 사이버펑크, 라이브/온에어 표시.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <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>
  </defs>
</svg>

<h1 class="neon">NEON</h1>
```

```css
.neon {
  font-size: 40px;
  font-weight: 900;
  color: #fff;
  filter: url(#neon);
}
```

**애니메이션 글로우 링** — `flood-color`는 CSS/SMIL 애니메이션이 가능하다.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="pulseRing" x="-20%" y="-20%" width="140%" height="140%"
            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="#22d3ee" result="col">
        <animate attributeName="flood-color"
                 values="#22d3ee;#a855f7;#f43f5e;#22d3ee"
                 dur="4s" repeatCount="indefinite"/>
      </feFlood>
      <feComposite in="col" in2="ring" operator="in" result="glowRing"/>
      <feGaussianBlur in="glowRing" stdDeviation="4" result="glow"/>
      <feMerge>
        <feMergeNode in="glow"/>
        <feMergeNode in="glowRing"/>
        <feMergeNode in="SourceGraphic"/>
      </feMerge>
    </filter>
  </defs>
</svg>
```

```css
.pulse-card {
  width: 200px;
  height: 130px;
  background: #111827;
  border-radius: 12px;
  filter: url(#pulseRing);
}
@media (prefers-reduced-motion: reduce) {
  .pulse-card { filter: none; box-shadow: 0 0 0 3px #22d3ee; }
}
```

**성능: 중** — `stdDeviation="14"` 블러가 주 비용. 애니메이션은 `flood-color`만 바꾸므로(형태 재계산 없음) 비교적 저렴하다.

---

## 20. 스포트라이트

**언제 쓰나** — 히어로 포커스, 카드 호버 강조, 어두운 씬의 연출.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <!-- (A) 빛을 더하는 방식 -->
    <filter id="spotAdd" 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 id="spotSource"
                     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>

    <!-- (B) 음영을 만드는 방식(비네트) -->
    <filter id="spotShade" 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.6"
                         lighting-color="#ffffff" result="light">
        <feSpotLight x="55" y="15" z="90"
                     pointsAtX="110" pointsAtY="80" pointsAtZ="0"
                     specularExponent="4" limitingConeAngle="55"/>
      </feDiffuseLighting>
      <feComposite in="light" in2="SourceAlpha" operator="in" result="lightClipped"/>
      <feBlend in="SourceGraphic" in2="lightClipped" mode="multiply"/>
    </filter>
  </defs>
</svg>

<div class="spot-card">Focus</div>
```

```css
.spot-card {
  width: 220px;
  height: 150px;
  background: #334155;
  border-radius: 14px;
  filter: url(#spotAdd);
}
```

**포인터 추적 스포트라이트**

```js
(function trackSpotlight() {
  const card = document.querySelector('.spot-card');
  const light = document.getElementById('spotSource');
  if (!card || !light) return;
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;

  let raf = null;
  let pending = null;

  function apply() {
    raf = null;
    if (!pending) return;
    light.setAttribute('x', String(pending.x));
    light.setAttribute('y', String(pending.y));
    light.setAttribute('pointsAtX', String(pending.x));
    light.setAttribute('pointsAtY', String(pending.y));
    pending = null;
  }

  card.addEventListener('pointermove', e => {
    const r = card.getBoundingClientRect();
    pending = { x: Math.round(e.clientX - r.left), y: Math.round(e.clientY - r.top) };
    if (raf === null) raf = requestAnimationFrame(apply);
  });

  card.addEventListener('pointerleave', () => {
    pending = { x: 55, y: 15 };
    if (raf === null) raf = requestAnimationFrame(apply);
  });
})();
```
> 검증됨: `setAttribute`로 `feSpotLight`의 `x/y/pointsAtX/pointsAtY`를 바꾸면 조명 위치가 즉시 갱신된다(광원을 좌상단→우하단으로 옮겨 빛 웅덩이가 따라 이동함을 확인). 포인터 이벤트 결선 자체는 표준 DOM API라 별도 위험이 없다.

**파라미터 가이드**

| 조절 | 효과 |
|---|---|
| `limitingConeAngle` 20(좁은 핀조명) ~ 70(넓은 조명) | 원뿔 반각 |
| `specularExponent` 1(균일) ~ 20(중심 집중) | 감쇠 |
| `z` 50(가까움/강함) ~ 200(멀음/균일) | 광원 거리 |
| `lighting-color` | 조명 색 — CSS 애니메이션 가능 |
| `mode="screen"` vs `"multiply"` | 밝히기 vs 어둡게(비네트) |

**함정** — `feComposite operator="in"` + `SourceAlpha` 클리핑을 빼면 필터 영역 전체가 불투명 조명 사각형이 된다.

**성능: 중** — 조명 연산은 픽셀당 노멀 계산이 들어가 블러보다 비싸다. 포인터 추적은 rAF로 throttle 필수.

---

## 21. 광택 / 엠보스

**언제 쓰나** — 3D 느낌 버튼, 스큐어모픽 요소, 젤리/캔디 UI.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="glossy" 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>
  </defs>
</svg>

<button class="glossy-btn">Press</button>
```

```css
.glossy-btn {
  width: 200px;
  height: 140px;
  background: #3b82f6;
  border: 0;
  border-radius: 20px;
  color: transparent;      /* 텍스트는 필터 밖 레이어에 두는 것을 권장 */
  filter: url(#glossy);
  cursor: pointer;
}
```

**파라미터 가이드**

| 조절 | 효과 |
|---|---|
| `stdDeviation`(범프 블러) 3~10 | 클수록 모서리가 둥근 3D |
| `surfaceScale` 2~10 | 돌출 높이 |
| `specularExponent` 5(넓은 광택) ~ 60(핀 하이라이트) | 광택 집중도 |
| `fePointLight z` 60(가까움) ~ 250(멀음) | 하이라이트 크기 |
| `specularConstant` 0.5~2 | 밝기 |

**엠보스 변형** (`feConvolveMatrix` — 돌/콘크리트 각인 질감)

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="embossTexture" x="0%" y="0%" width="100%" height="100%"
            color-interpolation-filters="sRGB">
      <feTurbulence type="fractalNoise" baseFrequency="0.06"
                    numOctaves="3" seed="4" result="n"/>
      <!-- 노이즈를 불투명 그레이로 -->
      <feColorMatrix in="n" type="matrix" result="g"
        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"/>
      <!-- 합이 0인 커널 + bias 0.5 = 중간 회색 기준 엠보스 -->
      <feConvolveMatrix in="g" order="3" preserveAlpha="true" divisor="1" bias="0.5"
                        kernelMatrix="-2 -1 0
                                      -1  0 1
                                       0  1 2" result="emb"/>
      <feComposite in="emb" in2="SourceAlpha" operator="in"/>
    </filter>
  </defs>
</svg>
```

```css
.stone { width: 180px; height: 130px; background: #8b8b8b;
         border-radius: 12px; filter: url(#embossTexture); }
```

> 검증됨. **커널의 합이 결과를 좌우한다**는 점을 기억하라.
> - 합 = 1 (`-2 -1 0 / -1 1 1 / 0 1 2`), `bias="0"` → 원본 밝기를 유지하며 엣지만 강조. 부드러운 그라디언트에는 거의 변화가 없다.
> - 합 = 0 (`-2 -1 0 / -1 0 1 / 0 1 2`), `bias="0.5"` → 엣지가 없는 곳은 **중간 회색**, 엣지에서만 밝고 어두워진다. **입력에 텍스처(노이즈)가 있어야 의미가 있다.**
> - 밝은 소스에 합=1 커널 + `bias="0.5"`를 쓰면 값이 1을 넘어 **전체가 흰색으로 날아간다**(실제로 재현함).

**샤픈** (변위 후 흐려진 엣지 복구용)
```xml
<feConvolveMatrix order="3" preserveAlpha="true"
                  kernelMatrix="0 -1 0  -1 5 -1  0 -1 0"/>
```

**함정** — 필터를 건 요소 안의 텍스트도 조명 처리되어 뿌옇게 된다. 텍스트는 별도 레이어로 얹어라.

**성능: 중**

---

## 22. 글리치 밴드

**언제 쓰나** — 로딩 실패 상태, 사이버펑크 전환 효과, 에러 화면. **순간적으로만** 쓴다.

**원리** — 대부분이 중립색(127,127)인 평면 위에, 얇은 가로 밴드만 다른 색으로 칠한 이미지를 변위맵으로 쓴다. 밴드가 지나가는 줄만 가로로 밀린다.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <defs>
    <filter id="glitch" x="-10%" y="-10%" width="120%" height="120%"
            primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
      <!-- 전체를 중립으로 채움: 변위 0 -->
      <feFlood flood-color="rgb(127,127,127)" x="0" y="0" width="100%" height="100%"
               result="neutral"/>

      <!-- 밴드 1 -->
      <feFlood flood-color="rgb(200,127,127)" x="0" y="0" width="100%" height="14"
               result="band1">
        <animate attributeName="y" dur="2.7s"
                 values="0;40;12;90;30;120;0" repeatCount="indefinite"/>
      </feFlood>

      <!-- 밴드 2 -->
      <feFlood flood-color="rgb(60,127,127)" x="0" y="0" width="100%" height="8"
               result="band2">
        <animate attributeName="y" dur="1.9s"
                 values="120;20;100;55;10;80;120" repeatCount="indefinite"/>
      </feFlood>

      <feMerge result="map">
        <feMergeNode in="neutral"/>
        <feMergeNode in="band1"/>
        <feMergeNode in="band2"/>
      </feMerge>

      <feDisplacementMap in="SourceGraphic" in2="map" scale="40"
                         xChannelSelector="R" yChannelSelector="G"/>
    </filter>
  </defs>
</svg>

<div class="glitch-box">SYSTEM ERROR</div>
```

```css
.glitch-box {
  width: 260px;
  height: 140px;
  display: grid;
  place-items: center;
  background: #111;
  color: #f43f5e;
  font-weight: 800;
  letter-spacing: 0.1em;
  filter: url(#glitch);
}
@media (prefers-reduced-motion: reduce) {
  .glitch-box { filter: none; }
}
```

> 검증됨: 밴드가 지나가는 줄만 가로로 밀리는 글리치가 정상 렌더된다. 밴드가 안 보이면 `primitiveUnits="userSpaceOnUse"`가 빠졌는지, `height`가 % 단위로 잘못 들어갔는지 먼저 확인하라.

**파라미터 가이드**

| 조절 | 효과 |
|---|---|
| 밴드 색의 R값 | 127에서 멀수록 그 줄이 크게 밀린다 (0~255) |
| `scale` | 전체 밀림 세기 (20~60) |
| 밴드 `height` | 밀리는 줄의 두께 |
| `dur` 값을 서로 소수로 | 반복 주기가 길어져 자연스러움 |

**성능: 하** — SMIL이 매 프레임 맵을 재구성하고 변위를 다시 계산한다. **상시 재생 금지.** 상태 전환 시 0.5~2초만 켜라.

---

## 부록 A. 필터 조합 시 순서 원칙

1. **소스 생성** (`feTurbulence`, `feFlood`, `feImage`) — 재료를 먼저 만든다
2. **기하 변형** (`feDisplacementMap`, `feMorphology`, `feOffset`) — 형태를 바꾼다
3. **블러** (`feGaussianBlur`) — 부드럽게 한다
4. **색/알파 조작** (`feColorMatrix`, `feComponentTransfer`) — 대비·색을 잡는다
5. **합성** (`feComposite`, `feBlend`, `feMerge`) — 원본과 합친다

순서를 바꾸면 결과가 달라진다. 예: 블러 → 알파대비 = gooey(붙는다) / 알파대비 → 블러 = 그냥 흐린 도형.

## 부록 B. 어디에 텍스트를 두는가

필터를 거는 요소 **안**에 텍스트를 두면 텍스트도 필터를 통과한다. 대부분의 레시피에서 이건 버그다.

```html
<!-- ❌ 나쁨: 텍스트가 뭉개진다 -->
<div class="gooey-card">텍스트</div>

<!-- ✅ 좋음: 표면 레이어와 콘텐츠 레이어 분리 -->
<div class="card">
  <div class="card-surface"></div>  <!-- filter: url(#...) -->
  <div class="card-content">텍스트</div>  <!-- 필터 없음 -->
</div>
```

```css
.card { position: relative; }
.card-surface { position: absolute; inset: 0; filter: url(#effect); }
.card-content { position: relative; }
```

예외: 레시피 08(색수차), 09(러프), 10(잉크), 12(물결), 17(아웃라인), 18(스티커), 19(네온)은 **텍스트에 거는 것이 목적**이다.

## 부록 C. 레시피 선택 트리

```
질감을 더하고 싶다
├─ 정적 배경 위 → 02. 필름 그레인 (성능 최우선)
├─ 그라디언트 밴딩 제거 → 03. 그레이니 그라디언트
└─ 물리적 요철 → 04. 종이 질감

배경을 왜곡하고 싶다
├─ 굴절이 꼭 필요 + Chromium 타겟 → 05 / 06
└─ 크로스브라우저 필수 → 07. 프로스티드 글래스

형태를 유기적으로 만들고 싶다
├─ 도형이 서로 융합 → 01. Gooey
├─ 손으로 그린 느낌 → 09 (정적) / 11 (애니메이션)
└─ 액체처럼 흐름 → 12. 물결

색을 통제하고 싶다
├─ 브랜드 2색으로 통일 → 13. 듀오톤
├─ 인쇄물 느낌 → 14 / 15 / 16
└─ 색 분리 효과 → 08. 색수차

빛을 다루고 싶다
├─ 포커스 연출 → 20. 스포트라이트
└─ 입체감 → 21. 광택/엠보스

텍스트를 강조하고 싶다
├─ 배경과 분리 → 17. 아웃라인
├─ 컷아웃 느낌 → 18. 스티커
└─ 발광 → 19. 네온
```
