1

I want to move an element based on the velocity of a user's swipe. I have the easing function to make it start fast and get slower over time, but the one variable I need to calculate based on their swipe is totalTime (or totalSteps).

How would I calculate this?

What I know:

  1. The time they started the swipe
  2. The time and distance of each touchmove
  3. The time the swipe ended (touchend)

From that I need to calculate how far to move them (the easing function will handle the distance of each individual step). How do i calculate this?

Easing function:

function easeOutCubic(currTime, beginningValue, change, duration)
{
    return change * ( ( currTime = currTime / duration - 1 ) * currTime * currTime + 1 ) + beginningValue;
}

The change is what i need to calculate.

Don Rhummy
  • 24,730
  • 42
  • 175
  • 330
  • 1
    Possible duplicate of [html5 canvas - touchmove - how to calculate velocity and direction?](http://stackoverflow.com/questions/16091701/html5-canvas-touchmove-how-to-calculate-velocity-and-direction) – Heretic Monkey Jan 12 '17 at 22:54
  • @MikeMcCaughan I read that question and did not find the answer in there. – Don Rhummy Jan 12 '17 at 23:14

2 Answers2

1

To make this works you need to cycle this like my example:

At first you need to get coordinates of first and last touches inside event and store it somewhere outside touch events:

let startCoords = { x: event.touches[0].pageX, y : event.touches[0].pageY } 
let endCoords = { /* same way */ } 

After getting finish coordinates execute this inside touchend event:

const animationTime = 0.5; // Animation time in seconds
const frameRate = 60;

var currentIteration = 0;
var iterationsCount = Math.round(frameRate * animationTime);



(function animate() {

    var x = easeOutCubic(currentIteration, startCoords.x, endCoords.x, iterationsCount);
    var y = easeOutCubic(currentIteration, startCoords.y, endCoords.y, iterationsCount);

                //here you set new x,y to your target element like this
                element.style.top = y +'px';
                element.style.left = x + 'px';


                currentIteration++;

                if (currentIteration < iterationsCount) {
                        requestAnimationFrame(animate);
                }
        })();

UPDATED

To make animation works more efficient you need to use touchmove event instead of touchend firing it within a delay.

Panama Prophet
  • 1,027
  • 6
  • 6
  • Thanks, but this doesn't fully work. Imagine you put your finger on the page and start moving it very slowly (take maybe 2 seconds to move about 40 pixels), then you flick your finger really fast. The page should move a lot and fast, but with your calculation, it took so much time from `touchstart` to `touchend`, it won't have the right velocity. (I tried it on a phone app and it moves a lot, fast) – Don Rhummy Jan 12 '17 at 23:02
  • ok, i know to figure it - you can use a touchmove event instead. with a little debounce on it. – Panama Prophet Jan 12 '17 at 23:10
0

To just get the time between dragstart and dragend:

var el = document.getElementById("foo");
var startTime = 0
var timeDelta = 0;

el.addEventListener('dragstart', function(evt){
  startTime = Date.now()/1000;
  });

el.addEventListener('dragend', function(evt){
  var endTime = Date.now()/1000;
  timeDelta = endTime - startTime;
  console.log(timeDelta);
  });
#foo {
  height: 100px;
  width: 100px;
  background: red;
  }
<div id="foo" draggable="true">
<div>

Obviously you will need to attach other events as well touchstart, touchend, etc.

This approach in action

If I understood your question correctly then this should help. It's pretty much taking touchstart and touchend as reference points and working with them.

