/* * Backstretch * http://srobbin.com/jquery-plugins/backstretch/ * * Copyright (c) 2013 Scott Robbin * Licensed under the MIT license. */ ; ( function ( $, window, undefined ) { 'use strict'; /** @const */ var YOUTUBE_REGEXP = /^.*(youtu\.be\/|youtube\.com\/v\/|youtube\.com\/embed\/|youtube\.com\/watch\?v=|youtube\.com\/watch\?.*\&v=)([^#\&\?]*).*/i; /* PLUGIN DEFINITION * ========================= */ $.fn.backstretch = function ( images, options ) { var args = arguments; /* * Scroll the page one pixel to get the right window height on iOS * Pretty harmless for everyone else */ if ( $( window ).scrollTop() === 0 ) { window.scrollTo( 0, 0 ); } var returnValues; this.each( function ( eachIndex ) { var $this = $( this ) , obj = $this.data( 'backstretch' ); // Do we already have an instance attached to this element? if ( obj ) { // Is this a method they're trying to execute? if ( typeof args[ 0 ] === 'string' && typeof obj[ args[ 0 ] ] === 'function' ) { // Call the method var returnValue = obj[ args[ 0 ] ].apply( obj, Array.prototype.slice.call( args, 1 ) ); if ( returnValue === obj ) { // If a method is chaining returnValue = undefined; } if ( returnValue !== undefined ) { returnValues = returnValues || []; returnValues[ eachIndex ] = returnValue; } return; // Nothing further to do } // Merge the old options with the new options = $.extend( obj.options, options ); // Remove the old instance if ( typeof obj === 'object' && 'destroy' in obj ) { obj.destroy( true ); } } // We need at least one image if ( !images || ( images && images.length === 0 ) ) { var cssBackgroundImage = $this.css( 'background-image' ); if ( cssBackgroundImage && cssBackgroundImage !== 'none' ) { images = [ { url: $this.css( 'backgroundImage' ).replace( /url\(|\)|"|'/g, "" ) } ]; } else { $.error( 'No images were supplied for Backstretch, or element must have a CSS-defined background image.' ); } } obj = new Backstretch( this, images, options || {} ); $this.data( 'backstretch', obj ); } ); return returnValues ? returnValues.length === 1 ? returnValues[ 0 ] : returnValues : this; }; // If no element is supplied, we'll attach to body $.backstretch = function ( images, options ) { // Return the instance return $( 'body' ) .backstretch( images, options ) .data( 'backstretch' ); }; // Custom selector $.expr[ ':' ].backstretch = function ( elem ) { return $( elem ).data( 'backstretch' ) !== undefined; }; /* DEFAULTS * ========================= */ $.fn.backstretch.defaults = { duration: 5000 // Amount of time in between slides (if slideshow) , transition: 'fade' // Type of transition between slides , transitionDuration: 0 // Duration of transition between slides , animateFirst: true // Animate the transition of first image of slideshow in? , alignX: 0.5 // The x-alignment for the image, can be 'left'|'center'|'right' or any number between 0.0 and 1.0 , alignY: 0.5 // The y-alignment for the image, can be 'top'|'center'|'bottom' or any number between 0.0 and 1.0 , paused: false // Whether the images should slide after given duration , start: 0 // Index of the first image to show , preload: 2 // How many images preload at a time? , preloadSize: 1 // How many images can we preload in parallel? , resolutionRefreshRate: 2500 // How long to wait before switching resolution? , resolutionChangeRatioThreshold: 0.1 // How much a change should it be before switching resolution? }; /* STYLES * * Baked-in styles that we'll apply to our elements. * In an effort to keep the plugin simple, these are not exposed as options. * That said, anyone can override these in their own stylesheet. * ========================= */ var styles = { wrap: { left: 0 , top: 0 , overflow: 'hidden' , margin: 0 , padding: 0 , height: '100%' , width: '100%' , zIndex: -999999 } , itemWrapper: { position: 'absolute' , display: 'none' , margin: 0 , padding: 0 , border: 'none' , width: '100%' , height: '100%' , zIndex: -999999 } , item: { position: 'absolute' , margin: 0 , padding: 0 , border: 'none' , width: '100%' , height: '100%' , maxWidth: 'none' } }; /* Given an array of different options for an image, * choose the optimal image for the container size. * * Given an image template (a string with {{ width }} and/or * {{height}} inside) and a container object, returns the * image url with the exact values for the size of that * container. * * Returns an array of urls optimized for the specified resolution. * */ var optimalSizeImages = ( function () { /* Sorts the array of image sizes based on width */ var widthInsertSort = function ( arr ) { for ( var i = 1; i < arr.length; i++ ) { var tmp = arr[ i ], j = i; while ( arr[ j - 1 ] && parseInt( arr[ j - 1 ].width, 10 ) > parseInt( tmp.width, 10 ) ) { arr[ j ] = arr[ j - 1 ]; --j; } arr[ j ] = tmp; } return arr; }; /* Given an array of various sizes of the same image and a container width, * return the best image. */ var selectBest = function ( containerWidth, containerHeight, imageSizes ) { var devicePixelRatio = window.devicePixelRatio || 1; var deviceOrientation = getDeviceOrientation(); var windowOrientation = getWindowOrientation(); var wrapperOrientation = ( containerHeight > containerWidth ) ? 'portrait' : ( containerWidth > containerHeight ? 'landscape' : 'square' ); var lastAllowedImage = 0; var testWidth; for ( var j = 0, image; j < imageSizes.length; j++ ) { image = imageSizes[ j ]; // In case a new image was pushed in, process it: if ( typeof image === 'string' ) { image = imageSizes[ j ] = { url: image }; } if ( image.pixelRatio && image.pixelRatio !== 'auto' && parseFloat( image.pixelRatio ) !== devicePixelRatio ) { // We disallowed choosing this image for current device pixel ratio, // So skip this one. continue; } if ( image.deviceOrientation && image.deviceOrientation !== deviceOrientation ) { // We disallowed choosing this image for current device orientation, // So skip this one. continue; } if ( image.windowOrientation && image.windowOrientation !== deviceOrientation ) { // We disallowed choosing this image for current window orientation, // So skip this one. continue; } if ( image.orientation && image.orientation !== wrapperOrientation ) { // We disallowed choosing this image for current element's orientation, // So skip this one. continue; } // Mark this one as the last one we investigated // which does not violate device pixel ratio rules. // We may choose this one later if there's no match. lastAllowedImage = j; // For most images, we match the specified width against element width, // And enforcing a limit depending on the "pixelRatio" property if specified. // But if a pixelRatio="auto", then we consider the width as the physical width of the image, // And match it while considering the device's pixel ratio. testWidth = containerWidth; if ( image.pixelRatio === 'auto' ) { containerWidth *= devicePixelRatio; } // Stop when the width of the image is larger or equal to the container width if ( image.width >= testWidth ) { break; } } // Use the image located at where we stopped return imageSizes[ Math.min( j, lastAllowedImage ) ]; }; var replaceTagsInUrl = function ( url, templateReplacer ) { if ( typeof url === 'string' ) { url = url.replace( /{{(width|height)}}/g, templateReplacer ); } else if ( url instanceof Array ) { for ( var i = 0; i < url.length; i++ ) { if ( url[ i ].src ) { url[ i ].src = replaceTagsInUrl( url[ i ].src, templateReplacer ); } else { url[ i ] = replaceTagsInUrl( url[ i ], templateReplacer ); } } } return url; }; return function ( $container, images ) { var containerWidth = $container.width(), containerHeight = $container.height(); var chosenImages = []; var templateReplacer = function ( match, key ) { if ( key === 'width' ) { return containerWidth; } if ( key === 'height' ) { return containerHeight; } return match; }; for ( var i = 0; i < images.length; i++ ) { if ( $.isArray( images[ i ] ) ) { images[ i ] = widthInsertSort( images[ i ] ); var chosen = selectBest( containerWidth, containerHeight, images[ i ] ); chosenImages.push( chosen ); } else { // In case a new image was pushed in, process it: if ( typeof images[ i ] === 'string' ) { images[ i ] = { url: images[ i ] }; } var item = $.extend( {}, images[ i ] ); item.url = replaceTagsInUrl( item.url, templateReplacer ); chosenImages.push( item ); } } return chosenImages; }; } )(); var isVideoSource = function ( source ) { return YOUTUBE_REGEXP.test( source.url ) || source.isVideo; }; /* Preload images */ var preload = ( function ( sources, startAt, count, batchSize, callback ) { // Plugin cache var cache = []; // Wrapper for cache var caching = function ( image ) { for ( var i = 0; i < cache.length; i++ ) { if ( cache[ i ].src === image.src ) { return cache[ i ]; } } cache.push( image ); return image; }; // Execute callback var exec = function ( sources, callback, last ) { if ( typeof callback === 'function' ) { callback.call( sources, last ); } }; // Closure to hide cache return function preload ( sources, startAt, count, batchSize, callback ) { // Check input data if ( typeof sources === 'undefined' ) { return; } if ( !$.isArray( sources ) ) { sources = [ sources ]; } if ( arguments.length < 5 && typeof arguments[ arguments.length - 1 ] === 'function' ) { callback = arguments[ arguments.length - 1 ]; } startAt = ( typeof startAt === 'function' || !startAt ) ? 0 : startAt; count = ( typeof count === 'function' || !count || count < 0 ) ? sources.length : Math.min( count, sources.length ); batchSize = ( typeof batchSize === 'function' || !batchSize ) ? 1 : batchSize; if ( startAt >= sources.length ) { startAt = 0; count = 0; } if ( batchSize < 0 ) { batchSize = count; } batchSize = Math.min( batchSize, count ); var next = sources.slice( startAt + batchSize, count - batchSize ); sources = sources.slice( startAt, batchSize ); count = sources.length; // If sources array is empty if ( !count ) { exec( sources, callback, true ); return; } // Image loading callback var countLoaded = 0; var loaded = function () { countLoaded++; if ( countLoaded !== count ) { return; } exec( sources, callback, !next ); preload( next, 0, 0, batchSize, callback ); }; // Loop sources to preload var image; for ( var i = 0; i < sources.length; i++ ) { if ( isVideoSource( sources[ i ] ) ) { // Do not preload videos. There are issues with that. // First - we need to keep an instance of the preloaded and use that exactly, not a copy. // Second - there are memory issues. // If there will be a requirement from users - I'll try to implement this. continue; } else { image = new Image(); image.src = sources[ i ].url; image = caching( image ); if ( image.complete ) { loaded(); } else { $( image ).on( 'load error', loaded ); } } } }; } )(); /* Process images array */ var processImagesArray = function ( images ) { var processed = []; for ( var i = 0; i < images.length; i++ ) { if ( typeof images[ i ] === 'string' ) { processed.push( { url: images[ i ] } ); } else if ( $.isArray( images[ i ] ) ) { processed.push( processImagesArray( images[ i ] ) ); } else { processed.push( processOptions( images[ i ] ) ); } } return processed; }; /* Process options */ var processOptions = function ( options, required ) { // Convert old options // centeredX/centeredY are deprecated if ( options.centeredX || options.centeredY ) { if ( window.console && window.console.log ) { window.console.log( 'jquery.backstretch: `centeredX`/`centeredY` is deprecated, please use `alignX`/`alignY`' ); } if ( options.centeredX ) { options.alignX = 0.5; } if ( options.centeredY ) { options.alignY = 0.5; } } // Deprecated spec if ( options.speed !== undefined ) { if ( window.console && window.console.log ) { window.console.log( 'jquery.backstretch: `speed` is deprecated, please use `transitionDuration`' ); } options.transitionDuration = options.speed; options.transition = 'fade'; } // Typo if ( options.resolutionChangeRatioTreshold !== undefined ) { window.console.log( 'jquery.backstretch: `treshold` is a typo!' ); options.resolutionChangeRatioThreshold = options.resolutionChangeRatioTreshold; } // Current spec that needs processing if ( options.fadeFirst !== undefined ) { options.animateFirst = options.fadeFirst; } if ( options.fade !== undefined ) { options.transitionDuration = options.fade; options.transition = 'fade'; } if ( options.scale ) { options.scale = validScale( options.scale ); } return processAlignOptions( options ); }; /* Process align options */ var processAlignOptions = function ( options, required ) { if ( options.alignX === 'left' ) { options.alignX = 0.0; } else if ( options.alignX === 'center' ) { options.alignX = 0.5; } else if ( options.alignX === 'right' ) { options.alignX = 1.0; } else { if ( options.alignX !== undefined || required ) { options.alignX = parseFloat( options.alignX ); if ( isNaN( options.alignX ) ) { options.alignX = 0.5; } } } if ( options.alignY === 'top' ) { options.alignY = 0.0; } else if ( options.alignY === 'center' ) { options.alignY = 0.5; } else if ( options.alignY === 'bottom' ) { options.alignY = 1.0; } else { if ( options.alignX !== undefined || required ) { options.alignY = parseFloat( options.alignY ); if ( isNaN( options.alignY ) ) { options.alignY = 0.5; } } } return options; }; var SUPPORTED_SCALE_OPTIONS = { 'cover': 'cover', 'fit': 'fit', 'fit-smaller': 'fit-smaller', 'fill': 'fill' }; function validScale ( scale ) { if ( !SUPPORTED_SCALE_OPTIONS.hasOwnProperty( scale ) ) { return 'cover'; } return scale; } /* CLASS DEFINITION * ========================= */ var Backstretch = function ( container, images, options ) { this.options = $.extend( {}, $.fn.backstretch.defaults, options || {} ); this.firstShow = true; // Process options processOptions( this.options, true ); /* In its simplest form, we allow Backstretch to be called on an image path. * e.g. $.backstretch('/path/to/image.jpg') * So, we need to turn this back into an array. */ this.images = processImagesArray( $.isArray( images ) ? images : [ images ] ); /** * Paused-Option */ if ( this.options.paused ) { this.paused = true; } /** * Start-Option (Index) */ if ( this.options.start >= this.images.length ) { this.options.start = this.images.length - 1; } if ( this.options.start < 0 ) { this.options.start = 0; } // Convenience reference to know if the container is body. this.isBody = container === document.body; /* We're keeping track of a few different elements * * Container: the element that Backstretch was called on. * Wrap: a DIV that we place the image into, so we can hide the overflow. * Root: Convenience reference to help calculate the correct height. */ var $window = $( window ); this.$container = $( container ); this.$root = this.isBody ? supportsFixedPosition ? $window : $( document ) : this.$container; this.originalImages = this.images; this.images = optimalSizeImages( this.options.alwaysTestWindowResolution ? $window : this.$root, this.originalImages ); /** * Pre-Loading. * This is the first image, so we will preload a minimum of 1 images. */ preload( this.images, this.options.start || 0, this.options.preload || 1 ); // Don't create a new wrap if one already exists (from a previous instance of Backstretch) var $existing = this.$container.children( ".backstretch" ).first(); this.$wrap = $existing.length ? $existing : $( '
' ) .css( this.options.bypassCss ? {} : styles.wrap ) .appendTo( this.$container ); if ( !this.options.bypassCss ) { // Non-body elements need some style adjustments if ( !this.isBody ) { // If the container is statically positioned, we need to make it relative, // and if no zIndex is defined, we should set it to zero. var position = this.$container.css( 'position' ) , zIndex = this.$container.css( 'zIndex' ); this.$container.css( { position: position === 'static' ? 'relative' : position , zIndex: zIndex === 'auto' ? 0 : zIndex } ); // Needs a higher z-index this.$wrap.css( { zIndex: -999998 } ); } // Fixed or absolute positioning? this.$wrap.css( { position: this.isBody && supportsFixedPosition ? 'fixed' : 'absolute' } ); } // Set the first image this.index = this.options.start; this.show( this.index ); // Listen for resize $window.on( 'resize.backstretch', $.proxy( this.resize, this ) ) .on( 'orientationchange.backstretch', $.proxy( function () { // Need to do this in order to get the right window height if ( this.isBody && window.pageYOffset === 0 ) { window.scrollTo( 0, 1 ); this.resize(); } }, this ) ); }; var performTransition = function ( options ) { var transition = options.transition || 'fade'; // Look for multiple options if ( typeof transition === 'string' && transition.indexOf( '|' ) > -1 ) { transition = transition.split( '|' ); } if ( transition instanceof Array ) { transition = transition[ Math.round( Math.random() * ( transition.length - 1 ) ) ]; } var $new = options[ 'new' ]; var $old = options[ 'old' ] ? options[ 'old' ] : $( [] ); switch ( transition.toString().toLowerCase() ) { default: case 'fade': $new.fadeIn( { duration: options.duration, complete: options.complete, easing: options.easing || undefined } ); break; case 'fadeinout': case 'fade_in_out': var fadeInNew = function () { $new.fadeIn( { duration: options.duration / 2, complete: options.complete, easing: options.easing || undefined } ); }; if ( $old.length ) { $old.fadeOut( { duration: options.duration / 2, complete: fadeInNew, easing: options.easing || undefined } ); } else { fadeInNew(); } break; case 'pushleft': case 'push_left': case 'pushright': case 'push_right': case 'pushup': case 'push_up': case 'pushdown': case 'push_down': case 'coverleft': case 'cover_left': case 'coverright': case 'cover_right': case 'coverup': case 'cover_up': case 'coverdown': case 'cover_down': var transitionParts = transition.match( /^(cover|push)_?(.*)$/ ); var animProp = transitionParts[ 2 ] === 'left' ? 'right' : transitionParts[ 2 ] === 'right' ? 'left' : transitionParts[ 2 ] === 'down' ? 'top' : transitionParts[ 2 ] === 'up' ? 'bottom' : 'right'; var newCssStart = { 'display': '' }, newCssAnim = {}; newCssStart[ animProp ] = '-100%'; newCssAnim[ animProp ] = 0; $new .css( newCssStart ) .animate( newCssAnim, { duration: options.duration, complete: function () { $new.css( animProp, '' ); options.complete.apply( this, arguments ); }, easing: options.easing || undefined } ); if ( transitionParts[ 1 ] === 'push' && $old.length ) { var oldCssAnim = {}; oldCssAnim[ animProp ] = '100%'; $old .animate( oldCssAnim, { duration: options.duration, complete: function () { $old.css( 'display', 'none' ); }, easing: options.easing || undefined } ); } break; } }; /* PUBLIC METHODS * ========================= */ Backstretch.prototype = { resize: function () { try { // Check for a better suited image after the resize var $resTest = this.options.alwaysTestWindowResolution ? $( window ) : this.$root; var newContainerWidth = $resTest.width(); var newContainerHeight = $resTest.height(); var changeRatioW = newContainerWidth / ( this._lastResizeContainerWidth || 0 ); var changeRatioH = newContainerHeight / ( this._lastResizeContainerHeight || 0 ); var resolutionChangeRatioThreshold = this.options.resolutionChangeRatioThreshold || 0.0; // check for big changes in container size if ( ( newContainerWidth !== this._lastResizeContainerWidth || newContainerHeight !== this._lastResizeContainerHeight ) && ( ( Math.abs( changeRatioW - 1 ) >= resolutionChangeRatioThreshold || isNaN( changeRatioW ) ) || ( Math.abs( changeRatioH - 1 ) >= resolutionChangeRatioThreshold || isNaN( changeRatioH ) ) ) ) { this._lastResizeContainerWidth = newContainerWidth; this._lastResizeContainerHeight = newContainerHeight; // Big change: rebuild the entire images array this.images = optimalSizeImages( $resTest, this.originalImages ); // Preload them (they will be automatically inserted on the next cycle) if ( this.options.preload ) { preload( this.images, ( this.index + 1 ) % this.images.length, this.options.preload ); } // In case there is no cycle and the new source is different than the current if ( this.images.length === 1 && this._currentImage.url !== this.images[ 0 ].url ) { // Wait a little an update the image being showed var that = this; clearTimeout( that._selectAnotherResolutionTimeout ); that._selectAnotherResolutionTimeout = setTimeout( function () { that.show( 0 ); }, this.options.resolutionRefreshRate ); } } var bgCSS = { left: 0, top: 0, right: 'auto', bottom: 'auto' } , boxWidth = this.isBody ? this.$root.width() : this.$root.innerWidth() , boxHeight = this.isBody ? ( window.innerHeight ? window.innerHeight : this.$root.height() ) : this.$root.innerHeight() , naturalWidth = this.$itemWrapper.data( 'width' ) , naturalHeight = this.$itemWrapper.data( 'height' ) , ratio = ( naturalWidth / naturalHeight ) || 1 , alignX = this._currentImage.alignX === undefined ? this.options.alignX : this._currentImage.alignX , alignY = this._currentImage.alignY === undefined ? this.options.alignY : this._currentImage.alignY , scale = validScale( this._currentImage.scale || this.options.scale ); var width, height; if ( scale === 'fit' || scale === 'fit-smaller' ) { width = naturalWidth; height = naturalHeight; if ( width > boxWidth || height > boxHeight || scale === 'fit-smaller' ) { var boxRatio = boxWidth / boxHeight; if ( boxRatio > ratio ) { width = Math.floor( boxHeight * ratio ); height = boxHeight; } else if ( boxRatio < ratio ) { width = boxWidth; height = Math.floor( boxWidth / ratio ); } else { width = boxWidth; height = boxHeight; } } } else if ( scale === 'fill' ) { width = boxWidth; height = boxHeight; } else { // 'cover' width = Math.max( boxHeight * ratio, boxWidth ); height = Math.max( width / ratio, boxHeight ); } // Make adjustments based on image ratio bgCSS.top = -( height - boxHeight ) * alignY; bgCSS.left = -( width - boxWidth ) * alignX; bgCSS.width = width; bgCSS.height = height; if ( !this.options.bypassCss ) { this.$wrap .css( { width: boxWidth, height: boxHeight } ) .find( '>.backstretch-item' ).not( '.deleteable' ) .each( function () { var $wrapper = $( this ); $wrapper.find( 'img,video,iframe' ) .css( bgCSS ); } ); } var evt = $.Event( 'backstretch.resize', { relatedTarget: this.$container[ 0 ] } ); this.$container.trigger( evt, this ); } catch ( err ) { // IE7 seems to trigger resize before the image is loaded. // This try/catch block is a hack to let it fail gracefully. } return this; } // Show the slide at a certain position , show: function ( newIndex, overrideOptions ) { // Validate index if ( Math.abs( newIndex ) > this.images.length - 1 ) { return; } // Vars var that = this , $oldItemWrapper = that.$wrap.find( '>.backstretch-item' ).addClass( 'deleteable' ) , oldVideoWrapper = that.videoWrapper , evtOptions = { relatedTarget: that.$container[ 0 ] }; // Trigger the "before" event that.$container.trigger( $.Event( 'backstretch.before', evtOptions ), [ that, newIndex ] ); // Set the new frame index this.index = newIndex; var selectedImage = that.images[ newIndex ]; // Pause the slideshow clearTimeout( that._cycleTimeout ); // New image delete that.videoWrapper; // Current item may not be a video var isVideo = isVideoSource( selectedImage ); if ( isVideo ) { that.videoWrapper = new VideoWrapper( selectedImage ); that.$item = that.videoWrapper.$video.css( 'pointer-events', 'none' ); } else { that.$item = $( '' ); } that.$itemWrapper = $( '
' ) .append( that.$item ); if ( this.options.bypassCss ) { that.$itemWrapper.css( { 'display': 'none' } ); } else { that.$itemWrapper.css( styles.itemWrapper ); that.$item.css( styles.item ); } that.$item.bind( isVideo ? 'canplay' : 'load', function ( e ) { var $this = $( this ) , $wrapper = $this.parent() , options = $wrapper.data( 'options' ); if ( overrideOptions ) { options = $.extend( {}, options, overrideOptions ); } var imgWidth = this.naturalWidth || this.videoWidth || this.width , imgHeight = this.naturalHeight || this.videoHeight || this.height; // Save the natural dimensions $wrapper .data( 'width', imgWidth ) .data( 'height', imgHeight ); var getOption = function ( opt ) { return options[ opt ] !== undefined ? options[ opt ] : that.options[ opt ]; }; var transition = getOption( 'transition' ); var transitionEasing = getOption( 'transitionEasing' ); var transitionDuration = getOption( 'transitionDuration' ); // Show the image, then delete the old one var bringInNextImage = function () { if ( oldVideoWrapper ) { oldVideoWrapper.stop(); oldVideoWrapper.destroy(); } $oldItemWrapper.remove(); // Resume the slideshow if ( !that.paused && that.images.length > 1 ) { that.cycle(); } // Now we can clear the background on the element, to spare memory if ( !that.options.bypassCss && !that.isBody ) { that.$container.css( 'background-image', 'none' ); } // Trigger the "after" and "show" events // "show" is being deprecated $( [ 'after', 'show' ] ).each( function () { that.$container.trigger( $.Event( 'backstretch.' + this, evtOptions ), [ that, newIndex ] ); } ); if ( isVideo ) { that.videoWrapper.play(); } }; if ( ( that.firstShow && !that.options.animateFirst ) || !transitionDuration || !transition ) { // Avoid transition on first show or if there's no transitionDuration value $wrapper.show(); bringInNextImage(); } else { performTransition( { 'new': $wrapper, old: $oldItemWrapper, transition: transition, duration: transitionDuration, easing: transitionEasing, complete: bringInNextImage } ); } that.firstShow = false; // Resize that.resize(); } ); that.$itemWrapper.appendTo( that.$wrap ); that.$item.attr( 'alt', selectedImage.alt || '' ); that.$itemWrapper.data( 'options', selectedImage ); if ( !isVideo ) { that.$item.attr( 'src', selectedImage.url ); } that._currentImage = selectedImage; return that; } , current: function () { return this.index; } , next: function () { var args = Array.prototype.slice.call( arguments, 0 ); args.unshift( this.index < this.images.length - 1 ? this.index + 1 : 0 ); return this.show.apply( this, args ); } , prev: function () { var args = Array.prototype.slice.call( arguments, 0 ); args.unshift( this.index === 0 ? this.images.length - 1 : this.index - 1 ); return this.show.apply( this, args ); } , pause: function () { // Pause the slideshow this.paused = true; if ( this.videoWrapper ) { this.videoWrapper.pause(); } return this; } , resume: function () { // Resume the slideshow this.paused = false; if ( this.videoWrapper ) { this.videoWrapper.play(); } this.cycle(); return this; } , cycle: function () { // Start/resume the slideshow if ( this.images.length > 1 ) { // Clear the timeout, just in case clearTimeout( this._cycleTimeout ); var duration = ( this._currentImage && this._currentImage.duration ) || this.options.duration; var isVideo = isVideoSource( this._currentImage ); var callNext = function () { this.$item.off( '.cycle' ); // Check for paused slideshow if ( !this.paused ) { this.next(); } }; // Special video handling if ( isVideo ) { // Leave video at last frame if ( !this._currentImage.loop ) { var lastFrameTimeout = 0; this.$item .on( 'playing.cycle', function () { var player = $( this ).data( 'player' ); clearTimeout( lastFrameTimeout ); lastFrameTimeout = setTimeout( function () { player.pause(); player.$video.trigger( 'ended' ); }, ( player.getDuration() - player.getCurrentTime() ) * 1000 ); } ) .on( 'ended.cycle', function () { clearTimeout( lastFrameTimeout ); } ); } // On error go to next this.$item.on( 'error.cycle initerror.cycle', $.proxy( callNext, this ) ); } if ( isVideo && !this._currentImage.duration ) { // It's a video - playing until end this.$item.on( 'ended.cycle', $.proxy( callNext, this ) ); } else { // Cycling according to specified duration this._cycleTimeout = setTimeout( $.proxy( callNext, this ), duration ); } } return this; } , destroy: function ( preserveBackground ) { // Stop the resize events $( window ).off( 'resize.backstretch orientationchange.backstretch' ); // Stop any videos if ( this.videoWrapper ) { this.videoWrapper.destroy(); } // Clear the timeout clearTimeout( this._cycleTimeout ); // Remove Backstretch if ( !preserveBackground ) { this.$wrap.remove(); } this.$container.removeData( 'backstretch' ); } }; /** * Video Abstraction Layer * * Static methods: * > VideoWrapper.loadYoutubeAPI() -> Call in order to load the Youtube API. * An 'youtube_api_load' event will be triggered on $(window) when the API is loaded. * * Generic: * > player.type -> type of the video * > player.video / player.$video -> contains the element holding the video * > player.play() -> plays the video * > player.pause() -> pauses the video * > player.setCurrentTime(position) -> seeks to a position by seconds * * Youtube: * > player.ytId will contain the youtube ID if the source is a youtube url * > player.ytReady is a flag telling whether the youtube source is ready for playback * */ var VideoWrapper = function () { this.init.apply( this, arguments ); }; /** * @param {Object} options * @param {String|Array|Array<{{src: String, type: String?}}>} options.url * @param {Boolean} options.loop=false * @param {Boolean?} options.mute=true * @param {String?} options.poster * loop, mute, poster */ VideoWrapper.prototype.init = function ( options ) { var that = this; var $video; var setVideoElement = function () { that.$video = $video; that.video = $video[ 0 ]; }; // Determine video type var videoType = 'video'; if ( !( options.url instanceof Array ) && YOUTUBE_REGEXP.test( options.url ) ) { videoType = 'youtube'; } that.type = videoType; if ( videoType === 'youtube' ) { // Try to load the API in the meantime VideoWrapper.loadYoutubeAPI(); that.ytId = options.url.match( YOUTUBE_REGEXP )[ 2 ]; var src = 'https://www.youtube.com/embed/' + that.ytId + '?rel=0&autoplay=0&showinfo=0&controls=0&modestbranding=1' + '&cc_load_policy=0&disablekb=1&iv_load_policy=3&loop=0' + '&enablejsapi=1&origin=' + encodeURIComponent( window.location.origin ); that.__ytStartMuted = !!options.mute || options.mute === undefined; $video = $( '