diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..41def0f --- /dev/null +++ b/.env.example @@ -0,0 +1,62 @@ +# designpaca 배포 환경 +# +# 이 파일은 커밋된다. 실제 값은 `.env` 에 넣어라 (.gitignore 에 있다). +# cp .env.example .env +# +# 채워진 값은 실측으로 확인한 것이고, 빈 값은 사람이 발급해야 하는 것이다. + +# ── NAS ────────────────────────────────────────────────────── +# Synology DS420+ · Linux 4.4.302+ · Tailscale 로 접근한다. +# SSH 키(id_ed25519)로 이미 붙는다. 비밀번호가 필요 없다. +NAS_HOST=100.116.83.60 +NAS_HOST_LAN=192.168.0.39 +NAS_USER=yunchan + +# ── Forgejo ────────────────────────────────────────────────── +# 컨테이너 kd-forgejo (codeberg.org/forgejo/forgejo:11) + postgres:16-alpine +# 웹 3000 → Cloudflare 프록시로 https://git.chanpaca.net +# git SSH 2222 → 컨테이너 내부 22 +# +# 주의: git.chanpaca.net 은 Cloudflare 뒤라 22/2222 가 막혀 있다. +# git push 는 도메인이 아니라 Tailscale 주소로 해야 한다. +FORGEJO_URL=https://git.chanpaca.net +FORGEJO_SSH_HOST=100.116.83.60 +FORGEJO_SSH_PORT=2222 +FORGEJO_CONTAINER=kd-forgejo + +# 등록된 SSH 키 이름: encep-win (yunchan 계정) +FORGEJO_OWNER=yunchan +FORGEJO_REPO=designpaca + +# 완성되는 remote URL: +# ssh://git@100.116.83.60:2222/yunchan/designpaca.git + +# ── 사람이 발급해야 하는 것 ────────────────────────────────── +# 아래는 전부 비워둔다. 값을 아는 사람만 채울 수 있다. + +# Forgejo API 토큰. 저장소 생성·시크릿 등록에 쓴다. +# git.chanpaca.net → 설정 → 애플리케이션 → 토큰 생성 +# 권한: repository(write), user(read) +FORGEJO_TOKEN= + +# npmjs 배포 토큰 (Automation 타입). +# npmjs.com → Access Tokens → Generate → Automation +NPM_TOKEN= + +# Forgejo 사설 npm 레지스트리 미러용 토큰. +# FORGEJO_TOKEN 과 같은 값을 써도 되지만, 권한을 나누려면 따로 발급한다. +FORGEJO_NPM_TOKEN= + +# Forgejo 릴리스 생성용 토큰. 워크플로가 draft 릴리스를 만든다. +RELEASE_TOKEN= + +# Cloudflare Pages 배포. +# dash.cloudflare.com → My Profile → API Tokens +# 권한: Account / Cloudflare Pages / Edit +CF_API_TOKEN= +CF_ACCOUNT_ID= + +# Cloudflare Pages 프로젝트 이름 (아직 만들지 않았다) +CF_PAGES_PROJECT= +# 연결할 도메인 +SITE_DOMAIN=designpaca.chanpaca.net diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..20b5441 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# 배포되는 텍스트는 어느 OS 에서 체크아웃하든 LF 다. +# SKILL.md 의 YAML 프론트매터가 CRLF 에서 파서에 따라 깨진다. +* text=auto eol=lf +*.png binary +*.webp binary +*.ico binary diff --git a/apps/site/design.md b/apps/site/design.md index fe35366..b513a46 100644 --- a/apps/site/design.md +++ b/apps/site/design.md @@ -189,6 +189,33 @@ WebGL 게이트: `prefers-reduced-motion` 이면 **아예 시작하지 않는다 `ResizeObserver` 로 캔버스를 따라가게 했다. `window.resize` 만 듣던 동안 히어로 높이 변화를 놓쳐 배경이 화면의 3분의 2만 덮고 아래가 검게 비었다. +## 패럴랙스 지도 (실측) + +깊이가 다른 층을 겹쳐 시차를 만든다. **한 층은 반드시 고정이다** — +전부 움직이면 기준이 없어 그냥 흔들리는 페이지가 된다. + +| 자리 | 층 | 방식 | 실측 이동 | +|---|---|---|---| +| 히어로 배경 | 셰이더 흐름 필드 | `scroll(root)` | **+56px** ↓ | +| 히어로 글자 | | `scroll(root)` | **−88px** ↑ (반대 방향) | +| 파이프라인 | 게이트 씬(three.js) | `sticky` + 카메라 | 6단계를 통과 | +| ㅤ└ 단계명 | | — | **0** 고정 | +| ㅤ└ 산출물 | | `view()` | ±14px | +| 재료 · 설치 · 프리플라이트 | 배경 사진 | `fixed` | **0 — 원경** | +| 사례 | 배경 사진 | `drift` 0.4 | 이미지 높이의 28.6% | +| 조판 | 종이 | `drift` 0.25 | −122px (근경) | +| 재료 타일 | | `view()` | ±26 / ±58px | +| 프리플라이트 항목 | | `view()` | 6 → 10px (뒤일수록 늦게) | +| **수치** | | **없음** | 의도적 정지 | + +세 가지 깊이가 다 있다 — **원경**(fixed) · **중경**(drift) · **근경**(콘텐츠와 함께). +종이를 원경으로 두지 않은 이유는 손에 닿는 거리의 재질이기 때문이다. + +모바일(390px)에서도 `fixed` 가 유지된다. `background-attachment: fixed` 를 안 쓴 덕이다. + +`prefers-reduced-motion` 에서는 게이트 씬이 아예 시작하지 않고, 고정 배경은 +`position: absolute` 로 흐름에 돌아가며, 모든 `view()` 애니메이션이 꺼진다. + ## 모션 문법 — 섹션마다 다른 말 같은 패럴랙스를 전부에 걸면 벽지다. 각 섹션의 주장을 움직임으로 옮겼다. @@ -213,14 +240,185 @@ WebGL 게이트: `prefers-reduced-motion` 이면 **아예 시작하지 않는다 | 레이어 | 무엇 | 어디 | 근거 | |---|---|---|---| -| `.bg-grid` | 간격 눈금 격자. 셀이 `--grid-cell = --space-6` | 파이프라인 · 수치 | "모든 값이 토큰에서 나온다"가 배경에서도 성립해야 한다. 계기판의 눈금이기도 하다 | -| `.bg-glow` | 광원 블롭 두 개. 섹션마다 `--glow-x` 로 자리를 옮긴다 | 재료 · 설치 | 히어로의 빛이 페이지를 관통해 마지막 CTA 에서 돌아온다 | +기준면은 `BgPhoto` 컴포넌트가 만든다(아래 표). -격자는 가장자리를 마스크로 지운다 — 화면 끝까지 가면 배경이 아니라 **표**가 된다. -배경이 가장 적게 움직인다. 앞의 것보다 크게 움직이면 앞으로 튀어나와 보인다. +**CSS 격자를 한동안 같이 깔았다가 걷어냈다.** 사진이 들어온 뒤로 배경이 두 겹이 되어 +서로 경쟁했고, 격자는 배경이라기보다 **표**로 읽혔다. 배경도 한 섹션에 하나다. -**실측 3층** (스크롤 1000 → 1600): 격자 **+14.9px** · 산출물 **-14px** · 단계명 **0**. -한 층은 반드시 고정이다. 전부 움직이면 기준이 없어 그냥 흔들리는 페이지가 된다. +한 층은 반드시 고정이다 — 파이프라인의 단계명은 움직이지 않는다. +전부 움직이면 기준이 없어 그냥 흔들리는 페이지가 된다. + +### 패럴랙스를 "고정"으로 바꿨다 + +세 번 고친 뒤에도 티가 나지 않았다. 원인은 **방식 자체**였다 — +느리게 같이 움직이는 것으로는 원경이 안 된다. 속도만 다를 뿐 방향이 같기 때문이다. + +**배경을 뷰포트에 고정한다.** `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 | **종이는 손에 닿는 거리다.** 원경으로 두면 거짓말이 된다 | + +### 유리 테두리는 conic 이 아니라 linear 로 돈다 + +헤더의 헤어라인이 **변마다 끊겨** 보였다. `conic-gradient` 로 만들었기 때문이다. +conic 은 중심에서 각도로 도는데, 1180×54 처럼 납작한 형태에서는 각도가 급변해서 +긴 변에서 색이 거의 안 변하고 짧은 변에서 한꺼번에 바뀐다. 실제로 아래쪽만 절반 밝았다. + +`linear-gradient(100deg, …)` 로 바꾸면 네 변이 같은 그라디언트를 공유해 +**테두리를 따라 이어진다.** 마스크로 1px 만 남기는 구조는 그대로다. + +그리고 아주 얕은 글로우를 얹었다 — `0 0 22px -8px` 에 강조색을 42% 섞었다. +유리가 차갑게만 보이지 않게 하는 정도이고, 밝은 밴드에서는 흰빛 대신 +그림자로 뒤집는다(`--glass-rim`·`--glass-bloom` 을 `.band-inverse` 에서 재정의). + +### 테두리 광을 회전시켰다 + +정지한 헤어라인 위에 **도는 빛**을 얹었다. `conic` 의 각도를 `@property` 로 돌리면 +매 프레임 배경을 다시 칠하므로(하드 게이트 #8), **정사각형 원뿔 레이어를 마스크 안에서 +`rotate` 시킨다.** 움직이는 것은 transform 뿐이다. + +`aspect-ratio: 1` 로 정사각형을 유지해야 회전 중 밝기가 일정하고, +`width: 200%` 로 크게 잡아야 1178×52 짜리 헤더에서도 모서리까지 닿는다. 주기 11초. + +### three.js 모바일 대응 + +**세로 화면에서 게이트가 통째로 안 보이고 있었다.** 수직 시야는 화면 비율과 무관하지만 +수평 시야는 aspect 에 비례한다 — 데스크톱(2.5:1)에서 정한 판 폭 15 가 +세로 화면(0.45:1)의 수평 시야 2.5 에 비하면 **여섯 배**라, 첫 판이 화면을 완전히 덮었다. + +`fit()` 에서 `camera.aspect` 를 갱신할 때 판의 `scale.x` 도 같이 잡는다. + +그리고 저사양 판정을 고쳤다. `hardwareConcurrency <= 4` 만 보면 **요즘 폰이 전부 +고사양으로 잡힌다**(8코어가 흔하다). `innerWidth < 760` 을 함께 본다 — +좁은 화면은 대개 배터리로 돌고 열이 더 난다. DPR 도 1.5 → **1.25**, +캔버스 불투명도도 0.7 → **0.45**(세로에서는 화면 전체를 덮으므로). + +### 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` 가 아니라 `` 를 +절대배치한다 — 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//exec-*.png`, 이벤트는 `custom_tool_call` + `image_generation_end`). +그래서 추출이 fail-loud 로 죽는다 — 이미지는 실제로 생성돼 있다. +그리고 `$imagegen` 은 **프롬프트 맨 앞**에 있어야 내장 도구가 발동한다. ## 그리드의 `wide` 트랙을 좁은 화면에서 닫는다 diff --git a/apps/site/src/assets/bg/bg-align.webp b/apps/site/src/assets/bg/bg-align.webp new file mode 100644 index 0000000..6283669 Binary files /dev/null and b/apps/site/src/assets/bg/bg-align.webp differ diff --git a/apps/site/src/assets/bg/bg-paper.webp b/apps/site/src/assets/bg/bg-paper.webp new file mode 100644 index 0000000..5fae845 Binary files /dev/null and b/apps/site/src/assets/bg/bg-paper.webp differ diff --git a/apps/site/src/assets/bg/bg-refraction.webp b/apps/site/src/assets/bg/bg-refraction.webp new file mode 100644 index 0000000..b48ccaa Binary files /dev/null and b/apps/site/src/assets/bg/bg-refraction.webp differ diff --git a/apps/site/src/assets/bg/bg-split.webp b/apps/site/src/assets/bg/bg-split.webp new file mode 100644 index 0000000..8354953 Binary files /dev/null and b/apps/site/src/assets/bg/bg-split.webp differ diff --git a/apps/site/src/components/BgPhoto.astro b/apps/site/src/components/BgPhoto.astro new file mode 100644 index 0000000..a1ee278 --- /dev/null +++ b/apps/site/src/components/BgPhoto.astro @@ -0,0 +1,138 @@ +--- +import { Image } from "astro:assets"; +import refraction from "../assets/bg/bg-refraction.webp"; +import paper from "../assets/bg/bg-paper.webp"; +import split from "../assets/bg/bg-split.webp"; +import align from "../assets/bg/bg-align.webp"; + +/** + * 배경은 섹션마다 다른 말을 해야 한다. 같은 그림을 전부에 깔면 그건 벽지다. + * 넷 다 gpt-image-2 로 만들었고, 프롬프트에 이 페이지의 토큰 값을 그대로 넣었다. + */ +const SOURCES = { refraction, paper, split, align } as const; + +interface Props { + variant?: keyof typeof SOURCES; + /** + * `fixed` — 뷰포트에 고정. 콘텐츠만 그 위를 지나간다. 무한히 먼 원경. + * `drift` — 스크롤보다 느리게 따라온다. 중경. 종이처럼 가까운 재질에 쓴다. + */ + mode?: "fixed" | "drift"; + /** drift 전용. 이미지를 컨테이너보다 이 비율만큼 키우고 그 초과분 전체를 움직인다 */ + depth?: number; + opacity?: number; + flip?: boolean; +} +const { + variant = "refraction", + mode = "fixed", + depth = 0.35, + opacity = 0.9, + flip = false, +} = Astro.props; +const src = SOURCES[variant]; + +// 밝은 종이만 그대로 얹고, 어두운 광원 사진은 더하기 합성한다. +const additive = variant !== "paper"; + +const scale = 1 + depth; +const shift = -(depth / scale) * 100; +--- + +{/* + 패럴랙스의 원형은 "배경이 고정되어 원경처럼 보이는 것"이다. + 느리게 같이 움직이는 것으로는 그 인상이 안 나온다 — 속도만 다를 뿐 같은 방향이다. + + `background-attachment: fixed` 는 쓰지 않는다. iOS Safari 가 GPU 메모리 때문에 + 그것을 throttle 해서 스크롤 중 배경이 튀거나 리셋된다. + 대신 부모에 `clip-path: inset(0)` 을 걸어 **fixed 의 컨테이닝 블록**으로 만든다. + 그러면 배경은 뷰포트에 고정되면서 그 섹션 영역에서만 보인다. + + 주의: 조상에 `transform`·`filter`·`mask` 가 있으면 그게 컨테이닝 블록이 되어 + 이 기법이 통째로 깨진다. 그래서 위아래 페이드 마스크를 포기했다 — + 마스크를 걸면 고정이 사라진다. 경계는 clip 으로 딱 자른다. +*/} + + + diff --git a/apps/site/src/components/Hero.astro b/apps/site/src/components/Hero.astro index 7f01df0..c625e94 100644 --- a/apps/site/src/components/Hero.astro +++ b/apps/site/src/components/Hero.astro @@ -162,12 +162,70 @@ const { c, ui } = Astro.props; diff --git a/apps/site/src/components/Install.astro b/apps/site/src/components/Install.astro index 5568e42..9b19919 100644 --- a/apps/site/src/components/Install.astro +++ b/apps/site/src/components/Install.astro @@ -1,5 +1,6 @@ --- import CopyCommand from "./CopyCommand.astro"; +import BgPhoto from "./BgPhoto.astro"; import type { Content } from "../i18n/content"; interface Props { c: Content["install"]; ui: Content["ui"]; } @@ -7,7 +8,8 @@ const { c, ui } = Astro.props; --- -
+
+

