designpaca 초기 구현 — 스킬 · 설치 CLI · 배포 파이프라인

웹 디자인 파이프라인 스킬과 이를 5개 에이전트에 설치하는 CLI 를 담은 모노레포.

스킬 (packages/skill)
- SKILL.md 261줄 + 참조 문서 16개 3,349줄. progressive disclosure 로
  본문은 절차와 인덱스만, 지식은 references/ 로 분리
- 0~6단계 파이프라인. 규모에 따라 전체·연장·국소 세 경로로 분기
- 하드 게이트 12개는 grep·카운트로 검증 가능한 것만. 취향 판단은 제외
- 미학 프리셋 5종, AI 슬롭 지문 목록, 한글 조판 규칙,
  SVG 필터·three.js·인터랙티브 모션·HTML-in-Canvas 실전 지침

설치 CLI (packages/cli, packages/core)
- npx designpaca 온보딩 TUI. Claude Code · Codex · Cursor · Windsurf · AGENTS.md
- 매니페스트에 설치 시점 해시를 기록해 사용자가 고친 파일은 update 가 건너뛴다
- 타깃별로 본문의 references/ 경로를 실제 설치 위치로 재작성
- AGENTS.md 는 항상 로드되므로 본문 대신 303자 포인터만 주입
- Windsurf 는 12,000자 상한 초과 시 설치를 차단

배포 (build/ci, .forgejo/workflows)
- 태그 v* → 검사·테스트·빌드 → npmjs 배포 + Forgejo 레지스트리 미러
  → draft 릴리스 → Cloudflare Pages. 재실행 멱등

근거 (research/)
- 약 250개 웹 소스 조사 결과와 도그푸딩 검증 2건. 스킬의 모든 수치는 여기서 나온다

테스트 22개 통과 (core 16 · cli 6)
This commit is contained in:
Yun Chan 2026-08-20 10:48:00 +09:00
commit 8808c672dc
135 changed files with 38838 additions and 0 deletions

View file

@ -0,0 +1,865 @@
# 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인가 → 사각 아티팩트

