blob: f90626960a74a9b93f0525967a0c69860a484e99 [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
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 Kupietz09b75752023-10-07 09:32:19 +0200400 destroy() {
401
402 this.element.remove();
403
404 }
405
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200406}