ParkerTenBroeck.github.io/static/js/script.js
ParkerTenBroeck 0a882b6614 video support
2026-01-19 13:13:23 -05:00

304 lines
No EOL
8.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
let lightboxHistoryActive = false;
const lightbox = document.getElementById("lightbox");
const stage = lightbox?.querySelector(".lightbox__stage");
const img = document.getElementById("lightbox-img");
if (!lightbox || !stage || !img) return;
let open = false;
// Transform state (relative to centered position)
let scale = 1;
let tx = 0;
let ty = 0;
// Base rendered size of the image at scale=1 (in px)
let baseW = 0;
let baseH = 0;
// Dragging
let lastX = 0;
let lastY = 0;
// Touch pinch
let touchMode = null;
let pinchStartDist = 0;
let pinchStartScale = 1;
let pinchMid = { x: 0, y: 0 };
const MIN_SCALE = 1;
const MAX_SCALE = 6;
const clamp = (n, a, b) => Math.max(a, Math.min(b, n));
const rect = (el) => el.getBoundingClientRect();
function measureBaseSize() {
// IMPORTANT: measure when tx/ty=0 and scale=1 so we get the centered base size.
const prev = img.style.transform;
img.style.transform = "translate(0px, 0px) scale(1)";
const r = rect(img);
baseW = r.width;
baseH = r.height;
img.style.transform = prev;
}
function clampPan() {
const s = rect(stage);
const scaledW = baseW * scale;
const scaledH = baseH * scale;
// When the image is smaller than the stage, keep it centered (no panning).
const maxX = Math.max(0, (scaledW - s.width) / 2);
const maxY = Math.max(0, (scaledH - s.height) / 2);
tx = clamp(tx, -maxX, maxX);
ty = clamp(ty, -maxY, maxY);
}
function applyTransform() {
clampPan();
img.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`;
img.classList.toggle("is-zoomed", scale > 1);
img.classList.toggle("is-unzoomed", scale === 1);
}
function resetView() {
scale = 1;
tx = 0;
ty = 0;
applyTransform();
}
function openLightbox(src, alt) {
img.onload = null;
img.src = src;
img.alt = alt || "";
lightbox.classList.add("open");
lightbox.setAttribute("aria-hidden", "false");
document.body.classList.add("lb-open");
open = true;
if (!lightboxHistoryActive) {
history.pushState({ lightbox: true }, "");
lightboxHistoryActive = true;
}
// Block background scroll/gestures while open (especially iOS)
document.addEventListener("touchmove", preventTouchScroll, { passive: false });
img.onload = () => {
// Wait for layout to settle, then measure and reset.
requestAnimationFrame(() => {
measureBaseSize();
resetView();
});
};
// If cached and instantly available
if (img.complete) {
requestAnimationFrame(() => {
measureBaseSize();
resetView();
});
}
}
function closeLightbox(fromPopState = false) {
lightbox.classList.remove("open");
lightbox.setAttribute("aria-hidden", "true");
document.body.classList.remove("lb-open");
open = false;
img.classList.remove("dragging");
touchMode = null;
document.removeEventListener("touchmove", preventTouchScroll, { passive: false });
if (lightboxHistoryActive && !fromPopState) {
history.back();
}
lightboxHistoryActive = false;
}
function preventTouchScroll(e) {
if (open) e.preventDefault();
}
function zoomAt(clientX, clientY, newScale) {
const s = rect(stage);
const cx = clientX - s.left - s.width / 2;
const cy = clientY - s.top - s.height / 2;
const prevScale = scale;
scale = clamp(newScale, MIN_SCALE, MAX_SCALE);
// Keep point under cursor stable (relative to center-based coordinates)
tx = cx - ((cx - tx) / prevScale) * scale;
ty = cy - ((cy - ty) / prevScale) * scale;
applyTransform();
}
// Open on thumbnail click; close on backdrop click
document.addEventListener("click", (e) => {
const thumb = e.target.closest("img.zoomable");
if (thumb) {
openLightbox(thumb.dataset.full || thumb.src, thumb.alt);
return;
}
if (open && (e.target === lightbox || e.target === stage)) closeLightbox();
});
// Prevent clicking the image from closing
img.addEventListener("click", (e) => {
if (open) e.stopPropagation();
});
// ESC to close
document.addEventListener("keydown", (e) => {
if (open && e.key === "Escape") closeLightbox();
});
// Wheel / trackpad zoom
stage.addEventListener("wheel", (e) => {
if (!open) return;
e.preventDefault();
const factor = e.deltaY < 0 ? 1.12 : 1 / 1.12;
zoomAt(e.clientX, e.clientY, scale * factor);
}, { passive: false });
// --- CLICK vs DRAG (Pointer Events) ---
let pointerId = null;
let downX = 0, downY = 0;
let startTx = 0, startTy = 0;
let moved = false;
const DRAG_THRESHOLD = 6; // px
const ZOOM_IN = 2.5;
function setZoomedClass() {
img.classList.toggle("is-zoomed", scale > 1);
}
img.addEventListener("pointerdown", (e) => {
if (!open) return;
pointerId = e.pointerId;
img.setPointerCapture(pointerId);
downX = e.clientX;
downY = e.clientY;
startTx = tx;
startTy = ty;
moved = false;
// Dont let the browser start image dragging/scrolling
e.preventDefault();
});
img.addEventListener("pointermove", (e) => {
if (!open || pointerId !== e.pointerId) return;
const dx = e.clientX - downX;
const dy = e.clientY - downY;
// Decide if this gesture is a pan
if (!moved && (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)) {
moved = true;
img.classList.add("dragging");
}
// If panning, update translate (pan) from the start position
if (moved && scale > 1) {
tx = startTx + dx;
ty = startTy + dy;
applyTransform();
}
});
img.addEventListener("pointerup", (e) => {
if (!open || pointerId !== e.pointerId) return;
img.classList.remove("dragging");
img.releasePointerCapture(pointerId);
pointerId = null;
// If we didn't move enough, treat as a click: toggle zoom
if (!moved) {
// Zoom around where they clicked
const target = (scale === 1) ? ZOOM_IN : 1;
zoomAt(e.clientX, e.clientY, target);
setZoomedClass();
}
moved = false;
});
img.addEventListener("pointercancel", () => {
img.classList.remove("dragging");
pointerId = null;
moved = false;
});
// Touch: pan + pinch
stage.addEventListener("touchstart", (e) => {
if (!open) return;
if (e.touches.length === 1) {
touchMode = "pan";
lastX = e.touches[0].clientX;
lastY = e.touches[0].clientY;
} else if (e.touches.length === 2) {
touchMode = "pinch";
const [a, b] = e.touches;
pinchStartDist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
pinchStartScale = scale;
pinchMid = { x: (a.clientX + b.clientX) / 2, y: (a.clientY + b.clientY) / 2 };
}
}, { passive: false });
stage.addEventListener("touchmove", (e) => {
if (!open) return;
e.preventDefault();
if (touchMode === "pan" && e.touches.length === 1) {
const x = e.touches[0].clientX;
const y = e.touches[0].clientY;
tx += x - lastX;
ty += y - lastY;
lastX = x;
lastY = y;
applyTransform();
}
if (touchMode === "pinch" && e.touches.length === 2) {
const [a, b] = e.touches;
const dist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
const ratio = dist / pinchStartDist;
zoomAt(pinchMid.x, pinchMid.y, pinchStartScale * ratio);
}
}, { passive: false });
stage.addEventListener("touchend", (e) => {
if (!open) return;
if (e.touches.length === 0) touchMode = null;
if (e.touches.length === 1) {
touchMode = "pan";
lastX = e.touches[0].clientX;
lastY = e.touches[0].clientY;
}
});
window.addEventListener("popstate", (e) => {
// If the lightbox is open, Back should close it — not leave the page
if (open) {
closeLightbox(true);
}
});
})();