Skip to content

Instantly share code, notes, and snippets.

@sekoyo
Created August 8, 2020 12:48
Show Gist options
  • Save sekoyo/b16ff0964ed1f0b8a4dedda3002f7d1c to your computer and use it in GitHub Desktop.
Save sekoyo/b16ff0964ed1f0b8a4dedda3002f7d1c to your computer and use it in GitHub Desktop.
DecayAnimation for scrolling
type UpdateCb = (x: number, y: number) => void;
export interface DecayAnimationProps {
deceleration?: number;
onUpdate: UpdateCb;
}
export class DecayAnimation {
deceleration: number;
_onUpdate: UpdateCb;
active = false;
startTime: number;
lastValue: number;
velocityX: number;
velocityY: number;
animationFrame: number;
fromValueX: number;
fromValueY: number;
lastValueX: number;
lastValueY: number;
constructor(props: DecayAnimationProps) {
this.deceleration = props.deceleration ?? 0.998;
this._onUpdate = props.onUpdate;
}
start(fromValueX: number, fromValueY: number, velocityX: number, velocityY: number) {
this.fromValueX = this.lastValueX = fromValueX;
this.fromValueY = this.lastValueY = fromValueY;
this.velocityX = velocityX;
this.velocityY = velocityY;
this.startTime = performance.now();
this.animationFrame = global.requestAnimationFrame(this.onUpdate);
}
onUpdate = () => {
const now = performance.now();
const valueX =
this.fromValueX +
(this.velocityX / (1 - this.deceleration)) *
(1 - Math.exp(-(1 - this.deceleration) * (now - this.startTime)));
const valueY =
this.fromValueY +
(this.velocityY / (1 - this.deceleration)) *
(1 - Math.exp(-(1 - this.deceleration) * (now - this.startTime)));
this._onUpdate(valueX, valueY);
if (Math.abs(this.lastValueX - valueX) >= 0.1 && Math.abs(this.lastValueY - valueY) >= 0.1) {
this.lastValueX = valueX;
this.lastValueY = valueY;
this.animationFrame = global.requestAnimationFrame(this.onUpdate);
} else {
this.active = false;
}
};
stop() {
this.active = false;
global.cancelAnimationFrame(this.animationFrame);
}
}
import { DecayAnimation } from './index';
type UpdateCb = (x: number, y: number) => void;
interface TouchScrollProps {
el: Element;
updateCallback: UpdateCb;
}
export class TouchScroll {
static GestureTimeout = 200;
el: Element;
updateCallback: UpdateCb;
moved = false;
startTouchX = 0;
startTouchY = 0;
startScrollLeft = 0;
startScrollTop = 0;
startTouchTime = 0;
moveDistX = 0;
moveDistY = 0;
timeDiff = 0;
prevStartTouchX = 0;
prevStartTouchY = 0;
prevStartScrollLeft = 0;
prevStartScrollTop = 0;
prevStartTouchTime = 0;
prevTimeDiff = 0;
decayAnimation: DecayAnimation;
constructor(props: TouchScrollProps) {
this.el = props.el;
this.updateCallback = props.updateCallback;
this.decayAnimation = new DecayAnimation({ onUpdate: this.updateCallback });
this.el.addEventListener('touchstart', this.onTouchStart);
this.el.addEventListener('touchmove', this.onTouchMove);
this.el.addEventListener('touchend', this.onTouchEnd);
}
destroy() {
this.el.removeEventListener('touchstart', this.onTouchStart);
this.el.removeEventListener('touchmove', this.onTouchMove);
this.el.removeEventListener('touchend', this.onTouchEnd);
}
onTouchStart = (e: TouchEvent) => {
this.moved = false;
this.decayAnimation.stop();
console.log('onTouchStart');
const touch = e.changedTouches[0];
this.startTouchX = touch.clientX;
this.startTouchY = touch.clientY;
this.startScrollLeft = this.el.scrollLeft;
this.startScrollTop = this.el.scrollTop;
this.startTouchTime = performance.now();
};
onTouchMove = (e: TouchEvent) => {
this.moved = true;
console.log('onTouchMove');
e.preventDefault();
const touch = e.changedTouches[0];
this.moveDistX = this.startTouchX - touch.clientX;
this.moveDistY = this.startTouchY - touch.clientY;
this.timeDiff = performance.now() - this.startTouchTime;
const nextScrollLeft = this.startScrollLeft + this.moveDistX;
const nextScrollTop = this.startScrollTop + this.moveDistY;
this.updateCallback(nextScrollLeft, nextScrollTop);
this.prevStartTouchX = this.startTouchX;
this.prevStartTouchY = this.startTouchY;
this.prevStartScrollLeft = this.startScrollLeft;
this.prevStartScrollTop = this.startScrollTop;
this.prevStartTouchTime = this.startTouchTime;
this.prevTimeDiff = this.timeDiff;
// Reset the values every ~200ms so we are just judging the velocity of the
// final part of the gesture.
if (this.timeDiff > TouchScroll.GestureTimeout) {
this.startTouchX = touch.clientX;
this.startTouchY = touch.clientY;
this.startScrollLeft = nextScrollLeft;
this.startScrollTop = nextScrollTop;
this.startTouchTime = performance.now();
}
};
onTouchEnd = () => {
if (!this.moved) {
return;
}
this.timeDiff = performance.now() - this.startTouchTime;
// If we didn't have enough time to measure a gesture, use the previous
// measurements.
if (this.timeDiff < TouchScroll.GestureTimeout) {
this.startTouchX = this.prevStartTouchX;
this.startTouchY = this.prevStartTouchY;
this.startScrollLeft = this.prevStartScrollLeft;
this.startScrollTop = this.prevStartScrollTop;
this.startTouchTime = this.prevStartTouchTime;
this.timeDiff = this.prevTimeDiff;
}
const velocityX = this.moveDistX / this.timeDiff;
const velocityY = this.moveDistY / this.timeDiff;
this.decayAnimation.start(this.el.scrollLeft, this.el.scrollTop, velocityX, velocityY);
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment