blob: f412a1826127b26838d48bdb4957baa6909a9562 [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' ),
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}