웹 디자인 파이프라인 스킬과 이를 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)
61 KiB
02. SVG 필터 레시피북 — 바로 붙여넣는 완성 코드
22개 레시피. 모든 코드는 생략 없는 완성본이며, 별도 표기가 없으면 Chromium 151에서 실제 렌더링 검증 완료다. 성능 등급: 상=스크롤/애니메이션 중에도 안전 · 중=정적이면 안전, 애니메이션은 작은 면적만 · 하=큰 면적·모바일·상시 애니메이션 금물
공통 전제: 아래 컨테이너를 문서 어딘가(보통 </body> 직전)에 한 번 넣고, 각 레시피의 <filter>를 그 안에 모은다.
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<defs>
<!-- 여기에 filter들 -->
</defs>
</svg>
목차
| # | 레시피 | 성능 | 핵심 프리미티브 |
|---|---|---|---|
| 01 | Gooey / Metaball | 중 | feGaussianBlur + feColorMatrix |
| 02 | 필름 그레인 오버레이 (CSS만) | 상 | feTurbulence (data URI) |
| 03 | 그레이니 그라디언트 | 상 | feTurbulence + CSS filter |
| 04 | 종이 질감 | 중 | feTurbulence + feDiffuseLighting |
| 05 | Liquid Glass (굴절 유리) | 중 | feImage + feDisplacementMap + backdrop-filter |
| 06 | Liquid Glass + 색수차 | 하 | 3× feDisplacementMap |
| 07 | 프로스티드 글래스 (폴백 겸용) | 상 | backdrop-filter + noise |
| 08 | 크로마틱 애버레이션 / RGB Split | 중 | feOffset + feColorMatrix + feBlend |
| 09 | 손그림 러프 엣지 | 중 | feTurbulence + feDisplacementMap |
| 10 | 잉크 번짐 | 중 | 변위 + 블러 + 알파 대비 |
| 11 | Squigglevision (애니메이션 손그림) | 하 | seed 순환 |
| 12 | 물결 텍스트 | 하 | SMIL baseFrequency |
| 13 | 듀오톤 / 그라디언트 맵 | 상 | feColorMatrix + feComponentTransfer |
| 14 | 포스터화 / 트라이톤 | 상 | feComponentTransfer discrete |
| 15 | 디더 / 임계값 | 중 | 노이즈 가산 + discrete |
| 16 | 리소그래프 인쇄 | 중 | mix-blend-mode + grain + 오프셋 |
| 17 | 텍스트 아웃라인 | 상 | feMorphology |
| 18 | 스티커 | 상 | feMorphology + feDropShadow |
| 19 | 네온 글로우 | 중 | 체인 feDropShadow |
| 20 | 스포트라이트 | 중 | feSpotLight + feDiffuseLighting |
| 21 | 광택/엠보스 | 중 | feSpecularLighting |
| 22 | 글리치 밴드 | 하 | feFlood 밴드 + feDisplacementMap |
01. Gooey / Metaball
언제 쓰나 — 떠다니는 블롭 배경, 확장되는 FAB 메뉴, 로딩 인디케이터, 유기적 로고 모션. 개별 도형들이 액체처럼 서로 융합해야 할 때.
원리 — 블러로 알파를 번지게 한 뒤, feColorMatrix로 알파 대비를 극단적으로 올려 다시 날카로운 경계를 만든다. 두 도형의 번진 알파가 겹치는 지점에서 임계값을 넘으면 하나로 붙는다.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="goo" x="-25%" y="-25%" width="150%" height="150%"
color-interpolation-filters="sRGB">
<feGaussianBlur in="SourceGraphic" stdDeviation="9" result="blur"/>
<feColorMatrix in="blur" type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 20 -9" result="goo"/>
<feComposite in="SourceGraphic" in2="goo" operator="atop"/>
</filter>
</defs>
</svg>
<div class="goo-menu">
<span></span><span></span><span></span>
</div>
.goo-menu {
filter: url(#goo);
display: flex;
gap: 6px;
align-items: center;
/* 필터 영역이 잘리지 않도록 여백 확보 */
padding: 24px;
}
.goo-menu span {
width: 56px;
height: 56px;
border-radius: 50%;
background: #34d399;
display: block;
}
@media (prefers-reduced-motion: reduce) {
.goo-menu { filter: none; }
}
파라미터 가이드
| 값 | 효과 |
|---|---|
stdDeviation 5 |
도형이 아주 가까워야 붙음. 미세한 융합 |
stdDeviation 9~12 |
표준. 목(neck)이 자연스럽게 늘어남 |
stdDeviation 20+ |
멀리서도 붙음. 형태가 뭉개짐 |
알파 행렬 M(=20) ↑ |
경계가 더 날카로움. 12~30 권장 |
O/M (=9/20=0.45) ↑ |
형태가 더 수축. 0.35~0.5 권장 |
마무리 방식 선택
| 코드 | 결과 |
|---|---|
<feBlend in="SourceGraphic" in2="goo"/> |
원본을 위에 얹음. 원본 색이 선명하게 살아남 |
<feComposite in="SourceGraphic" in2="goo" operator="atop"/> |
goo 영역 안에서만 원본. 모서리가 뾰족한 도형일 때 필수 |
| (마무리 없음) | goo만. 알파 대비 결과의 색이 그대로 나옴 |
함정
- 필터 컨테이너 안에 텍스트를 넣지 마라. 텍스트가 블러+임계값을 거쳐 판독 불가능한 덩어리가 된다(검증됨). 텍스트는 필터 밖 별도 레이어에 절대배치로 얹어라.
- 컨테이너에 여백이 없으면 블롭이 필터 영역 경계에서 잘린다.
성능: 중 — 정적이면 문제없음. 블롭이 움직이면 매 프레임 블러 재계산이라 면적이 크면 급격히 무거워진다. 400×400px 이하로 제한하고 will-change: filter는 쓰지 마라(§03 참고).
02. 필름 그레인 오버레이 (SVG 없이 CSS만으로)
언제 쓰나 — 어떤 배경에도 얹는 범용 텍스처. 밋밋한 그라디언트/단색을 아날로그하게 만든다. 가장 자주 쓰게 될 레시피.
원리 — SVG를 data URI 배경 이미지로 넣으면 한 번만 래스터화되어 캐시된다. 요소에 filter를 거는 것과 달리 리페인트마다 재계산되지 않는다.
.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로 바꿔라.
.grainy-light::after {
mix-blend-mode: multiply;
opacity: 0.25;
}
URL 인코딩 필수 문자: #→%23, <→%3C, >→%3E, %→%25, 큰따옴표는 작은따옴표로.
성능: 상 — 정적 배경 이미지라 리페인트 비용이 사실상 0. 가장 안전한 노이즈 방법.
03. 그레이니 그라디언트
언제 쓰나 — 그라디언트의 밴딩(계단 현상)을 감추면서 동시에 질감을 준다. 히어로 배경, 카드 표면.
.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으로 조명한다. 실제 종이 섬유의 미세한 요철을 시뮬레이션한다.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="paper" x="0%" y="0%" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency="0.04"
numOctaves="5" seed="3" result="noise"/>
<feDiffuseLighting in="noise" lighting-color="#e8e0d0"
surfaceScale="2" result="lit">
<feDistantLight azimuth="45" elevation="60"/>
</feDiffuseLighting>
<feComposite in="lit" in2="SourceAlpha" operator="in"/>
</filter>
</defs>
</svg>
<div class="paper-card">
<div class="paper-surface"></div>
<div class="paper-content">
<h2>Editorial</h2>
<p>텍스트는 반드시 필터 밖 레이어에 둔다.</p>
</div>
</div>
.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), 가장자리는 극단값인 맵이 렌즈 효과의 전부다.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="lens" x="0%" y="0%" width="100%" height="100%"
color-interpolation-filters="sRGB">
<feImage preserveAspectRatio="none" x="0" y="0" width="220" height="88" result="map"
href="data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='88'%3E%3Cdefs%3E%3ClinearGradient id='rx' x1='0' y1='0' x2='1' y2='0'%3E%3Cstop offset='0' stop-color='rgb(0,0,0)'/%3E%3Cstop offset='0.28' stop-color='rgb(128,0,0)'/%3E%3Cstop offset='0.72' stop-color='rgb(128,0,0)'/%3E%3Cstop offset='1' stop-color='rgb(255,0,0)'/%3E%3C/linearGradient%3E%3ClinearGradient id='gy' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='rgb(0,0,0)'/%3E%3Cstop offset='0.28' stop-color='rgb(0,128,0)'/%3E%3Cstop offset='0.72' stop-color='rgb(0,128,0)'/%3E%3Cstop offset='1' stop-color='rgb(0,255,0)'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='220' height='88' rx='44' fill='rgb(128,128,128)'/%3E%3Crect width='220' height='88' rx='44' fill='url(%23rx)' style='mix-blend-mode:screen'/%3E%3Crect width='220' height='88' rx='44' fill='url(%23gy)' style='mix-blend-mode:screen'/%3E%3C/svg%3E"/>
<feDisplacementMap in="SourceGraphic" in2="map" scale="-60"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
</svg>
<div class="glass-pill">Liquid Glass</div>
.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); }
}
변위맵 원문 (인코딩 전 — 크기를 바꿀 때 이걸 수정해 다시 인코딩)
<svg xmlns="http://www.w3.org/2000/svg" width="220" height="88">
<defs>
<linearGradient id="rx" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="rgb(0,0,0)"/>
<stop offset="0.28" stop-color="rgb(128,0,0)"/>
<stop offset="0.72" stop-color="rgb(128,0,0)"/>
<stop offset="1" stop-color="rgb(255,0,0)"/>
</linearGradient>
<linearGradient id="gy" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="rgb(0,0,0)"/>
<stop offset="0.28" stop-color="rgb(0,128,0)"/>
<stop offset="0.72" stop-color="rgb(0,128,0)"/>
<stop offset="1" stop-color="rgb(0,255,0)"/>
</linearGradient>
</defs>
<rect width="220" height="88" rx="44" fill="rgb(128,128,128)"/>
<rect width="220" height="88" rx="44" fill="url(#rx)" style="mix-blend-mode:screen"/>
<rect width="220" height="88" rx="44" fill="url(#gy)" style="mix-blend-mode:screen"/>
</svg>
파라미터 가이드
| 조절 | 방법 |
|---|---|
| 굴절 강도 | scale: −30(약함) ~ −90(강함). 음수가 볼록 렌즈 |
| 굴절 방향 반전 | scale 부호 뒤집기 (오목 렌즈) |
| 굴절이 몰리는 폭 | 그라디언트 stop 0.28/0.72를 0.15/0.85로 → 가장자리에 더 집중 |
| 유리 두께감 | box-shadow의 inset 하이라이트 강도 |
| 배경 밝기/채도 | backdrop-filter의 brightness() saturate() |
브라우저 지원
backdrop-filter: url(#id)는 Chromium 계열만 실제로 렌더한다. Safari·Firefox는 미지원(Firefox에는 기능 요청이 올라와 있음).- ⚠️
CSS.supports('backdrop-filter','url(#x)')는 값 파싱만 검사하므로 신뢰할 수 없다(Chromium에서 true 확인, 다른 엔진도 true를 반환할 가능성이 높음). 폴백은@supports가 아니라 "폴백이 그 자체로 괜찮게 보이도록" 설계하라 — 레시피 07을 기본으로 깔고 그 위에 렌즈를 얹는 방식.
함정
backdrop-filter의 필터 영역은 요소 크기에 자동으로 맞춰지지 않는다. 맵의width/height와 요소의width/height가 어긋나면 굴절이 엉뚱한 곳에 생긴다. 크기가 다른 버튼마다 별도의 맵을 만들어라.- 반응형으로 크기가 계속 변하는 요소에는 부적합. 고정 크기 칩/독/툴바에 쓴다.
성능: 중 — feImage는 한 번 래스터화 후 캐시된다. scale만 애니메이션하면 맵 재생성이 없어 비교적 싸다. 반대로 요소 크기를 애니메이션하면 매 프레임 맵 재계산 → 매우 비쌈.
06. Liquid Glass + 색수차 (프리즘 유리)
언제 쓰나 — 프리미엄 히어로, 프로덕트 쇼케이스. 유리 가장자리에서 빛이 무지개로 분산되는 효과.
원리 — 같은 변위맵을 R/G/B 각각 다른 scale 로 3번 적용한 뒤 screen으로 합친다. 굴절률이 파장마다 다른 물리 현상을 흉내낸다.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="lensRGB" x="0%" y="0%" width="100%" height="100%"
color-interpolation-filters="sRGB">
<feImage preserveAspectRatio="none" x="0" y="0" width="200" height="96" result="map"
href="data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='96'%3E%3Cdefs%3E%3ClinearGradient id='rx' x1='0' y1='0' x2='1' y2='0'%3E%3Cstop offset='0' stop-color='rgb(0,0,0)'/%3E%3Cstop offset='0.3' stop-color='rgb(128,0,0)'/%3E%3Cstop offset='0.7' stop-color='rgb(128,0,0)'/%3E%3Cstop offset='1' stop-color='rgb(255,0,0)'/%3E%3C/linearGradient%3E%3ClinearGradient id='gy' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='rgb(0,0,0)'/%3E%3Cstop offset='0.3' stop-color='rgb(0,128,0)'/%3E%3Cstop offset='0.7' stop-color='rgb(0,128,0)'/%3E%3Cstop offset='1' stop-color='rgb(0,255,0)'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='200' height='96' rx='48' fill='rgb(128,128,128)'/%3E%3Crect width='200' height='96' rx='48' fill='url(%23rx)' style='mix-blend-mode:screen'/%3E%3Crect width='200' height='96' rx='48' fill='url(%23gy)' style='mix-blend-mode:screen'/%3E%3C/svg%3E"/>
<feDisplacementMap in="SourceGraphic" in2="map" scale="-56"
xChannelSelector="R" yChannelSelector="G" result="dR"/>
<feColorMatrix in="dR" type="matrix" result="cR"
values="1 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 1 0"/>
<feDisplacementMap in="SourceGraphic" in2="map" scale="-60"
xChannelSelector="R" yChannelSelector="G" result="dG"/>
<feColorMatrix in="dG" type="matrix" result="cG"
values="0 0 0 0 0
0 1 0 0 0
0 0 0 0 0
0 0 0 1 0"/>
<feDisplacementMap in="SourceGraphic" in2="map" scale="-64"
xChannelSelector="R" yChannelSelector="G" result="dB"/>
<feColorMatrix in="dB" type="matrix" result="cB"
values="0 0 0 0 0
0 0 0 0 0
0 0 1 0 0
0 0 0 1 0"/>
<feBlend in="cR" in2="cG" mode="screen" result="rg"/>
<feBlend in="rg" in2="cB" mode="screen"/>
</filter>
</defs>
</svg>
<div class="prism-pill"></div>
.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의 기본 레이어로 항상 깔아둔다.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="frostNoise" x="0%" y="0%" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency="0.9"
numOctaves="3" stitchTiles="stitch" result="n"/>
<feColorMatrix in="n" type="saturate" values="0"/>
</filter>
</defs>
</svg>
<div class="frost">FROSTED</div>
.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 룩, 호버 시 순간 왜곡.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="chromatic" x="-20%" y="-20%" width="140%" height="140%"
color-interpolation-filters="sRGB">
<feOffset in="SourceGraphic" dx="-3" dy="0" result="rShift"/>
<feColorMatrix in="rShift" type="matrix" result="red"
values="1 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 1 0"/>
<feColorMatrix in="SourceGraphic" type="matrix" result="green"
values="0 0 0 0 0
0 1 0 0 0
0 0 0 0 0
0 0 0 1 0"/>
<feOffset in="SourceGraphic" dx="3" dy="0" result="bShift"/>
<feColorMatrix in="bShift" type="matrix" result="blue"
values="0 0 0 0 0
0 0 0 0 0
0 0 1 0 0
0 0 0 1 0"/>
<feBlend in="red" in2="green" mode="screen" result="rg"/>
<feBlend in="rg" in2="blue" mode="screen"/>
</filter>
</defs>
</svg>
<h1 class="chroma">GLITCH</h1>
.chroma {
font-size: 44px;
font-weight: 800;
color: #fff; /* 흰색 소스여야 RGB 분리가 선명하다 */
filter: url(#chromatic);
}
렌즈 색수차 변형 (블러 포함, 사진에 적용)
<filter id="chromaticLens" x="-10%" y="-10%" width="120%" height="120%"
color-interpolation-filters="sRGB">
<feColorMatrix in="SourceGraphic" type="matrix"
values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
<feOffset dx="2" dy="0"/>
<feGaussianBlur stdDeviation="2" result="redChannel"/>
<feColorMatrix in="SourceGraphic" type="matrix"
values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
<feGaussianBlur stdDeviation="0.5" result="greenChannel"/>
<feColorMatrix in="SourceGraphic" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
<feOffset dx="-2" dy="0"/>
<feGaussianBlur stdDeviation="2" result="blueChannel"/>
<feBlend in="redChannel" in2="greenChannel" mode="screen" result="redGreen"/>
<feBlend in="redGreen" in2="blueChannel" mode="screen"/>
</filter>
파라미터 가이드
| 조절 | 값 |
|---|---|
| 분리 강도 | dx: ±1(미묘) / ±3(표준) / ±8(강한 글리치) |
| 수직 분리 | dy도 함께 주면 대각 분리 |
| 초점 흐림 | 각 채널 stdDeviation — G는 낮게(0.5), R/B는 높게(2) 두면 렌즈 느낌 |
왜 screen인가 — R/G/B 각각을 분리하면 나머지 채널이 0(검정)이다. screen은 가산 합성이라 세 채널을 원래 색으로 되돌린다. normal이면 마지막 채널만 보인다.
함정 — 소스가 유채색이면 결과가 탁해진다. 흰색/밝은 회색 텍스트에서 가장 잘 나온다.
성능: 중 — 프리미티브 7개지만 전부 가벼운 연산. 호버 트리거로 짧게 쓰는 것을 권장.
09. 손그림 러프 엣지
언제 쓰나 — 딱딱한 사각형/버튼을 손으로 그린 듯 만들기. 일러스트레이션 톤의 브랜드, 노트/스케치 UI.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="rough" x="-10%" y="-10%" width="120%" height="120%">
<feTurbulence type="fractalNoise" baseFrequency="0.03"
numOctaves="4" seed="12" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="9"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
</svg>
<div class="rough-card">Hand drawn</div>
.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개 만들어 돌려 쓰면 살아난다.
<filter id="rough1"><feTurbulence type="fractalNoise" baseFrequency="0.03" numOctaves="4" seed="1" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="9" xChannelSelector="R" yChannelSelector="G"/></filter>
<filter id="rough2"><feTurbulence type="fractalNoise" baseFrequency="0.03" numOctaves="4" seed="7" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="9" xChannelSelector="R" yChannelSelector="G"/></filter>
<filter id="rough3"><feTurbulence type="fractalNoise" baseFrequency="0.03" numOctaves="4" seed="23" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="9" xChannelSelector="R" yChannelSelector="G"/></filter>
텍스트에 적용할 때 — scale을 4 이하로. 그 이상은 판독성이 급격히 떨어진다.
성능: 중 — 정적이면 안전. seed/scale 애니메이션은 §11 참고.
10. 잉크 번짐 / 마커 필
언제 쓰나 — 스탬프, 마커 하이라이트, 리소/실크스크린 텍스트, 손글씨 느낌 강조.
원리 — 변위로 윤곽을 흐트러뜨리고 → 블러로 번지게 하고 → 알파 대비로 다시 단단한 경계를 만든다. 잉크가 종이에 스며든 뒤 마른 결과와 같은 순서다.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="inkbleed" x="-20%" y="-20%" width="140%" height="140%"
color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.05"
numOctaves="4" seed="7" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="4"
xChannelSelector="R" yChannelSelector="G" result="d"/>
<feGaussianBlur in="d" stdDeviation="1.2" result="b"/>
<feColorMatrix in="b" type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 14 -6"/>
</filter>
</defs>
</svg>
<span class="ink">INK</span>
.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를 낮은 프레임레이트로 순환시킨다. 매 프레임 새 노이즈를 만드는 게 아니라 몇 장의 "그림"을 번갈아 보여주는 것이라 훨씬 싸고, 룩도 더 정확하다.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="squiggle" x="-10%" y="-10%" width="120%" height="120%">
<feTurbulence id="squiggleNoise" type="fractalNoise"
baseFrequency="0.02" numOctaves="3" seed="1" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="6"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
</svg>
<div class="squiggle-art">
<svg viewBox="0 0 200 120" width="200">
<rect x="20" y="20" width="160" height="80" rx="8"
fill="none" stroke="#0ea5e9" stroke-width="4"/>
<path d="M40 90 L80 40 L120 80 L160 30" fill="none"
stroke="#f43f5e" stroke-width="4" stroke-linecap="round"/>
</svg>
</div>
.squiggle-art { filter: url(#squiggle); }
@media (prefers-reduced-motion: reduce) {
.squiggle-art { filter: none; }
}
(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)
언제 쓰나 — 히어로 타이틀의 미묘한 액체 모션, 수중 테마, 열기 아지랑이.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="wave" x="-15%" y="-40%" width="130%" height="180%">
<feTurbulence type="turbulence" baseFrequency="0.008 0.04"
numOctaves="2" seed="2" result="n">
<animate attributeName="baseFrequency" dur="8s"
values="0.008 0.04;0.012 0.055;0.008 0.04"
repeatCount="indefinite"/>
</feTurbulence>
<feDisplacementMap in="SourceGraphic" in2="n" scale="7"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
</svg>
<h1 class="wave">LIQUID</h1>
.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만 애니메이션하면 노이즈는 캐시되고 변위만 바뀌어 훨씬 싸다.
<filter id="waveCheap" x="-15%" y="-40%" width="130%" height="180%">
<feTurbulence type="turbulence" baseFrequency="0.01 0.05"
numOctaves="2" seed="2" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="0"
xChannelSelector="R" yChannelSelector="G">
<animate attributeName="scale" dur="6s"
values="2;9;2" repeatCount="indefinite"/>
</feDisplacementMap>
</filter>
룩은 "흐르는" 대신 "숨쉬는" 쪽에 가깝지만 비용이 크게 낮다. 대부분의 경우 이쪽으로 충분하다.
성능: 하 (baseFrequency 애니메이션) / 중 (scale 애니메이션)
13. 듀오톤 / 그라디언트 맵
언제 쓰나 — 브랜드 컬러로 사진 톤을 통일. 잡다한 스톡 이미지들을 한 화면에 놓아도 일관돼 보이게 만드는 가장 강력한 기법.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="duotone" color-interpolation-filters="sRGB">
<!-- 1) Rec.709 휘도로 그레이스케일 -->
<feColorMatrix type="matrix" values="
0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0 0 0 1 0"/>
<!-- 2) 그림자 #0D1A59 → 하이라이트 #FA591A 로 리매핑 -->
<feComponentTransfer>
<feFuncR type="table" tableValues="0.05 0.98"/>
<feFuncG type="table" tableValues="0.10 0.35"/>
<feFuncB type="table" tableValues="0.35 0.10"/>
</feComponentTransfer>
</filter>
</defs>
</svg>
<img class="duo" src="photo.jpg" alt="설명">
.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"
// 헬퍼: hex 2개 → 듀오톤 필터 문자열
function duotoneFilter(id, shadowHex, highlightHex) {
const toRGB = h => {
const n = parseInt(h.replace('#', ''), 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
};
const s = toRGB(shadowHex);
const h = toRGB(highlightHex);
const f = i => `${(s[i] / 255).toFixed(4)} ${(h[i] / 255).toFixed(4)}`;
return `
<filter id="${id}" color-interpolation-filters="sRGB">
<feColorMatrix type="matrix" values="
0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0 0 0 1 0"/>
<feComponentTransfer>
<feFuncR type="table" tableValues="${f(0)}"/>
<feFuncG type="table" tableValues="${f(1)}"/>
<feFuncB type="table" tableValues="${f(2)}"/>
</feComponentTransfer>
</filter>`;
}
// duotoneFilter('brandDuo', '#12103A', '#FF6B35')
트라이톤 — tableValues에 값을 3개 넣는다 (그림자 / 중간톤 / 하이라이트).
<feComponentTransfer>
<feFuncR type="table" tableValues="0.05 0.55 0.98"/>
<feFuncG type="table" tableValues="0.10 0.20 0.35"/>
<feFuncB type="table" tableValues="0.35 0.45 0.10"/>
</feComponentTransfer>
함정
color-interpolation-filters="sRGB"가 없으면 색이 전부 어긋난다. 이 레시피에서는 필수.- 원본 대비가 낮은 사진은 듀오톤에서 뭉개진다. 앞에
feComponentTransfer type="linear" slope="1.3" intercept="-0.15"로 대비를 올려주면 좋다.
성능: 상 — 픽셀당 단순 산술 2회. 이미지에 적용해도 부담 없다.
14. 포스터화 / 컬러 밴딩
언제 쓰나 — 스크린프린트, 레트로 게임, 벡터 일러스트 느낌. 그라디언트를 계단으로 만든다.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="posterize" color-interpolation-filters="sRGB">
<feComponentTransfer>
<feFuncR type="discrete" tableValues="0 0.25 0.5 0.75 1"/>
<feFuncG type="discrete" tableValues="0 0.25 0.5 0.75 1"/>
<feFuncB type="discrete" tableValues="0 0.25 0.5 0.75 1"/>
</feComponentTransfer>
</filter>
</defs>
</svg>
.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 | 미묘한 밴딩 |
채널별로 다른 단계 수를 주면 색이 이동하며 인쇄 오차 느낌이 난다:
<feFuncR type="discrete" tableValues="0 0.5 1"/>
<feFuncG type="discrete" tableValues="0 0.33 0.66 1"/>
<feFuncB type="discrete" tableValues="0 1"/>
듀오톤과 결합 — 레시피 13의 그레이스케일 뒤에 discrete를 넣으면 2색 스크린프린트가 된다.
성능: 상
15. 디더 / 임계값 (하프톤 대체)
언제 쓰나 — 1비트 룩, 레트로 맥/게임보이, 리소 하프톤 근사. 그라디언트를 점 패턴으로 분해한다.
원리 — 그레이 노이즈를 소스에 가산한 뒤 2단계 discrete로 자른다. 노이즈가 임계선 주변을 흩뜨려 점 패턴이 생긴다.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="dither" x="0%" y="0%" width="100%" height="100%"
color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.75"
numOctaves="1" seed="9" result="n"/>
<!-- 노이즈를 불투명 그레이로 (알파 노이즈 제거) -->
<feColorMatrix in="n" type="matrix" result="gray"
values="0.33 0.33 0.33 0 0
0.33 0.33 0.33 0 0
0.33 0.33 0.33 0 0
0 0 0 0 1"/>
<!-- 소스 + (노이즈 × 0.5 − 0.25) -->
<feComposite in="SourceGraphic" in2="gray" operator="arithmetic"
k1="0" k2="1" k3="0.5" k4="-0.25" result="mixed"/>
<feComponentTransfer in="mixed">
<feFuncR type="discrete" tableValues="0 1"/>
<feFuncG type="discrete" tableValues="0 1"/>
<feFuncB type="discrete" tableValues="0 1"/>
</feComponentTransfer>
</filter>
</defs>
</svg>
.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) + 종이 그레인.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="risoGrain" x="0%" y="0%" width="100%" height="100%"
color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.7"
numOctaves="2" stitchTiles="stitch" seed="11" result="n"/>
<!-- RGB는 흰색 상수, 알파만 노이즈 → 흰 바탕에 랜덤 투명도 -->
<feColorMatrix in="n" type="matrix"
values="0 0 0 0 1
0 0 0 0 1
0 0 0 0 1
0.4 0.4 0.4 0 -0.1"/>
</filter>
</defs>
</svg>
<div class="riso">
<i class="ink-a"></i>
<i class="ink-b"></i>
</div>
.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와 달리 글자 두께를 깎지 않는다.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<!-- (A) 컬러 아웃라인 -->
<filter id="outline" x="-20%" y="-20%" width="140%" height="140%"
color-interpolation-filters="sRGB">
<feMorphology in="SourceAlpha" operator="dilate" radius="4" result="thick"/>
<feFlood flood-color="#22d3ee" result="col"/>
<feComposite in="col" in2="thick" operator="in" result="ring"/>
<feMerge>
<feMergeNode in="ring"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- (B) 속 빈 아웃라인 -->
<filter id="knockout" x="-25%" y="-25%" width="150%" height="150%"
color-interpolation-filters="sRGB">
<feMorphology in="SourceAlpha" operator="dilate" radius="3" result="thick"/>
<feComposite in="thick" in2="SourceAlpha" operator="out" result="ring"/>
<feFlood flood-color="#f472b6" result="col"/>
<feComposite in="col" in2="ring" operator="in"/>
</filter>
</defs>
</svg>
<h1 class="outlined">OUTLINE</h1>
<h1 class="hollow">HOLLOW</h1>
.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에 걸면 원본 색 그대로 굵어진다 |
이중 아웃라인 전체 코드
<filter id="doubleOutline" x="-30%" y="-30%" width="160%" height="160%"
color-interpolation-filters="sRGB">
<feMorphology in="SourceAlpha" operator="dilate" radius="8" result="outer"/>
<feFlood flood-color="#0f172a" result="outerCol"/>
<feComposite in="outerCol" in2="outer" operator="in" result="outerRing"/>
<feMorphology in="SourceAlpha" operator="dilate" radius="4" result="inner"/>
<feFlood flood-color="#fbbf24" result="innerCol"/>
<feComposite in="innerCol" in2="inner" operator="in" result="innerRing"/>
<feMerge>
<feMergeNode in="outerRing"/>
<feMergeNode in="innerRing"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
성능: 상 — 텍스트는 선택·검색·스크린리더 접근이 그대로 유지된다.
18. 스티커
언제 쓰나 — 배지, 이모지 스타일 라벨, 컷아웃 요소. 흰 테두리 + 아래로 떨어지는 그림자.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="sticker" x="-30%" y="-30%" width="160%" height="160%"
color-interpolation-filters="sRGB">
<feMorphology in="SourceAlpha" operator="dilate" radius="8" result="outer"/>
<feFlood flood-color="#ffffff" result="white"/>
<feComposite in="white" in2="outer" operator="in" result="whiteRing"/>
<feDropShadow in="whiteRing" dx="0" dy="4" stdDeviation="4"
flood-color="#000000" flood-opacity="0.35" result="shadowed"/>
<feMerge>
<feMergeNode in="shadowed"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
</svg>
<span class="sticker">STICKER</span>
.sticker {
font-size: 44px;
font-weight: 900;
color: #111;
filter: url(#sticker);
}
파라미터 가이드
| 조절 | 값 |
|---|---|
| 테두리 두께 | radius 4(얇음) ~ 14(두꺼움) |
| 떠 있는 높이 | dy 2stdDeviation 2 |
| 그림자 세기 | flood-opacity 0.2~0.5 |
| 테두리 색 | flood-color — 흰색 대신 배경보다 밝은 색도 좋다 |
이미지에도 적용 가능 — 투명 PNG 컷아웃에 그대로 걸면 스티커가 된다.
성능: 상
19. 네온 글로우
언제 쓰나 — 다크 UI의 강조, 사이버펑크, 라이브/온에어 표시.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="neon" x="-40%" y="-40%" width="180%" height="180%"
color-interpolation-filters="sRGB">
<feDropShadow dx="0" dy="0" stdDeviation="2"
flood-color="#22d3ee" flood-opacity="1" result="s1"/>
<feDropShadow in="s1" dx="0" dy="0" stdDeviation="6"
flood-color="#7c3aed" flood-opacity="0.9" result="s2"/>
<feDropShadow in="s2" dx="0" dy="0" stdDeviation="14"
flood-color="#ec4899" flood-opacity="0.7"/>
</filter>
</defs>
</svg>
<h1 class="neon">NEON</h1>
.neon {
font-size: 40px;
font-weight: 900;
color: #fff;
filter: url(#neon);
}
애니메이션 글로우 링 — flood-color는 CSS/SMIL 애니메이션이 가능하다.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="pulseRing" x="-20%" y="-20%" width="140%" height="140%"
color-interpolation-filters="sRGB">
<feMorphology in="SourceAlpha" operator="dilate" radius="3" result="thick"/>
<feComposite in="thick" in2="SourceAlpha" operator="out" result="ring"/>
<feFlood flood-color="#22d3ee" result="col">
<animate attributeName="flood-color"
values="#22d3ee;#a855f7;#f43f5e;#22d3ee"
dur="4s" repeatCount="indefinite"/>
</feFlood>
<feComposite in="col" in2="ring" operator="in" result="glowRing"/>
<feGaussianBlur in="glowRing" stdDeviation="4" result="glow"/>
<feMerge>
<feMergeNode in="glow"/>
<feMergeNode in="glowRing"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
</svg>
.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. 스포트라이트
언제 쓰나 — 히어로 포커스, 카드 호버 강조, 어두운 씬의 연출.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<!-- (A) 빛을 더하는 방식 -->
<filter id="spotAdd" x="-20%" y="-20%" width="140%" height="140%"
color-interpolation-filters="sRGB">
<feGaussianBlur in="SourceAlpha" stdDeviation="5" result="bump"/>
<feDiffuseLighting in="bump" surfaceScale="5" diffuseConstant="1"
lighting-color="#ffd88a" result="light">
<feSpotLight id="spotSource"
x="55" y="15" z="90"
pointsAtX="110" pointsAtY="80" pointsAtZ="0"
specularExponent="6" limitingConeAngle="42"/>
</feDiffuseLighting>
<feComposite in="light" in2="SourceAlpha" operator="in" result="lightClipped"/>
<feBlend in="SourceGraphic" in2="lightClipped" mode="screen"/>
</filter>
<!-- (B) 음영을 만드는 방식(비네트) -->
<filter id="spotShade" x="-20%" y="-20%" width="140%" height="140%"
color-interpolation-filters="sRGB">
<feGaussianBlur in="SourceAlpha" stdDeviation="5" result="bump"/>
<feDiffuseLighting in="bump" surfaceScale="5" diffuseConstant="1.6"
lighting-color="#ffffff" result="light">
<feSpotLight x="55" y="15" z="90"
pointsAtX="110" pointsAtY="80" pointsAtZ="0"
specularExponent="4" limitingConeAngle="55"/>
</feDiffuseLighting>
<feComposite in="light" in2="SourceAlpha" operator="in" result="lightClipped"/>
<feBlend in="SourceGraphic" in2="lightClipped" mode="multiply"/>
</filter>
</defs>
</svg>
<div class="spot-card">Focus</div>
.spot-card {
width: 220px;
height: 150px;
background: #334155;
border-radius: 14px;
filter: url(#spotAdd);
}
포인터 추적 스포트라이트
(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.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="glossy" x="-20%" y="-20%" width="140%" height="140%"
color-interpolation-filters="sRGB">
<feGaussianBlur in="SourceAlpha" stdDeviation="6" result="bump"/>
<feSpecularLighting in="bump" surfaceScale="6" specularConstant="1"
specularExponent="25" lighting-color="#ffffff" result="spec">
<fePointLight x="60" y="20" z="120"/>
</feSpecularLighting>
<feComposite in="spec" in2="SourceAlpha" operator="in" result="specClip"/>
<feComposite in="SourceGraphic" in2="specClip" operator="arithmetic"
k1="0" k2="1" k3="1" k4="0"/>
</filter>
</defs>
</svg>
<button class="glossy-btn">Press</button>
.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 — 돌/콘크리트 각인 질감)
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="embossTexture" x="0%" y="0%" width="100%" height="100%"
color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.06"
numOctaves="3" seed="4" result="n"/>
<!-- 노이즈를 불투명 그레이로 -->
<feColorMatrix in="n" type="matrix" result="g"
values="0.33 0.33 0.33 0 0
0.33 0.33 0.33 0 0
0.33 0.33 0.33 0 0
0 0 0 0 1"/>
<!-- 합이 0인 커널 + bias 0.5 = 중간 회색 기준 엠보스 -->
<feConvolveMatrix in="g" order="3" preserveAlpha="true" divisor="1" bias="0.5"
kernelMatrix="-2 -1 0
-1 0 1
0 1 2" result="emb"/>
<feComposite in="emb" in2="SourceAlpha" operator="in"/>
</filter>
</defs>
</svg>
.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을 넘어 전체가 흰색으로 날아간다(실제로 재현함).
샤픈 (변위 후 흐려진 엣지 복구용)
<feConvolveMatrix order="3" preserveAlpha="true"
kernelMatrix="0 -1 0 -1 5 -1 0 -1 0"/>
함정 — 필터를 건 요소 안의 텍스트도 조명 처리되어 뿌옇게 된다. 텍스트는 별도 레이어로 얹어라.
성능: 중
22. 글리치 밴드
언제 쓰나 — 로딩 실패 상태, 사이버펑크 전환 효과, 에러 화면. 순간적으로만 쓴다.
원리 — 대부분이 중립색(127,127)인 평면 위에, 얇은 가로 밴드만 다른 색으로 칠한 이미지를 변위맵으로 쓴다. 밴드가 지나가는 줄만 가로로 밀린다.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<filter id="glitch" x="-10%" y="-10%" width="120%" height="120%"
primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<!-- 전체를 중립으로 채움: 변위 0 -->
<feFlood flood-color="rgb(127,127,127)" x="0" y="0" width="100%" height="100%"
result="neutral"/>
<!-- 밴드 1 -->
<feFlood flood-color="rgb(200,127,127)" x="0" y="0" width="100%" height="14"
result="band1">
<animate attributeName="y" dur="2.7s"
values="0;40;12;90;30;120;0" repeatCount="indefinite"/>
</feFlood>
<!-- 밴드 2 -->
<feFlood flood-color="rgb(60,127,127)" x="0" y="0" width="100%" height="8"
result="band2">
<animate attributeName="y" dur="1.9s"
values="120;20;100;55;10;80;120" repeatCount="indefinite"/>
</feFlood>
<feMerge result="map">
<feMergeNode in="neutral"/>
<feMergeNode in="band1"/>
<feMergeNode in="band2"/>
</feMerge>
<feDisplacementMap in="SourceGraphic" in2="map" scale="40"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
</svg>
<div class="glitch-box">SYSTEM ERROR</div>
.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. 필터 조합 시 순서 원칙
- 소스 생성 (
feTurbulence,feFlood,feImage) — 재료를 먼저 만든다 - 기하 변형 (
feDisplacementMap,feMorphology,feOffset) — 형태를 바꾼다 - 블러 (
feGaussianBlur) — 부드럽게 한다 - 색/알파 조작 (
feColorMatrix,feComponentTransfer) — 대비·색을 잡는다 - 합성 (
feComposite,feBlend,feMerge) — 원본과 합친다
순서를 바꾸면 결과가 달라진다. 예: 블러 → 알파대비 = gooey(붙는다) / 알파대비 → 블러 = 그냥 흐린 도형.
부록 B. 어디에 텍스트를 두는가
필터를 거는 요소 안에 텍스트를 두면 텍스트도 필터를 통과한다. 대부분의 레시피에서 이건 버그다.
<!-- ❌ 나쁨: 텍스트가 뭉개진다 -->
<div class="gooey-card">텍스트</div>
<!-- ✅ 좋음: 표면 레이어와 콘텐츠 레이어 분리 -->
<div class="card">
<div class="card-surface"></div> <!-- filter: url(#...) -->
<div class="card-content">텍스트</div> <!-- 필터 없음 -->
</div>
.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. 네온