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