diff --git a/landing/app/components/sections/hero-dataflow.tsx b/landing/app/components/sections/hero-dataflow.tsx index b7084f2..6dd2fe1 100644 --- a/landing/app/components/sections/hero-dataflow.tsx +++ b/landing/app/components/sections/hero-dataflow.tsx @@ -29,7 +29,7 @@ export function HeroDataFlow() { {/* 분류 단계에서만 뜨는 무리 라벨. 아이콘만으로는 "무엇이" 분류됐는지 절반만 전달된다. 좌표는 캔버스가 쓰는 값을 그대로 받아 써서 둘이 어긋날 일이 없다. */} -
+
{CLUSTER_ANCHORS.map((anchor) => ( {/* 카피는 상단 55%, 데이터 흐름은 하단 밴드. 둘이 겹치지 않게 층을 나눈다. */} -
+
{/* 히어로 카피에는 진입 애니메이션을 걸지 않는다. 여기가 LCP 요소라 opacity:0 으로 시작하면 하이드레이션 전까지 첫 화면이 비고, 광고 유입처럼 JS 가 느린 환경에서 그대로 손실이 된다. 모션은 배경 캔버스가 맡는다. */} @@ -73,8 +73,8 @@ export function HeroDataFlow() {
{/* 순환 링 — 정원. 점선 궤도만 돌고 마디·글자는 고정이다. */} -
- +
+
) diff --git a/landing/app/components/ui/data-flow-canvas.tsx b/landing/app/components/ui/data-flow-canvas.tsx index 843bd0b..603e0c7 100644 --- a/landing/app/components/ui/data-flow-canvas.tsx +++ b/landing/app/components/ui/data-flow-canvas.tsx @@ -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)