Created
December 14, 2014 19:45
-
-
Save asakasinsky/b559f830bb15344fe91e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*global Scroller: true */ | |
/*global utils: true */ | |
/*global Touch: true */ | |
(function(window, document, Math, Zepto) | |
{ | |
"use strict"; | |
var defaults = | |
{ | |
scrollX: false, | |
scrollY: true, | |
momentum: true, | |
bounce: true, | |
bounceTime: 600, | |
bounceEasing: 'circular' | |
}; | |
var Scroller = function (el, options) | |
{ | |
this.wrapper = typeof el === 'string' ? document.querySelector(el) : el; | |
this.scroller = this.wrapper.children[0]; | |
this.scrollerStyle = this.scroller.style; // cache style for better performance | |
options = options || {}; | |
this.options = {}; | |
$.extend(this.options, defaults, options) | |
} | |
Scroller.prototype = | |
{ | |
name: 'Scroller', | |
version: '0.1', | |
debug: true, | |
startX: 0, | |
startY: 0, | |
x: 0, | |
y: 0, | |
touchInstance: null, | |
init: function () | |
{ | |
this.name = this.name + ' ' + this.wrapper.id; | |
this.enabled = true; | |
this.options.bounceEasing = typeof this.options.bounceEasing === 'string' ? utils.ease[this.options.bounceEasing] || utils.ease.circular : this.options.bounceEasing; | |
this.refresh(); | |
this.translateZ = ' translateZ(0)'; | |
this._bind(); | |
}, | |
destroy: function () | |
{ | |
this._unBind(); | |
}, | |
disable: function () { | |
this.enabled = false; | |
}, | |
enable: function () { | |
this.enabled = true; | |
}, | |
_bind: function () | |
{ | |
var that = this; | |
this.touchInstance = new window.Touch({ | |
onStart: function(touch){ that._onStart(touch); }, | |
onMove: function(touch){ that._onMove(touch); }, | |
onEnd: function(touch){ that._onEnd(touch); }, | |
$el: $(this.scroller) | |
}); | |
}, | |
_unBind: function () | |
{ | |
var that = this; | |
this.touchInstance.destroy(); | |
this.touchInstance = null | |
delete this.touchInstance; | |
}, | |
refresh: function () | |
{ | |
var rf = this.wrapper.offsetHeight; // Force reflow | |
this.wrapperWidth = this.wrapper.clientWidth; | |
this.wrapperHeight = this.wrapper.clientHeight; | |
this.scrollerWidth = this.scroller.offsetWidth; | |
this.scrollerHeight = this.scroller.offsetHeight; | |
this.maxScrollX = this.wrapperWidth - this.scrollerWidth; | |
this.maxScrollY = this.wrapperHeight - this.scrollerHeight; | |
if ( this.maxScrollX > 0 ) { | |
this.maxScrollX = 0; | |
} | |
if ( this.maxScrollY > 0 ) { | |
this.maxScrollY = 0; | |
} | |
this.hasHorizontalScroll = this.options.scrollX && this.maxScrollX < 0; | |
this.hasVerticalScroll = this.options.scrollY && this.maxScrollY < 0; | |
if ( !this.hasHorizontalScroll ) { | |
this.maxScrollX = 0; | |
this.scrollerWidth = this.wrapperWidth; | |
} | |
if ( !this.hasVerticalScroll ) { | |
this.maxScrollY = 0; | |
this.scrollerHeight = this.wrapperHeight; | |
} | |
this.endTime = 0; | |
this.directionX = 0; | |
this.directionY = 0; | |
this.wrapperOffset = utils.offset(this.wrapper); | |
this.started = false; | |
this.resetPosition(); | |
}, | |
resetPosition: function (time) { | |
var x = this.x, | |
y = this.y; | |
time = time || 0; | |
if ( !this.hasHorizontalScroll || this.x > 0 ) { | |
x = 0; | |
} else if ( this.x < this.maxScrollX ) { | |
x = this.maxScrollX; | |
} | |
if (this.hasHorizontalScroll && (x === this.x)) { | |
return false; | |
} | |
if ( !this.hasVerticalScroll || this.y > 0 ) { | |
y = 0; | |
} else if ( this.y < this.maxScrollY ) { | |
y = this.maxScrollY; | |
} | |
if (this.hasVerticalScroll && (y === this.y)) { | |
return false; | |
} | |
this.scrollTo(x, y, time, this.options.bounceEasing); | |
return true; | |
}, | |
// .o. o8o . o8o | |
// .888. `"' .o8 `"' | |
// .8"888. ooo. .oo. oooo ooo. .oo. .oo. .oooo. .o888oo oooo .ooooo. ooo. .oo. | |
// .8' `888. `888P"Y88b `888 `888P"Y88bP"Y88b `P )88b 888 `888 d88' `88b `888P"Y88b | |
// .88ooo8888. 888 888 888 888 888 888 .oP"888 888 888 888 888 888 888 | |
// .8' `888. 888 888 888 888 888 888 d8( 888 888 . 888 888 888 888 888 | |
// o88o o8888o o888o o888o o888o o888o o888o o888o `Y888""8o "888" o888o `Y8bod8P' o888o o888o | |
scrollTo: function (x, y, time, easing) { | |
easing = easing || utils.ease.quadratic; | |
this.isInTransition = (time > 0); | |
if ( !time ) { | |
// this._transitionTimingFunction(easing.style); | |
// this._transitionTime(time); | |
// this._translate(x, y); | |
this._translate(x, y); | |
} else { | |
this._animate(x, y, time, easing.fn); | |
} | |
this.x = x; | |
this.y = y; | |
}, | |
_translate: function (x, y) { | |
if(this.hasVerticalScroll) { | |
this.scrollerStyle[utils.style.transform] = 'translateY(' + y + 'px)' + this.translateZ; | |
} | |
if (this.hasHorizontalScroll) { | |
this.scrollerStyle[utils.style.transform] = 'translateX(' + x + 'px)' + this.translateZ; | |
} | |
}, | |
getComputedPosition: function () { | |
var matrix = window.getComputedStyle(this.scroller, null), | |
x, y; | |
matrix = matrix[utils.style.transform].split(')')[0].split(', '); | |
x = +(matrix[12] || matrix[4]); | |
y = +(matrix[13] || matrix[5]); | |
return { x: x, y: y }; | |
}, | |
_animate: function (destX, destY, duration, easingFn) { | |
var that = this, | |
startX = this.x, | |
startY = this.y, | |
startTime = utils.getTime(), | |
destTime = startTime + duration; | |
function step () { | |
var now = utils.getTime(), | |
newX, newY, | |
easing; | |
if(!that.isInTransition) { | |
destTime = 0; | |
return; | |
} | |
if ( now >= destTime ) { | |
that.isAnimating = false; | |
that._translate(destX, destY); | |
that.x = destX; | |
that.y = destY; | |
if(typeof that.options.onEnd === 'function') { | |
that.options.onEnd(); | |
} | |
if ( !that.resetPosition(that.options.bounceTime) ) { | |
that._execEvent('scrollEnd'); | |
} | |
return; | |
} | |
now = ( now - startTime ) / duration; | |
easing = easingFn(now); | |
newX = ( destX - startX ) * easing + startX; | |
newY = ( destY - startY ) * easing + startY; | |
that._translate(newX, newY); | |
if ( that.isAnimating ) { | |
window.requestAnimationFrame(step); | |
} | |
} | |
this.isAnimating = true; | |
step(); | |
}, | |
// ooooooooooooo oooo | |
// 8' 888 `8 `888 | |
// 888 .ooooo. oooo oooo .ooooo. 888 .oo. | |
// 888 d88' `88b `888 `888 d88' `"Y8 888P"Y88b | |
// 888 888 888 888 888 888 888 888 | |
// 888 888 888 888 888 888 .o8 888 888 | |
// o888o `Y8bod8P' `V88V"V8P' `Y8bod8P' o888o o888o | |
_onStart: function (touch) | |
{ | |
var pos; | |
this.startX = this.x; | |
this.startY = this.y; | |
if ( this.isInTransition ) { | |
this.isInTransition = false; | |
pos = this.getComputedPosition(); | |
this._translate(Math.round(pos.x), Math.round(pos.y)); | |
this._execEvent('scrollEnd'); | |
} | |
}, | |
_onMove: function (touch) | |
{ | |
if(!this.enabled) { | |
return false; | |
} | |
var newX, | |
newY, | |
deltaX = touch.currentTouch.pageX - touch.startTouch.pageX, | |
deltaY = touch.currentTouch.pageY - touch.startTouch.pageY; | |
if(this.hasHorizontalScroll && | |
touch.horizontalDirection && | |
!this.started | |
) { | |
this.started = true; | |
this._execEvent('scrollXStart'); | |
} | |
if(this.hasVerticalScroll && | |
touch.verticalDirection && | |
!this.started | |
) { | |
this.started = true; | |
this._execEvent('scrollYStart'); | |
} | |
if (this.started){ | |
deltaX = this.hasHorizontalScroll ? deltaX : 0; | |
deltaY = this.hasVerticalScroll ? deltaY : 0; | |
newX = this.x + deltaX; | |
newY = this.y + deltaY; | |
// Slow down if outside of the boundaries | |
if ( newX > 0 || newX < this.maxScrollX ) { | |
newX = this.options.bounce ? this.x + deltaX : newX > 0 ? 0 : this.maxScrollX; | |
// newX = this.x + deltaX / 3; | |
this.bounceX = newX; | |
} | |
if ( newY > 0 || newY < this.maxScrollY ) { | |
newY = this.options.bounce ? this.y + deltaY / 3 : newY > 0 ? 0 : this.maxScrollY; | |
this.bounceY = newY; | |
} | |
this.directionX = deltaX > 0 ? -1 : deltaX < 0 ? 1 : 0; | |
this.directionY = deltaY > 0 ? -1 : deltaY < 0 ? 1 : 0; | |
this._translate(newX, newY); | |
} | |
if ( touch.velocity > 300 ) { | |
// this.startTime = timestamp; | |
this.startX = this.x; | |
this.startY = this.y; | |
} | |
}, | |
_onEnd: function (touch) | |
{ | |
if(!this.enabled || !this.started) { | |
return false; | |
} | |
var deltaX = touch.currentTouch.pageX - touch.startTouch.pageX; | |
var deltaY = touch.currentTouch.pageY - touch.startTouch.pageY; | |
this.started = false; | |
this.x = this.x + (deltaX); | |
this.y = this.y + (deltaY); | |
if(typeof this.bounceX === 'number') { | |
this.x = this.bounceX; | |
this.bounceX = false; | |
} | |
if(typeof this.bounceY === 'number') { | |
this.y = this.bounceY; | |
this.bounceY = false; | |
} | |
this.isInTransition = false; | |
var momentumX, | |
momentumY, | |
duration = touch.velocity, | |
newX = Math.round(this.x), | |
newY = Math.round(this.y), | |
distanceX = Math.abs(newX - this.startX), | |
distanceY = Math.abs(newY - this.startY), | |
time = 0, | |
easing = ''; | |
// reset if we are outside of the boundaries | |
if ( this.resetPosition(this.options.bounceTime) ) { | |
return; | |
} | |
this.scrollTo(newX, newY); // ensures that the last position is rounded | |
// start momentum animation if needed | |
if ( this.options.momentum && duration < 300 ) { | |
momentumX = this.hasHorizontalScroll ? utils.momentum(this.x, this.startX, duration, this.maxScrollX, this.options.bounce ? this.wrapperWidth : 0, this.options.deceleration) : { destination: newX, duration: 0 }; | |
momentumY = this.hasVerticalScroll ? utils.momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0, this.options.deceleration) : { destination: newY, duration: 0 }; | |
newX = momentumX.destination; | |
newY = momentumY.destination; | |
time = Math.max(momentumX.duration, momentumY.duration); | |
} | |
if ( newX !== this.x || newY !== this.y ) { | |
// change easing function when scroller goes out of the boundaries | |
if ( newX > 0 || newX < this.maxScrollX || newY > 0 || newY < this.maxScrollY ) { | |
easing = utils.ease.quadratic; | |
} | |
this.isInTransition = true; | |
this.scrollTo(newX, newY, time, easing); | |
// return; | |
} | |
this._execEvent('scrollEnd'); | |
}, | |
_execEvent: function (eventName) | |
{ | |
switch(eventName){ | |
case 'scrollXStart': | |
if(typeof this.options.onStartX === 'function') { | |
this.options.onStartX(this); | |
} | |
break; | |
case 'scrollYStart': | |
if(typeof this.options.onStartY === 'function') { | |
this.options.onStartY(this); | |
} | |
break; | |
case 'scrollEnd': | |
if(typeof this.options.onEnd === 'function') { | |
this.options.onEnd(this); | |
} | |
break; | |
} | |
} | |
}; | |
if (typeof exports !== 'undefined') {exports.Scroller = Scroller;} | |
else {window.Scroller = Scroller;} | |
})(window, document, Math, Zepto); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment