art: variable gallery-inspector zoom (1×–4× toolbar) instead of a binary jump (Codex)
Reworked the lightbox desktop zoom from a fixed 2.5× toggle into a proper inspector: enter at 1.5×, a quiet floating toolbar (− / slider / + / % / Fit) drives a continuous 1×–4× scale in 0.1 steps, cursor movement keeps panning (transform-origin). Fit returns to the framed gallery view; Escape steps out then closes; the slider takes native arrow keys. Removed click-to-exit on the artwork (too easy to trigger while inspecting) — exit is the visible Fit control or Escape. Toolbar is a translucent dark pill, hidden on touch (native pinch). Zoom resets when the lightbox closes. Uses the cached full-res asset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -27,12 +27,15 @@
|
|||||||
// screens; desktop and portrait art stay upright). Aspect is read off the loaded image.
|
// screens; desktop and portrait art stay upright). Aspect is read off the loaded image.
|
||||||
let landscape = $state(false);
|
let landscape = $state(false);
|
||||||
|
|
||||||
// Desktop zoom: inside the lightbox, magnify the artwork and pan by moving the cursor
|
// Desktop zoom: a gallery inspector inside the lightbox. Variable 1×–4× via a floating
|
||||||
// (transform-origin tracks the pointer). Mobile keeps native pinch — the Zoom button is
|
// toolbar (− / slider / + / % / Fit); moving the cursor pans (transform-origin tracks the
|
||||||
// hidden there. ox/oy are the transform-origin in %.
|
// pointer). Mobile keeps native pinch — the Zoom button + toolbar are hidden there.
|
||||||
let zoomed = $state(false);
|
let zoomed = $state(false);
|
||||||
let ox = $state(50), oy = $state(50);
|
let zoomLevel = $state(1.5); // scale factor; enter at a gentle 1.5×
|
||||||
function enterZoom() { ox = 50; oy = 50; zoomed = true; }
|
let ox = $state(50), oy = $state(50); // transform-origin (%) — the panned-to point
|
||||||
|
function enterZoom() { zoomLevel = 1.5; ox = 50; oy = 50; zoomed = true; }
|
||||||
|
function fit() { zoomed = false; } // back to the framed gallery view
|
||||||
|
function setZoom(v) { zoomLevel = Math.max(1, Math.min(4, Math.round(v * 10) / 10)); }
|
||||||
function panZoom(e) {
|
function panZoom(e) {
|
||||||
const r = e.currentTarget.getBoundingClientRect();
|
const r = e.currentTarget.getBoundingClientRect();
|
||||||
ox = Math.max(0, Math.min(100, ((e.clientX - r.left) / r.width) * 100));
|
ox = Math.max(0, Math.min(100, ((e.clientX - r.left) / r.width) * 100));
|
||||||
@@ -41,11 +44,11 @@
|
|||||||
|
|
||||||
function onKey(e) {
|
function onKey(e) {
|
||||||
if (e.key !== 'Escape') return;
|
if (e.key !== 'Escape') return;
|
||||||
if (zoomed) zoomed = false; // Escape steps out of zoom first, then closes
|
if (zoomed) zoomed = false; // Escape steps out of inspection first, then closes
|
||||||
else if (zoom) zoom = false;
|
else if (zoom) zoom = false;
|
||||||
}
|
}
|
||||||
// Leaving the lightbox always drops zoom too, so re-opening starts framed.
|
// Leaving the lightbox always resets the inspector, so re-opening starts framed.
|
||||||
$effect(() => { if (!zoom) zoomed = false; });
|
$effect(() => { if (!zoom) { zoomed = false; zoomLevel = 1.5; } });
|
||||||
// Move focus to the lightbox when it opens, so Escape/Enter work and focus is trapped sanely.
|
// Move focus to the lightbox when it opens, so Escape/Enter work and focus is trapped sanely.
|
||||||
$effect(() => { if (zoom && lightboxEl) lightboxEl.focus(); });
|
$effect(() => { if (zoom && lightboxEl) lightboxEl.focus(); });
|
||||||
|
|
||||||
@@ -241,12 +244,18 @@
|
|||||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18" /></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18" /></svg>
|
||||||
</button>
|
</button>
|
||||||
{#if zoomed}
|
{#if zoomed}
|
||||||
<!-- magnified inspection view: move the cursor to pan, click to zoom back out -->
|
<!-- gallery inspector: move the cursor to pan; zoom with the toolbar; Fit/Esc to exit -->
|
||||||
<button class="lb-zoom" onpointermove={panZoom} onclick={() => (zoomed = false)} aria-label="Zoom out">
|
<div class="lb-zoom" onpointermove={panZoom} role="img" aria-label="{art.title}, magnified">
|
||||||
<img src={art.image_url_large || art.image_url} alt={art.title}
|
<img src={art.image_url_large || art.image_url} alt={art.title}
|
||||||
style="transform-origin:{ox}% {oy}%" draggable="false" />
|
style="transform:scale({zoomLevel});transform-origin:{ox}% {oy}%" draggable="false" />
|
||||||
</button>
|
</div>
|
||||||
<span class="lb-hint">Move to look closer · click to zoom out</span>
|
<div class="lb-tools" role="group" aria-label="Zoom controls">
|
||||||
|
<button class="lb-t" onclick={() => setZoom(zoomLevel - 0.1)} aria-label="Zoom out">−</button>
|
||||||
|
<input class="lb-slider" type="range" min="1" max="4" step="0.1" bind:value={zoomLevel} aria-label="Zoom level" />
|
||||||
|
<button class="lb-t" onclick={() => setZoom(zoomLevel + 0.1)} aria-label="Zoom in">+</button>
|
||||||
|
<span class="lb-pct">{Math.round(zoomLevel * 100)}%</span>
|
||||||
|
<button class="lb-fit" onclick={fit}>Fit</button>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="lb-stage">
|
<span class="lb-stage">
|
||||||
<span class="frame frame--{frame} lb-frame" style="--frame-scale:{thickness}">
|
<span class="frame frame--{frame} lb-frame" style="--frame-scale:{thickness}">
|
||||||
@@ -546,14 +555,33 @@
|
|||||||
/* magnified inspection view (desktop): cursor pans via transform-origin */
|
/* magnified inspection view (desktop): cursor pans via transform-origin */
|
||||||
.lb-zoom {
|
.lb-zoom {
|
||||||
position: relative; z-index: 1; width: 96vw; height: 88vh; overflow: hidden;
|
position: relative; z-index: 1; width: 96vw; height: 88vh; overflow: hidden;
|
||||||
border: none; background: none; padding: 0; cursor: zoom-out;
|
cursor: move; display: flex; align-items: center; justify-content: center;
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
}
|
}
|
||||||
.lb-zoom img {
|
.lb-zoom img {
|
||||||
max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain;
|
max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain;
|
||||||
transform: scale(2.5); will-change: transform; user-select: none; -webkit-user-drag: none;
|
will-change: transform; user-select: none; -webkit-user-drag: none;
|
||||||
}
|
}
|
||||||
.lb-hint { position: relative; z-index: 1; margin-top: 10px; color: #5b636e; font-size: 0.85rem; }
|
/* floating gallery-inspector toolbar — a quiet translucent dark pill, white controls */
|
||||||
|
.lb-tools {
|
||||||
|
position: absolute; bottom: clamp(14px, 3vh, 28px); left: 50%; transform: translateX(-50%);
|
||||||
|
z-index: 3; display: flex; align-items: center; gap: 12px;
|
||||||
|
background: rgba(20, 18, 16, 0.72); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
|
||||||
|
border-radius: 999px; padding: 8px 14px; color: #fff; box-shadow: 0 8px 24px -12px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.lb-t {
|
||||||
|
width: 26px; height: 26px; flex: none; border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.28);
|
||||||
|
background: none; color: #fff; cursor: pointer; font-size: 1.05rem; line-height: 1;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.lb-t:hover { background: rgba(255, 255, 255, 0.18); }
|
||||||
|
.lb-slider { width: clamp(120px, 22vw, 220px); accent-color: #fff; cursor: pointer; }
|
||||||
|
.lb-pct { font-size: 0.8rem; font-variant-numeric: tabular-nums; min-width: 3.2em; text-align: center; opacity: 0.92; }
|
||||||
|
.lb-fit {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.32); background: none; color: #fff; cursor: pointer;
|
||||||
|
border-radius: 999px; padding: 4px 13px; font-size: 0.82rem; font-family: inherit; font-weight: 600;
|
||||||
|
}
|
||||||
|
.lb-fit:hover { background: rgba(255, 255, 255, 0.18); }
|
||||||
|
@media (hover: none) { .lb-tools { display: none; } } /* mobile keeps native pinch */
|
||||||
.lb-zoombtn {
|
.lb-zoombtn {
|
||||||
display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
|
display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
|
||||||
border: 1px solid #d9cdeb; background: #fff; color: var(--accent);
|
border: 1px solid #d9cdeb; background: #fff; color: var(--accent);
|
||||||
|
|||||||
Reference in New Issue
Block a user