mirror of
https://github.com/futurepress/epub.js.git
synced 2025-10-04 15:09:16 +02:00
309 lines
7 KiB
JavaScript
309 lines
7 KiB
JavaScript
import {extend, defer, requestAnimationFrame, prefixed} from "../../utils/core";
|
|
import { EVENTS, DOM_EVENTS } from "../../utils/constants";
|
|
import EventEmitter from "event-emitter";
|
|
|
|
// easing equations from https://github.com/danro/easing-js/blob/master/easing.js
|
|
const PI_D2 = (Math.PI / 2);
|
|
const EASING_EQUATIONS = {
|
|
easeOutSine: function (pos) {
|
|
return Math.sin(pos * PI_D2);
|
|
},
|
|
easeInOutSine: function (pos) {
|
|
return (-0.5 * (Math.cos(Math.PI * pos) - 1));
|
|
},
|
|
easeInOutQuint: function (pos) {
|
|
if ((pos /= 0.5) < 1) {
|
|
return 0.5 * Math.pow(pos, 5);
|
|
}
|
|
return 0.5 * (Math.pow((pos - 2), 5) + 2);
|
|
},
|
|
easeInCubic: function(pos) {
|
|
return Math.pow(pos, 3);
|
|
}
|
|
};
|
|
|
|
class Snap {
|
|
constructor(manager, options) {
|
|
|
|
if (this.supportsTouch() === false) {
|
|
return;
|
|
}
|
|
|
|
this.settings = extend({
|
|
duration: 80,
|
|
minVelocity: 0.2,
|
|
minDistance: 10,
|
|
easing: EASING_EQUATIONS['easeInCubic']
|
|
}, options || {});
|
|
|
|
this.setup(manager);
|
|
}
|
|
|
|
setup(manager) {
|
|
this.manager = manager;
|
|
|
|
this.layout = this.manager.layout;
|
|
|
|
this.fullsize = this.manager.settings.fullsize;
|
|
if (this.fullsize) {
|
|
this.element = this.manager.stage.element;
|
|
this.scroller = window;
|
|
this.disableScroll();
|
|
} else {
|
|
this.element = this.manager.stage.container;
|
|
this.scroller = this.element;
|
|
this.element.style["WebkitOverflowScrolling"] = "touch";
|
|
}
|
|
|
|
|
|
this.overflow = this.manager.overflow;
|
|
|
|
// set lookahead offset to page width
|
|
this.manager.settings.offset = this.layout.width;
|
|
this.manager.settings.afterScrolledTimeout = this.settings.duration * 2;
|
|
|
|
this.isVertical = this.manager.settings.axis === "vertical";
|
|
|
|
// disable snapping if not paginated or axis in not horizontal
|
|
if (!this.manager.isPaginated || this.isVertical) {
|
|
return;
|
|
}
|
|
|
|
this.touchCanceler = false;
|
|
this.resizeCanceler = false;
|
|
this.snapping = false;
|
|
|
|
|
|
this.scrollLeft;
|
|
this.scrollTop;
|
|
|
|
this.startTouchX = undefined;
|
|
this.startTouchY = undefined;
|
|
this.startTime = undefined;
|
|
this.endTouchX = undefined;
|
|
this.endTouchY = undefined;
|
|
this.endTime = undefined;
|
|
|
|
this.addListeners();
|
|
}
|
|
|
|
supportsTouch() {
|
|
if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
disableScroll() {
|
|
this.element.style.overflow = "hidden";
|
|
}
|
|
|
|
enableScroll() {
|
|
this.element.style.overflow = "scroll";
|
|
}
|
|
|
|
addListeners() {
|
|
|
|
window.addEventListener('resize', this.onResize.bind(this));
|
|
|
|
this.scroller.addEventListener('scroll', this.onScroll.bind(this));
|
|
|
|
window.addEventListener('touchstart', this.onTouchStart.bind(this), { passive: true });
|
|
this.on('touchstart', this.onTouchStart.bind(this));
|
|
|
|
window.addEventListener('touchmove', this.onTouchMove.bind(this), { passive: true });
|
|
this.on('touchmove', this.onTouchMove.bind(this));
|
|
|
|
window.addEventListener('touchend', this.onTouchEnd.bind(this), { passive: true });
|
|
this.on('touchend', this.onTouchEnd.bind(this));
|
|
|
|
this.manager.on(EVENTS.MANAGERS.ADDED, this.afterDisplayed.bind(this));
|
|
}
|
|
|
|
afterDisplayed(view) {
|
|
let contents = view.contents;
|
|
["touchstart", "touchmove", "touchend"].forEach((e) => {
|
|
contents.on(e, (ev) => this.triggerViewEvent(ev, contents));
|
|
});
|
|
}
|
|
|
|
triggerViewEvent(e, contents){
|
|
this.emit(e.type, e, contents);
|
|
}
|
|
|
|
onScroll(e) {
|
|
this.scrollLeft = this.fullsize ? window.scrollX : this.scroller.scrollLeft;
|
|
this.scrollTop = this.fullsize ? window.scrollY : this.scroller.scrollTop;
|
|
}
|
|
|
|
onResize(e) {
|
|
this.resizeCanceler = true;
|
|
}
|
|
|
|
onTouchStart(e) {
|
|
let { screenX, screenY } = e.touches[0];
|
|
|
|
if (this.fullsize) {
|
|
this.enableScroll();
|
|
}
|
|
|
|
this.touchCanceler = true;
|
|
|
|
if (!this.startTouchX) {
|
|
this.startTouchX = screenX;
|
|
this.startTouchY = screenY;
|
|
this.startTime = this.now();
|
|
}
|
|
|
|
this.endTouchX = screenX;
|
|
this.endTouchY = screenY;
|
|
this.endTime = this.now();
|
|
}
|
|
|
|
onTouchMove(e) {
|
|
let { screenX, screenY } = e.touches[0];
|
|
let deltaY = Math.abs(screenY - this.endTouchY);
|
|
|
|
this.touchCanceler = true;
|
|
|
|
|
|
if (!this.fullsize && deltaY < 10) {
|
|
this.element.scrollLeft -= screenX - this.endTouchX;
|
|
}
|
|
|
|
this.endTouchX = screenX;
|
|
this.endTouchY = screenY;
|
|
this.endTime = this.now();
|
|
}
|
|
|
|
onTouchEnd(e) {
|
|
if (this.fullsize) {
|
|
this.disableScroll();
|
|
}
|
|
|
|
this.touchCanceler = false;
|
|
|
|
let swipped = this.wasSwiped();
|
|
|
|
if (swipped !== 0) {
|
|
this.snap(swipped);
|
|
} else {
|
|
this.snap();
|
|
}
|
|
|
|
this.startTouchX = undefined;
|
|
this.startTouchY = undefined;
|
|
this.startTime = undefined;
|
|
this.endTouchX = undefined;
|
|
this.endTouchY = undefined;
|
|
this.endTime = undefined;
|
|
}
|
|
|
|
wasSwiped() {
|
|
let snapWidth = this.layout.pageWidth * this.layout.divisor;
|
|
let distance = (this.endTouchX - this.startTouchX);
|
|
let absolute = Math.abs(distance);
|
|
let time = this.endTime - this.startTime;
|
|
let velocity = (distance / time);
|
|
let minVelocity = this.settings.minVelocity;
|
|
|
|
if (absolute <= this.settings.minDistance || absolute >= snapWidth) {
|
|
return 0;
|
|
}
|
|
|
|
if (velocity > minVelocity) {
|
|
// previous
|
|
return -1;
|
|
} else if (velocity < -minVelocity) {
|
|
// next
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
needsSnap() {
|
|
let left = this.scrollLeft;
|
|
let snapWidth = this.layout.pageWidth * this.layout.divisor;
|
|
return (left % snapWidth) !== 0;
|
|
}
|
|
|
|
snap(howMany=0) {
|
|
let left = this.scrollLeft;
|
|
let snapWidth = this.layout.pageWidth * this.layout.divisor;
|
|
let snapTo = Math.round(left / snapWidth) * snapWidth;
|
|
|
|
if (howMany) {
|
|
snapTo += (howMany * snapWidth);
|
|
}
|
|
|
|
return this.smoothScrollTo(snapTo);
|
|
}
|
|
|
|
smoothScrollTo(destination) {
|
|
const deferred = new defer();
|
|
const start = this.scrollLeft;
|
|
const startTime = this.now();
|
|
|
|
const duration = this.settings.duration;
|
|
const easing = this.settings.easing;
|
|
|
|
this.snapping = true;
|
|
|
|
// add animation loop
|
|
function tick() {
|
|
const now = this.now();
|
|
const time = Math.min(1, ((now - startTime) / duration));
|
|
const timeFunction = easing(time);
|
|
|
|
|
|
if (this.touchCanceler || this.resizeCanceler) {
|
|
this.resizeCanceler = false;
|
|
this.snapping = false;
|
|
deferred.resolve();
|
|
return;
|
|
}
|
|
|
|
if (time < 1) {
|
|
window.requestAnimationFrame(tick.bind(this));
|
|
this.scrollTo(start + ((destination - start) * time), 0);
|
|
} else {
|
|
this.scrollTo(destination, 0);
|
|
this.snapping = false;
|
|
deferred.resolve();
|
|
}
|
|
}
|
|
|
|
tick.call(this);
|
|
|
|
return deferred.promise;
|
|
}
|
|
|
|
scrollTo(left=0, top=0) {
|
|
if (this.fullsize) {
|
|
window.scroll(left, top);
|
|
} else {
|
|
this.scroller.scrollLeft = left;
|
|
this.scroller.scrollTop = top;
|
|
}
|
|
}
|
|
|
|
now() {
|
|
return ('now' in window.performance) ? performance.now() : new Date().getTime();
|
|
}
|
|
|
|
destroy() {
|
|
this.scroller.removeEventListener('scroll', this.onScroll.bind(this));
|
|
|
|
window.removeEventListener('resize', this.onResize.bind(this));
|
|
|
|
window.removeEventListener('touchstart', this.onTouchStart.bind(this), { passive: true });
|
|
|
|
window.removeEventListener('touchmove', this.onTouchMove.bind(this), { passive: true });
|
|
|
|
window.removeEventListener('touchend', this.onTouchEnd.bind(this), { passive: true });
|
|
}
|
|
}
|
|
|
|
EventEmitter(Snap.prototype);
|
|
|
|
export default Snap;
|