# svg-glass — 유리 재질: 헤어라인·굴절·머티리얼 위계 [svg-filters.md](svg-filters.md)에서 분리한 하위 문서다. 4-2에서 **재질을 유리로 정한 뒤에만** 연다. 재질을 고르는 표(§1), 공통 뼈대, 성능(§6), 접근성·폴백(§7), 채택 체크리스트(§8)는 svg-filters.md가 정본이고 여기서 다시 정의하지 않는다. 깊이·그림자 단계는 [elevation.md](elevation.md)를 따른다. ## 이 문서를 읽는 법 | 상황 | 읽을 곳 | |---|---| | 유리 패널 가장자리에 1px 광을 두른다 | §1 | | 테두리 광을 회전시킨다 | §1-1 | | 배경이 실제로 휘는 굴절 유리(리퀴드 글래스)를 만든다 | §2 — **코드를 짜기 전에 §2-3 함정부터** | | 굴절이 안 보인다 | §2-3 → §2-4 진단법 | | 유리 위 텍스트 가독성, 반투명 겹침, 스티키 헤더, 진입 전환, reduced transparency | §3 | | 감사 직전 | svg-filters.md §8 + 아래 §4 | ## 1. 유리 테두리(헤어라인)를 그릴 때 1px 테두리에 광을 흘리는 흔한 방법은 그라디언트를 깔고 마스크로 테두리만 남기는 것이다. ```css .glass::after { content: ""; position: absolute; inset: 0; padding: 1px; border-radius: inherit; background: var(--glass-rim); mask: linear-gradient(black 0 0) content-box, linear-gradient(black 0 0); mask-composite: exclude; /* -webkit-mask-composite: xor */ } ``` **`conic-gradient` 를 쓰지 마라.** 중심에서 각도로 돌기 때문에, 납작한 형태 (내비게이션 바처럼 1180×54)에서는 **각도가 급변해 변마다 광이 끊긴다.** 긴 변에서는 색이 거의 안 변하고 짧은 변에서 한꺼번에 바뀐다. ```css /* 네 변이 같은 그라디언트를 공유한다 — 테두리를 따라 이어진다 */ --glass-rim: linear-gradient(100deg, rgb(255 255 255 / 0.34) 0%, rgb(255 255 255 / 0.10) 26%, rgb(255 255 255 / 0.06) 58%, rgb(255 255 255 / 0.28) 100%); ``` 정사각형에 가까운 카드라면 conic 도 자연스럽다. **비율을 보고 골라라.** 글로우를 더한다면 테두리에서 얕게 번지는 정도로 충분하다 — `0 0 22px -8px` 처럼 **음수 spread** 를 써서 형태 밖으로 새지 않게 한다. 밝은 배경에서는 흰빛 대신 그림자로 뒤집어야 같은 인상이 난다. ### 1-1. 테두리 광을 회전시키려면 `conic-gradient(from var(--angle), …)` 를 `@property` 로 애니메이션하는 방법이 흔히 보이는데, **그건 매 프레임 배경을 다시 칠한다.** 컴포지터에서 못 돌고 메인 스레드가 페인트한다. 저비용 기본 선택에서 벗어나는 사례다. 목적과 성능 측정, 감소 모션 경로를 프로젝트 계약에 기록한다. **정사각형 원뿔 레이어를 마스크 안에서 회전시켜라.** 움직이는 것은 `rotate` 뿐이다. ```css .rim { /* 테두리만 남기는 마스크 */ position: absolute; inset: 0; border-radius: inherit; overflow: hidden; padding: 1px; mask: linear-gradient(black 0 0) content-box, linear-gradient(black 0 0); mask-composite: exclude; } .rim::before { /* 실제로 도는 것 */ content: ""; position: absolute; top: 50%; left: 50%; width: 200%; aspect-ratio: 1; /* 정사각형이라 회전해도 밝기가 균일하다 */ translate: -50% -50%; background: conic-gradient(from 0deg, transparent 0 58%, var(--glow) 74%, transparent 86%); animation: rim-spin 11s linear infinite; } @keyframes rim-spin { to { rotate: 360deg; } } ``` **두 가지를 지켜라.** - `aspect-ratio: 1` — 정사각형이어야 회전 중 밝기가 일정하다. 납작하면 변마다 달라진다 - `width: 200%` — 요소보다 크게 잡아야 납작한 형태에서도 모서리까지 닿는다 `prefers-reduced-motion` 에서는 애니메이션만 빼면 된다. 정지 상태의 광은 그대로 남는다. ## 2. 리퀴드 글래스 — `backdrop-filter: url()` 은 살아 있다 **정정부터.** 이 문서는 한때 "Chrome 은 `CSS.supports` 로 true 를 주면서 렌더에서 `backdrop-filter` 선언 전체를 버린다" 고 적고 있었다. **틀렸다.** Chrome 151 에서 실측하면 computed 값이 `none` 이 아니라 `url("#id")` 로 유지되고, 배경이 실제로 굴절된다. 우리가 겪은 실패는 다른 원인이었다(아래 §2-3). ### 2-1. 구조 ```html ``` ```css .glass { backdrop-filter: url(#lg); } ``` ### 2-2. 변위 지도를 어떻게 만드나 **노이즈가 아니다.** 유리는 무작위로 휘지 않는다 — 가장자리에서 **경계 법선 방향**으로 휜다. - `R` 채널 = X 변위, `G` 채널 = Y 변위, **128 이 중립**(변위 없음) - 값 인코딩: `r = 128 + x * 127`, `g = 128 + y * 127` - 가장자리에서 거리 `d` 를 구해 그 함수로 변위 크기를 정한다 - 감쇠 곡선은 **Squircle** `y = ⁴√(1 - (1-x)⁴)` 이 원보다 자연스럽다(Apple 이 쓰는 곡선) 캔버스로 즉석 생성해 `toDataURL()` 로 넘기면 이미지 파일이 필요 없다. 실측: 240×160 맵이 **2.7KB** data URI. ### 2-3. 함정 — 여기서 실패한다 1. **`feImage` 의 `width`/`height` 를 요소 크기와 맞춰라.** `backdrop-filter` 는 요소 크기에 맞춰 필터 이미지를 자동 조정하지 **않는다.** 크기가 어긋나면 굴절이 엉뚱한 곳에 걸리거나 아예 안 보인다. 요소가 리사이즈되면 맵을 다시 만들어야 한다 2. **Chrome 전용이다.** Safari·Firefox 는 `backdrop-filter` 에 SVG 필터를 받지 않는다. `@supports` 로 감싸고 `blur()` 폴백을 둬라 3. **모양·크기를 애니메이션하지 마라.** 변위 맵 전체가 다시 계산된다. `` 만 애니메이션해라 — 그건 재계산이 없다 4. `color-interpolation-filters="sRGB"` 를 빼면 색이 어둡게 뜬다 ```css .glass { backdrop-filter: blur(18px) saturate(150%); } /* 기본 */ @supports (backdrop-filter: url(#lg)) { .glass { backdrop-filter: url(#lg); } /* 굴절 */ } ``` > 표준화가 진행 중이다(w3c/svgwg#1142). 지금은 Chrome 한정 기법으로 다루고, > **굴절이 없어도 성립하는 화면**을 먼저 만든 뒤에 얹어라. ### 2-4. 실패 기록 — `feTurbulence` 를 변위 지도로 쓰면 안 된다 위에 "노이즈가 아니다" 라고 적어놓고, 나중에 이 문서를 안 보고 이렇게 썼다. ```xml ``` `backdrop-filter` 에 걸고 재보니 필터 전후 **평균 채널차가 1.66** 이었다. `scale` 을 42 에서 120 으로 세 배 올려도 2.96 — 사실상 아무 일도 안 일어난다. 그때 내린 결론은 "Chrome 이 `backdrop-filter` 체인에서 displacement 를 못 쓴다" 였고, **그것도 틀렸다.** 원인은 두 가지 다 이 문서에 이미 적혀 있었다. 1. **블러한 난류는 중립값으로 수렴한다.** 변위 지도에서 128 은 "움직이지 마라" 다. `fractalNoise` 의 평균이 그 근처인데 거기에 블러까지 걸면 지도 전체가 128 이 된다. `scale` 을 아무리 키워도 0 을 곱하는 것이라 변하지 않는다. 2. **`feImage` 의 크기를 요소에 맞추지 않았다.** 자동 조정되지 않는다. 교훈은 필터가 아니라 작업 방식이다 — **이미 기록해 둔 함정에 다시 빠지는 것은 문서가 없어서가 아니라 쓰기 전에 읽지 않아서다.** 4단계에서 필터를 쓰기로 했으면 레시피를 짜기 **전에** 이 문서의 §2-3 함정을 먼저 편다. > 진단법: 굴절이 안 보일 때 `scale` 부터 올리지 마라. 지도를 화면에 직접 그려 > **128 이 아닌 픽셀이 실제로 있는지** 먼저 봐라. 없으면 지도가 문제다. ### 2-5. 지도를 SVG 로 그릴 때 — R 과 G 를 한 장에 못 담는다 `linearGradient` 하나로는 R 과 G 를 각각 다르게 변화시킬 수 없다. 겹쳐 그리면 뒤엣것이 앞엣것을 덮고, 블렌드 모드로는 채널이 섞인다. **지도를 두 장 놓고 displacement 를 두 번 건다.** 쓰지 않는 축에는 128 로 고정된 `B` 채널을 물려 이동량을 0 으로 만든다. ```xml ``` 지도 한 장은 가장자리에서만 중립을 벗어나는 그라디언트다. data URI 안에서 `#` 은 URI 를 끊으므로 색은 `rgb()` 로 적는다. ```js `` + `` + `` + // 왼쪽 끝: +X 최대 `` + // 가운데: 중립 `` + `` + // 오른쪽 끝: -X 최대 `` + `` ``` **확인 방법**: 배경에 격자를 깔아라. 사진만 놓으면 무엇이 어떻게 밀렸는지 알 수 없다. 제대로 걸리면 **패널 가장자리에서 격자 셀이 눌린다** — 그것이 렌즈의 지문이다. 실측: 필터 전후 평균 채널차 1.66(난류 지도) → **9.29**(그린 지도), 최대 126. `backdrop-filter` 앞의 `blur()` 는 약하게 둬라. 세게 걸면 굴절이 뭉개져서 지도를 고쳐도 차이가 안 보인다. ## 3. 머티리얼 위계와 가독성 (vibrancy) 언제: 브리프나 골라 둔 방향이 유리·Apple풍 질감을 채택했을 때 연다. `swiss-minimal`처럼 평면 표면을 기본으로 하는 방향에서는 기본 비채택이다 — 리믹스 카드나 브리프에서 재질 축을 유리에 명시적으로 배정했을 때만 쓴다([svg-filters.md](svg-filters.md) §1). 아래 규칙은 "모든 nav·toolbar·sheet를 backdrop-filter로 만들어라"는 지시가 아니다 — 유리를 쓰기로 한 뒤의 마감 규칙이다[SKILL-APPLE-DESIGN]. **블러 위 텍스트 가독성(vibrancy)**: 반투명·블러 표면 위에 플랫 그레이 텍스트를 얹지 마라. 배경이 바뀌어도 읽히도록 대비를 올리고, 자간을 미세하게 늘리고, 굵기를 한 단계 올린다. 색은 솔리드 레이어에 두고 반투명 전경에 두지 않는다 — 반투명 위에서는 배경에 따라 채도가 흔들린다. **반투명 표면끼리 겹치지 않기**: 밝은 반투명 표면을 다른 반투명 표면 위에 쌓지 마라. 겹칠수록 대비가 무너져 가독성이 붕괴한다. 겹쳐야 하면 아래쪽을 솔리드에 가깝게 불투명도를 올려라. **무거운 재질은 구조, 가벼운 재질은 상호작용**: 더 어둡고 무거운 유리(강한 블러 + 깊은 그림자)는 사이드바 같은 구조적 영역을 분리하는 데 쓰고, 더 밝고 가벼운 유리는 버튼 같은 인터랙티브 요소로 주의를 끄는 데 쓴다. 큰 표면일수록 더 두껍게(강한 블러 + 깊은 그림자) 읽혀야 한다 — 작은 칩에 큰 표면급 블러를 걸지 않는다. 그림자 자체의 단계·값은 [elevation.md](elevation.md)를 따른다. 이 절은 그중 유리 재질에만 적용되는 위계 규칙이다. **스티키 헤더는 그래디언트 마스크로**: 스티키 헤더 아래에 1px 하드 보더를 긋지 마라. 플로팅 크롬이 콘텐츠와 실제로 겹치는 부분에만 작은 블러 또는 그래디언트 마스크를 페이드시켜, 경계가 아니라 스크롤 엣지 효과로 읽히게 한다. **materialize 전환**: 유리·블러 표면의 진입·퇴장은 `opacity` 하나만 바꾸지 마라. 블러 반경과 `scale`을 함께 애니메이션해 "머티리얼이 실제로 도착한다"는 인상을 만든다. 필터 애니메이션의 비용·감소 모션 경로는 [svg-filters.md](svg-filters.md) §6·§7을 그대로 따른다 — 이 절이 그 규칙을 면제하지 않는다. ```css .sheet { opacity: 0; transform: scale(0.96); backdrop-filter: blur(0px); transition: opacity var(--dur-quick) var(--ease-out), transform var(--dur-quick) var(--ease-out), backdrop-filter var(--dur-quick) var(--ease-out); /* 새 duration 리터럴을 만들지 않는다(tokens.md §4) */ } .sheet.is-open { opacity: 1; transform: scale(1); backdrop-filter: blur(18px) saturate(1.4); } ``` **`prefers-reduced-transparency` 대체**: 이 미디어 쿼리는 Chromium 계열(Chrome·Edge)에만 있고 Firefox·Safari는 지원하지 않는다[WEB-BASELINE] — 지원 브라우저에서만 통하는 점진적 향상으로 다룬다. 감지되면 반투명 표면의 배경 불투명도를 올리고 블러를 낮춘다. 미지원 브라우저에서도 [svg-filters.md](svg-filters.md) §7의 저사양 감지(`reduce-effects` 클래스)와 "필터를 전부 끈 상태에서 성립해야 한다"는 원칙이 안전망 역할을 한다. ```css @media (prefers-reduced-transparency: reduce) { .glass { backdrop-filter: blur(6px); background: rgba(255, 255, 255, 0.82); } } ``` 그라디언트를 재질의 일부로 쓸 때(예: 듀오톤·머티리얼 틴트)의 보간 공간 선택은 [color.md](color.md)를 참조한다. ## 4. 유리 체크리스트(svg-filters.md §8에 더한다 — 구현 중 자기 점검, 납품 판정은 preflight.md X3·A3) - 유리 위 본문 텍스트가 배경이 바뀌는 모든 스크롤 위치에서 대비 기준을 넘는다(§3 vibrancy) - 반투명 표면끼리 겹치지 않거나, 아래쪽 불투명도를 올렸다(§3) - 굴절(`backdrop-filter: url()`)은 `@supports` 안에 있고 `blur()` 폴백이 있다. 굴절 없이도 화면이 성립한다(§2-3) - `feImage` 변위 지도 크기가 요소 크기와 같고, 리사이즈 시 다시 만든다(§2-3) - 테두리 광을 돌린다면 `@property` 배경 재도색이 아니라 회전 레이어이며, 감소 모션에서 멈춘다(§1-1) - `prefers-reduced-transparency` 지원 브라우저에서 불투명도가 올라간다(§3)