Add easing/smooth scroll to click and drag with js
Asked Answered
B

2

6

I found a simple Codepen which allows me to drag and scroll a gallery with images. It is working fine, but I need a way to add "smooth grabbing/scrolling" to this function. Basically I want to emulate the scroll for example on an iPhone.

Can someone please help me out. I am a total beginner in Javascript. Here is the link to the code: Horizontal Click and Drag Scrolling with JS

const slider = document.querySelector('.items');
let isDown = false;
let startX;
let scrollLeft;

slider.addEventListener('mousedown', (e) => {
  isDown = true;
  slider.classList.add('active');
  startX = e.pageX - slider.offsetLeft;
  scrollLeft = slider.scrollLeft;
});
slider.addEventListener('mouseleave', () => {
  isDown = false;
  slider.classList.remove('active');
});
slider.addEventListener('mouseup', () => {
  isDown = false;
  slider.classList.remove('active');
});
slider.addEventListener('mousemove', (e) => {
  if(!isDown) return;
  e.preventDefault();
  const x = e.pageX - slider.offsetLeft;
  const walk = (x - startX) * 3; //scroll-fast
  slider.scrollLeft = scrollLeft - walk;
  console.log(walk);
});

Thanks in advance.

Biforate answered 23/11, 2019 at 14:1 Comment(1)
See this answer for a possible vanilla JavaScript solution - https://mcmap.net/q/1769666/-horizontal-scroll-with-momentum-in-vanilla-jsBollworm
S
16

If I understand your question correctly you'd like to emulate the way that a scrolling item slows to a stop after you release it on iOS.

1) First we need to track it's speed when it's being dragged by adding the following 2 lines to the mousemove event listener:

var velX;
slider.addEventListener('mousemove', (e) => {
  if(!isDown) return;
  e.preventDefault();
  const x = e.pageX - slider.offsetLeft;
  const walk = (x - startX) * 3; 
  // Store the previous scroll position
  var prevScrollLeft = slider.scrollLeft; 
  slider.scrollLeft = scrollLeft - walk;
  // Compare change in position to work out drag speed 
  velX = slider.scrollLeft - prevScrollLeft; 
});

2) When the drag is complete we create a frame loop that keeps scrolling at the drag velocity, slowing it down each iteration until it comes to a stop.

slider.addEventListener('mouseup', () => {
  isDown = false;
  slider.classList.remove('active');
  beginMomentumTracking(); // Start a frame loop to continue drag momentum 
});

// Momentum 

var momentumID;

function beginMomentumTracking(){
  cancelMomentumTracking();
  momentumID = requestAnimationFrame(momentumLoop); 
}

function cancelMomentumTracking(){
  cancelAnimationFrame(momentumID);
}

function momentumLoop(){
  slider.scrollLeft += velX; // Apply the velocity to the scroll position
  velX *= 0.95; // Slow the velocity slightly
  if (Math.abs(velX) > 0.5){ // Still moving?
    momentumID = requestAnimationFrame(momentumLoop); // Keep looping 
  }
}

3) Finally cancel the momentum loop when the user begins to interact with the scroll item

slider.addEventListener('mousedown', (e) => {
  isDown = true;
  slider.classList.add('active');
  startX = e.pageX - slider.offsetLeft;
  scrollLeft = slider.scrollLeft;
  cancelMomentumTracking(); // Stop the drag momentum loop
});

// Listen for mouse wheel events
slider.addEventListener('wheel', (e) => {
  cancelMomentumTracking(); // Stop the drag momentum loop
});  

See it working here:
https://codepen.io/loxks/details/KKpVvVW

Sanguinolent answered 14/2, 2020 at 1:11 Comment(0)
Y
-2

You can try this https://utsb-fmm.github.io/MobileLikeScroller/

More natural scrolling I found.

Here is the code:

