패럴랙스에 기준면을 준다 — 배경 격자와 광원 레이어

배경이 단색이면 패럴랙스는 성립하지 않는다. 앞의 것이 몇 px 어긋나도
비교할 대상이 없다. 히어로 말고는 기준면이 없어 시차가 안 보였다.

배경 이미지를 사 오는 대신 이 페이지가 이미 주장하는 것을 그렸다
  .bg-grid  간격 눈금 격자. 셀이 --grid-cell = --space-6
            파이프라인 · 수치. "모든 값이 토큰에서 나온다"가 배경에서도 성립
  .bg-glow  광원 블롭. 섹션마다 --glow-x 로 자리를 옮긴다
            재료 · 설치. 히어로의 빛이 마지막 CTA 에서 돌아온다

실측 3층 (스크롤 1000 -> 1600)
  격자 +14.9px  ·  산출물 -14px  ·  단계명 0
  한 층은 반드시 고정이다. 전부 움직이면 그냥 흔들리는 페이지다

그리드 wide 트랙을 좁은 화면에서 닫는다
  고정 10rem 이라 모바일에서도 좌우 80px 이 살아남아, 390px 에서 본문이
  x=112 부터 시작하고 폭이 278px 로 줄어 제목이 네 줄로 접혔다.
  --wide-pad 를 clamp 로 바꿔 닫았다. 이제 전부 x=32 에서 시작하고 두 줄이다

파이프라인 정렬선을 하나로
  제목 x=371 / 리드 x=435 / 스파인 x=143 이던 것을 전부 x=183 으로.
  제목·리드·브리프를 스파인과 같은 컨테이너에 넣었다

히어로 높이를 콘텐츠에 맞춘다
  모바일에서 콘텐츠 390px 에 736px 을 씌워 여백이 화면의 41% 였고
  다음 섹션이 화면 밖으로 밀려 스크롤 단서까지 사라졌다.
  모바일 min-height 제거(736 -> 554), 데스크톱 88dvh -> 78dvh
  배경 마스크 62% -> 82% 로 늘려 하단의 죽은 띠를 없앴다

스킬 motion.md 에 "패럴랙스에는 기준면이 필요하다" 를 추가.
페이지가 주장하는 것을 배경으로 그리는 표와, 한 층은 고정이라는 규칙.

390px / 1440px 실렌더: 가로 스크롤 0, 대비 실패 0. 테스트 22개 통과
This commit is contained in:
Yun Chan 2026-08-20 14:25:02 +09:00
parent 9c9ab2de7e
commit 0e03bbafac
9 changed files with 158 additions and 19 deletions

View file

@ -204,6 +204,36 @@ WebGL 게이트: `prefers-reduced-motion` 이면 **아예 시작하지 않는다
마지막 줄이 결정이다. 숫자가 스크롤에 흔들리면 그 값도 흔들려 보인다. 마지막 줄이 결정이다. 숫자가 스크롤에 흔들리면 그 값도 흔들려 보인다.
## 배경 레이어 — 패럴랙스의 기준면
**배경이 단색이면 패럴랙스는 성립하지 않는다.** 앞의 것이 몇 px 어긋나도 비교할 대상이 없다.
히어로 말고는 기준면이 없어서 시차가 안 보였다.
배경 이미지를 사 오는 대신 **이 페이지가 이미 주장하는 것**을 그렸다.
| 레이어 | 무엇 | 어디 | 근거 |
|---|---|---|---|
| `.bg-grid` | 간격 눈금 격자. 셀이 `--grid-cell = --space-6` | 파이프라인 · 수치 | "모든 값이 토큰에서 나온다"가 배경에서도 성립해야 한다. 계기판의 눈금이기도 하다 |
| `.bg-glow` | 광원 블롭 두 개. 섹션마다 `--glow-x` 로 자리를 옮긴다 | 재료 · 설치 | 히어로의 빛이 페이지를 관통해 마지막 CTA 에서 돌아온다 |
격자는 가장자리를 마스크로 지운다 — 화면 끝까지 가면 배경이 아니라 **표**가 된다.
배경이 가장 적게 움직인다. 앞의 것보다 크게 움직이면 앞으로 튀어나와 보인다.
**실측 3층** (스크롤 1000 → 1600): 격자 **+14.9px** · 산출물 **-14px** · 단계명 **0**.
한 층은 반드시 고정이다. 전부 움직이면 기준이 없어 그냥 흔들리는 페이지가 된다.
## 그리드의 `wide` 트랙을 좁은 화면에서 닫는다
`--wide-pad: clamp(0rem, -3.7rem + 15.2vw, 10rem)`
고정 `10rem` 으로 두는 동안 **모바일에서도 좌우 80px 씩 살아남아** 390px 화면에서
본문이 `x=112` 부터 시작하고 폭이 278px 로 줄었다. 파이프라인 제목이 네 줄로 접혔다.
닫고 나서 모든 텍스트가 `x=32` 에서 시작하고 제목이 두 줄이 됐다.
같은 이유로 파이프라인의 제목·리드·브리프를 스파인과 한 컨테이너에 넣었다.
섹션 기본 그리드에 두면 제목 `x=371` · 리드 `x=435` · 스파인 `x=143` 으로 **정렬선이 셋**이 된다.
지금은 전부 `x=183` 이다.
## 디자인 SSOT ## 디자인 SSOT
토큰이 없어서 깨져 있던 세 축을 채웠다. 토큰이 없어서 깨져 있던 세 축을 채웠다.

