Files
dalcode-website/components/GsapAnimations.tsx
T
2026-04-29 00:29:14 +08:00

190 lines
6.4 KiB
TypeScript

"use client"
import { useEffect } from "react"
export default function GsapAnimations() {
useEffect(() => {
let ctx: ReturnType<typeof import("gsap").gsap.context> | null = null
import("gsap").then(async ({ gsap }) => {
const { ScrollTrigger } = await import("gsap/ScrollTrigger")
const { Observer } = await import("gsap/Observer")
gsap.registerPlugin(ScrollTrigger, Observer)
ctx = gsap.context(() => {
initMarquee(gsap, Observer)
initCounters(gsap)
initScrollReveal(gsap, ScrollTrigger)
initButtonHovers(gsap)
initSmoothScroll()
})
})
return () => { ctx?.revert() }
}, [])
return null
}
function initMarquee(
gsap: typeof import("gsap").gsap,
Observer: typeof import("gsap/Observer").Observer,
) {
const marqueeItems = gsap.utils.toArray<HTMLElement>(".marquee-scroll-item")
if (!marqueeItems.length) return
const groups = new Map<HTMLElement, HTMLElement[]>()
marqueeItems.forEach((item) => {
const parent = item.parentElement
if (!parent) return
if (!groups.has(parent)) groups.set(parent, [])
groups.get(parent)!.push(item)
})
const instances: {
marqueeTimeline: gsap.core.Timeline
marqueeObject: { value: number }
hoverPauseProxy: { value: number }
isHoverPaused: boolean
lastDirection: number
}[] = []
groups.forEach((items, containerEl) => {
if (!items.length) return
const inst = {
marqueeObject: { value: 1 },
hoverPauseProxy: { value: 1 },
isHoverPaused: false,
lastDirection: 1,
marqueeTimeline: gsap.timeline({
repeat: -1,
onReverseComplete() { this.progress(1) },
}),
}
inst.marqueeTimeline.fromTo(items, { xPercent: 0 }, { xPercent: -100, duration: 50, ease: "none" })
const triggerSet = new Set<HTMLElement>()
if (containerEl.classList.contains("marquee-hover-stop")) triggerSet.add(containerEl)
containerEl.querySelectorAll<HTMLElement>(".marquee-hover-stop").forEach((el) => triggerSet.add(el))
if (triggerSet.size && window.matchMedia("(pointer: fine)").matches) {
const setPaused = (state: boolean) => {
inst.isHoverPaused = state
const ts = inst.marqueeTimeline.timeScale()
gsap.killTweensOf(inst.hoverPauseProxy)
gsap.fromTo(inst.hoverPauseProxy, { value: state ? ts : 0 }, {
value: state ? 0 : inst.lastDirection || 1,
duration: 0.4,
ease: "power2.out",
onUpdate: () => inst.marqueeTimeline.timeScale(inst.hoverPauseProxy.value),
})
}
triggerSet.forEach((el) => {
el.addEventListener("mouseenter", () => setPaused(true))
el.addEventListener("mouseleave", () => setPaused(false))
})
}
instances.push(inst)
})
if (instances.length) {
Observer.create({
target: window,
type: "wheel,scroll,touch",
onChangeY: (self: { velocityY: number }) => {
const velocity = gsap.utils.clamp(-40, 40, self.velocityY * 0.002)
const dir = velocity < 0 ? -1 : 1
instances.forEach((inst) => {
if (inst.isHoverPaused) return
inst.marqueeTimeline.timeScale(velocity)
inst.lastDirection = dir
gsap.fromTo(inst.marqueeObject, { value: velocity }, {
value: dir,
duration: 1,
ease: "power2.out",
onUpdate: () => { if (!inst.isHoverPaused) inst.marqueeTimeline.timeScale(inst.marqueeObject.value) },
})
})
},
})
}
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
instances.forEach((inst) => inst.marqueeTimeline.pause())
}
}
function initCounters(gsap: typeof import("gsap").gsap) {
const elements = gsap.utils.toArray<HTMLElement>(".count-up-number-animation")
if (!elements.length) return
elements.forEach((el, i) => {
const target = parseFloat(el.getAttribute("data-count") || "100")
const decimals = target % 1 !== 0 ? 1 : 0
gsap.fromTo(el, { textContent: "0" }, {
textContent: String(target),
duration: 2,
ease: "power1.out",
snap: decimals ? { textContent: 0.1 } : { textContent: 1 },
delay: i * 0.1,
scrollTrigger: { trigger: el, start: "top 80%", once: true },
onUpdate() {
const v = parseFloat(el.textContent || "0")
el.textContent = v.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals })
},
})
})
}
function initScrollReveal(
gsap: typeof import("gsap").gsap,
ScrollTrigger: typeof import("gsap/ScrollTrigger").ScrollTrigger,
) {
ScrollTrigger.batch(".animate-on-scroll", {
onEnter: (batch) => gsap.to(batch, { opacity: 1, y: 0, stagger: 0.15, overwrite: true }),
onLeave: (batch) => gsap.set(batch, { opacity: 0, y: 100, overwrite: true }),
onEnterBack: (batch) => gsap.to(batch, { opacity: 1, y: 0, stagger: 0.15, overwrite: true }),
onLeaveBack: (batch) => gsap.set(batch, { opacity: 0, y: -100, overwrite: true }),
})
}
function initButtonHovers(gsap: typeof import("gsap").gsap) {
document.querySelectorAll<HTMLElement>(".primary-button, .secondary-button, .footer-button").forEach((btn) => {
const icon = btn.querySelector<HTMLElement>(".button-icon-wrapper svg")
if (!icon) return
btn.addEventListener("mouseenter", () => {
gsap.to(icon, { x: 3, duration: 0.3, ease: "power2.out" })
})
btn.addEventListener("mouseleave", () => {
gsap.to(icon, { x: 0, duration: 0.3, ease: "power2.out" })
})
})
document.querySelectorAll<HTMLElement>(".footer-link, .footer-contact-link").forEach((link) => {
const bg = link.querySelector<HTMLElement>(".footer-link-bg, .footer-contact-link-bg")
if (!bg) return
link.addEventListener("mouseenter", () => {
gsap.to(bg, { width: "100%", duration: 0.3, ease: "power2.out" })
})
link.addEventListener("mouseleave", () => {
gsap.to(bg, { width: "0%", duration: 0.3, ease: "power2.out" })
})
})
document.querySelectorAll<HTMLElement>(".contact-v1-link").forEach((link) => {
const bg = link.querySelector<HTMLElement>(".contact-icon-bg")
if (!bg) return
link.addEventListener("mouseenter", () => {
gsap.to(bg, { width: "100%", duration: 0.4, ease: "power2.out" })
})
link.addEventListener("mouseleave", () => {
gsap.to(bg, { width: "0%", duration: 0.4, ease: "power2.out" })
})
})
}
function initSmoothScroll() {
document.documentElement.style.scrollBehavior = "smooth"
}