| import { queryAll } from '../utils/util.js' |
| import { colorToRgb, colorBrightness } from '../utils/color.js' |
| |
| /** |
| * Creates and updates slide backgrounds. |
| */ |
| export default class Backgrounds { |
| |
| constructor( Reveal ) { |
| |
| this.Reveal = Reveal; |
| |
| } |
| |
| render() { |
| |
| this.element = document.createElement( 'div' ); |
| this.element.className = 'backgrounds'; |
| this.Reveal.getRevealElement().appendChild( this.element ); |
| |
| } |
| |
| /** |
| * Creates the slide background elements and appends them |
| * to the background container. One element is created per |
| * slide no matter if the given slide has visible background. |
| */ |
| create() { |
| |
| // Clear prior backgrounds |
| this.element.innerHTML = ''; |
| this.element.classList.add( 'no-transition' ); |
| |
| // Iterate over all horizontal slides |
| this.Reveal.getHorizontalSlides().forEach( slideh => { |
| |
| let backgroundStack = this.createBackground( slideh, this.element ); |
| |
| // Iterate over all vertical slides |
| queryAll( slideh, 'section' ).forEach( slidev => { |
| |
| this.createBackground( slidev, backgroundStack ); |
| |
| backgroundStack.classList.add( 'stack' ); |
| |
| } ); |
| |
| } ); |
| |
| // Add parallax background if specified |
| if( this.Reveal.getConfig().parallaxBackgroundImage ) { |
| |
| this.element.style.backgroundImage = 'url("' + this.Reveal.getConfig().parallaxBackgroundImage + '")'; |
| this.element.style.backgroundSize = this.Reveal.getConfig().parallaxBackgroundSize; |
| this.element.style.backgroundRepeat = this.Reveal.getConfig().parallaxBackgroundRepeat; |
| this.element.style.backgroundPosition = this.Reveal.getConfig().parallaxBackgroundPosition; |
| |
| // Make sure the below properties are set on the element - these properties are |
| // needed for proper transitions to be set on the element via CSS. To remove |
| // annoying background slide-in effect when the presentation starts, apply |
| // these properties after short time delay |
| setTimeout( () => { |
| this.Reveal.getRevealElement().classList.add( 'has-parallax-background' ); |
| }, 1 ); |
| |
| } |
| else { |
| |
| this.element.style.backgroundImage = ''; |
| this.Reveal.getRevealElement().classList.remove( 'has-parallax-background' ); |
| |
| } |
| |
| } |
| |
| /** |
| * Creates a background for the given slide. |
| * |
| * @param {HTMLElement} slide |
| * @param {HTMLElement} container The element that the background |
| * should be appended to |
| * @return {HTMLElement} New background div |
| */ |
| createBackground( slide, container ) { |
| |
| // Main slide background element |
| let element = document.createElement( 'div' ); |
| element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' ); |
| |
| // Inner background element that wraps images/videos/iframes |
| let contentElement = document.createElement( 'div' ); |
| contentElement.className = 'slide-background-content'; |
| |
| element.appendChild( contentElement ); |
| container.appendChild( element ); |
| |
| slide.slideBackgroundElement = element; |
| slide.slideBackgroundContentElement = contentElement; |
| |
| // Syncs the background to reflect all current background settings |
| this.sync( slide ); |
| |
| return element; |
| |
| } |
| |
| /** |
| * Renders all of the visual properties of a slide background |
| * based on the various background attributes. |
| * |
| * @param {HTMLElement} slide |
| */ |
| sync( slide ) { |
| |
| const element = slide.slideBackgroundElement, |
| contentElement = slide.slideBackgroundContentElement; |
| |
| const data = { |
| background: slide.getAttribute( 'data-background' ), |
| backgroundSize: slide.getAttribute( 'data-background-size' ), |
| backgroundImage: slide.getAttribute( 'data-background-image' ), |
| backgroundVideo: slide.getAttribute( 'data-background-video' ), |
| backgroundIframe: slide.getAttribute( 'data-background-iframe' ), |
| backgroundColor: slide.getAttribute( 'data-background-color' ), |
| backgroundGradient: slide.getAttribute( 'data-background-gradient' ), |
| backgroundRepeat: slide.getAttribute( 'data-background-repeat' ), |
| backgroundPosition: slide.getAttribute( 'data-background-position' ), |
| backgroundTransition: slide.getAttribute( 'data-background-transition' ), |
| backgroundOpacity: slide.getAttribute( 'data-background-opacity' ), |
| }; |
| |
| const dataPreload = slide.hasAttribute( 'data-preload' ); |
| |
| // Reset the prior background state in case this is not the |
| // initial sync |
| slide.classList.remove( 'has-dark-background' ); |
| slide.classList.remove( 'has-light-background' ); |
| |
| element.removeAttribute( 'data-loaded' ); |
| element.removeAttribute( 'data-background-hash' ); |
| element.removeAttribute( 'data-background-size' ); |
| element.removeAttribute( 'data-background-transition' ); |
| element.style.backgroundColor = ''; |
| |
| contentElement.style.backgroundSize = ''; |
| contentElement.style.backgroundRepeat = ''; |
| contentElement.style.backgroundPosition = ''; |
| contentElement.style.backgroundImage = ''; |
| contentElement.style.opacity = ''; |
| contentElement.innerHTML = ''; |
| |
| if( data.background ) { |
| // Auto-wrap image urls in url(...) |
| if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp|webp)([?#\s]|$)/gi.test( data.background ) ) { |
| slide.setAttribute( 'data-background-image', data.background ); |
| } |
| else { |
| element.style.background = data.background; |
| } |
| } |
| |
| // Create a hash for this combination of background settings. |
| // This is used to determine when two slide backgrounds are |
| // the same. |
| if( data.background || data.backgroundColor || data.backgroundGradient || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) { |
| element.setAttribute( 'data-background-hash', data.background + |
| data.backgroundSize + |
| data.backgroundImage + |
| data.backgroundVideo + |
| data.backgroundIframe + |
| data.backgroundColor + |
| data.backgroundGradient + |
| data.backgroundRepeat + |
| data.backgroundPosition + |
| data.backgroundTransition + |
| data.backgroundOpacity ); |
| } |
| |
| // Additional and optional background properties |
| if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize ); |
| if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor; |
| if( data.backgroundGradient ) element.style.backgroundImage = data.backgroundGradient; |
| if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition ); |
| |
| if( dataPreload ) element.setAttribute( 'data-preload', '' ); |
| |
| // Background image options are set on the content wrapper |
| if( data.backgroundSize ) contentElement.style.backgroundSize = data.backgroundSize; |
| if( data.backgroundRepeat ) contentElement.style.backgroundRepeat = data.backgroundRepeat; |
| if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition; |
| if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity; |
| |
| const contrastClass = this.getContrastClass( slide ); |
| |
| if( typeof contrastClass === 'string' ) { |
| slide.classList.add( contrastClass ); |
| } |
| |
| } |
| |
| /** |
| * Returns a class name that can be applied to a slide to indicate |
| * if it has a light or dark background. |
| * |
| * @param {*} slide |
| * |
| * @returns {string|null} |
| */ |
| getContrastClass( slide ) { |
| |
| const element = slide.slideBackgroundElement; |
| |
| // If this slide has a background color, we add a class that |
| // signals if it is light or dark. If the slide has no background |
| // color, no class will be added |
| let contrastColor = slide.getAttribute( 'data-background-color' ); |
| |
| // If no bg color was found, or it cannot be converted by colorToRgb, check the computed background |
| if( !contrastColor || !colorToRgb( contrastColor ) ) { |
| let computedBackgroundStyle = window.getComputedStyle( element ); |
| if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) { |
| contrastColor = computedBackgroundStyle.backgroundColor; |
| } |
| } |
| |
| if( contrastColor ) { |
| const rgb = colorToRgb( contrastColor ); |
| |
| // Ignore fully transparent backgrounds. Some browsers return |
| // rgba(0,0,0,0) when reading the computed background color of |
| // an element with no background |
| if( rgb && rgb.a !== 0 ) { |
| if( colorBrightness( contrastColor ) < 128 ) { |
| return 'has-dark-background'; |
| } |
| else { |
| return 'has-light-background'; |
| } |
| } |
| } |
| |
| return null; |
| |
| } |
| |
| /** |
| * Bubble the 'has-light-background'/'has-dark-background' classes. |
| */ |
| bubbleSlideContrastClassToElement( slide, target ) { |
| |
| [ 'has-light-background', 'has-dark-background' ].forEach( classToBubble => { |
| if( slide.classList.contains( classToBubble ) ) { |
| target.classList.add( classToBubble ); |
| } |
| else { |
| target.classList.remove( classToBubble ); |
| } |
| }, this ); |
| |
| } |
| |
| /** |
| * Updates the background elements to reflect the current |
| * slide. |
| * |
| * @param {boolean} includeAll If true, the backgrounds of |
| * all vertical slides (not just the present) will be updated. |
| */ |
| update( includeAll = false ) { |
| |
| let config = this.Reveal.getConfig(); |
| let currentSlide = this.Reveal.getCurrentSlide(); |
| let indices = this.Reveal.getIndices(); |
| |
| let currentBackground = null; |
| |
| // Reverse past/future classes when in RTL mode |
| let horizontalPast = config.rtl ? 'future' : 'past', |
| horizontalFuture = config.rtl ? 'past' : 'future'; |
| |
| // Update the classes of all backgrounds to match the |
| // states of their slides (past/present/future) |
| Array.from( this.element.childNodes ).forEach( ( backgroundh, h ) => { |
| |
| backgroundh.classList.remove( 'past', 'present', 'future' ); |
| |
| if( h < indices.h ) { |
| backgroundh.classList.add( horizontalPast ); |
| } |
| else if ( h > indices.h ) { |
| backgroundh.classList.add( horizontalFuture ); |
| } |
| else { |
| backgroundh.classList.add( 'present' ); |
| |
| // Store a reference to the current background element |
| currentBackground = backgroundh; |
| } |
| |
| if( includeAll || h === indices.h ) { |
| queryAll( backgroundh, '.slide-background' ).forEach( ( backgroundv, v ) => { |
| |
| backgroundv.classList.remove( 'past', 'present', 'future' ); |
| |
| const indexv = typeof indices.v === 'number' ? indices.v : 0; |
| |
| if( v < indexv ) { |
| backgroundv.classList.add( 'past' ); |
| } |
| else if ( v > indexv ) { |
| backgroundv.classList.add( 'future' ); |
| } |
| else { |
| backgroundv.classList.add( 'present' ); |
| |
| // Only if this is the present horizontal and vertical slide |
| if( h === indices.h ) currentBackground = backgroundv; |
| } |
| |
| } ); |
| } |
| |
| } ); |
| |
| // The previous background may refer to a DOM element that has |
| // been removed after a presentation is synced & bgs are recreated |
| if( this.previousBackground && !this.previousBackground.closest( 'body' ) ) { |
| this.previousBackground = null; |
| } |
| |
| if( currentBackground && this.previousBackground ) { |
| |
| // Don't transition between identical backgrounds. This |
| // prevents unwanted flicker. |
| let previousBackgroundHash = this.previousBackground.getAttribute( 'data-background-hash' ); |
| let currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' ); |
| |
| if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== this.previousBackground ) { |
| this.element.classList.add( 'no-transition' ); |
| |
| // If multiple slides have the same background video, carry |
| // the <video> element forward so that it plays continuously |
| // across multiple slides |
| const currentVideo = currentBackground.querySelector( 'video' ); |
| const previousVideo = this.previousBackground.querySelector( 'video' ); |
| |
| if( currentVideo && previousVideo ) { |
| |
| const currentVideoParent = currentVideo.parentNode; |
| const previousVideoParent = previousVideo.parentNode; |
| |
| // Swap the two videos |
| previousVideoParent.appendChild( currentVideo ); |
| currentVideoParent.appendChild( previousVideo ); |
| |
| } |
| } |
| |
| } |
| |
| // Stop content inside of previous backgrounds |
| if( this.previousBackground ) { |
| |
| this.Reveal.slideContent.stopEmbeddedContent( this.previousBackground, { unloadIframes: !this.Reveal.slideContent.shouldPreload( this.previousBackground ) } ); |
| |
| } |
| |
| // Start content in the current background |
| if( currentBackground ) { |
| |
| this.Reveal.slideContent.startEmbeddedContent( currentBackground ); |
| |
| let currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' ); |
| if( currentBackgroundContent ) { |
| |
| let backgroundImageURL = currentBackgroundContent.style.backgroundImage || ''; |
| |
| // Restart GIFs (doesn't work in Firefox) |
| if( /\.gif/i.test( backgroundImageURL ) ) { |
| currentBackgroundContent.style.backgroundImage = ''; |
| window.getComputedStyle( currentBackgroundContent ).opacity; |
| currentBackgroundContent.style.backgroundImage = backgroundImageURL; |
| } |
| |
| } |
| |
| this.previousBackground = currentBackground; |
| |
| } |
| |
| // If there's a background brightness flag for this slide, |
| // bubble it to the .reveal container |
| if( currentSlide ) { |
| this.bubbleSlideContrastClassToElement( currentSlide, this.Reveal.getRevealElement() ); |
| } |
| |
| // Allow the first background to apply without transition |
| setTimeout( () => { |
| this.element.classList.remove( 'no-transition' ); |
| }, 10 ); |
| |
| } |
| |
| /** |
| * Updates the position of the parallax background based |
| * on the current slide index. |
| */ |
| updateParallax() { |
| |
| let indices = this.Reveal.getIndices(); |
| |
| if( this.Reveal.getConfig().parallaxBackgroundImage ) { |
| |
| let horizontalSlides = this.Reveal.getHorizontalSlides(), |
| verticalSlides = this.Reveal.getVerticalSlides(); |
| |
| let backgroundSize = this.element.style.backgroundSize.split( ' ' ), |
| backgroundWidth, backgroundHeight; |
| |
| if( backgroundSize.length === 1 ) { |
| backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 ); |
| } |
| else { |
| backgroundWidth = parseInt( backgroundSize[0], 10 ); |
| backgroundHeight = parseInt( backgroundSize[1], 10 ); |
| } |
| |
| let slideWidth = this.element.offsetWidth, |
| horizontalSlideCount = horizontalSlides.length, |
| horizontalOffsetMultiplier, |
| horizontalOffset; |
| |
| if( typeof this.Reveal.getConfig().parallaxBackgroundHorizontal === 'number' ) { |
| horizontalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundHorizontal; |
| } |
| else { |
| horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0; |
| } |
| |
| horizontalOffset = horizontalOffsetMultiplier * indices.h * -1; |
| |
| let slideHeight = this.element.offsetHeight, |
| verticalSlideCount = verticalSlides.length, |
| verticalOffsetMultiplier, |
| verticalOffset; |
| |
| if( typeof this.Reveal.getConfig().parallaxBackgroundVertical === 'number' ) { |
| verticalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundVertical; |
| } |
| else { |
| verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ); |
| } |
| |
| verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indices.v : 0; |
| |
| this.element.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px'; |
| |
| } |
| |
| } |
| |
| destroy() { |
| |
| this.element.remove(); |
| |
| } |
| |
| } |