1670
research/svg/02-recipes.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,493 @@
# 03. 성능 · 브라우저 함정 · 폴백
> "SVG 필터는 예쁘다"와 "SVG 필터를 프로덕션에 넣어도 된다"는 다른 문제다. 이 문서는 후자를 위한 것이다.
---
## 1. 비용 모델 — 무엇이 비싼가
### 1.1 기본 공식
```
프레임당 비용 ≈ (필터 영역 픽셀 수) × (프리미티브 수 × 프리미티브별 픽셀당 연산) × (재계산 빈도)
```
세 항 모두가 곱해진다. 하나만 줄여도 효과가 크고, 셋 다 방치하면 폭발한다.
### 1.2 프리미티브별 비용 등급
| 등급 | 프리미티브 | 픽셀당 연산 | 비고 |
|---|---|---|---|
| **매우 저렴** | `feOffset`, `feFlood`, `feMerge`, `feTile` | 복사/상수 | 사실상 공짜 |
| **저렴** | `feColorMatrix`, `feComponentTransfer`, `feBlend`, `feComposite` | 산술 수 회 | 마음껏 써도 됨 |
| **보통** | `feGaussianBlur`, `feDropShadow` | 분리형 컨볼루션 (반경 비례) | 반경이 커지면 급증 |
| **비쌈** | `feMorphology`, `feDisplacementMap` | 이웃 탐색 / 랜덤 액세스 | 캐시 미스 다발 |
| **매우 비쌈** | `feTurbulence`, `feConvolveMatrix`, `feDiffuseLighting`, `feSpecularLighting` | Perlin 반복 / N×N 커널 / 노멀 계산 | **애니메이션 금지 후보** |
`feTurbulence`는 별도로 언급할 가치가 있다. Firefox 버그 트래커에 "far too slow"로 15년 넘게 올라와 있고, 최신 노트북에서도 큰 면적에 걸면 CPU 코어를 즉시 포화시킨다는 보고가 있다. **`numOctaves`가 1 늘어날 때마다 노이즈 계산이 한 층씩 더 쌓인다.** 5 이상은 시각적 이득 대비 손해다.
### 1.3 필터 영역이 곱해지는 방식
`<filter>``width`/`height`는 **처리할 픽셀 수를 직접 결정**한다.
| 필터 영역 | 원본 대비 픽셀 수 |
|---|---|
| `100% × 100%` | 1.00× |
| `120% × 120%` (기본값) | 1.44× |
| `160% × 160%` | 2.56× |
| `180% × 180%` | 3.24× |
| `200% × 200%` | 4.00× |
> 노이즈 오버레이처럼 영역 확장이 필요 없는 필터는 **반드시 `x="0%" y="0%" width="100%" height="100%"`** 로 줄여라. 그것만으로 30% 절약이다.
### 1.4 재계산이 일어나는 시점
필터 결과는 요소가 변하지 않는 한 캐시된다. 아래 상황에서 캐시가 무효화된다.
| 상황 | 재계산 |
|---|---|
| 필터 어트리뷰트 변경 (`baseFrequency`, `scale`, `seed`, `stdDeviation`) | ✅ 전체 |
| `flood-color` / `lighting-color` 변경 | ✅ 전체 (단, 형태 계산은 재사용 가능) |
| 필터 걸린 요소의 **자식이 변함** (텍스트, 애니메이션) | ✅ 전체 |
| 필터 걸린 요소의 **크기 변경** | ✅ 전체 |
| 필터 걸린 요소의 `transform` / `opacity` 변경 | ⚠️ 엔진·상황에 따라 다름. 합성 레이어로 승격되면 재사용되지만, 그렇지 않으면 매 프레임 재계산 |
| `backdrop-filter`에서 **배경이 스크롤/변화** | ✅ 매 프레임 |
> **가장 흔한 성능 사고**: 필터 걸린 요소를 `transform`으로 움직이는 것. 브라우저가 캐시된 레이어를 합성하는 대신 매 프레임 필터 전체를 다시 돌릴 수 있다. **필터와 모션은 서로 다른 요소에 분리하라.**
```html
<!-- ❌ 같은 요소에 필터 + 모션 -->
<div class="blob" style="filter:url(#goo); animation: move 3s infinite"></div>
<!-- ✅ 바깥이 움직이고, 안쪽이 필터를 가진다 (혹은 그 반대) -->
<div class="mover" style="animation: move 3s infinite">
<div class="blob" style="filter:url(#goo)"></div>
</div>
```
### 1.5 `will-change: filter`를 쓰지 마라 (대부분의 경우)
- `will-change: filter`는 요소를 별도 합성 레이어로 승격시키고 **메모리를 상시 점유**한다.
- 필터 자체는 여전히 CPU/GPU에서 재계산된다 — 승격이 필터 연산을 없애주지 않는다.
- 다수 요소에 걸면 레이어 폭발로 오히려 느려진다.
- **예외**: 짧은 인터랙션 직전에 JS로 붙였다가 끝나면 제거하는 패턴은 유효하다.
```js
el.addEventListener('pointerenter', () => { el.style.willChange = 'filter'; });
el.addEventListener('transitionend', () => { el.style.willChange = 'auto'; });
```
### 1.6 하드웨어 가속의 현실
- 엔진별로 가속하는 필터가 다르다. Chrome이 가속하는 것, Firefox가 가속하는 것이 서로 다르고, 일부 안드로이드 GPU는 아무것도 가속하지 못한다.
- `feTurbulence`는 하드웨어 가속에 친화적이지 않은 것으로 알려져 있다.
- **Safari는 CSS 단축 필터(`blur()`, `drop-shadow()` 등)에서는 잘 동작하지만 SVG `<filter>`에서는 문제가 잦다.**
- Firefox에는 "하드웨어 가속을 켜면 대부분의 CSS/SVG 필터가 동작하지 않는다"는 장기 미해결 버그가 보고되어 있다.
**결론**: 가속을 전제로 설계하지 마라. **CPU에서 돌아도 부드러운 수준**으로 설계하고, 가속은 보너스로 취급하라.
---
## 2. CSS 필터 vs SVG 필터 — 선택 기준
| 판단 | 선택 |
|---|---|
| blur / brightness / contrast / saturate / hue-rotate / drop-shadow 만 필요 | **CSS 단축 필터** — 더 빠르고, 더 잘 가속되고, Safari에서 안전하며, 값 보간 애니메이션이 가능 |
| 노이즈, 변위, 채널 분리, 알파 임계, 조명, 커스텀 톤커브 | **SVG 필터** — 대안이 없다 |
| 애니메이션이 필요한 값 | CSS 단축 필터는 **보간된다**. SVG 필터 어트리뷰트는 SMIL/JS만 가능 |
**혼합 사용이 가능하다** — 무거운 SVG 필터는 정적으로 두고, 값이 변하는 부분만 CSS로 처리하는 것이 최적이다.
```css
.card {
/* SVG 필터는 고정, CSS 필터만 트랜지션 */
filter: url(#texture) brightness(1);
transition: filter 240ms ease;
}
.card:hover {
filter: url(#texture) brightness(1.15) saturate(1.2);
}
```
> `filter` 목록에서 `url()`은 그대로 두고 뒤의 CSS 함수만 바뀌면 브라우저가 그 부분만 보간한다.
**CSS 필터로 대체 가능한지 먼저 확인하라.**
| SVG 필터 조합 | CSS 대체 |
|---|---|
| `feColorMatrix type="saturate" values="0"` | `filter: grayscale(1)` (sRGB로 동작) |
| `feGaussianBlur` + `feOffset` + `feFlood` + `feMerge` | `filter: drop-shadow(x y blur color)` |
| `feColorMatrix type="hueRotate"` | `filter: hue-rotate(deg)` |
| 큰 소프트 글로우 | **`radialGradient` 또는 `box-shadow`가 압도적으로 싸다** |
---
## 3. `backdrop-filter` 함정
### 3.1 지원 현황
| 기능 | Chromium | Safari | Firefox |
|---|---|---|---|
| `backdrop-filter: blur()` 등 CSS 함수 | ✅ | ✅ (`-webkit-` 접두사 필요했던 시기 있음) | ✅ |
| **`backdrop-filter: url(#svgFilter)`** | ✅ | ❌ | ❌ |
`backdrop-filter`에 SVG 필터를 넣는 것은 **CSS 스펙에 없는 Chromium 확장**이다. Liquid Glass류 효과가 Chrome에서만 보이는 이유가 이것이다. Firefox에는 기능 요청이 접수되어 있고(2025), `mdn/browser-compat-data`에도 관련 이슈가 열려 있다.
### 3.2 기능 감지가 신뢰할 수 없다 ⚠️
```js
CSS.supports('backdrop-filter', 'url(#x)') // Chromium: true (검증됨)
CSS.supports('backdrop-filter', 'url(#x) brightness(1.1)') // Chromium: true (검증됨)
```
`CSS.supports``@supports`는 **값을 파싱할 수 있는지**만 검사한다. Firefox/Safari도 `url()`을 유효한 `<filter-function>` 문법으로 파싱하므로 `true`를 반환할 가능성이 높다 — 그런데 렌더링은 하지 않는다.
**권장 전략: 감지하지 말고, 계단식으로 쌓아라.**
```css
/* 1단계: 모든 브라우저에서 그럴듯한 기본값 */
.glass {
backdrop-filter: blur(14px) saturate(1.4);
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.28);
box-shadow: inset 0 1px 1px rgba(255,255,255,.6), 0 10px 30px rgba(0,0,0,.4);
}
/* 2단계: 굴절을 지원하면 덮어쓴다.
지원 안 하는 엔진에서는 이 선언이 무효 처리되어 1단계가 남는다. */
@supports (backdrop-filter: url(#lens)) {
.glass { backdrop-filter: url(#lens) brightness(1.06) saturate(1.25); }
}
```
> `@supports`가 통과해버리는 엔진에서는 `backdrop-filter`가 통째로 무시되어 유리 배경만 남는다. 그래서 **`background``box-shadow`만으로도 유리처럼 보이게** 설계해두는 것이 핵심이다.
**확실히 가르고 싶다면 렌더 프로브를 써라** (미검증 — 로직 참고용):
```js
async function supportsBackdropSVGFilter() {
if (!('CSS' in window) || !CSS.supports('backdrop-filter', 'blur(1px)')) return false;
// Chromium 계열만 확실히 지원한다는 사실에 기대는 실용적 판단
const ua = navigator.userAgent;
const isChromium = /Chrome|Chromium|Edg/.test(ua) && !/OPR|Firefox/.test(ua);
const isAppleWebKit = /Safari/.test(ua) && !/Chrome|Chromium|Edg/.test(ua);
return isChromium && !isAppleWebKit;
}
```
> UA 스니핑은 최후의 수단이다. 실패 시 폴백이 충분히 좋다면 감지 자체를 하지 않는 편이 낫다.
### 3.3 크기 불일치
`backdrop-filter`의 필터 영역은 요소 크기에 **자동으로 맞춰지지 않는다.** `feImage` 변위맵의 `width/height`가 요소와 다르면 굴절이 어긋나거나 잘린다.
- 크기별로 별도 맵을 만들어라.
- 반응형으로 크기가 변하는 요소에는 이 기법을 쓰지 마라.
- 크기 변경 애니메이션은 매 프레임 맵 재계산 = 최악의 시나리오.
### 3.4 `backdrop-filter`는 배경 합성을 강제한다
배경이 스크롤하거나 변하면 **매 프레임 배경을 다시 캡처해 필터를 돌린다.** 뷰포트 전체를 덮는 `backdrop-filter`는 스크롤 성능을 파괴한다. 작은 칩/툴바/모달 헤더에만 써라.
---
## 4. 좌표계·색공간 함정 (요약과 진단)
§01에 상세히 있고, 여기서는 **증상 → 원인** 진단표로 정리한다.
| 증상 | 원인 | 처방 |
|---|---|---|
| 효과가 사각형으로 잘림 | 필터 영역이 좁음 | `x/y/width/height` 확대 |
| 색이 물빠진 듯 밝고 채도가 낮음 | linearRGB 기본값 | `color-interpolation-filters="sRGB"` |
| 듀오톤 색이 지정한 hex와 다름 | 위와 동일 | 위와 동일 |
| `stdDeviation="10"`인데 화면이 사라짐 | `primitiveUnits="objectBoundingBox"` | 값을 0~1 스케일로 다시 계산 |
| 변위가 이상한 방향으로 감 | `xChannelSelector`/`yChannelSelector` 기본값이 `A` | `R`/`G` 명시 |
| 색이 전부 흰색으로 날아감 | `feColorMatrix` 값에 0~255를 씀 | 255로 나눠 0~1로 |
| 필터 영역 전체가 단색 사각형 | `feFlood`/조명 결과를 클리핑 안 함 | `feComposite operator="in"` + `SourceAlpha` |
| 분기 배선이 무시됨 | `in`을 명시하지 않아 직전 출력이 들어감 | 양쪽 모두 `in` 명시 |
| 노이즈가 컬러 스노우로 보임 | feTurbulence의 알파도 노이즈 | `feColorMatrix`로 A를 1 상수화 |
| 노이즈에 격자 사각형이 보임 | `seed`가 514/1977/2337/4777/8032/9615 등 | seed 변경 |
| 요소가 통째로 사라짐 | `url(#id)` 오타 (레거시 엔진) | ID 확인 |
---
## 5. CSS 레이아웃 부작용 (놓치기 쉬운 것)
`filter``none`이 아닌 값을 가지면 그 요소는:
1. **새 스태킹 컨텍스트**를 만든다 → `z-index` 계층이 갇힌다.
2. **`position: fixed` / `absolute` 자손의 컨테이닝 블록이 된다** → 하위의 `position: fixed` 모달이 뷰포트가 아니라 그 요소 기준으로 배치된다.
`backdrop-filter`, `perspective`, `will-change: transform`, `contain: layout|paint|strict|content`, `content-visibility: auto`도 동일한 부작용을 갖는다.
> 이 동작은 호환성 때문에 표준화된 것이다. 모바일 WebKit이 먼저 하고 Chrome 22에서 따라갔으며, 2015년 워킹그룹 결의를 거쳐 Firefox가 마지막으로 합류했다.
**실무 영향**
```html
<!-- 카드에 질감 필터를 걸었더니, 카드 안의 드롭다운이 화면 밖으로 못 나간다 -->
<div class="card" style="filter: url(#texture)">
<div class="dropdown" style="position: fixed">...</div> <!-- 카드 기준으로 배치됨 -->
</div>
```
**회피**
- 필터를 콘텐츠 컨테이너가 아니라 **별도의 표면(surface) 레이어**에만 건다 (레시피 부록 B의 패턴).
- 오버레이/모달은 Portal로 `<body>` 직속에 렌더한다.
- DevTools는 "이 요소가 컨테이닝 블록을 만든다"를 시각적으로 표시해주지 않는다. 증상(모달 위치가 이상함)에서 역추적하는 수밖에 없다.
---
## 6. 브라우저별 알려진 이슈
### 6.1 Safari / WebKit
| 이슈 | 영향 |
|---|---|
| `backdrop-filter: url()` 미지원 | Liquid Glass 계열 전부 폴백 필요 |
| `feImage`**외부 SVG 파일** 참조 미지원 | data URI로 우회 |
| `feImage` **fragment 참조**(`href="#id"`)는 Safari **만** 지원 | 크로스브라우저에서는 쓸 수 없음 |
| `feImage`가 체인에 있으면 **HTML 요소에 필터 적용 실패** 보고 | SVG 콘텐츠에만 적용하거나 폴백 |
| 대형 요소에 필터를 걸면 고해상도에서 **아예 렌더되지 않는** 경우 보고 | 필터 적용 면적 제한 |
| SVG `<filter>`보다 CSS 단축 필터가 훨씬 안정적 | 가능하면 CSS 단축 사용 |
| `feGaussianBlur``edgeMode`**Safari만 지원** | 크로스브라우저에서는 사용 불가 |
### 6.2 Firefox
| 이슈 | 영향 |
|---|---|
| `backdrop-filter: url()` 미지원 (기능 요청 계류 중) | 폴백 필요 |
| `feTurbulence` 성능 이슈 장기 보고 | 큰 면적 금지 |
| 하드웨어 가속 활성 시 필터 미동작 버그 보고 | 사용자 환경에 따라 효과가 안 보일 수 있음 |
| `feComposite operator="arithmetic"` 시각 아티팩트 버그 보고 | 대안 연산자 고려 |
| 블러 반경 상한(≈100px) | 큰 블러는 다른 방법으로 |
| `feGaussianBlur edgeMode` 미지원 | — |
### 6.3 모바일 전반
- 큰 면적 필터 + 애니메이션 = **배터리 소모 + 열 스로틀링**. 스로틀링이 걸리면 페이지 전체가 느려진다.
- 일부 안드로이드 GPU는 필터 가속을 전혀 못 한다.
- 저가 기기에서 `feTurbulence` 애니메이션은 사실상 사용 불가.
- GSAP 포럼 등에서 "데스크톱은 멀쩡한데 모바일에서 feTurbulence가 죽는다"는 사례가 반복 보고된다.
### 6.4 feTurbulence seed 아티팩트
특정 `seed` 값에서 노이즈에 **격자 사각형**이 생긴다. 보고된 값: `514, 1977, 2337, 4777, 8032, 9615` 등.
- 원인은 SVG 스펙의 참조 구현에 있는 정수 나눗셈 버그다. gradient 테이블의 두 값이 모두 0이 되는 경우가 생긴다.
- Chrome, Firefox, Batik, Inkscape 등 독립 구현들이 스펙을 그대로 따랐기 때문에 **모두 동일하게 재현**된다.
- **처방**: seed는 무작위로 생성하지 말고, 1~50 정도의 작은 값 중 눈으로 확인한 것을 하드코딩하라.
---
## 7. 접근성
### 7.1 `prefers-reduced-motion`
애니메이션되는 필터는 전정계 장애가 있는 사용자에게 실제로 불편을 준다. **모든 애니메이션 필터에 감소 모션 처리를 넣어라.**
```css
/* 방법 1: 필터 자체를 끈다 */
@media (prefers-reduced-motion: reduce) {
.wave, .squiggle-art, .glitch-box { filter: none; }
}
/* 방법 2: 정적 버전으로 대체 */
@media (prefers-reduced-motion: reduce) {
.pulse-card { filter: none; box-shadow: 0 0 0 3px #22d3ee; }
}
```
**SMIL은 CSS 미디어쿼리로 끌 수 없다.** JS로 처리해야 한다.
```js
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
function applyMotionPreference() {
const svgRoot = document.querySelector('#filterDefs');
if (!svgRoot) return;
if (mq.matches) {
// SMIL 타임라인 정지
if (typeof svgRoot.pauseAnimations === 'function') svgRoot.pauseAnimations();
} else {
if (typeof svgRoot.unpauseAnimations === 'function') svgRoot.unpauseAnimations();
}
}
applyMotionPreference();
mq.addEventListener('change', applyMotionPreference);
```
> **미검증**: `pauseAnimations()`/`unpauseAnimations()``SVGSVGElement`의 표준 메서드지만 이 세션에서 실측하지 않았다. SMIL을 아예 쓰지 않고 JS 타이머로 어트리뷰트를 바꾸는 방식(레시피 11)이 제어가 확실하다.
> ⚠️ SVG를 `<img>`나 CSS `background-image`로 넣으면 브라우저가 샌드박스 처리해 **내부의 `prefers-reduced-motion` 미디어쿼리가 평가되지 않는다.** 인라인 SVG를 쓰거나, 애니메이션을 바깥 CSS에서 제어하라.
### 7.2 텍스트 가독성
- 왜곡 필터를 텍스트에 걸면 **판독성이 떨어진다.** `feDisplacementMap scale`은 텍스트에서 4~8을 넘기지 마라.
- 필터는 **DOM 텍스트를 그대로 유지**한다 — 선택·복사·검색·스크린리더 접근은 살아있다. 이것이 이미지로 굽는 것보다 나은 결정적 이유다.
- 듀오톤/포스터화로 **대비가 낮아질 수 있다.** 텍스트 위에 적용한다면 WCAG 대비비를 실제 렌더 결과로 다시 측정하라.
- 색수차(RGB split)는 난독증·시각 피로가 있는 사용자에게 특히 읽기 어렵다. 본문 텍스트에는 쓰지 마라.
### 7.3 필터 정의 SVG의 접근성
```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<defs><filter id="...">...</filter></defs>
</svg>
```
- `aria-hidden="true"`: 스크린리더가 빈 그래픽으로 읽지 않게.
- `focusable="false"`: 구형 IE/Edge에서 탭 순서에 들어가지 않게.
- `width="0" height="0"` + `position:absolute`: 레이아웃에 영향 없음.
---
## 8. 폴백 · 저사양 대응 전략
### 8.1 계층적 설계 (권장)
```
Layer 0 — 필터 없이도 완성된 디자인 (색, 그림자, 그라디언트)
Layer 1 — CSS 단축 필터로 얹는 개선 (blur, saturate, drop-shadow)
Layer 2 — SVG 필터로 얹는 개선 (질감, 굴절, 색수차)
```
각 레이어가 없어도 아래 레이어가 완결되어야 한다. **필터를 지웠을 때 깨지는 디자인은 잘못 설계된 것이다.**
### 8.2 저사양 기기 감지
```js
function shouldDisableHeavyFilters() {
// 1) 사용자 선호
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return true;
// 2) 하드웨어 힌트 (모든 브라우저가 제공하지는 않음)
const cores = navigator.hardwareConcurrency; // Chromium/Firefox
const memory = navigator.deviceMemory; // Chromium only, GB 단위
if (typeof cores === 'number' && cores <= 4) return true;
if (typeof memory === 'number' && memory <= 4) return true;
// 3) 네트워크 절약 모드 (Chromium)
const conn = navigator.connection;
if (conn && conn.saveData) return true;
// 4) 좁은 화면 = 대체로 모바일
if (window.matchMedia('(max-width: 640px)').matches) return true;
return false;
}
if (shouldDisableHeavyFilters()) {
document.documentElement.classList.add('reduce-effects');
}
```
```css
/* 무거운 필터를 값싼 대체물로 교체 */
.reduce-effects .paper-surface { filter: none; background: #e8e0d0; }
.reduce-effects .squiggle-art { filter: none; }
.reduce-effects .prism-pill { backdrop-filter: blur(10px) saturate(1.4); }
.reduce-effects .grainy::after { opacity: 0.25; } /* 그레인은 싸니 약하게 유지 */
```
> `navigator.hardwareConcurrency`/`deviceMemory`는 Chromium에서 실측 확인했다(각각 24, 32 반환). Safari는 `deviceMemory`를 제공하지 않으므로 `typeof` 가드가 필수다.
### 8.3 뷰포트 밖 / 백그라운드 탭에서 정지
애니메이션 필터에는 필수다.
```js
function gateAnimation(targetEl, start, stop) {
let visible = false;
let focused = !document.hidden;
function sync() { (visible && focused) ? start() : stop(); }
if ('IntersectionObserver' in window) {
new IntersectionObserver(entries => {
visible = entries[0].isIntersecting;
sync();
}, { threshold: 0 }).observe(targetEl);
} else {
visible = true;
}
document.addEventListener('visibilitychange', () => {
focused = !document.hidden;
sync();
});
sync();
}
```
### 8.4 "구워서 쓰기" — 가장 확실한 최적화
정적 텍스처(종이 질감, 그레인 패턴, 리소 그레인)는 **한 번 렌더해서 PNG/WebP로 저장**하고 `background-image`로 쓰는 것이 압도적으로 싸다.
- 필터 체인이 아무리 길어도 런타임 비용은 0이 된다.
- 해상도 독립성을 잃지만, 노이즈/텍스처는 애초에 해상도 독립성이 필요 없다.
- 512×512 타일을 만들어 `background-repeat`로 깔면 파일도 작다.
- CSS-Tricks의 패턴 생성 글에서도 복잡한 필터 패턴은 "JPEG/PNG로 서빙해 사용자 CPU를 아끼라"고 권한다.
**data URI vs 요소 필터** — 이미 §02 레시피 02/03에서 쓴 방식이다. `background-image: url("data:image/svg+xml,...")`는 **한 번만 래스터화되어 캐시**되므로, 같은 노이즈를 `filter: url(#noise)`로 거는 것보다 훨씬 싸다. **노이즈는 언제나 data URI 배경으로.**
---
## 9. 측정과 디버깅
### 9.1 Chrome DevTools
1. **Performance 패널** → 녹화 → "Rendering" 트랙에서 Paint/Composite 시간 확인. 필터가 문제면 Paint가 길어진다.
2. **Rendering 탭** → "Paint flashing" 켜기 → 필터 영역이 매 프레임 초록색으로 깜빡이면 재계산 중이다.
3. **Rendering 탭** → "Layer borders"로 합성 레이어 승격 여부 확인.
4. **CPU throttling 4×/6×** 로 저사양 기기 시뮬레이션. 이것이 가장 현실적인 테스트다.
### 9.2 A/B 비교 방법
필터 하나가 얼마나 비싼지 알고 싶으면 DevTools 콘솔에서 껐다 켜본다.
```js
document.querySelectorAll('.effect').forEach(el => { el.style.filter = 'none'; });
```
FPS가 확 오르면 그 필터가 범인이다.
### 9.3 오프라인 도구
| 도구 | 용도 |
|---|---|
| [yoksel/svg-filters](https://yoksel.github.io/svg-filters/) | 필터 체인을 GUI로 만들고 코드 추출 |
| [yoksel/svg-gradient-map](https://yoksel.github.io/svg-gradient-map/) | 듀오톤/트라이톤 `tableValues` 자동 생성 |
| [fffuel nnnoise](https://www.fffuel.co/nnnoise/) | 노이즈 텍스처 SVG 생성 |
---
## 10. 프로덕션 체크리스트
배포 전에 이 목록을 통과시켜라.
**성능**
- [ ] 필터 영역(`x/y/width/height`)이 필요한 최소 크기인가
- [ ] `numOctaves`가 4를 넘지 않는가
- [ ] 애니메이션되는 필터의 적용 면적이 400×400px 이하인가
- [ ] 애니메이션 필터에 IntersectionObserver / visibilitychange 게이팅이 있는가
- [ ] 필터와 `transform` 애니메이션이 같은 요소에 걸려 있지 않은가
- [ ] `will-change: filter`를 남발하지 않았는가
- [ ] 정적 텍스처를 런타임 필터로 매번 계산하고 있지 않은가 (→ data URI 배경 또는 PNG)
- [ ] CPU throttling 4×에서 스크롤이 부드러운가
**호환성**
- [ ] `color-interpolation-filters`를 의도적으로 설정했는가
- [ ] `feDisplacementMap`의 채널 셀렉터를 명시했는가
- [ ] `feImage`를 쓴다면 data URI인가 (외부 파일/fragment 아님)
- [ ] `backdrop-filter: url()`에 의존한다면 폴백만으로도 완성도가 있는가
- [ ] `edgeMode`, `kernelUnitLength`, `BackgroundImage`, `FillPaint`를 쓰지 않았는가
- [ ] `feTurbulence` seed가 알려진 문제 값이 아닌가
- [ ] Safari / Firefox / 모바일 실기기에서 눈으로 확인했는가
**접근성**
- [ ] 애니메이션 필터에 `prefers-reduced-motion` 처리가 있는가
- [ ] 필터 정의 SVG에 `aria-hidden="true"`가 있는가
- [ ] 텍스트에 필터를 걸었다면 판독성과 대비비를 확인했는가
- [ ] 필터가 전부 꺼져도 디자인이 성립하는가
**레이아웃**
- [ ] 필터를 건 요소 안에 `position: fixed` 자손이 없는가
- [ ] 필터로 인한 스태킹 컨텍스트가 `z-index` 계층을 깨지 않는가
- [ ] 필터 컨테이너 안에 본문 텍스트가 들어가 있지 않은가

239
research/svg/04-sources.md Normal file
View file

@ -0,0 +1,239 @@
# 04. 조사 소스 목록
**183개**. `★` = 본문 전체를 직접 열람해 내용을 추출한 소스(21개), 그 외는 검색 결과 메타데이터·요약 수준에서 확인한 소스.
---
## A. 스펙 / 표준 문서
1. ★ https://www.w3.org/TR/filter-effects-1/ — Filter Effects Module Level 1. 필터 영역·서브영역 정의, 입력 키워드 6종, feBlend 모드 목록, CSS 단축 함수↔프리미티브 대응의 1차 출처.
2. ★ https://www.w3.org/TR/SVG11/filters.html — SVG 1.1 Filter Effects 챕터. 각 프리미티브의 초기값과 에러 처리 규칙의 원전.
3. https://drafts.fxtf.org/filter-effects-1/ — FXTF 에디터 초안(최신 편집본). `/filter-effects/` 경로는 404이므로 `-1` 붙은 쪽을 써야 한다.
4. https://www.w3.org/Graphics/SVG/1.1/filters.html — SVG 1.1 2003년판 필터 챕터. 서브영역 설명이 상세하다.
5. https://www.w3.org/TR/SVGFilter12/ — SVG Filters 1.2 Part 2. 채택되지 않았지만 feTile·서브영역 설명이 명확하다.
6. https://www.w3.org/TR/2000/CR-SVG-20000802/filters.html — SVG 1.0 CR. `lighting-color`가 애니메이터블임을 명시한 초기 문서.
7. https://lists.w3.org/Archives/Public/public-fxtf-archive/2017Nov/0003.html — filter가 fixed/absolute 자손의 컨테이닝 블록을 만들어야 한다는 워킹그룹 논의.
8. https://github.com/w3c/fxtf-drafts/issues/323 — FillPaint/StrokePaint의 올바른 동작이 무엇인지조차 합의되지 않았음을 보여주는 이슈. 실무에서 쓰면 안 되는 근거.
9. https://github.com/w3c/svgwg/issues/1142 — "liquid glass" UI를 위한 상호운용 가능한 backdrop displacement/refraction 정의 요청. 현재 이 기능이 비표준임을 보여준다.
10. https://github.com/w3c/csswg-drafts/issues/1273 — CSS에 turbulence/noise 이미지 함수를 넣자는 제안. 아직 SVG 필터가 유일한 절차적 노이즈 수단인 이유.
11. https://lists.w3.org/Archives/Public/www-svg/2007Feb/0053.html — 필터 서브영역 해석에 대한 초기 논의.
12. https://lists.w3.org/Archives/Public/www-style/2014Mar/0161.html — 필터 성능/구현에 관한 Adobe(Dirk Schulze)의 코멘트.
13. https://lists.w3.org/Archives/Public/www-svg/2010Aug/0000.html — feTurbulence seed 구현 문제 관련 메일.
## B. MDN 레퍼런스 (프리미티브별 정본)
14. ★ https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/filter — `<filter>`의 x/y/width/height 기본값(-10%/-10%/120%/120%), filterUnits/primitiveUnits.
15. ★ https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/feTurbulence — type/baseFrequency/numOctaves/seed/stitchTiles 전체 속성과 linearRGB 기본값.
16. ★ https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/feColorMatrix — 5×4 행렬 레이아웃, 항등 행렬, saturate/hueRotate/luminanceToAlpha 예제.
17. ★ https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/feComponentTransfer — identity/table/discrete/linear/gamma 5종의 정확한 공식과 기본값.
18. ★ https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/feDropShadow — dx=2/dy=2/stdDeviation=2 기본값, flood-color/flood-opacity.
19. ★ https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/feSpecularLighting — surfaceScale/specularConstant/specularExponent(1~128), 광원 3종 속성, feComposite arithmetic 합성 관례.
20. ★ https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/feGaussianBlur — stdDeviation의 number-optional-number, 큰 블러 시 필터 영역 확장 지침.
21. ★ https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/feImage — href/preserveAspectRatio/crossorigin, SVG 소스는 래스터화된다는 점.
22. ★ https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/feBlend — in/in2 의미와 CSS 블렌드 모드 지원.
23. ★ https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/feMorphology — erode/dilate 동작과 CSS 적용 예제.
24. https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/feDisplacementMap — 변위 공식 `P'(x,y) ← P(x + scale×(XC0.5), y + scale×(YC0.5))`.
25. https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/feDiffuseLighting — 난반사 조명, surfaceScale/diffuseConstant.
26. https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/color-interpolation-filters — 필터 기본값이 linearRGB이고 다른 색 연산은 sRGB라는 불일치의 정본.
27. https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-filters — 같은 속성의 CSS 프로퍼티 버전.
28. https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/edgeMode — edgeMode 값 3종(duplicate/wrap/none)과 기본값 none.
29. https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/flood-color — flood-color가 CSS 프로퍼티이자 애니메이터블임을 명시.
30. https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/flood-opacity — flood-opacity 기본값과 애니메이션 가능 여부.
31. https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/k3 — feComposite arithmetic의 k 계수 설명.
32. https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/k2 — 위와 같음.
33. https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter — backdrop-filter 정의와 지원 현황.
34. https://developer.mozilla.org/en-US/docs/Web/CSS/filter — CSS filter 프로퍼티, url() 참조 포함.
35. https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/blur — CSS blur()가 stdDeviation과 어떻게 대응하는지.
36. https://developer.mozilla.org/en-US/docs/Web/API/SVGFETurbulenceElement/seed — seed의 DOM 인터페이스, 절삭 규칙.
37. https://developer.mozilla.org/en-US/docs/Web/API/SVGFETurbulenceElement/numOctaves — numOctaves DOM 속성.
38. https://developer.mozilla.org/en-US/docs/Web/API/SVGFESpecularLightingElement/surfaceScale — surfaceScale DOM 속성.
39. https://developer.mozilla.org/en-US/docs/Web/API/SVGFEConvolveMatrixElement/edgeMode — feConvolveMatrix의 edgeMode 기본값(duplicate).
40. https://github.com/mdn/content/blob/main/files/en-us/web/svg/reference/element/feturbulence/index.md — MDN feTurbulence 문서 원본 마크다운.
41. https://github.com/mdn/browser-compat-data/issues/24110 — backdrop-filter의 SVG 필터 지원이 Firefox/Safari에 없다는 호환성 데이터 이슈.
42. https://github.com/mdn/browser-compat-data/issues/18810 — Safari의 feTurbulence 지원 데이터 정정 이슈.
43. https://caniuse.com/?search=feTurbulence — feTurbulence 브라우저 지원표.
## C. Sara Soueidan / Codrops SVG 필터 시리즈 (교과서)
44. ★ https://tympanus.net/codrops/2019/01/15/svg-filters-101/ — 필터 영역 기본값, in/in2/result 배선, SourceGraphic/SourceAlpha, 드롭섀도 체인 예제. 입문의 정본.
45. ★ https://tympanus.net/codrops/2019/01/22/svg-filter-effects-outline-text-with-femorphology/ — feMorphology 아웃라인/녹아웃 텍스트 전체 코드. stroke 대비 장점 설명.
46. ★ https://tympanus.net/codrops/2019/02/05/svg-filter-effects-duotone-images-with-fecomponenttransfer/ — 듀오톤 완전 코드, hex→tableValues 변환, sRGB 명시 필요성.
47. ★ https://tympanus.net/codrops/2019/02/19/svg-filter-effects-creating-texture-with-feturbulence/ — baseFrequency 실전 범위(0.02~0.2), numOctaves 한계, 종이 질감 feDiffuseLighting 코드.
48. https://tympanus.net/codrops/2019/02/12/svg-filter-effects-conforming-text-to-surface-texture-with-fedisplacementmap/ — 텍스처 표면에 텍스트를 밀착시키는 변위맵 기법.
49. https://tympanus.net/codrops/2019/02/26/svg-filter-effects-moving-forward/ — 시리즈 마무리. 추가 리소스·실험 모음.
50. https://www.sarasoueidan.com/blog/svg-filters-series/ — 시리즈 전체 인덱스.
51. https://tympanus.net/codrops/author/sarasoueidan/ — 저자 아카이브.
52. https://www.youtube.com/watch?v=kfOhlU_iRVU — "SVG Filters: The Crash Course" (An Event Apart). 영상 강의판.
53. https://hookedoncode.com/2019/07/svg-filters-a-crash-course-by-sara-soueidan/ — 위 강연 요약 노트.
54. https://tympanus.net/codrops/2015/03/10/creative-gooey-effects/ — Gooey 효과의 고전 아티클. blur+alpha contrast 원리.
55. https://sudonull.com/post/717-Effects-of-SVG-filters-Part-2-Outline-text-with-feMorphology — 위 45번의 러시아어 번역판.
56. https://webdesigntips.blog/more-website-design-news/svg-filter-effects-duotone-images-with-fecomponenttransfer/ — 46번의 미러.
57. https://emptyq.net/a?ID=fe324b95-2175-46c0-9a09-31e5a0c748e1 — 45번의 미러.
## D. CSS-Tricks / Smashing / Frontend Masters
58. ★ https://css-tricks.com/gooey-effect/ — Gooey 필터 정본 코드. `values="... 18 -7"`의 의미, feBlend vs feComposite atop 선택, Safari 주의.
59. ★ https://css-tricks.com/grainy-gradients/ — 노이즈 SVG를 배경으로 깔고 contrast/brightness로 극단화하는 기법.
60. ★ https://css-tricks.com/creating-patterns-with-svg-filters/ — feTurbulence + discrete/table로 별하늘·나무결·달마시안·위장무늬 패턴 생성. "복잡한 패턴은 이미지로 구워 쓰라"는 성능 권고.
61. https://css-tricks.com/using-svg-to-create-a-duotone-image-effect/ — feColorMatrix 기반 듀오톤(대안 접근).
62. https://css-tricks.com/almanac/properties/f/filter/ — CSS filter 프로퍼티 알마낙.
63. https://css-tricks.com/almanac/rules/m/media/prefers-reduced-motion/ — prefers-reduced-motion 사용법.
64. ★ https://www.smashingmagazine.com/2021/09/deep-dive-wonderful-world-svg-displacement-filtering/ — 변위 필터 심층 해설. 절대 변위맵 만들기, feImage 참조 방식별 브라우저 지원표, SMIL/anime.js 애니메이션, WebKit 제약. **가장 정보 밀도가 높은 단일 소스.**
65. https://www.smashingmagazine.com/2022/05/magical-svg-techniques/ — SVG 전반 기법 모음(Amelia Wattenberger의 "telescope" 비유 인용).
66. https://www.smashingmagazine.com/2026/01/unstacking-css-stacking-contexts/ — filter/backdrop-filter가 스태킹 컨텍스트와 컨테이닝 블록을 만드는 문제 정리.
67. https://frontendmasters.com/blog/grainy-gradients/ — 그레이니 그라디언트 후속 정리.
68. https://frontendmasters.com/blog/non-square-image-blur-extensions/ — feGaussianBlur edgeMode의 브라우저 지원(Safari 전용) 실측.
## E. Liquid Glass / 굴절 유리
69. ★ https://kube.io/blog/liquid-glass-css-svg/ — Snell 법칙 기반 변위맵 생성의 수학적 근거. R=x/G=y 인코딩(중립 128), scale 매핑, `color-interpolation-filters="sRGB"` 필수 이유, Chromium 전용 제약.
70. ★ https://blog.logrocket.com/how-create-liquid-glass-effects-css-and-svg/ — feImage 변위맵 + specular 레이어 + backdrop-filter의 완전한 React 구현. 크기별 맵을 따로 만들라는 실무 제약.
71. https://github.com/nikdelvin/liquid-glass — CSS+SVG만으로 iOS 26 Liquid Glass를 재현한 컴포넌트 모음.
72. https://github.com/deepika-builds/liquid-glass — SVG 변위 기반 굴절, 무의존성 단일 파일 구현.
73. https://www.cssscript.com/liquid-glass-ui/ — 순수 CSS+SVG 필터 Liquid Glass UI 튜토리얼.
74. https://medium.com/ekino-france/liquid-glass-in-css-and-svg-839985fcb88d — feImage PNG 변위맵 + feGaussianBlur + feDisplacementMap 조합 해설.
75. https://freefrontend.com/css-liquid-glass/ — Liquid Glass 스니펫 모음(레퍼런스 탐색용).
76. https://freefrontend.com/code/liquid-glass-effect-2026-02-05/ — 최신 Liquid Glass 데모.
77. https://connect.mozilla.org/t5/ideas/support-svg-filters-in-backdrop-filter-for-advanced-glass/idi-p/98453 — Firefox에 backdrop-filter SVG 필터 지원을 요청하는 공식 아이디어 스레드(2025).
78. https://codepen.io/iamvdo/pen/VLOGdw — SVG 필터로 backdrop-filter를 흉내내는 고전 데모.
79. https://www.svggenie.com/blog/advanced-svg-filters-glassmorphism-glitch — 글래스모피즘과 글리치 효과 조합.
## F. 노이즈 / 그레인 / 텍스처
80. https://www.freecodecamp.org/news/grainy-css-backgrounds-using-svg-filters/ — SVG 필터로 그레이니 배경 만들기 단계별 가이드.
81. https://daniel.do/article/making-noisy-svgs — 노이즈 SVG 제작 실전.
82. https://ibelick.com/blog/create-grainy-backgrounds-with-css — CSS만으로 그레인 배경 만들기.
83. https://gist.github.com/skeptrunedev/e1f0cf00641fb26bbd0acf937f57c6a5 — 필름 그레인 SVG+HTML 스니펫.
84. https://www.fffuel.co/nnnoise/ — 온라인 SVG 노이즈 텍스처 생성기. 파라미터 감 잡는 데 유용.
85. https://imagetosvg.com/how-to/svg-feturbulence-filter-primitive — feTurbulence 절차적 노이즈 가이드.
86. https://dev.to/hexshift/creating-organic-textures-with-svg-filter-distortions-1moj — 변위 기반 유기적 텍스처.
87. https://codepen.io/Chokcoco/pen/OJWLXPY — feTurbulence + feDiffuseLighting 거친 종이 질감 CodePen.
88. https://oreillymedia.github.io/Using_SVG/extras/ch16-feTurbulence.html — "Using SVG" 책의 feTurbulence 파형 만들기 챕터.
89. https://www.oreilly.com/library/view/designing-svg-web/0735711666/0735711666_ch07lev1sec3.html — SVG 필터 범위 개괄(고전 서적).
## G. 색상 / 듀오톤 / 색수차
90. ★ https://utilitybend.com/blog/revisiting-svg-filters-my-forgotten-powerhouse-for-duotones-noise-and-other-effects/ — 듀오톤/노이즈/방향성 모션블러(`stdDeviation="15 0"`) 실용 코드 모음. 필터 컨테이너 접근성 패턴.
91. ★ https://johndjameson.com/posts/chromatic-aberration-with-svg-filters — 색수차 완전 코드. 채널별 feColorMatrix + feOffset + feGaussianBlur + screen 블렌드.
92. https://johndjameson.com/posts/understanding-fecolormatrix-svg-filters — feColorMatrix 행렬 읽는 법 입문.
93. https://github.com/yoksel/svg-gradient-map — 듀오톤/트라이톤 필터 생성 도구 소스.
94. https://yoksel.github.io/svg-gradient-map/ — 위 도구 라이브 버전. 이미지 업로드 → 코드 복사.
95. https://yoksel.github.io/svg-filters/ — 필터 체인을 GUI로 조립하는 플레이그라운드. 프리셋 다수.
96. https://yoksel.github.io/svg-filters-02-2018/ — yoksel의 SVG 필터 강연 슬라이드(러시아어).
97. https://jmperezperez.com/blog/duotone-using-css-blend-modes/ — CSS 블렌드 모드 기반 듀오톤(SVG 대안, 정확도는 낮음).
98. https://codepen.io/lentilz/pen/mPOKdG/ — 듀오톤 SVG 필터 CodePen.
99. https://stuffandnonsense.co.uk/blog/svg-filters-for-emmas-website — 실제 사이트에 SVG 필터를 적용한 사례 연구.
## H. 손그림 / 왜곡 / 글리치
100. ★ https://camillovisini.com/coding/simulating-hand-drawn-motion-with-svg-filters — feTurbulence+feDisplacementMap으로 손그림 모션. baseFrequency 오프셋을 100ms마다 순환시키는 방식.
101. ★ https://henry.codes/writing/how-to-distort-text-with-svg/ — 텍스트 왜곡. scale을 로그 스케일(1→2→10→50→200)로 탐색하라는 실용 팁, 텍스트 선택 가능성 유지.
102. https://kirgroup.net/blog/2025/07/21-SVG-Hand-Drawing-without-js.html — JS 없이 SMIL만으로 손그림 모션 구현.
103. https://heredragonsabound.blogspot.com/2020/02/creating-pencil-effect-in-svg.html — 연필 효과: 변위된 선을 여러 번 겹쳐 feBlend로 합치는 기법.
104. https://bengammon.co.uk/rough-css-borders-with-svg-filters/ — CSS 테두리를 러프하게 만드는 최소 구현.
105. https://medium.com/swlh/using-the-svg-feturbulence-filter-for-wave-effects-2b8cb2546ee6 — 파형 효과에 feTurbulence 활용.
106. https://codepen.io/smashingmag/pen/yLXOgoQ — Smashing 기사의 글리치 트랜지션 CodePen(feFlood 밴드 + 변위).
107. https://codepen.io/JWS-Animation/pen/jOwOpZr — feTurbulence 글리치 텍스트.
108. https://www.nickhart.co.uk/2021/04/18/animated-svg-cyberpunk-logo-with-filters/ — 사이버펑크 로고: 색수차 + 글리치 애니메이션 조합.
109. https://codepen.io/Sid701/pen/OJXKMXw — 물결 리플 필터.
110. https://codepen.io/enxaneta/post/svg-waves-with-fedisplacementmap — feDisplacementMap 파형 심화.
111. https://redstapler.co/realistic-water-effect-svg-turbulence-filter/ — 사실적인 물 효과 튜토리얼.
112. https://dev.to/matteo1222/explained-4a4b — feDisplacementMap 동작 원리 해설.
113. https://medium.com/@jaikumarsharma94130/building-a-special-invisible-svg-component-that-defines-wavy-filters-2a09e6050de6 — 재사용 가능한 wavy 필터 컴포넌트 설계.
114. https://www.youtube.com/watch?v=lisZT8JIeKk — Squigglevision 메뉴 호버 효과 영상 튜토리얼.
115. https://observablehq.com/@abbe98/svg-paths-using-filters-animated — 필터로 애니메이션되는 SVG 경로 실험.
## I. 리소그래프 / 인쇄
116. https://osmanyy.com/projects/risograph-css/ — Risograph.css. mix-blend-mode multiply 잉크 레이어 + feTurbulence 그레인 마스크 + 커스텀 프로퍼티 misregistration. **본 문서 레시피 16의 직접적 참고.**
117. https://codepen.io/chriskirknielsen/pen/rNmgXyV — SVG 기반 리소그래프 그라디언트.
118. https://chrislemke.github.io/website_designs/designs/Risograph.html — 리소그래프 웹 디자인 레퍼런스.
119. https://libguides.reed.edu/c.php?g=1339150&p=9870131 — Reed College 리소 인쇄 가이드. 실제 인쇄 특성(어긋남 범위, 잉크 팔레트) 근거.
120. https://studio-ity.com/riso/ — 리소 효과 온라인 시뮬레이터. 2~4색 레이어, 그레인, 하프톤 파라미터 참고.
121. https://www.grayscaleimage.org/posts/duotone-design-guide — 듀오톤 디자인 가이드(색 선택 관점).
## J. 조명 / 컨볼루션
122. https://vanseodesign.com/web-design/svg-filter-primitives-fespecularlighting/ — feSpecularLighting 실전 튜토리얼.
123. https://vanseodesign.com/web-design/svg-filter-primitives-feconvolvematrix/ — feConvolveMatrix로 엠보스/베벨/샤픈 만들기.
124. https://vanseodesign.com/web-design/svg-filter-primitives-fecomposite/ — feComposite 연산자별 결과 비교.
125. https://apike.ca/prog_svg_filter_feComposite.html — feComposite arithmetic k1~k4 조합 시각 예제.
126. https://www.svgbasics.com/filters2.html — 조명 필터 기초 튜토리얼.
127. https://www.creativebloq.com/netmag/how-go-beyond-basics-svg-filters-71412280 — 조명·질감 조합 심화(net magazine).
128. https://docs.w3cub.com/svg/element/fespecularlighting — feSpecularLighting 레퍼런스 미러.
129. https://docs.w3cub.com/svg/element/femorphology — feMorphology 레퍼런스 미러.
130. https://docs.w3cub.com/svg/element/fecomposite.html — feComposite 레퍼런스 미러.
131. https://svgwrite.readthedocs.io/en/latest/classes/fe_specular_lighting.html — Python svgwrite 문서. 속성 기본값이 명시적으로 표로 정리돼 있어 참조하기 좋다.
132. https://svgwrite.readthedocs.io/en/latest/classes/fe_tile.html — feTile 속성 정리.
133. https://svgwrite.readthedocs.io/en/latest/classes/fe_morphology.html — feMorphology 속성 정리.
134. https://svgwrite.readthedocs.io/en/latest/classes/fe_composite.html — feComposite 속성 정리.
135. https://svgwrite.readthedocs.io/en/latest/classes/fe_turbulence.html — feTurbulence 속성 정리. seed 절삭 규칙 명시.
## K. 성능 / 버그 / 브라우저 이슈
136. ★ https://codepen.io/tigt/post/improving-svg-rendering-performance — Taylor Hunt의 SVG 렌더 성능 심층 분석. "SVG-on-SVG / SVG-in-CSS / CSS 단축" 3가지 경로, 엔진별 가속 불일치, "Safari는 CSS 단축 필터에서만 괜찮다"는 결론.
137. https://bugzilla.mozilla.org/show_bug.cgi?id=422371 — "SVG feTurbulence filter is far too slow". 15년 넘게 열려 있는 성능 버그.
138. https://bugzilla.mozilla.org/show_bug.cgi?id=862225 — seed 514에서 feTurbulence가 사각형을 만드는 버그. 스펙 참조 구현의 정수 나눗셈 문제로 확인됨.
139. https://bugs.launchpad.net/bugs/1406458 — 위와 같은 seed 버그의 Inkscape 리포트. 문제 seed 값 목록(514, 1977, 2337, 4777, 8032, 9615) 포함.
140. https://bz.apache.org/bugzilla/show_bug.cgi?id=36744 — Batik의 feTurbulence seed 구현 버그. 독립 구현들이 동일 문제를 공유함을 보여준다.
141. https://bugzilla.mozilla.org/show_bug.cgi?id=1441294 — feComposite arithmetic 사용 시 시각 아티팩트 버그.
142. https://bugzilla.mozilla.org/show_bug.cgi?id=1298137 — 하드웨어 가속을 켜면 대부분의 CSS/SVG 필터가 동작하지 않는 Firefox 버그.
143. https://bugzilla.mozilla.org/show_bug.cgi?id=1094305 — feGaussianBlur의 edgeMode="duplicate" 구현 요청(미구현 상태).
144. https://bugzilla.mozilla.org/show_bug.cgi?id=869496 — CSS/SVG 필터 GPU 가속 구현 트래킹.
145. https://www.chromium.org/developers/design-documents/image-filters/ — Chromium의 필터 구현 설계 문서(Blink/Skia 2중 구현).
146. https://developer.chrome.com/blog/hardware-accelerated-animations — 하드웨어 가속 애니메이션 대상 프로퍼티 현황(opacity/filter/transform).
147. https://gsap.com/community/forums/topic/33075-gsap-and-feturbulence-mobile-performance/ — 데스크톱은 멀쩡한데 모바일에서 feTurbulence가 죽는 실제 사례와 대응.
148. https://www.svgator.com/help/animation-and-interactivity/how-to-fix-svg-animation-lag-in-safari — Safari 필터 렌더 실패/랙 대응. 고해상도에서 큰 요소가 아예 안 그려지는 사례.
149. https://finance.biggo.com/news/202507211315_SVG_Filter_Performance_Issues — 손그림 애니메이션 효과의 CPU 포화 문제 보도.
150. https://imagetosvg.com/how-to/svg-filter-performance-tips — 필터 성능 팁 요약.
151. https://oreillymedia.github.io/Using_SVG/extras/ch19-performance.html — "Using SVG" 성능 계획 챕터.
152. https://iamsteve.me/blog/overcoming-a-couple-of-issues-with-svg-filter-effects — Safari에서 채도가 낮게 나오는 문제를 `color-interpolation-filters="sRGB"`로 해결한 실무 기록.
153. https://konadu.dev/backdrop-filter-breaks-position-fixed — backdrop-filter가 position:fixed를 깨뜨리는 이유와 회피법.
154. https://gist.github.com/vielhuber/e882f1f7c03f56d9bd70985fe4fe4a5d — blur/transform/will-change가 스태킹 컨텍스트를 만들어 생기는 포지셔닝 문제 모음.
155. https://takazudomodular.com/pj/zcss/docs/layout/positioning/containing-block-for-fixed/ — fixed 포지션의 컨테이닝 블록 규칙 정리.
## L. 접근성 / 폴백
156. https://a11y-101.com/development/reduced-motion — prefers-reduced-motion 기본기.
157. https://blog.pope.tech/2025/12/08/design-accessible-animation-and-movement/ — 접근 가능한 애니메이션 설계 코드 예제.
158. https://github.com/mgifford/ACCESSIBILITY.md/blob/main/examples/SVG_ACCESSIBILITY_BEST_PRACTICES.md — SVG 접근성 베스트 프랙티스. `<img>`로 넣은 SVG에서는 prefers-reduced-motion이 평가되지 않는다는 중요한 지적.
159. https://tympanus.net/codrops/css_reference/supports/ — @supports 레퍼런스.
160. https://iamsteve.me/blog/feature-detection-with-css — CSS 기능 감지 실무.
161. https://dev.to/hexshift/real-world-use-cases-for-svg-filters-in-modern-web-development-52fm — SVG 필터의 실제 사용 사례와 점진적 향상 관점.
## M. 기타 참고
162. https://redev.rocks/articles/18-filters/ — SVG 필터 종합 정리(러시아어).
163. https://webplatform.github.io/docs/svg/tutorials/smarter_svg_filters/ — WebPlatform Docs의 필터 튜토리얼(아카이브).
164. https://srufaculty.sru.edu/david.dailey/svg/SVGOpen2010/filters2.htm — SVG Open 2010 필터 발표 자료. 실험적 조합이 많다.
165. https://en.wikipedia.org/wiki/SVG_filter_effects — 개요.
166. https://riptutorial.com/svg/topic/3262/filters — 필터 토픽 요약.
167. https://www.geeksforgeeks.org/html/svg-fecomposite-element/ — feComposite 요약.
168. https://www.geeksforgeeks.org/html/svg-fespecularlighting-element/ — feSpecularLighting 요약.
169. https://osbo.com/svg/elements/fespecularlighting/ — feSpecularLighting 속성표.
170. https://docs.aspose.com/svg/net/drawing-basics/filters-and-gradients/ — 필터+그라디언트 조합 예제 모음.
171. https://animationpatterns.art/animations/gooey-blob-construction/ — gooey 블롭 구성 단계별 분해.
172. https://animationpatterns.art/animations/gooey-blob-metaball-filter/ — metaball 필터 파라미터 비교.
173. https://doathingy.com/blog/csssvg-gooey-effect-demo — gooey 효과 동작 원리 데모.
174. https://freefrontend.com/javascript-gooey/ — gooey 효과 JS 구현 모음.
175. https://x.com/jh3yy/status/1754546575248953457 — jhey의 `filter: blur(20px) contrast(30)` CSS-only gooey 트릭.
176. https://blog.logrocket.com/complete-guide-using-css-filters-svgs/ — CSS 필터를 SVG에 적용하는 전반 가이드.
177. https://www.testmuai.com/learning-hub/css-backdrop-filter-browser-support/ — backdrop-filter 브라우저 지원·한계 정리.
178. https://www.w3tweaks.com/css/css-filter-backdrop-filter/ — filter/backdrop-filter 비주얼 가이드.
179. https://imagetosvg.com/how-to/svg-duotone-effect — 듀오톤 효과 단계별 가이드.
180. https://www.svg2png.org/blog/svg-filter-effects-guide — 블러·섀도·색 트릭 개괄(2026).
181. https://vectosolve.com/blog/svg-filter-effects-guide — 고급 비주얼 효과 가이드.
182. https://blog.openreplay.com/modern-svg-animation-techniques/ — 현대적 SVG 애니메이션 기법 개괄.
183. https://dev.to/andyfitz/essential-svg-tools-41k5 — SVG 작업 필수 도구 목록.
---
## 검증 방법 기록
본 문서 세트의 코드는 다음 절차로 검증했다.
- **환경**: Playwright Chromium 151.0.0.0 (Windows 11), devicePixelRatio 1, 로컬 HTTP 서버(`python -m http.server`)
- **방법**: 데모 페이지 8개(`test1`~`test8.html`)를 작성해 렌더링 후 스크린샷으로 육안 확인
- **검증된 항목(레시피)**: Gooey(feBlend/atop 양쪽), 그레인 오버레이, 그레이니 그라디언트, 듀오톤, 색수차, squiggle, feMorphology 아웃라인·녹아웃, 종이 질감, feSpecularLighting 광택, 잉크 번짐, Liquid Glass v1/v2, RGB 분리 렌즈, 프로스티드 글래스, 포스터화, 디더, feTile 패턴, 리소그래프, 물결 텍스트(SMIL), 네온 체인 feDropShadow, flood-color SMIL 애니메이션, 러프 엣지, 스티커, 스포트라이트(screen/multiply + JS 광원 이동), 글리치 밴드, feConvolveMatrix 엠보스/샤픈, 필터 영역 클리핑 비교
- **추가 실측**: `CSS.supports('backdrop-filter','url(#x)')``true`(따라서 기능 감지로 부적합); `display:none` SVG 내부 필터도 Chromium에서는 동작; 존재하지 않는 필터 참조 시 필터만 무시되고 요소는 렌더됨(CSS·SVG 어트리뷰트 양쪽); `saturate(0)`이 linearRGB에서 `#808080`, sRGB에서 `#4a4a4a`로 갈림; blur 헤일로의 색공간별 차이; `primitiveUnits="objectBoundingBox"` 스케일링(90px 박스 × 0.05 ≈ 4.5px); feDisplacementMap 채널 기본값(A)과 R/G 명시의 결과 차이; feConvolveMatrix 커널 합(0 vs 1)과 bias의 상호작용; `navigator.hardwareConcurrency`/`deviceMemory` 반환값
- **미검증으로 남은 항목**: `SVGSVGElement.pauseAnimations()` 기반 SMIL 제어, UA 기반 backdrop-filter 프로브, **Firefox/Safari/모바일 실기기에서의 모든 동작**(브라우저 미보유). 문서 내 브라우저별 이슈는 벤더 버그 트래커·MDN 호환 데이터·1차 아티클의 보고에 근거한 것이며 직접 재현하지 않았다.