View file

@ -55,7 +55,7 @@ const { c, ui } = Astro.props;
position: relative; position: relative;
isolation: isolate; isolation: isolate;
overflow: clip; overflow: clip;
min-height: min(88dvh, 52rem); min-height: min(78dvh, 46rem);
display: grid; display: grid;
align-content: center; align-content: center;
padding-block: calc(var(--header-h) + var(--space-6)) var(--space-7); padding-block: calc(var(--header-h) + var(--space-6)) var(--space-7);
@ -72,8 +72,8 @@ const { c, ui } = Astro.props;
/* 아래로 갈수록 지운다. black 은 색이 아니라 마스크 알파다. /* 아래로 갈수록 지운다. black 은 색이 아니라 마스크 알파다.
없으면 히어로 하단에서 배경이 가로선으로 없으면 히어로 하단에서 배경이 가로선으로
뚝 끊겨 다음 섹션과 두 장의 종이처럼 보인다. */ 뚝 끊겨 다음 섹션과 두 장의 종이처럼 보인다. */
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 62%, transparent 96%); -webkit-mask-image: linear-gradient(to bottom, black 0%, black 82%, transparent 100%);
mask-image: linear-gradient(to bottom, black 0%, black 62%, transparent 96%); mask-image: linear-gradient(to bottom, black 0%, black 82%, transparent 100%);
} }
.hero-fallback, .hero-fallback,
.hero-canvas, .hero-canvas,
@ -143,7 +143,10 @@ const { c, ui } = Astro.props;
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.hero { min-height: min(94dvh, 46rem); } /* min-height 를 걸지 않는다. 콘텐츠가 390px 인데 736px 을 씌우면
여백이 화면의 41% 를 먹고, 다음 섹션이 화면 밖으로 밀려나
스크롤할 것이 있다는 단서까지 사라진다. */
.hero { min-height: 0; }
.hero-grid { grid-template-columns: 1fr; gap: var(--space-5); } .hero-grid { grid-template-columns: 1fr; gap: var(--space-5); }
.hero-title { max-width: 100%; } .hero-title { max-width: 100%; }
/* 세로 화면에서는 글자가 위쪽 절반을 다 쓴다. 가로 방향 스크림으로는 /* 세로 화면에서는 글자가 위쪽 절반을 다 쓴다. 가로 방향 스크림으로는

View file

@ -7,7 +7,7 @@ const { c, ui } = Astro.props;
--- ---
<section id="install" class="full install" aria-labelledby="install-h"> <section id="install" class="full install bg-glow" style="--glow-x: 30%" aria-labelledby="install-h">
<div class="install-grid"> <div class="install-grid">
<div class="install-head"> <div class="install-head">
<h2 id="install-h">{c.h2}</h2> <h2 id="install-h">{c.h2}</h2>

View file

@ -10,7 +10,7 @@ const { c } = Astro.props;
배경은 three.js 셰이더 플레인, 전경은 SVG 필터를 실제로 먹인 타일 3장. 배경은 three.js 셰이더 플레인, 전경은 SVG 필터를 실제로 먹인 타일 3장.
둘 다 설명이 아니라 지금 이 화면에서 돌고 있는 것이다. 둘 다 설명이 아니라 지금 이 화면에서 돌고 있는 것이다.
*/} */}
<section class="section materials" aria-labelledby="mat-h"> <section class="section materials bg-glow" style="--glow-x: 70%" aria-labelledby="mat-h">
<h2 id="mat-h" class="reveal">{c.h2}</h2> <h2 id="mat-h" class="reveal">{c.h2}</h2>
<p class="lead mat-lead reveal">{c.lead}</p> <p class="lead mat-lead reveal">{c.lead}</p>

