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" && ( +
- {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.lead}
-{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 =