From ae2980872b6c87dec87531099d1912dce606cbc0 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Thu, 20 Aug 2026 23:23:24 +0900 Subject: [PATCH] =?UTF-8?q?feat(site):=20=EC=9E=AC=EB=A3=8C=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=EC=9D=84=20=EC=8B=A4=EB=AC=B4=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?3=EC=A2=85=EC=9C=BC=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EA=B3=A0=20=ED=91=B8=ED=84=B0=EB=A5=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 푸터를 없애라는 지시가 반영돼 있지 않았다. .install-colophon 이 사실상 푸터였고, .install-grid 밖에 있어 padding-inline 을 못 받아 화면 왼쪽 끝에 붙어 있기까지 했다. 제거하고 저장소 링크는 헤더 nav 로 옮겼다. 재료 섹션은 "필터로 질감 만들기"(그레인/굴절/듀오톤)였고 화면에서 아무 말도 하지 않았다. 실무에서 손이 줄어드는 자리로 바꾼다. - 글자 모양대로 생기는 스크림 — 흰 글자 대비 1.70:1 → 4.79:1 (AA 통과) - 색각 이상 시뮬레이션 — 상태 배지 셋의 최소 색거리 82 → 2 - 구이(gooey) — 가로로 세어 덩어리 3개 → 1개 세 값 모두 화면을 캡처해 잰 것이다. 처음에는 데모보다 카피를 먼저 썼고, 그 결과 "2.1:1 → 12.4:1" 같은 재지 않은 숫자가 페이지에 실렸다. 전부 교체했다. backdrop-filter 굴절 데모는 뺐다. feTurbulence 를 변위 지도로 써서 지도가 중립값(128)으로 수렴했고, scale 을 120 까지 올려도 채널차 2.96 이었다. svg-filters.md 에 "변위 지도는 노이즈가 아니다" 라고 이미 적혀 있었는데 쓰기 전에 읽지 않았다. 섹션 끝의 note 도 삭제했다. color-interpolation-filters 를 명시했다는 문장은 방문자가 그것으로 할 수 있는 일이 없는 내부 자랑이었다. 스킬 내재화: - svg-filters.md 실무 레시피 3종(스크림/색각/구이) + 구이가 붙는 문턱 계산 + feTurbulence 변위 지도 실패 기록 - antipatterns.md 데모를 만들기 전에 카피부터 쓰지 마라 + Playwright animations:'disabled' 가 끝 프레임을 찍는 함정 --- apps/site/src/components/FilterArt.astro | 136 ++++++++++ apps/site/src/components/Install.astro | 24 +- apps/site/src/components/Materials.astro | 286 +++++++++------------- apps/site/src/i18n/content.ts | 123 +++++----- apps/site/src/layouts/Base.astro | 12 + packages/skill/references/antipatterns.md | 43 +++- packages/skill/references/svg-filters.md | 105 ++++++++ 7 files changed, 473 insertions(+), 256 deletions(-) create mode 100644 apps/site/src/components/FilterArt.astro diff --git a/apps/site/src/components/FilterArt.astro b/apps/site/src/components/FilterArt.astro new file mode 100644 index 0000000..6aa600d --- /dev/null +++ b/apps/site/src/components/FilterArt.astro @@ -0,0 +1,136 @@ +--- +import { Image } from "astro:assets"; +import paper from "../assets/bg/bg-paper.webp"; + +/** + * 필터 데모의 소재. 같은 소재를 두 번 그리고 한쪽에만 필터를 건다. + * 변수를 하나만 바꿔야 데모가 무언가를 증명한다 — 소재가 다르면 + * 보이는 차이가 필터 때문인지 소재 때문인지 알 수 없다. + */ +interface Props { + kind: "scrim" | "cvd" | "goo"; + on: boolean; +} +const { kind, on } = Astro.props; + + +--- + +{ + kind === "scrim" && ( +
+ + 산미 8.5 · 로스팅 2일차 +
+ ) +} + +{ + kind === "cvd" && ( +
+ 통과 + 주의 + 실패 +
+ ) +} + +{ + kind === "goo" && ( +
+ {/* 같은 간격으로 놓인 세 덩어리. 왼쪽은 셋으로 보이고 오른쪽은 하나로 보인다. + 움직임으로 증명하려다 실패했다 — 지나가는 프레임은 방문자도 놓치고 + 측정도 매번 다른 값을 준다. 정지된 두 장이 더 정확하게 말한다. */} +
+ +
+
+ ) +} + + diff --git a/apps/site/src/components/Install.astro b/apps/site/src/components/Install.astro index 9b19919..4ec0b1d 100644 --- a/apps/site/src/components/Install.astro +++ b/apps/site/src/components/Install.astro @@ -41,25 +41,10 @@ const { c, ui } = Astro.props; - - {/* 푸터를 없앴으므로 출처와 라이선스는 여기 한 줄로 남는다. - 링크 하나 없이 배포하면 사용자가 소스를 찾을 방법이 사라진다. */} -

