UNPKG

scroll-parallax

Version:

ES6/ES2015 unobstrusive modern HW accelerated scrollable images parallax

619 lines (535 loc) 16.5 kB
(function (global, factory) { if (typeof define === "function" && define.amd) { define('Parallax', ['module'], factory); } else if (typeof exports !== "undefined") { factory(module); } else { var mod = { exports: {} }; factory(mod); global.Parallax = mod.exports; } })(this, function (module) { 'use strict'; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function $$(selector, ctx) { var els = void 0; if (typeof selector == 'string') { els = (ctx || document).querySelectorAll(selector); } else { els = selector; } return Array.prototype.slice.call(els); } function extend(src) { var obj = void 0, args = arguments; for (var i = 1; i < args.length; ++i) { if (obj = args[i]) { for (var key in obj) { src[key] = obj[key]; } } } return src; } function isUndefined(val) { return typeof val == 'undefined'; } function toCamel(string) { return string.replace(/-(\w)/g, function (_, c) { return c.toUpperCase(); }); } function elementData(el, attr) { if (attr) return el.dataset[attr] || el.getAttribute('data-' + attr);else return el.dataset || Array.prototype.slice.call(el.attributes).reduce(function (ret, attribute) { if (/data-/.test(attribute.name)) ret[toCamel(attribute.name)] = attribute.value; return ret; }, {}); } function prefix(obj, prop) { var prefixes = ['ms', 'o', 'Moz', 'webkit']; var i = prefixes.length; while (i--) { var _prefix = prefixes[i], p = _prefix ? _prefix + prop[0].toUpperCase() + prop.substr(1) : prop.toLowerCase() + prop.substr(1); if (p in obj) { return p; } } return ''; } var observable = function observable(el) { el = el || {}; var callbacks = {}, slice = Array.prototype.slice; Object.defineProperties(el, { on: { value: function value(event, fn) { if (typeof fn == 'function') (callbacks[event] = callbacks[event] || []).push(fn); return el; }, enumerable: false, writable: false, configurable: false }, off: { value: function value(event, fn) { if (event == '*' && !fn) callbacks = {};else { if (fn) { var arr = callbacks[event]; for (var i = 0, cb; cb = arr && arr[i]; ++i) { if (cb == fn) arr.splice(i--, 1); } } else delete callbacks[event]; } return el; }, enumerable: false, writable: false, configurable: false }, one: { value: function value(event, fn) { function on() { el.off(event, on); fn.apply(el, arguments); } return el.on(event, on); }, enumerable: false, writable: false, configurable: false }, trigger: { value: function value(event) { var arglen = arguments.length - 1, args = new Array(arglen), fns, fn, i; for (i = 0; i < arglen; i++) { args[i] = arguments[i + 1]; } fns = slice.call(callbacks[event] || [], 0); for (i = 0; fn = fns[i]; ++i) { fn.apply(el, args); } if (callbacks['*'] && event != '*') el.trigger.apply(el, ['*', event].concat(args)); return el; }, enumerable: false, writable: false, configurable: false } }); return el; }; var rAF = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || function (cb) { setTimeout(cb, 1000 / 60); }; var RESIZE_DELAY = 20; var Stage = function () { function Stage() { _classCallCheck(this, Stage); observable(this); this.resizeTimer = null; this.tick = false; this.bind(); } _createClass(Stage, [{ key: 'bind', value: function bind() { var _this = this; window.addEventListener('scroll', function () { return _this.scroll(); }, true); window.addEventListener('mousewheel', function () { return _this.scroll(); }, true); window.addEventListener('touchmove', function () { return _this.scroll(); }, true); window.addEventListener('resize', function () { return _this.resize(); }, true); window.addEventListener('orientationchange', function () { return _this.resize(); }, true); window.onload = function () { return _this.scroll(); }; return this; } }, { key: 'scroll', value: function scroll() { var _this2 = this; if (this.tick) return this; this.tick = !this.tick; rAF(function () { return _this2.update(); }); return this; } }, { key: 'update', value: function update() { this.trigger('scroll', this.scrollTop); this.tick = !this.tick; return this; } }, { key: 'resize', value: function resize() { var _this3 = this; if (this.resizeTimer) clearTimeout(this.resizeTimer); this.resizeTimer = setTimeout(function () { return _this3.trigger('resize', _this3.size); }, RESIZE_DELAY); return this; } }, { key: 'scrollTop', get: function get() { var top = (window.pageYOffset || document.scrollTop) - (document.clientTop || 0); return window.isNaN(top) ? 0 : top; } }, { key: 'height', get: function get() { return window.innerHeight; } }, { key: 'width', get: function get() { return window.innerWidth; } }, { key: 'size', get: function get() { return { width: this.width, height: this.height }; } }]); return Stage; }(); var TRANSFORM_PREFIX = function (div) { return prefix(div.style, 'transform'); }(document.createElement('div')); var HAS_MATRIX = function (div) { div.style[TRANSFORM_PREFIX] = 'matrix(1, 0, 0, 1, 0, 0)'; return (/matrix/g.test(div.style.cssText) ); }(document.createElement('div')); var Canvas = function () { function Canvas(element, opts) { _classCallCheck(this, Canvas); observable(this); this.opts = opts; this.element = element; this.wrapper = element.parentNode; this.isLoaded = false; this.initial = element.cloneNode(true); } _createClass(Canvas, [{ key: 'load', value: function load() { var _this4 = this; var isImage = this.element.complete !== undefined; var isVideo = this.element.oncanplay !== undefined; if (isImage && !this.element.complete) { this.element.onload = function () { return _this4.onElementLoaded(); }; } else if (isVideo && !this.element.oncanplay) { this.element.onload = this.element.oncanplay = function () { return _this4.onElementLoaded(); }; } else { this.onElementLoaded(); } return this; } }, { key: 'destroy', value: function destroy() { this.element.parentNode.replaceChild(this.initial, this.element); this.off('*'); } }, { key: 'onElementLoaded', value: function onElementLoaded() { this.isLoaded = true; this.update(); this.element.style.willChange = 'transform'; this.trigger('loaded', this.element); return this; } }, { key: 'update', value: function update() { var iw = this.element.naturalWidth || this.element.width || this.element.offsetWidth, ih = this.element.naturalHeight || this.element.height || this.element.offsetHeight, ratio = iw / ih, size = this.size; var nh = void 0, nw = void 0, offsetTop = void 0, offsetLeft = void 0; if (size.width / ratio <= size.height) { nw = size.height * ratio; nh = size.height; } else { nw = size.width; nh = size.width / ratio; } if (nh <= size.height + size.height * this.opts.safeHeight) { nw += nw * this.opts.safeHeight; nh += nh * this.opts.safeHeight; } offsetTop = -~~((nh - size.height) / 2); offsetLeft = -~~((nw - size.width) / 2); this.element.width = nw; this.element.height = nh; this.element.style.top = offsetTop + 'px'; this.element.style.left = offsetLeft + 'px'; return this; } }, { key: 'draw', value: function draw(_ref) { var scrollTop = _ref.scrollTop, width = _ref.width, height = _ref.height; var size = this.size, perc = (this.offset.top + size.height * this.opts.center + height / 2 - scrollTop) / height - 1, offset = ~~(perc * (this.element.height / size.height / 2 * this.opts.intensity) * 10); this.element.style[TRANSFORM_PREFIX] = HAS_MATRIX ? 'matrix(1,0,0,1, 0, ' + -offset + ')' : 'translate(0, ' + -offset + 'px)'; return this; } }, { key: 'bounds', get: function get() { return this.wrapper.getBoundingClientRect(); } }, { key: 'offset', get: function get() { return { top: this.wrapper.offsetTop, left: this.wrapper.offsetLeft }; } }, { key: 'size', get: function get() { var props = this.bounds; return { height: props.height | 0, width: props.width | 0 }; } }]); return Canvas; }(); var stage = void 0; var Parallax = function () { function Parallax() { var selector = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, Parallax); observable(this); this.opts = opts; this.selector = selector; this.canvases = []; this.bound = false; if (selector !== null) { this.add(selector); } if (!stage) stage = new Stage(); return this; } _createClass(Parallax, [{ key: 'init', value: function init() { if (this.bound) { throw 'The parallax instance has already been initialized'; } if (!this.canvases.length && this.selector !== null) { console.warn('No elements were found with the selector "' + this.selector + '"'); } else { this.elementsLoaded = 0; this.bind(); } return this; } }, { key: 'bind', value: function bind() { var _this5 = this; this._onResize = function () { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _this5.resize.apply(_this5, args); }; this._onScroll = function () { for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } return _this5.scroll.apply(_this5, args); }; stage.on('resize', this._onResize); stage.on('scroll', this._onScroll); this.canvases.forEach(function (canvas) { canvas.one('loaded', function () { return _this5.onCanvasLoaded(canvas); }); canvas.load(); }); this.bound = true; return this; } }, { key: 'refresh', value: function refresh() { this._onResize(stage.size); this._onScroll(stage.scrollTop); return this; } }, { key: 'onCanvasLoaded', value: function onCanvasLoaded(canvas) { this.trigger('element:loaded', canvas.element, canvas); this.elementsLoaded++; canvas.draw(stage); if (this.elementsLoaded == this.canvases.length) this.trigger('elements:loaded'); return this; } }, { key: 'scroll', value: function scroll(scrollTop) { var offsetYBounds = this.opts.offsetYBounds, _stage = stage, height = _stage.height, width = _stage.width; var i = this.canvases.length; while (i--) { var canvas = this.canvases[i], canvasHeight = canvas.size.height, canvasOffset = canvas.offset; if (canvas.isLoaded && scrollTop + stage.height + offsetYBounds > canvasOffset.top && canvasOffset.top + canvasHeight > scrollTop - offsetYBounds) { canvas.draw({ height: height, scrollTop: scrollTop, width: width }); this.trigger('draw', canvas.element); } } this.trigger('update', scrollTop); return this; } }, { key: 'add', value: function add(els) { var _this6 = this; var canvases = this.createCanvases($$(els)); if (this.bound) { canvases.forEach(function (canvas) { canvas.one('loaded', function () { return _this6.onCanvasLoaded(canvas); }); canvas.load(); }); } this.canvases = this.canvases.concat(canvases); return this; } }, { key: 'remove', value: function remove(els) { var _this7 = this; $$(els).forEach(function (el) { var i = _this7.canvases.length; while (i--) { if (el == _this7.canvases[i].element) { _this7.canvases[i].destroy(); _this7.canvases.splice(i, 1); break; } } }); return this; } }, { key: 'destroy', value: function destroy() { this.off('*'); this.canvases = []; stage.off('resize', this._onResize).off('scroll', this._onScroll); return this; } }, { key: 'resize', value: function resize(size) { var i = this.canvases.length; while (i--) { var canvas = this.canvases[i]; if (!canvas.isLoaded) return; canvas.update().draw(stage); } this.trigger('resize'); return this; } }, { key: 'createCanvases', value: function createCanvases(els) { var _this8 = this; return els.map(function (el) { var data = elementData(el); return new Canvas(el, { intensity: !isUndefined(data.intensity) ? +data.intensity : _this8.opts.intensity, center: !isUndefined(data.center) ? +data.center : _this8.opts.center, safeHeight: !isUndefined(data.safeHeight) ? +data.safeHeight : _this8.opts.safeHeight }); }); } }, { key: 'opts', set: function set(opts) { this._defaults = { offsetYBounds: 50, intensity: 30, center: 0.5, safeHeight: 0.15 }; extend(this._defaults, opts); }, get: function get() { return this._defaults; } }]); return Parallax; }(); module.exports = Parallax; });