class MobileLikeScroller {
constructor(elem,direction='xy') {
    this.previousTouchX=[0,0,0];
    this.previousTouchY=[0,0,0];
    this.previousTouchTime=[0,0,0];
    this.direction=direction;
    this.scrollAtT0=[0,0];
    this.inertialTimerInterval = null;
    this.target = elem;
    this.childrenEventListeners = [];
    this.childEventObject=null;
    this.blockChildrenTimeout = null;
    this.$BlockedInputs=[];

    $(elem).on('mousedown', (e) => this.touchstart(e));

}

touchstart(e) {
    if (e.button === 0) { // Check for left click
        e.preventDefault();
        $(this.target).css('cursor', 'grabbing');
        this.previousTouchX = [e.pageX, e.pageX, e.pageX];
        this.previousTouchY = [e.pageY, e.pageY, e.pageY];
        this.previousTouchTime = [Date.now() - 2, Date.now() - 1, Date.now()];
        $(document).on('mousemove.scroller', (e) => this.touchmove(e));
        $(document).on('mouseup.scroller', (e) => this.touchend(e));
        $(document).on('click.scroller', (e) => this.click(e));
        if (this.inertialTimerInterval) {
            clearInterval(this.inertialTimerInterval);
            this.inertialTimerInterval=null;
        }
        //The two following lines are for blocking clicks on child items after 300 ms of long press (i.e swiping)
        this.childEventObject=null;
        this.blockChildrenTimeout = setTimeout(() => { this.preventChildClicks(); },300); // Prevent children from being clicked after 300ms when we are sure that the user is grabbing the parent to scroll
    }
}

touchmove(e) {
    this.previousTouchX = [this.previousTouchX[1], this.previousTouchX[2], e.pageX];
    this.previousTouchY = [this.previousTouchY[1], this.previousTouchY[2], e.pageY];
    this.previousTouchTime = [this.previousTouchTime[1], this.previousTouchTime[2], Date.now()];
    if(this.direction!='y') this.target.scrollLeft -= this.previousTouchX[2] - this.previousTouchX[1];
    if(this.direction!='x') this.target.scrollTop -= this.previousTouchY[2] - this.previousTouchY[1];

    if(this.blockChildrenTimeout && (this.previousTouchX[2] - this.previousTouchX[1])**2+(this.previousTouchY[2] - this.previousTouchY[1])**2>25) { 
        // If fast mouse movement, this is not a click on children, do not wait 300ms
        this.preventChildClicks();
    }
    $(this.target).trigger('scroll');
    
}

touchend(e) {
    $(document).off('mousemove.scroller mouseup.scroller');
    $(this.target).css('cursor', '');
    this.scrollAtT0 = [$(this.target).scrollLeft(), $(this.target).scrollTop()];
    this.inertialTimerInterval = setInterval(() => this.inertialmove(), 16);
    $(this.target).trigger('initiateinertial');
}

click(e) { // Click is trigger after mouseup. Parent click is trigger after child click so we cannot remove child click before
    $(document).off('click.scroller');
    if(this.blockChildrenTimeout===null) {
        this.childrenEventListeners.forEach((t) => {
            t[0].removeEventListener('click', t[1], true);
        }); //,true make this event prioritary
        this.childrenEventListeners=[];
        setTimeout(() => { // The event for the change is done after the click event, so we need to wait for the click event to be done before re-enabling the inputs
            this.$BlockedInputs.prop('disabled',false);
            this.$BlockedInputs=[];
        },0);
    }
    else {
        clearTimeout(this.blockChildrenTimeout);
        this.blockChildrenTimeout=null;
    }
}

preventChildClicks() {
    $(this.target).find('*').each((i,elem) => {
        let listener = (e) => this.childclick(e);
        elem.addEventListener('click', listener, true)
        this.childrenEventListeners.push([elem,listener]);
    });
    this.$BlockedInputs=$(this.target).find('input:not(:disabled)').prop('disabled',true);
    clearInterval(this.blockChildrenTimeout);
    this.blockChildrenTimeout=null;
}
childclick(e) {
    e.stopPropagation();
    this.click(e);
}

inertialmove() {
    var v0X = 0, v0Y = 0;
    if(this.direction!='y') v0X = (this.previousTouchX[2] - this.previousTouchX[0]) / (this.previousTouchTime[2] - this.previousTouchTime[0])*1000/$(this.target).width();  // page per second    
    if(this.direction!='x') v0Y = (this.previousTouchY[2] - this.previousTouchY[0]) / (this.previousTouchTime[2] - this.previousTouchTime[0])*1000/$(this.target).height();  // page per second

    var av0 = this.direction=='xy'?Math.sqrt(v0X*v0X+v0Y*v0Y):(this.direction=='y'?Math.abs(v0Y):Math.abs(v0X));
    var unitVector = [v0X / av0, v0Y / av0];
    av0 = Math.min(12, Math.max(-12, 1.2*av0));
    
    var t = (Date.now() - this.previousTouchTime[2])/1000;
    var v = av0 - 14.278 * t + 75.24 * t * t / av0 - 149.72 * t * t * t / av0 / av0; //This is the equation of inertia determined by reverse engineering on chrome. A Clear better experience.
    
    if (av0 == 0 || v <= 0 || isNaN(av0)) {
        clearInterval(this.inertialTimerInterval);
        this.inertialTimerInterval = null;
        $(this.target).trigger('scrollend');
    } else {
        var deltaX = $(this.target).width()*unitVector[0] * (av0 * t - 7.1397 * t * t + 25.08 * t * t * t / av0 - 37.43 * t * t * t * t / av0 / av0);
        var deltaY = $(this.target).height()*unitVector[1] * (av0 * t - 7.1397 * t * t + 25.08 * t * t * t / av0 - 37.43 * t * t * t * t / av0 / av0);
        let maxScroll = [this.target.scrollWidth - $(this.target).width(), this.target.scrollHeight - $(this.target).height()];
        let newScroll = [Math.min(maxScroll[0],Math.max(0,this.scrollAtT0[0] - deltaX)), Math.min(maxScroll[1],Math.max(0,this.scrollAtT0[1] - deltaY))];
        
        if ((newScroll[0]==0 || newScroll[0]==maxScroll[0]) && (newScroll[1]==0 || newScroll[1]==maxScroll[1]))  {
            clearInterval(this.inertialTimerInterval);
            this.inertialTimerInterval = null;
        }
        if(this.direction!='y')
            this.target.scrollLeft = newScroll[0];
        if(this.direction!='x')
            this.target.scrollTop = newScroll[1];
        $(this.target).trigger('scroll');
    }
}
}
Yang answered 27/6, 2023 at 12:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.