- {c.colophon.repo} - {c.colophon.license} - {c.colophon.made} -

diff --git a/apps/site/src/components/Materials.astro b/apps/site/src/components/Materials.astro index fbf2cd2..4e732f0 100644 --- a/apps/site/src/components/Materials.astro +++ b/apps/site/src/components/Materials.astro @@ -1,5 +1,6 @@ --- import BgPhoto from "./BgPhoto.astro"; +import FilterArt from "./FilterArt.astro"; import type { Content } from "../i18n/content"; interface Props { c: Content["materials"]; } @@ -7,246 +8,179 @@ const { c } = Astro.props; --- {/* - 레이아웃 패밀리: 레이어드 캔버스. - 배경은 three.js 셰이더 플레인, 전경은 SVG 필터를 실제로 먹인 타일 3장. - 둘 다 설명이 아니라 지금 이 화면에서 돌고 있는 것이다. + 이 섹션은 한동안 "필터로 재질 만들기" 였다 — 추상 사각형에 그레인·굴절·듀오톤. + 화면에서 아무 말도 하지 않았다. 필터가 실제로 값을 하는 자리는 질감이 아니라 + **실무에서 반복되는 네 가지 수작업**이고, 넷 다 이 스킬의 프리플라이트 항목이다. + 왼쪽이 문제, 오른쪽이 같은 소재에 선언 하나만 얹은 것이다. */}
+

{c.eyebrow}

{c.h2}

{c.lead}

-
+
- {/* GPU 가 만드는 재질. WebGL 이 없으면 위 그라디언트가 그대로 결과물이다 */} + {/* GPU 가 만드는 배경. WebGL 이 없으면 위 그라디언트가 그대로 결과물이다 */} -
    +
      { - c.tiles.map((t, i) => ( -
    1. -
    2. +

      {d.name}

      +

      {d.problem}

      + +
      + {[false, true].map((on) => ( +
      + +
      + {on ? d.afterLabel : d.beforeLabel} +
      +
      + ))}
      -

      {t.name}

      -

      {t.use}

      -

      {t.how}

      -

      {t.cost}

      + +

      {d.how}

      +

      {d.verdict}

    3. )) } -
+
- -

{c.note}

