Last active
August 2, 2019 10:28
-
-
Save pbuzdin/3cadb1839479ba454b86de28b1e3f1cf to your computer and use it in GitHub Desktop.
rellax-without-requestAnimationFrame.js
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
// ------------------------------------------ | |
// 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