# 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 ``` 필터 영역 밖은 **잘려 나간다**. 기본값은 바운딩 박스보다 사방 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` | ``의 `x/y/width/height` | | `primitiveUnits` | `userSpaceOnUse` | 각 프리미티브의 `x/y/width/height` **그리고 `stdDeviation`, `radius`, `dx/dy`, `scale` 같은 길이값** | - `objectBoundingBox` → 0~1 또는 % 로 해석. `0.05` = 바운딩 박스 크기의 5%. - `userSpaceOnUse` → 현재 사용자 좌표계의 절대 길이(대개 px). ```xml ``` > 검증됨: 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 ... ``` | 케이스 | 권장 | |---|---| | 듀오톤, 그라디언트 맵, 브랜드 컬러 정확도 | **반드시 `sRGB`** | | 알파 대비 트릭(gooey), 합성 위주 | `sRGB` (예측 가능) | | 조명(`feDiffuseLighting`/`feSpecularLighting`)에서 물리적 자연스러움 | linearRGB 기본이 더 그럴듯할 때도 있음 — 둘 다 보고 고른다 | | feTurbulence 노이즈 자체 | 어느 쪽이든 무방, 단 후속 대비 조정 결과가 달라짐 | > **함정 5**: `color-interpolation-filters`는 개별 프리미티브에도 걸 수 있다. 체인 중간에서 색공간이 바뀌면 브라우저가 변환을 삽입한다 → 비용 발생 + 예측 어려움. **`` 한 곳에만 지정**하는 것이 안전하다. ### 0.6 필터 붙이는 법 ```html
``` 필터 정의를 담는 SVG 컨테이너 패턴: ```html ``` > 검증됨: Chromium에서는 `` 안에 정의한 필터도 정상 동작했다. 하지만 과거 엔진 이슈 보고가 있고 Firefox/WebKit은 이 세션에서 미검증이므로, **`width=0 height=0 + position:absolute` 패턴을 기본값으로 쓴다.** > 검증됨: 존재하지 않는 필터를 참조하면(`filter:url(#없음)`) Chromium은 **필터를 무시하고 원본을 그대로 렌더**한다(CSS·SVG 어트리뷰트 양쪽 모두). 현행 Filter Effects L1과 일치. 다만 구 SVG 1.1은 "요소를 렌더하지 않음"으로 정의했었으므로, 레거시 엔진에서 요소가 사라질 수 있다 — **ID 오타는 치명적일 수 있다.** --- ## 1. 소스 생성 프리미티브 입력 없이 새 이미지를 만들어내는 것들. 필터 그래프의 "재료". --- ### 1.1 `` — 절차적 노이즈 가장 강력하고, 가장 비싼 프리미티브. 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 ``` 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 `` — 단색 채우기 | 속성 | 기본값 | 의미 | |---|---|---| | `flood-color` | `black` | 채울 색. **CSS 프로퍼티이기도 하다 → CSS/SMIL로 애니메이션 가능** | | `flood-opacity` | `1` | 0~1 | 필터 **서브영역 전체**를 색으로 채운다. 단독으로는 쓸모없고 `feComposite operator="in"`으로 마스킹해서 쓴다. ```xml ``` > **함정 6**: `feFlood`는 서브영역 전체를 채우므로, 서브영역이 필터 영역(기본 120%)이면 색이 사방으로 넘친다. `operator="in"` 마스킹을 **반드시** 뒤에 붙이거나 `x/y/width/height`로 서브영역을 제한하라. > **활용**: `flood-color`가 CSS 프로퍼티라는 점이 핵심이다. `baseFrequency`·`stdDeviation` 같은 어트리뷰트는 CSS로 애니메이션할 수 없지만, **`flood-color`, `flood-opacity`, `lighting-color`는 CSS 트랜지션/애니메이션이 가능하다.** (검증됨: SMIL `` 정상 동작) --- ### 1.3 `` — 외부 이미지 끌어오기 | 속성 | 기본값 | 의미 | |---|---|---| | `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 `` — 서브영역 타일링 `in`의 **필터 서브영역**을 하나의 타일로 삼아, 자기 서브영역을 가득 채울 때까지 x/y로 반복한다. ```xml ``` > 검증됨 (Chromium). 작은 타일만 계산하고 복사하므로 **큰 면적 노이즈보다 훨씬 싸다** — 성능 최적화 기법으로도 유용. > **함정 10**: `feTile`은 `in`에 **서브영역이 명시되어 있어야** 동작한다. 앞 프리미티브에 `x/y/width/height`가 없으면 타일 크기가 필터 영역 전체가 되어 반복이 일어나지 않는다. --- ## 2. 기하/변형 프리미티브 --- ### 2.1 `` | 속성 | 기본값 | 의미 | |---|---|---| | `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 `` | 속성 | 기본값 | 의미 | |---|---|---| | `dx`, `dy` | `0` | 이동량 (primitiveUnits 기준) | 가장 싼 프리미티브. 그림자·색분리(RGB split)의 기본 부품. > **함정 13**: `feOffset`으로 민 결과는 필터 영역 밖으로 나가면 잘린다. `dx="20"`이면 영역도 그만큼 넓혀라. --- ### 2.3 `` — 단축 그림자 | 속성 | 기본값 | |---|---| | `dx` | `2` | | `dy` | `2` | | `stdDeviation` | `2` | | `flood-color` | `black` | | `flood-opacity` | `1` | 내부적으로 `feGaussianBlur + feOffset + feFlood + feComposite + feMerge`와 동등. 훨씬 짧고 최적화도 잘 된다. **체이닝으로 다중 글로우** (검증됨 — 네온 텍스트): ```xml ``` > `feDropShadow`는 **결과에 원본을 포함**하므로 `feMerge`가 필요 없다. 체인으로 연결하면 그림자가 누적된다. --- ### 2.4 `` — 팽창/침식 | 속성 | 기본값 | 의미 | |---|---|---| | `operator` | `erode` | `erode`(축소/가늘게) \| `dilate`(팽창/굵게) | | `radius` | `0` | 반경. `x y` 2값 가능. 음수는 에러 | **핵심 용도**: 텍스트 아웃라인. `stroke`는 선이 글자 안쪽으로 반 들어가 글자를 얇게 만들지만, `feMorphology dilate`는 **바깥으로만 키운다.** ```xml ``` ```xml ``` > **함정 14**: `feMorphology`의 커널은 **사각형**이다. `radius`가 커지면 둥근 모서리가 각지게 뭉개진다. radius 8 이상은 결과를 반드시 눈으로 확인하라. 부드러운 확장이 필요하면 `feGaussianBlur` + `feComponentTransfer`(알파 대비)로 대체. > **함정 15**: `radius` 기본값이 `0`이라 지정 안 하면 아무 일도 안 일어난다. --- ### 2.5 `` — 변위 (유기적 디자인의 심장) | 속성 | 기본값 | 의미 | |---|---|---| | `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 ``` ```xml ``` (b)의 맵 SVG 원문 (인코딩 전) — **가장자리에만 변위가 몰리는 렌즈 프로파일**: ```xml ``` > 검증됨: `mix-blend-mode: screen`이 `feImage`가 래스터화하는 SVG 내부에서 정상 동작한다. R채널만 있는 레이어와 G채널만 있는 레이어를 screen으로 합치면 채널이 서로 간섭 없이 합쳐진다(= 채널별 가산). 중앙 28~72% 구간을 128로 평평하게 두는 것이 "가장자리에서만 굴절"의 핵심. --- ### 2.6 `` — 컨볼루션 커널 | 속성 | 기본값 | 의미 | |---|---|---| | `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 ``` > **함정 19**: `preserveAlpha="false"`(기본값)면 알파에도 커널이 적용되어 형태가 무너진다. 색만 만지고 싶으면 `true`로. > **함정 20**: 큰 `order`(5×5 이상)는 픽셀당 연산이 제곱으로 늘어 매우 비싸다. 실시간 애니메이션에는 쓰지 마라. --- ## 3. 색 조작 프리미티브 --- ### 3.1 `` | 속성 | 기본값 | 의미 | |---|---|---| | `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 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" values="0 0 0 0 0.133 0 0 0 0 0.827 0 0 0 0 0.933 0 0 0 1 0" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7" 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 `` + `` 채널별 **전달 함수**를 적용한다. 톤 커브 도구라고 생각하면 된다. | type | 파라미터 | 공식 | |---|---|---| | `identity` | — | `C' = C` | | `table` | `tableValues="v0 v1 ... vn"` | n개 값을 균등 구간으로 **선형 보간** | | `discrete` | `tableValues="v0 v1 ... vn"` | 균등 구간별 **계단 함수**(보간 없음) | | `linear` | `slope`(1), `intercept`(0) | `C' = slope·C + intercept` | | `gamma` | `amplitude`(1), `exponent`(1), `offset`(0) | `C' = amplitude·C^exponent + offset` | **table의 동작**: 값이 n개면 n−1개 구간이 만들어진다. - `tableValues="0 1"` → 항등(0→0, 1→1) - `tableValues="0.05 0.98"` → 그림자를 0.05로, 하이라이트를 0.98로 리매핑 → **듀오톤의 원리** - `tableValues="0 0.5 1"` → 3점 톤 커브 **discrete의 동작**: `tableValues="0 0.25 0.5 0.75 1"` → 5단계 포스터화. (검증됨) **듀오톤 = 그레이스케일 + 채널별 2점 table** (검증됨) ```xml ``` **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 `` | 속성 | 기본값 | |---|---| | `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 `` | 속성 | 기본값 | |---|---| | `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 `` / `` 여러 레이어를 순서대로 `over` 합성한다. **뒤에 오는 `feMergeNode`가 위**에 쌓인다. ```xml ``` > `feComposite operator="over"`를 여러 번 체인하는 것과 같지만 훨씬 읽기 쉽다. --- ## 5. 조명 프리미티브 알파 채널을 **높이맵(범프맵)** 으로 해석해 3D 조명을 계산한다. `feTurbulence`와 조합하면 종이·가죽·금속 질감이 나온다. ### 5.1 `` (난반사 — 무광) | 속성 | 기본값 | 의미 | |---|---|---| | `surfaceScale` | `1` | 알파=1일 때의 표면 높이. 클수록 굴곡 심함 | | `diffuseConstant` | `1` | 반사 계수(kd). 0 이상 | | `lighting-color` | `white` | 광원 색. **CSS 프로퍼티 → 애니메이션 가능** | | `kernelUnitLength` | (없음) | 노멀 계산 샘플 간격. 대개 생략 | 결과는 **불투명한 RGBA**다(알파=1). 그래서 반드시 원본 알파로 다시 잘라야 한다. ### 5.2 `` (정반사 — 광택) | 속성 | 기본값 | 범위 | |---|---|---| | `surfaceScale` | `1` | — | | `specularConstant` | `1` | ks, 0 이상 | | `specularExponent` | `1` | **1 ~ 128**. 클수록 하이라이트가 작고 날카로움 | | `lighting-color` | `white` | — | 결과는 **정반사 성분만 담긴 RGBA**로, 알파가 0이 아닌 영역이 하이라이트다. 원본에 **가산**해야 한다. ### 5.3 광원 요소 (정확히 하나를 자식으로) ```xml ``` | 속성 | 기본값 | 의미 | |---|---|---| | `azimuth` | `0` | xy평면 방향각 0~360° | | `elevation` | `0` | 고도각 0~90°. 낮을수록 그림자가 길고 대비 강함 | ```xml ``` | 속성 | 기본값 | |---|---| | `x`, `y`, `z` | `0` | ```xml ``` | 속성 | 기본값 | 의미 | |---|---|---| | `x/y/z` | `0` | 광원 위치 | | `pointsAtX/Y/Z` | `0` | 조준점 | | `specularExponent` | `1` | 중심부 집중도 | | `limitingConeAngle` | (없음=제한 없음) | 원뿔 반각(도). 넘으면 빛 없음 | ### 5.4 검증된 조합 패턴 ```xml ``` ```xml ``` ```xml ``` > 검증됨: (A)(B)(C) 모두 Chromium에서 의도대로 렌더. (C)에서 `mode="multiply"`로 바꾸면 음영/비네트 룩이 된다. > **함정 29**: 조명 결과를 그냥 출력하면 **필터 영역 전체가 불투명 사각형**으로 칠해진다. `feComposite operator="in"` + `SourceAlpha` 클리핑을 잊지 마라. > **함정 30**: `surfaceScale`이 크면 (20~50) 스투코/구겨진 플라스틱 같은 과장된 질감이 된다. 종이 느낌은 1~3이 적당. > **함정 31**: `kernelUnitLength`는 엔진마다 해석이 달라 결과가 갈린다. 크로스브라우저가 중요하면 **쓰지 마라.** --- ## 6. CSS 단축 필터 ↔ 프리미티브 대응표 | CSS 함수 | 등가 프리미티브 | 비고 | |---|---|---| | `blur()` | `feGaussianBlur stdDeviation=""` | 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 ``는 **linearRGB가 기본**이다. 같은 결과를 원하면 `color-interpolation-filters="sRGB"`를 명시하라. **언제 CSS 단축을 쓰나**: 단순 blur/그림자/채도만 필요하면 CSS 단축이 **더 빠르고 더 잘 가속되며 Safari 호환성이 좋다.** SVG 필터는 CSS로 표현 불가능한 것(노이즈, 변위, 채널 분리, 알파 대비, 조명)에만 써라. --- ## 7. 애니메이션 가능성 매트릭스 | 대상 | SMIL `` | 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 ``` ```xml ``` ```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인가 → 사각 아티팩트