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(); } }