///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// ///// /** * ScrollReveal * ------------ * Version : 3.3.4 * Website : scrollrevealjs.org * Repo : github.com/jlmakes/scrollreveal.js * Author : Julian Lloyd (@jlmakes) */ ;(function () { 'use strict' var sr var _requestAnimationFrame 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.version = '3.3.4' sr.tools = new Tools() // *required utilities if (sr.isSupported()) { sr.tools.extend(sr.defaults, config || {}) sr.defaults.container = _resolveContainer(sr.defaults) sr.store = { elements: {}, containers: [] } sr.sequences = {} sr.history = [] sr.uid = 0 sr.initialized = false } else if (typeof console !== 'undefined' && console !== null) { // Note: IE9 only supports console if devtools are open. console.log('ScrollReveal is not supported in this browser.') } return sr } /** * 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)', // `` is the default reveal container. You can pass either: // DOM Node, e.g. document.querySelector('.fooContainer') // Selector, e.g. '.fooContainer' container: window.document.documentElement, // true/false to control reveal animations on mobile. mobile: false, // 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: https://scrollrevealjs.org/assets/viewoffset.png viewOffset: { top: 0, right: 0, bottom: 0, left: 0 }, // Callbacks that fire for each triggered element reveal, and reset. beforeReveal: function (domEl) {}, beforeReset: function (domEl) {}, // Callbacks that fire for each completed element reveal, and reset. afterReveal: function (domEl) {}, afterReset: function (domEl) {} } /** * 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 {Node|NodeList|string} [target] The node, node list or selector to use for animation. * @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 (target, config, interval, sync) { var container var elements var elem var elemId var sequence var sequenceId // No custom configuration was passed, but a sequence interval instead. // let’s shuffle things around to make sure everything works. if (config !== undefined && typeof config === 'number') { interval = config config = {} } else if (config === undefined || config === null) { config = {} } container = _resolveContainer(config) elements = _getRevealElements(target, container) if (!elements.length) { console.log('ScrollReveal: reveal on "' + target + '" failed, no elements found.') return sr } // 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] } else { // Otherwise, let’s do some basic setup. 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’s time to update its configuration, styles, // and send the updates to our store. _configure(elem, config, container) _style(elem) _updateStore(elem) // We need to make sure elements are set to visibility: visible, 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 } else if (!elem.revealing) { // Otherwise, proceed normally. 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’t want to // record or intiialize each reveal during syncing. if (!sync && sr.isSupported()) { _record(target, config, interval) // 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) // https://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 targets. * @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.target, record.config, record.interval, true) } _init() } else { console.log('ScrollReveal: sync failed, no reveals found.') } return sr } /** * Private Methods * --------------- */ function _resolveContainer (config) { if (config && config.container) { if (typeof config.container === 'string') { return window.document.documentElement.querySelector(config.container) } else if (sr.tools.isNode(config.container)) { return config.container } else { console.log('ScrollReveal: invalid container "' + config.container + '" provided.') console.log('ScrollReveal: falling back to default container.') } } return sr.defaults.container } /** * check to see if a node or node list was passed in as the target, * otherwise query the container using target as a selector. * * @param {Node|NodeList|string} [target] client input for reveal target. * @param {Node} [container] parent element for selector queries. * * @return {array} elements to be revealed. */ function _getRevealElements (target, container) { if (typeof target === 'string') { return Array.prototype.slice.call(container.querySelectorAll(target)) } else if (sr.tools.isNode(target)) { return [target] } else if (sr.tools.isNodeList(target)) { return Array.prototype.slice.call(target) } return [] } /** * A consistent way of creating unique IDs. * @returns {number} */ function _nextUid () { return ++sr.uid } function _configure (elem, config, container) { // If a container was passed as a part of the config object, // let’s overwrite it with the resolved container passed in. if (config.container) config.container = container // If the element hasn’t already been configured, let’s 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) } else { // Otherwise, let’s use a clone of the existing element configuration extended // by the configuration passed as the second argument. 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' } } 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: // https://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 cssDistance var transform = elem.styles.transform // Let’s make sure our our pixel distances are negative for top and left. // e.g. origin = 'top' and distance = '25px' starts at `top: -25px` in CSS. if (config.origin === 'top' || config.origin === 'left') { cssDistance = /^-/.test(config.distance) ? config.distance.substr(1) : '-' + config.distance } else { cssDistance = config.distance } if (parseInt(config.distance)) { transform.initial += ' translate' + config.axis + '(' + cssDistance + ')' 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’s container isn’t already in the store, let’s 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 (target, 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 = { target: target, 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’s 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 var elem var elemId var sequence // Loop through all sequences sr.tools.forOwn(sr.sequences, function (sequenceId) { sequence = sr.sequences[sequenceId] active = false // For each sequenced elemenet, let’s check visibility and if // any are visible, set it’s 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 var 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’s see if we should revealand if so, // trigger the `beforeReveal` callback and // determine whether or not to use delay. if (_shouldReveal(elem)) { elem.config.beforeReveal(elem.domEl) 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’s queue the `afterReveal` callback // and mark the element as seen and revealing. _queueCallback('reveal', elem, delayed) elem.revealing = true elem.seen = true if (elem.sequence) { _queueNextInSequence(elem, delayed) } } else if (_shouldReset(elem)) { //Otherwise reset our element and // trigger the `beforeReset` callback. elem.config.beforeReset(elem.domEl) elem.domEl.setAttribute('style', elem.styles.inline + elem.styles.transform.initial + elem.styles.transition.instant ) // And queue the `afterReset` callback. _queueCallback('reset', elem) elem.revealing = false } }) } function _queueNextInSequence (elem, delayed) { var elapsed = 0 var delay = 0 var sequence = sr.sequences[elem.sequence.id] // We’re processing a sequenced element, so let's block other elements in this sequence. sequence.blocked = true // Since we’re 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 var duration = 0 var callback = 'after' // Check which callback we’re 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’s 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 } } else { // Otherwise, default to the window object’s scroll values. return { x: window.pageXOffset, y: window.pageYOffset } } } function _getOffset (domEl) { var offsetTop = 0 var offsetLeft = 0 // Grab the element’s dimensions. var offsetHeight = domEl.offsetHeight var 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’s top and left origin. do { if (!isNaN(domEl.offsetTop)) { offsetTop += domEl.offsetTop } if (!isNaN(domEl.offsetLeft)) { offsetLeft += domEl.offsetLeft } domEl = domEl.offsetParent } while (domEl) return { top: offsetTop, left: offsetLeft, height: offsetHeight, width: offsetWidth } } function _isElemVisible (elem) { var offset = _getOffset(elem.domEl) var container = _getContainer(elem.config.container) var scrolled = _getScrolled(elem.config.container) var vF = elem.config.viewFactor // Define the element geometry. var elemHeight = offset.height var elemWidth = offset.width var elemTop = offset.top var elemLeft = offset.left var elemBottom = elemTop + elemHeight var elemRight = elemLeft + elemWidth return confirmBounds() || isPositionFixed() function confirmBounds () { // Define the element’s functional boundaries using its view factor. var top = elemTop + elemHeight * vF var left = elemLeft + elemWidth * vF var bottom = elemBottom - elemHeight * vF var right = elemRight - elemWidth * vF // Define the container functional boundaries using its view offset. var viewTop = scrolled.y + elem.config.viewOffset.top var viewLeft = scrolled.x + elem.config.viewOffset.left var viewBottom = scrolled.y - elem.config.viewOffset.bottom + container.height var 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') } } /** * Utilities * --------- */ function Tools () {} Tools.prototype.isObject = function (object) { return object !== null && typeof object === 'object' && object.constructor === Object } Tools.prototype.isNode = function (object) { return typeof window.Node === 'object' ? object instanceof window.Node : object && typeof object === 'object' && typeof object.nodeType === 'number' && typeof object.nodeName === 'string' } Tools.prototype.isNodeList = function (object) { var prototypeToString = Object.prototype.toString.call(object) var regex = /^\[object (HTMLCollection|NodeList|Object)\]$/ return typeof window.NodeList === 'object' ? object instanceof window.NodeList : object && typeof object === 'object' && regex.test(prototypeToString) && typeof object.length === 'number' && (object.length === 0 || this.isNode(object[0])) } 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) } /** * Polyfills * -------- */ _requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60) } /** * Module Wrapper * -------------- */ if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { define(function () { return ScrollReveal }) } else if (typeof module !== 'undefined' && module.exports) { module.exports = ScrollReveal } else { window.ScrollReveal = ScrollReveal } })();