/**
 *
 * Copyright 2016 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
'use strict';

class Cards {
  constructor () {
    this.cards = Array.from(document.querySelectorAll('.card'));

    this.onStart = this.onStart.bind(this);
    this.onMove = this.onMove.bind(this);
    this.onEnd = this.onEnd.bind(this);
    this.update = this.update.bind(this);
    this.targetBCR = null;
    this.target = null;
    this.startX = 0;
    this.startTime = 0;
    this.endTime = 0;
    this.currentX = 0;
    this.screenX = 0;
    this.targetX = 0;
    this.lastVelocity = 0;
    this.draggingCard = false;

    this.addEventListeners();

    requestAnimationFrame(this.update);
  }

  addEventListeners () {
    document.addEventListener('touchstart', this.onStart);
    document.addEventListener('touchmove', this.onMove);
    document.addEventListener('touchend', this.onEnd);

    document.addEventListener('mousedown', this.onStart);
    document.addEventListener('mousemove', this.onMove);
    document.addEventListener('mouseup', this.onEnd);
  }

  onStart (evt) {
    if (this.target)
      return;

    if (!evt.target.classList.contains('card'))
      return;

    this.target = evt.target;
    this.targetBCR = this.target.getBoundingClientRect();

    this.startX = evt.pageX || evt.touches[0].pageX;
    this.startTime = Date.now()/1000;
    this.currentX = this.startX;

    this.draggingCard = true;
    this.target.style.willChange = 'transform';

    evt.preventDefault();
  }

  onMove (evt) {
    if (!this.target)
      return;

    this.currentX = evt.pageX || evt.touches[0].pageX;
  }

  onEnd (evt) {
    if (!this.target)
      return;

    this.targetX = 0;
    this.endTime = Date.now() /1000;
    let screenX = this.currentX - this.startX;
    const threshold = this.targetBCR.width * 0.35;
    if (Math.abs(screenX) > threshold) {
      this.targetX = (screenX > 0) ?
           this.targetBCR.width :
          -this.targetBCR.width;
    }

    this.draggingCard = false;
    
    // calculate velocity
    this.lastVelocity = (evt.pageX - this.startX )/ (this.endTime - this.startTime);
    console.log(this.lastVelocity);
  }

  update () {

    requestAnimationFrame(this.update);

    if (!this.target)
      return;

    if (this.draggingCard) {
      this.screenX = this.currentX - this.startX;
    } else {
      this.screenX += this.lastVelocity / 20; // change the number 20 to change the velocity applied to animation
    }
   

    const normalizedDragDistance =
        (Math.abs(this.screenX) / this.targetBCR.width);
    const opacity = 1 - Math.pow(normalizedDragDistance, 3);

    this.target.style.transform = `translateX(${this.screenX}px)`;
    this.target.style.opacity = opacity;

    // User has finished dragging.
    if (this.draggingCard)
      return;

    const isNearlyAtStart = (Math.abs(this.screenX) < 0.1);
    const isNearlyInvisible = (opacity < 0.01);

    // If the card is nearly gone.
    if (isNearlyInvisible) {

      // Bail if there's no target or it's not attached to a parent anymore.
      if (!this.target || !this.target.parentNode)
        return;

      this.target.parentNode.removeChild(this.target);

      const targetIndex = this.cards.indexOf(this.target);
      this.cards.splice(targetIndex, 1);

      // Slide all the other cards.
      this.animateOtherCardsIntoPosition(targetIndex);

    } else if (isNearlyAtStart) {
      this.resetTarget();
    }
  }

  animateOtherCardsIntoPosition (startIndex) {
    // If removed card was the last one, there is nothing to animate.
    // Remove the target.
    if (startIndex === this.cards.length) {
      this.resetTarget();
      return;
    }

    const onAnimationComplete = evt => {
      const card = evt.target;
      card.removeEventListener('transitionend', onAnimationComplete);
      card.style.transition = '';
      card.style.transform = '';

      this.resetTarget();
    };

    // Set up all the card animations.
    for (let i = startIndex; i < this.cards.length; i++) {
      const card = this.cards[i];

      // Move the card down then slide it up.
      card.style.transform = `translateY(${this.targetBCR.height + 20}px)`;
      card.addEventListener('transitionend', onAnimationComplete);
    }

    // Now init them.
    requestAnimationFrame(_ => {
      for (let i = startIndex; i < this.cards.length; i++) {
        const card = this.cards[i];

        // Move the card down then slide it up, with delay according to "distance"
        card.style.transition = `transform 150ms cubic-bezier(0,0,0.31,1) ${i*50}ms`;
        card.style.transform = '';
      }
    });
  }

  resetTarget () {
    if (!this.target)
      return;

    this.target.style.willChange = 'initial';
    this.target.style.transform = 'none';
    this.target = null;
  }
}

window.addEventListener('load', () => new Cards());
/**
 *
 * Copyright 2016 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
html, body {
  margin: 0;
  padding: 0;
  background: #FAFAFA;
  font-family: Arial;
  font-size: 30px;
  color: #333;
}

* {
  box-sizing: border-box;
}

.card-container {
  width: 100%;
  max-width: 450px;
  padding: 16px;
  margin: 0 auto;
}

.card {
  background: #FFF;
  border-radius: 3px;
  box-shadow: 0 3px 4px rgba(0,0,0,0.3);
  margin: 20px 0;
  height: 120px;
  display: flex;
  align-items: center;
  justify-content: space-around;
  cursor: pointer;
}
<!--
Copyright 2016 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="card-container">
    <div class="card">Das Surma</div>
    <div class="card">Aerotwist</div>
    <div class="card">Kinlanimus Maximus</div>
    <div class="card">Addyoooooooooo</div>
    <div class="card">Gaunty McGaunty Gaunt</div>
    <div class="card">Jack Archibungle</div>
    <div class="card">Sam "The Dutts" Dutton</div>
  </div>

I am not the creator of the code, if you need any more samples of performant front end you can find them here and follow aerotwist

Edit

So now it uses the velocity of the actual drag, you still may need some tweaking to get the right feeling, I've added a comment in update function there.

Community
  • 1
  • 1
T.Chmelevskij
  • 1,904
  • 17
  • 30