View file

@ -19,7 +19,7 @@ const rest = c.items.filter((m) => !m.feature);
흔들리면 그 값도 흔들리는 것처럼 읽힌다. 계기판의 바늘은 떨지 않는다. 흔들리면 그 값도 흔들리는 것처럼 읽힌다. 계기판의 바늘은 떨지 않는다.
움직임이 의미를 만드는 자리가 있으면, 움직이지 않는 것이 의미인 자리도 있다. 움직임이 의미를 만드는 자리가 있으면, 움직이지 않는 것이 의미인 자리도 있다.
*/} */}
<section id="metrics" class="section metrics" aria-labelledby="metrics-h"> <section id="metrics" class="section metrics bg-grid" aria-labelledby="metrics-h">
<div class="head"> <div class="head">
<p class="eyebrow reveal">{c.eyebrow}</p> <p class="eyebrow reveal">{c.eyebrow}</p>
<h2 id="metrics-h" class="reveal">{c.h2}</h2> <h2 id="metrics-h" class="reveal">{c.h2}</h2>

View file

@ -14,17 +14,20 @@ const { c } = Astro.props;
③ 의미 — <ol role="list"> — 화면에 번호는 없지만 보조기술은 "6개 중 n번째"로 읽는다 ③ 의미 — <ol role="list"> — 화면에 번호는 없지만 보조기술은 "6개 중 n번째"로 읽는다
role="list" 는 list-style:none 일 때 Safari/VoiceOver 가 목록 의미를 버리는 것을 되돌린다 role="list" 는 list-style:none 일 때 Safari/VoiceOver 가 목록 의미를 버리는 것을 되돌린다
*/} */}
<section id="pipeline" class="section" aria-labelledby="pipeline-h"> <section id="pipeline" class="section bg-grid" aria-labelledby="pipeline-h">
<p class="eyebrow reveal">{c.eyebrow}</p> {/* 제목·리드·브리프·스파인을 한 컨테이너에 담는다.
<h2 id="pipeline-h" class="reveal">{c.h2}</h2> 섹션 기본 그리드에 두면 제목은 main 칼럼의 음수 마진으로 x=371,
<p class="lead pipeline-lead reveal">{c.lead}</p> 리드는 x=435, 스파인은 x=143 에 서서 정렬선이 셋이 된다. */}
<div class="brief reveal"><div class="brief-core">
<span class="brief-label">{c.briefLabel}</span>
<p class="brief-text">{c.brief}</p>
</div></div>
<div class="full spine-wrap"> <div class="full spine-wrap">
<p class="eyebrow reveal">{c.eyebrow}</p>
<h2 id="pipeline-h" class="reveal">{c.h2}</h2>
<p class="lead pipeline-lead reveal">{c.lead}</p>
<div class="brief reveal"><div class="brief-core">
<span class="brief-label">{c.briefLabel}</span>
<p class="brief-text">{c.brief}</p>
</div></div>
<ol role="list" class="spine"> <ol role="list" class="spine">
{ {
c.stages.map((s) => ( c.stages.map((s) => (
@ -43,9 +46,13 @@ const { c } = Astro.props;
</section> </section>
<style> <style>
/* 컨테이너는 넓어도 읽는 것은 measure 안에 둔다 */
.pipeline-lead { .pipeline-lead {
margin-bottom: var(--space-6); margin-bottom: var(--space-6);
max-width: var(--measure);
} }
.brief { max-width: var(--measure); }
#pipeline-h { max-width: 24ch; }
/* 스파인은 페이지의 본체다. main 칼럼(561px)에 가두면 /* 스파인은 페이지의 본체다. main 칼럼(561px)에 가두면
페이지에서 가장 긴 섹션이 가장 좁아진다. 페이지에서 가장 긴 섹션이 가장 좁아진다.

View file

@ -139,9 +139,9 @@ section[id] { scroll-margin-top: calc(var(--header-h) + var(--space-4)); }
display: grid; display: grid;
grid-template-columns: grid-template-columns:
[full-start] minmax(var(--gutter), 1fr) [full-start] minmax(var(--gutter), 1fr)
[wide-start] minmax(0, var(--space-8)) [wide-start] minmax(0, var(--wide-pad))
[main-start] minmax(0, var(--measure)) [main-end] [main-start] minmax(0, var(--measure)) [main-end]
minmax(0, var(--space-8)) [wide-end] minmax(0, var(--wide-pad)) [wide-end]
minmax(var(--gutter), 1fr) [full-end]; minmax(var(--gutter), 1fr) [full-end];
} }
.page > * { grid-column: main; } .page > * { grid-column: main; }
@ -415,6 +415,61 @@ a:hover { color: var(--accent); }
} }
} }
/* ---- 배경 레이어 --------------------------------------------
패럴랙스는 깊이 차가 보여야 성립한다. 배경이 단색이면 앞의 것이
px 어긋나 봐야 아무도 느낀다 기준면이 없기 때문이다.
그래서 배경을 깐다. 다만 이미지가 아니라 페이지가 주장하는 ,
간격 눈금 자체를 격자로 그린다. --grid-cell --space-5 . */
.bg-grid,
.bg-glow {
position: relative;
isolation: isolate;
}
.bg-grid::before,
.bg-glow::before {
content: "";
position: absolute;
inset: -12% 0;
z-index: -1;
pointer-events: none;
}
.bg-grid::before {
background-image:
repeating-linear-gradient(to right,
var(--line-strong) 0 1px, transparent 1px var(--grid-cell)),
repeating-linear-gradient(to bottom,
var(--line-strong) 0 1px, transparent 1px var(--grid-cell));
/* 가장자리에서 지운다. 화면 끝까지 격자가 가면 표가 되어버린다 */
-webkit-mask-image: radial-gradient(135% 95% at 50% 45%, black 22%, transparent 92%);
mask-image: radial-gradient(135% 95% at 50% 45%, black 22%, transparent 92%);
opacity: 0.7;
}
/* 히어로의 두 광원이 페이지를 관통한다. 섹션마다 자리가 다르다 */
.bg-glow::before {
background:
radial-gradient(42% 46% at var(--glow-x, 78%) var(--glow-y, 32%),
color-mix(in oklab, var(--accent) 15%, transparent), transparent 70%),
radial-gradient(38% 42% at calc(100% - var(--glow-x, 78%)) 78%,
color-mix(in oklab, var(--glow-2) 13%, transparent), transparent 68%);
}
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: view()) {
/* 배경이 가장 느리다. 콘텐츠보다 적게 움직여야 뒤에 있는 것으로 읽힌다 */
.bg-grid::before { --par-from: -34px; --par-to: 34px; }
.bg-glow::before { --par-from: -58px; --par-to: 58px; }
.bg-grid::before,
.bg-glow::before {
animation: par linear both;
animation-timeline: view();
animation-range: cover 0% cover 100%;
}
}
}
/* ---- 유틸 -------------------------------------------------- */ /* ---- 유틸 -------------------------------------------------- */
.visually-hidden { .visually-hidden {
position: absolute; position: absolute;

View file

@ -128,6 +128,18 @@
모바일 구간 32px 깔고 넓은 화면에서만 40px 까지 연다. */ 모바일 구간 32px 깔고 넓은 화면에서만 40px 까지 연다. */
--gutter: clamp(2rem, 1.4rem + 1.8vw, 2.5rem); --gutter: clamp(2rem, 1.4rem + 1.8vw, 2.5rem);
/* 배경 격자의 . 간격 스케일에서 가져온다
장식용 격자가 아니라 페이지가 실제로 쓰는 눈금이어야
"모든 값이 토큰에서 나온다" 주장이 배경에서도 성립한다. */
--grid-cell: var(--space-6);
/* main 칼럼 바깥의 여유 (wide 트랙). 넓은 화면에서 제목과 넓은 블록이
본문보다 밖으로 나갈 자리를 만든다.
값을 고정으로 두면 모바일에서도 좌우 80px 살아남아, 390px 화면에서
본문이 x=112 부터 시작하고 폭이 278px 줄어든다 제목이 줄로 접힌다.
좁은 화면에서는 0 으로 닫는다. */
--wide-pad: clamp(0rem, -3.7rem + 15.2vw, 10rem);
/* 헤더가 차지하는 높이. 있는 헤더라 문서 흐름에서 빠져 있고, /* 헤더가 차지하는 높이. 있는 헤더라 문서 흐름에서 빠져 있고,
그래서 히어로 상단 여백과 앵커 목적지가 값을 알아야 한다. 그래서 히어로 상단 여백과 앵커 목적지가 값을 알아야 한다.
간격 토큰을 clamp 바꾸는 순간 관계가 끊겨 h1 헤더 뒤로 간격 토큰을 clamp 바꾸는 순간 관계가 끊겨 h1 헤더 뒤로

View file

@ -448,6 +448,38 @@ JS 쪽 판정도 한 곳에 모은다. 설정이 도중에 바뀌면 반영한
안 움직이는 섹션이 하나도 없으면 움직임에 의미가 없다는 뜻이고, 안 움직이는 섹션이 하나도 없으면 움직임에 의미가 없다는 뜻이고,
`design.md` 에 "여기는 의도적으로 정지"라고 적어두지 않으면 다음 사람이 "빠뜨린 것"으로 보고 채운다. `design.md` 에 "여기는 의도적으로 정지"라고 적어두지 않으면 다음 사람이 "빠뜨린 것"으로 보고 채운다.
### 패럴랙스에는 기준면이 필요하다
**배경이 단색이면 패럴랙스는 성립하지 않는다.** 앞의 것이 몇 px 어긋나 봐야
비교할 대상이 없어서 아무도 못 느낀다. 시차는 두 층 사이에서만 보인다.
그렇다고 배경 이미지를 사 오지 마라. **그 페이지가 이미 주장하는 것을 배경으로 그려라.**
| 페이지가 주장하는 것 | 배경으로 그릴 것 |
|---|---|
| 값이 토큰 체계에서 나온다 | **간격 눈금 격자**. 셀 크기를 실제 space 토큰으로 |
| 재질·표면이 주제다 | 광원 블롭. 섹션마다 자리를 옮긴다 |
| 데이터·계측이 주제다 | 눈금, 축, 등고선 |
| 문서·아카이브다 | 괘선, 여백선 |
격자를 그릴 때 두 가지를 지켜라.
1. **가장자리를 마스크로 지운다.** 화면 끝까지 격자가 가면 배경이 아니라 **표**가 된다
2. **배경이 가장 적게 움직인다.** 앞의 것보다 크게 움직이면 앞으로 튀어나와 보인다
```css
.bg-grid::before {
content: ""; position: absolute; inset: -12% 0; z-index: -1;
background-image:
repeating-linear-gradient(to right, var(--line-strong) 0 1px, transparent 1px var(--grid-cell)),
repeating-linear-gradient(to bottom, var(--line-strong) 0 1px, transparent 1px var(--grid-cell));
mask-image: radial-gradient(135% 95% at 50% 45%, black 22%, transparent 92%);
}
```
세 층이 되면 시차가 확실해진다 — 실측: 배경 격자 **+14.9px**, 산출물 **-14px**, 단계명 **0**.
**한 층은 반드시 고정이어야 한다.** 전부 움직이면 기준이 없어 그냥 흔들리는 페이지다.
### 구현은 scroll-driven animation 으로 ### 구현은 scroll-driven animation 으로
```css ```css