소개 페이지 전면 재디자인 — Ethereal Glass

design-taste-frontend, high-end-visual-design, redesign-existing-projects 를
로드해 리디자인 모드로 진행했다. 이전 페이지는 디자인 스킬을 파는 자리가
문서처럼 생겼다는 지적을 받았다.

Design Read
- redesign-overhaul / 개발자 대상 / 정밀기기 언어
- DESIGN_VARIANCE 8 · MOTION_INTENSITY 6 · VISUAL_DENSITY 3

방향
- 다크 잉크 바탕에 three.js 셰이더 광원. 유리 패널이 그 위에 뜬다
- 리퀴드 글래스는 이 브리프에서 장식이 아니라 제품 시연이다.
  SVG 필터와 three.js 의 결합 그 자체이기 때문이다
- 밝은 밴드는 조판 섹션 하나뿐. 한글이 종이 위에 있어야 하는 자리다

제품 증명
- 히어로 배경에 셰이더(초기 번들 밖, 첫 페인트 후 지연 로드)
- 재료 섹션에 SVG 필터 3종을 실제로 먹인 타일: 그레인, 굴절, 듀오톤
- 도그푸딩 산출물 2개를 실제 스크린샷으로. 가짜 목업이 아니다

감사에서 걸려 고친 것
- backdrop-filter 에 SVG 를 얹으면 Chrome 이 선언 전체를 버린다.
  CSS.supports 는 참을 주면서 computed 는 none 이 된다
- main 에서 그리드 클래스가 빠져 subgrid 가 붕괴, 가로 444px 오버플로
- 진입 연출이 두 번 콘텐츠를 삼켰다. opacity 를 버리고 transform 만 남겼다
- eyebrow 4개, 제목 em-dash 2건

수치는 불리해진 것도 그대로 싣는다
- 반투명 패널 1개 -> 29개. 유리가 주제가 됐으므로
- three.js 185KB(gzip). 데모라서 JS 예산을 올렸고 그 사실을 페이지에 적었다
This commit is contained in:
Yun Chan 2026-08-20 11:45:38 +09:00
parent 6e05e69ca4
commit be343cdab5
22 changed files with 1695 additions and 439 deletions

View file