{c.h2}

@@ -39,10 +41,25 @@ const { c, ui } = Astro.props;
+ + {/* 푸터를 없앴으므로 출처와 라이선스는 여기 한 줄로 남는다. + 링크 하나 없이 배포하면 사용자가 소스를 찾을 방법이 사라진다. */} +

+ {c.colophon.repo} + {c.colophon.license} + {c.colophon.made} +

diff --git a/apps/site/src/components/Materials.astro b/apps/site/src/components/Materials.astro index e4d902d..2dfd10b 100644 --- a/apps/site/src/components/Materials.astro +++ b/apps/site/src/components/Materials.astro @@ -1,4 +1,5 @@ --- +import BgPhoto from "./BgPhoto.astro"; import type { Content } from "../i18n/content"; interface Props { c: Content["materials"]; } @@ -10,11 +11,13 @@ const { c } = Astro.props; 배경은 three.js 셰이더 플레인, 전경은 SVG 필터를 실제로 먹인 타일 3장. 둘 다 설명이 아니라 지금 이 화면에서 돌고 있는 것이다. */} -
+
+

{c.h2}

{c.lead}

+ {/* GPU 가 만드는 재질. WebGL 이 없으면 위 그라디언트가 그대로 결과물이다 */} diff --git a/apps/site/src/components/Metrics.astro b/apps/site/src/components/Metrics.astro index 64aac25..4957d1e 100644 --- a/apps/site/src/components/Metrics.astro +++ b/apps/site/src/components/Metrics.astro @@ -19,7 +19,7 @@ const rest = c.items.filter((m) => !m.feature); 흔들리면 그 값도 흔들리는 것처럼 읽힌다. 계기판의 바늘은 떨지 않는다. 움직임이 의미를 만드는 자리가 있으면, 움직이지 않는 것이 의미인 자리도 있다. */} -
+

