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.