@ -0,0 +1,158 @@
---
import type { Content } from "../i18n/content";
interface Props { c: Content["materials"]; }
const { c } = Astro.props;
---
{/*
레이아웃 패밀리: 레이어드 캔버스.
배경은 three.js 셰이더 플레인, 전경은 SVG 필터를 실제로 먹인 타일 3장.
둘 다 설명이 아니라 지금 이 화면에서 돌고 있는 것이다.
*/}
<section class="section materials" aria-labelledby="mat-h">
<h2 id="mat-h" class="reveal">{c.h2}</h2>
<p class="lead mat-lead reveal">{c.lead}</p>
<div class="full stage glass">
<div class="stage-fallback" aria-hidden="true"></div>
<ul class="tiles">
{
c.tiles.map((t, i) => (
<li class="tile reveal" style={`--reveal-delay:${i * 80}ms`}>
<div class="shell">
<div class="core">
<div class={`swatch swatch-${t.id}`} aria-hidden="true"></div>
</div>
</div>
<h3 class="tile-name">{t.name}</h3>
<p class="tile-how muted">{t.how}</p>
<p class="tile-cost mono">{t.cost}</p>
</li>
))
}
</ul>
</div>
<p class="mat-note muted reveal">{c.note}</p>
</section>
{/* 필터 정의. 화면에 그려지지 않고 참조만 된다 */}
<svg class="visually-hidden" aria-hidden="true" focusable="false">
<defs>
<!-- 종이: 미세한 프랙탈 노이즈를 표면에 얹는다 -->
<filter id="f-grain" x="0" y="0" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" result="n" />
<feColorMatrix in="n" type="saturate" values="0" result="g" />
<feComponentTransfer in="g" result="soft">
<feFuncA type="linear" slope="0.42" />
</feComponentTransfer>
<feComposite in="SourceGraphic" in2="soft" operator="over" />
</filter>
<!-- 유리: 난류로 만든 변위 지도를 굴절에 쓴다. 필터 영역을 넓혀 가장자리 잘림을 막는다 -->
<filter id="f-glass" x="-12%" y="-12%" width="124%" height="124%"
color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.012 0.02" numOctaves="2" seed="7" result="warp" />
<feDisplacementMap in="SourceGraphic" in2="warp" scale="26"
xChannelSelector="R" yChannelSelector="G" result="bent" />
<feGaussianBlur in="bent" stdDeviation="0.4" />
</filter>
<!-- 인쇄: 명도를 두 색 사이로 다시 매핑한다 -->
<filter id="f-duotone" x="0" y="0" width="100%" height="100%"
color-interpolation-filters="sRGB">
<feColorMatrix type="matrix" result="mono"
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 1 0" />
<feComponentTransfer in="mono">
<feFuncR type="table" tableValues="0.05 0.06 0.42" />
<feFuncG type="table" tableValues="0.09 0.42 0.66" />
<feFuncB type="table" tableValues="0.08 0.39 0.60" />
</feComponentTransfer>
</filter>
</defs>
</svg>
<style>
.mat-lead { margin-block: var(--space-4) var(--space-6); }
.mat-note { margin-top: var(--space-5); font-size: var(--step-0); }
.stage {
position: relative;
isolation: isolate;
max-width: 1180px;
width: 100%;
margin-inline: auto;
padding: var(--space-6) var(--space-4);
border-radius: var(--radius-shell);
overflow: hidden;
}
.stage-fallback {
position: absolute;
inset: 0;
z-index: -1;
width: 100%;
height: 100%;
display: block;
}
.stage-fallback {
background:
radial-gradient(120% 90% at 12% 8%, color-mix(in oklab, var(--accent) 22%, transparent), transparent 60%),
radial-gradient(90% 80% at 88% 92%, color-mix(in oklab, var(--accent) 14%, transparent), transparent 62%),
var(--surface-raised);
}
/* 3개 항목에 3개 셀. 빈 칸이 생기지 않고 균등 3열도 아니다 */
.tiles {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: var(--space-4);
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
grid-template-areas:
"big top"
"big bottom";
}
.tile:nth-child(1) { grid-area: big; }
.tile:nth-child(2) { grid-area: top; }
.tile:nth-child(3) { grid-area: bottom; }
/* 큰 셀은 더 넓은 화면을 갖는다 */
.tile:nth-child(1) .swatch { aspect-ratio: 5 / 4; }
.tile:nth-child(n + 2) .swatch { aspect-ratio: 16 / 7; }
.tile { display: grid; gap: var(--space-2); align-content: start; }
.tile .shell { box-shadow: var(--glass-hi), var(--shadow-2); }
.tile .core { overflow: hidden; }
/* 필터를 먹일 대상. 같은 도형에 서로 다른 재질을 입힌다 */
.swatch {
aspect-ratio: 4 / 3;
background:
repeating-linear-gradient(
58deg,
var(--accent) 0 14px,
color-mix(in oklab, var(--accent) 55%, var(--surface)) 14px 28px
);
}
.swatch-grain { filter: url(#f-grain); }
.swatch-glass { filter: url(#f-glass); }
.swatch-duotone { filter: url(#f-duotone); }
/* 필터를 지원하지 않거나 저사양이면 줄무늬만 남는다. 그래도 성립한다 */
@supports not (filter: url(#f-grain)) {
.swatch-grain, .swatch-glass, .swatch-duotone { filter: none; }
}
.tile-name { font-size: var(--step-1); margin-top: var(--space-1); }
.tile-how { margin: 0; font-size: var(--step-0); }
.tile-cost { margin: 0; color: var(--accent); }
@media (max-width: 760px) {
.tiles { grid-template-columns: 1fr; grid-template-areas: "big" "top" "bottom"; }
.tile:nth-child(n + 2) .swatch { aspect-ratio: 5 / 4; }
}
</style>