게이트 씬을 파이프라인으로 옮기면서 히어로에도 남겨뒀더니 같은 그림이
페이지에 두 번 나왔다. 역할이 다르면 씬도 달라야 한다.
히어로 shader-plane 흐름 필드 노이즈를 두 번 접어 빛의 결을 만든다
공간을 만드는 재질. 아무것도 반응 안 한다
파이프라인 gate-scene 유리판 여섯 통과하면 그 단계가 켜진다. 주인공
재료 shader-plane 스테이지 필터 타일이 앉는 무대
히어로에는 무거운 씬을 임포트하지 않는다 — three.md §0 대로 셰이더 플레인 하나다.
lib/hero-scene.ts -> lib/gate-scene.ts 로 리네임했다. 이름이 용도와 다르면
다음 사람이 히어로 것인 줄 알고 고친다.
대비 실측: 캔버스 배경 최악 8.46:1(가장 밝은 중앙 여백), h1 자리 14.83:1.
readPixels 로 재려다 전부 0 이 나왔다 — preserveDrawingBuffer: false 면
버퍼가 비어 있다. 스크린샷 픽셀로 쟀고, 글자 없는 영역만 골랐다.
스킬 three.md
씬을 두 자리에 재사용하지 마라. 역할(주인공/재질/무대)로 나눠라
파일 이름을 용도와 맞춰라
WebGL 위 대비는 readPixels 가 아니라 스크린샷 픽셀로 잰다.
0 을 검정으로 착각하면 대비가 통과한 것처럼 보인다
414 lines
28 KiB
Markdown
414 lines
28 KiB
Markdown
# design.md — designpaca 소개 페이지
|
||
|
||
> 6단계 산출물. 다음 실행(사람이든 에이전트든)은 이 파일을 먼저 읽고 같은 결정을 이어간다.
|
||
|
||
## 개정 이력
|
||
|
||
| 날짜 | 판 | 무엇이 바뀌었나 |
|
||
|---|---|---|
|
||
| 2026-08-20 | 초판 | `swiss-minimal` · three.js 미사용 · 라이트 기반 |
|
||
| 2026-08-20 | **2판(현행)** | `dark-instrument` · three.js 채택 · 전체 경로 재주행 |
|
||
|
||
**초판이 거짓말이 됐던 경위를 남긴다.** 사용자가 "글래스모피즘·리퀴드글래스·히어로에 힘"을 요청했을 때
|
||
그것은 2단계 방향 재결정이었고, 스킬은 그럴 때 **전체 경로로 올려 1단계부터 다시 돌라**고 씌어 있다.
|
||
그러지 않고 4단계 구현으로 바로 들어갔다. 레퍼런스 없이 재질만 갈아끼운 결과가 **스위스 미니멀 뼈대에
|
||
유리를 바른 페이지**였고, `design.md` 는 초판 상태로 남아 프리셋·이펙트·형태 수치가 전부 어긋났다.
|
||
2판은 0단계부터 다시 돌았다.
|
||
|
||
---
|
||
|
||
## 브리프 3줄
|
||
|
||
- **무엇을**: designpaca 소개 페이지. 파이프라인 스킬 + npx 설치 CLI 를 소개하는 단일 페이지, 한국어·영어 두 판
|
||
- **누구에게**: Claude Code · Codex · Cursor 를 쓰는 개발자. 이 페이지를 보고 `npx designpaca` 를 터미널에 치게 만드는 것이 목표
|
||
- **제약**: Astro 정적 → Cloudflare Pages. 이 페이지 자체가 스킬의 품질 증거물이다 — 스킬이 금지하는 것을 이 페이지가 하면 안 된다
|
||
|
||
## 레퍼런스 (전부 실측. computed style 추출까지 돌렸다)
|
||
|
||
| 슬롯 | 사이트 | 업종 | 실측값 | 가져온 **원리** | 가져오지 **않은** 것 |
|
||
|---|---|---|---|---|---|
|
||
| R1 · 구조 | zed.dev | 코드 에디터 | 컨테이너 **1120px**(68회) ↔ 전체폭 1425px(50회) 교대 · 본문 칼럼 512px · **radius 2px 가 621회** · 모노 524회로 지배 | 폭의 교대, 각진 형태 어휘, 모노를 정체성으로 | 세리프 h1(우리는 한글) · 최대÷본문 3.0배(위계가 약하다) · 섹션 11개 |
|
||
| R2 · 톤 | lusion.co | 3D 프로덕션 스튜디오 | 배경 **흰색** · 텍스트 색 **2단계**(검정 135 / 흰 16) · 폰트 **1개**(Aeonik) · 강조색 rgb(0,22,236) **4회** · 캔버스는 radius 15px 액자 안 · **h1 이 36px** | **3D 는 액자 안, 텍스트는 액자 밖.** 3D 가 크면 타이포는 물러난다. 색은 캔버스 안에서만 소리친다 | 흰 배경 자체 · 파랑 |
|
||
| R3 · 디테일 | Codrops "Inside HAOQI.DESIGN" | — | 축 5 전용 | DOM 이 구조를 소유하고 WebGL 은 셰이더 uniform 만 만진다. 한 rAF 에서 스크롤을 공유하고 뷰포트 밖에서는 정지 | R3F/Lenis 스택 |
|
||
|
||
**R3 구현 제약**: 셰이더 uniform 만 갱신하므로 `transform`/`opacity` 외의 CSS 를 애니메이션하지 않는다(게이트 #8 안전).
|
||
뷰포트 밖 렌더 정지가 필수 조건이다.
|
||
|
||
**접근 실패 기록** — `iglooinc.com` 은 hugedomains 파킹으로 302, `lasvit.com` 은 403.
|
||
`reference-method.md` §3-b 가 경고한 그대로였다. 왕복 세 번에서 끊고 우회했다.
|
||
|
||
### 히어로 국소 레퍼런스 (1′)
|
||
|
||
R1~R3 로 구조를 잡은 뒤에도 히어로가 어색해서 **국소 브리프 표**로 두 곳을 더 실측했다.
|
||
`design.md` 는 "3D 는 액자 안"까지만 답하고 **액자가 화면의 몇 %여야 하는지는 답하지 않는다.**
|
||
|
||
| 사이트 | 3D 영역 | 텍스트 위치 | h1 |
|
||
|---|---|---|---|
|
||
| lusion.co | 1440×645 = 첫 화면의 **71%** | 캔버스 **위**, 조밀하게 | 36px |
|
||
| basement.studio | 1425×900 = **100%** | 캔버스 **아래**(y=916) | 87px, weight 600 |
|
||
| railway.com | SVG 1062×755, h1 아래 | 중앙 상단 | 54px, **1줄** |
|
||
| **우리(고치기 전)** | 1352×465 = **52%** | 위 380px | 41.5px |
|
||
|
||
셋 다 3D 와 텍스트를 세로로 완전히 분리하고, 3D 가 첫 화면의 **70% 이상**을 먹는다.
|
||
우리는 텍스트 43% / 3D 52% 로 **둘 다 어중간했다.** basement 형(첫 화면 전체 3D)은 CTA 가
|
||
첫 화면에서 밀려나므로 전환이 목적인 우리에게 맞지 않는다. lusion 비율을 택했다.
|
||
|
||
**현행: 텍스트 24% · 액자 59% · 캡션과 링크가 액자 아래 한 줄.**
|
||
링크(저장소·npm)를 텍스트 블록에서 액자 캡션 줄로 내려 상단을 비웠다.
|
||
|
||
**레퍼런스를 보기 전에 네 번 고쳤다.** 판 폭 7.4 → 15, 액자 비율 16/7 → 16/5.5 → 16/6.6 → 16/6,
|
||
sway 0.5 → 0.9 → 0.16. 전부 계산이지 근거가 아니었다. 레퍼런스 두 곳을 재자 한 번에 정리됐다.
|
||
|
||
## 한 문장 컨셉
|
||
|
||
> **검사대 위의 물체.** 액자가 게이트고, 안에 든 것이 검사 중인 디자인이고, 밖의 글자가 판정 기록이다.
|
||
|
||
## 프리셋 — `dark-instrument`
|
||
|
||
초판의 `swiss-minimal` 에서 바꿨다. 페이지가 이미 어둡고 제품이 CLI 다.
|
||
**초판에는 이 재질을 정당화하는 프리셋이 아예 없었다** — 그것이 "유리를 발랐는데 유리일 이유가 없다"의 원인이다.
|
||
|
||
프리셋은 "글래스모피즘은 내비게이션 바 정도까지"라고 반대한다. 사용자의 명시적 지시가 프리셋보다 위이므로
|
||
없애지 않고 **의미가 생기는 자리로 옮겼다**: 유리는 **액자에만** 쓴다 — 헤더 · 검사대 프레임 · 명령창.
|
||
콘텐츠 카드(브리프, 조판 데모)에서는 뺐다. 유리는 무언가를 담을 때만 의미가 있다.
|
||
|
||
## 감수한 리스크 — 하나
|
||
|
||
**히어로에서 거대 타이포를 버리고 힘을 3D 액자로 넘긴다.**
|
||
초판 히어로의 `--display` 는 최대 112px 로 화면 높이의 절반을 먹었고, 그 아래 캔버스가 깔려 있었다.
|
||
R2 의 h1 은 36px 인데 히어로가 압도적이다 — 힘이 타이포가 아니라 물체에서 나오기 때문이다.
|
||
현행 히어로 h1 은 `clamp(var(--step-2), 1.1rem + 2.4vw, var(--step-4))`, 최대 **41.5px** 다.
|
||
페이지에서 가장 큰 글자(96px)는 히어로가 아니라 수치 섹션에 있다 — R2 도 그렇다.
|
||
|
||
## 토큰
|
||
|
||
`src/styles/tokens.css` 전문. 초판에서 바뀐 것만 적는다.
|
||
|
||
- **radius 2종**: `--radius-sm: 3px`(요소) · `--radius-frame: 14px`(액자).
|
||
근거는 R1 의 2px(621회)과 R2 의 15px. `999px` pill 3곳을 없앴다 — 계기판에 알약은 없다
|
||
- **`--glow-2`: `#6E7BFF` → `#3D7BD6`**. 앞의 값은 R 이 높아 보라-파랑으로 읽힌다.
|
||
`antipatterns.md` 의 슬롭 지문이자 `dark-instrument` 가 "어두운 배경일수록 빠지기 쉬운 함정"으로 지목한 색이다
|
||
- **유리 색값을 전부 토큰으로**: `--glass-sheen` · `--glass-rim` · `--core-hi`.
|
||
`base.css` 에 흩어져 있던 `rgb(255 255 255 / …)` 7곳이 하드 게이트 #1 위반이었다
|
||
- **이중 베젤(`.shell`/`.core`) 폐기** → 단일 `.frame`. 껍데기와 속의 radius 가 서로 달라 그것만으로 형태 어휘가 두 종 늘어난다
|
||
- **`--step--1: 0.8125rem` 추가**. 게이트 판독기가 스케일 밖 13px 을 쓰고 있었다
|
||
- 강조색은 여전히 딥 틸 하나. 라이트/다크 쌍(`--accent` / `--accent-inverse`)은 규칙으로 정의된 반전이라 1종으로 센다
|
||
|
||
**타입 스케일 실측**: `step--1` ~ `step-4` 로 **6단계**, `--display`(96px)는 스케일 밖, 모노는 본문의 0.9375배 파생.
|
||
최대÷본문 = **5.6배**. 초판 페이지는 "5단계"라고 실었는데 실측은 9종이었다 — 아래 8·9번 참조.
|
||
|
||
## 성능 — 예산과 실측
|
||
|
||
**예산을 올렸고, 올렸다고 말한다.** 초판 예산은 히어로까지 JS 50KB 였다.
|
||
이 페이지는 three.js 를 파는 제품의 데모이므로 그 판단을 검증할 수 있어야 한다.
|
||
|
||
**예산 숫자 자체를 한 번 고쳤다.** 처음 three.js 예산을 160KB 로 적었는데 실측이 184KB 였다.
|
||
three.js 크기는 미리 알 수 있는 값이므로 이건 예산 설정 실패다. 190KB 로 고쳤고, **사후 조정이라는 사실을 남긴다.**
|
||
실측을 보고 예산을 맞추는 것은 예산이 아니다 — 다음 실행은 여기서부터 시작해야 한다.
|
||
|
||
| 항목 | 예산 | 실측 | 판정 |
|
||
|---|---|---|---|
|
||
| 히어로까지 JS, 초기 (gzip) | 50KB | 복사 버튼 314B + 씬 부트 | 통과 |
|
||
| three.js, 지연 로드 (gzip) | 190KB | **184KB** (동적 `import()`, IntersectionObserver 로 화면에 들어올 때만) | 통과 |
|
||
| 첫 인터랙션 | 2초 | **미측정** | 배포 후 Lighthouse |
|
||
| LCP / CLS | 2.5초 / 0.1 | **미측정** | 배포 후 |
|
||
|
||
WebGL 게이트: `prefers-reduced-motion` 이면 **아예 시작하지 않는다**. DPR 1.5 클램프.
|
||
`hardwareConcurrency <= 4` 면 판을 6장에서 3장으로 줄인다. 탭이 숨거나 화면 밖이면 그리지 않는다.
|
||
|
||
## 채택한 이펙트와 폴백
|
||
|
||
| 이펙트 | 구현 | 폴백 |
|
||
|---|---|---|
|
||
| 검사대 3D | 유리판 6장이 깊이 축에 서고 카메라가 스크롤로 통과. 굴절은 렌더 타겟 1장 샘플링(`transmission` 재질보다 훨씬 싸다) | 캔버스 아래 CSS 그라디언트가 **폴백이 아니라 결과물**이다. 액자·판독기·캡션은 그대로 남는다 |
|
||
| 게이트 판독기 | 통과한 판이 바뀔 때만 DOM 을 쓴다. 프레임마다 쓰지 않는다 | JS 없으면 `게이트 01 / 06` 정적 표시 |
|
||
| 복사 버튼 | 인라인 314B | 클립보드 실패 시 텍스트를 선택 상태로 만든다 |
|
||
|
||
## 5단계에서 실제로 걸린 것
|
||
|
||
정적 검사와 실렌더를 둘 다 돌렸다. **걸린 것을 전부 적는다 — 유리한 것만 고르면 리스크가 자기 홍보가 된다.**
|
||
|
||
1. **하드 게이트 #3 위반** — radius 4종(`sm`/`md`/`shell`/`999px`). 2종으로 정리
|
||
2. **하드 게이트 #1 위반** — `base.css` 에 토큰 밖 `rgb()` 7곳. 토큰으로 올림
|
||
3. **하드 게이트 #7 위반** — 헤더 "English" 버튼 **2.59:1**. `--ink-muted` → `--ink` 로 올려 통과
|
||
4. **카운트 규칙 위반** — 히어로 h1 이 4줄이었다(상한 2줄). 폰트 크기를 낮춰 2줄
|
||
5. **셰이더 결함** — 판이 정면으로 서 있어 `dot(n, view) ≈ 1`, **프레넬이 구조적으로 0**이었다.
|
||
유리로 읽히게 하는 유일한 신호가 죽어 있었고 그것이 "그냥 흐릿한 사각형"의 원인이다.
|
||
법선을 미세하게 굽히고 UV 경계로 테두리 광을 따로 만들어 해결
|
||
6. **셰이더 UV 오류** — `gl_FragCoord` 를 렌더 타겟 크기(캔버스의 0.6배)로 나누고 있었다. UV 가 1 을 넘어 텍스처가 잘렸다.
|
||
`getDrawingBufferSize()` 로 교정
|
||
7. **폭 리듬 부재** — 9개 섹션 중 파이프라인이 **1440px 중 625px(43%)** 만 쓰면서 높이는 1637px 로 가장 길었다.
|
||
페이지의 본체가 가장 좁았다. 2열로 열어 1113px
|
||
8. **페이지가 실은 수치가 틀렸다** — "타입 단계 5단계" 라고 실었는데 실측 **9종**이었다.
|
||
이 페이지의 리스크가 "자기 수치를 공개한다" 인데 공개한 값이 틀리면 리스크가 아니라 허위다
|
||
9. **`lang` 속성이 타입 스케일을 새게 했다** — 언어 전환 링크에 `lang="en"` 이 붙어 있고(스크린리더가
|
||
"English" 를 영어로 읽어야 하므로 옳다) `[lang]` 셀렉터가 스케일을 통째로 재정의해 그 링크만 16px 이 됐다.
|
||
접근성 표시가 조용히 디자인 토큰을 바꾸고 있었다. 크기는 헤더에서 확정하고 링크는 상속만 받게 고쳤다
|
||
|
||
**측정 스크립트 자체의 오류도 한 번 있었다.** 대비 계산에서 반투명 배경을 불투명으로 취급해
|
||
"English 1.15:1" 이라는 틀린 값이 나왔다. 알파를 합성하도록 고친 뒤 실패 0건.
|
||
**검사 도구가 틀리면 통과도 실패도 의미가 없다.**
|
||
|
||
### 최종 판정 (1440px · 320px 실렌더)
|
||
|
||
- 하드 게이트 12개 **전부 "아니오"**
|
||
- 대비: 본문·링크·버튼 전 요소 AA 이상, 실패 **0건**
|
||
- 320px: 가로 스크롤 없음(문서 폭 305px), 뷰포트 초과 요소 0, 버튼 줄바꿈 0
|
||
- 카운트: 섹션 9 / eyebrow 3(상한 `ceil(9/3)=3`) / 마퀴 0 / 히어로 헤드라인 2줄 / radius 어휘 2종 / 반투명 패널 2개
|
||
- 슬롭 지문 grep: 0건
|
||
|
||
## 의도적으로 하지 않은 것과 그 이유
|
||
|
||
- **"Anti-Slop" 이라는 명명** — tasteskill 이 쓰는 말이다. 같은 단어를 쓰면 즉시 아류로 읽힌다
|
||
- **경쟁 제품의 실측치를 페이지에 싣기** — 남의 수치는 반박 가능하다. 우리 수치만 쓴다
|
||
- **GitHub 스타 등 인기 지표** — 우리는 숫자가 0이다. 인기 대신 규율을 증거로 쓴다
|
||
- **3D 위에 본문 얹기** — R2 가 준 원리의 핵심이다. 액자 위에 얹는 글자는 판독기 하나뿐이고, 그것도 바탕을 깔았다
|
||
- **GLB 모델** — 조사 결과 추상 지오메트리가 이 브리프에서 더 낫다. 모델 하나가 수 MB 이고, 우리가 파는 것은 모델이 아니라 절차다
|
||
- **다크를 "멋있어서" 고르기** — 프리셋이 다크 기본값을 슬롭 시그니처 1위로 지목한다. 제품이 터미널이라는 근거로 고른다
|
||
- **언어 자동 리다이렉트 / 국기 아이콘** — 사용자를 놀라게 하고, 언어는 국가가 아니다
|
||
- **두 판을 번역으로 만들기** — 각 언어판을 따로 썼다
|
||
|
||
## 3D 의 역할 — 주인공이 아니라 재질
|
||
|
||
**한 번 액자 안에 넣었다가 되돌렸다.** R2(lusion.co)의 "3D 는 액자 안"은 **3D 자체가 상품일 때**의
|
||
규칙이다. 그 스튜디오는 3D 를 팔기 때문에 액자가 쇼케이스 프레임으로 정당하다.
|
||
우리는 절차를 파는데 액자 안에 넣을 상품이 없어서, 보는 사람이 "저 상자는 뭐냐"고 묻게 됐다.
|
||
게다가 의미를 `게이트 01 / 06` 판독기가 **자막으로 설명하고 있었다** — 그림이 스스로 말하지
|
||
못해서 자막을 단 것이다.
|
||
|
||
이 씬의 역할은 **재질**이다. 공간을 만드는 표면이지 볼거리가 아니다. 그래서 규칙이 반대다:
|
||
|
||
| | 주인공(액자) | 재질(배경) — 현행 |
|
||
|---|---|---|
|
||
| 테두리 광 | 0.85 | **0.42** |
|
||
| 알파 | 0.72 | **0.34** |
|
||
| UV 경계 | `smoothstep(0, 0.05)` | **`smoothstep(0, 0.17)` + 제곱** |
|
||
| 카메라 이동 | 전 구간 | **60%** |
|
||
| 텍스트 | 액자 밖 | 위에 앉되 **스크림으로 자리를 눌러줌** |
|
||
|
||
`ResizeObserver` 로 캔버스를 따라가게 했다. `window.resize` 만 듣던 동안 히어로 높이 변화를
|
||
놓쳐 배경이 화면의 3분의 2만 덮고 아래가 검게 비었다.
|
||
|
||
## 모션 문법 — 섹션마다 다른 말
|
||
|
||
같은 패럴랙스를 전부에 걸면 벽지다. 각 섹션의 주장을 움직임으로 옮겼다.
|
||
|
||
| 섹션 | 주장 | 움직임 |
|
||
|---|---|---|
|
||
| 히어로 | 결정하지 않으면 기본값이 나온다 | 글자 ↑ / 배경 ↓ — **반대 방향**이라야 시차가 보인다 |
|
||
| 파이프라인 | 단계를 밟으면 결과가 나온다 | 단계명 **고정**, 산출물만 지연 |
|
||
| 사례 | 같은 스킬에서 다른 결과 | 두 판이 다른 속도 |
|
||
| 재료 | 표면은 한 겹이 아니다 | 타일이 서로 다른 깊이 |
|
||
| 프리플라이트 | 시간순 기록 | 뒤 항목일수록 늦게 |
|
||
| **수치** | 공개한 값은 검증 가능하다 | **없음 — 의도적 정지** |
|
||
|
||
마지막 줄이 결정이다. 숫자가 스크롤에 흔들리면 그 값도 흔들려 보인다.
|
||
|
||
## 배경 레이어 — 패럴랙스의 기준면
|
||
|
||
**배경이 단색이면 패럴랙스는 성립하지 않는다.** 앞의 것이 몇 px 어긋나도 비교할 대상이 없다.
|
||
히어로 말고는 기준면이 없어서 시차가 안 보였다.
|
||
|
||
배경 이미지를 사 오는 대신 **이 페이지가 이미 주장하는 것**을 그렸다.
|
||
|
||
| 레이어 | 무엇 | 어디 | 근거 |
|
||
|---|---|---|---|
|
||
기준면은 `BgPhoto` 컴포넌트가 만든다(아래 표).
|
||
|
||
**CSS 격자를 한동안 같이 깔았다가 걷어냈다.** 사진이 들어온 뒤로 배경이 두 겹이 되어
|
||
서로 경쟁했고, 격자는 배경이라기보다 **표**로 읽혔다. 배경도 한 섹션에 하나다.
|
||
|
||
한 층은 반드시 고정이다 — 파이프라인의 단계명은 움직이지 않는다.
|
||
전부 움직이면 기준이 없어 그냥 흔들리는 페이지가 된다.
|
||
|
||
### 패럴랙스를 "고정"으로 바꿨다
|
||
|
||
세 번 고친 뒤에도 티가 나지 않았다. 원인은 **방식 자체**였다 —
|
||
느리게 같이 움직이는 것으로는 원경이 안 된다. 속도만 다를 뿐 방향이 같기 때문이다.
|
||
|
||
**배경을 뷰포트에 고정한다.** `background-attachment: fixed` 는 iOS Safari 가 throttle 하므로
|
||
쓰지 않고, 섹션에 `clip-path: inset(0)` 을 걸어 **`position: fixed` 의 컨테이닝 블록**으로 만든다.
|
||
그러면 배경이 화면에 멈춘 채 그 섹션 안에서만 보인다.
|
||
|
||
실측: 스크롤 1000px 구간에서 배경의 `viewportTop` 이 계속 **0**.
|
||
|
||
대가가 둘 있고 둘 다 받아들였다.
|
||
|
||
- **조상에 `transform`·`filter`·`mask` 를 못 쓴다.** 그것들도 컨테이닝 블록을 만들어
|
||
고정을 깨뜨린다. 그래서 위아래 페이드 마스크를 포기하고 경계는 clip 으로 딱 자른다
|
||
- `prefers-reduced-motion` 에서는 `position: absolute` 로 되돌린다
|
||
|
||
**전부 고정하지는 않았다.** 원경과 중경을 나눴다.
|
||
|
||
| 섹션 | 모드 | 왜 |
|
||
|---|---|---|
|
||
| 재료 · 설치 · 프리플라이트 | `fixed` | 원경. 콘텐츠만 그 위를 지나간다 |
|
||
| 사례 | `drift` 0.4 | 두 결과물이 흐르는 자리라 배경도 같이 흐른다 |
|
||
| 조판 | `drift` 0.25 | **종이는 손에 닿는 거리다.** 원경으로 두면 거짓말이 된다 |
|
||
|
||
### three.js 씬 셋은 서로 다른 것이어야 한다
|
||
|
||
게이트 씬을 파이프라인으로 옮기면서 히어로에도 그대로 남겨뒀더니
|
||
**같은 그림이 페이지에 두 번** 나왔다. 히어로를 다른 것으로 바꿨다.
|
||
|
||
| 자리 | 씬 | 무엇 |
|
||
|---|---|---|
|
||
| 히어로 | `shader-plane` 흐름 필드 | 노이즈를 두 번 접어 빛이 유리를 지난 결을 만든다. 공간을 만드는 **재질** |
|
||
| 파이프라인 | `gate-scene` 유리판 여섯 장 | 스크롤하면 통과하고 그 단계가 켜진다. 의미를 가진 **주인공** |
|
||
| 재료 | `shader-plane` 스테이지 | SVG 필터 타일이 앉는 무대 |
|
||
|
||
히어로에는 무거운 씬을 임포트하지 않는다 — `three.md` §0 이 권하는 대로
|
||
**셰이더 플레인 하나**다. 파일 이름도 `hero-scene.ts` → `gate-scene.ts` 로 고쳤다.
|
||
이름이 실제 용도와 다르면 다음 사람이 히어로 것인 줄 알고 고친다.
|
||
|
||
캔버스 배경 위 본문 대비 실측(스크린샷 픽셀): 최악 **8.46:1**(가장 밝은 중앙 여백),
|
||
h1 자리 14.83:1.
|
||
|
||
> `readPixels` 로 재려다 전부 0 이 나왔다. `preserveDrawingBuffer: false` 면 버퍼가
|
||
> 비워져 있다. **WebGL 위 대비는 스크린샷 픽셀로 재라.**
|
||
|
||
### 게이트 씬을 파이프라인으로 옮겼다
|
||
|
||
히어로에서 뺐던 유리판 여섯 장을 여기로 되살렸다. **같은 씬인데 자막이 필요 없어졌다.**
|
||
|
||
| | 히어로에 뒀을 때 | 파이프라인으로 옮긴 뒤 |
|
||
|---|---|---|
|
||
| 옆에 있는 것 | 헤드라인과 CTA | **여섯 단계의 실제 이름** |
|
||
| 의미 전달 | `게이트 01 / 06` 판독기 — 자막 | 판을 통과하면 그 단계가 켜진다 |
|
||
|
||
캔버스는 `position: sticky; height: 100dvh` 로 화면에 머물고 섹션이 지나간다.
|
||
**진행도는 섹션 높이가 아니라 (섹션 높이 − 뷰포트 높이)로 잰다.**
|
||
섹션 높이로 나눴더니 캔버스가 사라진 뒤에도 진행이 남아 6단계 중 **3단계에서 멈췄다.**
|
||
그리고 기준 요소는 캔버스가 아니라 섹션이다 — sticky 캔버스는 자기 rect 가 변하지 않는다.
|
||
|
||
캔버스 위 텍스트 대비 실측 **14.2:1**(캔버스 불투명도 0.7).
|
||
|
||
### 패럴랙스가 세 번 실패한 경위
|
||
|
||
**1. 이동량이 모자랐다.** 여유를 28% 잡아놓고 `±46px`(총 92px)만 움직였다.
|
||
스크롤 1800px 구간에서 이동 비율 5% — 아무도 시차를 느끼지 못했다.
|
||
공식은 **이미지를 컨테이너보다 크게 만들고 그 초과분 전체를 움직이는 것**이다.
|
||
`depth 0.45` → 이미지 145%, 이동 `translateY(-31.03%)`. 같은 구간에서 **374px**.
|
||
|
||
**2. `overflow: hidden` 이 `view()` 타임라인을 죽이고 있었다.**
|
||
`overflow: hidden` 은 스크롤 컨테이너를 만들고, 그러면 자식의 `view()` 가 문서가 아니라
|
||
그 상자를 기준으로 잡힌다. 이미지는 상자 안에서 늘 같은 자리라 진행도가 고정된다.
|
||
**콘솔 에러도 없고 computed 값도 정상이라** 두 스크롤 위치에서 `transform` 을 재보고서야
|
||
이동 0px 인 것을 알았다. 잘라내는 일은 섹션의 `overflow: clip` 에 맡겼다 —
|
||
`clip` 은 스크롤 컨테이너를 만들지 않는다.
|
||
|
||
**3. `opacity` 로 눌러 이미지를 지웠다.** 어두운 광원 사진을 `opacity: 0.5` 로 깔면
|
||
검정도 빛도 같이 죽어 **있는지조차 보이지 않는다.** `mix-blend-mode: screen` 으로 바꿨다 —
|
||
검정은 투명해지고 밝은 곳만 더해지므로 불투명도를 0.85~1.0 으로 유지해도 본문 대비가 안전하다.
|
||
밝은 종이만 `normal` 이다.
|
||
|
||
**생성 프롬프트에도 같은 함정이 있었다.** "본문 뒤에 깔리니 어둡게"를 너무 강하게 넣어서
|
||
안전한 이미지가 아니라 **없는 이미지**를 만들었다. 대비는 합성 방식으로 풀 문제였다.
|
||
|
||
### 배경 사진 넉 장 (gpt-image-2 로 생성)
|
||
|
||
CSS 그라디언트 광원만으로는 기준면이 약해서, 굴절과 그레인이 실제로 들어 있는 이미지를 넣었다.
|
||
Codex CLI 의 내장 `image_gen`(gpt-image-2)을 ChatGPT 구독 인증으로 호출해 만들었다 — 스톡 이미지가 아니다.
|
||
|
||
**섹션마다 다른 그림을 쓴다.** 같은 배경을 전부에 깔면 그건 벽지고,
|
||
모션에 적용한 규칙("섹션마다 다른 말을 해야 한다")이 배경에서만 면제될 이유가 없다.
|
||
|
||
| 이미지 | 무엇 | 어디 | 그 섹션의 주장 |
|
||
|---|---|---|---|
|
||
| `bg-refraction` | 겹친 유리판을 통과한 빛 | 재료 · 설치(flip) | 표면은 한 겹이 아니다 |
|
||
| `bg-split` | 한 빔이 둘로 갈라진다 | 사례 | 같은 파이프라인, 다른 결과 |
|
||
| `bg-align` | 정렬선 여럿 중 **하나가 어긋나 끊겨 있다** | 프리플라이트 | 자기 결과물을 남의 것처럼 본다 |
|
||
| `bg-paper` | 무광 인쇄용지, 섬유와 데클 엣지 | 조판(밝은 밴드) | 한글이 종이 위에 있어야 한다 |
|
||
|
||
**격자를 쓰는 파이프라인·수치에는 사진을 넣지 않았다.** 거기서는 격자가 그 섹션의 주장이다.
|
||
규칙·히어로도 그대로 뒀다 — 히어로에는 three.js 가 있고, 규칙 목록은 담백해야 한다.
|
||
|
||
프롬프트는 **이 페이지의 토큰 값을 그대로** 넣었다:
|
||
|
||
- 바탕 `#0B0E12` — 순흑도 네이비도 아니게
|
||
- 광원 **둘만**: 좌상단 딥 틸 `#4FD6C4`, 우하단 채도 낮은 파랑 `#3D7BD6`.
|
||
보라·마젠타·바이올렛·무지개·따뜻한 색 **금지** (슬롭 지문)
|
||
- 프로스티드 글래스 판이 거의 옆에서 보이게 겹쳐, 가장자리만 얇게 빛나고 가운데는 거의 안 보이게
|
||
- **대비를 아주 낮고 고르게** — "이 이미지는 흰 본문 텍스트 뒤에 깔린다. 활자와 경쟁할 만큼
|
||
밝은 곳이 있으면 안 된다. 중앙 띠는 조용하고 어둡게, 밝기는 모서리 쪽에"
|
||
- 미세 필름 그레인, 밴딩 없이
|
||
- 금지: 사물·로고·글자·숫자·사람·렌즈 플레어·보케·딱딱한 도형·비네트
|
||
|
||
`components/BgPhoto.astro` 로 감쌌다. CSS `background-image` 가 아니라 `<Image loading="lazy">` 를
|
||
절대배치한다 — background-image 로 깔면 **항상 받고 최적화도 안 된다.**
|
||
같은 이미지를 설치 섹션에서는 `flip` 으로 좌우 반전해 같은 그림으로 읽히지 않게 했다.
|
||
|
||
소스 WebP: `refraction` 37KB · `split` 14KB · `align` 11KB · `paper` 112KB
|
||
(종이만 큰 이유는 섬유 질감이 고주파라 압축이 안 붙기 때문이다)
|
||
|
||
빌드 결과 12개 파일 합계 **276KB**. 실제로는 뷰포트에 맞는 크기 하나씩만,
|
||
그것도 `loading="lazy"` 로 받는다.
|
||
|
||
**절대배치 배경이 그리드 안에서 561px 로 잘렸다.** `.section` 은 subgrid 이고
|
||
`.section > * { grid-column: main; }` 이 걸려 있는데, 절대배치 요소라도 그리드 영역이
|
||
잡히면 **그 영역이 컨테이닝 블록**이 된다. 그래서 배경이 화면 가운데 561px 띠로만 깔렸고,
|
||
`overflow: visible` 이라 위아래 14% 여유분이 이웃 섹션 위로 넘쳐 조판 섹션의 종이가
|
||
프리플라이트 하단에 밝은 띠로 나타났다.
|
||
`.bg-photo { grid-column: full }` + `.has-bg { overflow: clip }` 으로 고쳤다.
|
||
**`position: absolute` 는 그리드에서 배치를 면제해주지 않는다.**
|
||
|
||
**codex 0.147.0 메모** — `codex-image` 스킬의 `extract_imagegen.py` 는 rollout JSONL 의
|
||
`image_generation_call` base64 를 찾는데, 0.147.0 은 다시 **파일**로 저장한다
|
||
(`~/.codex/generated_images/<sid>/exec-*.png`, 이벤트는 `custom_tool_call` + `image_generation_end`).
|
||
그래서 추출이 fail-loud 로 죽는다 — 이미지는 실제로 생성돼 있다.
|
||
그리고 `$imagegen` 은 **프롬프트 맨 앞**에 있어야 내장 도구가 발동한다.
|
||
|
||
## 그리드의 `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
|
||
|
||
토큰이 없어서 깨져 있던 세 축을 채웠다.
|
||
|
||
- **웨이트 3종** — `400 / 500 / 600`. 이전에는 코드 곳곳에 `500·560·600·620·660` 이 흩어져
|
||
**6종**이었고 셋은 토큰에 없는 값이었다. R1 모바일 실측은 `400` 이 832회, 나머지 12회다
|
||
- **컨트롤 치수** — `--control-h: 44px` · `--control-h-sm: 36px` · `--control-pad-x`.
|
||
버튼처럼 생긴 것은 전부 `.btn` + `.btn-solid` / `.btn-quiet` 를 쓴다.
|
||
이전에는 두 버튼이 높이 36 vs 44, 패딩 16 vs 8, 테두리 1px vs 0, 웨이트 400 vs 500 이었다
|
||
- **`--header-h`** — 떠 있는 헤더의 높이. 간격 토큰을 `clamp()` 로 바꾸는 순간 관계가 끊겨
|
||
h1 이 헤더 뒤로 들어갔다. **값이 아니라 관계를 토큰으로 둔다**
|
||
- **반응형 간격** — `--space-5~8` 과 `--gutter` 를 `clamp()` 로. 고정이면 모바일에서
|
||
섹션 상하 112px 이 그대로 들어가 화면의 26% 를 여백이 먹는다
|
||
- **스크롤바** — `scrollbar-color` 지정. 기본값이면 어두운 페이지 옆에 OS 색 밝은 막대가 선다
|
||
|
||
## 모바일
|
||
|
||
- 좌우 여백 24px → **32px** (R1 모바일 실측값)
|
||
- 섹션 상하 112px → **56px**
|
||
- 헤더에 섹션 앵커 4개 추가. 900px 미만에서는 `<details>` 기반 메뉴로 접힌다 —
|
||
JS 0바이트이고 열림 상태·Esc·포커스 순서·`aria-expanded` 를 브라우저가 맞게 한다
|
||
- 390px 실렌더: 가로 스크롤 0, 대비 실패 0, 웨이트 3종, radius 2종
|
||
|
||
## 히어로 3D 의 형태 결정
|
||
|
||
- 판 **15 × 6** (액자 비율 2.5:1 과 일치). 첫 판이 화면을 완전히 덮어 이미 첫 게이트 안에 들어와 있다
|
||
- 판이 화면보다 좁으면 통과하는 내내 좌우 테두리가 남아 **세로 기둥 두 개**로 보인다. 실제로 그렇게 보였다
|
||
- 판 높이가 시야보다 작으면 위아래 테두리가 액자 경계에 겹쳐 **이중선**이 된다
|
||
- 어긋남(sway)은 `0.16 + 0.05i` 로 아주 작게. 키우면 '통과하는 문'이 아니라 **쌓아둔 사각형**이 된다.
|
||
중심축이 살아야 원근이 터널로 읽힌다
|
||
- `GATE_GAP` 6.4, 카메라 시작 z 7.2
|
||
|
||
## 다음 실행이 해야 할 일
|
||
|
||
1. **배포 후 Lighthouse 로 첫 인터랙션·LCP·CLS 를 재고 "미측정" 을 채운다.** 채우기 전까지 비워둔다
|
||
2. 폰트를 CDN 의존에서 자체 호스팅으로 옮길지 판단한다 (현재 jsDelivr · Fontshare · Google Fonts 3곳)
|
||
3. OG 이미지가 없다
|
||
4. 섹션이 9개, 총 높이 11,197px 이다. 스크롤 보상이 충분한지 재검토할 여지가 있다
|
||
|
||
---
|
||
|
||
`2026-08-20` · 2판. 0~6단계 전체 경로 재주행.
|