diff --git a/src/components/FloatingKeywords.jsx b/src/components/FloatingKeywords.jsx new file mode 100644 index 0000000..8f8c581 --- /dev/null +++ b/src/components/FloatingKeywords.jsx @@ -0,0 +1,67 @@ +import { useEffect, useRef } from "react"; +import { motion } from "framer-motion"; + +const KEYWORDS = [ + { text: "UTM", size: 18 }, + { text: "UAM", size: 22 }, + { text: "UATM", size: 24 }, + { text: "AI System", size: 15 }, + { text: "항공관제", size: 13 }, + { text: "드론관제", size: 11 }, + { text: "R&D", size: 20 }, + { text: "SI", size: 16 }, + { text: "솔루션", size: 12 }, + { text: "항공 데이터", size: 11 }, + { text: "스마트 공역", size: 14 }, + { text: "Flight Control", size: 10 }, + { text: "PAL Networks", size: 11 }, +]; + +// 랜덤 고정 위치 (렌더링마다 바뀌지 않게 미리 계산) +const POSITIONS = [ + { x: 12, y: 18 }, + { x: 68, y: 8 }, + { x: 82, y: 32 }, + { x: 55, y: 22 }, + { x: 20, y: 55 }, + { x: 75, y: 58 }, + { x: 40, y: 72 }, + { x: 88, y: 75 }, + { x: 60, y: 45 }, + { x: 30, y: 38 }, + { x: 50, y: 85 }, + { x: 78, y: 88 }, + { x: 15, y: 82 }, +]; + +export default function FloatingKeywords() { + return ( +
+ {KEYWORDS.map((kw, i) => ( + + {kw.text} + + ))} +
+ ); +} diff --git a/src/components/SubHero.jsx b/src/components/SubHero.jsx index ee76f23..435a588 100644 --- a/src/components/SubHero.jsx +++ b/src/components/SubHero.jsx @@ -1,3 +1,4 @@ +import { useEffect, useRef, useState } from "react"; import { Link, useLocation } from "react-router-dom"; import { motion } from "framer-motion"; @@ -9,9 +10,172 @@ const menuMap = { "/contact": { label: "Contact Us" }, }; +function NetworkGlobe() { + const canvasRef = useRef(null); + const mouseRef = useRef({ x: 0, y: 0 }); + + useEffect(() => { + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + let raf; + let rotX = 0.3; + let rotY = 0; + + const COLORS = ["#d94889", "#7b3fa0", "#593a84", "#1a1f5e", "#198dc7"]; + const N_NODES = 60; + + const nodes = Array.from({ length: N_NODES }, (_, i) => { + const phi = Math.acos(1 - (2 * (i + 0.5)) / N_NODES); + const theta = Math.PI * (1 + Math.sqrt(5)) * i; + return { + ox: Math.sin(phi) * Math.cos(theta), + oy: Math.cos(phi), + oz: Math.sin(phi) * Math.sin(theta), + color: COLORS[i % COLORS.length], + }; + }); + + function resize() { + canvas.width = canvas.offsetWidth; + canvas.height = canvas.offsetHeight; + } + + function onMouseMove(e) { + const rect = canvas.getBoundingClientRect(); + mouseRef.current = { + x: ((e.clientX - rect.left) / rect.width - 0.5) * 2, + y: ((e.clientY - rect.top) / rect.height - 0.5) * 2, + }; + } + function onMouseLeave() { + mouseRef.current = { x: 0, y: 0 }; + } + + canvas.addEventListener("mousemove", onMouseMove); + canvas.addEventListener("mouseleave", onMouseLeave); + + function project(x, y, z, cx, cy, R) { + const cosY = Math.cos(rotY), + sinY = Math.sin(rotY); + const x1 = x * cosY - z * sinY; + const z1 = x * sinY + z * cosY; + const cosX = Math.cos(rotX), + sinX = Math.sin(rotX); + const y2 = y * cosX - z1 * sinX; + const z2 = y * sinX + z1 * cosX; + const scale = 1 / (1.6 + z2 * 0.4); + return { sx: cx + x1 * R * scale, sy: cy + y2 * R * scale, sz: z2, scale }; + } + + function draw() { + const w = canvas.width; + const h = canvas.height; + const cx = w / 2; + const cy = h / 2; + const R = Math.min(w, h) * 0.4; + + ctx.clearRect(0, 0, w, h); + + rotY += 0.004 + mouseRef.current.x * 0.002; + rotX = 0.3 + mouseRef.current.y * 0.15; + + const projected = nodes + .map((n) => ({ + ...project(n.ox, n.oy, n.oz, cx, cy, R), + color: n.color, + ox: n.ox, + oy: n.oy, + oz: n.oz, + })) + .sort((a, b) => a.sz - b.sz); + + // 연결선 + for (let i = 0; i < projected.length; i++) { + for (let j = i + 1; j < projected.length; j++) { + const a = projected[i], + b = projected[j]; + const dx = a.ox - b.ox, + dy = a.oy - b.oy, + dz = a.oz - b.oz; + const d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d < 0.55) { + const depth = (a.sz + b.sz) * 0.5; + const alpha = Math.max(0, 0.05 + (depth + 1) * 0.1) * (1 - d / 0.55); + const aHex = Math.round(Math.min(255, alpha * 255)) + .toString(16) + .padStart(2, "0"); + const grad = ctx.createLinearGradient(a.sx, a.sy, b.sx, b.sy); + grad.addColorStop(0, a.color + aHex); + grad.addColorStop(1, b.color + aHex); + ctx.beginPath(); + ctx.moveTo(a.sx, a.sy); + ctx.lineTo(b.sx, b.sy); + ctx.strokeStyle = grad; + ctx.lineWidth = 0.8; + ctx.stroke(); + } + } + } + + // 노드 + projected.forEach((n) => { + const depth = (n.sz + 1) * 0.5; + const r = (1.2 + depth * 2.2) * n.scale; + const alpha = 0.25 + depth * 0.75; + const aHex = Math.round(Math.min(255, alpha * 255)) + .toString(16) + .padStart(2, "0"); + const gHex = Math.round(Math.min(255, alpha * 0.35 * 255)) + .toString(16) + .padStart(2, "0"); + + const glow = ctx.createRadialGradient(n.sx, n.sy, 0, n.sx, n.sy, r * 3.5); + glow.addColorStop(0, n.color + gHex); + glow.addColorStop(1, n.color + "00"); + ctx.beginPath(); + ctx.arc(n.sx, n.sy, r * 3.5, 0, Math.PI * 2); + ctx.fillStyle = glow; + ctx.fill(); + + ctx.beginPath(); + ctx.arc(n.sx, n.sy, r, 0, Math.PI * 2); + ctx.fillStyle = n.color + aHex; + ctx.fill(); + }); + + raf = requestAnimationFrame(draw); + } + + resize(); + draw(); + + const ro = new ResizeObserver(resize); + ro.observe(canvas); + + return () => { + cancelAnimationFrame(raf); + ro.disconnect(); + canvas.removeEventListener("mousemove", onMouseMove); + canvas.removeEventListener("mouseleave", onMouseLeave); + }; + }, []); + + return ; +} + export default function SubHero({ title, desc, navItems, rightSlot }) { const { pathname } = useLocation(); const titleLines = typeof title === "string" ? title.split("\n") : [title]; + const [isPill, setIsPill] = useState(false); + const navRef = useRef(null); + + useEffect(() => { + const onScroll = () => { + setIsPill(window.scrollY > 80); + }; + window.addEventListener("scroll", onScroll, { passive: true }); + return () => window.removeEventListener("scroll", onScroll); + }, []); return ( <> @@ -52,7 +216,7 @@ export default function SubHero({ title, desc, navItems, rightSlot }) { {rightSlot && ( - + {rightSlot} )} @@ -60,7 +224,7 @@ export default function SubHero({ title, desc, navItems, rightSlot }) { {navItems?.length > 1 && ( -