Skip to content

Instantly share code, notes, and snippets.

@panayotoff
Last active September 3, 2018 08:42
Show Gist options
  • Save panayotoff/88a34aeb12ccee0aa35c2c063840b327 to your computer and use it in GitHub Desktop.
Save panayotoff/88a34aeb12ccee0aa35c2c063840b327 to your computer and use it in GitHub Desktop.
MquScroll - JS util for creating scrolling animations
var MquScroll = function(options) {
//--------------------------------------------------------------
// Compatibility
//--------------------------------------------------------------
var raf = window.requestAnimationFrame || window.webkitRequestAnimationFrame || function(callback) {
window.setTimeout(callback, 1000 / 60)
};
var passiveSupported = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function() {
passiveSupported = true;
}
});
window.addEventListener('testPassive', null, opts);
window.removeEventListener('testPassive', null, opts);
} catch (error) {}
function extend(obj, withObj) {
for (var key in withObj) {
if (withObj.hasOwnProperty(key)) {
obj[key] = withObj[key];
}
}
return obj;
}
//--------------------------------------------------------------
// Logic
//--------------------------------------------------------------
var triggers = [],
resizeTimer, scrollTimer, scrolling, windowHeight;
var settings = extend({
scrollStarted: function() {
//document.body.style.pointerEvents = 'none';
},
scrollEnded: function() {
//document.body.style.pointerEvents = '';
},
autoKill: function() {},
refresh: function() {}
}, options);
function checkTrigger(trigger) {
var options = trigger.options;
if (!options.enabled) {
return;
}
var el = trigger.el;
var boundRect = el.getBoundingClientRect();
var elRect = {
top: boundRect.top - options.offsetTop,
bottom: boundRect.bottom + options.offsetBottom,
height: boundRect.height + options.offsetTop + options.offsetBottom
};
var inViewport = (elRect.top >= -elRect.height && elRect.bottom <= (windowHeight + elRect.height));
if (!inViewport && !options.entered) {
return;
}
// Enter
if (inViewport && !options.entered) {
options.entered = true;
options.enter(el);
if (options.exited) {
options.exited = false;
}
}
// Update
var percentViewport = 1 - ((elRect.top + elRect.height) / (windowHeight + elRect.height));
percentViewport = Math.min(Math.max(percentViewport, 0), 1);
options.update(el, percentViewport);
// Trigger
var shouldTrigger = ((percentViewport >= options.triggerFactor) && (percentViewport <= (1 - options.triggerFactor)));
if (!options.triggered && shouldTrigger) {
options.triggered = true;
options.trigger(el);
} else if (options.triggered && !shouldTrigger) {
options.triggered = false;
}
// Exit
if (!inViewport && !options.exited) {
options.exited = true;
options.entered = false;
options.exit(el);
if (options.once) {
options.release(el);
remove(trigger.id);
} else {
options.triggered = false;
}
}
}
//--------------------------------------------------------------
// Events
//--------------------------------------------------------------
function loop() {
!scrolling && onScrollStart();
triggers.length && triggers.forEach(checkTrigger);
clearTimeout(scrollTimer);
scrollTimer = setTimeout(onScrollEnd, 500);
}
function onScroll() {
raf(loop);
}
function onScrollStart() {
scrolling = true;
settings.scrollStarted();
}
function onScrollEnd() {
scrolling = false;
settings.scrollEnded();
}
function onResize() {
windowHeight = window.innerHeight;
onScroll();
}
function debouncedResize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(onResize, 250);
}
//--------------------------------------------------------------
// Api / Defaults
//--------------------------------------------------------------
function init() {
window.addEventListener('scroll', onScroll, passiveSupported ? {
passive: true
} : false);
window.addEventListener('resize', debouncedResize);
window.addEventListener('orientationchange', debouncedResize);
onResize();
onScroll();
}
function destroy() {
triggers.length = 0;
clearTimeout(scrollTimer);
onScrollEnd();
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', debouncedResize);
window.removeEventListener('orientationchange', debouncedResize);
}
function add(triggerElement, options) {
var trigger = {
el: triggerElement,
id: options.id || 'ms' + Math.random().toString(36).substr(2, 9),
setEnabled: function(enabled) {
this.options.enabled = enabled;
},
options: extend({
// State
entered: false,
triggered: false,
exited: false,
enabled: true,
// Options
offsetTop: 0, // px how much to extend top
offsetBottom: 0, // px how much to extend bottom
triggerFactor: .1, // how the item have to enter to do the callback trigger
once: true,
// Callbacks
created: function(el) {},
enter: function(el) {},
update: function(el, percentViewport) {},
trigger: function(el) {},
exit: function(el) {},
release: function(el) {},
// Enable/Disable callbacks
enable: function(el) {},
disable: function(el) {}
}, options)
};
trigger.options.created(triggerElement);
triggers.push(trigger);
return trigger;
}
function remove(triggerId) {
for (var i = 0, j = triggers.length; i < j; i++) {
if (triggers[i].id === triggerId) {
triggers.splice(i, 1);
(!triggers.length) && settings.autoKill && destroy();
return;
}
}
}
return {
init: init,
destroy: destroy,
add: add,
remove: remove,
passiveSupported: passiveSupported
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment