PageSourceSearch

https://hly-123.github.io/HiFun/script.js?v=6e7f8b5c3643

js hly-123.github.io collected 2026-10-03 09:17:59 UTC 10,758 bytes, 260 lines download raw bytes

1'use strict';
2
3// Videos autoplay only while visible, unless reduced motion is enabled.
4const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
5const videoStates = new Map();
6const visibilityObserver = new IntersectionObserver(entries => {
7  for (const entry of entries) {
8    const video = entry.target;
9    const state = videoStates.get(video);
10    state.visible = entry.isIntersecting && entry.intersectionRatio >= 0.15;
11    if (!state.visible) pauseOffscreen(video, state);
12    else if (state.wantsPlayback && !document.hidden) attemptPlay(video, state);
13  }
14}, {threshold: [0, 0.15, 0.5]});
15
16async function loadVideo(video) {
17  const source = video.querySelector('source[data-src]');
18  if (source && !source.hasAttribute('src')) {
19    // Pages serves full MP4 responses without byte ranges. A local Blob gives
20    // native video controls a seekable source, while keeping the site static.
21    if (location.hostname.endsWith('.pages.dev')) {
22      const response = await fetch(source.dataset.src);
23      if (!response.ok) throw new Error('Video download failed');
24      const blob = await response.blob();
25      video.src = URL.createObjectURL(blob);
26    }
27    source.src = source.dataset.src;
28    video.load();
29  }
30}
31
32async function attemptPlay(video, state) {
33  if (state.pending || !video.paused) return;
34  state.pending = true;
35  const button = state.frame.querySelector('.video-start');
36  const label = button.innerHTML;
37  button.textContent = 'Loading video…';
38  button.disabled = true;
39  try {
40    await loadVideo(video);
41    if (!state.visible || document.hidden || !state.wantsPlayback) return;
42    await video.play();
43    if (!state.visible || document.hidden) pauseOffscreen(video, state);
44  } catch (error) {
45    // Autoplay restrictions leave the poster and explicit play button available.
46    if (error.name !== 'AbortError') state.frame.classList.remove('is-started');
47  } finally {
48    state.pending = false;
49    button.disabled = false;
50    button.innerHTML = label;
51  }
52}
53
54function pauseOffscreen(video, state) {
55  if (!video.paused) {
56    state.environmentPause = true;
57    video.pause();
58  }
59}
60
61document.querySelectorAll('.managed-video').forEach(video => {
62  const frame = video.closest('.media-frame');
63  const state = {frame, visible:false, wantsPlayback:video.hasAttribute('data-autoplay') && !reduceMotion.matches, pending:false, environmentPause:false};
64  videoStates.set(video, state);
65  const toggle = frame.querySelector('.video-toggle');
66  const updateToggle = () => {
67    if (!toggle) return;
68    const action = video.ended ? 'Replay' : video.paused ? 'Play' : 'Pause';
69    toggle.textContent = action;
70    toggle.setAttribute('aria-label', `${action} ${video.getAttribute('aria-label')}`);
71  };
72  if (toggle) {
73    toggle.addEventListener('click', () => {
74      state.environmentPause = false;
75      if (!video.paused) {
76        state.wantsPlayback = false;
77        video.pause();
78      } else {
79        state.wantsPlayback = true;
80        if (video.ended) video.currentTime = 0;
81        attemptPlay(video, state);
82      }
83    });
84    video.addEventListener('play', updateToggle);
85    video.addEventListener('pause', updateToggle);
86    video.addEventListener('ended', updateToggle);
87    frame.addEventListener('contextmenu', event => event.preventDefault());
88    const fullscreen = frame.querySelector('.video-fullscreen');
89    fullscreen.hidden = !frame.requestFullscreen;
90    fullscreen.addEventListener('click', async () => {
91      try {
92        if (document.fullscreenElement === frame) await document.exitFullscreen();
93        else await frame.requestFullscreen();
94      } catch (_) { /* Fullscreen may be unavailable in embedded browsers. */ }
95    });
96    updateToggle();
97  }
98  frame.querySelector('.video-start').addEventListener('click', () => {
99    state.wantsPlayback = true;
100    state.visible = true;
101    state.environmentPause = false;
102    if (video.ended) video.currentTime = 0;
103    attemptPlay(video, state);
104  });
105  video.addEventListener('playing', () => {
106    frame.classList.add('is-started');
107    state.wantsPlayback = true;
108  });
109  video.addEventListener('pause', () => {
110    if (state.environmentPause) state.environmentPause = false;
111    else state.wantsPlayback = false;
112  });
113  video.addEventListener('ended', () => {
114    state.wantsPlayback = false;
115    if (!video.hasAttribute('data-hold-last-frame')) frame.classList.remove('is-started');
116  });
117  video.addEventListener('error', () => {
118    state.wantsPlayback = false;
119    const button = frame.querySelector('.video-start');
120    frame.classList.remove('is-started');
121    button.textContent = 'Video unavailable · retry';
122    button.setAttribute('aria-label', 'Retry loading video');
123    const source = video.querySelector('source');
124    if (source) source.removeAttribute('src');
125  });
126  visibilityObserver.observe(video);
127});
128
129// Comparison clips retain their own clocks; no speed matching is applied.
130document.querySelectorAll('.play-comparison').forEach(button => {
131  const videos = [...button.closest('.intervention-comparison').querySelectorAll('video')];
132  const updateLabel = () => { button.textContent = videos.some(video => !video.paused) ? 'Pause both' : 'Play both'; };
133  videos.forEach(video => { video.addEventListener('play', updateLabel); video.addEventListener('pause', updateLabel); });
134  button.addEventListener('click', () => {
135    const pause = videos.some(video => !video.paused);
136    for (const video of videos) {
137      const state = videoStates.get(video);
138      state.wantsPlayback = !pause;
139      state.environmentPause = false;
140      if (pause) video.pause();
141      else if (state.visible) attemptPlay(video, state);
142    }
143    updateLabel();
144  });
145});
146
147document.addEventListener('visibilitychange', () => {
148  for (const [video, state] of videoStates) {
149    if (document.hidden) pauseOffscreen(video, state);
150    else if (state.visible && state.wantsPlayback) attemptPlay(video, state);
151  }
152});
153reduceMotion.addEventListener('change', () => {
154  if (!reduceMotion.matches) return;
155  for (const [video, state] of videoStates) {
156    if (video.hasAttribute('data-autoplay')) {
157      state.wantsPlayback = false;
158      pauseOffscreen(video, state);
159    }
160  }
161});
162
163const heroVideo = document.querySelector('#hero-video');
164heroVideo.addEventListener('timeupdate', () => {
165  const scene = heroVideo.currentTime < 7 ? 0 : heroVideo.currentTime < 18 ? 1 : 2;
166  document.querySelector('#hero-scene').textContent = ['Contact, leverage, retrieve', 'Align the tool, actuate the trigger', 'Lose contact, adapt, recover'][scene];
167  document.querySelector('#hero-index').textContent = `0${scene + 1} / 03`;
168});
169
170const menuButton = document.querySelector('.menu-toggle');
171const nav = document.querySelector('#nav-links');
172function closeMenu() {
173  menuButton.setAttribute('aria-expanded', 'false');
174  nav.classList.remove('is-open');
175}
176menuButton.addEventListener('click', () => {
177  const open = menuButton.getAttribute('aria-expanded') !== 'true';
178  menuButton.setAttribute('aria-expanded', String(open));
179  nav.classList.toggle('is-open', open);
180});
181nav.addEventListener('click', event => {if (event.target.closest('a')) closeMenu();});
182document.addEventListener('keydown', event => {
183  if (event.key === 'Escape' && menuButton.getAttribute('aria-expanded') === 'true') {
184    closeMenu();
185    menuButton.focus();
186  }
187});
188
189const links = [...nav.querySelectorAll('a')];
190const sections = links.map(link => document.querySelector(link.hash));
191let scrollPending = false;
192function updateScroll() {
193  const available = document.documentElement.scrollHeight - innerHeight;
194  document.querySelector('#progress').style.width = `${available > 0 ? Math.min(100, scrollY / available * 100) : 0}%`;
195  let active = -1;
196  sections.forEach((section, index) => {if (section.getBoundingClientRect().top < innerHeight * .35) active = index;});
197  links.forEach((link, index) => {
198    link.classList.toggle('active', index === active);
199    if (index === active) link.setAttribute('aria-current', 'location');
200    else link.removeAttribute('aria-current');
201  });
202  scrollPending = false;
203}
204function scheduleScroll() {if (!scrollPending) {scrollPending = true; requestAnimationFrame(updateScroll);}}
205addEventListener('scroll', scheduleScroll, {passive:true});
206addEventListener('resize', scheduleScroll);
207document.querySelectorAll('details').forEach(details => details.addEventListener('toggle', scheduleScroll));
208updateScroll();
209
210// Keep previously shared links useful after removing the standalone evidence/resources chapters.
211const legacyAnchors = {
212  insights:'challenges',
213  'coordination-evidence':'challenge-skill',
214  'value-evidence':'challenge-critic',
215  'iaw-evidence':'challenge-iaw',
216  'training-cost':'highlights',
217  resources:'citation-section'
218};
219function resolveLegacyAnchor() {
220  const replacement = legacyAnchors[location.hash.slice(1)];
221  if (replacement) location.replace('#' + replacement);
222}
223addEventListener('hashchange', resolveLegacyAnchor);
224resolveLegacyAnchor();
225
226const dialog = document.querySelector('#figure-dialog');
227let figureTrigger = null;
228document.querySelectorAll('[data-zoom]').forEach(button => button.addEventListener('click', () => {
229  figureTrigger = button;
230  const image = document.querySelector('#enlarged-figure');
231  image.src = button.dataset.zoom;
232  image.alt = button.querySelector('img').alt;
233  document.querySelector('#figure-dialog-title').textContent = button.getAttribute('aria-label').replace(/^Enlarge /, '');
234  dialog.classList.toggle('is-panorama', button.closest('.recovery-sequence') !== null);
235  dialog.querySelector('.dialog-image-scroll').scrollLeft = 0;
236  dialog.showModal();
237}));
238document.querySelector('#close-figure').addEventListener('click', () => dialog.close());
239dialog.addEventListener('click', event => {if (event.target === dialog) dialog.close();});
240dialog.addEventListener('close', () => {if (figureTrigger) figureTrigger.focus();});
241
242document.querySelector('#copy-citation').addEventListener('click', async () => {
243  const button = document.querySelector('#copy-citation');
244  const citation = document.querySelector('#citation');
245  const status = document.querySelector('#copy-status');
246  try {
247    await navigator.clipboard.writeText(citation.textContent);
248    button.textContent = 'Copied';
249    status.textContent = 'BibTeX copied to clipboard.';
250    setTimeout(() => {button.textContent = 'Copy BibTeX';}, 2000);
251  } catch {
252    const range = document.createRange();
253    range.selectNodeContents(citation);
254    const selection = getSelection();
255    selection.removeAllRanges();
256    selection.addRange(range);
257    button.textContent = 'Selected · press Ctrl/Cmd+C';
258    status.textContent = 'Citation selected. Press Control C or Command C to copy.';
259  }
260});

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.