{c.eyebrow}

{c.h2}

diff --git a/apps/site/src/components/Pipeline.astro b/apps/site/src/components/Pipeline.astro index b65203b..239c2ce 100644 --- a/apps/site/src/components/Pipeline.astro +++ b/apps/site/src/components/Pipeline.astro @@ -14,7 +14,15 @@ const { c } = Astro.props; ③ 의미 —
    — 화면에 번호는 없지만 보조기술은 "6개 중 n번째"로 읽는다 role="list" 는 list-style:none 일 때 Safari/VoiceOver 가 목록 의미를 버리는 것을 되돌린다 */} -
    +
    + {/* 히어로에 있던 게이트 씬을 여기로 옮겼다. + 히어로에서는 그 은유를 판독기가 자막으로 설명해야 했다 — 그림이 스스로 + 말하지 못한다는 뜻이다. 여기서는 옆에 실제 여섯 단계가 적혀 있고, + 판을 통과할 때 그 단계가 켜진다. 자막이 아니라 콘텐츠가 반응한다. */} + {/* 제목·리드·브리프·스파인을 한 컨테이너에 담는다. 섹션 기본 그리드에 두면 제목은 main 칼럼의 음수 마진으로 x=371, 리드는 x=435, 스파인은 x=143 에 서서 정렬선이 셋이 된다. */} @@ -30,8 +38,8 @@ const { c } = Astro.props;
      { - c.stages.map((s) => ( -
    1. + c.stages.map((s, i) => ( +
    2. {s.name}

      {s.does}

      @@ -54,6 +62,48 @@ const { c } = Astro.props; .brief { max-width: var(--measure); } #pipeline-h { max-width: 24ch; } + /* ---- 게이트 배경 ------------------------------------------ + sticky 로 화면에 머물고 음수 마진으로 레이아웃 자리는 차지하지 않는다. + 섹션이 지나가는 동안 카메라가 판 여섯 장을 통과한다. */ + .gate-bg { + position: sticky; + top: 0; + height: 100dvh; + margin-bottom: -100dvh; + z-index: -1; + pointer-events: none; + grid-column: full; + } + .gate-fallback, + .gate-canvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + display: block; + } + .gate-fallback { + background: + radial-gradient(46% 54% at 28% 34%, + color-mix(in oklab, var(--accent) 20%, transparent), transparent 70%), + radial-gradient(42% 50% at 76% 68%, + color-mix(in oklab, var(--glow-2) 18%, transparent), transparent 68%); + } + .gate-canvas { opacity: 0; transition: opacity 1s var(--ease-soft); } + .gate-canvas.is-live { opacity: 0.7; } + /* 세로 화면에서는 캔버스가 화면 전체를 덮는다. 같은 밝기면 본문이 묻힌다 */ + @media (max-width: 760px) { + .gate-canvas.is-live { opacity: 0.45; } + } + + /* 통과한 단계가 켜진다. 3D 와 텍스트를 잇는 유일한 연결이다 */ + .stop { transition: opacity var(--dur-quick) var(--ease-out); } + .stop.is-active .stop-name { color: var(--accent); } + .stop.is-active::before { + background: var(--accent); + box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent) 22%, transparent); + } + /* 스파인은 페이지의 본체다. main 칼럼(561px)에 가두면 페이지에서 가장 긴 섹션이 가장 좁아진다. R1(zed.dev) 실측: 컨테이너 1120px 과 전체폭이 교대한다. */ @@ -173,3 +223,28 @@ const { c } = Astro.props; 이 섹션은 페이지의 본체다. 콘텐츠를 걸고 하는 연출은 하지 않는다. */ + + diff --git a/apps/site/src/components/Preflight.astro b/apps/site/src/components/Preflight.astro index 825f33d..0da6a2c 100644 --- a/apps/site/src/components/Preflight.astro +++ b/apps/site/src/components/Preflight.astro @@ -1,4 +1,5 @@ --- +import BgPhoto from "./BgPhoto.astro"; import type { Content } from "../i18n/content"; interface Props { c: Content["preflight"]; } @@ -10,7 +11,8 @@ const { c } = Astro.props; 걸린 것과 고친 것이 한 줄에서 이어진다. 상자에 담지 않는다. 왼쪽 세로선이 로그라는 형식을 만든다. */} -
      +
      +

      {c.h2}

      {c.lead}

      diff --git a/apps/site/src/components/Showcase.astro b/apps/site/src/components/Showcase.astro index cb9d4fd..ae11738 100644 --- a/apps/site/src/components/Showcase.astro +++ b/apps/site/src/components/Showcase.astro @@ -2,6 +2,7 @@ import { Image } from "astro:assets"; import shotCoffee from "../assets/shots/shot-coffee.png"; import shotSaas from "../assets/shots/shot-saas.png"; +import BgPhoto from "./BgPhoto.astro"; import type { Content } from "../i18n/content"; interface Props { c: Content["showcase"]; } @@ -10,7 +11,8 @@ const shots = [shotCoffee, shotSaas]; --- {/* 레이아웃 패밀리: Z축 캐스케이드. 두 판이 서로 겹치고 각도가 다르다 */} -
      +
      +

      {c.h2}

      {c.lead}

      diff --git a/apps/site/src/components/Typography.astro b/apps/site/src/components/Typography.astro index 7512ac3..f73d68a 100644 --- a/apps/site/src/components/Typography.astro +++ b/apps/site/src/components/Typography.astro @@ -1,4 +1,5 @@ --- +import BgPhoto from "./BgPhoto.astro"; import type { Content } from "../i18n/content"; interface Props { @@ -7,7 +8,8 @@ interface Props { const { c } = Astro.props; --- -
      +
      +

      {c.eyebrow}

      {c.h2}

      {c.lead}

      diff --git a/apps/site/src/i18n/content.ts b/apps/site/src/i18n/content.ts index fc9293a..385d425 100644 --- a/apps/site/src/i18n/content.ts +++ b/apps/site/src/i18n/content.ts @@ -86,8 +86,8 @@ export interface Content { command: string; targets: { name: string; path: string }[]; commands: { cmd: string; what: string }[]; + colophon: { made: string; repo: string; license: string }; }; - footer: { made: string; repo: string; license: string }; nav: { items: { label: string; href: string }[]; menu: string }; ui: { copy: string; copied: string }; } @@ -377,11 +377,11 @@ export const ko: Content = { { cmd: "npx designpaca doctor", what: "설치 상태와 드리프트 진단" }, { cmd: "npx designpaca uninstall", what: "매니페스트 기반 정확한 제거" }, ], - }, - footer: { - made: "이 페이지는 designpaca 로 만들어졌다.", - repo: "git.chanpaca.net/yunchan/designpaca", - license: "MIT", + colophon: { + made: "designpaca 로 만들었다", + repo: "git.chanpaca.net/yunchan/designpaca", + license: "MIT", + }, }, nav: { items: [ @@ -612,11 +612,11 @@ export const en: Content = { { cmd: "npx designpaca doctor", what: "Diagnose install state and drift" }, { cmd: "npx designpaca uninstall", what: "Exact removal, driven by the manifest" }, ], - }, - footer: { - made: "This page was built with designpaca.", - repo: "git.chanpaca.net/yunchan/designpaca", - license: "MIT", + colophon: { + made: "Built with designpaca", + repo: "git.chanpaca.net/yunchan/designpaca", + license: "MIT", + }, }, nav: { items: [ diff --git a/apps/site/src/layouts/Base.astro b/apps/site/src/layouts/Base.astro index 2049791..200a643 100644 --- a/apps/site/src/layouts/Base.astro +++ b/apps/site/src/layouts/Base.astro @@ -87,6 +87,7 @@ const FAVICON =