PageSourceSearch

https://hero-humanoid.github.io/static/js/interactive-demo.js

js hero-humanoid.github.io collected 2026-10-03 09:17:32 UTC 10,072 bytes, 241 lines download raw bytes

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.