소개 페이지 전면 재디자인 — 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

@ -16,23 +16,41 @@ const FONT_CSS = {
} as const;
const alt = lang === "ko" ? "/en/" : "/";
// 파비콘: 페이지의 스파인 모티프를 그대로 쓴 단순 기하 마크
const FAVICON =
"data:image/svg+xml," +
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#0E6B63"/>
<path d="M11 7v18" stroke="#FAF9F6" stroke-width="2" stroke-linecap="round"/>
<circle cx="11" cy="10" r="3" fill="#FAF9F6"/>
<circle cx="11" cy="16" r="3" fill="#FAF9F6"/>
<circle cx="11" cy="22" r="3" fill="#FAF9F6"/>
<path d="M18 10h7M18 16h5M18 22h7" stroke="#FAF9F6" stroke-width="2" stroke-linecap="round" opacity=".55"/>
</svg>`,
);
---
<!doctype html>
<html lang={lang}>
<html lang={lang} class="no-js">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="icon" href={FAVICON} />
<link rel="canonical" href={new URL(Astro.url.pathname, Astro.site)} />
<link rel="alternate" hreflang="ko" href={new URL("/", Astro.site)} />
<link rel="alternate" hreflang="en" href={new URL("/en/", Astro.site)} />
<link rel="alternate" hreflang="x-default" href={new URL("/", Astro.site)} />
{/* 본문 폰트만 preconnect·preload 한다. 모노는 코드 블록에만 쓰인다 */}
<link rel="preconnect" href={lang === "ko" ? "https://cdn.jsdelivr.net" : "https://api.fontshare.com"} crossorigin />
<link
rel="preconnect"
href={lang === "ko" ? "https://cdn.jsdelivr.net" : "https://api.fontshare.com"}
crossorigin
/>
<link rel="stylesheet" href={FONT_CSS[lang]} />
<link
rel="stylesheet"
@ -44,20 +62,36 @@ const alt = lang === "ko" ? "/en/" : "/";
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:locale" content={lang === "ko" ? "ko_KR" : "en_US"} />
<meta name="color-scheme" content="light dark" />
{/* JS 가 살아 있으면 no-js 를 떼고 진입 연출을 켠다 */}
<script is:inline>
document.documentElement.classList.remove("no-js");
</script>
</head>
<body>
<a class="skip" href="#main">{lang === "ko" ? "본문으로 건너뛰기" : "Skip to content"}</a>
<header class="page site-header">
<a class="wordmark" href={lang === "ko" ? "/" : "/en/"}>designpaca</a>
<nav aria-label={lang === "ko" ? "언어" : "Language"}>
{/* 언어는 국가가 아니다 — 국기 아이콘을 쓰지 않는다 */}
<a href={alt} hreflang={lang === "ko" ? "en" : "ko"} lang={lang === "ko" ? "en" : "ko"}>
{lang === "ko" ? "English" : "한국어"}
<header class="site-header">
<div class="header-inner glass">
<a class="wordmark" href={lang === "ko" ? "/" : "/en/"} aria-label="designpaca">
<svg class="mark" viewBox="0 0 32 32" width="20" height="20" aria-hidden="true">
<path d="M11 7v18" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" />
<circle cx="11" cy="10" r="3" fill="currentColor" />
<circle cx="11" cy="16" r="3" fill="currentColor" />
<circle cx="11" cy="22" r="3" fill="currentColor" />
</svg>
<span>designpaca</span>
</a>
</nav>
<nav aria-label={lang === "ko" ? "언어" : "Language"}>
{/* 언어는 국가가 아니다. 국기 아이콘을 쓰지 않는다 */}
<a href={alt} hreflang={lang === "ko" ? "en" : "ko"} lang={lang === "ko" ? "en" : "ko"}>
{lang === "ko" ? "English" : "한국어"}
</a>
</nav>
</div>
</header>
<main id="main" class="page">
@ -67,6 +101,29 @@ const alt = lang === "ko" ? "/en/" : "/";
<footer class="page site-footer">
<slot name="footer" />
</footer>
<script is:inline>
// 진입 연출. 한 번 보이면 계속 보인다.
// scroll 리스너를 쓰지 않는다(하드 게이트 10).
(function () {
var els = document.querySelectorAll(".reveal");
if (!("IntersectionObserver" in window) || !els.length) {
els.forEach(function (el) { el.classList.add("is-in"); });
return;
}
var io = new IntersectionObserver(
function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
e.target.classList.add("is-in");
io.unobserve(e.target);
});
},
{ rootMargin: "200px 0px 200px 0px", threshold: 0 },
);
els.forEach(function (el) { io.observe(el); });
})();
</script>
</body>
</html>
@ -82,31 +139,51 @@ const alt = lang === "ko" ? "/en/" : "/";
z-index: 10;
transition: transform var(--dur-quick) var(--ease-out);
}
.skip:focus {
transform: translateY(calc(var(--space-3) * 5));
}
.skip:focus { transform: translateY(calc(var(--space-3) * 5)); }
/* 상단에 붙은 풀블리드 바 대신 떠 있는 헤더 */
/* 상단에 붙은 바가 아니라 떠 있는 유리 알약 */
.site-header {
padding-block: var(--space-4);
position: sticky;
top: var(--space-3);
z-index: 3;
padding-inline: var(--space-4);
margin-top: var(--space-3);
}
.site-header > * {
grid-column: main;
}
.site-header {
align-items: baseline;
.header-inner {
max-width: 1180px;
margin-inline: auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
border-radius: 999px;
}
.wordmark {
font-weight: 600;
display: inline-flex;
align-items: center;
gap: var(--space-2);
font-weight: 560;
letter-spacing: -0.01em;
text-decoration: none;
}
.site-header nav {
justify-self: end;
grid-row: 1;
grid-column: main;
.mark { color: var(--accent); flex: none; }
.site-header nav a {
display: inline-flex;
align-items: center;
min-height: 36px;
padding-inline: var(--space-3);
border-radius: 999px;
background: var(--glass-tint-2);
border: 1px solid var(--glass-edge);
font-size: var(--step-0);
color: var(--ink-muted);
text-decoration: none;
transition: color var(--dur-instant) var(--ease-out),
background var(--dur-instant) var(--ease-out);
}
.site-header nav a:hover { color: var(--ink); background: var(--glass-tint); }
.site-footer {
padding-block: var(--space-6);