Skip to content

Instantly share code, notes, and snippets.

@asakasinsky
Created December 14, 2014 19:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save asakasinsky/b559f830bb15344fe91e to your computer and use it in GitHub Desktop.
Save asakasinsky/b559f830bb15344fe91e to your computer and use it in GitHub Desktop.
/*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