// ------------------------------------------ // 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; }));