Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 1 | import { queryAll } from '../utils/util.js' |
| 2 | import { colorToRgb, colorBrightness } from '../utils/color.js' |
| 3 | |
| 4 | /** |
| 5 | * Creates and updates slide backgrounds. |
| 6 | */ |
| 7 | export default class Backgrounds { |
| 8 | |
| 9 | constructor( Reveal ) { |
| 10 | |
| 11 | this.Reveal = Reveal; |
| 12 | |
| 13 | } |
| 14 | |
| 15 | render() { |
| 16 | |
| 17 | this.element = document.createElement( 'div' ); |
| 18 | this.element.className = 'backgrounds'; |
| 19 | this.Reveal.getRevealElement().appendChild( this.element ); |
| 20 | |
| 21 | } |
| 22 | |
| 23 | /** |
| 24 | * Creates the slide background elements and appends them |
| 25 | * to the background container. One element is created per |
| 26 | * slide no matter if the given slide has visible background. |
| 27 | */ |
| 28 | create() { |
| 29 | |
| 30 | // Clear prior backgrounds |
| 31 | this.element.innerHTML = ''; |
| 32 | this.element.classList.add( 'no-transition' ); |
| 33 | |
| 34 | // Iterate over all horizontal slides |
| 35 | this.Reveal.getHorizontalSlides().forEach( slideh => { |
| 36 | |
| 37 | let backgroundStack = this.createBackground( slideh, this.element ); |
| 38 | |
| 39 | // Iterate over all vertical slides |
| 40 | queryAll( slideh, 'section' ).forEach( slidev => { |
| 41 | |
| 42 | this.createBackground( slidev, backgroundStack ); |
| 43 | |
| 44 | backgroundStack.classList.add( 'stack' ); |
| 45 | |
| 46 | } ); |
| 47 | |
| 48 | } ); |
| 49 | |
| 50 | // Add parallax background if specified |
| 51 | if( this.Reveal.getConfig().parallaxBackgroundImage ) { |
| 52 | |
| 53 | this.element.style.backgroundImage = 'url("' + this.Reveal.getConfig().parallaxBackgroundImage + '")'; |
| 54 | this.element.style.backgroundSize = this.Reveal.getConfig().parallaxBackgroundSize; |
| 55 | this.element.style.backgroundRepeat = this.Reveal.getConfig().parallaxBackgroundRepeat; |
| 56 | this.element.style.backgroundPosition = this.Reveal.getConfig().parallaxBackgroundPosition; |
| 57 | |
| 58 | // Make sure the below properties are set on the element - these properties are |
| 59 | // needed for proper transitions to be set on the element via CSS. To remove |
| 60 | // annoying background slide-in effect when the presentation starts, apply |
| 61 | // these properties after short time delay |
| 62 | setTimeout( () => { |
| 63 | this.Reveal.getRevealElement().classList.add( 'has-parallax-background' ); |
| 64 | }, 1 ); |
| 65 | |
| 66 | } |
| 67 | else { |
| 68 | |
| 69 | this.element.style.backgroundImage = ''; |
| 70 | this.Reveal.getRevealElement().classList.remove( 'has-parallax-background' ); |
| 71 | |
| 72 | } |
| 73 | |
| 74 | } |
| 75 | |
| 76 | /** |
| 77 | * Creates a background for the given slide. |
| 78 | * |
| 79 | * @param {HTMLElement} slide |
| 80 | * @param {HTMLElement} container The element that the background |
| 81 | * should be appended to |
| 82 | * @return {HTMLElement} New background div |
| 83 | */ |
| 84 | createBackground( slide, container ) { |
| 85 | |
| 86 | // Main slide background element |
| 87 | let element = document.createElement( 'div' ); |
| 88 | element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' ); |
| 89 | |
| 90 | // Inner background element that wraps images/videos/iframes |
| 91 | let contentElement = document.createElement( 'div' ); |
| 92 | contentElement.className = 'slide-background-content'; |
| 93 | |
| 94 | element.appendChild( contentElement ); |
| 95 | container.appendChild( element ); |
| 96 | |
| 97 | slide.slideBackgroundElement = element; |
| 98 | slide.slideBackgroundContentElement = contentElement; |
| 99 | |
| 100 | // Syncs the background to reflect all current background settings |
| 101 | this.sync( slide ); |
| 102 | |
| 103 | return element; |
| 104 | |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Renders all of the visual properties of a slide background |
| 109 | * based on the various background attributes. |
| 110 | * |
| 111 | * @param {HTMLElement} slide |
| 112 | */ |
| 113 | sync( slide ) { |
| 114 | |
| 115 | const element = slide.slideBackgroundElement, |
| 116 | contentElement = slide.slideBackgroundContentElement; |
| 117 | |
| 118 | const data = { |
| 119 | background: slide.getAttribute( 'data-background' ), |
| 120 | backgroundSize: slide.getAttribute( 'data-background-size' ), |
| 121 | backgroundImage: slide.getAttribute( 'data-background-image' ), |
| 122 | backgroundVideo: slide.getAttribute( 'data-background-video' ), |
| 123 | backgroundIframe: slide.getAttribute( 'data-background-iframe' ), |
| 124 | backgroundColor: slide.getAttribute( 'data-background-color' ), |
Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 125 | backgroundGradient: slide.getAttribute( 'data-background-gradient' ), |
Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 126 | backgroundRepeat: slide.getAttribute( 'data-background-repeat' ), |
| 127 | backgroundPosition: slide.getAttribute( 'data-background-position' ), |
| 128 | backgroundTransition: slide.getAttribute( 'data-background-transition' ), |
| 129 | backgroundOpacity: slide.getAttribute( 'data-background-opacity' ), |
| 130 | }; |
| 131 | |
| 132 | const dataPreload = slide.hasAttribute( 'data-preload' ); |
| 133 | |
| 134 | // Reset the prior background state in case this is not the |
| 135 | // initial sync |
| 136 | slide.classList.remove( 'has-dark-background' ); |
| 137 | slide.classList.remove( 'has-light-background' ); |
| 138 | |
| 139 | element.removeAttribute( 'data-loaded' ); |
| 140 | element.removeAttribute( 'data-background-hash' ); |
| 141 | element.removeAttribute( 'data-background-size' ); |
| 142 | element.removeAttribute( 'data-background-transition' ); |
| 143 | element.style.backgroundColor = ''; |
| 144 | |
| 145 | contentElement.style.backgroundSize = ''; |
| 146 | contentElement.style.backgroundRepeat = ''; |
| 147 | contentElement.style.backgroundPosition = ''; |
| 148 | contentElement.style.backgroundImage = ''; |
| 149 | contentElement.style.opacity = ''; |
| 150 | contentElement.innerHTML = ''; |
| 151 | |
| 152 | if( data.background ) { |
| 153 | // Auto-wrap image urls in url(...) |
Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 154 | if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp|webp)([?#\s]|$)/gi.test( data.background ) ) { |
Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 155 | slide.setAttribute( 'data-background-image', data.background ); |
| 156 | } |
| 157 | else { |
| 158 | element.style.background = data.background; |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | // Create a hash for this combination of background settings. |
| 163 | // This is used to determine when two slide backgrounds are |
| 164 | // the same. |
Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 165 | if( data.background || data.backgroundColor || data.backgroundGradient || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) { |
Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 166 | element.setAttribute( 'data-background-hash', data.background + |
| 167 | data.backgroundSize + |
| 168 | data.backgroundImage + |
| 169 | data.backgroundVideo + |
| 170 | data.backgroundIframe + |
| 171 | data.backgroundColor + |
Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 172 | data.backgroundGradient + |
Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 173 | data.backgroundRepeat + |
| 174 | data.backgroundPosition + |
| 175 | data.backgroundTransition + |
| 176 | data.backgroundOpacity ); |
| 177 | } |
| 178 | |
| 179 | // Additional and optional background properties |
| 180 | if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize ); |
| 181 | if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor; |
Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 182 | if( data.backgroundGradient ) element.style.backgroundImage = data.backgroundGradient; |
Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 183 | if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition ); |
| 184 | |
| 185 | if( dataPreload ) element.setAttribute( 'data-preload', '' ); |
| 186 | |
| 187 | // Background image options are set on the content wrapper |
| 188 | if( data.backgroundSize ) contentElement.style.backgroundSize = data.backgroundSize; |
| 189 | if( data.backgroundRepeat ) contentElement.style.backgroundRepeat = data.backgroundRepeat; |
| 190 | if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition; |
| 191 | if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity; |
| 192 | |
| 193 | // If this slide has a background color, we add a class that |
| 194 | // signals if it is light or dark. If the slide has no background |
| 195 | // color, no class will be added |
| 196 | let contrastColor = data.backgroundColor; |
| 197 | |
| 198 | // If no bg color was found, or it cannot be converted by colorToRgb, check the computed background |
| 199 | if( !contrastColor || !colorToRgb( contrastColor ) ) { |
| 200 | let computedBackgroundStyle = window.getComputedStyle( element ); |
| 201 | if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) { |
| 202 | contrastColor = computedBackgroundStyle.backgroundColor; |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | if( contrastColor ) { |
| 207 | const rgb = colorToRgb( contrastColor ); |
| 208 | |
| 209 | // Ignore fully transparent backgrounds. Some browsers return |
| 210 | // rgba(0,0,0,0) when reading the computed background color of |
| 211 | // an element with no background |
| 212 | if( rgb && rgb.a !== 0 ) { |
| 213 | if( colorBrightness( contrastColor ) < 128 ) { |
| 214 | slide.classList.add( 'has-dark-background' ); |
| 215 | } |
| 216 | else { |
| 217 | slide.classList.add( 'has-light-background' ); |
| 218 | } |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * Updates the background elements to reflect the current |
| 226 | * slide. |
| 227 | * |
| 228 | * @param {boolean} includeAll If true, the backgrounds of |
| 229 | * all vertical slides (not just the present) will be updated. |
| 230 | */ |
| 231 | update( includeAll = false ) { |
| 232 | |
| 233 | let currentSlide = this.Reveal.getCurrentSlide(); |
| 234 | let indices = this.Reveal.getIndices(); |
| 235 | |
| 236 | let currentBackground = null; |
| 237 | |
| 238 | // Reverse past/future classes when in RTL mode |
| 239 | let horizontalPast = this.Reveal.getConfig().rtl ? 'future' : 'past', |
| 240 | horizontalFuture = this.Reveal.getConfig().rtl ? 'past' : 'future'; |
| 241 | |
| 242 | // Update the classes of all backgrounds to match the |
| 243 | // states of their slides (past/present/future) |
| 244 | Array.from( this.element.childNodes ).forEach( ( backgroundh, h ) => { |
| 245 | |
| 246 | backgroundh.classList.remove( 'past', 'present', 'future' ); |
| 247 | |
| 248 | if( h < indices.h ) { |
| 249 | backgroundh.classList.add( horizontalPast ); |
| 250 | } |
| 251 | else if ( h > indices.h ) { |
| 252 | backgroundh.classList.add( horizontalFuture ); |
| 253 | } |
| 254 | else { |
| 255 | backgroundh.classList.add( 'present' ); |
| 256 | |
| 257 | // Store a reference to the current background element |
| 258 | currentBackground = backgroundh; |
| 259 | } |
| 260 | |
| 261 | if( includeAll || h === indices.h ) { |
| 262 | queryAll( backgroundh, '.slide-background' ).forEach( ( backgroundv, v ) => { |
| 263 | |
| 264 | backgroundv.classList.remove( 'past', 'present', 'future' ); |
| 265 | |
| 266 | if( v < indices.v ) { |
| 267 | backgroundv.classList.add( 'past' ); |
| 268 | } |
| 269 | else if ( v > indices.v ) { |
| 270 | backgroundv.classList.add( 'future' ); |
| 271 | } |
| 272 | else { |
| 273 | backgroundv.classList.add( 'present' ); |
| 274 | |
| 275 | // Only if this is the present horizontal and vertical slide |
| 276 | if( h === indices.h ) currentBackground = backgroundv; |
| 277 | } |
| 278 | |
| 279 | } ); |
| 280 | } |
| 281 | |
| 282 | } ); |
| 283 | |
| 284 | // Stop content inside of previous backgrounds |
| 285 | if( this.previousBackground ) { |
| 286 | |
| 287 | this.Reveal.slideContent.stopEmbeddedContent( this.previousBackground, { unloadIframes: !this.Reveal.slideContent.shouldPreload( this.previousBackground ) } ); |
| 288 | |
| 289 | } |
| 290 | |
| 291 | // Start content in the current background |
| 292 | if( currentBackground ) { |
| 293 | |
| 294 | this.Reveal.slideContent.startEmbeddedContent( currentBackground ); |
| 295 | |
| 296 | let currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' ); |
| 297 | if( currentBackgroundContent ) { |
| 298 | |
| 299 | let backgroundImageURL = currentBackgroundContent.style.backgroundImage || ''; |
| 300 | |
| 301 | // Restart GIFs (doesn't work in Firefox) |
| 302 | if( /\.gif/i.test( backgroundImageURL ) ) { |
| 303 | currentBackgroundContent.style.backgroundImage = ''; |
| 304 | window.getComputedStyle( currentBackgroundContent ).opacity; |
| 305 | currentBackgroundContent.style.backgroundImage = backgroundImageURL; |
| 306 | } |
| 307 | |
| 308 | } |
| 309 | |
| 310 | // Don't transition between identical backgrounds. This |
| 311 | // prevents unwanted flicker. |
| 312 | let previousBackgroundHash = this.previousBackground ? this.previousBackground.getAttribute( 'data-background-hash' ) : null; |
| 313 | let currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' ); |
| 314 | if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== this.previousBackground ) { |
| 315 | this.element.classList.add( 'no-transition' ); |
| 316 | } |
| 317 | |
| 318 | this.previousBackground = currentBackground; |
| 319 | |
| 320 | } |
| 321 | |
| 322 | // If there's a background brightness flag for this slide, |
| 323 | // bubble it to the .reveal container |
| 324 | if( currentSlide ) { |
| 325 | [ 'has-light-background', 'has-dark-background' ].forEach( classToBubble => { |
| 326 | if( currentSlide.classList.contains( classToBubble ) ) { |
| 327 | this.Reveal.getRevealElement().classList.add( classToBubble ); |
| 328 | } |
| 329 | else { |
| 330 | this.Reveal.getRevealElement().classList.remove( classToBubble ); |
| 331 | } |
| 332 | }, this ); |
| 333 | } |
| 334 | |
| 335 | // Allow the first background to apply without transition |
| 336 | setTimeout( () => { |
| 337 | this.element.classList.remove( 'no-transition' ); |
| 338 | }, 1 ); |
| 339 | |
| 340 | } |
| 341 | |
| 342 | /** |
| 343 | * Updates the position of the parallax background based |
| 344 | * on the current slide index. |
| 345 | */ |
| 346 | updateParallax() { |
| 347 | |
| 348 | let indices = this.Reveal.getIndices(); |
| 349 | |
| 350 | if( this.Reveal.getConfig().parallaxBackgroundImage ) { |
| 351 | |
| 352 | let horizontalSlides = this.Reveal.getHorizontalSlides(), |
| 353 | verticalSlides = this.Reveal.getVerticalSlides(); |
| 354 | |
| 355 | let backgroundSize = this.element.style.backgroundSize.split( ' ' ), |
| 356 | backgroundWidth, backgroundHeight; |
| 357 | |
| 358 | if( backgroundSize.length === 1 ) { |
| 359 | backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 ); |
| 360 | } |
| 361 | else { |
| 362 | backgroundWidth = parseInt( backgroundSize[0], 10 ); |
| 363 | backgroundHeight = parseInt( backgroundSize[1], 10 ); |
| 364 | } |
| 365 | |
| 366 | let slideWidth = this.element.offsetWidth, |
| 367 | horizontalSlideCount = horizontalSlides.length, |
| 368 | horizontalOffsetMultiplier, |
| 369 | horizontalOffset; |
| 370 | |
| 371 | if( typeof this.Reveal.getConfig().parallaxBackgroundHorizontal === 'number' ) { |
| 372 | horizontalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundHorizontal; |
| 373 | } |
| 374 | else { |
| 375 | horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0; |
| 376 | } |
| 377 | |
| 378 | horizontalOffset = horizontalOffsetMultiplier * indices.h * -1; |
| 379 | |
| 380 | let slideHeight = this.element.offsetHeight, |
| 381 | verticalSlideCount = verticalSlides.length, |
| 382 | verticalOffsetMultiplier, |
| 383 | verticalOffset; |
| 384 | |
| 385 | if( typeof this.Reveal.getConfig().parallaxBackgroundVertical === 'number' ) { |
| 386 | verticalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundVertical; |
| 387 | } |
| 388 | else { |
| 389 | verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ); |
| 390 | } |
| 391 | |
| 392 | verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indices.v : 0; |
| 393 | |
| 394 | this.element.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px'; |
| 395 | |
| 396 | } |
| 397 | |
| 398 | } |
| 399 | |
Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 400 | destroy() { |
| 401 | |
| 402 | this.element.remove(); |
| 403 | |
| 404 | } |
| 405 | |
Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 406 | } |