498 lines
19 KiB
JavaScript
498 lines
19 KiB
JavaScript
|
|
// ------------------------------------------
|
|
// 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 = window.requestAnimationFrame ||
|
|
window.webkitRequestAnimationFrame ||
|
|
window.mozRequestAnimationFrame ||
|
|
window.msRequestAnimationFrame ||
|
|
window.oRequestAnimationFrame ||
|
|
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,
|
|
verticalSpeed: null,
|
|
horizontalSpeed: null,
|
|
breakpoints: [576, 768, 1201],
|
|
center: false,
|
|
wrapper: null,
|
|
relativeToWrapper: false,
|
|
round: true,
|
|
vertical: true,
|
|
horizontal: false,
|
|
verticalScrollAxis: "y",
|
|
horizontalScrollAxis: "x",
|
|
callback: function() {},
|
|
};
|
|
|
|
// User defined options (might have more in the future)
|
|
if (options){
|
|
Object.keys(options).forEach(function(key){
|
|
self.options[key] = options[key];
|
|
});
|
|
}
|
|
|
|
function validateCustomBreakpoints () {
|
|
if (self.options.breakpoints.length === 3 && Array.isArray(self.options.breakpoints)) {
|
|
var isAscending = true;
|
|
var isNumerical = true;
|
|
var lastVal;
|
|
self.options.breakpoints.forEach(function (i) {
|
|
if (typeof i !== 'number') isNumerical = false;
|
|
if (lastVal !== null) {
|
|
if (i < lastVal) isAscending = false;
|
|
}
|
|
lastVal = i;
|
|
});
|
|
if (isAscending && isNumerical) return;
|
|
}
|
|
// revert defaults if set incorrectly
|
|
self.options.breakpoints = [576, 768, 1201];
|
|
console.warn("Rellax: You must pass an array of 3 numbers in ascending order to the breakpoints option. Defaults reverted");
|
|
}
|
|
|
|
if (options && options.breakpoints) {
|
|
validateCustomBreakpoints();
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// set a placeholder for the current breakpoint
|
|
var currentBreakpoint;
|
|
|
|
// helper to determine current breakpoint
|
|
var getCurrentBreakpoint = function (w) {
|
|
var bp = self.options.breakpoints;
|
|
if (w < bp[0]) return 'xs';
|
|
if (w >= bp[0] && w < bp[1]) return 'sm';
|
|
if (w >= bp[1] && w < bp[2]) return 'md';
|
|
return 'lg';
|
|
};
|
|
|
|
// 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;
|
|
currentBreakpoint = getCurrentBreakpoint(screenX);
|
|
|
|
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 dataXsSpeed = el.getAttribute( 'data-rellax-xs-speed' );
|
|
var dataMobileSpeed = el.getAttribute( 'data-rellax-mobile-speed' );
|
|
var dataTabletSpeed = el.getAttribute( 'data-rellax-tablet-speed' );
|
|
var dataDesktopSpeed = el.getAttribute( 'data-rellax-desktop-speed' );
|
|
var dataVerticalSpeed = el.getAttribute('data-rellax-vertical-speed');
|
|
var dataHorizontalSpeed = el.getAttribute('data-rellax-horizontal-speed');
|
|
var dataVericalScrollAxis = el.getAttribute('data-rellax-vertical-scroll-axis');
|
|
var dataHorizontalScrollAxis = el.getAttribute('data-rellax-horizontal-scroll-axis');
|
|
var dataZindex = el.getAttribute( 'data-rellax-zindex' ) || 0;
|
|
var dataMin = el.getAttribute( 'data-rellax-min' );
|
|
var dataMax = el.getAttribute( 'data-rellax-max' );
|
|
var dataMinX = el.getAttribute('data-rellax-min-x');
|
|
var dataMaxX = el.getAttribute('data-rellax-max-x');
|
|
var dataMinY = el.getAttribute('data-rellax-min-y');
|
|
var dataMaxY = el.getAttribute('data-rellax-max-y');
|
|
var mapBreakpoints;
|
|
var breakpoints = true;
|
|
|
|
if (!dataXsSpeed && !dataMobileSpeed && !dataTabletSpeed && !dataDesktopSpeed) {
|
|
breakpoints = false;
|
|
} else {
|
|
mapBreakpoints = {
|
|
'xs': dataXsSpeed,
|
|
'sm': dataMobileSpeed,
|
|
'md': dataTabletSpeed,
|
|
'lg': dataDesktopSpeed
|
|
};
|
|
}
|
|
|
|
// 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 = (breakpoints && mapBreakpoints[currentBreakpoint] !== null) ? Number(mapBreakpoints[currentBreakpoint]) : (dataSpeed ? dataSpeed : self.options.speed);
|
|
var verticalSpeed = dataVerticalSpeed ? dataVerticalSpeed : self.options.verticalSpeed;
|
|
var horizontalSpeed = dataHorizontalSpeed ? dataHorizontalSpeed : self.options.horizontalSpeed;
|
|
|
|
// Optional individual block movement axis direction as data attr, otherwise gobal movement direction
|
|
var verticalScrollAxis = dataVericalScrollAxis ? dataVericalScrollAxis : self.options.verticalScrollAxis;
|
|
var horizontalScrollAxis = dataHorizontalScrollAxis ? dataHorizontalScrollAxis : self.options.horizontalScrollAxis;
|
|
|
|
var bases = updatePosition(percentageX, percentageY, speed, verticalSpeed, horizontalSpeed);
|
|
|
|
// ~~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,
|
|
verticalSpeed: verticalSpeed,
|
|
horizontalSpeed: horizontalSpeed,
|
|
verticalScrollAxis: verticalScrollAxis,
|
|
horizontalScrollAxis: horizontalScrollAxis,
|
|
style: style,
|
|
transform: transform,
|
|
zindex: dataZindex,
|
|
min: dataMin,
|
|
max: dataMax,
|
|
minX: dataMinX,
|
|
maxX: dataMaxX,
|
|
minY: dataMinY,
|
|
maxY: dataMaxY
|
|
};
|
|
};
|
|
|
|
// 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, verticalSpeed, horizontalSpeed) {
|
|
var result = {};
|
|
var valueX = ((horizontalSpeed ? horizontalSpeed : speed) * (100 * (1 - percentageX)));
|
|
var valueY = ((verticalSpeed ? verticalSpeed : 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++){
|
|
// Determine relevant movement directions
|
|
var verticalScrollAxis = blocks[i].verticalScrollAxis.toLowerCase();
|
|
var horizontalScrollAxis = blocks[i].horizontalScrollAxis.toLowerCase();
|
|
var verticalScrollX = verticalScrollAxis.indexOf("x") != -1 ? posY : 0;
|
|
var verticalScrollY = verticalScrollAxis.indexOf("y") != -1 ? posY : 0;
|
|
var horizontalScrollX = horizontalScrollAxis.indexOf("x") != -1 ? posX : 0;
|
|
var horizontalScrollY = horizontalScrollAxis.indexOf("y") != -1 ? posX : 0;
|
|
|
|
var percentageY = ((verticalScrollY + horizontalScrollY - blocks[i].top + screenY) / (blocks[i].height + screenY));
|
|
var percentageX = ((verticalScrollX + horizontalScrollX - 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].verticalSpeed, blocks[i].horizontalSpeed);
|
|
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 directional min limits are defined
|
|
if (blocks[i].minY != null) {
|
|
positionY = positionY <= blocks[i].minY ? blocks[i].minY : positionY;
|
|
}
|
|
if (blocks[i].minX != null) {
|
|
positionX = positionX <= blocks[i].minX ? blocks[i].minX : 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;
|
|
}
|
|
}
|
|
|
|
// Check if directional max limits are defined
|
|
if (blocks[i].maxY != null) {
|
|
positionY = positionY >= blocks[i].maxY ? blocks[i].maxY : positionY;
|
|
}
|
|
if (blocks[i].maxX != null) {
|
|
positionX = positionX >= blocks[i].maxX ? blocks[i].maxX : 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;
|
|
}));
|