1/* Film â 10s final-frame hold â Amy-only fade â campaign diagonal reveal. */ 2(() => { 3 const host = document.querySelector('[data-amy-reveal]'); 4 if (!host) return; 5 const video = host.querySelector('[data-amy-reveal-video]'); 6 const toggle = host.querySelector('[data-amy-reveal-toggle]'); 7 const media = host.querySelector('.amy-reveal__media'); 8 const root = host.closest('[data-yta-marketing]'); 9 const reduce = matchMedia('(prefers-reduced-motion: reduce)'); 10 const mobile = matchMedia('(max-width: 600px)'); 11 const connection = navigator.connection; 12 const assetBase = new URL('.', document.currentScript.src); 13 const overlay = document.createElement('div'); 14 overlay.className = 'yta-glaze'; 15 overlay.setAttribute('aria-hidden', 'true'); 16 const plate = document.createElement('img'); 17 plate.className = 'yta-glaze__plate'; 18 plate.alt = ''; 19 const panel = document.createElement('div'); 20 panel.className = 'yta-glaze__panel'; 21 overlay.append(plate, panel); 22 media.append(overlay); 23 const setPlate = () => { 24 plate.src = new URL(mobile.matches ? 'amy-background-mobile-20260913.webp' : 'amy-background-desktop-20260913.webp', assetBase).href; 25 }; 26 setPlate(); 27 28 let phase = 'idle'; 29 let elapsed = 0; 30 let visible = false; 31 let userPaused = false; 32 let manualPlay = false; 33 let pending = false; 34 let generation = 0; 35 let raf = 0; 36 let lastFrame = 0; 37 const quiet = () => reduce.matches || connection?.saveData; 38 const blocked = () => document.hidden || !visible || userPaused || 39 root.classList.contains('yta-motion-paused') || (quiet() && !manualPlay); 40 function label() { 41 const action = userPaused || (phase === 'idle' && !pending) ? 'Play' : 'Pause'; 42 toggle.textContent = action + ' intro'; 43 toggle.setAttribute('aria-label', action + ' Amy introduction'); 44 } 45 function stop() { 46 generation += 1; 47 pending = false; 48 cancelAnimationFrame(raf); 49 raf = 0; 50 lastFrame = 0; 51 video.pause(); 52 label(); 53 } 54 function poster() { 55 stop(); 56 phase = 'idle'; 57 elapsed = 0; 58 plate.style.opacity = '0'; 59 panel.style.visibility = 'hidden'; 60 host.classList.remove('is-showing-video'); 61 label(); 62 } 63 function schedule() { 64 if (!raf && !blocked() && ['hold', 'fade', 'wipe'].includes(phase)) { 65 raf = requestAnimationFrame(tick); 66 } 67 } 68 function tick(now) { 69 raf = 0; 70 if (blocked()) { lastFrame = 0; return; } 71 // Background tabs and offscreen/paused time never count toward the hold. 72 elapsed += lastFrame ? Math.min(now - lastFrame, 100) : 0; 73 lastFrame = now; 74 if (phase === 'hold' && elapsed >= 10000) { 75 // Failed plate download must not flash an empty or broken-image frame. 76 if (!plate.complete || !plate.naturalWidth) { userPaused = true; label(); return; } 77 phase = 'fade'; 78 elapsed = 0; 79 } 80 if (phase === 'fade') { 81 const progress = Math.min(elapsed / 1000, 1); 82 plate.style.opacity = String(progress * progress * (3 - 2 * progress)); 83 if (progress === 1) { 84 lastFrame = 0; 85 if (!pending) play(true); 86 return; 87 } 88 } 89 if (phase === 'wipe') { 90 // Exact social Wipe: 24 frames, -18deg, 70%-wide coral/navy, mint edge. 91 const progress = Math.min(elapsed / 800, 1); 92 const left = progress < .5 ? -40 + progress * 130 : 25 + (progress - .5) * 250; 93 panel.style.left = left + '%'; 94 panel.style.backgroundColor = progress < .5 ? '#ff785d' : '#020b24'; 95 panel.style.visibility = 'visible'; 96 plate.style.opacity = progress < .5 && plate.complete && plate.naturalWidth ? '1' : '0'; 97 if (progress === 1) { 98 panel.style.visibility = 'hidden'; 99 phase = 'video'; 100 elapsed = 0; 101 lastFrame = 0; 102 } 103 } 104 schedule(); 105 } 106 async function play(restart = false) { 107 if (blocked() || pending) return; 108 const request = ++generation; 109 pending = true; 110 label(); 111 if (!video.getAttribute('src')) { 112 video.src = mobile.matches ? video.dataset.mobileSrc : video.dataset.desktopSrc; 113 } 114 if (restart || video.ended) video.currentTime = 0; 115 try { 116 await video.play(); 117 if (request !== generation || blocked()) return; 118 host.classList.add('is-showing-video'); 119 if (restart || phase === 'idle') { 120 phase = quiet() ? 'video' : 'wipe';
121 elapsed = 0; 122 lastFrame = 0; 123 plate.style.opacity = quiet() ? '0' : plate.style.opacity; 124 } 125 schedule(); 126 } catch { 127 if (request === generation) { userPaused = true; poster(); } 128 } finally { 129 if (request === generation) pending = false; 130 label(); 131 } 132 } 133 function update() { 134 if (blocked()) { stop(); return; } 135 if (phase === 'idle') play(true); 136 else if (phase === 'video' || phase === 'wipe') { 137 if (video.paused) play(); 138 else schedule(); 139 } else schedule(); 140 } 141 toggle.hidden = false; 142 label(); 143 toggle.addEventListener('click', () => { 144 if (!userPaused && (pending || phase !== 'idle')) { 145 userPaused = true; 146 stop(); 147 } else { 148 userPaused = false; 149 manualPlay = true; 150 update(); 151 } 152 }); 153 video.addEventListener('ended', () => { 154 cancelAnimationFrame(raf); 155 raf = 0; 156 panel.style.visibility = 'hidden'; 157 plate.style.opacity = '0'; 158 // Keep the real final video frame visible, rather than reverting to its poster. 159 phase = 'hold'; 160 elapsed = 0; 161 lastFrame = 0; 162 if (quiet()) { userPaused = true; poster(); } 163 else { label(); schedule(); } 164 }); 165 video.addEventListener('error', () => { userPaused = true; poster(); }); 166 new IntersectionObserver(entries => { 167 visible = entries[0].isIntersecting && entries[0].intersectionRatio >= .15; 168 update(); 169 }, {threshold: .15}).observe(host); 170 document.addEventListener('visibilitychange', update); 171 new MutationObserver(update).observe(root, {attributes: true, attributeFilter: ['class']}); 172 function preferenceChange() { 173 manualPlay = false; 174 if (quiet()) poster(); 175 else update(); 176 } 177 reduce.addEventListener('change', preferenceChange); 178 connection?.addEventListener('change', preferenceChange); 179 mobile.addEventListener('change', () => { 180 poster(); 181 video.removeAttribute('src'); 182 video.load(); 183 setPlate(); 184 update(); 185 }); 186})();
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.