跳到主要内容
Example+

滚动联动

ui

0%

src

TSX
// npm install gsap
"use client";

import { useLayoutEffect, useRef, useState } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

const SATELLITES = [0, 60, 120, 180, 240, 300];

export default function Page() {
    const scope = useRef<HTMLDivElement>(null);
    const [pct, setPct] = useState(0);

    useLayoutEffect(() => {
        const ctx = gsap.context(() => {
            /* 一条时间线挂一个 ScrollTrigger,所有元素共享同一份滚动进度 */
            const tl = gsap.timeline({
                defaults: { ease: "none", duration: 1 },
                scrollTrigger: {
                    trigger: scope.current,
                    start: "top 90%",
                    end: "bottom 10%",
                    scrub: 0.5,
                    onUpdate: (self) => setPct(Math.round(self.progress * 100)),
                },
            });

            tl.fromTo(
                ".scrub-core",
                { rotation: 0, scale: 1, borderRadius: "0%" },
                { rotation: 360, scale: 1.5, borderRadius: "50%" },
                0
            )
                .fromTo(
                    ".scrub-track",
                    { rotation: 0, scale: 0.8, opacity: 0.25 },
                    { rotation: -180, scale: 1.06, opacity: 0.7 },
                    0
                )
                .fromTo(".scrub-orbit", { rotation: 0, scale: 0.65 }, { rotation: 270, scale: 1.06 }, 0)
                /* stagger 在 scrub 下变成「随滚动逐个亮起」 */
                .fromTo(".scrub-sat", { opacity: 0 }, { opacity: 1, duration: 0.8, stagger: 0.12 }, 0)
                .fromTo(".scrub-bar", { scaleX: 0 }, { scaleX: 1 }, 0);
        }, scope);
        return () => ctx.revert();
    }, []);

    return (
        <div ref={scope} className="flex min-h-screen flex-col items-center justify-center gap-16">
            <div className="relative flex size-64 items-center justify-center">
                <div className="scrub-track absolute inset-0 rounded-full border border-dashed border-fuchsia-500/35" />
                <div className="scrub-orbit absolute inset-0">
                    {SATELLITES.map((a) => (
                        <div key={a} className="absolute inset-0" style={{ rotate: `${a}deg` }}>
                            <span className="scrub-sat absolute left-1/2 top-0 size-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-fuchsia-500" />
                        </div>
                    ))}
                </div>
                <div className="scrub-core size-24 bg-gradient-to-br from-blue-600 via-fuchsia-600 to-pink-500" />
            </div>

            <div className="flex w-96 items-center gap-5">
                <div className="h-1 flex-1 bg-fuchsia-500/15">
                    <div className="scrub-bar h-full w-full origin-left bg-gradient-to-r from-blue-600 via-fuchsia-600 to-pink-500" />
                </div>
                <span className="w-14 text-right font-mono tabular-nums text-white/50">{pct}%</span>
            </div>
        </div>
    );
}