-{/* 필터 정의. 화면에 그려지지 않고 참조만 된다 */} +{/* + 필터 정의. 화면에 그려지지 않고 참조만 된다. + + 전부 color-interpolation-filters 를 명시한다. 기본값은 linearRGB 인데, + 블렌드와 합성이 씻겨 보인다 — 대부분 sRGB 가 맞다. + 단 하나, 색각 시뮬레이션은 반대다. 색각 변환은 물리적인 빛의 혼합이라 + 선형 공간에서 계산해야 실제로 그 사람이 보는 색이 나온다. +*/} - - diff --git a/apps/site/src/i18n/content.ts b/apps/site/src/i18n/content.ts index 6749b2d..1603f77 100644 --- a/apps/site/src/i18n/content.ts +++ b/apps/site/src/i18n/content.ts @@ -60,8 +60,15 @@ export interface Content { eyebrow: string; h2: string; lead: string; - tiles: { id: "grain" | "glass" | "duotone"; name: string; use: string; how: string; cost: string }[]; - note: string; + demos: { + id: "scrim" | "cvd" | "goo"; + name: string; + problem: string; + beforeLabel: string; + afterLabel: string; + how: string; + verdict: string; + }[]; }; metrics: { eyebrow: string; h2: string; lead: string; items: Metric[]; caveat: string }; preflight: { @@ -91,9 +98,11 @@ export interface Content { command: string; targets: { name: string; path: string }[]; commands: { cmd: string; what: string }[]; - colophon: { made: string; repo: string; license: string }; }; - nav: { items: { label: string; href: string }[]; menu: string }; + /* 저장소 링크는 푸터가 아니라 헤더에 둔다. + 푸터를 없애면 소스로 가는 길이 사라지고, 개발 도구 소개 페이지에서 + 그 링크는 장식이 아니라 목적지다. */ + nav: { items: { label: string; href: string }[]; menu: string; repo: string; repoLabel: string }; ui: { copy: string; copied: string }; } @@ -220,35 +229,39 @@ export const ko: Content = { ], }, materials: { - eyebrow: "재료", - h2: "텍스처 200KB 를 300바이트가 대신한다", + eyebrow: "SVG 필터", + h2: "이미지 편집기를 열지 않고 고치는 세 가지", lead: - "필터는 장식이 아니라 재료다. 셋 다 실제로 쓰이는 자리에 걸어 뒀다 — 그레인은 매끈한 면에, 굴절은 유리 뒤 글자에, 듀오톤은 사진에. 배경은 지금 three.js 셰이더가 그리고 있다.", - tiles: [ + "질감을 만드는 필터 데모는 많다. 손이 실제로 줄어드는 자리는 따로 있다. 아래 셋은 왼쪽이 매번 손으로 맞추던 것, 오른쪽이 같은 소재에 선언 하나만 얹은 것이다. 아래 수치는 이 화면을 그대로 캡처해 잰 값이다.", + demos: [ { - id: "grain", - name: "그레인", - use: "매끈한 그라디언트가 싸구려로 보일 때", - how: "feTurbulence 가 만든 프랙탈 노이즈를 알파로 눌러 표면에 얹는다. 이미지 요청이 한 건도 늘지 않는다.", - cost: "300바이트 · 정지 상태", + id: "scrim", + name: "글자 모양대로 생기는 스크림", + problem: "사진 위 캡션이 대비 4.5:1 을 못 넘는다. 사진마다 밝기가 달라 반투명 박스를 손으로 맞춰야 한다.", + beforeLabel: "그냥 얹음", + afterLabel: "filter: url(#scrim)", + how: "feMorphology 로 글자 알파를 부풀리고 블러한 뒤, 그 모양대로만 feFlood 를 채운다. 박스가 아니라 글자를 따라가므로 사진을 가리지 않는다.", + verdict: "흰 글자 대비 1.70:1 → 4.79:1 · 사진을 바꿔도 다시 맞출 것이 없다", }, { - id: "glass", - name: "굴절", - use: "유리 뒤의 글자가 실제로 휘어야 할 때", - how: "난류를 변위 지도로 써서 픽셀을 민다. 필터 영역을 넓혀야 가장자리가 잘리지 않는다.", - cost: "중간 · 애니메이션 금지", + id: "cvd", + name: "색각 이상 시뮬레이션", + problem: "상태를 색으로만 구분하면 남성 12명 중 1명에게 같은 배지가 된다. 눈으로는 절대 못 잡는다.", + beforeLabel: "내가 보는 화면", + afterLabel: "적록색약(2형)", + how: "feColorMatrix 하나다. 이것만은 sRGB 로 두면 안 된다 — 색각 변환은 물리적 혼합이라 linearRGB 가 맞다. 필터 전체에서 유일한 예외다.", + verdict: "세 배지의 최소 색거리 82 → 2 · 주의와 실패가 같은 색이 된다", }, { - id: "duotone", - name: "듀오톤", - use: "출처가 제각각인 사진을 한 팔레트로 묶을 때", - how: "명도만 남긴 뒤 feComponentTransfer 로 두 색 사이에 다시 매핑한다. 원본을 보정하지 않는다.", - cost: "낮음 · 사진에 쓴다", + id: "goo", + name: "떨어진 것이 하나로 뭉치기", + problem: "조각이 서로 녹아 붙는 표현을 하려고 캔버스나 물리 라이브러리를 켠다. 로더, 메뉴 병합, 인디케이터 전환에서 반복된다.", + beforeLabel: "필터 없음 · 원 3개", + afterLabel: "filter: url(#goo)", + how: "크게 블러한 뒤 feColorMatrix 로 알파만 18배 세워 잘라낸다. 붙을지 말지는 눈대중이 아니라 산수다 — 두 원 사이 중간점의 블러 알파가 문턱(7÷18 ≒ 0.39)을 넘어야 한다. 간격 18px 에 블러 7 로는 0.2 근처라 못 넘었고, 12px 에 10 으로 넘겼다.", + verdict: "가로로 세어 덩어리 3개 → 1개 · JS 0줄", }, ], - note: - "이 셋은 SVG 필터가 지금 먹고 있는 화면이고, 그 뒤 배경은 three.js 셰이더가 그리고 있다. three 는 초기 번들에 없다 — 화면에 가까워질 때만 받고, WebGL 이 없거나 모션을 줄이는 설정이면 아예 받지 않는다. 그때 보이는 그라디언트는 폴백이 아니라 그대로 결과물이다." }, metrics: { eyebrow: "이 페이지의 수치", @@ -405,11 +418,6 @@ export const ko: Content = { { cmd: "npx designpaca doctor", what: "설치 상태와 드리프트 진단" }, { cmd: "npx designpaca uninstall", what: "매니페스트 기반 정확한 제거" }, ], - colophon: { - made: "designpaca 로 만들었다", - repo: "git.chanpaca.net/yunchan/designpaca", - license: "MIT", - }, }, nav: { items: [ @@ -419,6 +427,8 @@ export const ko: Content = { { label: "설치", href: "#install" }, ], menu: "메뉴", + repo: "https://git.chanpaca.net/yunchan/designpaca", + repoLabel: "저장소", }, ui: { copy: "복사", copied: "복사됨" }, }; @@ -474,35 +484,39 @@ export const en: Content = { ], }, materials: { - eyebrow: "Materials", - h2: "300 bytes replaces a 200KB texture", + eyebrow: "SVG filters", + h2: "Three things you fix without opening an image editor", lead: - "Filters are material, not decoration. Each one sits where it is actually used — grain on a flat gradient, refraction behind glass over type, duotone on a photograph. The backdrop is a three.js shader, running now.", - tiles: [ + "There is no shortage of filter demos that make texture. The places where filters actually save you work are elsewhere. In all three below, the left is what you used to hand-tune every time and the right is the same material with one declaration added. The numbers come from capturing this screen and measuring it.", + demos: [ { - id: "grain", - name: "Grain", - use: "when a smooth gradient reads as cheap", - how: "feTurbulence generates fractal noise, pushed down through alpha onto the surface. Not one extra image request.", - cost: "300 bytes · static", + id: "scrim", + name: "A scrim shaped like the letters", + problem: "A caption over a photo fails 4.5:1. Every photo has a different brightness, so the translucent box gets hand-tuned each time.", + beforeLabel: "just placed on top", + afterLabel: "filter: url(#scrim)", + how: "feMorphology fattens the glyph alpha, a blur softens it, and feFlood fills only that shape. It follows the letters instead of covering the photo with a box.", + verdict: "white-on-photo contrast 1.70:1 → 4.79:1 · swap the photo, nothing to re-tune", }, { - id: "glass", - name: "Refraction", - use: "when type behind glass has to actually bend", - how: "Turbulence drives a displacement map that pushes pixels. Widen the filter region or the edges clip.", - cost: "medium · never animate", + id: "cvd", + name: "Colour-vision simulation", + problem: "State encoded in colour alone becomes one badge for 1 in 12 men. No amount of looking at your own screen catches it.", + beforeLabel: "what you see", + afterLabel: "deuteranopia", + how: "A single feColorMatrix. This is the one filter that must not run in sRGB — colour-vision transforms are physical mixing, so linearRGB is correct here.", + verdict: "minimum colour distance across the three badges 82 → 2 · warning and failure become one colour", }, { - id: "duotone", - name: "Duotone", - use: "when photos from different sources need one palette", - how: "Keep luminance only, then remap between two colours with feComponentTransfer. The source file is untouched.", - cost: "low · use it on photos", + id: "goo", + name: "Separate shapes fusing into one", + problem: "Getting pieces to melt together usually means reaching for canvas or a physics library. It comes up in loaders, merging menus, indicator transitions.", + beforeLabel: "no filter · 3 circles", + afterLabel: "filter: url(#goo)", + how: "Blur hard, then stand the alpha up 18× with feColorMatrix. Whether they fuse is arithmetic, not taste — the blurred alpha midway between two circles has to clear the threshold (7÷18 ≒ 0.39). At 18px apart with a blur of 7 it sat near 0.2 and never merged; 12px and 10 clears it.", + verdict: "counted across the middle row: 3 blobs → 1 · 0 lines of JS", }, ], - note: - "These three are SVG filters running on live pixels, and the field behind them is a three.js shader. three is not in the initial bundle — it arrives only as the section approaches, and never at all when WebGL is missing or motion is reduced. What you see then is not a fallback; it is the result." }, metrics: { eyebrow: "This page, measured", @@ -663,11 +677,6 @@ export const en: Content = { { cmd: "npx designpaca doctor", what: "Diagnose install state and drift" }, { cmd: "npx designpaca uninstall", what: "Exact removal, driven by the manifest" }, ], - colophon: { - made: "Built with designpaca", - repo: "git.chanpaca.net/yunchan/designpaca", - license: "MIT", - }, }, nav: { items: [ @@ -677,6 +686,8 @@ export const en: Content = { { label: "Install", href: "#install" }, ], menu: "Menu", + repo: "https://git.chanpaca.net/yunchan/designpaca", + repoLabel: "Source", }, ui: { copy: "Copy", copied: "Copied" }, }; diff --git a/apps/site/src/layouts/Base.astro b/apps/site/src/layouts/Base.astro index 200a643..7cf670b 100644 --- a/apps/site/src/layouts/Base.astro +++ b/apps/site/src/layouts/Base.astro @@ -100,6 +100,14 @@ const FAVICON =
@@ -123,6 +131,7 @@ const FAVICON =
@@ -225,6 +234,9 @@ const FAVICON = background-color var(--dur-instant) var(--ease-out); } .nav-main a:hover { color: var(--ink); background: var(--glass-tint); } + /* 바깥으로 나가는 링크는 섹션 앵커와 같은 무게로 두지 않는다 */ + .nav-repo { gap: var(--space-1); opacity: 0.72; } + .nav-repo:hover { opacity: 1; } .header-end { display: flex; diff --git a/packages/skill/references/antipatterns.md b/packages/skill/references/antipatterns.md index f1f8000..3688380 100644 --- a/packages/skill/references/antipatterns.md +++ b/packages/skill/references/antipatterns.md @@ -318,7 +318,17 @@ const buf = await sharp(f).extract(box).png().toBuffer(); const s = await sharp(buf).stats(); // 잘라낸 버퍼를 다시 물려야 한다 ``` -**③ 눈으로 의심한 것이 실측에서 뒤집힐 수 있다.** +**③ Playwright 의 `animations: 'disabled'` 는 애니메이션을 끝 상태로 보낸다.** +움직이는 데모를 중간 프레임에서 재려고 `animation-play-state: paused` 로 세워 뒀는데, +스크린샷 옵션이 그걸 무시하고 100% 지점을 찍었다. 그 프레임에서는 두 도형이 이미 +겹쳐 있어서 "필터가 안 붙는다" 는 잘못된 결론이 나왔다. 멈춘 프레임을 그대로 +찍으려면 그 옵션을 **빼야** 한다. + +> 여기서 더 중요한 결론이 나왔다 — **움직임으로 증명하려 들지 마라.** +> 지나가는 프레임은 방문자도 놓치고 측정도 매번 다른 값을 준다. +> 정지된 before/after 두 장이 더 정확하게 말한다. + +**④ 눈으로 의심한 것이 실측에서 뒤집힐 수 있다.** WebGL 판이 본문 뒤를 지나가는 화면을 보고 "대비가 죽었다" 고 판단해 캔버스를 어둡게 만들려 했다. 스크린샷에서 글자 사이 빈 띠의 배경 휘도를 재보니 최악 지점이 **9.83:1** — AAA(7:1)를 넘었다. 고칠 필요가 없었다. @@ -329,6 +339,36 @@ WebGL 판이 본문 뒤를 지나가는 화면을 보고 "대비가 죽었다" --- +### 데모를 만들기 전에 카피부터 쓰지 마라 + +실측 사례. 필터 데모 넷을 만들면서 각 데모의 결론 문장을 먼저 썼다 — +"대비 2.1:1 → 12.4:1", "하나로 이어진다", "배경을 휜다". +나중에 재보니 **넷 중 셋이 거짓이었다.** + +| 화면에 쓴 말 | 실제 측정 | +|---|---| +| 대비 2.1:1 → 12.4:1 | 1.70:1 → **3.12:1** (AA 미달) | +| 하나로 이어진다 | 필터를 켠 쪽도 **덩어리 3개** | +| 배경을 휜다 | 평균 채널차 **1.66** (거의 변화 없음) | + +숫자를 지어낸 것이 아니라 **"이 정도 나오겠지" 를 적고 확인하지 않은 것**이다. +결과는 같다 — 페이지에 거짓이 실렸다. + +순서를 뒤집어라. + +1. 데모를 만든다 +2. **잰다** (before/after 를 각각 캡처해 픽셀로) +3. 잰 값으로 문장을 쓴다 +4. 값이 주장을 뒷받침하지 못하면 **문장이 아니라 데모를 고친다** + +4번을 지키면 카피가 저절로 구체적이 된다. 위 셋은 고친 뒤 이렇게 됐다 — +"흰 글자 대비 1.70:1 → 4.79:1", "가로로 세어 덩어리 3개 → 1개", +그리고 세 번째는 끝내 작동하지 않아 **섹션에서 뺐다.** + +> 작동하지 않는 데모를 남기고 문장만 다듬는 선택지는 없다. + +--- + ## 10. 자가 채점표 | 카테고리 | 걸린 항목 | 조치 | @@ -353,6 +393,7 @@ WebGL 판이 본문 뒤를 지나가는 화면을 보고 "대비가 죽었다" - [ ] 키보드만으로 전 인터랙션이 가능하고 포커스 링이 보인다 - [ ] (한국어) 한글 폰트가 명시되어 있고 `word-break: keep-all`이 있다 - [ ] 모든 섹션에서 제목→본문 간격이 **같은 값**이다 (0px 인 섹션이 하나도 없다) +- [ ] 화면에 적은 수치는 **전부 그 화면을 캡처해 잰 값**이다 (추정치·기대치가 섞여 있지 않다) **판정** diff --git a/packages/skill/references/svg-filters.md b/packages/skill/references/svg-filters.md index c8d402d..912f5f6 100644 --- a/packages/skill/references/svg-filters.md +++ b/packages/skill/references/svg-filters.md @@ -259,6 +259,84 @@ SVG 필터는 장식이 아니라 재질을 만드는 도구다. "예뻐 보이 **함정**: 큰 `stdDeviation`(50+)은 매우 비싸다. **넓은 소프트 글로우는 `radial-gradient` 배경이 압도적으로 싸다** **비용: 중** — dark-instrument에서는 강조 하나에만, 좁게 +## 3-b. 실무에서 손이 줄어드는 레시피 + +질감을 만드는 필터는 데모에서 예쁘고, 아래 셋은 **매번 손으로 하던 일을 없앤다.** +전부 실측으로 확인한 값이다. + +### 글자 모양대로 생기는 스크림 + +사진 위 캡션이 대비를 못 넘길 때, 반투명 박스를 사진마다 손으로 맞추는 대신. + +```xml + + + + + + + + + + + +``` + +실측: 밝은 사진 위 흰 글자 **1.70:1 → 4.79:1**(AA 통과). +`slope` 를 1.8 로 뒀을 때는 3.12:1 에서 멈췄다 — **블러 뒤에 알파를 다시 세우는 단계가 핵심이다.** +박스가 아니라 글자를 따라가므로 사진을 덜 가린다. + +### 색각 이상 시뮬레이션 — 유일하게 `linearRGB` 인 필터 + +상태를 색으로만 구분했는지 **눈으로는 못 잡는다.** 검사 도구로 쓴다. + +```xml + + + + +``` + +실측: 흔한 초록·노랑·빨강 상태 배지 셋의 **최소 색거리 82 → 2**. +주의와 실패가 같은 색이 된다. 이 값이 한 자리로 떨어지면 색 말고 다른 단서가 필요하다. + +### 구이(gooey) — 붙을지 말지는 눈대중이 아니라 산수다 + +커뮤니티에서 가장 많이 만들어지는 필터다(로더·메뉴 병합·인디케이터 전환). + +```xml + + + + + +``` + +**붙는 조건**: 두 도형 사이 중간점의 블러된 알파가 `offset ÷ slope`(여기서는 7÷18 ≒ 0.39)를 넘어야 한다. + +실측 — 지름 46px 원 셋: + +| 간격 | stdDeviation | 중간점 알파 | 결과 | +|---|---|---|---| +| 18px | 7 | ≈ 0.2 | **안 붙는다** (덩어리 3개 그대로) | +| 12px | 10 | 문턱 통과 | 덩어리 **1개** | + +값이 안 붙으면 `slope` 부터 만지지 마라 — 올리면 도형이 깎이고 각져 보인다. +**간격과 블러 반경의 비율**이 먼저다. +마지막 `feComposite atop` 이 없으면 블러된 헤일로가 남는다. + +--- + ## 4. CSS로 되는 것은 CSS로 값싼 쪽을 먼저 쓴다. SVG 필터는 CSS로 표현 **불가능한** 것에만. @@ -541,3 +619,30 @@ Chrome 151 에서 실측하면 computed 값이 `none` 이 아니라 `url("#id")` > 표준화가 진행 중이다(w3c/svgwg#1142). 지금은 Chrome 한정 기법으로 다루고, > **굴절이 없어도 성립하는 화면**을 먼저 만든 뒤에 얹어라. + +### 실패 기록 — `feTurbulence` 를 변위 지도로 쓰면 안 된다 + +위에 "노이즈가 아니다" 라고 적어놓고, 나중에 이 문서를 안 보고 이렇게 썼다. + +```xml + + + +``` + +`backdrop-filter` 에 걸고 재보니 필터 전후 **평균 채널차가 1.66** 이었다. +`scale` 을 42 에서 120 으로 세 배 올려도 2.96 — 사실상 아무 일도 안 일어난다. +그때 내린 결론은 "Chrome 이 `backdrop-filter` 체인에서 displacement 를 못 쓴다" 였고, +**그것도 틀렸다.** 원인은 두 가지 다 이 문서에 이미 적혀 있었다. + +1. **블러한 난류는 중립값으로 수렴한다.** 변위 지도에서 128 은 "움직이지 마라" 다. + `fractalNoise` 의 평균이 그 근처인데 거기에 블러까지 걸면 지도 전체가 128 이 된다. + `scale` 을 아무리 키워도 0 을 곱하는 것이라 변하지 않는다. +2. **`feImage` 의 크기를 요소에 맞추지 않았다.** 자동 조정되지 않는다. + +교훈은 필터가 아니라 작업 방식이다 — **이미 기록해 둔 함정에 다시 빠지는 것은 +문서가 없어서가 아니라 쓰기 전에 읽지 않아서다.** 4단계에서 필터를 쓰기로 했으면 +레시피를 짜기 **전에** 이 문서의 §함정을 먼저 편다. + +> 진단법: 굴절이 안 보일 때 `scale` 부터 올리지 마라. 지도를 화면에 직접 그려 +> **128 이 아닌 픽셀이 실제로 있는지** 먼저 봐라. 없으면 지도가 문제다.