Skip to content

Commit a2ecb99

Browse files
johanrinclaude
andcommitted
Replace checkbox-based image zoom with JS overlay
Simplify render-image.html by removing the checkbox hack and section checks. Add a smooth animated zoom overlay with backdrop, keyboard dismiss, and scroll-to-close support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9ae2bbe commit a2ecb99

2 files changed

Lines changed: 116 additions & 42 deletions

File tree

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,10 @@
1-
<!-- Checks if page is part of section and page is not section itself -->
2-
{{- if and (ne .Page.Kind "section") (.Page.Section ) }}
3-
<!-- Generate a unique id for each image -->
4-
{{- $random := (substr (md5 .Destination) 0 5) }}
5-
{{- if .Title }}
6-
<figure>
7-
<input type="checkbox" id="zoomCheck-{{$random}}" hidden>
8-
<label for="zoomCheck-{{$random}}">
9-
<img class="zoomCheck" loading="lazy" decoding="async"
10-
src="{{ .Destination | safeURL }}" alt="{{ .Text }}" />
11-
</label>
12-
<figcaption>{{ .Title }}</figcaption>
13-
</figure>
14-
{{- else }}
15-
<input type="checkbox" id="zoomCheck-{{$random}}" hidden>
16-
<label for="zoomCheck-{{$random}}">
17-
<img class="zoomCheck" loading="lazy" decoding="async"
18-
src="{{ .Destination | safeURL }}" alt="{{ .Text }}" />
19-
</label>
20-
{{- end }}
1+
{{- if .Title }}
2+
<figure>
3+
<img class="zoomable" loading="lazy" decoding="async"
4+
src="{{ .Destination | safeURL }}" alt="{{ .Text }}" />
5+
<figcaption>{{ .Title }}</figcaption>
6+
</figure>
217
{{- else }}
22-
{{- if .Title }}
23-
<figure>
24-
<img loading="lazy" decoding="async" src="{{ .Destination | safeURL }}" alt="{{ .Text }}" />
25-
<figcaption>{{ .Title }}</figcaption>
26-
</figure>
27-
{{- else }}
28-
<img loading="lazy" decoding="async" src="{{ .Destination | safeURL }}" alt="{{ .Text }}" />
29-
{{- end }}
8+
<img class="zoomable" loading="lazy" decoding="async"
9+
src="{{ .Destination | safeURL }}" alt="{{ .Text }}" />
3010
{{- end }}

layouts/partials/custom-head.html

Lines changed: 108 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,20 +95,114 @@
9595
{{ end }}
9696

9797
<style>
98-
@media screen and (min-width: 769px) {
99-
/* .post-content is a class which will be present only on single pages
100-
and not lists and section pages in Hugo */
101-
.page-content input[type="checkbox"]:checked ~ label > img {
102-
transform: scale(1.6);
103-
cursor: zoom-out;
104-
position: relative;
105-
z-index: 999;
106-
}
98+
img.zoomable {
99+
cursor: zoom-in;
100+
}
107101

108-
.page-content img.zoomCheck {
109-
transition: transform 0.15s ease;
110-
z-index: 999;
111-
cursor: zoom-in;
112-
}
102+
.zoom-overlay {
103+
position: fixed;
104+
inset: 0;
105+
z-index: 9998;
106+
background: rgba(0, 0, 0, 0.9);
107+
opacity: 0;
108+
transition: opacity 0.3s cubic-bezier(0.2, 0, 0.2, 1);
109+
cursor: zoom-out;
110+
}
111+
112+
.zoom-overlay.active {
113+
opacity: 1;
114+
}
115+
116+
.zoom-clone {
117+
position: fixed;
118+
z-index: 9999;
119+
cursor: zoom-out;
120+
will-change: transform;
121+
transition: transform 0.3s cubic-bezier(0.2, 0, 0.2, 1);
113122
}
114123
</style>
124+
<script>
125+
(function () {
126+
var overlay, clone, sourceImg, scrollStart, animating;
127+
128+
function open(img) {
129+
if (animating || overlay) return;
130+
animating = true;
131+
sourceImg = img;
132+
scrollStart = window.scrollY;
133+
134+
// Hide original to avoid doubling
135+
sourceImg.style.visibility = 'hidden';
136+
137+
// Create overlay backdrop
138+
overlay = document.createElement('div');
139+
overlay.className = 'zoom-overlay';
140+
document.body.appendChild(overlay);
141+
142+
// Create clone at exact position of original
143+
var rect = img.getBoundingClientRect();
144+
clone = document.createElement('img');
145+
clone.src = img.src;
146+
clone.alt = img.alt;
147+
clone.className = 'zoom-clone';
148+
clone.style.top = rect.top + 'px';
149+
clone.style.left = rect.left + 'px';
150+
clone.style.width = rect.width + 'px';
151+
clone.style.height = rect.height + 'px';
152+
document.body.appendChild(clone);
153+
154+
// Force reflow, then animate
155+
clone.offsetHeight;
156+
overlay.classList.add('active');
157+
158+
// Calculate transform to center and scale
159+
var scale = Math.min(
160+
(window.innerWidth * 0.95) / rect.width,
161+
(window.innerHeight * 0.95) / rect.height
162+
);
163+
var translateX = (window.innerWidth / 2) - (rect.left + rect.width / 2);
164+
var translateY = (window.innerHeight / 2) - (rect.top + rect.height / 2);
165+
166+
clone.style.transform = 'translate(' + translateX + 'px, ' + translateY + 'px) scale(' + scale + ')';
167+
168+
clone.addEventListener('transitionend', function () {
169+
animating = false;
170+
}, { once: true });
171+
}
172+
173+
function close() {
174+
if (animating || !overlay) return;
175+
animating = true;
176+
177+
overlay.classList.remove('active');
178+
179+
// Animate clone back to original position (accounting for scroll change)
180+
var scrollDelta = window.scrollY - scrollStart;
181+
clone.style.transform = 'translate(0, ' + (-scrollDelta) + 'px) scale(1)';
182+
183+
clone.addEventListener('transitionend', function () {
184+
overlay.remove();
185+
clone.remove();
186+
sourceImg.style.visibility = '';
187+
overlay = clone = sourceImg = null;
188+
animating = false;
189+
}, { once: true });
190+
}
191+
192+
document.addEventListener('click', function (e) {
193+
if (overlay) { close(); return; }
194+
var img = e.target.closest('img.zoomable');
195+
if (img) open(img);
196+
});
197+
198+
document.addEventListener('keydown', function (e) {
199+
if (e.key === 'Escape') close();
200+
});
201+
202+
// Scroll to dismiss
203+
window.addEventListener('scroll', function () {
204+
if (!overlay || animating) return;
205+
if (Math.abs(window.scrollY - scrollStart) > 50) close();
206+
}, { passive: true });
207+
})();
208+
</script>

0 commit comments

Comments
 (0)