two-dimension-scroll
Version:
A smooth scroll library that detects both horizontal and vertical scroll and converts them to vertical smooth scrolling
2 lines (1 loc) • 11.2 kB
JavaScript
const t={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>t*(2-t),easeInOutQuad:t=>t<.5?2*t*t:(4-2*t)*t-1,easeInCubic:t=>t*t*t,easeOutCubic:t=>--t*t*t+1,easeInOutCubic:t=>t<.5?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1};function i(){return"undefined"!=typeof window&&(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)||window.innerWidth<=768)}function o(){return"undefined"!=typeof window&&("ontouchstart"in window||navigator.maxTouchPoints>0)}function e(t,i,o){return Math.min(Math.max(t,i),o)}function s(t,i,o){return t+(i-t)*o}function n(){return"undefined"==typeof document?0:Math.max(document.body.scrollHeight-window.innerHeight,document.documentElement.scrollHeight-window.innerHeight,0)}function h(){return"undefined"==typeof window?0:window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0}const a="undefined"==typeof window?t=>setTimeout(t,16):window.requestAnimationFrame||window.webkitRequestAnimationFrame||(t=>setTimeout(t,16)),r="undefined"==typeof window?clearTimeout:window.cancelAnimationFrame||window.webkitCancelAnimationFrame||clearTimeout;function l(){if("undefined"==typeof window)return!1;let t=!1;try{const i=Object.defineProperty({},"passive",{get:()=>(t=!0,!0)});window.addEventListener("testPassive",()=>{},i),window.removeEventListener("testPassive",()=>{},i)}catch(t){}return t}function c(t,i){let o;return(...e)=>{clearTimeout(o),o=setTimeout(()=>t.apply(null,e),i)}}function u(t,i){let o;return(...e)=>{o||(t.apply(null,e),o=!0,setTimeout(()=>o=!1,i))}}class d{constructor(e={}){this.isAnimating=!1,this.animationFrame=null,this.rafId=null,this.lastScrollTop=0,this.touchStartY=0,this.touchStartX=0,this.touchStartTime=0,this.isScrolling=!1,this.scrollCallbacks=new Set,this.isMobileDevice=!1,this.passive=!1,this.lastTouchY=0,this.lastTouchX=0,this.lastTouchTime=0,this.touchVelocityX=0,this.touchVelocityY=0,this.touchMoveCount=0,this.touchStopTimer=null,this.touchDirection=null,this.touchDirectionLocked=!1,this.touchStartDeltaX=0,this.touchStartDeltaY=0,this.oppositeDirectionCount=0,this.lastDeltaX=0,this.lastDeltaY=0,this.smoothedDeltaX=0,this.smoothedDeltaY=0,this.directionChangeStartTime=0,this.verticalScrollDirection=null,this.isModalOpen=!1,this.onWheel=t=>{if(this.options.disabled||this.isScrolling)return;t.preventDefault();const i=t.deltaX,o=t.deltaY,e=this.options.wheelMultiplier||1,s=i*this.options.horizontalSensitivity*e,n=o*this.options.verticalSensitivity*e;let h;h=this.options.prioritizeVertical?0!==n?n:s:Math.abs(s)>Math.abs(n)?s:n,h*=2,this.options.debug,this.handleScroll(h,"wheel")},this.onTouchStart=t=>{if(this.options.disabled)return;const i=t.touches[0];this.touchStartX=i.clientX,this.touchStartY=i.clientY,this.touchStartTime=Date.now(),this.lastTouchY=i.clientY,this.lastTouchX=i.clientX,this.lastTouchTime=this.touchStartTime,this.touchVelocityX=0,this.touchVelocityY=0,this.touchMoveCount=0,this.touchDirection=null,this.touchDirectionLocked=!1,this.touchStartDeltaX=0,this.touchStartDeltaY=0,this.oppositeDirectionCount=0,this.lastDeltaX=0,this.lastDeltaY=0,this.smoothedDeltaX=0,this.smoothedDeltaY=0,this.directionChangeStartTime=0,this.verticalScrollDirection=null,this.touchStopTimer&&(clearTimeout(this.touchStopTimer),this.touchStopTimer=null)},this.onTouchMove=t=>{if(this.options.disabled)return;const i=t.touches[0],o=Date.now(),e=this.lastTouchX-i.clientX,s=this.lastTouchY-i.clientY;if(Math.sqrt(e*e+s*s)>this.options.touchStopThreshold){this.touchStopTimer&&(clearTimeout(this.touchStopTimer),this.touchStopTimer=null);const t=o-this.lastTouchTime;t>0&&(this.touchVelocityX=e/t,this.touchVelocityY=s/t);const n=e*this.options.horizontalSensitivity*(this.options.touchMultiplier||1),h=s*this.options.verticalSensitivity*(this.options.touchMultiplier||1);if(this.isModalOpen)this.options.debug;else if(Math.abs(n)>3||Math.abs(h)>3){const t=this.calculateCombinedDelta(n,h);this.addToScroll(t),this.options.debug}this.lastTouchX=i.clientX,this.lastTouchY=i.clientY,this.lastTouchTime=o,this.touchMoveCount++}else{const t=this;this.touchStopTimer||(this.touchStopTimer=setTimeout(()=>{t.touchVelocityX*=.8,t.touchVelocityY*=.8,t.touchStopTimer=null},100))}},this.onTouchEnd=t=>{if(this.options.disabled)return;this.touchStopTimer&&(clearTimeout(this.touchStopTimer),this.touchStopTimer=null);const i=t.changedTouches[0],o=Date.now()-this.touchStartTime,e=this.touchStartY-i.clientY;if(o<300&&Math.abs(e)>50&&this.touchMoveCount>3){const t=400*this.touchVelocityY*(this.options.flingMultiplier||1);Math.abs(t)>50&&(this.isModalOpen||this.addToScroll(t),this.options.debug)}this.touchVelocityX=0,this.touchVelocityY=0,this.touchMoveCount=0,this.options.debug},this.onKeyDown=t=>{if(this.options.disabled||this.isScrolling)return;let i=0;const o=.8*window.innerHeight;switch(t.key){case"ArrowUp":case"PageUp":i=-o;break;case"ArrowDown":case"PageDown":case" ":i=o;break;case"Home":i=-h();break;case"End":i=n()-h();break;default:return}t.preventDefault(),this.handleScroll(i,"keyboard")},this.onResize=()=>{const t=n();h()>t&&this.scrollTo(t)},this.targetScroll=0,this.animate=()=>{if(!this.animationFrame)return;const t=performance.now()-this.animationFrame.startTime,i=Math.min(t/this.animationFrame.duration,1),o=this.animationFrame.easing(i),e=this.animationFrame.startPosition+(this.animationFrame.targetPosition-this.animationFrame.startPosition)*o;window.scrollTo(0,e),i<1?this.rafId=a(this.animate):(this.isAnimating=!1,this.isScrolling=!1,this.animationFrame=null,this.rafId=null)};const s={duration:1e3,easing:t.easeOutCubic,horizontalSensitivity:1,verticalSensitivity:1,disabled:!1,useNativeScrollOnMobile:!0,scrollableSelector:"body",debug:!1},r=i(),c=o()&&!r;let u={};e.mobile&&r?u={...e.mobile}:e.tablet&&c?u={...e.tablet}:!e.desktop||r||c||(u={...e.desktop}),this.options={...s,...e,...u},this.options.debug,this.isMobileDevice=i(),this.passive=!!l()&&{passive:!1},this.lastScrollTop=h(),this.init()}init(){"undefined"!=typeof window&&(this.isMobileDevice&&this.options.useNativeScrollOnMobile?this.log("모바일 네이티브 스크롤 모드"):(this.bindEvents(),this.log("TwoDimensionScroll 초기화 완료")))}bindEvents(){document.addEventListener("wheel",this.onWheel,this.passive),o()&&(document.addEventListener("touchstart",this.onTouchStart,this.passive),document.addEventListener("touchmove",this.onTouchMove,this.passive),document.addEventListener("touchend",this.onTouchEnd,this.passive)),document.addEventListener("keydown",this.onKeyDown),window.addEventListener("resize",u(this.onResize,100))}calculateCombinedDelta(t,i){if(this.options.useAngleBasedDirection){let o=this.options.horizontalAngleThreshold||20;this.options.prioritizeVertical&&(o=15);const e=Math.atan2(Math.abs(i),Math.abs(t))*(180/Math.PI);if(this.options.debug,e<=o)return t;{const o=Math.sqrt(t*t+i*i);return this.touchDirectionLocked||(this.verticalScrollDirection=i>0?"down":"up",this.touchDirectionLocked=!0,this.options.debug),"down"===this.verticalScrollDirection?o:-o}}if(this.options.lockTouchDirection){const o=this.options.touchDirectionThreshold||15,e=!1!==this.options.allowDirectionChange,s=this.options.directionChangeThreshold||25,n=this.options.directionChangeSmoothness||.3;if(this.smoothedDeltaX=this.smoothedDeltaX*(1-n)+t*n,this.smoothedDeltaY=this.smoothedDeltaY*(1-n)+i*n,!this.touchDirectionLocked&&(Math.abs(t)>o||Math.abs(i)>o)&&(this.options.prioritizeVertical?this.touchDirection=Math.abs(i)>5?"vertical":"horizontal":this.touchDirection=Math.abs(t)>Math.abs(i)?"horizontal":"vertical",this.touchDirectionLocked=!0,this.oppositeDirectionCount=0,this.options.debug),this.touchDirectionLocked&&e){const o="horizontal"===this.touchDirection,e=o?t:i,n=o?i:t;Math.abs(n)>Math.abs(e)&&Math.abs(n)>s?(this.oppositeDirectionCount++,1===this.oppositeDirectionCount&&(this.directionChangeStartTime=Date.now()),this.oppositeDirectionCount>=3&&(this.touchDirection=o?"vertical":"horizontal",this.oppositeDirectionCount=0,this.options.debug)):this.oppositeDirectionCount=Math.max(0,this.oppositeDirectionCount-.5);const h="horizontal"===this.touchDirection?this.smoothedDeltaX:this.smoothedDeltaY,a=50;if(0!==this.lastDeltaX||0!==this.lastDeltaY){const o="horizontal"===this.touchDirection?this.lastDeltaX:this.lastDeltaY;if(Math.abs(h-o)>a){const e=o+Math.sign(h-o)*a;return this.lastDeltaX="horizontal"===this.touchDirection?e:t,this.lastDeltaY="vertical"===this.touchDirection?e:i,e}}return this.lastDeltaX=t,this.lastDeltaY=i,h}if(this.touchDirectionLocked)return"horizontal"===this.touchDirection?t:i}if(this.options.prioritizeVertical)return 0!==i?i:t;if(Math.abs(t)>Math.abs(i))return t;if(Math.abs(i)>=Math.abs(t))return i;const o=Math.sqrt(t*t+i*i),e=Math.atan2(i,t);return Math.abs(e)<Math.PI/4||Math.abs(e)>3*Math.PI/4?t>0?o:-o:i>0?o:-o}handleScroll(t,i){if(Math.abs(t)<.5)return;const o=h(),s=e(o+1.5*t,0,n());if(Math.abs(s-o)<.5)return;const a=t>0?1:-1;this.options.debug,window.scrollTo(0,s),this.isScrolling=!0,setTimeout(()=>{this.isScrolling=!1},10);const r={deltaX:"wheel"===i?t:0,deltaY:t,scrollTop:s,direction:a,type:i};this.scrollCallbacks.forEach(t=>{if("function"==typeof t)try{t(r)}catch(t){}})}addToScroll(t){const i=n(),o=this.targetScroll||h();this.targetScroll=e(o+t,0,i),Math.abs(this.targetScroll-o)>.1&&!this.rafId&&(this.options.debug,this.startAnimationLoop())}startAnimationLoop(){void 0!==this.targetScroll&&window.scrollTo(0,this.targetScroll)}smoothScrollTo(t){this.isAnimating&&r(this.rafId);const i=h(),o=t-i;if(Math.abs(o)<1)return;this.options.debug;if(this.options.lerp<=.5)return window.scrollTo(0,t),this.isAnimating=!1,void(this.isScrolling=!1);this.animationFrame={startTime:performance.now(),startPosition:i,targetPosition:t,duration:Math.max(50,this.options.duration/10),easing:this.options.easing},this.isAnimating=!0,this.isScrolling=!0,this.animate()}scrollTo(t,i){const o=e(t,0,n());if(void 0!==i){const t=this.options.duration;this.options.duration=i,this.smoothScrollTo(o),this.options.duration=t}else this.smoothScrollTo(o)}on(t){this.scrollCallbacks.add(t)}off(t){this.scrollCallbacks.delete(t)}disable(){this.options.disabled=!0,this.log("스크롤 비활성화")}enable(){this.options.disabled=!1,this.log("스크롤 활성화")}updateOptions(t){this.options={...this.options,...t},this.log("옵션 업데이트",t)}getCurrentPosition(){return h()}getMaxPosition(){return n()}destroy(){this.isAnimating&&this.rafId&&r(this.rafId),document.removeEventListener("wheel",this.onWheel),document.removeEventListener("touchstart",this.onTouchStart),document.removeEventListener("touchmove",this.onTouchMove),document.removeEventListener("touchend",this.onTouchEnd),document.removeEventListener("keydown",this.onKeyDown),window.removeEventListener("resize",this.onResize),this.isAnimating=!1,this.isScrolling=!1,this.animationFrame=null,this.rafId=null,this.scrollCallbacks.clear(),this.log("TwoDimensionScroll 해제")}log(...t){this.options.debug}}"undefined"!=typeof window&&(window.TwoDimensionScroll=d);export{t as Easing,d as TwoDimensionScroll,r as cancelRaf,e as clamp,c as debounce,d as default,h as getCurrentScrollTop,n as getMaxScrollTop,i as isMobile,o as isTouchDevice,s as lerp,a as raf,l as supportsPassive,u as throttle};