PageSourceSearch

https://hanshengchen.com/asymflow/static/js/viewer.js

js hanshengchen.com collected 2026-10-03 22:39:43 UTC 7,159 bytes, 240 lines download raw bytes

1/* viewer.js adapter for the AsymFlow project page.
2   The light-box / zoom logic below is copied from lakonlab/ui/media_viewer/viewer.js.
3   Only the thumbnail source discovery and click delegation are adapted for this page.
4*/
5
6const lightboxItems = Array.from(document.querySelectorAll('.teaser-card, .figure'))
7    .map((item, idx) => {
8        const media = item.querySelector('img');
9        const caption = item.querySelector('.caption');
10        if (!media) return null;
11        item.classList.add('item');
12        item.dataset.idx = idx;
13        return {
14            source: media.currentSrc || media.src,
15            caption: caption ? caption.textContent.trim() : media.alt,
16        };
17    })
18    .filter(Boolean);
19
20const viewerData = {
21    sources: lightboxItems.map(item => item.source),
22    captions: lightboxItems.map(item => item.caption),
23};
24
25const INITIAL_SCALE_WHEN_FITS = 1.5;
26
27const {
28    sources,
29    captions
30} = viewerData;
31
32const overlay = document.createElement('div');
33overlay.id = 'overlay';
34overlay.innerHTML = `
35  <div id="container"></div>
36  <div id="caption"></div>
37`;
38document.body.appendChild(overlay);
39
40const container = overlay.querySelector('#container');
41const captionEl = overlay.querySelector('#caption');
42
43let idx = 0;
44let isZoomed = false;
45let canZoom = false; // false for videos
46let media = null; // <img> | <video>
47let baseWidth = 0; // natural width for wheel-zoom
48let scale = 1;
49let zoomModifierDown = false;
50
51const mediaTag = src =>
52    src.toLowerCase().endsWith('.mp4') ?
53    `<video src="${src}" controls autoplay></video>` :
54    `<img src="${src}" alt="preview">`;
55
56/* Center or pin content when zoomed */
57function adjustAlignment() {
58    if (!isZoomed) {
59        container.style.alignSelf = '';
60        overlay.classList.remove('scrollY');
61        return;
62    }
63    const hOverflow = overlay.scrollWidth > overlay.clientWidth;
64    const vOverflow = overlay.scrollHeight > overlay.clientHeight;
65
66    container.style.alignSelf = hOverflow ? 'flex-start' : '';
67    overlay.classList.toggle('scrollY', vOverflow);
68}
69
70function show(i) {
71    idx = (i + sources.length) % sources.length;
72
73    container.innerHTML = mediaTag(sources[idx]);
74    captionEl.textContent = captions[idx];
75    if (window.MathJax && window.MathJax.typesetPromise) {
76        window.MathJax.typesetPromise([captionEl]);
77    }
78    overlay.style.display = 'flex';
79    document.body.style.overflow = 'hidden'; // lock background scroll
80
81    media = container.firstElementChild;
82    canZoom = media.tagName === 'IMG';
83    isZoomed = false;
84    scale = 1;
85
86    media.style.width = '';
87    media.classList.remove('zoomed');
88    overlay.classList.remove('scrollY');
89    container.style.alignSelf = '';
90
91    media.onclick = canZoom ? toggleZoom : null;
92    adjustAlignment();
93}
94
95function hide() {
96    overlay.style.display = 'none'; // make it invisible
97    document.body.style.overflow = '';
98    isZoomed = false;
99    /* container will be overwritten on next show() call */
100}
101
102function toggleZoom() {
103    if (!canZoom) return; // ignore for videos
104
105    isZoomed = !isZoomed;
106    media.classList.toggle('zoomed', isZoomed);
107
108    if (isZoomed) {
109        enterZoom(media.naturalWidth, INITIAL_SCALE_WHEN_FITS);
110    } else {
111        media.style.width = '';
112        scale = 1;
113    }
114    adjustAlignment();
115}
116
117function enterZoom(width, initialScale) {
118    baseWidth = width;
119    scale = initialScale;
120    media.style.width = (baseWidth * scale) + 'px';
121    requestAnimationFrame(centerZoomedImage);
122}
123
124function centerZoomedImage() {
125    const rect = media.getBoundingClientRect();
126    const viewportCenterX = overlay.clientWidth / 2;
127    const viewportCenterY = overlay.clientHeight / 2;
128
129    overlay.scrollLeft += rect.left + rect.width / 2 - viewportCenterX;
130    overlay.scrollTop += rect.top + rect.height / 2 - viewportCenterY;
131}
132
133function setZoomScale(nextScale) {
134    const rect = media.getBoundingClientRect();
135    const viewportCenterX = overlay.clientWidth / 2;
136    const viewportCenterY = overlay.clientHeight / 2;
137    const ratioX = rect.width > 0 ? (viewportCenterX - rect.left) / rect.width : 0.5;
138    const ratioY = rect.height > 0 ? (viewportCenterY - rect.top) / rect.height : 0.5;
139
140    scale = Math.max(0.2, Math.min(5, nextScale));
141    media.style.width = (baseWidth * scale) + 'px';
142
143    requestAnimationFrame(() => {
144        const nextRect = media.getBoundingClientRect();
145        overlay.scrollLeft += nextRect.left + ratioX * nextRect.width - viewportCenterX;
146        overlay.scrollTop += nextRect.top + ratioY * nextRect.height - viewportCenterY;
147        adjustAlignment();
148    });
149}
150
151/* Thumbnail clicks - activate only when a page image itself is clicked */
152document.addEventListener('click', e => {
153    if (!e.target.matches('.teaser-card img, .figure img')) return;
154    const idx = +e.target.closest('.item').dataset.idx;
155    show(idx);
156});
157
158/* Keyboard */
159document.addEventListener('keydown', e => {
160    if (e.key === 'Control' || e.key === 'Meta') {
161        zoomModifierDown = true;
162    }
163
164    if (overlay.style.display !== 'flex') return;
165
166    /* Esc - exit zoom first, then close overlay */
167    if (e.key === 'Escape') {
168        e.preventDefault(); // stop "Stop loading" in Chrome/Firefox
169        if (isZoomed) toggleZoom();
170        else hide();
171        return;
172    }
173
174    /* Enter toggles zoom for images */
175    if (e.key === 'Enter' && canZoom) {
176        toggleZoom();
177        return;
178    }
179
180    /* Arrow navigation disabled while zoomed */
181    if (isZoomed) return;
182    if (e.key === 'ArrowRight') show(idx + 1);
183    else if (e.key === 'ArrowLeft') show(idx - 1);
184});
185
186document.addEventListener('keyup', e => {
187    if (e.key === 'Control' || e.key === 'Meta') {
188        zoomModifierDown = false;
189    }
190});
191
192window.addEventListener('blur', () => {
193    zoomModifierDown = false;
194});
195
196/* Click backdrop to close */
197overlay.addEventListener('click', e => {
198    if (e.target === overlay) hide();
199});
200
201function handleCtrlWheel(e) {
202    if (overlay.style.display !== 'flex' || !canZoom) return;
203    if (!(e.ctrlKey || e.metaKey || zoomModifierDown)) return;
204
205    e.preventDefault();
206    e.stopPropagation();
207    if (typeof e.stopImmediatePropagation === 'function') {
208        e.stopImmediatePropagation();
209    }
210
211    if (!isZoomed) {
212        const renderedWidth = media.getBoundingClientRect().width;
213        isZoomed = true;
214        media.classList.add('zoomed');
215        enterZoom(renderedWidth, 1);
216        adjustAlignment();
217        return;
218    }
219    setZoomScale(scale + (e.deltaY < 0 ? 0.1 : -0.1));
220}
221
222/* Ctrl/Cmd + wheel zoom for images only.
223   Capture-phase handling prevents browser/page zoom from consuming the gesture. */
224window.addEventListener('wheel', handleCtrlWheel, {
225    capture: true,
226    passive: false
227});
228
229window.addEventListener('gesturestart', e => {
230    if (overlay.style.display !== 'flex') return;
231    e.preventDefault();
232}, {passive: false});
233
234window.addEventListener('gesturechange', e => {
235    if (overlay.style.display !== 'flex') return;
236    e.preventDefault();
237}, {passive: false});
238
239/* Keep centering correct on resize / browser zoom */
240window.addEventListener('resize', adjustAlignment);

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.