[fix] landing: 모바일 대응 — 글리프 배율·밴드 리맵·헤더 겹침

- 글리프 크기를 화면 폭에서 파생. 격자 간격은 dx × width 로 폭에 비례하는데
  글리프가 CSS px 고정이라 375px 에서 셀(7.7px)보다 커져(22px) 서로 겹쳐 덩어리가 됐다.
- 좁은 화면(<640px)에서는 밴드를 링 뒤로 내리고 군집 라벨을 접는다.
  모바일은 CTA 가 세로로 쌓여 세로 예산이 부족하고, 375px 에서 3군집을 나란히 두면
  군집당 100px 남짓이라 어차피 판독이 안 된다. 의미는 순환 링이 담는다.
- 히어로 상단 여백을 vh 대신 모바일 고정값으로. 헤더는 고정 높이라
  pt-[7vh](모바일 57px)로는 헤더(약 70px)를 피할 수 없어 헤드라인이 겹쳤다.
- 데스크톱 상단 여백 복원(14vh)과 밴드 재배치(0.575/0.155), 링 290px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Haewon Kam 2026-07-31 18:27:02 +09:00
parent 87b896fc2f
commit 5917942fe3
2 changed files with 22 additions and 10 deletions

View File

@ -29,7 +29,7 @@ export function HeroDataFlow() {
{/* 분류 단계에서만 뜨는 무리 라벨. 아이콘만으로는 "무엇이" 분류됐는지 절반만 전달된다.
좌표는 캔버스가 쓰는 값을 그대로 받아 써서 둘이 어긋날 일이 없다. */}
<div aria-hidden className="absolute inset-0 pointer-events-none z-10">
<div aria-hidden className="absolute inset-0 pointer-events-none z-10 hidden md:block">
{CLUSTER_ANCHORS.map((anchor) => (
<span
key={anchor.key}
@ -44,7 +44,7 @@ export function HeroDataFlow() {
</div>
{/* 카피는 상단 55%, 데이터 흐름은 하단 밴드. 둘이 겹치지 않게 층을 나눈다. */}
<div className="relative z-10 max-w-4xl mx-auto px-6 w-full text-center pt-[7vh]">
<div className="relative z-10 max-w-4xl mx-auto px-6 w-full text-center pt-[104px] md:pt-[14vh]">
{/* 히어로 카피에는 진입 애니메이션을 걸지 않는다.
여기가 LCP 요소라 opacity:0 으로 시작하면 하이드레이션 전까지 첫 화면이 비고,
광고 유입처럼 JS 가 느린 환경에서 그대로 손실이 된다. 모션은 배경 캔버스가 맡는다. */}
@ -73,8 +73,8 @@ export function HeroDataFlow() {
</div>
{/* 순환 링 — 정원. 점선 궤도만 돌고 마디·글자는 고정이다. */}
<div className="relative z-10 mt-auto flex justify-center pb-6">
<OrbitRing phase={phase} progressRef={progressRef} className="w-[300px] md:w-[340px] h-auto" />
<div className="relative z-10 mt-auto flex justify-center pb-6 md:pb-6">
<OrbitRing phase={phase} progressRef={progressRef} className="w-[250px] md:w-[290px] h-auto" />
</div>
</section>
)

View File

@ -58,8 +58,8 @@ export type DataFlowPhase = (typeof PHASES)[number]["key"]
/* 구도는 화면 하단 밴드에 앉힌다. 헤드라인·CTA 는 그 위에 서고, 데이터는 무대 바닥을
흐르는 그림. 아래 목표 좌표는 전부 밴드 기준 지역 좌표(0..1)로 쓰고 band() 로 옮긴다. */
const BAND_TOP = 0.50
const BAND_H = 0.20 /* 아래쪽은 순환 링(정원) 자리로 비워둔다 */
const BAND_TOP = 0.575
const BAND_H = 0.155 /* 위는 카피, 아래는 순환 링(정원) 자리 */
const band = (localY: number) => BAND_TOP + localY * BAND_H
/* 카메라 — 소실점과 원근 세기. draw() 와 링 좌표 계산이 같은 값을 써야 어긋나지 않는다. */
@ -280,6 +280,13 @@ export function DataFlowCanvas({
let height = 0
let dpr = 1
let lastPhase = -1
/* 글리프 배율. 격자 간격은 dx × width 로 폭에 비례하는데 글리프가 CSS px 고정이면
좁은 화면에서 셀보다 커져 서로 겹치고 덩어리가 된다. 같은 기준(폭)에서 파생시킨다. */
let glyphScale = 1
/* 좁은 화면에서는 카피가 길어져(CTA 가 세로로 쌓인다) 밴드가 CTA 를 침범한다.
게다가 375px 에서 3군집을 나란히 두면 군집당 100px 남짓이라 어차피 판독이 안 된다.
밴드를 링 뒤쪽으로 내리고, 군집 라벨은 히어로 쪽에서 접는다. */
let narrow = false
const resize = () => {
const rect = canvas.getBoundingClientRect()
@ -289,6 +296,8 @@ export function DataFlowCanvas({
canvas.width = Math.round(width * dpr)
canvas.height = Math.round(height * dpr)
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
glyphScale = Math.min(1, Math.max(0.3, width / 1180))
narrow = width < 640
}
resize()
@ -356,9 +365,12 @@ export function DataFlowCanvas({
const to = p.targets[nextIdx]
const wobble = Math.sin(time * 0.0004 * p.speed + p.drift) * wobbleAmp
const nx = from[0] + (to[0] - from[0]) * t + wobble
const ny = from[1] + (to[1] - from[1]) * t + wobble * 0.7
let ny = from[1] + (to[1] - from[1]) * t + wobble * 0.7
const nz = from[2] + (to[2] - from[2]) * t
// 좁은 화면이면 밴드를 통째로 아래(링 뒤)로 옮긴다. 목표 좌표는 그대로 두고 여기서만 remap.
if (narrow) ny = 0.6 + ((ny - BAND_TOP) / BAND_H) * 0.22
const k = perspective(nz)
const sx = VANISH_X + (nx - VANISH_X) * k
const sy = VANISH_Y + (ny - VANISH_Y) * k
@ -379,7 +391,7 @@ export function DataFlowCanvas({
const buckets = [new Path2D(), new Path2D(), new Path2D()]
for (const i of sorted) {
if (particles[i].type !== type) continue
const r = Math.max(0.35, particles[i].dot * pk[i])
const r = Math.max(0.35, particles[i].dot * pk[i] * Math.max(0.55, glyphScale))
const path = buckets[bucketOf(ps[i] * (0.45 + 0.55 * pk[i]))]
path.moveTo(px[i] + r, py[i])
path.arc(px[i], py[i], r, 0, Math.PI * 2)
@ -401,8 +413,8 @@ export function DataFlowCanvas({
const [bw, bh] = GLYPH_SIZE[type]
const r = particles[i].ratio
// 품목은 너비가, 협력사는 높이가 개체마다 다르다 — 값이 있는 데이터처럼 보이게.
const gw = (type === T_ITEM ? bw * r : bw) * pk[i]
const gh = (type === T_PARTNER ? bh * r : bh) * pk[i]
const gw = (type === T_ITEM ? bw * r : bw) * pk[i] * glyphScale
const gh = (type === T_PARTNER ? bh * r : bh) * pk[i] * glyphScale
ctx.globalAlpha =
Math.min(1, TYPE_ALPHA[type] * particles[i].emphasis) * detail * ps[i] * (0.4 + 0.6 * pk[i])
ctx.drawImage(sprites[type], px[i] - gw / 2, py[i] - gh / 2, gw, gh)