7 changed files with 432 additions and 28 deletions
|
After Width: | Height: | Size: 1.1 MiB |
@ -0,0 +1,330 @@ |
|||||||
|
import { useEffect, useRef } from "react"; |
||||||
|
import { gsap } from "gsap"; |
||||||
|
import { ScrollTrigger } from "gsap/ScrollTrigger"; |
||||||
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger); |
||||||
|
|
||||||
|
const UAM_SPECS = [ |
||||||
|
{ |
||||||
|
id: "01", |
||||||
|
anchor: "navigation", |
||||||
|
label: "자율 항법 시스템", |
||||||
|
title: "AI 기반\n정밀 항법", |
||||||
|
desc: "다중 센서 융합과 실시간 AI 연산으로 도심 상공에서도 센티미터 단위의 정밀 경로 제어를 실현합니다.", |
||||||
|
stat: "±0.03m", |
||||||
|
statLabel: "위치 정확도", |
||||||
|
cx: "28%", |
||||||
|
cy: "38%", |
||||||
|
cardSide: "left", |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: "02", |
||||||
|
anchor: "comm", |
||||||
|
label: "통합 통신 모듈", |
||||||
|
title: "5G·위성\n이중 통신", |
||||||
|
desc: "5G와 위성 통신을 동시에 운용하는 이중 링크 구조로 어떤 환경에서도 끊김 없는 데이터 채널을 보장합니다.", |
||||||
|
stat: "<8ms", |
||||||
|
statLabel: "지연 시간", |
||||||
|
cx: "72%", |
||||||
|
cy: "30%", |
||||||
|
cardSide: "right", |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: "03", |
||||||
|
anchor: "power", |
||||||
|
label: "하이브리드 추진", |
||||||
|
title: "전기·수소\n하이브리드", |
||||||
|
desc: "전기 모터와 수소 셀을 결합한 하이브리드 추진 시스템으로 항속 거리를 극대화하고 탄소 배출을 최소화합니다.", |
||||||
|
stat: "320km", |
||||||
|
statLabel: "최대 항속", |
||||||
|
cx: "50%", |
||||||
|
cy: "72%", |
||||||
|
cardSide: "left", |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: "04", |
||||||
|
anchor: "sensor", |
||||||
|
label: "능동 안전 센서", |
||||||
|
title: "360° 장애물\n회피", |
||||||
|
desc: "LiDAR·레이더·광학 카메라 트리플 센서가 360도 전방위를 실시간 스캔해 돌발 장애물에 즉각 대응합니다.", |
||||||
|
stat: "0.12s", |
||||||
|
statLabel: "반응 속도", |
||||||
|
cx: "78%", |
||||||
|
cy: "65%", |
||||||
|
cardSide: "right", |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
function MainUam() { |
||||||
|
const sectionRef = useRef(null); |
||||||
|
const eyebrowRef = useRef(null); |
||||||
|
const titleRef = useRef(null); |
||||||
|
const subRef = useRef(null); |
||||||
|
const aircraftRef = useRef(null); |
||||||
|
const dotRefs = useRef([]); |
||||||
|
const lineRefs = useRef([]); |
||||||
|
const cardRefs = useRef([]); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const ctx = gsap.context(() => { |
||||||
|
dotRefs.current = dotRefs.current.slice(0, UAM_SPECS.length); |
||||||
|
lineRefs.current = lineRefs.current.slice(0, UAM_SPECS.length); |
||||||
|
cardRefs.current = cardRefs.current.slice(0, UAM_SPECS.length); |
||||||
|
|
||||||
|
gsap.set(aircraftRef.current, { |
||||||
|
opacity: 0, |
||||||
|
scale: 0.72, |
||||||
|
x: "-18vw", |
||||||
|
y: "12vh", |
||||||
|
}); |
||||||
|
|
||||||
|
gsap.set(eyebrowRef.current, { |
||||||
|
opacity: 0, |
||||||
|
y: 28, |
||||||
|
filter: "blur(8px)", |
||||||
|
}); |
||||||
|
|
||||||
|
gsap.set(titleRef.current, { |
||||||
|
opacity: 0, |
||||||
|
y: 36, |
||||||
|
filter: "blur(8px)", |
||||||
|
}); |
||||||
|
|
||||||
|
gsap.set(subRef.current, { |
||||||
|
opacity: 0, |
||||||
|
y: 24, |
||||||
|
}); |
||||||
|
|
||||||
|
cardRefs.current.forEach((el) => { |
||||||
|
if (el) gsap.set(el, { opacity: 0, y: 28, scale: 0.96, filter: "blur(6px)" }); |
||||||
|
}); |
||||||
|
|
||||||
|
dotRefs.current.forEach((el) => { |
||||||
|
if (el) gsap.set(el, { scale: 0, opacity: 0 }); |
||||||
|
}); |
||||||
|
|
||||||
|
lineRefs.current.forEach((el) => { |
||||||
|
if (el) gsap.set(el, { scaleX: 0, opacity: 0 }); |
||||||
|
}); |
||||||
|
|
||||||
|
ScrollTrigger.matchMedia({ |
||||||
|
"(min-width: 992px)": () => { |
||||||
|
const tl = gsap.timeline({ |
||||||
|
scrollTrigger: { |
||||||
|
trigger: sectionRef.current, |
||||||
|
start: "top top", |
||||||
|
end: "+=3200", |
||||||
|
scrub: 1.05, |
||||||
|
pin: true, |
||||||
|
anticipatePin: 1, |
||||||
|
invalidateOnRefresh: true, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
tl.to(eyebrowRef.current, { opacity: 1, y: 0, filter: "blur(0px)", ease: "none", duration: 0.5 }, 0).to(titleRef.current, { opacity: 1, y: 0, filter: "blur(0px)", ease: "none", duration: 0.65 }, 0.08).to(subRef.current, { opacity: 1, y: 0, ease: "none", duration: 0.45 }, 0.22).to( |
||||||
|
aircraftRef.current, |
||||||
|
{ |
||||||
|
opacity: 1, |
||||||
|
scale: 1, |
||||||
|
x: 0, |
||||||
|
y: 0, |
||||||
|
ease: "none", |
||||||
|
duration: 1.8, |
||||||
|
}, |
||||||
|
0.12, |
||||||
|
); |
||||||
|
|
||||||
|
UAM_SPECS.forEach((spec, i) => { |
||||||
|
const baseTime = 0.5 + i * 0.52; |
||||||
|
const origin = spec.cardSide === "left" ? "right center" : "left center"; |
||||||
|
|
||||||
|
tl.to( |
||||||
|
dotRefs.current[i], |
||||||
|
{ |
||||||
|
scale: 1, |
||||||
|
opacity: 1, |
||||||
|
ease: "none", |
||||||
|
duration: 0.18, |
||||||
|
}, |
||||||
|
baseTime, |
||||||
|
) |
||||||
|
.to( |
||||||
|
lineRefs.current[i], |
||||||
|
{ |
||||||
|
scaleX: 1, |
||||||
|
opacity: 1, |
||||||
|
transformOrigin: origin, |
||||||
|
ease: "none", |
||||||
|
duration: 0.24, |
||||||
|
}, |
||||||
|
baseTime + 0.08, |
||||||
|
) |
||||||
|
.to( |
||||||
|
cardRefs.current[i], |
||||||
|
{ |
||||||
|
opacity: 1, |
||||||
|
y: 0, |
||||||
|
scale: 1, |
||||||
|
filter: "blur(0px)", |
||||||
|
ease: "none", |
||||||
|
duration: 0.32, |
||||||
|
}, |
||||||
|
baseTime + 0.18, |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
tl.to( |
||||||
|
aircraftRef.current, |
||||||
|
{ |
||||||
|
scale: 1.05, |
||||||
|
ease: "none", |
||||||
|
duration: 0.45, |
||||||
|
}, |
||||||
|
2.7, |
||||||
|
); |
||||||
|
}, |
||||||
|
|
||||||
|
"(max-width: 991px)": () => { |
||||||
|
const tl = gsap.timeline({ |
||||||
|
scrollTrigger: { |
||||||
|
trigger: sectionRef.current, |
||||||
|
start: "top 80%", |
||||||
|
end: "bottom 30%", |
||||||
|
scrub: 0.8, |
||||||
|
invalidateOnRefresh: true, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
tl.to(eyebrowRef.current, { opacity: 1, y: 0, filter: "blur(0px)", ease: "none", duration: 0.22 }, 0) |
||||||
|
.to(titleRef.current, { opacity: 1, y: 0, filter: "blur(0px)", ease: "none", duration: 0.24 }, 0.05) |
||||||
|
.to(subRef.current, { opacity: 1, y: 0, ease: "none", duration: 0.2 }, 0.1) |
||||||
|
.to( |
||||||
|
aircraftRef.current, |
||||||
|
{ |
||||||
|
opacity: 1, |
||||||
|
scale: 1, |
||||||
|
x: 0, |
||||||
|
y: 0, |
||||||
|
ease: "none", |
||||||
|
duration: 0.28, |
||||||
|
}, |
||||||
|
0.16, |
||||||
|
) |
||||||
|
.to( |
||||||
|
dotRefs.current, |
||||||
|
{ |
||||||
|
scale: 1, |
||||||
|
opacity: 1, |
||||||
|
stagger: 0.05, |
||||||
|
ease: "none", |
||||||
|
duration: 0.18, |
||||||
|
}, |
||||||
|
0.24, |
||||||
|
) |
||||||
|
.to( |
||||||
|
lineRefs.current, |
||||||
|
{ |
||||||
|
scaleX: 1, |
||||||
|
opacity: 1, |
||||||
|
stagger: 0.05, |
||||||
|
ease: "none", |
||||||
|
duration: 0.18, |
||||||
|
}, |
||||||
|
0.3, |
||||||
|
) |
||||||
|
.to( |
||||||
|
cardRefs.current, |
||||||
|
{ |
||||||
|
opacity: 1, |
||||||
|
y: 0, |
||||||
|
scale: 1, |
||||||
|
filter: "blur(0px)", |
||||||
|
stagger: 0.06, |
||||||
|
ease: "none", |
||||||
|
duration: 0.2, |
||||||
|
}, |
||||||
|
0.36, |
||||||
|
); |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
gsap.to(aircraftRef.current, { |
||||||
|
y: -8, |
||||||
|
duration: 2.8, |
||||||
|
ease: "sine.inOut", |
||||||
|
repeat: -1, |
||||||
|
yoyo: true, |
||||||
|
}); |
||||||
|
}, sectionRef); |
||||||
|
|
||||||
|
return () => ctx.revert(); |
||||||
|
}, []); |
||||||
|
|
||||||
|
return ( |
||||||
|
<section className="uam-section" ref={sectionRef}> |
||||||
|
<div className="uam-bg-grid"></div> |
||||||
|
<div className="uam-bg-glow uam-bg-glow--a"></div> |
||||||
|
<div className="uam-bg-glow uam-bg-glow--b"></div> |
||||||
|
|
||||||
|
<div className="uam-inner"> |
||||||
|
<div className="uam-header"> |
||||||
|
<div className="uam-eyebrow" ref={eyebrowRef}> |
||||||
|
<span className="uam-eyebrow-dot"></span> |
||||||
|
UAM TECHNOLOGY |
||||||
|
</div> |
||||||
|
|
||||||
|
<h2 className="uam-title" ref={titleRef}> |
||||||
|
도심 항공 모빌리티, |
||||||
|
<br /> |
||||||
|
<em>기술로 완성합니다</em> |
||||||
|
</h2> |
||||||
|
|
||||||
|
<p className="uam-sub" ref={subRef}> |
||||||
|
PAL NETWORKS의 4대 핵심 기술이 안전한 하늘길을 만듭니다 |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="uam-stage"> |
||||||
|
<div className="uam-aircraft-wrap" ref={aircraftRef}> |
||||||
|
<img className="uam-aircraft-img" src="./images/uam.png" alt="UAM 기체" draggable={false} /> |
||||||
|
<div className="uam-aircraft-shadow"></div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{UAM_SPECS.map((spec, i) => ( |
||||||
|
<div key={spec.id} className="uam-point-group"> |
||||||
|
<div className="uam-dot" ref={(el) => (dotRefs.current[i] = el)} style={{ left: spec.cx, top: spec.cy }}> |
||||||
|
<span className="uam-dot-ring"></span> |
||||||
|
<span className="uam-dot-core"></span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className={`uam-line uam-line--${spec.cardSide}`} ref={(el) => (lineRefs.current[i] = el)} style={{ left: spec.cx, top: spec.cy }}></div> |
||||||
|
|
||||||
|
<div className={`uam-card uam-card--${spec.cardSide}`} ref={(el) => (cardRefs.current[i] = el)} style={{ left: spec.cx, top: `calc(${spec.cy} - 10px)` }}> |
||||||
|
<div className="uam-card-index">{spec.id}</div> |
||||||
|
<div className="uam-card-label">{spec.label}</div> |
||||||
|
|
||||||
|
<strong className="uam-card-title"> |
||||||
|
{spec.title.split("\n").map((line, j) => ( |
||||||
|
<span key={j}> |
||||||
|
{line} |
||||||
|
{j === 0 && <br />} |
||||||
|
</span> |
||||||
|
))} |
||||||
|
</strong> |
||||||
|
|
||||||
|
<p className="uam-card-desc">{spec.desc}</p> |
||||||
|
|
||||||
|
<div className="uam-card-stat"> |
||||||
|
<span className="uam-card-stat-value">{spec.stat}</span> |
||||||
|
<span className="uam-card-stat-label">{spec.statLabel}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export default MainUam; |
||||||
Loading…
Reference in new issue