PageSourceSearch

https://www.renewrome.com/content/dam/renewrome/js/video-scroll.js

js renewrome.com collected 2026-10-04 03:31:26 UTC 17,739 bytes, 647 lines download raw bytes

1const video = document.getElementById('scrollVideo');
2const heroSection = document.getElementById('video-section');
3const preloadOverlay = document.getElementById('preload-overlay');
4const preloadPercent = document.getElementById('preload-percent');
5
6document.documentElement.classList.add('video-locked');
7
8// When the loader ends, hero animations should be playing. We keep this flag so
9// late-loading Lotties can start (or recover) on DOMLoaded.
10let heroAnimationsShouldPlay = false;
11
12// Shared breakpoint for both video source selection and mobile Lottie set.
13const compactQuery = window.matchMedia('(max-width: 480px)');
14
15// Autoplay requires muted video; user can enable audio via gesture.
16let userEnabledVideoAudio = false;
17let hasVideoFinishedLoading = false;
18// Hero video audio: enabled by user gesture + toggle button
19(function initHeroVideoAudio() {
20  if (!heroSection || !video) return;
21
22  const toggleBtn = document.getElementById('audio-toggle');
23  const tooltip = document.getElementById('audio-tooltip');
24
25  let hasEverEnabledAudio = false;
26  let isHovering = false;
27
28  const canShowHoverTooltip =
29    typeof window.matchMedia === 'function' &&
30    window.matchMedia('(hover: hover) and (pointer: fine)').matches;
31
32  function isAudioEnabled() {
33    return Boolean(!video.muted && video.volume > 0);
34  }
35
36  video.addEventListener('waiting', () => {
37    // console.log('hasVideoFinishedLoading : ', hasVideoFinishedLoading);
38    // if (!hasVideoFinishedLoading) {
39    //   hasVideoFinishedLoading = true;
40    //   return;
41    // } else {
42    disableAudio();
43    // }
44  });
45
46  function updateToggleUi() {
47    if (!toggleBtn) return;
48    const on = isAudioEnabled();
49    toggleBtn.setAttribute('aria-pressed', on ? 'true' : 'false');
50    toggleBtn.setAttribute(
51      'aria-label',
52      on ? 'Disattiva audio' : 'Attiva audio',
53    );
54    toggleBtn.textContent = on ? 'Audio OFF' : 'Audio ON';
55  }
56
57  function hideTooltip() {
58    if (!tooltip) return;
59    tooltip.classList.remove('is-visible');
60    tooltip.style.transform = 'translate3d(-9999px, -9999px, 0)';
61  }
62
63  function maybeShowTooltip() {
64    if (!tooltip || !canShowHoverTooltip) return;
65    if (!isHovering) return;
66    if (hasEverEnabledAudio) return;
67    tooltip.classList.add('is-visible');
68  }
69
70  function setTooltipPosition(clientX, clientY) {
71    if (!tooltip) return;
72
73    // Always anchor the tooltip top-right of the cursor.
74    // We intentionally do NOT clamp X, otherwise it will "stick" to an edge.
75    const offsetX = 16;
76    const offsetY = 16;
77
78    const rect = tooltip.getBoundingClientRect();
79    const vh = window.innerHeight;
80
81    let x = clientX + offsetX;
82    let y = clientY - rect.height - offsetY;
83
84    // If it would go above the viewport, place it below the cursor instead.
85    if (y < 8) {
86      y = clientY + offsetY;
87    }
88
89    // Keep Y within viewport so it remains readable.
90    if (y + rect.height + 8 > vh) y = vh - rect.height - 8;
91    if (y < 8) y = 8;
92
93    tooltip.style.transform = `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)`;
94  }
95
96  async function enableAudioFromGesture() {
97    try {
98      // Unmute + ensure volume before resuming.
99      video.muted = false;
100      video.volume = 1;
101
102      const p = video.play();
103      if (p && typeof p.then === 'function') {
104        await p;
105      }
106
107      userEnabledVideoAudio = true;
108      hasEverEnabledAudio = true;
109      updateToggleUi();
110      hideTooltip();
111      heroSection.removeEventListener('pointerdown', onHeroGesture);
112      heroSection.removeEventListener('click', onHeroGesture);
113      return true;
114    } catch {
115      // If enabling audio fails, revert to muted autoplay-safe state.
116      video.muted = true;
117      updateToggleUi();
118      return false;
119    }
120  }
121
122  function disableAudio() {
123    try {
124      video.muted = true;
125    } catch {
126      // ignore
127    }
128    userEnabledVideoAudio = false;
129    updateToggleUi();
130  }
131
132  // Enable on first click anywhere in hero (until it has played once)
133  function onHeroGesture() {
134    if (hasEverEnabledAudio || isAudioEnabled()) {
135      updateToggleUi();
136      hideTooltip();
137      return;
138    }
139    enableAudioFromGesture();
140  }
141
142  // Desktop only: allow enabling audio by clicking anywhere in the hero.
143  // Mobile: only the top-right button enables/disables audio.
144  if (canShowHoverTooltip) {
145    heroSection.addEventListener('pointerdown', onHeroGesture);
146    heroSection.addEventListener('click', onHeroGesture);
147  }
148
149  // Toggle button
150  if (toggleBtn) {
151    toggleBtn.addEventListener('pointerdown', (e) => e.stopPropagation());
152    toggleBtn.addEventListener('click', async (e) => {
153      e.stopPropagation();
154
155      if (isAudioEnabled()) {
156        disableAudio();
157        return;
158      }
159
160      const ok = await enableAudioFromGesture();
161      if (!ok) {
162        // If play is blocked for any reason, keep UI consistent
163        disableAudio();
164      }
165    });
166  }
167
168  // Hover tooltip following cursor (desktop only, until first play)
169  if (canShowHoverTooltip && tooltip) {
170    heroSection.addEventListener('mouseenter', () => {
171      isHovering = true;
172      maybeShowTooltip();
173    });
174
175    heroSection.addEventListener('mouseleave', () => {
176      isHovering = false;
177      hideTooltip();
178    });
179
180    heroSection.addEventListener('mousemove', (e) => {
181      if (hasEverEnabledAudio) {
182        hideTooltip();
183        return;
184      }
185      setTooltipPosition(e.clientX, e.clientY);
186      maybeShowTooltip();
187    });
188  }
189
190  updateToggleUi();
191})();
192
193// Shared helper: prepare animation data and load a lottie instance
194function prepareAnimData(animData) {
195  // Mark fonts as loaded externally so lottie doesn't wait
196  if (animData.fonts && animData.fonts.list) {
197    animData.fonts.list.forEach(function (f) {
198      f.fOrigin = 1;
199      f.fPath = '';
200    });
201  }
202
203  // Strip expression-based text animators & effects that lottie-web can't evaluate
204  if (animData.layers) {
205    animData.layers.forEach(function (layer) {
206      if (layer.ty === 5) {
207        if (layer.t && layer.t.a) layer.t.a = [];
208        if (layer.ef) layer.ef = [];
209      }
210    });
211  }
212
213  return animData;
214}
215
216function loadLottieInstance(container, animData, svgAlign) {
217  var result = { anim: null, totalFrames: 0 };
218  result.anim = lottie.loadAnimation({
219    container: container,
220    renderer: 'svg',
221    loop: true,
222    autoplay: false,
223    animationData: prepareAnimData(animData),
224  });
225  result.anim.addEventListener('DOMLoaded', function () {
226    result.totalFrames = result.anim.totalFrames;
227    result.anim.goToAndStop(0, true);
228    // Pin SVG alignment so it sticks to the correct edge on narrow screens
229    if (svgAlign) {
230      var svg = container.querySelector('svg');
231      if (svg) svg.setAttribute('preserveAspectRatio', svgAlign);
232    }
233
234    // If the loader has already finished, ensure this animation actually plays.
235    // This avoids a race where play() happens before DOMLoaded, then goToAndStop()
236    // fires later and leaves the animation paused.
237    if (heroAnimationsShouldPlay) {
238      result.anim.play();
239    }
240  });
241  return result;
242}
243
244// Animation JSON paths (used when fetching in AEM mode).
245// These are relative paths; aem-paths.sh converts them for AEM.
246var heroAnimJsonPaths = {
247  __heroTopAnimData: '/content/dam/renewrome/animazioni-json/hero_top.json',
248  __heroBottomAnimData: '/content/dam/renewrome/animazioni-json/hero_bottom.json',
249  __heroTopAnimDataMobile: '/content/dam/renewrome/animazioni-json/hero_top_mobile.json',
250  __heroBottomAnimDataMobile: '/content/dam/renewrome/animazioni-json/hero_bottom_mobile.json',
251};
252
253function initHeroLottie(containerId, globalKey, svgAlign) {
254  var container = document.getElementById(containerId);
255  if (!container || typeof lottie === 'undefined') return null;
256
257  // Local mode: inlined data already available as a window global
258  if (window[globalKey]) {
259    return loadLottieInstance(container, window[globalKey], svgAlign);
260  }
261
262  // AEM mode: fetch JSON file at runtime (no CORS issue on same origin)
263  var jsonPath = heroAnimJsonPaths[globalKey];
264  if (!jsonPath) return null;
265
266  // Return a placeholder that will be populated when fetch completes
267  var result = { anim: null, totalFrames: 0 };
268  fetch(jsonPath)
269    .then(function (res) {
270      return res.json();
271    })
272    .then(function (animData) {
273      var loaded = loadLottieInstance(container, animData, svgAlign);
274      result.anim = loaded.anim;
275      // Update totalFrames once DOMLoaded fires inside loadLottieInstance
276      loaded.anim.addEventListener('DOMLoaded', function () {
277        result.totalFrames = loaded.totalFrames;
278      });
279    })
280    .catch(function (err) {
281      console.warn('Lottie fetch failed for ' + globalKey + ':', err);
282    });
283  return result;
284}
285
286// Hero Lottie animations (scroll-synced)
287function getHeroAnimKey(desktopKey, mobileKey) {
288  return compactQuery.matches ? mobileKey : desktopKey;
289}
290
291function destroyHeroLottie(instance, containerId) {
292  if (
293    instance &&
294    instance.anim &&
295    typeof instance.anim.destroy === 'function'
296  ) {
297    instance.anim.destroy();
298  }
299  var container = document.getElementById(containerId);
300  if (container) {
301    container.innerHTML = '';
302  }
303}
304
305var heroTopKey = getHeroAnimKey('__heroTopAnimData', '__heroTopAnimDataMobile');
306var heroBottomKey = getHeroAnimKey(
307  '__heroBottomAnimData',
308  '__heroBottomAnimDataMobile',
309);
310
311var heroTop = initHeroLottie('hero-top-lottie', heroTopKey, 'xMidYMin meet');
312var heroBottom = initHeroLottie(
313  'hero-bottom-lottie',
314  heroBottomKey,
315  'xMidYMax meet',
316);
317
318function reinitHeroLottiesForBreakpoint() {
319  var nextTopKey = getHeroAnimKey(
320    '__heroTopAnimData',
321    '__heroTopAnimDataMobile',
322  );
323  var nextBottomKey = getHeroAnimKey(
324    '__heroBottomAnimData',
325    '__heroBottomAnimDataMobile',
326  );
327
328  var needsReinit =
329    nextTopKey !== heroTopKey || nextBottomKey !== heroBottomKey;
330
331  if (!needsReinit) return;
332
333  destroyHeroLottie(heroTop, 'hero-top-lottie');
334  destroyHeroLottie(heroBottom, 'hero-bottom-lottie');
335
336  heroTopKey = nextTopKey;
337  heroBottomKey = nextBottomKey;
338
339  heroTop = initHeroLottie('hero-top-lottie', heroTopKey, 'xMidYMin meet');
340  heroBottom = initHeroLottie(
341    'hero-bottom-lottie',
342    heroBottomKey,
343    'xMidYMax meet',
344  );
345
346  if (heroAnimationsShouldPlay) {
347    startHeroAnimations();
348  }
349}
350
351let videoDuration = 0;
352let metadataReady = false;
353let downloadComplete = false;
354let videoFps = 24;
355let allowSrcSwap = true;
356let preloadRunId = 0;
357let activePreloadAbortController = null;
358function setHeroVisible(isVisible) {
359  document.documentElement.classList.toggle(
360    'video-playing',
361    Boolean(isVisible),
362  );
363}
364
365// Hide header while hero is visible; show it once the hero is out of view.
366if (heroSection) {
367  setHeroVisible(true);
368
369  if (typeof IntersectionObserver !== 'undefined') {
370    const heroObserver = new IntersectionObserver(
371      (entries) => {
372        const entry = entries[0];
373        // Consider the hero visible if any part intersects the viewport.
374        setHeroVisible(Boolean(entry && entry.isIntersecting));
375      },
376      { threshold: 0.01 },
377    );
378
379    heroObserver.observe(heroSection);
380  } else {
381    const onScroll = () => {
382      const rect = heroSection.getBoundingClientRect();
383      const isVisible = rect.bottom > 0 && rect.top < window.innerHeight;
384      setHeroVisible(isVisible);
385    };
386
387    window.addEventListener('scroll', onScroll, { passive: true });
388    onScroll();
389  }
390}
391
392function startHeroAnimations() {
393  heroAnimationsShouldPlay = true;
394  [heroTop, heroBottom].forEach(function (h) {
395    if (h && h.anim) {
396      h.anim.play();
397    }
398  });
399}
400
401function hidePreloadOverlay() {
402  if (preloadOverlay) {
403    preloadOverlay.classList.add('hidden');
404  }
405  document.documentElement.classList.remove('video-locked');
406  startHeroAnimations();
407}
408
409function tryAutoPlay() {
410  if (!video) {
411    return;
412  }
413  video.loop = true;
414  video.muted = true;
415  const playPromise = video.play();
416  if (playPromise && typeof playPromise.catch === 'function') {
417    playPromise.catch(() => {
418      // Autoplay can be blocked by browser policies; keep video ready for user interaction.
419    });
420  }
421}
422
423// Restart hero Lottie animations from frame 0 on each video loop.
424(function initVideoLoopRestart() {
425  let lastVideoTime = 0;
426  video.addEventListener('timeupdate', () => {
427    const current = video.currentTime;
428    // A significant backwards jump in currentTime signals a loop wrap.
429    if (current < lastVideoTime - 0.5) {
430      [heroTop, heroBottom].forEach(function (h) {
431        if (h && h.anim) {
432          h.anim.stop();
433          h.anim.goToAndStop(0, true);
434          h.anim.play();
435        }
436      });
437    }
438    lastVideoTime = current;
439  });
440})();
441
442// Once playback actually starts, never swap src again (avoids mid-play restarts).
443video.addEventListener(
444  'playing',
445  () => {
446    allowSrcSwap = false;
447  },
448  { once: true },
449);
450
451video.addEventListener('loadedmetadata', () => {
452  videoDuration = video.duration;
453  metadataReady = true;
454  const fpsAttr = Number(video.dataset.fps);
455  if (Number.isFinite(fpsAttr) && fpsAttr > 0) {
456    videoFps = fpsAttr;
457  }
458  if (downloadComplete) {
459    hidePreloadOverlay();
460    tryAutoPlay();
461  }
462});
463
464video.addEventListener(
465  'canplay',
466  () => {
467    hidePreloadOverlay();
468    tryAutoPlay();
469  },
470  { once: true },
471);
472
473// Also re-init animations if the breakpoint changes (e.g. rotate device).
474if (typeof compactQuery.addEventListener === 'function') {
475  compactQuery.addEventListener('change', reinitHeroLottiesForBreakpoint);
476} else if (typeof compactQuery.addListener === 'function') {
477  compactQuery.addListener(reinitHeroLottiesForBreakpoint);
478}
479
480function getVideoUrl() {
481  if (compactQuery.matches && video.dataset.srcMobile) {
482    return video.dataset.srcMobile;
483  }
484  return video.dataset.src;
485}
486
487async function getTotalBytes(url) {
488  try {
489    const head = await fetch(url, {
490      method: 'HEAD',
491      signal: activePreloadAbortController
492        ? activePreloadAbortController.signal
493        : undefined,
494    });
495    const length = Number(head.headers.get('Content-Length')) || 0;
496    if (head.ok && length > 0) {
497      return length;
498    }
499  } catch (error) {
500    // Ignore and fall back to range.
501  }
502
503  try {
504    const range = await fetch(url, {
505      headers: {
506        Range: 'bytes=0-0',
507      },
508      signal: activePreloadAbortController
509        ? activePreloadAbortController.signal
510        : undefined,
511    });
512    const contentRange = range.headers.get('Content-Range');
513    if (!range.ok || !contentRange) {
514      return 0;
515    }
516    const parts = contentRange.split('/');
517    const total = Number(parts[1]) || 0;
518    return total;
519  } catch (error) {
520    return 0;
521  }
522}
523
524async function preloadVideo() {
525  const runId = ++preloadRunId;
526  if (activePreloadAbortController) {
527    activePreloadAbortController.abort();
528  }
529  activePreloadAbortController = new AbortController();
530
531  const videoUrl = getVideoUrl();
532  if (!videoUrl) {
533    return;
534  }
535
536  video.dataset.activeSrc = videoUrl;
537
538  try {
539    const totalBytes = await getTotalBytes(videoUrl);
540    if (runId !== preloadRunId) {
541      return;
542    }
543
544    const response = await fetch(videoUrl, {
545      signal: activePreloadAbortController.signal,
546    });
547    if (!response.ok || !response.body) {
548      throw new Error('Fetch failed');
549    }
550
551    const reader = response.body.getReader();
552    const chunks = [];
553    let received = 0;
554
555    while (true) {
556      const { done, value } = await reader.read();
557      if (done) {
558        break;
559      }
560
561      if (runId !== preloadRunId) {
562        return;
563      }
564
565      chunks.push(value);
566      received += value.length;
567      if (totalBytes > 0) {
568        const percent = Math.min(Math.floor((received / totalBytes) * 100), 99);
569        if (preloadPercent) {
570          preloadPercent.textContent = `${percent}%`;
571        }
572      }
573    }
574
575    if (runId !== preloadRunId) {
576      return;
577    }
578
579    const blob = new Blob(chunks, { type: 'video/mp4' });
580    const objectUrl = URL.createObjectURL(blob);
581    video.src = objectUrl;
582    video.load();
583    if (preloadPercent) {
584      preloadPercent.textContent = '100%';
585    }
586    downloadComplete = true;
587    if (metadataReady) {
588      hidePreloadOverlay();
589      tryAutoPlay();
590    }
591  } catch (error) {
592    if (error && (error.name === 'AbortError' || error.code === 20)) {
593      return;
594    }
595
596    video.src = videoUrl;
597    video.load();
598
599    const updateBufferedPercent = () => {
600      if (
601        !metadataReady ||
602        videoDuration === 0 ||
603        video.buffered.length === 0
604      ) {
605        return;
606      }
607      const lastIndex = video.buffered.length - 1;
608      const end = video.buffered.end(lastIndex);
609      const ratio = Math.min(end / videoDuration, 1);
610      const percent = Math.floor(ratio * 100);
611      if (preloadPercent) {
612        preloadPercent.textContent = `${percent}%`;
613      }
614    };
615
616    video.addEventListener('progress', updateBufferedPercent);
617    video.addEventListener('loadedmetadata', updateBufferedPercent);
618    video.addEventListener('canplaythrough', () => {
619      if (preloadPercent) {
620        preloadPercent.textContent = '100%';
621      }
622      downloadComplete = true;
623      hidePreloadOverlay();
624      tryAutoPlay();
625    });
626  }
627}
628
629function onCompactQueryChange() {
630  if (!allowSrcSwap) {
631    return;
632  }
633  const nextUrl = getVideoUrl();
634  if (nextUrl && nextUrl !== video.dataset.activeSrc) {
635    downloadComplete = false;
636    metadataReady = false;
637    preloadVideo();
638  }
639}
640
641if (typeof compactQuery.addEventListener === 'function') {
642  compactQuery.addEventListener('change', onCompactQueryChange);
643} else if (typeof compactQuery.addListener === 'function') {
644  compactQuery.addListener(onCompactQueryChange);
645}
646
647preloadVideo();

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.