Skip to content

Instantly share code, notes, and snippets.

@pbuzdin
Last active August 2, 2019 10:28
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 pbuzdin/3cadb1839479ba454b86de28b1e3f1cf to your computer and use it in GitHub Desktop.
Save pbuzdin/3cadb1839479ba454b86de28b1e3f1cf to your computer and use it in GitHub Desktop.
rellax-without-requestAnimationFrame.js
// ------------------------------------------
// Rellax.js
// Buttery smooth parallax library
// Copyright (c) 2016 Moe Amaya (@moeamaya)
// MIT license
//
// Thanks to Paraxify.js and Jaime Cabllero
// for parallax concepts
// ------------------------------------------
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals (root is window)
root.Rellax = factory();
}
}(typeof window !== "undefined" ? window : global, function () {
var Rellax = function(el, options){
"use strict";
var self = Object.create(Rellax.prototype);
var posY = 0;
var screenY = 0;
var posX = 0;
var screenX = 0;
var blocks = [];
var pause = true;
// check what requestAnimationFrame to use, and if
// it's not supported, use the onscroll event
var loop = function(callback){ return setTimeout(callback, 1000 / 60); };
// store the id for later use
var loopId = null;
// Test via a getter in the options object to see if the passive property is accessed
var supportsPassive = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function() {
supportsPassive = true;
}
});
window.addEventListener("testPassive", null, opts);
window.removeEventListener("testPassive", null, opts);
} catch (e) {}
// check what cancelAnimation method to use
var clearLoop = window.cancelAnimationFrame || window.mozCancelAnimationFrame || clearTimeout;
// check which transform property to use
var transformProp = window.transformProp || (function(){
var testEl = document.createElement('div');
if (testEl.style.transform === null) {
var vendors = ['Webkit', 'Moz', 'ms'];
for (var vendor in vendors) {
if (testEl.style[ vendors[vendor] + 'Transform' ] !== undefined) {
return vendors[vendor] + 'Transform';
}
}
}
return 'transform';
})();
// Default Settings
self.options = {
speed: -2,
center: false,
wrapper: null,
relativeToWrapper: false,
round: true,
vertical: true,
horizontal: false,
callback: function() {},
};
// User defined options (might have more in the future)
if (options){
Object.keys(options).forEach(function(key){
self.options[key] = options[key];
});
}
// By default, rellax class
if (!el) {
el = '.rellax';
}
// check if el is a className or a node
var elements = typeof el === 'string' ? document.querySelectorAll(el) : [el];
// Now query selector
if (elements.length > 0) {
self.elems = elements;
}
// The elements don't exist
else {
console.warn("Rellax: The elements you're trying to select don't exist.");
return;
}
// Has a wrapper and it exists
if (self.options.wrapper) {
if (!self.options.wrapper.nodeType) {
var wrapper = document.querySelector(self.options.wrapper);
if (wrapper) {
self.options.wrapper = wrapper;
} else {
console.warn("Rellax: The wrapper you're trying to use doesn't exist.");
return;
}
}
}
// Get and cache initial position of all elements
var cacheBlocks = function() {
for (var i = 0; i < self.elems.length; i++){
var block = createBlock(self.elems[i]);
blocks.push(block);
}
};
// Let's kick this script off
// Build array for cached element values
var init = function() {
for (var i = 0; i < blocks.length; i++){
self.elems[i].style.cssText = blocks[i].style;
}
blocks = [];
screenY = window.innerHeight;
screenX = window.innerWidth;
setPosition();
cacheBlocks();
animate();
// If paused, unpause and set listener for window resizing events
if (pause) {
window.addEventListener('resize', init);
pause = false;
// Start the loop
update();
}
};
// We want to cache the parallax blocks'
// values: base, top, height, speed
// el: is dom object, return: el cache values
var createBlock = function(el) {
var dataPercentage = el.getAttribute( 'data-rellax-percentage' );
var dataSpeed = el.getAttribute( 'data-rellax-speed' );
var dataZindex = el.getAttribute( 'data-rellax-zindex' ) || 0;
var dataMin = el.getAttribute( 'data-rellax-min' );
var dataMax = el.getAttribute( 'data-rellax-max' );
// initializing at scrollY = 0 (top of browser), scrollX = 0 (left of browser)
// ensures elements are positioned based on HTML layout.
//
// If the element has the percentage attribute, the posY and posX needs to be
// the current scroll position's value, so that the elements are still positioned based on HTML layout
var wrapperPosY = self.options.wrapper ? self.options.wrapper.scrollTop : (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
// If the option relativeToWrapper is true, use the wrappers offset to top, subtracted from the current page scroll.
if (self.options.relativeToWrapper) {
var scrollPosY = (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
wrapperPosY = scrollPosY - self.options.wrapper.offsetTop;
}
var posY = self.options.vertical ? ( dataPercentage || self.options.center ? wrapperPosY : 0 ) : 0;
var posX = self.options.horizontal ? ( dataPercentage || self.options.center ? self.options.wrapper ? self.options.wrapper.scrollLeft : (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft) : 0 ) : 0;
var blockTop = posY + el.getBoundingClientRect().top;
var blockHeight = el.clientHeight || el.offsetHeight || el.scrollHeight;
var blockLeft = posX + el.getBoundingClientRect().left;
var blockWidth = el.clientWidth || el.offsetWidth || el.scrollWidth;
// apparently parallax equation everyone uses
var percentageY = dataPercentage ? dataPercentage : (posY - blockTop + screenY) / (blockHeight + screenY);
var percentageX = dataPercentage ? dataPercentage : (posX - blockLeft + screenX) / (blockWidth + screenX);
if(self.options.center){ percentageX = 0.5; percentageY = 0.5; }
// Optional individual block speed as data attr, otherwise global speed
var speed = dataSpeed ? dataSpeed : self.options.speed;
var bases = updatePosition(percentageX, percentageY, speed);
// ~~Store non-translate3d transforms~~
// Store inline styles and extract transforms
var style = el.style.cssText;
var transform = '';
// Check if there's an inline styled transform
var searchResult = /transform\s*:/i.exec(style);
if (searchResult) {
// Get the index of the transform
var index = searchResult.index;
// Trim the style to the transform point and get the following semi-colon index
var trimmedStyle = style.slice(index);
var delimiter = trimmedStyle.indexOf(';');
// Remove "transform" string and save the attribute
if (delimiter) {
transform = " " + trimmedStyle.slice(11, delimiter).replace(/\s/g,'');
} else {
transform = " " + trimmedStyle.slice(11).replace(/\s/g,'');
}
}
return {
baseX: bases.x,
baseY: bases.y,
top: blockTop,
left: blockLeft,
height: blockHeight,
width: blockWidth,
speed: speed,
style: style,
transform: transform,
zindex: dataZindex,
min: dataMin,
max: dataMax
};
};
// set scroll position (posY, posX)
// side effect method is not ideal, but okay for now
// returns true if the scroll changed, false if nothing happened
var setPosition = function() {
var oldY = posY;
var oldX = posX;
posY = self.options.wrapper ? self.options.wrapper.scrollTop : (document.documentElement || document.body.parentNode || document.body).scrollTop || window.pageYOffset;
posX = self.options.wrapper ? self.options.wrapper.scrollLeft : (document.documentElement || document.body.parentNode || document.body).scrollLeft || window.pageXOffset;
// If option relativeToWrapper is true, use relative wrapper value instead.
if (self.options.relativeToWrapper) {
var scrollPosY = (document.documentElement || document.body.parentNode || document.body).scrollTop || window.pageYOffset;
posY = scrollPosY - self.options.wrapper.offsetTop;
}
if (oldY != posY && self.options.vertical) {
// scroll changed, return true
return true;
}
if (oldX != posX && self.options.horizontal) {
// scroll changed, return true
return true;
}
// scroll did not change
return false;
};
// Ahh a pure function, gets new transform value
// based on scrollPosition and speed
// Allow for decimal pixel values
var updatePosition = function(percentageX, percentageY, speed) {
var result = {};
var valueX = (speed * (100 * (1 - percentageX)));
var valueY = (speed * (100 * (1 - percentageY)));
result.x = self.options.round ? Math.round(valueX) : Math.round(valueX * 100) / 100;
result.y = self.options.round ? Math.round(valueY) : Math.round(valueY * 100) / 100;
return result;
};
// Remove event listeners and loop again
var deferredUpdate = function() {
window.removeEventListener('resize', deferredUpdate);
window.removeEventListener('orientationchange', deferredUpdate);
(self.options.wrapper ? self.options.wrapper : window).removeEventListener('scroll', deferredUpdate);
(self.options.wrapper ? self.options.wrapper : document).removeEventListener('touchmove', deferredUpdate);
// loop again
loopId = loop(update);
};
// Loop
var update = function() {
if (setPosition() && pause === false) {
animate();
// loop again
loopId = loop(update);
} else {
loopId = null;
// Don't animate until we get a position updating event
window.addEventListener('resize', deferredUpdate);
window.addEventListener('orientationchange', deferredUpdate);
(self.options.wrapper ? self.options.wrapper : window).addEventListener('scroll', deferredUpdate, supportsPassive ? { passive: true } : false);
(self.options.wrapper ? self.options.wrapper : document).addEventListener('touchmove', deferredUpdate, supportsPassive ? { passive: true } : false);
}
};
// Transform3d on parallax element
var animate = function() {
var positions;
for (var i = 0; i < self.elems.length; i++){
var percentageY = ((posY - blocks[i].top + screenY) / (blocks[i].height + screenY));
var percentageX = ((posX - blocks[i].left + screenX) / (blocks[i].width + screenX));
// Subtracting initialize value, so element stays in same spot as HTML
positions = updatePosition(percentageX, percentageY, blocks[i].speed);// - blocks[i].baseX;
var positionY = positions.y - blocks[i].baseY;
var positionX = positions.x - blocks[i].baseX;
// The next two "if" blocks go like this:
// Check if a limit is defined (first "min", then "max");
// Check if we need to change the Y or the X
// (Currently working only if just one of the axes is enabled)
// Then, check if the new position is inside the allowed limit
// If so, use new position. If not, set position to limit.
// Check if a min limit is defined
if (blocks[i].min !== null) {
if (self.options.vertical && !self.options.horizontal) {
positionY = positionY <= blocks[i].min ? blocks[i].min : positionY;
}
if (self.options.horizontal && !self.options.vertical) {
positionX = positionX <= blocks[i].min ? blocks[i].min : positionX;
}
}
// Check if a max limit is defined
if (blocks[i].max !== null) {
if (self.options.vertical && !self.options.horizontal) {
positionY = positionY >= blocks[i].max ? blocks[i].max : positionY;
}
if (self.options.horizontal && !self.options.vertical) {
positionX = positionX >= blocks[i].max ? blocks[i].max : positionX;
}
}
var zindex = blocks[i].zindex;
// Move that element
// (Set the new translation and append initial inline transforms.)
var translate = 'translate3d(' + (self.options.horizontal ? positionX : '0') + 'px,' + (self.options.vertical ? positionY : '0') + 'px,' + zindex + 'px) ' + blocks[i].transform;
self.elems[i].style[transformProp] = translate;
}
self.options.callback(positions);
};
self.destroy = function() {
for (var i = 0; i < self.elems.length; i++){
self.elems[i].style.cssText = blocks[i].style;
}
// Remove resize event listener if not pause, and pause
if (!pause) {
window.removeEventListener('resize', init);
pause = true;
}
// Clear the animation loop to prevent possible memory leak
clearLoop(loopId);
loopId = null;
};
// Init
init();
// Allow to recalculate the initial values whenever we want
self.refresh = init;
return self;
};
return Rellax;
}));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment