diff options
Diffstat (limited to 'present/js/controllers/backgrounds.js')
| -rw-r--r-- | present/js/controllers/backgrounds.js | 406 | 
1 files changed, 406 insertions, 0 deletions
| diff --git a/present/js/controllers/backgrounds.js b/present/js/controllers/backgrounds.js new file mode 100644 index 0000000..f906269 --- /dev/null +++ b/present/js/controllers/backgrounds.js @@ -0,0 +1,406 @@ +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; + +		// 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 = data.backgroundColor; + +		// 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 ) { +					slide.classList.add( 'has-dark-background' ); +				} +				else { +					slide.classList.add( 'has-light-background' ); +				} +			} +		} + +	} + +	/** +	 * 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 currentSlide = this.Reveal.getCurrentSlide(); +		let indices = this.Reveal.getIndices(); + +		let currentBackground = null; + +		// Reverse past/future classes when in RTL mode +		let horizontalPast = this.Reveal.getConfig().rtl ? 'future' : 'past', +			horizontalFuture = this.Reveal.getConfig().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' ); + +					if( v < indices.v ) { +						backgroundv.classList.add( 'past' ); +					} +					else if ( v > indices.v ) { +						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; +					} + +				} ); +			} + +		} ); + +		// 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; +				} + +			} + +			// Don't transition between identical backgrounds. This +			// prevents unwanted flicker. +			let previousBackgroundHash = this.previousBackground ? this.previousBackground.getAttribute( 'data-background-hash' ) : null; +			let currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' ); +			if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== this.previousBackground ) { +				this.element.classList.add( 'no-transition' ); +			} + +			this.previousBackground = currentBackground; + +		} + +		// If there's a background brightness flag for this slide, +		// bubble it to the .reveal container +		if( currentSlide ) { +			[ 'has-light-background', 'has-dark-background' ].forEach( classToBubble => { +				if( currentSlide.classList.contains( classToBubble ) ) { +					this.Reveal.getRevealElement().classList.add( classToBubble ); +				} +				else { +					this.Reveal.getRevealElement().classList.remove( classToBubble ); +				} +			}, this ); +		} + +		// Allow the first background to apply without transition +		setTimeout( () => { +			this.element.classList.remove( 'no-transition' ); +		}, 1 ); + +	} + +	/** +	 * 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(); + +	} + +} | 
