designpaca/research/svg/01-primitives-reference.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

38 KiB
Raw Blame History

01. SVG 필터 프리미티브 전수 레퍼런스

designpaca 스킬용 실전 레퍼런스. 모든 기본값·값 범위는 W3C SVG 1.1 / Filter Effects Module Level 1 스펙과 MDN 기준. "검증됨" 표시는 Chromium 151(Playwright)에서 실제 렌더링으로 확인한 항목이다. 그 외 엔진(Firefox / WebKit)은 별도 표기.


0. 실행 모델부터 이해하기

SVG 필터는 이미지 처리 파이프라인이다. CSS filter: blur(4px)처럼 "효과 하나"가 아니라, 노드 그래프를 직접 배선하는 도구다.

SourceGraphic ─┬─→ [feGaussianBlur] → result="blur" ─┐
               │                                      ├─→ [feBlend] → 최종 출력
               └──────────────────────────────────────┘

0.1 배선 규칙 (in / in2 / result)

규칙 내용
in 생략 시 첫 번째 프리미티브면 SourceGraphic, 그 외에는 직전 프리미티브의 출력
in2 입력 2개가 필요한 프리미티브(feBlend, feComposite, feDisplacementMap, feTile은 예외적으로 in만)에서 두 번째 입력
result 이 프리미티브의 출력에 이름을 붙여 뒤에서 재사용. 이름을 안 붙이면 직전 것만 참조 가능
출력 개수 프리미티브당 항상 1개

함정 1: result를 붙여 놓고도 다음 프리미티브에 in을 안 쓰면, 어차피 직전 출력이 들어가서 "왜 배선이 무시되지?"가 된다. 분기(branch)를 만들 때는 양쪽 모두 in을 명시하라.

0.2 소스 키워드

키워드 의미 실전 사용 가능?
SourceGraphic 필터가 적용된 원본 그래픽 (RGBA 전체) ✅
SourceAlpha 원본의 알파 채널만 (RGB는 전부 0=검정) ✅ 그림자·아웃라인·범프맵의 기본 재료
BackgroundImage 요소 뒤 배경 ❌ 어떤 브라우저도 구현 안 함. backdrop-filter로 대체
BackgroundAlpha 배경의 알파 ❌ 미구현
FillPaint 대상의 fill 페인트로 무한 평면 채움 ❌ 사실상 신뢰 불가 (엔진별 해석 불일치)
StrokePaint 대상의 stroke 페인트로 무한 평면 채움 ❌ 사실상 신뢰 불가

