designpaca/research/svg/02-recipes.md
Yun Chan 8808c672dc 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)
2026-08-20 10:48:00 +09:00

61 KiB
Raw Blame History

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.720.15/0.85로 → 가장자리에 더 집중
유리 두께감 box-shadowinset 하이라이트 강도
배경 밝기/채도 backdrop-filterbrightness() 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
표면 결정감 ::afteropacity 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>

텍스트에 적용할 때scale4 이하로. 그 이상은 판독성이 급격히 떨어진다.

성능: 중 — 정적이면 안전. 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
그레인 강도 ::afteropacity 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 28, stdDeviation 28
그림자 세기 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);
  });
})();

검증됨: setAttributefeSpotLightx/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. 필터 조합 시 순서 원칙

  1. 소스 생성 (feTurbulence, feFlood, feImage) — 재료를 먼저 만든다
  2. 기하 변형 (feDisplacementMap, feMorphology, feOffset) — 형태를 바꾼다
  3. 블러 (feGaussianBlur) — 부드럽게 한다
  4. 색/알파 조작 (feColorMatrix, feComponentTransfer) — 대비·색을 잡는다
  5. 합성 (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. 네온