rellax.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. // ------------------------------------------
  2. // Rellax.js
  3. // Buttery smooth parallax library
  4. // Copyright (c) 2016 Moe Amaya (@moeamaya)
  5. // MIT license
  6. //
  7. // Thanks to Paraxify.js and Jaime Cabllero
  8. // for parallax concepts
  9. // ------------------------------------------
  10. (function (root, factory) {
  11. if (typeof define === 'function' && define.amd) {
  12. // AMD. Register as an anonymous module.
  13. define([], factory);
  14. } else if (typeof module === 'object' && module.exports) {
  15. // Node. Does not work with strict CommonJS, but
  16. // only CommonJS-like environments that support module.exports,
  17. // like Node.
  18. module.exports = factory();
  19. } else {
  20. // Browser globals (root is window)
  21. root.Rellax = factory();
  22. }
  23. }(typeof window !== "undefined" ? window : global, function () {
  24. var Rellax = function(el, options){
  25. "use strict";
  26. var self = Object.create(Rellax.prototype);
  27. var posY = 0;
  28. var screenY = 0;
  29. var posX = 0;
  30. var screenX = 0;
  31. var blocks = [];
  32. var pause = true;
  33. // check what requestAnimationFrame to use, and if
  34. // it's not supported, use the onscroll event
  35. var loop = window.requestAnimationFrame ||
  36. window.webkitRequestAnimationFrame ||
  37. window.mozRequestAnimationFrame ||
  38. window.msRequestAnimationFrame ||
  39. window.oRequestAnimationFrame ||
  40. function(callback){ return setTimeout(callback, 1000 / 60); };
  41. // store the id for later use
  42. var loopId = null;
  43. // Test via a getter in the options object to see if the passive property is accessed
  44. var supportsPassive = false;
  45. try {
  46. var opts = Object.defineProperty({}, 'passive', {
  47. get: function() {
  48. supportsPassive = true;
  49. }
  50. });
  51. window.addEventListener("testPassive", null, opts);
  52. window.removeEventListener("testPassive", null, opts);
  53. } catch (e) {}
  54. // check what cancelAnimation method to use
  55. var clearLoop = window.cancelAnimationFrame || window.mozCancelAnimationFrame || clearTimeout;
  56. // check which transform property to use
  57. var transformProp = window.transformProp || (function(){
  58. var testEl = document.createElement('div');
  59. if (testEl.style.transform === null) {
  60. var vendors = ['Webkit', 'Moz', 'ms'];
  61. for (var vendor in vendors) {
  62. if (testEl.style[ vendors[vendor] + 'Transform' ] !== undefined) {
  63. return vendors[vendor] + 'Transform';
  64. }
  65. }
  66. }
  67. return 'transform';
  68. })();
  69. // Default Settings
  70. self.options = {
  71. speed: -2,
  72. verticalSpeed: null,
  73. horizontalSpeed: null,
  74. breakpoints: [576, 768, 1201],
  75. center: false,
  76. wrapper: null,
  77. relativeToWrapper: false,
  78. round: true,
  79. vertical: true,
  80. horizontal: false,
  81. verticalScrollAxis: "y",
  82. horizontalScrollAxis: "x",
  83. callback: function() {},
  84. };
  85. // User defined options (might have more in the future)
  86. if (options){
  87. Object.keys(options).forEach(function(key){
  88. self.options[key] = options[key];
  89. });
  90. }
  91. function validateCustomBreakpoints () {
  92. if (self.options.breakpoints.length === 3 && Array.isArray(self.options.breakpoints)) {
  93. var isAscending = true;
  94. var isNumerical = true;
  95. var lastVal;
  96. self.options.breakpoints.forEach(function (i) {
  97. if (typeof i !== 'number') isNumerical = false;
  98. if (lastVal !== null) {
  99. if (i < lastVal) isAscending = false;
  100. }
  101. lastVal = i;
  102. });
  103. if (isAscending && isNumerical) return;
  104. }
  105. // revert defaults if set incorrectly
  106. self.options.breakpoints = [576, 768, 1201];
  107. console.warn("Rellax: You must pass an array of 3 numbers in ascending order to the breakpoints option. Defaults reverted");
  108. }
  109. if (options && options.breakpoints) {
  110. validateCustomBreakpoints();
  111. }
  112. // By default, rellax class
  113. if (!el) {
  114. el = '.rellax';
  115. }
  116. // check if el is a className or a node
  117. var elements = typeof el === 'string' ? document.querySelectorAll(el) : [el];
  118. // Now query selector
  119. if (elements.length > 0) {
  120. self.elems = elements;
  121. }
  122. // The elements don't exist
  123. else {
  124. console.warn("Rellax: The elements you're trying to select don't exist.");
  125. return;
  126. }
  127. // Has a wrapper and it exists
  128. if (self.options.wrapper) {
  129. if (!self.options.wrapper.nodeType) {
  130. var wrapper = document.querySelector(self.options.wrapper);
  131. if (wrapper) {
  132. self.options.wrapper = wrapper;
  133. } else {
  134. console.warn("Rellax: The wrapper you're trying to use doesn't exist.");
  135. return;
  136. }
  137. }
  138. }
  139. // set a placeholder for the current breakpoint
  140. var currentBreakpoint;
  141. // helper to determine current breakpoint
  142. var getCurrentBreakpoint = function (w) {
  143. var bp = self.options.breakpoints;
  144. if (w < bp[0]) return 'xs';
  145. if (w >= bp[0] && w < bp[1]) return 'sm';
  146. if (w >= bp[1] && w < bp[2]) return 'md';
  147. return 'lg';
  148. };
  149. // Get and cache initial position of all elements
  150. var cacheBlocks = function() {
  151. for (var i = 0; i < self.elems.length; i++){
  152. var block = createBlock(self.elems[i]);
  153. blocks.push(block);
  154. }
  155. };
  156. // Let's kick this script off
  157. // Build array for cached element values
  158. var init = function() {
  159. for (var i = 0; i < blocks.length; i++){
  160. self.elems[i].style.cssText = blocks[i].style;
  161. }
  162. blocks = [];
  163. screenY = window.innerHeight;
  164. screenX = window.innerWidth;
  165. currentBreakpoint = getCurrentBreakpoint(screenX);
  166. setPosition();
  167. cacheBlocks();
  168. animate();
  169. // If paused, unpause and set listener for window resizing events
  170. if (pause) {
  171. window.addEventListener('resize', init);
  172. pause = false;
  173. // Start the loop
  174. update();
  175. }
  176. };
  177. // We want to cache the parallax blocks'
  178. // values: base, top, height, speed
  179. // el: is dom object, return: el cache values
  180. var createBlock = function(el) {
  181. var dataPercentage = el.getAttribute( 'data-rellax-percentage' );
  182. var dataSpeed = el.getAttribute( 'data-rellax-speed' );
  183. var dataXsSpeed = el.getAttribute( 'data-rellax-xs-speed' );
  184. var dataMobileSpeed = el.getAttribute( 'data-rellax-mobile-speed' );
  185. var dataTabletSpeed = el.getAttribute( 'data-rellax-tablet-speed' );
  186. var dataDesktopSpeed = el.getAttribute( 'data-rellax-desktop-speed' );
  187. var dataVerticalSpeed = el.getAttribute('data-rellax-vertical-speed');
  188. var dataHorizontalSpeed = el.getAttribute('data-rellax-horizontal-speed');
  189. var dataVericalScrollAxis = el.getAttribute('data-rellax-vertical-scroll-axis');
  190. var dataHorizontalScrollAxis = el.getAttribute('data-rellax-horizontal-scroll-axis');
  191. var dataZindex = el.getAttribute( 'data-rellax-zindex' ) || 0;
  192. var dataMin = el.getAttribute( 'data-rellax-min' );
  193. var dataMax = el.getAttribute( 'data-rellax-max' );
  194. var dataMinX = el.getAttribute('data-rellax-min-x');
  195. var dataMaxX = el.getAttribute('data-rellax-max-x');
  196. var dataMinY = el.getAttribute('data-rellax-min-y');
  197. var dataMaxY = el.getAttribute('data-rellax-max-y');
  198. var mapBreakpoints;
  199. var breakpoints = true;
  200. if (!dataXsSpeed && !dataMobileSpeed && !dataTabletSpeed && !dataDesktopSpeed) {
  201. breakpoints = false;
  202. } else {
  203. mapBreakpoints = {
  204. 'xs': dataXsSpeed,
  205. 'sm': dataMobileSpeed,
  206. 'md': dataTabletSpeed,
  207. 'lg': dataDesktopSpeed
  208. };
  209. }
  210. // initializing at scrollY = 0 (top of browser), scrollX = 0 (left of browser)
  211. // ensures elements are positioned based on HTML layout.
  212. //
  213. // If the element has the percentage attribute, the posY and posX needs to be
  214. // the current scroll position's value, so that the elements are still positioned based on HTML layout
  215. var wrapperPosY = self.options.wrapper ? self.options.wrapper.scrollTop : (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
  216. // If the option relativeToWrapper is true, use the wrappers offset to top, subtracted from the current page scroll.
  217. if (self.options.relativeToWrapper) {
  218. var scrollPosY = (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
  219. wrapperPosY = scrollPosY - self.options.wrapper.offsetTop;
  220. }
  221. var posY = self.options.vertical ? ( dataPercentage || self.options.center ? wrapperPosY : 0 ) : 0;
  222. 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;
  223. var blockTop = posY + el.getBoundingClientRect().top;
  224. var blockHeight = el.clientHeight || el.offsetHeight || el.scrollHeight;
  225. var blockLeft = posX + el.getBoundingClientRect().left;
  226. var blockWidth = el.clientWidth || el.offsetWidth || el.scrollWidth;
  227. // apparently parallax equation everyone uses
  228. var percentageY = dataPercentage ? dataPercentage : (posY - blockTop + screenY) / (blockHeight + screenY);
  229. var percentageX = dataPercentage ? dataPercentage : (posX - blockLeft + screenX) / (blockWidth + screenX);
  230. if(self.options.center){ percentageX = 0.5; percentageY = 0.5; }
  231. // Optional individual block speed as data attr, otherwise global speed
  232. var speed = (breakpoints && mapBreakpoints[currentBreakpoint] !== null) ? Number(mapBreakpoints[currentBreakpoint]) : (dataSpeed ? dataSpeed : self.options.speed);
  233. var verticalSpeed = dataVerticalSpeed ? dataVerticalSpeed : self.options.verticalSpeed;
  234. var horizontalSpeed = dataHorizontalSpeed ? dataHorizontalSpeed : self.options.horizontalSpeed;
  235. // Optional individual block movement axis direction as data attr, otherwise gobal movement direction
  236. var verticalScrollAxis = dataVericalScrollAxis ? dataVericalScrollAxis : self.options.verticalScrollAxis;
  237. var horizontalScrollAxis = dataHorizontalScrollAxis ? dataHorizontalScrollAxis : self.options.horizontalScrollAxis;
  238. var bases = updatePosition(percentageX, percentageY, speed, verticalSpeed, horizontalSpeed);
  239. // ~~Store non-translate3d transforms~~
  240. // Store inline styles and extract transforms
  241. var style = el.style.cssText;
  242. var transform = '';
  243. // Check if there's an inline styled transform
  244. var searchResult = /transform\s*:/i.exec(style);
  245. if (searchResult) {
  246. // Get the index of the transform
  247. var index = searchResult.index;
  248. // Trim the style to the transform point and get the following semi-colon index
  249. var trimmedStyle = style.slice(index);
  250. var delimiter = trimmedStyle.indexOf(';');
  251. // Remove "transform" string and save the attribute
  252. if (delimiter) {
  253. transform = " " + trimmedStyle.slice(11, delimiter).replace(/\s/g,'');
  254. } else {
  255. transform = " " + trimmedStyle.slice(11).replace(/\s/g,'');
  256. }
  257. }
  258. return {
  259. baseX: bases.x,
  260. baseY: bases.y,
  261. top: blockTop,
  262. left: blockLeft,
  263. height: blockHeight,
  264. width: blockWidth,
  265. speed: speed,
  266. verticalSpeed: verticalSpeed,
  267. horizontalSpeed: horizontalSpeed,
  268. verticalScrollAxis: verticalScrollAxis,
  269. horizontalScrollAxis: horizontalScrollAxis,
  270. style: style,
  271. transform: transform,
  272. zindex: dataZindex,
  273. min: dataMin,
  274. max: dataMax,
  275. minX: dataMinX,
  276. maxX: dataMaxX,
  277. minY: dataMinY,
  278. maxY: dataMaxY
  279. };
  280. };
  281. // set scroll position (posY, posX)
  282. // side effect method is not ideal, but okay for now
  283. // returns true if the scroll changed, false if nothing happened
  284. var setPosition = function() {
  285. var oldY = posY;
  286. var oldX = posX;
  287. posY = self.options.wrapper ? self.options.wrapper.scrollTop : (document.documentElement || document.body.parentNode || document.body).scrollTop || window.pageYOffset;
  288. posX = self.options.wrapper ? self.options.wrapper.scrollLeft : (document.documentElement || document.body.parentNode || document.body).scrollLeft || window.pageXOffset;
  289. // If option relativeToWrapper is true, use relative wrapper value instead.
  290. if (self.options.relativeToWrapper) {
  291. var scrollPosY = (document.documentElement || document.body.parentNode || document.body).scrollTop || window.pageYOffset;
  292. posY = scrollPosY - self.options.wrapper.offsetTop;
  293. }
  294. if (oldY != posY && self.options.vertical) {
  295. // scroll changed, return true
  296. return true;
  297. }
  298. if (oldX != posX && self.options.horizontal) {
  299. // scroll changed, return true
  300. return true;
  301. }
  302. // scroll did not change
  303. return false;
  304. };
  305. // Ahh a pure function, gets new transform value
  306. // based on scrollPosition and speed
  307. // Allow for decimal pixel values
  308. var updatePosition = function(percentageX, percentageY, speed, verticalSpeed, horizontalSpeed) {
  309. var result = {};
  310. var valueX = ((horizontalSpeed ? horizontalSpeed : speed) * (100 * (1 - percentageX)));
  311. var valueY = ((verticalSpeed ? verticalSpeed : speed) * (100 * (1 - percentageY)));
  312. result.x = self.options.round ? Math.round(valueX) : Math.round(valueX * 100) / 100;
  313. result.y = self.options.round ? Math.round(valueY) : Math.round(valueY * 100) / 100;
  314. return result;
  315. };
  316. // Remove event listeners and loop again
  317. var deferredUpdate = function() {
  318. window.removeEventListener('resize', deferredUpdate);
  319. window.removeEventListener('orientationchange', deferredUpdate);
  320. (self.options.wrapper ? self.options.wrapper : window).removeEventListener('scroll', deferredUpdate);
  321. (self.options.wrapper ? self.options.wrapper : document).removeEventListener('touchmove', deferredUpdate);
  322. // loop again
  323. loopId = loop(update);
  324. };
  325. // Loop
  326. var update = function() {
  327. if (setPosition() && pause === false) {
  328. animate();
  329. // loop again
  330. loopId = loop(update);
  331. } else {
  332. loopId = null;
  333. // Don't animate until we get a position updating event
  334. window.addEventListener('resize', deferredUpdate);
  335. window.addEventListener('orientationchange', deferredUpdate);
  336. (self.options.wrapper ? self.options.wrapper : window).addEventListener('scroll', deferredUpdate, supportsPassive ? { passive: true } : false);
  337. (self.options.wrapper ? self.options.wrapper : document).addEventListener('touchmove', deferredUpdate, supportsPassive ? { passive: true } : false);
  338. }
  339. };
  340. // Transform3d on parallax element
  341. var animate = function() {
  342. var positions;
  343. for (var i = 0; i < self.elems.length; i++){
  344. // Determine relevant movement directions
  345. var verticalScrollAxis = blocks[i].verticalScrollAxis.toLowerCase();
  346. var horizontalScrollAxis = blocks[i].horizontalScrollAxis.toLowerCase();
  347. var verticalScrollX = verticalScrollAxis.indexOf("x") != -1 ? posY : 0;
  348. var verticalScrollY = verticalScrollAxis.indexOf("y") != -1 ? posY : 0;
  349. var horizontalScrollX = horizontalScrollAxis.indexOf("x") != -1 ? posX : 0;
  350. var horizontalScrollY = horizontalScrollAxis.indexOf("y") != -1 ? posX : 0;
  351. var percentageY = ((verticalScrollY + horizontalScrollY - blocks[i].top + screenY) / (blocks[i].height + screenY));
  352. var percentageX = ((verticalScrollX + horizontalScrollX - blocks[i].left + screenX) / (blocks[i].width + screenX));
  353. // Subtracting initialize value, so element stays in same spot as HTML
  354. positions = updatePosition(percentageX, percentageY, blocks[i].speed, blocks[i].verticalSpeed, blocks[i].horizontalSpeed);
  355. var positionY = positions.y - blocks[i].baseY;
  356. var positionX = positions.x - blocks[i].baseX;
  357. // The next two "if" blocks go like this:
  358. // Check if a limit is defined (first "min", then "max");
  359. // Check if we need to change the Y or the X
  360. // (Currently working only if just one of the axes is enabled)
  361. // Then, check if the new position is inside the allowed limit
  362. // If so, use new position. If not, set position to limit.
  363. // Check if a min limit is defined
  364. if (blocks[i].min !== null) {
  365. if (self.options.vertical && !self.options.horizontal) {
  366. positionY = positionY <= blocks[i].min ? blocks[i].min : positionY;
  367. }
  368. if (self.options.horizontal && !self.options.vertical) {
  369. positionX = positionX <= blocks[i].min ? blocks[i].min : positionX;
  370. }
  371. }
  372. // Check if directional min limits are defined
  373. if (blocks[i].minY != null) {
  374. positionY = positionY <= blocks[i].minY ? blocks[i].minY : positionY;
  375. }
  376. if (blocks[i].minX != null) {
  377. positionX = positionX <= blocks[i].minX ? blocks[i].minX : positionX;
  378. }
  379. // Check if a max limit is defined
  380. if (blocks[i].max !== null) {
  381. if (self.options.vertical && !self.options.horizontal) {
  382. positionY = positionY >= blocks[i].max ? blocks[i].max : positionY;
  383. }
  384. if (self.options.horizontal && !self.options.vertical) {
  385. positionX = positionX >= blocks[i].max ? blocks[i].max : positionX;
  386. }
  387. }
  388. // Check if directional max limits are defined
  389. if (blocks[i].maxY != null) {
  390. positionY = positionY >= blocks[i].maxY ? blocks[i].maxY : positionY;
  391. }
  392. if (blocks[i].maxX != null) {
  393. positionX = positionX >= blocks[i].maxX ? blocks[i].maxX : positionX;
  394. }
  395. var zindex = blocks[i].zindex;
  396. // Move that element
  397. // (Set the new translation and append initial inline transforms.)
  398. var translate = 'translate3d(' + (self.options.horizontal ? positionX : '0') + 'px,' + (self.options.vertical ? positionY : '0') + 'px,' + zindex + 'px) ' + blocks[i].transform;
  399. self.elems[i].style[transformProp] = translate;
  400. }
  401. self.options.callback(positions);
  402. };
  403. self.destroy = function() {
  404. for (var i = 0; i < self.elems.length; i++){
  405. self.elems[i].style.cssText = blocks[i].style;
  406. }
  407. // Remove resize event listener if not pause, and pause
  408. if (!pause) {
  409. window.removeEventListener('resize', init);
  410. pause = true;
  411. }
  412. // Clear the animation loop to prevent possible memory leak
  413. clearLoop(loopId);
  414. loopId = null;
  415. };
  416. // Init
  417. init();
  418. // Allow to recalculate the initial values whenever we want
  419. self.refresh = init;
  420. return self;
  421. };
  422. return Rellax;
  423. }));