Javascript
[Javascript] fullpage (GSAP)
- 2025.08.18 08:55:51
[!]HTML[/!]
<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;} [!]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 전역 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)); // 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(); } }; if (getdevice() === 'pc') { main_fullpage.init(); } window.addEventListener('resize', () => { if (getdevice() === 'pc') { main_fullpage.init(); } }); |