import { useEffect, useRef, type RefObject } from "react" import type { DataFlowPhase } from "@/components/ui/data-flow-canvas" /** * 파이프라인 순환 링 — 정원. * * 앞서 3D 로 눕힌 타원을 썼는데, 깊이는 생기지만 정원의 완결감이 사라진다. * negotium 의 메시지는 "닫힌 루프"라서 완결감 쪽이 맞다. * * 구조는 레퍼런스(infinith)와 같다 — 점선 원만 천천히 돌고, 마디와 글자는 고정이다. * 글자까지 같이 돌면 읽을 수가 없다. */ const NODES: { key: DataFlowPhase; label: string }[] = [ { key: "scatter", label: "Collect" }, { key: "cluster", label: "Classify" }, { key: "stream", label: "Benchmark" }, { key: "card", label: "Anchor" }, { key: "negotiate", label: "Negotiate" }, { key: "learn", label: "Learn" }, ] /* viewBox 는 정사각이 아니다. 라벨이 궤도 바깥 좌우로 뻗기 때문에 가로 여백이 더 필요한데, 정사각으로 두면 "Benchmark" 같은 긴 라벨이 잘린다(SVG 는 viewBox 밖을 그냥 자른다). 가로만 넓히면 링 크기는 유지한 채 라벨 자리를 확보할 수 있다. */ const VB_W = 176 const VB_H = 150 const CX = VB_W / 2 const CY = VB_H / 2 const R = 46 // 궤도 반지름 const R_LABEL = 58 // 라벨은 궤도 바깥에 const rad = (ratio: number) => ratio * Math.PI * 2 - Math.PI / 2 // 12시에서 시계방향 const at = (ratio: number, radius: number) => ({ x: CX + Math.cos(rad(ratio)) * radius, y: CY + Math.sin(rad(ratio)) * radius, }) export function OrbitRing({ phase, progressRef, className = "", }: { phase: DataFlowPhase /** 캔버스가 매 프레임 써 넣는 루프 진행도 0..1 */ progressRef: RefObject className?: string }) { const activeIndex = Math.max( 0, NODES.findIndex((n) => n.key === phase), ) const headRef = useRef(null) // 궤도를 도는 머리는 rAF 로 직접 갱신한다. 60fps 로 setState 를 부르면 페이지가 매 프레임 리렌더된다. useEffect(() => { let raf = 0 const tick = () => { const p = progressRef.current ?? 0 const { x, y } = at(p, R) if (headRef.current) { headRef.current.setAttribute("cx", String(x)) headRef.current.setAttribute("cy", String(y)) } raf = requestAnimationFrame(tick) } raf = requestAnimationFrame(tick) return () => cancelAnimationFrame(raf) }, [progressRef]) return ( {/* 점선 궤도 — 이 레이어만 돈다 */} {/* 바깥 보조 호 — 정지. 원이 하나면 심심하고, 둘이면 공간이 생긴다. */} {/* 마디 + 라벨 — 고정 레이어 */} {NODES.map((n, i) => { const ratio = i / NODES.length const p = at(ratio, R) const l = at(ratio, R_LABEL) const cos = Math.cos(rad(ratio)) const anchor = cos > 0.3 ? "start" : cos < -0.3 ? "end" : "middle" const active = i === activeIndex return ( {n.label} ) })} {/* 궤도를 도는 머리 */} {/* 가운데 */} {NODES[activeIndex].label} {String(activeIndex + 1).padStart(2, "0")} / {String(NODES.length).padStart(2, "0")} ) }