(function(root, factory) { if (typeof define === 'function' && define.amd) { define(factory); } else if (typeof exports === 'object') { module.exports = factory(require, exports, module); } else { root.ScrollReveal = factory(); } }(this, function(require, exports, module) { /** * ScrollReveal * ------------ * Version : 3.1.1 * Website : scrollrevealjs.org * Repo : github.com/jlmakes/scrollreveal.js * Author : Julian Lloyd (@jlmakes) */ ; (function() { 'use strict'; var sr, Tools, _requestAnimationFrame; this.ScrollReveal = (function() { /** * Configuration * ------------- * This object signature can be passed directly to the ScrollReveal constructor, * or as the second argument of the `reveal()` method. */ ScrollReveal.prototype.defaults = { // 'bottom', 'left', 'top', 'right' origin: 'bottom', // Can be any valid CSS distance, e.g. '5rem', '10%', '20vw', etc. distance: '20px', // Time in milliseconds. duration: 500, delay: 0, // Starting angles in degrees, will transition from these values to 0 in all axes. rotate: { x: 0, y: 0, z: 0 }, // Starting opacity value, before transitioning to the computed opacity. opacity: 0, // Starting scale value, will transition from this value to 1 scale: 0.9, // Accepts any valid CSS easing, e.g. 'ease', 'ease-in-out', 'linear', etc. easing: 'cubic-bezier(0.6, 0.2, 0.1, 1)', // When null, `` is assumed to be the reveal container. You can pass a // DOM node as a custom container, e.g. document.querySelector('.fooContainer') // or a selector, e.g. '.fooContainer' container: null, // true/false to control reveal animations on mobile. mobile: true, // true: reveals occur every time elements become visible // false: reveals occur once as elements become visible reset: false, // 'always' 鈥� delay for all reveal animations // 'once' 鈥� delay only the first time reveals occur // 'onload' - delay only for animations triggered by first load useDelay: 'always', // Change when an element is considered in the viewport. The default value // of 0.20 means 20% of an element must be visible for its reveal to occur. viewFactor: 0.2, // Pixel values that alter the container boundaries. // e.g. Set `{ top: 48 }`, if you have a 48px tall fixed toolbar. // -- // Visual Aid: http://scrollrevealjs.org/assets/viewoffset.png viewOffset: { top: 0, right: 0, bottom: 0, left: 0 }, // Callbacks that fire for each completed element reveal, and if // `config.reset = true`, for each completed element reset. When creating your // callbacks, remember they are passed the element鈥檚 DOM node that triggered // it as the first argument. afterReveal: function(domEl) {}, afterReset: function(domEl) {} }; function ScrollReveal(config) { // Support instantiation without the `new` keyword. if (typeof this == 'undefined' || Object.getPrototypeOf(this) !== ScrollReveal.prototype) { return new ScrollReveal(config) } sr = this; // Save reference to instance. sr.tools = new Tools(); // *required utilities if (sr.isSupported()) { sr.tools.extend(sr.defaults, config || {}); _resolveContainer(sr.defaults); sr.store = { elements: {}, containers: [] }; sr.sequences = {}; sr.history = []; sr.uid = 0; sr.initialized = false; } // Note: IE9 only supports console if devtools are open. else if (typeof console !== 'undefined' && console !== null) { console.log('ScrollReveal is not supported in this browser.'); } return sr } /** * Check if client supports CSS Transform and CSS Transition. * @return {boolean} */ ScrollReveal.prototype.isSupported = function() { var style = document.documentElement.style; return 'WebkitTransition' in style && 'WebkitTransform' in style || 'transition' in style && 'transform' in style }; /** * Creates a reveal set, a group of elements that will animate when they * become visible. If [interval] is provided, a new sequence is created * that will ensure elements reveal in the order they appear in the DOM. * * @param {string|Node} [selector] The element (node) or elements (selector) to animate. * @param {Object} [config] Override the defaults for this reveal set. * @param {number} [interval] Time between sequenced element animations (milliseconds). * @param {boolean} [sync] Used internally when updating reveals for async content. * * @return {Object} The current ScrollReveal instance. */ ScrollReveal.prototype.reveal = function(selector, config, interval, sync) { var container, elements, elem, elemId, sequence, sequenceId; // Resolve container. if (config && config.container) { container = _resolveContainer(config); } else { container = sr.defaults.container; } // Let鈥檚 check to see if a DOM node was passed in as the first argument, // otherwise query the container for all elements matching the selector. if (sr.tools.isNode(selector)) { elements = [selector]; } else { elements = Array.prototype.slice.call(container.querySelectorAll(selector)); } if (!elements.length) { console.log('ScrollReveal: reveal on "' + selector + '"" failed, no elements found.'); return sr } // No custom configuration was passed, but a sequence interval instead. // let鈥檚 shuffle things around to make sure everything works. if (config && typeof config == 'number') { interval = config; config = {}; } // Prepare a new sequence if an interval is passed. if (interval && typeof interval == 'number') { sequenceId = _nextUid(); sequence = sr.sequences[sequenceId] = { id: sequenceId, interval: interval, elemIds: [], active: false } } // Begin main loop to configure ScrollReveal elements. for (var i = 0; i < elements.length; i++) { // Check if the element has already been configured and grab it from the store. elemId = elements[i].getAttribute('data-sr-id'); if (elemId) { elem = sr.store.elements[elemId]; } // Otherwise, let鈥檚 do some basic setup. else { elem = { id: _nextUid(), domEl: elements[i], seen: false, revealing: false }; elem.domEl.setAttribute('data-sr-id', elem.id); } // Sequence only setup if (sequence) { elem.sequence = { id: sequence.id, index: sequence.elemIds.length }; sequence.elemIds.push(elem.id); } // New or existing element, it鈥檚 time to update its configuration, styles, // and send the updates to our store. _configure(elem, config || {}); _style(elem); _updateStore(elem); // We need to make sure elements are set to visibility: visibile, even when // on mobile and `config.mobile == false`, or if unsupported. if (sr.tools.isMobile() && !elem.config.mobile || !sr.isSupported()) { elem.domEl.setAttribute('style', elem.styles.inline); elem.disabled = true; } // Otherwise, proceed normally. else if (!elem.revealing) { elem.domEl.setAttribute('style', elem.styles.inline + elem.styles.transform.initial ); } } // Each `reveal()` is recorded so that when calling `sync()` while working // with asynchronously loaded content, it can re-trace your steps but with // all your new elements now in the DOM. // Since `reveal()` is called internally by `sync()`, we don鈥檛 want to // record or intiialize each reveal during syncing. if (!sync && sr.isSupported()) { _record(selector, config); // We push initialization to the event queue using setTimeout, so that we can // give ScrollReveal room to process all reveal calls before putting things into motion. // -- // Philip Roberts - What the heck is the event loop anyway? (JSConf EU 2014) // http://www.youtube.com/watch?v=8aGhZQkoFbQ if (sr.initTimeout) { window.clearTimeout(sr.initTimeout); } sr.initTimeout = window.setTimeout(_init, 0); } return sr }; /** * Re-runs `reveal()` for each record stored in history, effectively capturing * any content loaded asynchronously that matches existing reveal set selectors. * * @return {Object} The current ScrollReveal instance. */ ScrollReveal.prototype.sync = function() { if (sr.history.length && sr.isSupported()) { for (var i = 0; i < sr.history.length; i++) { var record = sr.history[i]; sr.reveal(record.selector, record.config, record.interval, true); }; _init(); } else { console.log('ScrollReveal: sync failed, no reveals found.'); } return sr }; /** * Private Methods * --------------- * These methods remain accessible only to the ScrollReveal instance, even * though they only "exist" during instantiation outside of the constructors scope. * -- * http://stackoverflow.com/questions/111102/how-do-javascript-closures-work */ function _resolveContainer(config) { var container = config.container; // Check if our container is defined by a selector. if (container && typeof container == 'string') { return config.container = window.document.querySelector(container); } // Check if our container is defined by a node. else if (container && !sr.tools.isNode(container)) { console.log('ScrollReveal: Invalid container provided, using instead.'); config.container = null; } // Otherwise use by default. if (container == null) { config.container = window.document.documentElement; } return config.container } /** * A consistent way of creating unique IDs. * @returns {number} */ function _nextUid() { return ++sr.uid; } function _configure(elem, config) { // If the element hasn鈥檛 already been configured, let鈥檚 use a clone of the // defaults extended by the configuration passed as the second argument. if (!elem.config) { elem.config = sr.tools.extendClone(sr.defaults, config); } // Otherwise, let鈥檚 use a clone of the existing element configuration extended // by the configuration passed as the second argument. else { elem.config = sr.tools.extendClone(elem.config, config); } // Infer CSS Transform axis from origin string. if (elem.config.origin === 'top' || elem.config.origin === 'bottom') { elem.config.axis = 'Y'; } else { elem.config.axis = 'X'; } // Let鈥檚 make sure our our pixel distances are negative for top and left. // e.g. config.origin = 'top' and config.distance = '25px' starts at `top: -25px` in CSS. if (elem.config.origin === 'top' || elem.config.origin === 'left') { elem.config.distance = '-' + elem.config.distance; } } function _style(elem) { var computed = window.getComputedStyle(elem.domEl); if (!elem.styles) { elem.styles = { transition: {}, transform: {}, computed: {} }; // Capture any existing inline styles, and add our visibility override. // -- // See section 4.2. in the Documentation: // http://github.com/jlmakes/scrollreveal.js#42-improve-user-experience elem.styles.inline = elem.domEl.getAttribute('style') || ''; elem.styles.inline += '; visibility: visible; '; // grab the elements existing opacity. elem.styles.computed.opacity = computed.opacity; // grab the elements existing transitions. if (!computed.transition || computed.transition == 'all 0s ease 0s') { elem.styles.computed.transition = ''; } else { elem.styles.computed.transition = computed.transition + ', '; } } // Create transition styles elem.styles.transition.instant = _generateTransition(elem, 0); elem.styles.transition.delayed = _generateTransition(elem, elem.config.delay); // Generate transform styles, first with the webkit prefix. elem.styles.transform.initial = ' -webkit-transform:'; elem.styles.transform.target = ' -webkit-transform:'; _generateTransform(elem); // And again without any prefix. elem.styles.transform.initial += 'transform:'; elem.styles.transform.target += 'transform:'; _generateTransform(elem); } function _generateTransition(elem, delay) { var config = elem.config; return '-webkit-transition: ' + elem.styles.computed.transition + '-webkit-transform ' + config.duration / 1000 + 's ' + config.easing + ' ' + delay / 1000 + 's, opacity ' + config.duration / 1000 + 's ' + config.easing + ' ' + delay / 1000 + 's; ' + 'transition: ' + elem.styles.computed.transition + 'transform ' + config.duration / 1000 + 's ' + config.easing + ' ' + delay / 1000 + 's, opacity ' + config.duration / 1000 + 's ' + config.easing + ' ' + delay / 1000 + 's; ' } function _generateTransform(elem) { var config = elem.config; var transform = elem.styles.transform; if (parseInt(config.distance)) { transform.initial += ' translate' + config.axis + '(' + config.distance + ')'; transform.target += ' translate' + config.axis + '(0)'; } if (config.scale) { transform.initial += ' scale(' + config.scale + ')'; transform.target += ' scale(1)'; } if (config.rotate.x) { transform.initial += ' rotateX(' + config.rotate.x + 'deg)'; transform.target += ' rotateX(0)'; } if (config.rotate.y) { transform.initial += ' rotateY(' + config.rotate.y + 'deg)'; transform.target += ' rotateY(0)'; } if (config.rotate.z) { transform.initial += ' rotateZ(' + config.rotate.z + 'deg)'; transform.target += ' rotateZ(0)'; } transform.initial += '; opacity: ' + config.opacity + ';'; transform.target += '; opacity: ' + elem.styles.computed.opacity + ';'; } function _updateStore(elem) { var container = elem.config.container; // If this element鈥檚 container isn鈥檛 already in the store, let鈥檚 add it. if (container && sr.store.containers.indexOf(container) == -1) { sr.store.containers.push(elem.config.container); } // Update the element stored with our new element. sr.store.elements[elem.id] = elem; }; function _record(selector, config, interval) { // Save the `reveal()` arguments that triggered this `_record()` call, so we // can re-trace our steps when calling the `sync()` method. var record = { selector: selector, config: config, interval: interval }; sr.history.push(record); } function _init() { if (sr.isSupported()) { // Initial animate call triggers valid reveal animations on first load. // Subsequent animate calls are made inside the event handler. _animate(); // Then we loop through all container nodes in the store and bind event // listeners to each. for (var i = 0; i < sr.store.containers.length; i++) { sr.store.containers[i].addEventListener('scroll', _handler); sr.store.containers[i].addEventListener('resize', _handler); } // Let鈥檚 also do a one-time binding of window event listeners. if (!sr.initialized) { window.addEventListener('scroll', _handler); window.addEventListener('resize', _handler); sr.initialized = true; } } return sr } function _handler() { _requestAnimationFrame(_animate); } function _setActiveSequences() { var active, elem, elemId, sequence; // Loop through all sequences sr.tools.forOwn(sr.sequences, function(sequenceId) { sequence = sr.sequences[sequenceId]; active = false; // For each sequenced elemenet, let鈥檚 check visibility and if // any are visible, set it鈥檚 sequence to active. for (var i = 0; i < sequence.elemIds.length; i++) { elemId = sequence.elemIds[i] elem = sr.store.elements[elemId]; if (_isElemVisible(elem) && !active) { active = true; } } sequence.active = active; }); } function _animate() { var delayed, elem; _setActiveSequences(); // Loop through all elements in the store sr.tools.forOwn(sr.store.elements, function(elemId) { elem = sr.store.elements[elemId]; delayed = _shouldUseDelay(elem); // Let鈥檚 see if we should reveal, and if so, whether to use delay. if (_shouldReveal(elem)) { if (delayed) { elem.domEl.setAttribute('style', elem.styles.inline + elem.styles.transform.target + elem.styles.transition.delayed ); } else { elem.domEl.setAttribute('style', elem.styles.inline + elem.styles.transform.target + elem.styles.transition.instant ); } // Let鈥檚 queue the `afterReveal` callback and tag the element. _queueCallback('reveal', elem, delayed); elem.revealing = true; elem.seen = true; if (elem.sequence) { _queueNextInSequence(elem, delayed); } } // If we got this far our element shouldn鈥檛 reveal, but should it reset? else if (_shouldReset(elem)) { elem.domEl.setAttribute('style', elem.styles.inline + elem.styles.transform.initial + elem.styles.transition.instant ); _queueCallback('reset', elem); elem.revealing = false; } }); } /** * Sequence callback that triggers the next element. */ function _queueNextInSequence(elem, delayed) { var elapsed = 0, delay = 0, sequence = sr.sequences[elem.sequence.id]; // We鈥檙e processing a sequenced element, so let's block other elements in this sequence. sequence.blocked = true; // Since we鈥檙e triggering animations a part of a sequence after animations on first load, // we need to check for that condition and explicitly add the delay to our timer. if (delayed && elem.config.useDelay == 'onload') { delay = elem.config.delay; } // If a sequence timer is already running, capture the elapsed time and clear it. if (elem.sequence.timer) { elapsed = Math.abs(elem.sequence.timer.started - new Date()); window.clearTimeout(elem.sequence.timer); } // Start a new timer. elem.sequence.timer = { started: new Date() }; elem.sequence.timer.clock = window.setTimeout(function() { // Sequence interval has passed, so unblock the sequence and re-run the handler. sequence.blocked = false; elem.sequence.timer = null; _handler(); }, Math.abs(sequence.interval) + delay - elapsed); } function _queueCallback(type, elem, delayed) { var elapsed = 0, duration = 0, callback = 'after'; // Check which callback we鈥檙e working with. switch (type) { case 'reveal': duration = elem.config.duration; if (delayed) { duration += elem.config.delay; } callback += 'Reveal'; break case 'reset': duration = elem.config.duration; callback += 'Reset'; break } // If a timer is already running, capture the elapsed time and clear it. if (elem.timer) { elapsed = Math.abs(elem.timer.started - new Date()); window.clearTimeout(elem.timer.clock); } // Start a new timer. elem.timer = { started: new Date() }; elem.timer.clock = window.setTimeout(function() { // The timer completed, so let鈥檚 fire the callback and null the timer. elem.config[callback](elem.domEl); elem.timer = null; }, duration - elapsed); } function _shouldReveal(elem) { if (elem.sequence) { var sequence = sr.sequences[elem.sequence.id]; return sequence.active && !sequence.blocked && !elem.revealing && !elem.disabled } return _isElemVisible(elem) && !elem.revealing && !elem.disabled } function _shouldUseDelay(elem) { var config = elem.config.useDelay; return config === 'always' || (config === 'onload' && !sr.initialized) || (config === 'once' && !elem.seen) } function _shouldReset(elem) { if (elem.sequence) { var sequence = sr.sequences[elem.sequence.id]; return !sequence.active && elem.config.reset && elem.revealing && !elem.disabled } return !_isElemVisible(elem) && elem.config.reset && elem.revealing && !elem.disabled } function _getContainer(container) { return { width: container.clientWidth, height: container.clientHeight } } function _getScrolled(container) { // Return the container scroll values, plus the its offset. if (container && container !== window.document.documentElement) { var offset = _getOffset(container); return { x: container.scrollLeft + offset.left, y: container.scrollTop + offset.top } } // Otherwise, default to the window object鈥檚 scroll values. else { return { x: window.pageXOffset, y: window.pageYOffset } } } function _getOffset(domEl) { var offsetTop = 0, offsetLeft = 0, // Grab the element鈥檚 dimensions. offsetHeight = domEl.offsetHeight, offsetWidth = domEl.offsetWidth; // Now calculate the distance between the element and its parent, then // again for the parent to its parent, and again etc... until we have the // total distance of the element to the document鈥檚 top and left origin. do { if (!isNaN(domEl.offsetTop)) { offsetTop += domEl.offsetTop; } if (!isNaN(domEl.offsetLeft)) { offsetLeft += domEl.offsetLeft; } } while (domEl = domEl.offsetParent); return { top: offsetTop, left: offsetLeft, height: offsetHeight, width: offsetWidth } } function _isElemVisible(elem) { var offset = _getOffset(elem.domEl), container = _getContainer(elem.config.container), scrolled = _getScrolled(elem.config.container), vF = elem.config.viewFactor, // Define the element geometry. elemHeight = offset.height, elemWidth = offset.width, elemTop = offset.top, elemLeft = offset.left, elemBottom = elemTop + elemHeight, elemRight = elemLeft + elemWidth; return confirmBounds() || isPositionFixed() function confirmBounds() { var // Define the element鈥檚 functional boundaries using its view factor. top = elemTop + elemHeight * vF, left = elemLeft + elemWidth * vF, bottom = elemBottom - elemHeight * vF, right = elemRight - elemWidth * vF, // Define the container functional boundaries using its view offset. viewTop = scrolled.y + elem.config.viewOffset.top, viewLeft = scrolled.x + elem.config.viewOffset.left, viewBottom = scrolled.y - elem.config.viewOffset.bottom + container.height, viewRight = scrolled.x - elem.config.viewOffset.right + container.width; return top < viewBottom && bottom > viewTop && left > viewLeft && right < viewRight } function isPositionFixed() { return (window.getComputedStyle(elem.domEl).position === 'fixed') } } return ScrollReveal })(); /** * helper.tools.js * --------------- * Simple deep object extend, and a few other agnostic helper methods. * gist: http://gist.github.com/jlmakes/9f104e3f1b4d86334987 */ Tools = (function() { Tools.prototype.isObject = function(object) { return object !== null && typeof object === 'object' && object.constructor == Object }; Tools.prototype.isNode = function(object) { return typeof Node === 'object' ? object instanceof Node : object && typeof object === 'object' && typeof object.nodeType === 'number' && typeof object.nodeName === 'string' }; Tools.prototype.forOwn = function(object, callback) { if (!this.isObject(object)) { throw new TypeError('Expected "object", but received "' + typeof object + '".'); } else { for (var property in object) { if (object.hasOwnProperty(property)) { callback(property); } } } }; Tools.prototype.extend = function(target, source) { this.forOwn(source, function(property) { if (this.isObject(source[property])) { if (!target[property] || !this.isObject(target[property])) { target[property] = {}; } this.extend(target[property], source[property]); } else { target[property] = source[property]; } }.bind(this)); return target }; Tools.prototype.extendClone = function(target, source) { return this.extend(this.extend({}, target), source) }; Tools.prototype.isMobile = function() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) }; function Tools() {}; return Tools })(); _requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; }).call(this); return this.ScrollReveal; }));