PageSourceSearch

https://sol.vin/js/carousel.js

js sol.vin collected 2026-10-09 21:58:40 UTC 23,953 bytes, 544 lines download raw bytes

1/**
2 * Modular Photo & Video Carousel Component
3 * Features:
4 * - Lazy loading for all images, direct videos, and YouTube embeds (`loading="lazy"`)
5 * - YouTube iframe embed support with COEP `credentialless` compatibility (`https://www.youtube-nocookie.com/embed/...`)
6 * - Direct HTML5 video support (`.mp4`, `.webm`, `.ogg`)
7 * - Prev / Next slide navigation & keyboard arrow support
8 * - Native Autoplay Slideshow engine (`[ ⏵ PLAY ]` / `[ ⏸ PAUSE ]`) with Spacebar toggle
9 * - Counter badge updating
10 * - Auto-fading controls after 3 seconds of inactivity
11 * - Bottom-center button to open a 3-column grid thumbnail gallery modal with pagination across all cards
12 * - Video & YouTube icon badges on gallery thumbnails
13 * - Retro ImageViewer lightbox integration with full zoom and next/prev gallery navigation
14 */
15(function() {
16  class PhotoCarousel {
17    static activeInstance = null;
18    static modalBound = false;
19
20    constructor(container) {
21      this.container = typeof container === 'string' ? document.querySelector(container) : container;
22      if (!this.container) return;
23
24      this.mainFrame = this.container.querySelector('.gallery-main-frame, .carousel-main-frame') || this.container.querySelector('#gallery-main-img, .carousel-main-img')?.parentElement;
25      this.mainImg = this.container.querySelector('#gallery-main-img, .carousel-main-img');
26      this.prevBtn = this.container.querySelector('#gallery-prev-btn, .carousel-prev-btn');
27      this.nextBtn = this.container.querySelector('#gallery-next-btn, .carousel-next-btn');
28      this.playBtn = this.container.querySelector('#gallery-play-btn, .carousel-play-btn');
29      this.counter = this.container.querySelector('#gallery-counter, .carousel-counter');
30      this.thumbsToggleBtn = this.container.querySelector('#gallery-thumbs-toggle-btn, .carousel-thumbs-btn');
31
32      this.thumbs = Array.from(this.container.querySelectorAll('.gallery-thumb, .carousel-thumb'));
33      this.currentIndex = 0;
34      this.itemsPerPage = 9; // 3 columns x 3 rows
35      this.modalCurrentPage = 0;
36      this.fadeTimer = null;
37      this.autoplayTimer = null;
38      this.isAutoplaying = false;
39      this.isBottomLayout = this.container.classList.contains('carousel-layout-bottom') || this.container.getAttribute('data-gui-layout') === 'bottom';
40      if (this.isBottomLayout) {
41        this.fadeControls = [];
42      } else {
43        this.fadeControls = Array.from(this.container.querySelectorAll('.carousel-overlay-control, #gallery-prev-btn, #gallery-next-btn, #gallery-play-btn, #gallery-counter, #gallery-thumbs-toggle-btn, .carousel-prev-btn, .carousel-next-btn, .carousel-play-btn, .carousel-counter, .carousel-thumbs-btn'));
44      }
45
46      this.container._photoCarousel = this;
47      this.init();
48    }
49
50    init() {
51      if (this.thumbs.length === 0) return;
52
53      if (this.mainFrame) {
54        this.mainFrame.style.cursor = 'pointer';
55        this.mainFrame.addEventListener('click', (e) => {
56          if (e.target.closest('.carousel-overlay-control, .carousel-prev-btn, .carousel-next-btn, .carousel-play-btn, .carousel-thumbs-btn, iframe, video')) return;
57          this.stopAutoplay();
58          if (window.ImageViewer) {
59            window.ImageViewer.getInstance().openFromCarousel(this, this.currentIndex);
60          }
61        });
62      }
63
64      if (this.prevBtn) {
65        this.prevBtn.addEventListener('click', (e) => {
66          e.preventDefault();
67          this.updateGallery(this.currentIndex - 1);
68          if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer();
69        });
70      }
71
72      if (this.nextBtn) {
73        this.nextBtn.addEventListener('click', (e) => {
74          e.preventDefault();
75          this.updateGallery(this.currentIndex + 1);
76          if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer();
77        });
78      }
79
80      if (this.playBtn) {
81        this.playBtn.addEventListener('click', (e) => {
82          e.preventDefault();
83          e.stopPropagation();
84          this.toggleAutoplay();
85          if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer();
86        });
87      }
88
89      this.thumbs.forEach((t, idx) => {
90        t.setAttribute('loading', 'lazy');
91        t.addEventListener('click', () => {
92          this.updateGallery(idx);
93        });
94      });
95
96      // Keyboard navigation & shortcuts
97      document.addEventListener('keydown', (e) => {
98        const gamePlayerModal = document.getElementById('game-player-modal-overlay');
99        const thumbsModal = document.getElementById('carousel-thumbs-modal-overlay');
100        const imageViewerModal = document.getElementById('gui-image-modal');
101        if ((gamePlayerModal && gamePlayerModal.style.display !== 'none') || 
102            (thumbsModal && thumbsModal.style.display !== 'none') || 
103            (imageViewerModal && imageViewerModal.classList.contains('is-active'))) {
104          return;
105        }
106
107        const rect = this.container.getBoundingClientRect();
108        const inViewport = rect.top < window.innerHeight && rect.bottom > 0;
109        if (!inViewport) return;
110
111        if (e.key === 'ArrowLeft') {
112          this.updateGallery(this.currentIndex - 1);
113          if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer();
114        } else if (e.key === 'ArrowRight') {
115          this.updateGallery(this.currentIndex + 1);
116          if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer();
117        } else if ((e.key === ' ' || e.code === 'Space') && e.target === document.body) {
118          if (this.thumbs.length > 1) {
119            e.preventDefault();
120            this.toggleAutoplay();
121            if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer();
122          }
123        }
124      });
125
126      // Bottom center thumbnail gallery toggle button
127      if (this.thumbsToggleBtn) {
128        this.thumbsToggleBtn.addEventListener('click', (e) => {
129          e.preventDefault();
130          this.stopAutoplay();
131          this.openThumbsModal();
132          if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer();
133        });
134      }
135
136      PhotoCarousel.bindGlobalModalEvents();
137      this.setupAutoFade();
138      this.updateGallery(0);
139
140      // Auto-start if container specifies data-autoplay="true"
141      if (this.container.getAttribute('data-autoplay') === 'true') {
142        this.startAutoplay();
143      }
144    }
145
146    startAutoplay(interval) {
147      if (this.thumbs.length <= 1) return;
148      this.stopAutoplay();
149      this.isAutoplaying = true;
150      const dur = interval || this.autoplayInterval || 3500;
151      this.updatePlayBtnUi(true);
152      this.autoplayTimer = setInterval(() => {
153        this.updateGallery(this.currentIndex + 1);
154      }, dur);
155    }
156
157    stopAutoplay() {
158      if (this.autoplayTimer) {
159        clearInterval(this.autoplayTimer);
160        this.autoplayTimer = null;
161      }
162      this.isAutoplaying = false;
163      this.updatePlayBtnUi(false);
164    }
165
166    toggleAutoplay() {
167      if (this.isAutoplaying) {
168        this.stopAutoplay();
169      } else {
170        this.startAutoplay();
171      }
172    }
173
174    updatePlayBtnUi(isPlaying) {
175      if (!this.playBtn) return;
176      if (isPlaying) {
177        this.playBtn.textContent = '[ ⏸ PAUSE ]';
178        this.playBtn.classList.add('retro-btn-active');
179        this.playBtn.title = 'Pause Automatic Slideshow (Spacebar)';
180      } else {
181        this.playBtn.textContent = '[ ⏵ PLAY ]';
182        this.playBtn.classList.remove('retro-btn-active');
183        this.playBtn.title = 'Start Automatic Slideshow (Spacebar)';
184      }
185    }
186
187    static bindGlobalModalEvents() {
188      if (PhotoCarousel.modalBound) return;
189      PhotoCarousel.modalBound = true;
190
191      document.addEventListener('click', (e) => {
192        const thumbsBtn = e.target.closest('#gallery-thumbs-toggle-btn, .carousel-thumbs-btn');
193        if (thumbsBtn) {
194          e.preventDefault();
195          const wrapper = thumbsBtn.closest('.game-carousel-wrapper, .photo-carousel-wrapper, .photo-carousel-container, .terminal-window, .project-card');
196          if (wrapper && wrapper._photoCarousel) {
197            wrapper._photoCarousel.openThumbsModal();
198          }
199          return;
200        }
201
202        const closeBtn = e.target.closest('#carousel-thumbs-modal-close');
203        const modalOverlay = document.getElementById('carousel-thumbs-modal-overlay');
204        if (closeBtn || e.target === modalOverlay) {
205          PhotoCarousel.closeThumbsModal();
206        }
207
208        const prevBtn = e.target.closest('#carousel-thumbs-prev-page');
209        if (prevBtn && PhotoCarousel.activeInstance) {
210          e.preventDefault();
211          if (PhotoCarousel.activeInstance.modalCurrentPage > 0) {
212            PhotoCarousel.activeInstance.modalCurrentPage--;
213            PhotoCarousel.activeInstance.renderModalPage();
214          }
215        }
216
217        const nextBtn = e.target.closest('#carousel-thumbs-next-page');
218        if (nextBtn && PhotoCarousel.activeInstance) {
219          e.preventDefault();
220          const totalPages = Math.ceil(PhotoCarousel.activeInstance.thumbs.length / PhotoCarousel.activeInstance.itemsPerPage);
221          if (PhotoCarousel.activeInstance.modalCurrentPage < totalPages - 1) {
222            PhotoCarousel.activeInstance.modalCurrentPage++;
223            PhotoCarousel.activeInstance.renderModalPage();
224          }
225        }
226      });
227
228      document.addEventListener('keydown', (e) => {
229        const thumbsModal = document.getElementById('carousel-thumbs-modal-overlay');
230        if (e.key === 'Escape' && thumbsModal && thumbsModal.style.display !== 'none') {
231          PhotoCarousel.closeThumbsModal();
232        }
233      });
234    }
235
236    updateGallery(idx) {
237      if (idx < 0) idx = this.thumbs.length - 1;
238      if (idx >= this.thumbs.length) idx = 0;
239      this.currentIndex = idx;
240
241      const activeThumb = this.thumbs[this.currentIndex];
242      const targetSrc = activeThumb.getAttribute('data-full-src') || activeThumb.src || activeThumb.getAttribute('data-url');
243      const mediaType = activeThumb.getAttribute('data-media-type') || this.detectMediaType(targetSrc);
244      const youtubeId = activeThumb.getAttribute('data-youtube-id') || this.extractYoutubeId(targetSrc);
245      const isExplicitNoFilter = this.container.getAttribute('data-palette-filter') === 'false' || 
246                                 this.container.closest('.art-stage-container, [data-palette-filter="false"], .art-viewer-body') !== null ||
247                                 activeThumb.getAttribute('data-palette-filter') === 'false';
248      const shouldApplyDuotone = !isExplicitNoFilter;
249
250      if (this.mainFrame) {
251        // Clear all active media elements and YouTube placeholder boxes
252        const existingMedia = this.mainFrame.querySelectorAll('img, video, iframe, .carousel-youtube-box');
253        existingMedia.forEach(el => el.remove());
254        this.mainImg = null;
255        
256        if (mediaType === 'youtube' && youtubeId) {
257          const disableEmbeds = this.container.hasAttribute('data-disable-embeds') || 
258                                this.container.getAttribute('data-disable-embeds') === 'true' ||
259                                document.body.classList.contains('disable-carousel-embeds');
260
261          if (disableEmbeds) {
262            const wrapper = document.createElement('div');
263            wrapper.className = 'carousel-youtube-box';
264            
265            const icon = document.createElement('span');
266            icon.className = 'youtube-box-icon';
267            icon.style.cssText = '-webkit-mask-image: url(\'/icons/play.svg\'); mask-image: url(\'/icons/play.svg\');';
268            wrapper.appendChild(icon);
269
270            const title = document.createElement('div');
271            title.className = 'youtube-box-title';
272            title.textContent = '[ YOUTUBE VIDEO DEMO ]';
273            wrapper.appendChild(title);
274
275            const watchBtn = document.createElement('a');
276            watchBtn.href = `https://www.youtube.com/watch?v=${youtubeId}`;
277            watchBtn.target = '_blank';
278            watchBtn.rel = 'noopener noreferrer';
279            watchBtn.className = 'export-btn';
280            watchBtn.style.cssText = 'display: inline-flex; align-items: center; gap: 0.5rem;';
281            watchBtn.innerHTML = '<span class="stat-icon" style="-webkit-mask-image: url(\'/icons/youtube.svg\'); mask-image: url(\'/icons/youtube.svg\'); width: 14px; height: 14px; display: inline-block; background-color: currentColor;"></span> <span>[ WATCH ON YOUTUBE ↗ ]</span>';
282            wrapper.appendChild(watchBtn);
283
284            this.mainFrame.insertBefore(wrapper, this.mainFrame.firstChild);
285          } else {
286            const iframe = document.createElement('iframe');
287            iframe.src = `https://www.youtube-nocookie.com/embed/${youtubeId}`;
288            iframe.setAttribute('loading', 'lazy');
289            iframe.setAttribute('credentialless', 'true');
290            iframe.setAttribute('frameborder', '0');
291            iframe.setAttribute('allow', 'accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture');
292            iframe.setAttribute('allowfullscreen', 'true');
293            iframe.style.cssText = 'width: 100%; height: 100%; border: none; display: block; z-index: 1;';
294            this.mainFrame.insertBefore(iframe, this.mainFrame.firstChild);
295          }
296        } else if (mediaType === 'video') {
297          const video = document.createElement('video');
298          video.src = targetSrc;
299          video.controls = true;
300          video.autoplay = false;
301          video.loop = true;
302          video.muted = true;
303          video.setAttribute('loading', 'lazy');
304          video.style.cssText = 'width: 100%; height: 100%; object-fit: contain; display: block; z-index: 1;';
305          this.mainFrame.insertBefore(video, this.mainFrame.firstChild);
306        } else {
307          const img = document.createElement('img');
308          img.id = 'gallery-main-img';
309          img.className = 'carousel-main-img';
310          img.src = targetSrc;
311          img.setAttribute('loading', 'lazy');
312          img.alt = 'Gallery Media';
313          if (shouldApplyDuotone) {
314            img.setAttribute('data-palette-filter', 'true');
315            img.removeAttribute('filter');
316            img.removeAttribute('data-filter');
317            img.classList.remove('no-palette-filter', 'no-monochrome');
318          } else {
319            img.setAttribute('data-palette-filter', 'false');
320            img.setAttribute('filter', '0');
321            img.setAttribute('data-filter', '0');
322            img.classList.add('no-palette-filter', 'no-monochrome');
323            img.style.filter = 'none';
324            img.style.webkitFilter = 'none';
325          }
326          img.style.cssText = 'max-width: 100%; max-height: 100%; object-fit: contain; display: block; margin: 0 auto; z-index: 1;';
327          this.mainFrame.insertBefore(img, this.mainFrame.firstChild);
328          this.mainImg = img;
329
330          if (shouldApplyDuotone && window.PaletteEngine && typeof window.PaletteEngine.initDuotoneImages === 'function') {
331            window.PaletteEngine.initDuotoneImages(this.mainFrame);
332          }
333        }
334      }
335
336      this.thumbs.forEach((t, i) => {
337        if (i === this.currentIndex) {
338          t.classList.add('active-thumb');
339          try {
340            t.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
341          } catch(e) {}
342        } else {
343          t.classList.remove('active-thumb');
344        }
345      });
346
347      if (this.counter) {
348        this.counter.textContent = '[ ' + (this.currentIndex + 1) + ' / ' + this.thumbs.length + ' ]';
349      }
350
351      // Preload next image
352      const nextIdx = (this.currentIndex + 1) % this.thumbs.length;
353      const nextThumb = this.thumbs[nextIdx];
354      if (nextThumb) {
355        const nSrc = nextThumb.getAttribute('data-full-src') || nextThumb.src;
356        if (nSrc && !nSrc.includes('youtube.com')) {
357          const preImg = new Image();
358          preImg.src = nSrc;
359        }
360      }
361    }
362
363    detectMediaType(src) {
364      if (!src) return 'image';
365      const s = src.toLowerCase();
366      if (s.includes('youtube.com') || s.includes('youtu.be')) return 'youtube';
367      if (s.endsWith('.mp4') || s.endsWith('.webm') || s.endsWith('.ogg')) return 'video';
368      return 'image';
369    }
370
371    extractYoutubeId(url) {
372      if (!url) return '';
373      if (url.includes('v=')) return url.split('v=')[1]?.split('&')[0] || '';
374      if (url.includes('youtu.be/')) return url.split('youtu.be/')[1]?.split('?')[0] || '';
375      if (url.includes('embed/')) return url.split('embed/')[1]?.split('?')[0] || '';
376      return '';
377    }
378
379    openThumbsModal() {
380      const modalOverlay = document.getElementById('carousel-thumbs-modal-overlay');
381      const modalGrid = document.getElementById('carousel-thumbs-modal-grid');
382      if (!modalOverlay || !modalGrid) return;
383
384      PhotoCarousel.activeInstance = this;
385      this.modalCurrentPage = Math.floor(this.currentIndex / this.itemsPerPage);
386      this.renderModalPage();
387      modalOverlay.style.cssText = 'display: flex !important; z-index: 999999;';
388      document.body.style.overflow = 'hidden';
389    }
390
391    renderModalPage() {
392      const modalGrid = document.getElementById('carousel-thumbs-modal-grid');
393      const pageBadge = document.getElementById('carousel-thumbs-page-badge');
394      const prevPageBtn = document.getElementById('carousel-thumbs-prev-page');
395      const nextPageBtn = document.getElementById('carousel-thumbs-next-page');
396      const paginationContainer = document.getElementById('carousel-thumbs-modal-pagination');
397
398      if (!modalGrid) return;
399
400      const totalPages = Math.ceil(this.thumbs.length / this.itemsPerPage) || 1;
401      if (this.modalCurrentPage >= totalPages) this.modalCurrentPage = totalPages - 1;
402      if (this.modalCurrentPage < 0) this.modalCurrentPage = 0;
403
404      const startIdx = this.modalCurrentPage * this.itemsPerPage;
405      const endIdx = Math.min(startIdx + this.itemsPerPage, this.thumbs.length);
406
407      modalGrid.style.cssText = 'display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 0.5rem; margin-bottom: 1rem; align-items: center; justify-items: center;';
408      modalGrid.innerHTML = '';
409
410      for (let idx = startIdx; idx < endIdx; idx++) {
411        const t = this.thumbs[idx];
412        const fullSrc = t.getAttribute('data-full-src') || t.src || '';
413        const thumbSrc = t.src || t.getAttribute('data-thumbnail') || fullSrc;
414        const mediaType = t.getAttribute('data-media-type') || this.detectMediaType(fullSrc);
415
416        const card = document.createElement('div');
417        card.style.cssText = 'position: relative; width: 100%; aspect-ratio: 16/9; max-height: 130px; border: 2px solid var(--border-color); cursor: pointer; border-radius: 3px; overflow: hidden; display: flex; align-items: center; justify-content: center; background: var(--bg-window); box-sizing: border-box; transition: border-color 0.2s ease, transform 0.15s ease;';
418
419        if (idx === this.currentIndex) {
420          card.style.borderColor = 'var(--accent-color)';
421        }
422
423        const thumbImg = document.createElement('img');
424        thumbImg.src = thumbSrc;
425        thumbImg.alt = `Thumbnail ${idx + 1}`;
426        thumbImg.setAttribute('loading', 'lazy');
427        thumbImg.style.cssText = 'width: 100%; height: 100%; object-fit: cover; object-position: center; display: block; border: none; margin: 0; padding: 0;';
428
429        card.appendChild(thumbImg);
430
431        // Add Video / YouTube Badge Overlay
432        if (mediaType === 'youtube' || mediaType === 'video') {
433          const badge = document.createElement('div');
434          badge.style.cssText = 'position: absolute; bottom: 6px; right: 6px; background: rgba(0, 0, 0, 0.85); color: var(--accent-color); font-size: 0.7rem; font-weight: bold; padding: 2px 6px; border: 1px solid var(--accent-color); border-radius: 2px; text-transform: uppercase; z-index: 2; display: flex; align-items: center; gap: 4px;';
435          const iconFile = mediaType === 'youtube' ? 'youtube.svg' : 'video.svg';
436          const iconHtml = `<span class="stat-icon" style="-webkit-mask-image: url('/icons/${iconFile}'); mask-image: url('/icons/${iconFile}'); width: 12px; height: 12px; display: inline-block; background-color: currentColor;"></span>`;
437          badge.innerHTML = `${iconHtml} <span>${mediaType === 'youtube' ? 'YOUTUBE' : 'VIDEO'}</span>`;
438          card.appendChild(badge);
439        }
440
441        card.addEventListener('mouseenter', () => {
442          card.style.transform = 'scale(1.03)';
443        });
444        card.addEventListener('mouseleave', () => {
445          card.style.transform = 'scale(1.0)';
446        });
447
448        card.addEventListener('click', () => {
449          this.updateGallery(idx);
450          PhotoCarousel.closeThumbsModal();
451          if (window.ImageViewer && window.ImageViewer.instance && window.ImageViewer.instance.modal && window.ImageViewer.instance.modal.classList.contains('is-active')) {
452            window.ImageViewer.instance.updateFromCarousel(this);
453          }
454        });
455
456        modalGrid.appendChild(card);
457      }
458
459      if (pageBadge) {
460        pageBadge.textContent = `[ PAGE ${this.modalCurrentPage + 1} / ${totalPages} ]`;
461      }
462
463      if (prevPageBtn) {
464        prevPageBtn.style.opacity = this.modalCurrentPage > 0 ? '1' : '0.4';
465        prevPageBtn.style.pointerEvents = this.modalCurrentPage > 0 ? 'auto' : 'none';
466      }
467
468      if (nextPageBtn) {
469        nextPageBtn.style.opacity = this.modalCurrentPage < totalPages - 1 ? '1' : '0.4';
470        nextPageBtn.style.pointerEvents = this.modalCurrentPage < totalPages - 1 ? 'auto' : 'none';
471      }
472
473      if (paginationContainer) {
474        paginationContainer.style.display = totalPages > 1 ? 'flex' : 'none';
475      }
476    }
477
478    static closeThumbsModal() {
479      const modalOverlay = document.getElementById('carousel-thumbs-modal-overlay');
480      if (!modalOverlay) return;
481      modalOverlay.style.cssText = 'display: none !important;';
482      const isImageViewerActive = window.ImageViewer && window.ImageViewer.instance && window.ImageViewer.instance.modal && window.ImageViewer.instance.modal.classList.contains('is-active');
483      if (!isImageViewerActive) {
484        document.body.style.overflow = '';
485      }
486    }
487
488    setupAutoFade() {
489      if (!this.fadeControls || this.fadeControls.length === 0) return;
490
491      const showControls = () => {
492        this.fadeControls.forEach(ctrl => {
493          ctrl.style.opacity = '1';
494          ctrl.style.pointerEvents = 'auto';
495        });
496      };
497
498      const hideControls = () => {
499        this.fadeControls.forEach(ctrl => {
500          ctrl.style.opacity = '0';
501          ctrl.style.pointerEvents = 'none';
502        });
503      };
504
505      this.resetFadeTimer = () => {
506        showControls();
507        if (this.fadeTimer) clearTimeout(this.fadeTimer);
508        this.fadeTimer = setTimeout(() => {
509          hideControls();
510        }, 3000);
511      };
512
513      this.fadeControls.forEach(ctrl => {
514        ctrl.style.transition = 'opacity 0.4s ease';
515      });
516
517      this.container.addEventListener('mousemove', this.resetFadeTimer);
518      this.container.addEventListener('mouseenter', this.resetFadeTimer);
519      this.container.addEventListener('focusin', this.resetFadeTimer);
520      this.container.addEventListener('mouseleave', () => {
521        if (this.fadeTimer) clearTimeout(this.fadeTimer);
522        this.fadeTimer = setTimeout(hideControls, 1000);
523      });
524
525      this.resetFadeTimer();
526    }
527  }
528
529  window.PhotoCarousel = PhotoCarousel;
530
531  function initCarousels() {
532    document.querySelectorAll('.game-carousel-wrapper, .photo-carousel-wrapper, .photo-carousel-container').forEach(wrapper => {
533      if (!wrapper._photoCarousel) {
534        new PhotoCarousel(wrapper);
535      }
536    });
537  }
538
539  if (document.readyState === 'loading') {
540    document.addEventListener('DOMContentLoaded', initCarousels);
541  } else {
542    initCarousels();
543  }
544})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.