1(() => { 2 'use strict'; 3 4 const shell = document.getElementById('hero-demo-shell'); 5 if (!shell) return; 6 7 const preview = document.getElementById('hero-demo-preview'); 8 const host = document.getElementById('hero-demo-frame-host'); 9 const launch = document.getElementById('hero-demo-launch'); 10 const expand = document.getElementById('hero-demo-expand'); 11 const close = document.getElementById('hero-demo-close'); 12 const state = document.getElementById('hero-demo-state'); 13 const status = document.getElementById('hero-demo-status'); 14 let frame = null; 15 let documentObserver = null; 16 let sizeObserver = null; 17 let pausedHomepageVideos = []; 18 19 const pauseHomepageVideos = () => { 20 // Free video decoding/rendering work for the live simulation, while 21 // preserving videos the visitor had already paused or finished. 22 pausedHomepageVideos = Array.from(document.querySelectorAll('video')) 23 .filter(video => !video.paused && !video.ended); 24 pausedHomepageVideos.forEach(video => video.pause()); 25 }; 26 27 const resumeHomepageVideos = () => { 28 const videos = pausedHomepageVideos; 29 pausedHomepageVideos = []; 30 if (document.hidden) return; 31 videos.forEach(video => { 32 if (!video.isConnected || video.ended || !video.paused) return; 33 try { 34 // Browsers may deny playback after a tab or media state change. 35 // Such a denial should never interrupt closing the simulation. 36 video.play()?.catch(() => {}); 37 } catch { /* Keep the video's normal playback controls available. */ } 38 }); 39 }; 40 41 const setStatus = (value, message) => { 42 state.dataset.state = value; 43 state.hidden = value !== 'loading' && value !== 'error'; 44 if (status.textContent !== message) status.textContent = message; 45 host.setAttribute('aria-busy', String(value === 'loading')); 46 }; 47 48 const resizeFrame = (height) => { 49 if (!frame || !Number.isFinite(height)) return; 50 // The desktop embed has its own scrolling controls. Only the narrow, 51 // single-column layout needs document height; reading desktop scrollHeight 52 // would create a feedback loop with the embed's viewport-relative layout. 53 if (frame.getBoundingClientRect().width <= 760) { 54 const pixels = Math.ceil(Math.max(640, Math.min(height, 4800))); 55 frame.style.height = `${pixels}px`; 56 } else { 57 frame.style.removeProperty('height'); 58 } 59 }; 60 61 const disconnectObservers = () => { 62 documentObserver?.disconnect(); 63 sizeObserver?.disconnect(); 64 documentObserver = null; 65 sizeObserver = null; 66 }; 67 68 const observeLoadedDocument = () => { 69 disconnectObservers(); 70 if (!frame) return; 71 // Read-only fallback for a same-origin embed. No app state, policy, 72 // controller, worker, or simulation methods are accessed here. 73 try { 74 const doc = frame.contentDocument; 75 if (!doc?.body || !doc.querySelector('#simulation')) { 76 setStatus('error', 'Could not load the demo. Close it and try again.'); 77 return; 78 } 79 const loadingCard = doc.getElementById('loading-card'); 80 const sync = () => { 81 if (loadingCard?.classList.contains('error')) { 82 setStatus('error', 'The simulation could not start. Try opening it in a new tab.'); 83 } else if (loadingCard?.hidden) { 84 setStatus('ready', 'Ready to interact'); 85 } else { 86 setStatus('loading', 'Loading robot and policyâ¦'); 87 } 88 if (frame?.getBoundingClientRect().width <= 760) { 89 resizeFrame(Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight)); 90 } 91 }; 92 if (loadingCard) { 93 documentObserver = new MutationObserver(sync); 94 documentObserver.observe(loadingCard, { attributes: true, attributeFilter: ['hidden', 'class'] }); 95 } 96 if (typeof ResizeObserver !== 'undefined') { 97 sizeObserver = new ResizeObserver(() => { 98 if (frame?.getBoundingClientRect().width <= 760) resizeFrame(doc.body.scrollHeight); 99 }); 100 sizeObserver.observe(doc.body); 101 } 102 sync(); 103 } catch { 104 // A cross-origin navigation cannot be inspected; leave the standalone 105 // link and explicit close control available without changing the session. 106 setStatus('error', 'Open the demo in a new tab to continue.'); 107 } 108 }; 109 110 // --- Presentation only: entrance reveal, pointer tilt, and whole-card 111 // launch. Nothing here touches the simulation or its browsing context. --- 112 const section = document.getElementById('interactive-demo'); 113 const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 114 const finePointer = window.matchMedia('(hover: hover) and (pointer: fine)').matches; 115 116 if (section) { 117 section.classList.add('has-reveal'); 118 if (reduceMotion || typeof IntersectionObserver === 'undefined') { 119 section.classList.add('is-inview'); 120 } else { 121 const revealer = new IntersectionObserver((entries) => { 122 if (!entries.some(entry => entry.isIntersecting)) return; 123 section.classList.add('is-inview'); 124 revealer.disconnect(); 125 }, { threshold: 0.15 }); 126 revealer.observe(section); 127 } 128 } 129 130 let tiltFrame = 0; 131 let pointer = null; 132 const setTilt = (px, py) => { 133 const maxDeg = 2.4; 134 shell.style.setProperty('--hd-px', px.toFixed(3)); 135 shell.style.setProperty('--hd-py', py.toFixed(3)); 136 shell.style.setProperty('--hd-tilt-x', `${((py - 0.5) * 2 * maxDeg).toFixed(2)}deg`); 137 shell.style.setProperty('--hd-tilt-y', `${((0.5 - px) * 2 * maxDeg).toFixed(2)}deg`); 138 }; 139 const resetTilt = () => { 140 if (tiltFrame) cancelAnimationFrame(tiltFrame); 141 tiltFrame = 0; 142 pointer = null; 143 shell.classList.remove('is-tilting'); 144 setTilt(0.5, 0.5); 145 }; 146 if (finePointer && !reduceMotion) { 147 preview.addEventListener('pointerenter', () => shell.classList.add('is-tilting')); 148 preview.addEventListener('pointermove', (event) => { 149 pointer = event; 150 if (tiltFrame) return; 151 tiltFrame = requestAnimationFrame(() => { 152 tiltFrame = 0; 153 if (!pointer) return; 154 const rect = preview.getBoundingClientRect(); 155 if (!rect.width || !rect.height) return; 156 setTilt( 157 Math.min(1, Math.max(0, (pointer.clientX - rect.left) / rect.width)), 158 Math.min(1, Math.max(0, (pointer.clientY - rect.top) / rect.height)), 159 ); 160 }); 161 }, { passive: true }); 162 preview.addEventListener('pointerleave', resetTilt); 163 } 164 165 // The whole preview card is a launch affordance. The button stays the single 166 // focusable control and keeps its link semantics; clicks elsewhere defer to it. 167 preview.addEventListener('click', (event) => { 168 if (event.target.closest('a, button')) return; 169 launch.click(); 170 }); 171 172 launch.addEventListener('click', (event) => { 173 // Small screens use the standalone layout and its Back to HERO link. 174 // Modifier-clicks and JavaScript-free navigation keep normal link behavior. 175 if (window.innerWidth <= 760 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; 176 event.preventDefault(); 177 if (frame) return; 178 // The existing floating contents panel sits over the demo's controls. 179 // Use its own toggle so its open-state bookkeeping stays consistent. 180 if (document.getElementById('tocPanel')?.classList.contains('open')) { 181 document.getElementById('tocToggle')?.click(); 182 } 183 pauseHomepageVideos(); 184 setStatus('loading', 'Loading simulatorâ¦'); 185 preview.hidden = true; 186 resetTilt(); 187 host.hidden = false;
188 expand.hidden = false; 189 close.hidden = false; 190 frame = document.createElement('iframe'); 191 frame.id = 'hero-tabletop-frame'; 192 frame.title = 'HERO interactive tabletop grasping simulation'; 193 frame.allow = 'fullscreen'; 194 frame.allowFullscreen = true; 195 frame.addEventListener('load', observeLoadedDocument); 196 frame.addEventListener('error', () => setStatus('error', 'Could not load the demo. Close it and try again.')); 197 // Deliberately assigned only after this explicit user action. There is no 198 // hidden iframe, preload, fetch, or simulation asset request on page load. 199 frame.src = './demo/index.html?embed=1'; 200 host.appendChild(frame); 201 expand.focus({ preventScroll: true }); 202 }); 203 204 expand.addEventListener('click', () => { 205 const expanded = shell.classList.toggle('is-expanded'); 206 expand.setAttribute('aria-expanded', String(expanded)); 207 expand.querySelector('[data-expand-label]').textContent = expanded ? 'Compact view' : 'Expand view'; 208 // Keep the existing browsing context and simulation session intact. 209 if (frame && frame.getBoundingClientRect().width > 760) frame.style.removeProperty('height'); 210 }); 211 212 close.addEventListener('click', () => { 213 disconnectObservers(); 214 frame?.remove(); 215 frame = null; 216 host.hidden = true; 217 preview.hidden = false; 218 expand.hidden = true; 219 close.hidden = true; 220 shell.classList.remove('is-expanded'); 221 expand.setAttribute('aria-expanded', 'false'); 222 expand.querySelector('[data-expand-label]').textContent = 'Expand view'; 223 setStatus('idle', 'Session closed · launch to start again'); 224 resumeHomepageVideos(); 225 launch.focus({ preventScroll: true }); 226 }); 227 228 window.addEventListener('message', (event) => { 229 if (!frame || event.source !== frame.contentWindow || event.origin !== window.location.origin) return; 230 const message = event.data; 231 if (!message || typeof message !== 'object' || message.source !== 'hero-tabletop-demo') return; 232 if (message.type === 'ready') setStatus('ready', 'Ready to interact'); 233 if (message.type === 'loading') setStatus('loading', 'Loading robot and policyâ¦'); 234 if (message.type === 'error') setStatus('error', 'The simulation could not start. Try opening it in a new tab.'); 235 if (message.type === 'resize') resizeFrame(message.height); 236 }); 237 238 window.addEventListener('resize', () => { 239 if (frame && frame.getBoundingClientRect().width > 760) frame.style.removeProperty('height'); 240 }, { passive: true }); 241})();
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.