통합검색

Javascript

[Javascript] fullpage (GSAP)

  • 2025.08.18 08:55:51
[!]HTML[/!]
 
<div id="fp_dots">
    <a href="#">visual</a>
    <a href="#">1</a>
    <a href="#">2</a>
    <a href="#">3</a>
    <a href="#">4</a>
    <a href="#">foot</a>
</div>



<section class="section">
    <h3>visual</h3>
</section>

<section class="section">
    <h3>1</h3>
</section>

<section class="section">
    <h3>2</h3>
</section>

<section class="section">
    <h3>4</h3>
</section>

<section class="section">
    <h3>5</h3>
</section>

<section class="section foot">
    <h3>footer</h3>
</section>




[!]CSS[/!]
 
#main .section {display: flex;justify-content: center;align-items: center;flex-wrap: wrap;}
#main .section h3 {font-size: 40px;color: #222;font-weight: 700;}
#main .section:nth-child(odd) {background: #000;}
#main .section:nth-child(odd) h3 {color: #fff;}
#main .section:not(.foot) {min-height: 100vh;}
#main .section.foot {height: 600px;}

#fp_dots {position: fixed;top: 50%;left: 70px;display: flex;justify-content: center;align-items: center;flex-direction: column;transform: translateY(-50%);z-index: 33;}
#fp_dots > a {margin: 4px 0;width: 8px;height: 8px;background: #fff;border-radius: 4px;overflow: hidden;text-indent: -9999px;}
#fp_dots > a.on {background: var(--color);height: 30px;}




[!]Javascript[/!]
 
// fullpage
const main_fullpage = {
    init: function () {
        this.action();
    },
    action: function () {
   
        gsap.registerPlugin(ScrollToPlugin);

        const wrap = document.getElementById('main') || document.body;
        const secs = Array.from(document.querySelectorAll('#main .section, .section')); // #main 기준 없으면 .section 전역
        const dots = Array.from(document.querySelectorAll('#fp_dots > a')); // #main 기준 없으면 .section 전역
        if (!secs.length) return;

        let tops = [];
        let current = 0;
        let speed = 0.8;
        let isAnimating = false;

        // active
        const setActive = () => {
            secs.forEach((sec, i) => sec.classList.toggle('active', i === current));
            dots.forEach((dot, i) => dot.classList.toggle('on', i === current));

            document.getElementById("main").removeAttribute("class");
            document.querySelector('#main').classList.toggle('sec'+current+'-active');

            // console.log(current);
            const head = document.querySelector('#header');
            if(current != '0'){
                head.classList.add('bg');
            }else{
                head.classList.remove('bg');
            }
        };

        const calcPos = () => {
            tops = [];
            let i = 0, len = secs.length;
            while (i < len) { tops.push(secs[i].offsetTop); i++; }
            // 현재 스크롤 위치에 가장 가까운 섹션 인덱스 갱신
            const y = window.scrollY || 0;
            current = nearestIndex(y, tops);
            setActive();
        };

        const nearestIndex = (y, arr) => {
            let idx = 0, min = Math.abs((arr[0] || 0) - y);
            let i = 1, len = arr.length;
            while (i < len) {
            const d = Math.abs(arr[i] - y);
            if (d < min) { min = d; idx = i; }
            i++;
            }
            return idx;
        };

        const goTo = (y) => {
            isAnimating = true;
            gsap.to(window, {
            'scrollTo' : { 'y' : y, 'autoKill' : false },
            'duration' : speed,
            'ease' : 'power2.out',
            'onComplete': () => {
                isAnimating = false;
                setActive(); // 이동 후 active 처리
            }
            });
        };

        const next = () => {
            if (isAnimating) return;
            if (current < tops.length - 1) { current += 1; goTo(tops[current]); }
            else { // 마지막(footer 포함)에서 더 내리면 문서 최하단
            const maxY = document.documentElement.scrollHeight - window.innerHeight;
            goTo(maxY < 0 ? 0 : maxY);
            }
        };

        const prev = () => {
            if (isAnimating) return;
            if (current > 0) { current -= 1; goTo(tops[current]); }
            else { goTo(0); }
        };

        const onWheel = (e) => {
            // 스크롤 막고(네이티브), 우리가 애니메이션으로 이동
            e.preventDefault();
            if (isAnimating) return;
            const dy = e.deltaY || (e.wheelDelta ? -e.wheelDelta : 0) || 0;
            if (Math.abs(dy) < 2) return;
            if (dy > 0) next(); else prev();
        };

        // 터치 스와이프(옵션)
        let tsY = 0;
        const onTouchStart = (e) => { if (e.touches && e.touches[0]) tsY = e.touches[0].clientY; };
        const onTouchEnd = (e) => {
            if (isAnimating) return;
            const teY = (e.changedTouches && e.changedTouches[0]) ? e.changedTouches[0].clientY : tsY;
            const dist = teY - tsY;
            if (Math.abs(dist) < 40) return;
            if (dist < 0) next(); else prev();
        };

        // 리사이즈/로드/컨텐츠 변경(footer 높이 auto 포함)에 대응
        const debounce = (fn, d=100) => { let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn.apply(this,a), d); }; };
        const refresh = debounce(calcPos, 100);
        window.addEventListener('wheel', onWheel, { passive: false });
        window.addEventListener('touchstart', onTouchStart, { passive: true });
        window.addEventListener('touchend', onTouchEnd, { passive: true });
        window.addEventListener('resize', refresh, { passive: true });
        window.addEventListener('load', refresh, { passive: true });
        // footer 높이 auto 대응: 래퍼/푸터에 ResizeObserver (지원 브라우저에서)
        if (window.ResizeObserver) {
            const ro = new ResizeObserver(refresh);
            ro.observe(wrap);
            const foot = document.querySelector('#main .section.foot, .section.foot');
            if (foot) ro.observe(foot);
        }

        calcPos();




        // 인덱스로 이동 (섹션 엘리먼트 또는 픽셀 목표로 이동)
        const goToIndex = (i) => {
            if (isAnimating) return;
            current = i; // <-- 핵심: 먼저 갱신
            isAnimating = true;
            gsap.to(window, {
                'scrollTo' : { 'y' : tops[i], 'autoKill' : false }, // secs[i] 대신 미리 계산한 tops 사용 권장
                'duration' : speed,
                'ease' : 'power2.out',
                'onComplete': () => { isAnimating = false; setActive(); }
            });
        };

        const aside_btns = document.querySelectorAll('#fp_dots > a');
        aside_btns.forEach((btn, index) => {
            btn.addEventListener('click', e => {
                e.preventDefault();
                // console.log(index);
                goToIndex(index);
            })
        })


        //go top button
        document.querySelector('#gotop').addEventListener('click', e => {
            e.preventDefault();
            goToIndex(0);
        })


    }
};
 
if (getdevice() === 'pc') { main_fullpage.init(); }
window.addEventListener('resize', () => {
    if (getdevice() === 'pc') { main_fullpage.init(); }
});