UNPKG

dmpixi

Version:

多卖pixi 淘宝小程序 支持骨骼动画,整屏滚动等

1,154 lines (921 loc) 39.5 kB
/* * Scroller * http://github.com/zynga/scroller * * Copyright 2011, Zynga Inc. * Licensed under the MIT License. * https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt * * Based on the work of: Unify Project (unify-project.org) * http://unify-project.org * Copyright 2011, Deutsche Telekom AG * License: MIT + Apache (V2) */ (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD define(['./animate'], factory); } else if (typeof module === 'object') { // CommonJS module.exports = factory(require('./animate')); } else { // Browser globals root.Scroller = factory(root.animate); } }(this, function (animate) { var NOOP = function () { }; /** * A pure logic 'component' for 'virtual' scrolling/zooming. */ var Scroller = function (callback, options) { this.__callback = callback; this.options = { /** Enable scrolling on x-axis */ scrollingX: true, /** Enable scrolling on y-axis */ scrollingY: true, /** Enable animations for deceleration, snap back, zooming and scrolling */ animating: true, /** duration for animations triggered by scrollTo/zoomTo */ animationDuration: 250, /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */ bouncing: true, /** Enable locking to the main axis if user moves only slightly on one of them at start */ locking: true, /** Enable pagination mode (switching between full page content panes) */ paging: false, /** Enable snapping of content to a configured pixel grid */ snapping: false, /** Enable zooming of content via API, fingers and mouse wheel */ zooming: false, /** Minimum zoom level */ minZoom: 0.5, /** Maximum zoom level */ maxZoom: 3, /** Multiply or decrease scrolling speed **/ speedMultiplier: 1, /** Callback that is fired on the later of touch end or deceleration end, provided that another scrolling action has not begun. Used to know when to fade out a scrollbar. */ scrollingComplete: NOOP, /** This configures the amount of change applied to deceleration when reaching boundaries **/ penetrationDeceleration: 0.03, /** This configures the amount of change applied to acceleration when reaching boundaries **/ penetrationAcceleration: 0.08 }; for (var key in options) { this.options[key] = options[key]; } }; // Easing Equations (c) 2003 Robert Penner, all rights reserved. // Open source under the BSD License. /** * @param pos {Number} position between 0 (start of effect) and 1 (end of effect) **/ var easeOutCubic = function (pos) { return (Math.pow((pos - 1), 3) + 1); }; /** * @param pos {Number} position between 0 (start of effect) and 1 (end of effect) **/ var easeInOutCubic = function (pos) { if ((pos /= 0.5) < 1) { return 0.5 * Math.pow(pos, 3); } return 0.5 * (Math.pow((pos - 2), 3) + 2); }; Scroller.prototype = { /* --------------------------------------------------------------------------- INTERNAL FIELDS :: STATUS --------------------------------------------------------------------------- */ /** {Boolean} Whether only a single finger is used in touch handling */ __isSingleTouch: false, /** {Boolean} Whether a touch event sequence is in progress */ __isTracking: false, /** {Boolean} Whether a deceleration animation went to completion. */ __didDecelerationComplete: false, /** * {Boolean} Whether a gesture zoom/rotate event is in progress. Activates when * a gesturestart event happens. This has higher priority than dragging. */ __isGesturing: false, /** * {Boolean} Whether the user has moved by such a distance that we have enabled * dragging mode. Hint: It's only enabled after some pixels of movement to * not interrupt with clicks etc. */ __isDragging: false, /** * {Boolean} Not touching and dragging anymore, and smoothly animating the * touch sequence using deceleration. */ __isDecelerating: false, /** * {Boolean} Smoothly animating the currently configured change */ __isAnimating: false, /* --------------------------------------------------------------------------- INTERNAL FIELDS :: DIMENSIONS --------------------------------------------------------------------------- */ /** {Integer} Viewport left boundary */ __clientLeft: 0, /** {Integer} Viewport right boundary */ __clientTop: 0, /** {Integer} Viewport width */ __clientWidth: 0, /** {Integer} Viewport height */ __clientHeight: 0, /** {Integer} Full content's width */ __contentWidth: 0, /** {Integer} Full content's height */ __contentHeight: 0, /** {Integer} Snapping width for content */ __snapWidth: 100, /** {Integer} Snapping height for content */ __snapHeight: 100, /** {Number} Zoom level */ __zoomLevel: 1, /** {Number} Scroll position on x-axis */ __scrollLeft: 0, /** {Number} Scroll position on y-axis */ __scrollTop: 0, /** {Integer} Maximum allowed scroll position on x-axis */ __maxScrollLeft: 0, /** {Integer} Maximum allowed scroll position on y-axis */ __maxScrollTop: 0, /* {Number} Scheduled left position (final position when animating) */ __scheduledLeft: 0, /* {Number} Scheduled top position (final position when animating) */ __scheduledTop: 0, /* {Number} Scheduled zoom level (final scale when animating) */ __scheduledZoom: 0, /* --------------------------------------------------------------------------- INTERNAL FIELDS :: LAST POSITIONS --------------------------------------------------------------------------- */ /** {Number} Left position of finger at start */ __lastTouchLeft: null, /** {Number} Top position of finger at start */ __lastTouchTop: null, /** {Date} Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */ __lastTouchMove: null, /** {Array} List of positions, uses three indexes for each state: left, top, timestamp */ __positions: null, /* --------------------------------------------------------------------------- INTERNAL FIELDS :: DECELERATION SUPPORT --------------------------------------------------------------------------- */ /** {Integer} Minimum left scroll position during deceleration */ __minDecelerationScrollLeft: null, /** {Integer} Minimum top scroll position during deceleration */ __minDecelerationScrollTop: null, /** {Integer} Maximum left scroll position during deceleration */ __maxDecelerationScrollLeft: null, /** {Integer} Maximum top scroll position during deceleration */ __maxDecelerationScrollTop: null, /** {Number} Current factor to modify horizontal scroll position with on every step */ __decelerationVelocityX: null, /** {Number} Current factor to modify vertical scroll position with on every step */ __decelerationVelocityY: null, /* --------------------------------------------------------------------------- PUBLIC API --------------------------------------------------------------------------- */ /** * Configures the dimensions of the client (outer) and content (inner) elements. * Requires the available space for the outer element and the outer size of the inner element. * All values which are falsy (null or zero etc.) are ignored and the old value is kept. * * @param clientWidth {Integer ? null} Inner width of outer element * @param clientHeight {Integer ? null} Inner height of outer element * @param contentWidth {Integer ? null} Outer width of inner element * @param contentHeight {Integer ? null} Outer height of inner element */ setDimensions: function (clientWidth, clientHeight, contentWidth, contentHeight) { // Only update values which are defined if (clientWidth !== null) { this.__clientWidth = clientWidth; } if (clientHeight !== null) { this.__clientHeight = clientHeight; } if (contentWidth !== null) { this.__contentWidth = contentWidth; } if (contentHeight !== null) { this.__contentHeight = contentHeight; } // Refresh maximums this.__computeScrollMax(); // Refresh scroll position this.scrollTo(this.__scrollLeft, this.__scrollTop, true); }, /** * Sets the client coordinates in relation to the document. * * @param left {Integer ? 0} Left position of outer element * @param top {Integer ? 0} Top position of outer element */ setPosition: function (left, top) { this.__clientLeft = left || 0; this.__clientTop = top || 0; }, /** * Configures the snapping (when snapping is active) * * @param width {Integer} Snapping width * @param height {Integer} Snapping height */ setSnapSize: function (width, height) { this.__snapWidth = width; this.__snapHeight = height; }, /** * Returns the scroll position and zooming values * * @return {Map} `left` and `top` scroll position and `zoom` level */ getValues: function () { return { left: this.__scrollLeft, top: this.__scrollTop, right: this.__scrollLeft + this.__clientWidth / this.__zoomLevel, bottom: this.__scrollTop + this.__clientHeight / this.__zoomLevel, zoom: this.__zoomLevel }; }, /** * Get point in in content space from scroll coordinates. */ getPoint: function (scrollLeft, scrollTop) { var values = this.getValues(); return { left: scrollLeft / values.zoom, top: scrollTop / values.zoom }; }, /** * Returns the maximum scroll values * * @return {Map} `left` and `top` maximum scroll values */ getScrollMax: function () { return { left: this.__maxScrollLeft, top: this.__maxScrollTop }; }, /** * Zooms to the given level. Supports optional animation. Zooms * the center when no coordinates are given. * * @param level {Number} Level to zoom to * @param isAnimated {Boolean ? false} Whether to use animation * @param fixedLeft {Number ? undefined} Stationary point's left coordinate (vector in client space) * @param fixedTop {Number ? undefined} Stationary point's top coordinate (vector in client space) * @param callback {Function ? null} A callback that gets fired when the zoom is complete. */ zoomTo: function (level, isAnimated, fixedLeft, fixedTop, callback) { if (!this.options.zooming) { throw new Error("Zooming is not enabled!"); } // Add callback if exists if (callback) { this.__zoomComplete = callback; } // Stop deceleration if (this.__isDecelerating) { animate.stop(this.__isDecelerating); this.__isDecelerating = false; } var oldLevel = this.__zoomLevel; // Normalize fixed point to center of viewport if not defined if (fixedLeft === undefined) { fixedLeft = this.__clientWidth / 2; } if (fixedTop === undefined) { fixedTop = this.__clientHeight / 2; } // Limit level according to configuration level = Math.max(Math.min(level, this.options.maxZoom), this.options.minZoom); // Recompute maximum values while temporary tweaking maximum scroll ranges this.__computeScrollMax(level); // Recompute left and top scroll positions based on new zoom level. // Choosing the new viewport so that the origin's position remains // fixed, we have central dilation about the origin. // * Fixed point, $F$, remains stationary in content space and in the // viewport. // * Initial scroll position, $S_i$, in content space. // * Final scroll position, $S_f$, in content space. // * Initial scaling factor, $k_i$. // * Final scaling factor, $k_f$. // // * $S_i \mapsto S_f$. // * $(S_i - F) k_i = (S_f - F) k_f$. // * $(S_i - F) k_i/k_f = (S_f - F)$. // * $S_f = F + (S_i - F) k_i/k_f$. // // Fixed point location, $\vector{f} = (F - S_i) k_i$. // * $F = S_i + \vector{f}/k_i$. // * $S_f = S_i + \vector{f}/k_i + (S_i - S_i - \vector{f}/k_i) k_i/k_f$. // * $S_f = S_i + \vector{f}/k_i - \vector{f}/k_f$. // * $S_f k_f = S_i k_f + (k_f/k_i - 1)\vector{f}$. // * $S_f k_f = (k_f/k_i)(S_i k_i) + (k_f/k_i - 1) \vector{f}$. var k = level / oldLevel; var left = k * (this.__scrollLeft + fixedLeft) - fixedLeft; var top = k * (this.__scrollTop + fixedTop) - fixedTop; // Limit x-axis if (left > this.__maxScrollLeft) { left = this.__maxScrollLeft; } else if (left < 0) { left = 0; } // Limit y-axis if (top > this.__maxScrollTop) { top = this.__maxScrollTop; } else if (top < 0) { top = 0; } // Push values out this.__publish(left, top, level, isAnimated); }, /** * Zooms the content by the given factor. * * @param factor {Number} Zoom by given factor * @param isAnimated {Boolean ? false} Whether to use animation * @param originLeft {Number ? 0} Zoom in at given left coordinate * @param originTop {Number ? 0} Zoom in at given top coordinate * @param callback {Function ? null} A callback that gets fired when the zoom is complete. */ zoomBy: function (factor, isAnimated, originLeft, originTop, callback) { this.zoomTo(this.__zoomLevel * factor, isAnimated, originLeft, originTop, callback); }, /** * Scrolls to the given position. Respect limitations and snapping automatically. * * @param left {Number?null} Horizontal scroll position, keeps current if value is <code>null</code> * @param top {Number?null} Vertical scroll position, keeps current if value is <code>null</code> * @param isAnimated {Boolean?false} Whether the scrolling should happen using an animation * @param zoom {Number} [1.0] Zoom level to go to */ scrollTo: function (left, top, isAnimated, zoom) { // Stop deceleration if (this.__isDecelerating) { animate.stop(this.__isDecelerating); this.__isDecelerating = false; } // Correct coordinates based on new zoom level if (zoom !== undefined && zoom !== this.__zoomLevel) { if (!this.options.zooming) { throw new Error("Zooming is not enabled!"); } left *= zoom; top *= zoom; // Recompute maximum values while temporary tweaking maximum scroll ranges this.__computeScrollMax(zoom); } else { // Keep zoom when not defined zoom = this.__zoomLevel; } if (!this.options.scrollingX) { left = this.__scrollLeft; } else { if (this.options.paging) { left = Math.round(left / this.__clientWidth) * this.__clientWidth; } else if (this.options.snapping) { left = Math.round(left / this.__snapWidth) * this.__snapWidth; } } if (!this.options.scrollingY) { top = this.__scrollTop; } else { if (this.options.paging) { top = Math.round(top / this.__clientHeight) * this.__clientHeight; } else if (this.options.snapping) { top = Math.round(top / this.__snapHeight) * this.__snapHeight; } } // Limit for allowed ranges left = Math.max(Math.min(this.__maxScrollLeft, left), 0); top = Math.max(Math.min(this.__maxScrollTop, top), 0); // Don't animate when no change detected, still call publish to make sure // that rendered position is really in-sync with internal data if (left === this.__scrollLeft && top === this.__scrollTop) { isAnimated = false; } // Publish new values this.__publish(left, top, zoom, isAnimated); }, /** * Scroll by the given offset * * @param left {Number ? 0} Scroll x-axis by given offset * @param top {Number ? 0} Scroll x-axis by given offset * @param isAnimated {Boolean ? false} Whether to animate the given change */ scrollBy: function (left, top, isAnimated) { var startLeft = this.__isAnimating ? this.__scheduledLeft : this.__scrollLeft; var startTop = this.__isAnimating ? this.__scheduledTop : this.__scrollTop; this.scrollTo(startLeft + (left || 0), startTop + (top || 0), isAnimated); }, /* --------------------------------------------------------------------------- EVENT CALLBACKS --------------------------------------------------------------------------- */ /** * Mouse wheel handler for zooming support */ doMouseZoom: function (wheelDelta, timeStamp, pageX, pageY) { var change = wheelDelta > 0 ? 0.97 : 1.03; return this.zoomTo(this.__zoomLevel * change, false, pageX - this.__clientLeft, pageY - this.__clientTop); }, /** * Touch start handler for scrolling support */ doTouchStart: function (touches, timeStamp) { // Array-like check is enough here if (touches.length === undefined) { throw new Error("Invalid touch list: " + touches); } if (timeStamp instanceof Date) { timeStamp = timeStamp.valueOf(); } if (typeof timeStamp !== "number") { throw new Error("Invalid timestamp value: " + timeStamp); } // Reset interruptedAnimation flag this.__interruptedAnimation = true; // Stop deceleration if (this.__isDecelerating) { animate.stop(this.__isDecelerating); this.__isDecelerating = false; this.__interruptedAnimation = true; } // Stop animation if (this.__isAnimating) { animate.stop(this.__isAnimating); this.__isAnimating = false; this.__interruptedAnimation = true; } // Use center point when dealing with two fingers var currentTouchLeft, currentTouchTop; var isSingleTouch = touches.length === 1; if (isSingleTouch) { currentTouchLeft = touches[0].clientX || touches[0].x || 0; currentTouchTop = touches[0].clientY || touches[0].y || 0; } else if (touches.length >= 2) { currentTouchLeft = Math.abs(touches[0].clientX || touches[0].x || 0 + touches[1].clientX || touches[1].x || 0) / 2; currentTouchTop = Math.abs(touches[0].clientY || touches[0].y || 0 + touches[1].clientY || touches[1].y || 0) / 2; } // Store initial positions this.__initialTouchLeft = currentTouchLeft; this.__initialTouchTop = currentTouchTop; // Store current zoom level this.__zoomLevelStart = this.__zoomLevel; // Store initial touch positions this.__lastTouchLeft = currentTouchLeft; this.__lastTouchTop = currentTouchTop; // Store initial move time stamp this.__lastTouchMove = timeStamp; // Reset initial scale this.__lastScale = 1; // Reset locking flags this.__enableScrollX = !isSingleTouch && this.options.scrollingX; this.__enableScrollY = !isSingleTouch && this.options.scrollingY; // Reset tracking flag this.__isTracking = true; // Reset deceleration complete flag this.__didDecelerationComplete = false; // Dragging starts directly with two fingers, otherwise lazy with an offset this.__isDragging = !isSingleTouch; // Some features are disabled in multi touch scenarios this.__isSingleTouch = isSingleTouch; // Clearing data structure this.__positions = []; }, /** * Touch move handler for scrolling support * @param {Number} [1.0] scale - .... */ doTouchMove: function (touches, timeStamp, scale) { // Array-like check is enough here if (!touches || !touches.length || JSON.stringify(touches) == "{}") { throw new Error("Invalid touch list: " + touches); } if (timeStamp instanceof Date) { timeStamp = timeStamp.valueOf(); } if (typeof timeStamp !== "number") { throw new Error("Invalid timestamp value: " + timeStamp); } // Ignore event when tracking is not enabled (event might be outside of element) if (!this.__isTracking) { return; } var currentTouchLeft, currentTouchTop; // Compute move based around of center of fingers if (touches.length === 2) { console.log("dddd", touches) currentTouchLeft = Math.abs((touches[0].clientX || touches[0].x || 0) + (touches[1].clientX || touches[1].x || 0)) / 2; currentTouchTop = Math.abs(touches[0].clientY || touches[0].y || 0 + touches[1].clientY || touches[1].y || 0) / 2; } else { console.log("tttttt", touches[0]) currentTouchLeft = touches[0].clientX || touches[0].x || 0; currentTouchTop = touches[0].clientY || touches[0].y || 0; } var positions = this.__positions; // Are we already is dragging mode? if (this.__isDragging) { // Compute move distance var moveX = currentTouchLeft - this.__lastTouchLeft; var moveY = currentTouchTop - this.__lastTouchTop; // Read previous scroll position and zooming var scrollLeft = this.__scrollLeft; var scrollTop = this.__scrollTop; var level = this.__zoomLevel; // Work with scaling if (scale !== undefined && this.options.zooming) { var oldLevel = level; // Recompute level based on previous scale and new scale level = level / this.__lastScale * scale; // Limit level according to configuration level = Math.max(Math.min(level, this.options.maxZoom), this.options.minZoom); // Only do further compution when change happened if (oldLevel !== level) { // Compute relative event position to container var currentTouchLeftRel = currentTouchLeft - this.__clientLeft; var currentTouchTopRel = currentTouchTop - this.__clientTop; // Recompute left and top coordinates based on new zoom level scrollLeft = ((currentTouchLeftRel + scrollLeft) * level / oldLevel) - currentTouchLeftRel; scrollTop = ((currentTouchTopRel + scrollTop) * level / oldLevel) - currentTouchTopRel; // Recompute max scroll values this.__computeScrollMax(level); } } if (this.__enableScrollX) { scrollLeft -= moveX * this.options.speedMultiplier; var maxScrollLeft = this.__maxScrollLeft; if (scrollLeft > maxScrollLeft || scrollLeft < 0) { // Slow down on the edges if (this.options.bouncing) { scrollLeft += (moveX / 2 * this.options.speedMultiplier); } else if (scrollLeft > maxScrollLeft) { scrollLeft = maxScrollLeft; } else { scrollLeft = 0; } } } // Compute new vertical scroll position if (this.__enableScrollY) { scrollTop -= moveY * this.options.speedMultiplier; var maxScrollTop = this.__maxScrollTop; if (scrollTop > maxScrollTop || scrollTop < 0) { // Slow down on the edges if (this.options.bouncing) { scrollTop += (moveY / 2 * this.options.speedMultiplier); } else if (scrollTop > maxScrollTop) { scrollTop = maxScrollTop; } else { scrollTop = 0; } } } // Keep list from growing infinitely (holding min 10, max 20 measure points) if (positions.length > 60) { positions.splice(0, 30); } // Track scroll movement for decleration positions.push(scrollLeft, scrollTop, timeStamp); // Sync scroll position this.__publish(scrollLeft, scrollTop, level); // Otherwise figure out whether we are switching into dragging mode now. } else { var minimumTrackingForScroll = this.options.locking ? 3 : 0; var minimumTrackingForDrag = 5; var distanceX = Math.abs(currentTouchLeft - this.__initialTouchLeft); var distanceY = Math.abs(currentTouchTop - this.__initialTouchTop); this.__enableScrollX = this.options.scrollingX && distanceX >= minimumTrackingForScroll; this.__enableScrollY = this.options.scrollingY && distanceY >= minimumTrackingForScroll; positions.push(this.__scrollLeft, this.__scrollTop, timeStamp); this.__isDragging = (this.__enableScrollX || this.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag); if (this.__isDragging) { this.__interruptedAnimation = false; } } // Update last touch positions and time stamp for next event this.__lastTouchLeft = currentTouchLeft; this.__lastTouchTop = currentTouchTop; this.__lastTouchMove = timeStamp; this.__lastScale = scale; }, /** * Touch end handler for scrolling support */ doTouchEnd: function (timeStamp) { if (timeStamp instanceof Date) { timeStamp = timeStamp.valueOf(); } if (typeof timeStamp !== "number") { throw new Error("Invalid timestamp value: " + timeStamp); } // Ignore event when tracking is not enabled (no touchstart event on element) // This is required as this listener ('touchmove') sits on the document and not on the element itself. if (!this.__isTracking) { return; } // Not touching anymore (when two finger hit the screen there are two touch end events) this.__isTracking = false; // Be sure to reset the dragging flag now. Here we also detect whether // the finger has moved fast enough to switch into a deceleration animation. if (this.__isDragging) { // Reset dragging flag this.__isDragging = false; // Start deceleration // Verify that the last move detected was in some relevant time frame if (this.__isSingleTouch && this.options.animating && (timeStamp - this.__lastTouchMove) <= 100) { // Then figure out what the scroll position was about 100ms ago var positions = this.__positions; var endPos = positions.length - 1; var startPos = endPos; // Move pointer to position measured 100ms ago for (var i = endPos; i > 0 && positions[i] > (this.__lastTouchMove - 100); i -= 3) { startPos = i; } // If start and stop position is identical in a 100ms timeframe, // we cannot compute any useful deceleration. if (startPos !== endPos) { // Compute relative movement between these two points var timeOffset = positions[endPos] - positions[startPos]; var movedLeft = this.__scrollLeft - positions[startPos - 2]; var movedTop = this.__scrollTop - positions[startPos - 1]; // Based on 50ms compute the movement to apply for each render step this.__decelerationVelocityX = movedLeft / timeOffset * (1000 / 60); this.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60); // How much velocity is required to start the deceleration var minVelocityToStartDeceleration = this.options.paging || this.options.snapping ? 4 : 1; // Verify that we have enough velocity to start deceleration if (Math.abs(this.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(this.__decelerationVelocityY) > minVelocityToStartDeceleration) { this.__startDeceleration(timeStamp); } } else { this.options.scrollingComplete(); } } else if ((timeStamp - this.__lastTouchMove) > 100) { this.options.scrollingComplete(); } } // If this was a slower move it is per default non decelerated, but this // still means that we want snap back to the bounds which is done here. // This is placed outside the condition above to improve edge case stability // e.g. touchend fired without enabled dragging. This should normally do not // have modified the scroll positions or even showed the scrollbars though. if (!this.__isDecelerating) { if (this.__interruptedAnimation || this.__isDragging) { this.options.scrollingComplete(); } this.scrollTo(this.__scrollLeft, this.__scrollTop, true, this.__zoomLevel); } // Fully cleanup list this.__positions.length = 0; }, /* --------------------------------------------------------------------------- PRIVATE API --------------------------------------------------------------------------- */ /** * Applies the scroll position to the content element * * @param left {Number} Left scroll position * @param top {Number} Top scroll position * @param isAnimated {Boolean?false} Whether animation should be used to move to the new coordinates */ __publish: function (left, top, zoom, isAnimated) { // Remember whether we had an animation, then we try to continue // based on the current "drive" of the animation. var wasAnimating = this.__isAnimating; if (wasAnimating) { animate.stop(wasAnimating); this.__isAnimating = false; } if (isAnimated && this.options.animating) { // Keep scheduled positions for scrollBy/zoomBy functionality. this.__scheduledLeft = left; this.__scheduledTop = top; this.__scheduledZoom = zoom; var oldLeft = this.__scrollLeft; var oldTop = this.__scrollTop; var oldZoom = this.__zoomLevel; var diffLeft = left - oldLeft; var diffTop = top - oldTop; var diffZoom = zoom - oldZoom; var step = function (percent, now, render) { if (render) { this.__scrollLeft = oldLeft + (diffLeft * percent); this.__scrollTop = oldTop + (diffTop * percent); this.__zoomLevel = oldZoom + (diffZoom * percent); // Push values out if (this.__callback) { this.__callback(this.__scrollLeft, this.__scrollTop, this.__zoomLevel); } } }.bind(this); var verify = function (id) { return this.__isAnimating === id; }.bind(this); var completed = function (renderedFramesPerSecond, animationId, wasFinished) { if (animationId === this.__isAnimating) { this.__isAnimating = false; } if (this.__didDecelerationComplete || wasFinished) { this.options.scrollingComplete(); } if (this.options.zooming) { this.__computeScrollMax(); if (this.__zoomComplete) { this.__zoomComplete(); this.__zoomComplete = null; } } }.bind(this); // When continuing based on previous animation we choose an ease-out animation instead of ease-in-out this.__isAnimating = animate.start(step, verify, completed, this.options.animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic); } else { this.__scheduledLeft = this.__scrollLeft = left; this.__scheduledTop = this.__scrollTop = top; this.__scheduledZoom = this.__zoomLevel = zoom; // Push values out if (this.__callback) { this.__callback(left, top, zoom); } // Fix max scroll ranges if (this.options.zooming) { this.__computeScrollMax(); if (this.__zoomComplete) { this.__zoomComplete(); this.__zoomComplete = null; } } } }, /** * Recomputes scroll minimum values based on client dimensions and content dimensions. */ __computeScrollMax: function (zoomLevel) { if (zoomLevel === undefined) { zoomLevel = this.__zoomLevel; } this.__maxScrollLeft = Math.max(this.__contentWidth * zoomLevel - this.__clientWidth, 0); this.__maxScrollTop = Math.max(this.__contentHeight * zoomLevel - this.__clientHeight, 0); }, /* --------------------------------------------------------------------------- ANIMATION (DECELERATION) SUPPORT --------------------------------------------------------------------------- */ /** * Called when a touch sequence end and the speed of the finger was high enough * to switch into deceleration mode. */ __startDeceleration: function (timeStamp) { if (this.options.paging) { var scrollLeft = Math.max(Math.min(this.__scrollLeft, this.__maxScrollLeft), 0); var scrollTop = Math.max(Math.min(this.__scrollTop, this.__maxScrollTop), 0); var clientWidth = this.__clientWidth; var clientHeight = this.__clientHeight; // We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area. // Each page should have exactly the size of the client area. this.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth; this.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight; this.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth; this.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight; } else { this.__minDecelerationScrollLeft = 0; this.__minDecelerationScrollTop = 0; this.__maxDecelerationScrollLeft = this.__maxScrollLeft; this.__maxDecelerationScrollTop = this.__maxScrollTop; } // Wrap class method var step = function (percent, now, render) { this.__stepThroughDeceleration(render); }.bind(this); // How much velocity is required to keep the deceleration running var minVelocityToKeepDecelerating = this.options.snapping ? 4 : 0.1; // Detect whether it's still worth to continue animating steps // If we are already slow enough to not being user perceivable anymore, we stop the whole process here. var verify = function () { var shouldContinue = Math.abs(this.__decelerationVelocityX) >= minVelocityToKeepDecelerating || Math.abs(this.__decelerationVelocityY) >= minVelocityToKeepDecelerating; if (!shouldContinue) { this.__didDecelerationComplete = true; } return shouldContinue; }.bind(this); var completed = function (renderedFramesPerSecond, animationId, wasFinished) { this.__isDecelerating = false; if (this.__didDecelerationComplete) { this.options.scrollingComplete(); } // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions this.scrollTo(this.__scrollLeft, this.__scrollTop, this.options.snapping); }.bind(this); // Start animation and switch on flag this.__isDecelerating = animate.start(step, verify, completed); }, /** * Called on every step of the animation * * @param inMemory {Boolean?false} Whether to not render the current step, but keep it in memory only. Used internally only! */ __stepThroughDeceleration: function (render) { // // COMPUTE NEXT SCROLL POSITION // // Add deceleration to scroll position var scrollLeft = this.__scrollLeft + this.__decelerationVelocityX; var scrollTop = this.__scrollTop + this.__decelerationVelocityY; // // HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE // if (!this.options.bouncing) { var scrollLeftFixed = Math.max(Math.min(this.__maxDecelerationScrollLeft, scrollLeft), this.__minDecelerationScrollLeft); if (scrollLeftFixed !== scrollLeft) { scrollLeft = scrollLeftFixed; this.__decelerationVelocityX = 0; } var scrollTopFixed = Math.max(Math.min(this.__maxDecelerationScrollTop, scrollTop), this.__minDecelerationScrollTop); if (scrollTopFixed !== scrollTop) { scrollTop = scrollTopFixed; this.__decelerationVelocityY = 0; } } // // UPDATE SCROLL POSITION // if (render) { this.__publish(scrollLeft, scrollTop, this.__zoomLevel); } else { this.__scrollLeft = scrollLeft; this.__scrollTop = scrollTop; } // // SLOW DOWN // // Slow down velocity on every iteration if (!this.options.paging) { // This is the factor applied to every iteration of the animation // to slow down the process. This should emulate natural behavior where // objects slow down when the initiator of the movement is removed var frictionFactor = 0.95; this.__decelerationVelocityX *= frictionFactor; this.__decelerationVelocityY *= frictionFactor; } // // BOUNCING SUPPORT // if (this.options.bouncing) { var scrollOutsideX = 0; var scrollOutsideY = 0; // This configures the amount of change applied to deceleration/acceleration when reaching boundaries var penetrationDeceleration = this.options.penetrationDeceleration; var penetrationAcceleration = this.options.penetrationAcceleration; // Check limits if (scrollLeft < this.__minDecelerationScrollLeft) { scrollOutsideX = this.__minDecelerationScrollLeft - scrollLeft; } else if (scrollLeft > this.__maxDecelerationScrollLeft) { scrollOutsideX = this.__maxDecelerationScrollLeft - scrollLeft; } if (scrollTop < this.__minDecelerationScrollTop) { scrollOutsideY = this.__minDecelerationScrollTop - scrollTop; } else if (scrollTop > this.__maxDecelerationScrollTop) { scrollOutsideY = this.__maxDecelerationScrollTop - scrollTop; } // Slow down until slow enough, then flip back to snap position if (scrollOutsideX !== 0) { if (scrollOutsideX * this.__decelerationVelocityX <= 0) { this.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration; } else { this.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration; } } if (scrollOutsideY !== 0) { if (scrollOutsideY * this.__decelerationVelocityY <= 0) { this.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration; } else { this.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration; } } } } }; return Scroller; }));