blob: e7ca644fcc81f24ea013d1ca6f54b93c986dae07 [file] [log] [blame]
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001import { queryAll } from '../utils/util.js'
2import { colorToRgb, colorBrightness } from '../utils/color.js'
3
4/**
5 * Creates and updates slide backgrounds.
6 */
7export 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 Kupietz09b75752023-10-07 09:32:19 +0200125 backgroundGradient: slide.getAttribute( 'data-background-gradient' ),
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200126 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 Kupietz09b75752023-10-07 09:32:19 +0200154 if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp|webp)([?#\s]|$)/gi.test( data.background ) ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200155 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 Kupietz09b75752023-10-07 09:32:19 +0200165 if( data.background || data.backgroundColor || data.backgroundGradient || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200166 element.setAttribute( 'data-background-hash', data.background +
167 data.backgroundSize +
168 data.backgroundImage +
169 data.backgroundVideo +
170 data.backgroundIframe +
171 data.backgroundColor +
Marc Kupietz09b75752023-10-07 09:32:19 +0200172 data.backgroundGradient +
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200173 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 Kupietz09b75752023-10-07 09:32:19 +0200182 if( data.backgroundGradient ) element.style.backgroundImage = data.backgroundGradient;
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200183 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
Marc Kupietz9c036a42024-05-14 13:17:25 +0200193 const contrastClass = this.getContrastClass( slide );
194
195 if( typeof contrastClass === 'string' ) {
196 slide.classList.add( contrastClass );
197 }
198
199 }
200
201 /**
202 * Returns a class name that can be applied to a slide to indicate
203 * if it has a light or dark background.
204 *
205 * @param {*} slide
206 *
207 * @returns {string|null}
208 */
209 getContrastClass( slide ) {
210
211 const element = slide.slideBackgroundElement;
212
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200213 // If this slide has a background color, we add a class that
214 // signals if it is light or dark. If the slide has no background
215 // color, no class will be added
Marc Kupietz9c036a42024-05-14 13:17:25 +0200216 let contrastColor = slide.getAttribute( 'data-background-color' );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200217
218 // If no bg color was found, or it cannot be converted by colorToRgb, check the computed background
219 if( !contrastColor || !colorToRgb( contrastColor ) ) {
220 let computedBackgroundStyle = window.getComputedStyle( element );
221 if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
222 contrastColor = computedBackgroundStyle.backgroundColor;
223 }
224 }
225
226 if( contrastColor ) {
227 const rgb = colorToRgb( contrastColor );
228
229 // Ignore fully transparent backgrounds. Some browsers return
230 // rgba(0,0,0,0) when reading the computed background color of
231 // an element with no background
232 if( rgb && rgb.a !== 0 ) {
233 if( colorBrightness( contrastColor ) < 128 ) {
Marc Kupietz9c036a42024-05-14 13:17:25 +0200234 return 'has-dark-background';
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200235 }
236 else {
Marc Kupietz9c036a42024-05-14 13:17:25 +0200237 return 'has-light-background';
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200238 }
239 }
240 }
241
Marc Kupietz9c036a42024-05-14 13:17:25 +0200242 return null;
243
244 }
245
246 /**
247 * Bubble the 'has-light-background'/'has-dark-background' classes.
248 */
249 bubbleSlideContrastClassToElement( slide, target ) {
250
251 [ 'has-light-background', 'has-dark-background' ].forEach( classToBubble => {
252 if( slide.classList.contains( classToBubble ) ) {
253 target.classList.add( classToBubble );
254 }
255 else {
256 target.classList.remove( classToBubble );
257 }
258 }, this );
259
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200260 }
261
262 /**
263 * Updates the background elements to reflect the current
264 * slide.
265 *
266 * @param {boolean} includeAll If true, the backgrounds of
267 * all vertical slides (not just the present) will be updated.
268 */
269 update( includeAll = false ) {
270
Marc Kupietz9c036a42024-05-14 13:17:25 +0200271 let config = this.Reveal.getConfig();
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200272 let currentSlide = this.Reveal.getCurrentSlide();
273 let indices = this.Reveal.getIndices();
274
275 let currentBackground = null;
276
277 // Reverse past/future classes when in RTL mode
Marc Kupietz9c036a42024-05-14 13:17:25 +0200278 let horizontalPast = config.rtl ? 'future' : 'past',
279 horizontalFuture = config.rtl ? 'past' : 'future';
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200280
281 // Update the classes of all backgrounds to match the
282 // states of their slides (past/present/future)
283 Array.from( this.element.childNodes ).forEach( ( backgroundh, h ) => {
284
285 backgroundh.classList.remove( 'past', 'present', 'future' );
286
287 if( h < indices.h ) {
288 backgroundh.classList.add( horizontalPast );
289 }
290 else if ( h > indices.h ) {
291 backgroundh.classList.add( horizontalFuture );
292 }
293 else {
294 backgroundh.classList.add( 'present' );
295
296 // Store a reference to the current background element
297 currentBackground = backgroundh;
298 }
299
300 if( includeAll || h === indices.h ) {
301 queryAll( backgroundh, '.slide-background' ).forEach( ( backgroundv, v ) => {
302
303 backgroundv.classList.remove( 'past', 'present', 'future' );
304
Marc Kupietz9c036a42024-05-14 13:17:25 +0200305 const indexv = typeof indices.v === 'number' ? indices.v : 0;
306
307 if( v < indexv ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200308 backgroundv.classList.add( 'past' );
309 }
Marc Kupietz9c036a42024-05-14 13:17:25 +0200310 else if ( v > indexv ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200311 backgroundv.classList.add( 'future' );
312 }
313 else {
314 backgroundv.classList.add( 'present' );
315
316 // Only if this is the present horizontal and vertical slide
317 if( h === indices.h ) currentBackground = backgroundv;
318 }
319
320 } );
321 }
322
323 } );
324
Marc Kupietz9c036a42024-05-14 13:17:25 +0200325 // The previous background may refer to a DOM element that has
326 // been removed after a presentation is synced & bgs are recreated
327 if( this.previousBackground && !this.previousBackground.closest( 'body' ) ) {
328 this.previousBackground = null;
329 }
330
331 if( currentBackground && this.previousBackground ) {
332
333 // Don't transition between identical backgrounds. This
334 // prevents unwanted flicker.
335 let previousBackgroundHash = this.previousBackground.getAttribute( 'data-background-hash' );
336 let currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );
337
338 if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== this.previousBackground ) {
339 this.element.classList.add( 'no-transition' );
340
341 // If multiple slides have the same background video, carry
342 // the <video> element forward so that it plays continuously
343 // across multiple slides
344 const currentVideo = currentBackground.querySelector( 'video' );
345 const previousVideo = this.previousBackground.querySelector( 'video' );
346
347 if( currentVideo && previousVideo ) {
348
349 const currentVideoParent = currentVideo.parentNode;
350 const previousVideoParent = previousVideo.parentNode;
351
352 // Swap the two videos
353 previousVideoParent.appendChild( currentVideo );
354 currentVideoParent.appendChild( previousVideo );
355
356 }
357 }
358
359 }
360
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200361 // Stop content inside of previous backgrounds
362 if( this.previousBackground ) {
363
364 this.Reveal.slideContent.stopEmbeddedContent( this.previousBackground, { unloadIframes: !this.Reveal.slideContent.shouldPreload( this.previousBackground ) } );
365
366 }
367
368 // Start content in the current background
369 if( currentBackground ) {
370
371 this.Reveal.slideContent.startEmbeddedContent( currentBackground );
372
373 let currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' );
374 if( currentBackgroundContent ) {
375
376 let backgroundImageURL = currentBackgroundContent.style.backgroundImage || '';
377
378 // Restart GIFs (doesn't work in Firefox)
379 if( /\.gif/i.test( backgroundImageURL ) ) {
380 currentBackgroundContent.style.backgroundImage = '';
381 window.getComputedStyle( currentBackgroundContent ).opacity;
382 currentBackgroundContent.style.backgroundImage = backgroundImageURL;
383 }
384
385 }
386
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200387 this.previousBackground = currentBackground;
388
389 }
390
391 // If there's a background brightness flag for this slide,
392 // bubble it to the .reveal container
393 if( currentSlide ) {
Marc Kupietz9c036a42024-05-14 13:17:25 +0200394 this.bubbleSlideContrastClassToElement( currentSlide, this.Reveal.getRevealElement() );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200395 }
396
397 // Allow the first background to apply without transition
398 setTimeout( () => {
399 this.element.classList.remove( 'no-transition' );
Marc Kupietz9c036a42024-05-14 13:17:25 +0200400 }, 10 );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200401
402 }
403
404 /**
405 * Updates the position of the parallax background based
406 * on the current slide index.
407 */
408 updateParallax() {
409
410 let indices = this.Reveal.getIndices();
411
412 if( this.Reveal.getConfig().parallaxBackgroundImage ) {
413
414 let horizontalSlides = this.Reveal.getHorizontalSlides(),
415 verticalSlides = this.Reveal.getVerticalSlides();
416
417 let backgroundSize = this.element.style.backgroundSize.split( ' ' ),
418 backgroundWidth, backgroundHeight;
419
420 if( backgroundSize.length === 1 ) {
421 backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 );
422 }
423 else {
424 backgroundWidth = parseInt( backgroundSize[0], 10 );
425 backgroundHeight = parseInt( backgroundSize[1], 10 );
426 }
427
428 let slideWidth = this.element.offsetWidth,
429 horizontalSlideCount = horizontalSlides.length,
430 horizontalOffsetMultiplier,
431 horizontalOffset;
432
433 if( typeof this.Reveal.getConfig().parallaxBackgroundHorizontal === 'number' ) {
434 horizontalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundHorizontal;
435 }
436 else {
437 horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;
438 }
439
440 horizontalOffset = horizontalOffsetMultiplier * indices.h * -1;
441
442 let slideHeight = this.element.offsetHeight,
443 verticalSlideCount = verticalSlides.length,
444 verticalOffsetMultiplier,
445 verticalOffset;
446
447 if( typeof this.Reveal.getConfig().parallaxBackgroundVertical === 'number' ) {
448 verticalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundVertical;
449 }
450 else {
451 verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
452 }
453
454 verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indices.v : 0;
455
456 this.element.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
457
458 }
459
460 }
461
Marc Kupietz09b75752023-10-07 09:32:19 +0200462 destroy() {
463
464 this.element.remove();
465
466 }
467
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200468}