diff --git a/landing/app/components/sections/hero-dataflow.tsx b/landing/app/components/sections/hero-dataflow.tsx
index 845e5c4..b7084f2 100644
--- a/landing/app/components/sections/hero-dataflow.tsx
+++ b/landing/app/components/sections/hero-dataflow.tsx
@@ -36,7 +36,7 @@ export function HeroDataFlow() {
className={`absolute -translate-x-1/2 -translate-y-1/2 whitespace-nowrap text-[13px] font-semibold text-on-stage-soft/80 transition-opacity duration-500 ${
phase === "cluster" ? "opacity-100" : "opacity-0"
}`}
- style={{ left: `${anchor.x * 100}%`, top: `${anchor.labelY * 100}%` }}
+ style={{ left: `${anchor.screenX * 100}%`, top: `${anchor.labelY * 100}%` }}
>
{anchor.label}
@@ -44,7 +44,7 @@ export function HeroDataFlow() {
{/* 카피는 상단 55%, 데이터 흐름은 하단 밴드. 둘이 겹치지 않게 층을 나눈다. */}
-
+
{/* 히어로 카피에는 진입 애니메이션을 걸지 않는다.
여기가 LCP 요소라 opacity:0 으로 시작하면 하이드레이션 전까지 첫 화면이 비고,
광고 유입처럼 JS 가 느린 환경에서 그대로 손실이 된다. 모션은 배경 캔버스가 맡는다. */}
diff --git a/landing/app/components/ui/data-flow-canvas.tsx b/landing/app/components/ui/data-flow-canvas.tsx
index 3dbdeda..843bd0b 100644
--- a/landing/app/components/ui/data-flow-canvas.tsx
+++ b/landing/app/components/ui/data-flow-canvas.tsx
@@ -70,13 +70,25 @@ export const perspective = (z: number) => 0.42 + 0.58 * z
/* 분류 단계에서 각 무리가 앉는 자리와 깊이.
무리마다 z 평면을 하나씩 준다 — 입자마다 z 를 따로 주면 격자가 3D 로 뒤틀려
- 표로 안 읽힌다. 무리 단위로 주면 격자는 온전한 채 그룹 사이에만 깊이가 생긴다. */
-export const CLUSTER_ANCHORS = [
- { key: "item", x: 0.17, z: 0.9, labelY: 0.525, label: "거래 품목" },
- { key: "user", x: 0.45, z: 0.66, labelY: 0.525, label: "사용자" },
- { key: "partner", x: 0.76, z: 0.44, labelY: 0.525, label: "협력사" },
+ 표로 안 읽힌다. 무리 단위로 주면 격자는 온전한 채 그룹 사이에만 깊이가 생긴다.
+
+ x 는 "투영된 뒤 어디에 놓일지"를 먼저 정하고 역산한 값이다.
+ 투영은 깊이마다 다른 배율로 중앙으로 당기기 때문에(k = 0.42+0.58z), 세계 좌표를
+ 눈대중으로 고르면 가장 깊은 무리가 제일 많이 끌려와 구도가 한쪽으로 쏠린다. */
+const CLUSTER_PLAN = [
+ { key: "item", screenX: 0.19, z: 0.9, label: "거래 품목" },
+ { key: "user", screenX: 0.5, z: 0.66, label: "사용자" },
+ { key: "partner", screenX: 0.81, z: 0.44, label: "협력사" },
] as const
+export const CLUSTER_ANCHORS = CLUSTER_PLAN.map((c) => ({
+ ...c,
+ /** 투영 전 세계 좌표 — 투영을 거치면 screenX 에 놓인다 */
+ x: VANISH_X + (c.screenX - VANISH_X) / perspective(c.z),
+ /** 라벨은 격자 위. 투영 후 y 라서 화면 좌표 그대로 쓴다 */
+ labelY: VANISH_Y + (0.5 - VANISH_Y) * perspective(c.z),
+}))
+
/* 격자 배치 — 종류별 [열, 열간격(x, 화면비), 행간격(y, 밴드지역비)].
간격은 글리프보다 확실히 커야 한다. 붙으면 표가 아니라 노이즈로 읽힌다. */
const GRID = [