함정 2: 튜토리얼에서 BackgroundImage를 봤다면 2010년대 초 문서다. 유리/글래스 효과는 backdrop-filter: url(#id)로 간다(§03 참고).

0.3 필터 영역 (filter region) — 가장 흔한 버그의 원인

<!-- 기본값 -->
<filter id="f" x="-10%" y="-10%" width="120%" height="120%"
        filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse">

필터 영역 밖은 잘려 나간다. 기본값은 바운딩 박스보다 사방 10%씩만 넓으므로, stdDeviation이 크거나 feOffset/feMorphology로 밀어내면 잘린 사각형이 그대로 보인다.

경험칙

상황 권장 영역
블러 stdDeviation ≤ 5 기본값(120%)으로 충분
블러 6~15, 글로우, 아웃라인 x="-30%" y="-30%" width="160%" height="160%"
다중 드롭섀도(네온), 큰 변위 x="-40%" y="-40%" width="180%" height="180%"
노이즈/그레인 오버레이처럼 영역을 넓히면 안 되는 경우 x="0%" y="0%" width="100%" height="100%" (성능도 이득)

필터 영역을 넓히면 처리 픽셀 수가 제곱으로 증가한다. 180% × 180% = 원본의 3.24배. 필요한 만큼만 넓혀라. (검증됨 — §02 레시피 18번 비교 데모)

0.4 좌표계: filterUnits vs primitiveUnits

속성 기본값 영향 대상
filterUnits objectBoundingBox <filter>의 x/y/width/height
primitiveUnits userSpaceOnUse 각 프리미티브의 x/y/width/height 그리고 stdDeviation, radius, dx/dy, scale 같은 길이값
  • objectBoundingBox → 0~1 또는 % 로 해석. 0.05 = 바운딩 박스 크기의 5%.
  • userSpaceOnUse → 현재 사용자 좌표계의 절대 길이(대개 px).
<!-- 요소 크기에 비례하는 블러 (반응형 컴포넌트에 유용) -->
<filter id="relBlur" primitiveUnits="objectBoundingBox">
  <feGaussianBlur stdDeviation="0.05"/>  <!-- 박스 폭의 5% -->
</filter>

검증됨: 90×90px 박스에 primitiveUnits="objectBoundingBox" + stdDeviation="0.05" → 약 4.5px 블러.

함정 3: primitiveUnits="objectBoundingBox"로 바꾸면 stdDeviation="10"이 "박스 10배 크기 블러"가 되어 화면이 통째로 사라진다. 단위를 바꾸면 모든 수치를 다시 계산해야 한다.

함정 4 (HTML 요소): filter: url(#f)를 HTML 요소에 걸면 바운딩 박스는 border box 기준이다. objectBoundingBox를 쓰는 필터를 SVG용으로 만들어놓고 HTML에 재활용하면 크기가 달라진다.

0.5 색공간: color-interpolation-filters

SVG 필터의 기본 연산 색공간은 linearRGB다. CSS의 filter: blur() 같은 단축 함수는 sRGB다. 이 불일치가 "같은 값인데 왜 다르게 보이지?"의 정체다.

검증된 실측 (Chromium, #e11d48 사각형에 feColorMatrix type="saturate" values="0"):

설정 결과
기본(linearRGB) 밝은 회색 (≈ #808080)
color-interpolation-filters="sRGB" 어두운 회색 (≈ #4a4a4a, 계산값 R=G=B=74)

같은 조건의 feGaussianBlur stdDeviation="10":

  • linearRGB → 헤일로가 밝고 넓게 번짐(물리적으로 정확한 빛의 합성)
  • sRGB → 헤일로가 좁고 진함(포토샵/CSS와 동일한 룩)

실전 지침

<!-- 디자인 툴/CSS와 색을 맞추고 싶으면 항상 filter에 sRGB를 명시 -->
<filter id="f" color-interpolation-filters="sRGB"> ... </filter>
케이스 권장
듀오톤, 그라디언트 맵, 브랜드 컬러 정확도 반드시 sRGB
알파 대비 트릭(gooey), 합성 위주 sRGB (예측 가능)
조명(feDiffuseLighting/feSpecularLighting)에서 물리적 자연스러움 linearRGB 기본이 더 그럴듯할 때도 있음 — 둘 다 보고 고른다
feTurbulence 노이즈 자체 어느 쪽이든 무방, 단 후속 대비 조정 결과가 달라짐

함정 5: color-interpolation-filters는 개별 프리미티브에도 걸 수 있다. 체인 중간에서 색공간이 바뀌면 브라우저가 변환을 삽입한다 → 비용 발생 + 예측 어려움. <filter> 한 곳에만 지정하는 것이 안전하다.

0.6 필터 붙이는 법

<!-- 1. SVG 요소에 (프레젠테이션 어트리뷰트) -->
<circle filter="url(#f)" .../>

<!-- 2. HTML 요소에 (CSS) -->
<div style="filter: url(#f)"></div>

<!-- 3. 배경에 (Chromium 계열만) -->
<div style="backdrop-filter: url(#f)"></div>

필터 정의를 담는 SVG 컨테이너 패턴:

<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <defs>
    <filter id="myFilter"> ... </filter>
  </defs>
</svg>

검증됨: Chromium에서는 <svg style="display:none"> 안에 정의한 필터도 정상 동작했다. 하지만 과거 엔진 이슈 보고가 있고 Firefox/WebKit은 이 세션에서 미검증이므로, width=0 height=0 + position:absolute 패턴을 기본값으로 쓴다.

검증됨: 존재하지 않는 필터를 참조하면(filter:url(#없음)) Chromium은 필터를 무시하고 원본을 그대로 렌더한다(CSS·SVG 어트리뷰트 양쪽 모두). 현행 Filter Effects L1과 일치. 다만 구 SVG 1.1은 "요소를 렌더하지 않음"으로 정의했었으므로, 레거시 엔진에서 요소가 사라질 수 있다 — ID 오타는 치명적일 수 있다.


1. 소스 생성 프리미티브

입력 없이 새 이미지를 만들어내는 것들. 필터 그래프의 "재료".


1.1 <feTurbulence> — 절차적 노이즈

가장 강력하고, 가장 비싼 프리미티브. Perlin 노이즈를 생성한다.

속성 기본값 범위/타입 의미
type turbulence turbulence | fractalNoise turbulence=결·물결·대리석, fractalNoise=구름·그레인
baseFrequency 0 0 이상 실수, x y 2개 가능 노이즈의 촘촘함. 작을수록 큰 무늬
numOctaves 1 양의 정수 겹칠 주파수 층 수. 디테일↑ 비용↑
seed 0 정수(소수는 0 방향으로 절삭) 난수 시드
stitchTiles noStitch noStitch | stitch 타일 경계 이음매 제거

baseFrequency 실전 값 지도

값 결과 용도
0.002 ~ 0.01 거대한 유기적 덩어리 액체 배경, 오로라, 마블
0.01 ~ 0.03 큰 파형 손그림 왜곡, 물결 텍스트
0.03 ~ 0.08 중간 결 종이 질감, 잉크 번짐, 러프 엣지
0.1 ~ 0.3 촘촘한 결 천·직물, 얕은 디스토션
0.6 ~ 0.95 픽셀 단위 그레인 필름 그레인 / 노이즈 오버레이
> 1.0 에일리어싱 발생, 무의미 ❌

2값 형식: baseFrequency="0.008 0.05" → x축은 큰 파형, y축은 촘촘 → 가로로 늘어진 결. 물결·글리치 스캔라인에 필수.

numOctaves 실전: 1=밋밋, 23=대부분의 경우 최적, 45=거친 종이, 5 초과는 시각적 이득이 거의 없고 비용만 늘어난다.

함정

  1. baseFrequency의 기본값은 0 = 아무것도 안 나온다. 반드시 지정하라.
  2. 알파 채널도 노이즈다. feTurbulence 출력은 RGB뿐 아니라 A도 랜덤이라 그냥 보면 반투명 컬러 스노우다. 불투명 그레이 노이즈가 필요하면 feColorMatrix로 알파를 1로 고정하라:
    <feColorMatrix type="matrix" values="
      0.33 0.33 0.33 0 0
      0.33 0.33 0.33 0 0
      0.33 0.33 0.33 0 0
      0    0    0    0 1"/>   <!-- 마지막 행: A = 1 상수 -->
    
  3. 특정 seed 값에서 사각형 아티팩트가 나온다. 보고된 값: 514, 1977, 2337, 4777, 8032, 9615 등. 스펙 참조 구현의 정수 나눗셈 버그라 Chrome·Firefox·Batik이 동일하게 재현한다. → seed는 1~50 같은 작은 값에서 눈으로 확인하고 고정하라.
  4. CPU 비용이 가장 크다. baseFrequency를 애니메이션하면 매 프레임 전체 노이즈를 재생성한다. §03 참고.
  5. stitchTiles="stitch"는 타일링해서 배경으로 쓸 때만 의미가 있다. feTile이나 CSS background-repeat와 조합할 때 켜라.

1.2 <feFlood> — 단색 채우기

속성 기본값 의미
flood-color black 채울 색. CSS 프로퍼티이기도 하다 → CSS/SMIL로 애니메이션 가능
flood-opacity 1 0~1

필터 서브영역 전체를 색으로 채운다. 단독으로는 쓸모없고 feComposite operator="in"으로 마스킹해서 쓴다.

<!-- 알파 모양대로 특정 색 칠하기 = 실루엣 -->
<feFlood flood-color="#22d3ee" result="col"/>
<feComposite in="col" in2="SourceAlpha" operator="in"/>

함정 6: feFlood는 서브영역 전체를 채우므로, 서브영역이 필터 영역(기본 120%)이면 색이 사방으로 넘친다. operator="in" 마스킹을 반드시 뒤에 붙이거나 x/y/width/height로 서브영역을 제한하라.

활용: flood-color가 CSS 프로퍼티라는 점이 핵심이다. baseFrequency·stdDeviation 같은 어트리뷰트는 CSS로 애니메이션할 수 없지만, flood-color, flood-opacity, lighting-color는 CSS 트랜지션/애니메이션이 가능하다. (검증됨: SMIL <animate attributeName="flood-color"> 정상 동작)


1.3 <feImage> — 외부 이미지 끌어오기

속성 기본값 의미
href — 이미지 URL. xlink:href는 레거시
preserveAspectRatio xMidYMid meet 서브영역에 맞추는 방식. 변위맵으로 쓸 땐 none 권장
crossorigin — CORS

Liquid Glass 같은 정밀한 굴절에서 미리 계산된 변위맵을 넣는 통로.

참조 방식별 지원 현황

방식 Chromium WebKit Firefox
외부 파일 href="map.png" ✅ 부분적 ✅
외부 SVG 파일 href="map.svg" ✅ (래스터화) ❌ 보고됨 ✅
같은 문서 fragment href="#id" ❌ ✅ ❌
data URI href="data:image/svg+xml;..." ✅ ✅ ✅

함정 7: fragment 참조는 Safari 전용, 외부 SVG는 Safari 미지원 — 크로스브라우저로는 data URI가 유일한 안전 경로다. (검증됨: data URI로 SVG 변위맵 생성 → Chromium 정상)

함정 8: feImage는 x/y/width/height를 명시하지 않으면 필터 영역에 맞춰진다. backdrop-filter에서 쓸 때 요소 크기와 맵 크기가 어긋나면 굴절이 엉뚱한 위치에 생긴다. 픽셀 값으로 정확히 지정하라.

함정 9: data URI 안의 SVG는 완전히 URL 인코딩해야 한다. 최소한 # → %23, < → %3C, > → %3E, " → '. #을 인코딩 안 하면 fragment로 잘려서 조용히 실패한다.


1.4 <feTile> — 서브영역 타일링

in의 필터 서브영역을 하나의 타일로 삼아, 자기 서브영역을 가득 채울 때까지 x/y로 반복한다.

<filter id="tiled" primitiveUnits="userSpaceOnUse"
        x="0%" y="0%" width="100%" height="100%">
  <!-- 40×40 영역만 노이즈 생성 = 타일 원본 -->
  <feTurbulence type="turbulence" baseFrequency="0.12" numOctaves="2" seed="5"
                x="0" y="0" width="40" height="40" result="tile"/>
  <feTile in="tile" x="0" y="0" width="220" height="140"/>
</filter>

검증됨 (Chromium). 작은 타일만 계산하고 복사하므로 큰 면적 노이즈보다 훨씬 싸다 — 성능 최적화 기법으로도 유용.

함정 10: feTile은 in에 서브영역이 명시되어 있어야 동작한다. 앞 프리미티브에 x/y/width/height가 없으면 타일 크기가 필터 영역 전체가 되어 반복이 일어나지 않는다.


2. 기하/변형 프리미티브


2.1 <feGaussianBlur>

속성 기본값 의미
stdDeviation 0 가우시안 표준편차. x y 2값 가능
edgeMode none duplicate | wrap | none
  • stdDeviation="0" = 효과 없음(기본값이므로 반드시 지정).
  • 2값 = 방향성 블러: stdDeviation="15 0" → 가로 모션 블러. 세로 결/속도감 표현에 필수.
  • 음수는 에러(필터 전체 무시).
  • CSS blur(6px) ≈ feGaussianBlur stdDeviation="6" (단, CSS는 sRGB).

함정 11: edgeMode는 Safari에서만 지원된다(Chrome·Firefox 미지원). edgeMode="duplicate"로 가장자리 하드컷을 만드는 트릭은 크로스브라우저 불가 — feComposite operator="in"으로 원본 알파에 다시 클리핑하는 방식으로 대체하라.

함정 12: 큰 블러(≈100px 이상)는 엔진이 상한을 걸거나 극단적으로 느려진다. 큰 소프트 글로우는 블러 대신 radialGradient로 흉내내는 것이 압도적으로 싸다.


2.2 <feOffset>

속성 기본값 의미
dx, dy 0 이동량 (primitiveUnits 기준)

가장 싼 프리미티브. 그림자·색분리(RGB split)의 기본 부품.

함정 13: feOffset으로 민 결과는 필터 영역 밖으로 나가면 잘린다. dx="20"이면 영역도 그만큼 넓혀라.


2.3 <feDropShadow> — 단축 그림자

속성 기본값
dx 2
dy 2
stdDeviation 2
flood-color black
flood-opacity 1

내부적으로 feGaussianBlur + feOffset + feFlood + feComposite + feMerge와 동등. 훨씬 짧고 최적화도 잘 된다.

체이닝으로 다중 글로우 (검증됨 — 네온 텍스트):

<filter id="neon" x="-40%" y="-40%" width="180%" height="180%"
        color-interpolation-filters="sRGB">
  <feDropShadow dx="0" dy="0" stdDeviation="2"  flood-color="#22d3ee" flood-opacity="1"   result="s1"/>
  <feDropShadow in="s1" dx="0" dy="0" stdDeviation="6"  flood-color="#7c3aed" flood-opacity="0.9" result="s2"/>
  <feDropShadow in="s2" dx="0" dy="0" stdDeviation="14" flood-color="#ec4899" flood-opacity="0.7"/>
</filter>

feDropShadow는 결과에 원본을 포함하므로 feMerge가 필요 없다. 체인으로 연결하면 그림자가 누적된다.


2.4 <feMorphology> — 팽창/침식

속성 기본값 의미
operator erode erode(축소/가늘게) | dilate(팽창/굵게)
radius 0 반경. x y 2값 가능. 음수는 에러

핵심 용도: 텍스트 아웃라인. stroke는 선이 글자 안쪽으로 반 들어가 글자를 얇게 만들지만, feMorphology dilate는 바깥으로만 키운다.

<!-- 컬러 아웃라인 (검증됨) -->
<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>
<!-- 속 빈(knockout) 아웃라인 (검증됨) -->
<filter id="knockout" x="-25%" y="-25%" width="150%" height="150%"
        color-interpolation-filters="sRGB">
  <feMorphology in="SourceAlpha" operator="dilate" radius="3" result="thick"/>
  <feComposite in="thick" in2="SourceAlpha" operator="out" result="ring"/>
  <feFlood flood-color="#f472b6" result="col"/>
  <feComposite in="col" in2="ring" operator="in"/>
</filter>

함정 14: feMorphology의 커널은 사각형이다. radius가 커지면 둥근 모서리가 각지게 뭉개진다. radius 8 이상은 결과를 반드시 눈으로 확인하라. 부드러운 확장이 필요하면 feGaussianBlur + feComponentTransfer(알파 대비)로 대체.

함정 15: radius 기본값이 0이라 지정 안 하면 아무 일도 안 일어난다.


2.5 <feDisplacementMap> — 변위 (유기적 디자인의 심장)

속성 기본값 의미
in — 왜곡될 이미지
in2 — 변위맵
scale 0 최대 이동량(px). 음수 가능(방향 반전)
xChannelSelector A R|G|B|A
yChannelSelector A R|G|B|A

공식

P'(x,y) ← P( x + scale × (XC(x,y) − 0.5),
             y + scale × (YC(x,y) − 0.5) )
  • 채널 값 128(=0.5) → 변위 0 (중립)
  • 0 → −scale/2px 이동, 255 → +scale/2px 이동
  • 즉 scale은 총 이동 폭이고 한쪽 방향 최대치는 scale/2

함정 16 (가장 자주 틀림): xChannelSelector/yChannelSelector의 **기본값은 A(알파)**다. 지정하지 않으면 알파 채널로 왜곡한다. feTurbulence를 맵으로 쓸 때 알파도 노이즈라 "되긴 되는데 왜 이렇게 나오지?"가 된다. 항상 R/G를 명시하라. (검증됨: 기본값과 R/G 명시의 결과가 눈에 띄게 다름)

함정 17: 변위 후에는 안티에일리어싱이 깨진다. 가장자리가 계단처럼 보이면 feGaussianBlur stdDeviation="0.5"를 뒤에 살짝 넣거나, 텍스트라면 scale을 6 이하로 낮춰라.

함정 18: 변위는 필터 영역 안에서만 일어난다. 영역 밖 픽셀은 존재하지 않으므로, 큰 scale은 가장자리를 투명하게 빨아들인다. scale의 절반 이상만큼 영역을 넓혀라.

두 가지 맵 만드는 법

<!-- (a) 절차적: feTurbulence — 유기적, 손그림, 물결 -->
<feTurbulence type="fractalNoise" baseFrequency="0.03" numOctaves="4" seed="12" result="n"/>
<feDisplacementMap in="SourceGraphic" in2="n" scale="9"
                   xChannelSelector="R" yChannelSelector="G"/>
<!-- (b) 결정론적: feImage + 그라디언트 — 렌즈, 굴절, 유리 -->
<!-- R채널 = x변위, G채널 = y변위. 중립은 128. -->
<feImage preserveAspectRatio="none" x="0" y="0" width="220" height="88" result="map"
  href="data:image/svg+xml;charset=utf-8,%3Csvg .../%3E"/>
<feDisplacementMap in="SourceGraphic" in2="map" scale="-60"
                   xChannelSelector="R" yChannelSelector="G"/>

(b)의 맵 SVG 원문 (인코딩 전) — 가장자리에만 변위가 몰리는 렌즈 프로파일:

<svg xmlns="http://www.w3.org/2000/svg" width="220" height="88">
  <defs>
    <linearGradient id="rx" x1="0" y1="0" x2="1" y2="0">
      <stop offset="0"    stop-color="rgb(0,0,0)"/>
      <stop offset="0.28" stop-color="rgb(128,0,0)"/>
      <stop offset="0.72" stop-color="rgb(128,0,0)"/>
      <stop offset="1"    stop-color="rgb(255,0,0)"/>
    </linearGradient>
    <linearGradient id="gy" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0"    stop-color="rgb(0,0,0)"/>
      <stop offset="0.28" stop-color="rgb(0,128,0)"/>
      <stop offset="0.72" stop-color="rgb(0,128,0)"/>
      <stop offset="1"    stop-color="rgb(0,255,0)"/>
    </linearGradient>
  </defs>
  <rect width="220" height="88" rx="44" fill="rgb(128,128,128)"/>
  <rect width="220" height="88" rx="44" fill="url(#rx)" style="mix-blend-mode:screen"/>
  <rect width="220" height="88" rx="44" fill="url(#gy)" style="mix-blend-mode:screen"/>
</svg>

검증됨: mix-blend-mode: screen이 feImage가 래스터화하는 SVG 내부에서 정상 동작한다. R채널만 있는 레이어와 G채널만 있는 레이어를 screen으로 합치면 채널이 서로 간섭 없이 합쳐진다(= 채널별 가산). 중앙 28~72% 구간을 128로 평평하게 두는 것이 "가장자리에서만 굴절"의 핵심.


2.6 <feConvolveMatrix> — 컨볼루션 커널

속성 기본값 의미
order 3 커널 크기 (3 = 3×3)
kernelMatrix — order²개의 값 (필수)
divisor 커널 합(0이면 1) 결과를 나눌 값
bias 0 더할 상수
targetX/targetY floor(order/2) 커널 중심 위치
edgeMode duplicate duplicate|wrap|none
preserveAlpha false true면 알파를 건드리지 않음
<!-- 샤픈: 변위 후 흐릿해진 엣지 복구 -->
<feConvolveMatrix order="3" preserveAlpha="true"
                  kernelMatrix="0 -1 0  -1 5 -1  0 -1 0"/>

<!-- 엠보스 -->
<feConvolveMatrix order="3" preserveAlpha="true" bias="0.5"
                  kernelMatrix="-2 -1 0  -1 1 1  0 1 2"/>

<!-- 엣지 검출(윤곽선 추출) -->
<feConvolveMatrix order="3" preserveAlpha="true"
                  kernelMatrix="0 1 0  1 -4 1  0 1 0"/>

함정 19: preserveAlpha="false"(기본값)면 알파에도 커널이 적용되어 형태가 무너진다. 색만 만지고 싶으면 true로. 함정 20: 큰 order(5×5 이상)는 픽셀당 연산이 제곱으로 늘어 매우 비싸다. 실시간 애니메이션에는 쓰지 마라.


3. 색 조작 프리미티브


3.1 <feColorMatrix>

속성 기본값 의미
type matrix matrix|saturate|hueRotate|luminanceToAlpha
values type별 항등 matrix=20개 값, saturate=0~1(초과도 허용), hueRotate=각도

5×4 행렬 레이아웃 (열: R G B A 상수 / 행: R' G' B' A')

R' = r1·R + r2·G + r3·B + r4·A + r5
G' = g1·R + g2·G + g3·B + g4·A + g5
B' = b1·R + b2·G + b3·B + b4·A + b5
A' = a1·R + a2·G + a3·B + a4·A + a5

모든 값은 0~1 정규화 기준이고 결과는 0~1로 clamp된다. 5번째 열은 상수 오프셋(1이 곱해짐).

항등 행렬 (변화 없음, 커스텀의 출발점):

1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 1 0

자주 쓰는 행렬 사전

<!-- 그레이스케일 (Rec.709 휘도) -->
values="0.2126 0.7152 0.0722 0 0
        0.2126 0.7152 0.0722 0 0
        0.2126 0.7152 0.0722 0 0
        0      0      0      1 0"

<!-- 노이즈 → 불투명 그레이 (알파 노이즈 제거) -->
values="0.33 0.33 0.33 0 0
        0.33 0.33 0.33 0 0
        0.33 0.33 0.33 0 0
        0    0    0    0 1"

<!-- 단색 실루엣 (#22d3ee = 34,211,238 → /255) -->
values="0 0 0 0 0.133
        0 0 0 0 0.827
        0 0 0 0 0.933
        0 0 0 1 0"

<!-- 알파 대비 극대화 = gooey/threshold 핵심 -->
values="1 0 0 0  0
        0 1 0 0  0
        0 0 1 0  0
        0 0 0 18 -7"

<!-- 채널 분리 (R만) -->
values="1 0 0 0 0
        0 0 0 0 0
        0 0 0 0 0
        0 0 0 1 0"

<!-- 색 반전 -->
values="-1  0  0 0 1
         0 -1  0 0 1
         0  0 -1 0 1
         0  0  0 1 0"

알파 대비 트릭의 수학 — values="... 0 0 0 M -O" 이면 A' = M·A − O:

  • 임계점: A = O / M
  • 전이 폭: 1 / M
  • 예) M=18, O=7 → 임계 0.389, 폭 0.056 → A가 0.389~0.444 사이에서 0→1로 급전환.
  • M을 키우면 더 날카롭게, O/M을 키우면 형태가 더 수축한다.

type 단축형

type values 결과
saturate 0 흑백
saturate 0.5 채도 절반
saturate 3 과채도 (1 초과 허용)
hueRotate 180 색상환 180° 회전
luminanceToAlpha (없음) RGB→0, A=휘도 → 밝기를 마스크로 변환

함정 21: luminanceToAlpha는 결과의 RGB가 전부 0(검정)이다. 마스크로만 쓰고, 색이 필요하면 feFlood+feComposite in으로 다시 칠하라.

함정 22: 행렬 값은 0~1 스케일이다. #ff8000을 넣겠다고 255 128 0을 쓰면 전부 clamp되어 흰색이 된다. 반드시 255로 나눠라.

함정 23: type="saturate"의 결과는 색공간에 따라 극적으로 다르다(§0.5). 브랜드 흑백 톤을 맞추려면 sRGB를 명시하라.


3.2 <feComponentTransfer> + <feFuncR/G/B/A>

채널별 전달 함수를 적용한다. 톤 커브 도구라고 생각하면 된다.

type 파라미터 공식
identity — C' = C
table tableValues="v0 v1 ... vn" n개 값을 균등 구간으로 선형 보간
discrete tableValues="v0 v1 ... vn" 균등 구간별 계단 함수(보간 없음)
linear slope(1), intercept(0) C' = slope·C + intercept
gamma amplitude(1), exponent(1), offset(0) C' = amplitude·C^exponent + offset

table의 동작: 값이 n개면 n−1개 구간이 만들어진다.

  • tableValues="0 1" → 항등(0→0, 1→1)
  • tableValues="0.05 0.98" → 그림자를 0.05로, 하이라이트를 0.98로 리매핑 → 듀오톤의 원리
  • tableValues="0 0.5 1" → 3점 톤 커브

discrete의 동작: tableValues="0 0.25 0.5 0.75 1" → 5단계 포스터화. (검증됨)

듀오톤 = 그레이스케일 + 채널별 2점 table (검증됨)

<filter id="duotone" color-interpolation-filters="sRGB">
  <feColorMatrix type="matrix" values="
    0.2126 0.7152 0.0722 0 0
    0.2126 0.7152 0.0722 0 0
    0.2126 0.7152 0.0722 0 0
    0      0      0      1 0"/>
  <feComponentTransfer>
    <!-- 그림자색 #0D1A59, 하이라이트색 #FA591A -->
    <feFuncR type="table" tableValues="0.05 0.98"/>
    <feFuncG type="table" tableValues="0.10 0.35"/>
    <feFuncB type="table" tableValues="0.35 0.10"/>
  </feComponentTransfer>
</filter>

hex → tableValues 변환 공식

shadowColor #RRGGBB, highlightColor #RRGGBB
feFuncR tableValues = "R_shadow/255  R_highlight/255"
feFuncG tableValues = "G_shadow/255  G_highlight/255"
feFuncB tableValues = "B_shadow/255  B_highlight/255"

3색(트라이톤)이면 값 3개, 4색이면 4개를 넣으면 된다.

함정 24: color-interpolation-filters="sRGB"를 안 넣으면 듀오톤 색이 전부 어긋난다. 이 프리미티브에서는 필수라고 봐도 된다.

함정 25: 같은 채널의 feFunc*를 여러 개 쓰면 마지막 것만 적용된다.

함정 26: 계산은 비프리멀티플라이드(non-premultiplied) 값으로 이뤄진다. 반투명 영역에서 색이 튀면 이 때문이다.


4. 합성 프리미티브


4.1 <feBlend>

속성 기본값
mode normal

지원 모드 (CSS 블렌드 모드와 동일): normal, multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, hue, saturation, color, luminosity

in이 위, in2가 아래 레이어다. 포토샵 레이어 순서와 반대로 헷갈리기 쉽다.


4.2 <feComposite>

속성 기본값
operator over
k1, k2, k3, k4 0

Porter-Duff 연산자

operator 결과
over in을 in2 위에 (기본)
in in2의 알파로 in을 자른다 ← 마스킹의 핵심
out in2가 없는 곳의 in만 남긴다 ← 속 빈 아웃라인
atop in2 영역 안에서만 in을 얹는다
xor 겹치지 않는 부분만
lighter 가산 합성
arithmetic 아래 공식

arithmetic 공식

result = k1·i1·i2 + k2·i1 + k3·i2 + k4
목적 k1 k2 k3 k4
단순 덧셈 (조명 합성) 0 1 1 0
곱셈 (multiply) 1 0 0 0
in만 통과 0 1 0 0
50:50 블렌드 0 0.5 0.5 0
노이즈 가산 (±0.25) 0 1 0.5 −0.25

함정 27: arithmetic은 프리멀티플라이드 알파 값에 대해 계산된다. 반투명 입력에서 예상 밖 결과가 나오면 feFlood+in으로 불투명화 후 처리하라. Firefox에는 arithmetic 관련 시각 아티팩트 버그 리포트도 있다.

함정 28: 조명 프리미티브 결과를 원본과 합칠 때는 관례적으로 operator="arithmetic" k1=0 k2=1 k3=1 k4=0(가산)을 쓴다. 그냥 over로 얹으면 조명이 원본을 덮어버린다.


4.3 <feMerge> / <feMergeNode>

여러 레이어를 순서대로 over 합성한다. 뒤에 오는 feMergeNode가 위에 쌓인다.

<feMerge>
  <feMergeNode in="glow"/>        <!-- 맨 아래 -->
  <feMergeNode in="ring"/>
  <feMergeNode in="SourceGraphic"/> <!-- 맨 위 -->
</feMerge>

feComposite operator="over"를 여러 번 체인하는 것과 같지만 훨씬 읽기 쉽다.


5. 조명 프리미티브

알파 채널을 높이맵(범프맵) 으로 해석해 3D 조명을 계산한다. feTurbulence와 조합하면 종이·가죽·금속 질감이 나온다.

5.1 <feDiffuseLighting> (난반사 — 무광)

속성 기본값 의미
surfaceScale 1 알파=1일 때의 표면 높이. 클수록 굴곡 심함
diffuseConstant 1 반사 계수(kd). 0 이상
lighting-color white 광원 색. CSS 프로퍼티 → 애니메이션 가능
kernelUnitLength (없음) 노멀 계산 샘플 간격. 대개 생략

결과는 불투명한 RGBA다(알파=1). 그래서 반드시 원본 알파로 다시 잘라야 한다.

5.2 <feSpecularLighting> (정반사 — 광택)

속성 기본값 범위
surfaceScale 1 —
specularConstant 1 ks, 0 이상
specularExponent 1 1 ~ 128. 클수록 하이라이트가 작고 날카로움
lighting-color white —

결과는 정반사 성분만 담긴 RGBA로, 알파가 0이 아닌 영역이 하이라이트다. 원본에 가산해야 한다.

5.3 광원 요소 (정확히 하나를 자식으로)

<feDistantLight azimuth="45" elevation="60"/>
속성 기본값 의미
azimuth 0 xy평면 방향각 0~360°
elevation 0 고도각 0~90°. 낮을수록 그림자가 길고 대비 강함
<fePointLight x="60" y="20" z="120"/>
속성 기본값
x, y, z 0
<feSpotLight x="55" y="15" z="90"
             pointsAtX="110" pointsAtY="80" pointsAtZ="0"
             specularExponent="6" limitingConeAngle="42"/>
속성 기본값 의미
x/y/z 0 광원 위치
pointsAtX/Y/Z 0 조준점
specularExponent 1 중심부 집중도
limitingConeAngle (없음=제한 없음) 원뿔 반각(도). 넘으면 빛 없음

5.4 검증된 조합 패턴

<!-- (A) 종이 질감: 노이즈를 높이맵으로 -->
<filter id="paper" x="0%" y="0%" width="100%" height="100%">
  <feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="5" seed="3" result="noise"/>
  <feDiffuseLighting in="noise" lighting-color="#e8e0d0" surfaceScale="2" result="lit">
    <feDistantLight azimuth="45" elevation="60"/>
  </feDiffuseLighting>
  <feComposite in="lit" in2="SourceAlpha" operator="in"/>
</filter>
<!-- (B) 광택 버튼: 알파 블러를 범프맵으로 -->
<filter id="specular" x="-20%" y="-20%" width="140%" height="140%"
        color-interpolation-filters="sRGB">
  <feGaussianBlur in="SourceAlpha" stdDeviation="6" result="bump"/>
  <feSpecularLighting in="bump" surfaceScale="6" specularConstant="1"
                      specularExponent="25" lighting-color="#ffffff" result="spec">
    <fePointLight x="60" y="20" z="120"/>
  </feSpecularLighting>
  <feComposite in="spec" in2="SourceAlpha" operator="in" result="specClip"/>
  <feComposite in="SourceGraphic" in2="specClip" operator="arithmetic"
               k1="0" k2="1" k3="1" k4="0"/>
</filter>
<!-- (C) 스포트라이트: 조명을 screen으로 얹기 -->
<filter id="spot" x="-20%" y="-20%" width="140%" height="140%"
        color-interpolation-filters="sRGB">
  <feGaussianBlur in="SourceAlpha" stdDeviation="5" result="bump"/>
  <feDiffuseLighting in="bump" surfaceScale="5" diffuseConstant="1"
                     lighting-color="#ffd88a" result="light">
    <feSpotLight x="55" y="15" z="90" pointsAtX="110" pointsAtY="80" pointsAtZ="0"
                 specularExponent="6" limitingConeAngle="42"/>
  </feDiffuseLighting>
  <feComposite in="light" in2="SourceAlpha" operator="in" result="lightClipped"/>
  <feBlend in="SourceGraphic" in2="lightClipped" mode="screen"/>
</filter>

검증됨: (A)(B)(C) 모두 Chromium에서 의도대로 렌더. (C)에서 mode="multiply"로 바꾸면 음영/비네트 룩이 된다.

함정 29: 조명 결과를 그냥 출력하면 필터 영역 전체가 불투명 사각형으로 칠해진다. feComposite operator="in" + SourceAlpha 클리핑을 잊지 마라.

함정 30: surfaceScale이 크면 (2050) 스투코/구겨진 플라스틱 같은 과장된 질감이 된다. 종이 느낌은 13이 적당.

함정 31: kernelUnitLength는 엔진마다 해석이 달라 결과가 갈린다. 크로스브라우저가 중요하면 쓰지 마라.


6. CSS 단축 필터 ↔ 프리미티브 대응표

CSS 함수 등가 프리미티브 비고
blur(<len>) feGaussianBlur stdDeviation="<len>" CSS는 sRGB
brightness(a) feComponentTransfer + type="linear" slope="a"
contrast(a) feComponentTransfer + type="linear" slope="a" intercept="-(0.5a)+0.5"
grayscale(a) feColorMatrix type="matrix" (휘도 행렬 보간)
sepia(a) feColorMatrix type="matrix"
saturate(a) feColorMatrix type="saturate" values="a"
hue-rotate(deg) feColorMatrix type="hueRotate" values="deg"
invert(a) feComponentTransfer type="table" tableValues="a 1-a"
opacity(a) feComponentTransfer feFuncA type="table" tableValues="0 a"
drop-shadow(...) feDropShadow

결정적 차이: CSS 단축 함수는 스펙상 sRGB에서 동작하고, SVG <filter>는 linearRGB가 기본이다. 같은 결과를 원하면 color-interpolation-filters="sRGB"를 명시하라.

언제 CSS 단축을 쓰나: 단순 blur/그림자/채도만 필요하면 CSS 단축이 더 빠르고 더 잘 가속되며 Safari 호환성이 좋다. SVG 필터는 CSS로 표현 불가능한 것(노이즈, 변위, 채널 분리, 알파 대비, 조명)에만 써라.


7. 애니메이션 가능성 매트릭스

대상 SMIL <animate> CSS 애니메이션 JS (setAttribute)
baseFrequency, stdDeviation, scale, radius, dx/dy, k1~k4, values ✅ ❌ (CSS 프로퍼티가 아님) ✅
flood-color, flood-opacity, lighting-color ✅ ✅ CSS 프로퍼티다 ✅
filter 속성 자체 (url(#a) → url(#b)) — ❌ 보간 불가, 즉시 전환만 —
CSS 단축 필터 (blur(2px) → blur(8px)) — ✅ 보간됨 —
필터 걸린 요소의 transform/opacity — ✅ (단, 필터 재계산 유발 가능) —

핵심: feTurbulence의 baseFrequency를 CSS로 애니메이션하려는 시도는 작동하지 않는다. SMIL이나 JS를 써야 한다. 대신 flood-color/lighting-color는 CSS 트랜지션으로 부드럽게 다룰 수 있어, 호버 인터랙션에 매우 유용하다.

<!-- SMIL: baseFrequency 애니메이션 (검증됨) -->
<feTurbulence type="turbulence" baseFrequency="0.008 0.04" numOctaves="2" seed="2" result="n">
  <animate attributeName="baseFrequency" dur="8s"
           values="0.008 0.04;0.012 0.055;0.008 0.04" repeatCount="indefinite"/>
</feTurbulence>
<!-- SMIL: flood-color 애니메이션 (검증됨) -->
<feFlood flood-color="#22d3ee" result="col">
  <animate attributeName="flood-color"
           values="#22d3ee;#a855f7;#f43f5e;#22d3ee" dur="4s" repeatCount="indefinite"/>
</feFlood>
// JS: seed 순환으로 squigglevision (SMIL보다 제어가 쉽다)
const t = document.querySelector('#squiggle feTurbulence');
let i = 0;
const seeds = [1, 2, 3, 4];
setInterval(() => { t.setAttribute('seed', seeds[i++ % seeds.length]); }, 120);

8. 작성 전 체크리스트

작성한 필터가 이상하면 위에서부터 확인하라.

  1. id 오타 / url(#id)의 # 누락
  2. baseFrequency, stdDeviation, radius, scale — 기본값이 0이라 지정 안 하면 무효과
  3. feDisplacementMap의 xChannelSelector/yChannelSelector — 기본값 A
  4. 필터 영역이 좁아 잘리는가 → x/y/width/height 확대
  5. 색이 이상한가 → color-interpolation-filters="sRGB"
  6. feFlood/조명 결과가 사각형으로 넘치는가 → feComposite operator="in" 누락
  7. 분기했는데 무시되는가 → in을 양쪽 다 명시했는가
  8. feColorMatrix 값을 0255로 썼는가 → 01로
  9. 텍스트가 뭉개지는가 → 필터 컨테이너 안에 텍스트를 넣지 마라(§03)
  10. feTurbulence seed가 514/1977/2337/4777/8032/9615인가 → 사각 아티팩트