From 10b451dc1c6efd03c3e0498b35d7ad3a7ce688a2 Mon Sep 17 00:00:00 2001 From: Fred Chasen Date: Sat, 3 Nov 2018 14:34:20 -0700 Subject: [PATCH] Added Snap helper --- examples/embedded.html | 141 +------------- examples/swipe.html | 23 +-- package-lock.json | 2 +- src/contents.js | 2 +- src/managers/continuous/index.js | 32 +++- src/managers/helpers/snap.js | 309 +++++++++++++++++++++++++++++++ src/rendition.js | 10 +- src/utils/constants.js | 2 +- types/rendition.d.ts | 1 + 9 files changed, 362 insertions(+), 160 deletions(-) create mode 100644 src/managers/helpers/snap.js diff --git a/examples/embedded.html b/examples/embedded.html index 50256d2..e9946f8 100644 --- a/examples/embedded.html +++ b/examples/embedded.html @@ -11,24 +11,8 @@ body { margin: 0; - -webkit-scroll-snap-type: mandatory; - -webkit-scroll-snap-points-x: repeat(100%); - -webkit-overflow-scrolling: auto; - - /*This scroll snap functionality is part of a polyfill - that enables the functionality in Chrome.*/ - scroll-snap-type: mandatory; - scroll-snap-points-x: repeat(100%); - width: 100vw; - overflow: auto; } - .epub-container { - margin: 0; - /*-webkit-scroll-snap-type: mandatory; - -webkit-scroll-snap-points-x: repeat(100%);*/ - /* -webkit-overflow-scrolling: touch; */ - } @@ -42,18 +26,15 @@ // Load the opf var book = ePub(url || "https://s3.amazonaws.com/moby-dick/"); var rendition = book.renderTo(document.body, { - // width: "100vw", - // height: "100vh", - overflow: "visible", manager: "continuous", - // flow: "paginated" + snap: true }); rendition.display(currentCfi || currentSectionIndex); rendition.on("rendered", function(section){ - console.log("rendered", section); + // console.log("rendered", section); var nextSection = section.next(); var prevSection = section.prev(); @@ -65,9 +46,9 @@ }); rendition.on("relocated", function(location){ - console.log("locationChanged", location) + // console.log("locationChanged", location) console.log("locationChanged start", location.start.cfi) - console.log("locationChanged end", location.end.cfi) + // console.log("locationChanged end", location.end.cfi) }); window.addEventListener("unload", function () { @@ -76,119 +57,5 @@ }); - - diff --git a/examples/swipe.html b/examples/swipe.html index 0f509f3..6154be4 100644 --- a/examples/swipe.html +++ b/examples/swipe.html @@ -5,8 +5,8 @@ EPUB.js Pagination Example - - + + @@ -35,7 +35,7 @@ height: 96.5%; } #viewer iframe { - pointer-events: none; + /* pointer-events: none; */ } .arrow { position: inherit; @@ -56,7 +56,8 @@ manager: "continuous", flow: "paginated", width: "100%", - height: "100%" + height: "100%", + snap: true }); var displayed = rendition.display("chapter_001.xhtml"); @@ -94,13 +95,13 @@ }, false); - $(window).on( "swipeleft", function( event ) { - rendition.next(); - }); - - $(window).on( "swiperight", function( event ) { - rendition.prev(); - }); + // $(window).on( "swipeleft", function( event ) { + // rendition.next(); + // }); + // + // $(window).on( "swiperight", function( event ) { + // rendition.prev(); + // }); diff --git a/package-lock.json b/package-lock.json index a89789e..ad505b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "epubjs", - "version": "0.3.75", + "version": "0.3.78", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/contents.js b/src/contents.js index 56caa8a..e706977 100644 --- a/src/contents.js +++ b/src/contents.js @@ -814,7 +814,7 @@ class Contents { } DOM_EVENTS.forEach(function(eventName){ - this.document.addEventListener(eventName, this.triggerEvent.bind(this), false); + this.document.addEventListener(eventName, this.triggerEvent.bind(this), { passive: true }); }, this); } diff --git a/src/managers/continuous/index.js b/src/managers/continuous/index.js index 18b119f..917ac41 100644 --- a/src/managers/continuous/index.js +++ b/src/managers/continuous/index.js @@ -1,5 +1,6 @@ import {extend, defer, requestAnimationFrame} from "../../utils/core"; import DefaultViewManager from "../default"; +import Snap from "../helpers/snap"; import { EVENTS } from "../../utils/constants"; import debounce from "lodash/debounce"; @@ -17,7 +18,9 @@ class ContinuousViewManager extends DefaultViewManager { offset: 500, offsetDelta: 250, width: undefined, - height: undefined + height: undefined, + snap: false, + afterScrolledTimeout: 10 }); extend(this.settings, options.settings || {}); @@ -75,10 +78,10 @@ class ContinuousViewManager extends DefaultViewManager { if(!this.isPaginated) { distY = offset.top; - offsetY = offset.top+this.settings.offset; + offsetY = offset.top+this.settings.offsetDelta; } else { distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta; - offsetX = distX+this.settings.offset; + offsetX = distX+this.settings.offsetDelta; } if (distX > 0 || distY > 0) { @@ -371,6 +374,10 @@ class ContinuousViewManager extends DefaultViewManager { }.bind(this)); this.addScrollListeners(); + + if (this.isPaginated && this.settings.snap) { + this.snapper = new Snap(this, this.settings.snap && (typeof this.settings.snap === "object") && this.settings.snap); + } } addScrollListeners() { @@ -455,12 +462,14 @@ class ContinuousViewManager extends DefaultViewManager { this.scrollDeltaHorz = 0; }.bind(this), 150); + clearTimeout(this.afterScrolled); this.didScroll = false; } scrolled() { + this.q.enqueue(function() { this.check(); }.bind(this)); @@ -472,11 +481,18 @@ class ContinuousViewManager extends DefaultViewManager { clearTimeout(this.afterScrolled); this.afterScrolled = setTimeout(function () { + + // Don't report scroll if we are about the snap + if (this.snapper && this.snapper.needsSnap()) { + return; + } + this.emit(EVENTS.MANAGERS.SCROLLED, { top: this.scrollTop, left: this.scrollLeft }); - }.bind(this)); + + }.bind(this), this.settings.afterScrolledTimeout); } next(){ @@ -560,6 +576,14 @@ class ContinuousViewManager extends DefaultViewManager { } } + destroy(){ + super.destroy(); + + if (this.snapper) { + this.snapper.destroy(); + } + } + } export default ContinuousViewManager; diff --git a/src/managers/helpers/snap.js b/src/managers/helpers/snap.js new file mode 100644 index 0000000..927911c --- /dev/null +++ b/src/managers/helpers/snap.js @@ -0,0 +1,309 @@ +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; diff --git a/src/rendition.js b/src/rendition.js index 2980f62..14b6735 100644 --- a/src/rendition.js +++ b/src/rendition.js @@ -8,7 +8,7 @@ import Layout from "./layout"; import Themes from "./themes"; import Contents from "./contents"; import Annotations from "./annotations"; -import { EVENTS } from "./utils/constants"; +import { EVENTS, DOM_EVENTS } from "./utils/constants"; // Default Views import IframeView from "./managers/views/iframe"; @@ -35,6 +35,7 @@ import ContinuousViewManager from "./managers/continuous/index"; * @param {string} [options.stylesheet] url of stylesheet to be injected * @param {boolean} [options.resizeOnOrientationChange] false to disable orientation events * @param {string} [options.script] url of script to be injected + * @param {boolean | object} [options.snap=false] use snap scrolling */ class Rendition { constructor(book, options) { @@ -51,7 +52,8 @@ class Rendition { minSpreadWidth: 800, stylesheet: null, resizeOnOrientationChange: true, - script: null + script: null, + snap: false }); extend(this.settings, options); @@ -852,9 +854,7 @@ class Rendition { * @param {Contents} view contents */ passEvents(contents){ - var listenedEvents = Contents.listenedEvents; - - listenedEvents.forEach((e) => { + DOM_EVENTS.forEach((e) => { contents.on(e, (ev) => this.triggerViewEvent(ev, contents)); }); diff --git a/src/utils/constants.js b/src/utils/constants.js index 11db1c6..49ca80e 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -1,7 +1,7 @@ export const EPUBJS_VERSION = "0.3"; // Dom events to listen for -export const DOM_EVENTS = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart"]; +export const DOM_EVENTS = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart", "touchmove"]; export const EVENTS = { BOOK : { diff --git a/types/rendition.d.ts b/types/rendition.d.ts index a19db74..3d1b08f 100644 --- a/types/rendition.d.ts +++ b/types/rendition.d.ts @@ -23,6 +23,7 @@ export interface RenditionOptions { script?: string, infinite?: boolean, overflow?: string, + snap?: boolean | object, } export interface DisplayedLocation {