cgeorgiaw HF Staff commited on
Commit
c977220
·
verified ·
1 Parent(s): 8c58d6a

Add wheel-zoom and drag-pan to images

Browse files
Files changed (1) hide show
  1. app.py +68 -2
app.py CHANGED
@@ -297,8 +297,71 @@ def choose(side: str, session: dict):
297
 
298
  # ---------- UI ----------------------------------------------------------------
299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
- with gr.Blocks(title="Denoising A/B Judging", theme=gr.themes.Soft()) as demo:
302
  session_state = gr.State(_empty_session())
303
 
304
  gr.Markdown("# Denoising A/B Judging")
@@ -307,7 +370,7 @@ with gr.Blocks(title="Denoising A/B Judging", theme=gr.themes.Soft()) as demo:
307
  "denoised versions of it below, labelled **A** and **B**. The two "
308
  "versions come from different denoising approaches, presented in a "
309
  "random left/right order so the comparison stays blind. "
310
- "Click any image to zoom in.\n\n"
311
  "**Which denoised image would you rather work with?**"
312
  )
313
 
@@ -328,6 +391,7 @@ with gr.Blocks(title="Denoising A/B Judging", theme=gr.themes.Soft()) as demo:
328
  show_download_button=False,
329
  show_fullscreen_button=True,
330
  height=420,
 
331
  )
332
  with gr.Row():
333
  with gr.Column():
@@ -338,6 +402,7 @@ with gr.Blocks(title="Denoising A/B Judging", theme=gr.themes.Soft()) as demo:
338
  show_download_button=False,
339
  show_fullscreen_button=True,
340
  height=420,
 
341
  )
342
  a_btn = gr.Button("A is better", variant="primary")
343
  with gr.Column():
@@ -348,6 +413,7 @@ with gr.Blocks(title="Denoising A/B Judging", theme=gr.themes.Soft()) as demo:
348
  show_download_button=False,
349
  show_fullscreen_button=True,
350
  height=420,
 
351
  )
352
  b_btn = gr.Button("B is better", variant="primary")
353
  done_md = gr.Markdown(visible=False)
 
297
 
298
  # ---------- UI ----------------------------------------------------------------
299
 
300
+ # Wheel-zoom + drag-pan + dbl-click reset on every <img> inside a `.zoomable`
301
+ # Gradio image component. Resets zoom when the image src changes.
302
+ ZOOM_HEAD = """
303
+ <style>
304
+ .zoomable [data-testid="image"], .zoomable .image-frame, .zoomable .image-container {
305
+ overflow: hidden;
306
+ }
307
+ .zoomable img {
308
+ transform-origin: 0 0;
309
+ transition: transform 0.05s ease-out;
310
+ will-change: transform;
311
+ }
312
+ </style>
313
+ <script>
314
+ (function () {
315
+ function bind(img) {
316
+ if (img.__zoomBound) return;
317
+ img.__zoomBound = true;
318
+ let scale = 1, tx = 0, ty = 0;
319
+ let dragging = false, lastX = 0, lastY = 0;
320
+ const apply = () => { img.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`; };
321
+ const reset = () => { scale = 1; tx = 0; ty = 0; apply(); };
322
+ img.addEventListener('wheel', (e) => {
323
+ e.preventDefault();
324
+ const factor = e.deltaY < 0 ? 1.2 : 1 / 1.2;
325
+ const ns = Math.min(12, Math.max(1, scale * factor));
326
+ if (ns === scale) return;
327
+ const rect = img.getBoundingClientRect();
328
+ const ox = e.clientX - rect.left;
329
+ const oy = e.clientY - rect.top;
330
+ tx = ox - (ox - tx) * (ns / scale);
331
+ ty = oy - (oy - ty) * (ns / scale);
332
+ scale = ns;
333
+ if (scale <= 1.001) reset(); else apply();
334
+ }, { passive: false });
335
+ img.addEventListener('mousedown', (e) => {
336
+ if (scale > 1) {
337
+ dragging = true; lastX = e.clientX; lastY = e.clientY;
338
+ img.style.cursor = 'grabbing';
339
+ e.preventDefault();
340
+ }
341
+ });
342
+ window.addEventListener('mousemove', (e) => {
343
+ if (!dragging) return;
344
+ tx += e.clientX - lastX; ty += e.clientY - lastY;
345
+ lastX = e.clientX; lastY = e.clientY;
346
+ apply();
347
+ });
348
+ window.addEventListener('mouseup', () => { dragging = false; img.style.cursor = ''; });
349
+ img.addEventListener('dblclick', reset);
350
+ new MutationObserver(reset).observe(img, { attributes: true, attributeFilter: ['src'] });
351
+ }
352
+ function scan() { document.querySelectorAll('.zoomable img').forEach(bind); }
353
+ new MutationObserver(scan).observe(document.body, { childList: true, subtree: true });
354
+ if (document.readyState === 'loading') {
355
+ document.addEventListener('DOMContentLoaded', scan);
356
+ } else {
357
+ scan();
358
+ }
359
+ })();
360
+ </script>
361
+ """
362
+
363
 
364
+ with gr.Blocks(title="Denoising A/B Judging", theme=gr.themes.Soft(), head=ZOOM_HEAD) as demo:
365
  session_state = gr.State(_empty_session())
366
 
367
  gr.Markdown("# Denoising A/B Judging")
 
370
  "denoised versions of it below, labelled **A** and **B**. The two "
371
  "versions come from different denoising approaches, presented in a "
372
  "random left/right order so the comparison stays blind. "
373
+ "Scroll on an image to zoom, drag to pan, double-click to reset.\n\n"
374
  "**Which denoised image would you rather work with?**"
375
  )
376
 
 
391
  show_download_button=False,
392
  show_fullscreen_button=True,
393
  height=420,
394
+ elem_classes=["zoomable"],
395
  )
396
  with gr.Row():
397
  with gr.Column():
 
402
  show_download_button=False,
403
  show_fullscreen_button=True,
404
  height=420,
405
+ elem_classes=["zoomable"],
406
  )
407
  a_btn = gr.Button("A is better", variant="primary")
408
  with gr.Column():
 
413
  show_download_button=False,
414
  show_fullscreen_button=True,
415
  height=420,
416
+ elem_classes=["zoomable"],
417
  )
418
  b_btn = gr.Button("B is better", variant="primary")
419
  done_md = gr.Markdown(visible=False)