# 02. SVG 필터 레시피북 — 바로 붙여넣는 완성 코드 > 22개 레시피. 모든 코드는 **생략 없는 완성본**이며, 별도 표기가 없으면 **Chromium 151에서 실제 렌더링 검증 완료**다. > 성능 등급: **상**=스크롤/애니메이션 중에도 안전 · **중**=정적이면 안전, 애니메이션은 작은 면적만 · **하**=큰 면적·모바일·상시 애니메이션 금물 **공통 전제**: 아래 컨테이너를 문서 어딘가(보통 `` 직전)에 한 번 넣고, 각 레시피의 ``를 그 안에 모은다. ```html ``` --- ## 목차 | # | 레시피 | 성능 | 핵심 프리미티브 | |---|---|---|---| | 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
``` ```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 권장 | **마무리 방식 선택** | 코드 | 결과 | |---|---| | `` | 원본을 위에 얹음. 원본 색이 선명하게 살아남 | | `` | 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

Editorial

텍스트는 반드시 필터 밖 레이어에 둔다.

``` ```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
Liquid Glass
``` ```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 ``` **파라미터 가이드** | 조절 | 방법 | |---|---| | 굴절 강도 | `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
``` ```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
FROSTED
``` ```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

GLITCH

``` ```css .chroma { font-size: 44px; font-weight: 800; color: #fff; /* 흰색 소스여야 RGB 분리가 선명하다 */ filter: url(#chromatic); } ``` **렌즈 색수차 변형** (블러 포함, 사진에 적용) ```xml ``` **파라미터 가이드** | 조절 | 값 | |---|---| | 분리 강도 | `dx`: ±1(미묘) / ±3(표준) / ±8(강한 글리치) | | 수직 분리 | `dy`도 함께 주면 대각 분리 | | 초점 흐림 | 각 채널 `stdDeviation` — G는 낮게(0.5), R/B는 높게(2) 두면 렌즈 느낌 | **왜 `screen`인가** — R/G/B 각각을 분리하면 나머지 채널이 0(검정)이다. `screen`은 가산 합성이라 세 채널을 원래 색으로 되돌린다. `normal`이면 마지막 채널만 보인다. **함정** — **소스가 유채색이면 결과가 탁해진다.** 흰색/밝은 회색 텍스트에서 가장 잘 나온다. **성능: 중** — 프리미티브 7개지만 전부 가벼운 연산. 호버 트리거로 짧게 쓰는 것을 권장. --- ## 09. 손그림 러프 엣지 **언제 쓰나** — 딱딱한 사각형/버튼을 손으로 그린 듯 만들기. 일러스트레이션 톤의 브랜드, 노트/스케치 UI. ```html
Hand drawn
``` ```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 ``` **텍스트에 적용할 때** — `scale`을 **4 이하**로. 그 이상은 판독성이 급격히 떨어진다. **성능: 중** — 정적이면 안전. seed/scale 애니메이션은 §11 참고. --- ## 10. 잉크 번짐 / 마커 필 **언제 쓰나** — 스탬프, 마커 하이라이트, 리소/실크스크린 텍스트, 손글씨 느낌 강조. **원리** — 변위로 윤곽을 흐트러뜨리고 → 블러로 번지게 하고 → 알파 대비로 다시 단단한 경계를 만든다. 잉크가 종이에 스며든 뒤 마른 결과와 같은 순서다. ```html INK ``` ```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
``` ```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

LIQUID

``` ```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 ``` > 룩은 "흐르는" 대신 "숨쉬는" 쪽에 가깝지만 비용이 크게 낮다. 대부분의 경우 이쪽으로 충분하다. **성능: 하** (`baseFrequency` 애니메이션) / **중** (`scale` 애니메이션) --- ## 13. 듀오톤 / 그라디언트 맵 **언제 쓰나** — 브랜드 컬러로 사진 톤을 통일. 잡다한 스톡 이미지들을 한 화면에 놓아도 일관돼 보이게 만드는 가장 강력한 기법. ```html 설명 ``` ```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 ` `; } // duotoneFilter('brandDuo', '#12103A', '#FF6B35') ``` **트라이톤** — `tableValues`에 값을 3개 넣는다 (그림자 / 중간톤 / 하이라이트). ```xml ``` **함정** - **`color-interpolation-filters="sRGB"`가 없으면 색이 전부 어긋난다.** 이 레시피에서는 필수. - 원본 대비가 낮은 사진은 듀오톤에서 뭉개진다. 앞에 `feComponentTransfer type="linear" slope="1.3" intercept="-0.15"`로 대비를 올려주면 좋다. **성능: 상** — 픽셀당 단순 산술 2회. 이미지에 적용해도 부담 없다. --- ## 14. 포스터화 / 컬러 밴딩 **언제 쓰나** — 스크린프린트, 레트로 게임, 벡터 일러스트 느낌. 그라디언트를 계단으로 만든다. ```html ``` ```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 ``` **듀오톤과 결합** — 레시피 13의 그레이스케일 뒤에 `discrete`를 넣으면 **2색 스크린프린트**가 된다. **성능: 상** --- ## 15. 디더 / 임계값 (하프톤 대체) **언제 쓰나** — 1비트 룩, 레트로 맥/게임보이, 리소 하프톤 근사. 그라디언트를 점 패턴으로 분해한다. **원리** — 그레이 노이즈를 소스에 **가산**한 뒤 2단계 `discrete`로 자른다. 노이즈가 임계선 주변을 흩뜨려 점 패턴이 생긴다. ```html ``` ```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
``` ```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

OUTLINE

HOLLOW

``` ```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 ``` **성능: 상** — 텍스트는 선택·검색·스크린리더 접근이 그대로 유지된다. --- ## 18. 스티커 **언제 쓰나** — 배지, 이모지 스타일 라벨, 컷아웃 요소. 흰 테두리 + 아래로 떨어지는 그림자. ```html STICKER ``` ```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

NEON

``` ```css .neon { font-size: 40px; font-weight: 900; color: #fff; filter: url(#neon); } ``` **애니메이션 글로우 링** — `flood-color`는 CSS/SMIL 애니메이션이 가능하다. ```html ``` ```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
Focus
``` ```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 ``` ```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 ``` ```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 ``` **함정** — 필터를 건 요소 안의 텍스트도 조명 처리되어 뿌옇게 된다. 텍스트는 별도 레이어로 얹어라. **성능: 중** --- ## 22. 글리치 밴드 **언제 쓰나** — 로딩 실패 상태, 사이버펑크 전환 효과, 에러 화면. **순간적으로만** 쓴다. **원리** — 대부분이 중립색(127,127)인 평면 위에, 얇은 가로 밴드만 다른 색으로 칠한 이미지를 변위맵으로 쓴다. 밴드가 지나가는 줄만 가로로 밀린다. ```html
SYSTEM ERROR
``` ```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
텍스트
텍스트
``` ```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. 네온 ```