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 Permalink 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: feImagex/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: feTilein서브영역이 명시되어 있어야 동작한다. 앞 프리미티브에 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: edgeModeSafari에서만 지원된다(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: screenfeImage가 래스터화하는 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개면 n1개 구간이 만들어진다.

  • 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 inin2 위에 (기본)
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 (단, 필터 재계산 유발 가능)

핵심: feTurbulencebaseFrequency를 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. feDisplacementMapxChannelSelector/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인가 → 사각 아티팩트