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' ), |
| 125 | backgroundRepeat: slide.getAttribute( 'data-background-repeat' ), |
| 126 | backgroundPosition: slide.getAttribute( 'data-background-position' ), |
| 127 | backgroundTransition: slide.getAttribute( 'data-background-transition' ), |
| 128 | backgroundOpacity: slide.getAttribute( 'data-background-opacity' ), |
| 129 | }; |
| 130 | |
| 131 | const dataPreload = slide.hasAttribute( 'data-preload' ); |
| 132 | |
| 133 | // Reset the prior background state in case this is not the |
| 134 | // initial sync |
| 135 | slide.classList.remove( 'has-dark-background' ); |
| 136 | slide.classList.remove( 'has-light-background' ); |
| 137 | |
| 138 | element.removeAttribute( 'data-loaded' ); |
| 139 | element.removeAttribute( 'data-background-hash' ); |
| 140 | element.removeAttribute( 'data-background-size' ); |
| 141 | element.removeAttribute( 'data-background-transition' ); |
| 142 | element.style.backgroundColor = ''; |
| 143 | |
| 144 | contentElement.style.backgroundSize = ''; |
| 145 | contentElement.style.backgroundRepeat = ''; |
| 146 | contentElement.style.backgroundPosition = ''; |
| 147 | contentElement.style.backgroundImage = ''; |
| 148 | contentElement.style.opacity = ''; |
| 149 | contentElement.innerHTML = ''; |
| 150 | |
| 151 | if( data.background ) { |
| 152 | // Auto-wrap image urls in url(...) |
| 153 | if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)([?#\s]|$)/gi.test( data.background ) ) { |
| 154 | slide.setAttribute( 'data-background-image', data.background ); |
| 155 | } |
| 156 | else { |
| 157 | element.style.background = data.background; |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | // Create a hash for this combination of background settings. |
| 162 | // This is used to determine when two slide backgrounds are |
| 163 | // the same. |
| 164 | if( data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) { |
| 165 | element.setAttribute( 'data-background-hash', data.background + |
| 166 | data.backgroundSize + |
| 167 | data.backgroundImage + |
| 168 | data.backgroundVideo + |
| 169 | data.backgroundIframe + |
| 170 | data.backgroundColor + |
| 171 | data.backgroundRepeat + |
| 172 | data.backgroundPosition + |
| 173 | data.backgroundTransition + |
| 174 | data.backgroundOpacity ); |
| 175 | } |
| 176 | |
| 177 | // Additional and optional background properties |
| 178 | if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize ); |
| 179 | if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor; |
| 180 | if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition ); |
| 181 | |
| 182 | if( dataPreload ) element.setAttribute( 'data-preload', '' ); |
| 183 | |
| 184 | // Background image options are set on the content wrapper |
| 185 | if( data.backgroundSize ) contentElement.style.backgroundSize = data.backgroundSize; |
| 186 | if( data.backgroundRepeat ) contentElement.style.backgroundRepeat = data.backgroundRepeat; |
| 187 | if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition; |
| 188 | if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity; |
| 189 | |
| 190 | // If this slide has a background color, we add a class that |
| 191 | // signals if it is light or dark. If the slide has no background |
| 192 | // color, no class will be added |
| 193 | let contrastColor = data.backgroundColor; |
| 194 | |
| 195 | // If no bg color was found, or it cannot be converted by colorToRgb, check the computed background |
| 196 | if( !contrastColor || !colorToRgb( contrastColor ) ) { |
| 197 | let computedBackgroundStyle = window.getComputedStyle( element ); |
| 198 | if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) { |
| 199 | contrastColor = computedBackgroundStyle.backgroundColor; |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | if( contrastColor ) { |
| 204 | const rgb = colorToRgb( contrastColor ); |
| 205 | |
| 206 | // Ignore fully transparent backgrounds. Some browsers return |
| 207 | // rgba(0,0,0,0) when reading the computed background color of |
| 208 | // an element with no background |
| 209 | if( rgb && rgb.a !== 0 ) { |
| 210 | if( colorBrightness( contrastColor ) < 128 ) { |
| 211 | slide.classList.add( 'has-dark-background' ); |
| 212 | } |
| 213 | else { |
| 214 | slide.classList.add( 'has-light-background' ); |
| 215 | } |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | } |
| 220 | |
| 221 | /** |
| 222 | * Updates the background elements to reflect the current |
| 223 | * slide. |
| 224 | * |
| 225 | * @param {boolean} includeAll If true, the backgrounds of |
| 226 | * all vertical slides (not just the present) will be updated. |
| 227 | */ |
| 228 | update( includeAll = false ) { |
| 229 | |
| 230 | let currentSlide = this.Reveal.getCurrentSlide(); |
| 231 | let indices = this.Reveal.getIndices(); |
| 232 | |
| 233 | let currentBackground = null; |
| 234 | |
| 235 | // Reverse past/future classes when in RTL mode |
| 236 | let horizontalPast = this.Reveal.getConfig().rtl ? 'future' : 'past', |
| 237 | horizontalFuture = this.Reveal.getConfig().rtl ? 'past' : 'future'; |
| 238 | |
| 239 | // Update the classes of all backgrounds to match the |
| 240 | // states of their slides (past/present/future) |
| 241 | Array.from( this.element.childNodes ).forEach( ( backgroundh, h ) => { |
| 242 | |
| 243 | backgroundh.classList.remove( 'past', 'present', 'future' ); |
| 244 | |
| 245 | if( h < indices.h ) { |
| 246 | backgroundh.classList.add( horizontalPast ); |
| 247 | } |
| 248 | else if ( h > indices.h ) { |
| 249 | backgroundh.classList.add( horizontalFuture ); |
| 250 | } |
| 251 | else { |
| 252 | backgroundh.classList.add( 'present' ); |
| 253 | |
| 254 | // Store a reference to the current background element |
| 255 | currentBackground = backgroundh; |
| 256 | } |
| 257 | |
| 258 | if( includeAll || h === indices.h ) { |
| 259 | queryAll( backgroundh, '.slide-background' ).forEach( ( backgroundv, v ) => { |
| 260 | |
| 261 | backgroundv.classList.remove( 'past', 'present', 'future' ); |
| 262 | |
| 263 | if( v < indices.v ) { |
| 264 | backgroundv.classList.add( 'past' ); |
| 265 | } |
| 266 | else if ( v > indices.v ) { |
| 267 | backgroundv.classList.add( 'future' ); |
| 268 | } |
| 269 | else { |
| 270 | backgroundv.classList.add( 'present' ); |
| 271 | |
| 272 | // Only if this is the present horizontal and vertical slide |
| 273 | if( h === indices.h ) currentBackground = backgroundv; |
| 274 | } |
| 275 | |
| 276 | } ); |
| 277 | } |
| 278 | |
| 279 | } ); |
| 280 | |
| 281 | // Stop content inside of previous backgrounds |
| 282 | if( this.previousBackground ) { |
| 283 | |
| 284 | this.Reveal.slideContent.stopEmbeddedContent( this.previousBackground, { unloadIframes: !this.Reveal.slideContent.shouldPreload( this.previousBackground ) } ); |
| 285 | |
| 286 | } |
| 287 | |
| 288 | // Start content in the current background |
| 289 | if( currentBackground ) { |
| 290 | |
| 291 | this.Reveal.slideContent.startEmbeddedContent( currentBackground ); |
| 292 | |
| 293 | let currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' ); |
| 294 | if( currentBackgroundContent ) { |
| 295 | |
| 296 | let backgroundImageURL = currentBackgroundContent.style.backgroundImage || ''; |
| 297 | |
| 298 | // Restart GIFs (doesn't work in Firefox) |
| 299 | if( /\.gif/i.test( backgroundImageURL ) ) { |
| 300 | currentBackgroundContent.style.backgroundImage = ''; |
| 301 | window.getComputedStyle( currentBackgroundContent ).opacity; |
| 302 | currentBackgroundContent.style.backgroundImage = backgroundImageURL; |
| 303 | } |
| 304 | |
| 305 | } |
| 306 | |
| 307 | // Don't transition between identical backgrounds. This |
| 308 | // prevents unwanted flicker. |
| 309 | let previousBackgroundHash = this.previousBackground ? this.previousBackground.getAttribute( 'data-background-hash' ) : null; |
| 310 | let currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' ); |
| 311 | if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== this.previousBackground ) { |
| 312 | this.element.classList.add( 'no-transition' ); |
| 313 | } |
| 314 | |
| 315 | this.previousBackground = currentBackground; |
| 316 | |
| 317 | } |
| 318 | |
| 319 | // If there's a background brightness flag for this slide, |
| 320 | // bubble it to the .reveal container |
| 321 | if( currentSlide ) { |
| 322 | [ 'has-light-background', 'has-dark-background' ].forEach( classToBubble => { |
| 323 | if( currentSlide.classList.contains( classToBubble ) ) { |
| 324 | this.Reveal.getRevealElement().classList.add( classToBubble ); |
| 325 | } |
| 326 | else { |
| 327 | this.Reveal.getRevealElement().classList.remove( classToBubble ); |
| 328 | } |
| 329 | }, this ); |
| 330 | } |
| 331 | |
| 332 | // Allow the first background to apply without transition |
| 333 | setTimeout( () => { |
| 334 | this.element.classList.remove( 'no-transition' ); |
| 335 | }, 1 ); |
| 336 | |
| 337 | } |
| 338 | |
| 339 | /** |
| 340 | * Updates the position of the parallax background based |
| 341 | * on the current slide index. |
| 342 | */ |
| 343 | updateParallax() { |
| 344 | |
| 345 | let indices = this.Reveal.getIndices(); |
| 346 | |
| 347 | if( this.Reveal.getConfig().parallaxBackgroundImage ) { |
| 348 | |
| 349 | let horizontalSlides = this.Reveal.getHorizontalSlides(), |
| 350 | verticalSlides = this.Reveal.getVerticalSlides(); |
| 351 | |
| 352 | let backgroundSize = this.element.style.backgroundSize.split( ' ' ), |
| 353 | backgroundWidth, backgroundHeight; |
| 354 | |
| 355 | if( backgroundSize.length === 1 ) { |
| 356 | backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 ); |
| 357 | } |
| 358 | else { |
| 359 | backgroundWidth = parseInt( backgroundSize[0], 10 ); |
| 360 | backgroundHeight = parseInt( backgroundSize[1], 10 ); |
| 361 | } |
| 362 | |
| 363 | let slideWidth = this.element.offsetWidth, |
| 364 | horizontalSlideCount = horizontalSlides.length, |
| 365 | horizontalOffsetMultiplier, |
| 366 | horizontalOffset; |
| 367 | |
| 368 | if( typeof this.Reveal.getConfig().parallaxBackgroundHorizontal === 'number' ) { |
| 369 | horizontalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundHorizontal; |
| 370 | } |
| 371 | else { |
| 372 | horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0; |
| 373 | } |
| 374 | |
| 375 | horizontalOffset = horizontalOffsetMultiplier * indices.h * -1; |
| 376 | |
| 377 | let slideHeight = this.element.offsetHeight, |
| 378 | verticalSlideCount = verticalSlides.length, |
| 379 | verticalOffsetMultiplier, |
| 380 | verticalOffset; |
| 381 | |
| 382 | if( typeof this.Reveal.getConfig().parallaxBackgroundVertical === 'number' ) { |
| 383 | verticalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundVertical; |
| 384 | } |
| 385 | else { |
| 386 | verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ); |
| 387 | } |
| 388 | |
| 389 | verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indices.v : 0; |
| 390 | |
| 391 | this.element.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px'; |
| 392 | |
| 393 | } |
| 394 | |
| 395 | } |
| 396 | |
| 397 | } |