1308 lines
43 KiB
JavaScript
1308 lines
43 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
let overlayEl = null;
|
|
let backdropEl = null;
|
|
let toolbarEl = null;
|
|
let helpButtonEl = null;
|
|
let helpPanelEl = null;
|
|
let compareButtonEl = null;
|
|
let zoomControlEl = null;
|
|
let zoomSliderEl = null;
|
|
let zoomValueEl = null;
|
|
let opacityControlEl = null;
|
|
let opacitySliderEl = null;
|
|
let opacityValueEl = null;
|
|
let stageEl = null;
|
|
let frameEl = null;
|
|
let baseLayerEl = null;
|
|
let overlayLayerEl = null;
|
|
let imageEl = null;
|
|
let overlayImageEl = null;
|
|
let primaryInfoEl = null;
|
|
let primaryTitleEl = null;
|
|
let primaryGroupsEl = null;
|
|
let primaryHintEl = null;
|
|
let secondaryInfoEl = null;
|
|
let secondaryTitleEl = null;
|
|
let secondaryGroupsEl = null;
|
|
let secondaryHintEl = null;
|
|
let zoomed = false;
|
|
let previousFocusedEl = null;
|
|
|
|
const LIGHTBOX_ZOOM_SCALE = 6.66;
|
|
const LIGHTBOX_ZOOM_STEP = 0.1;
|
|
const LIGHTBOX_PAN_STEP = 4;
|
|
const LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY = 0.5;
|
|
const LIGHTBOX_COMPARE_SEQUENCE_STEP_KEYS = new Set(["ArrowLeft", "ArrowRight"]);
|
|
|
|
const lightboxState = {
|
|
isOpen: false,
|
|
compareMode: false,
|
|
allowOverlayCompare: false,
|
|
primaryCard: null,
|
|
secondaryCard: null,
|
|
sequenceIds: [],
|
|
resolveCardById: null,
|
|
onSelectCardId: null,
|
|
overlayOpacity: LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY,
|
|
zoomScale: LIGHTBOX_ZOOM_SCALE,
|
|
helpOpen: false,
|
|
primaryRotated: false,
|
|
overlayRotated: false,
|
|
zoomOriginX: 50,
|
|
zoomOriginY: 50
|
|
};
|
|
|
|
function hasSecondaryCard() {
|
|
return Boolean(lightboxState.secondaryCard?.src);
|
|
}
|
|
|
|
function clampOverlayOpacity(value) {
|
|
const numericValue = Number(value);
|
|
if (!Number.isFinite(numericValue)) {
|
|
return LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY;
|
|
}
|
|
|
|
return Math.min(1, Math.max(0.05, numericValue));
|
|
}
|
|
|
|
function clampZoomScale(value) {
|
|
const numericValue = Number(value);
|
|
if (!Number.isFinite(numericValue)) {
|
|
return 1;
|
|
}
|
|
|
|
return Math.min(LIGHTBOX_ZOOM_SCALE, Math.max(1, numericValue));
|
|
}
|
|
|
|
function normalizeCompareDetails(compareDetails) {
|
|
if (!Array.isArray(compareDetails)) {
|
|
return [];
|
|
}
|
|
|
|
return compareDetails
|
|
.map((group) => ({
|
|
title: String(group?.title || "").trim(),
|
|
items: Array.isArray(group?.items)
|
|
? [...new Set(group.items.map((item) => String(item || "").trim()).filter(Boolean))]
|
|
: []
|
|
}))
|
|
.filter((group) => group.title && group.items.length);
|
|
}
|
|
|
|
function normalizeOpenRequest(srcOrOptions, altText, extraOptions) {
|
|
if (srcOrOptions && typeof srcOrOptions === "object" && !Array.isArray(srcOrOptions)) {
|
|
return {
|
|
...srcOrOptions
|
|
};
|
|
}
|
|
|
|
return {
|
|
...(extraOptions || {}),
|
|
src: srcOrOptions,
|
|
altText
|
|
};
|
|
}
|
|
|
|
function normalizeCardRequest(request) {
|
|
const normalized = normalizeOpenRequest(request);
|
|
const label = String(normalized.label || normalized.altText || "Tarot card enlarged image").trim() || "Tarot card enlarged image";
|
|
return {
|
|
src: String(normalized.src || "").trim(),
|
|
altText: String(normalized.altText || label).trim() || label,
|
|
label,
|
|
cardId: String(normalized.cardId || "").trim(),
|
|
compareDetails: normalizeCompareDetails(normalized.compareDetails)
|
|
};
|
|
}
|
|
|
|
function resolveCardRequestById(cardId) {
|
|
if (!cardId || typeof lightboxState.resolveCardById !== "function") {
|
|
return null;
|
|
}
|
|
|
|
const resolved = lightboxState.resolveCardById(cardId);
|
|
if (!resolved) {
|
|
return null;
|
|
}
|
|
|
|
return normalizeCardRequest({
|
|
...resolved,
|
|
cardId
|
|
});
|
|
}
|
|
|
|
function clearSecondaryCard() {
|
|
lightboxState.secondaryCard = null;
|
|
if (overlayImageEl) {
|
|
overlayImageEl.removeAttribute("src");
|
|
overlayImageEl.alt = "";
|
|
overlayImageEl.style.display = "none";
|
|
}
|
|
|
|
syncComparePanels();
|
|
syncOpacityControl();
|
|
}
|
|
|
|
function setOverlayOpacity(value) {
|
|
const opacity = clampOverlayOpacity(value);
|
|
lightboxState.overlayOpacity = opacity;
|
|
|
|
if (overlayImageEl) {
|
|
overlayImageEl.style.opacity = String(opacity);
|
|
}
|
|
|
|
if (opacitySliderEl) {
|
|
opacitySliderEl.value = String(Math.round(opacity * 100));
|
|
opacitySliderEl.disabled = !lightboxState.compareMode || !hasSecondaryCard();
|
|
}
|
|
|
|
if (opacityValueEl) {
|
|
opacityValueEl.textContent = `${Math.round(opacity * 100)}%`;
|
|
}
|
|
}
|
|
|
|
function updateImageCursor() {
|
|
if (!imageEl) {
|
|
return;
|
|
}
|
|
|
|
imageEl.style.cursor = zoomed ? "zoom-out" : "zoom-in";
|
|
}
|
|
|
|
function buildRotationTransform(rotated) {
|
|
return rotated ? "rotate(180deg)" : "rotate(0deg)";
|
|
}
|
|
|
|
function isPrimaryRotationActive() {
|
|
return !lightboxState.compareMode && lightboxState.primaryRotated;
|
|
}
|
|
|
|
function isOverlayRotationActive() {
|
|
return lightboxState.compareMode && hasSecondaryCard() && lightboxState.overlayRotated;
|
|
}
|
|
|
|
function applyTransformOrigins(originX = lightboxState.zoomOriginX, originY = lightboxState.zoomOriginY) {
|
|
const nextOrigin = `${originX}% ${originY}%`;
|
|
|
|
if (baseLayerEl) {
|
|
baseLayerEl.style.transformOrigin = nextOrigin;
|
|
}
|
|
|
|
if (overlayLayerEl) {
|
|
overlayLayerEl.style.transformOrigin = nextOrigin;
|
|
}
|
|
}
|
|
|
|
function applyZoomTransform() {
|
|
const activeZoomScale = zoomed ? lightboxState.zoomScale : 1;
|
|
const showPrimaryRotation = isPrimaryRotationActive();
|
|
const showOverlayRotation = isOverlayRotationActive();
|
|
|
|
if (baseLayerEl) {
|
|
baseLayerEl.style.transform = `scale(${activeZoomScale})`;
|
|
}
|
|
|
|
if (overlayLayerEl) {
|
|
overlayLayerEl.style.transform = `scale(${activeZoomScale})`;
|
|
}
|
|
|
|
if (imageEl) {
|
|
imageEl.style.transform = buildRotationTransform(showPrimaryRotation);
|
|
}
|
|
|
|
if (overlayImageEl) {
|
|
overlayImageEl.style.transform = buildRotationTransform(showOverlayRotation);
|
|
}
|
|
|
|
applyTransformOrigins();
|
|
|
|
updateImageCursor();
|
|
}
|
|
|
|
function setZoomScale(value) {
|
|
const zoomScale = clampZoomScale(value);
|
|
lightboxState.zoomScale = zoomScale;
|
|
|
|
if (zoomSliderEl) {
|
|
zoomSliderEl.value = String(Math.round(zoomScale * 100));
|
|
}
|
|
|
|
if (zoomValueEl) {
|
|
zoomValueEl.textContent = `${Math.round(zoomScale * 100)}%`;
|
|
}
|
|
|
|
if (lightboxState.isOpen) {
|
|
applyComparePresentation();
|
|
return;
|
|
}
|
|
|
|
applyZoomTransform();
|
|
}
|
|
|
|
function isZoomInKey(event) {
|
|
return event.key === "+"
|
|
|| event.key === "="
|
|
|| event.code === "NumpadAdd";
|
|
}
|
|
|
|
function isZoomOutKey(event) {
|
|
return event.key === "-"
|
|
|| event.key === "_"
|
|
|| event.code === "NumpadSubtract";
|
|
}
|
|
|
|
function isRotateKey(event) {
|
|
return event.code === "KeyR" || String(event.key || "").toLowerCase() === "r";
|
|
}
|
|
|
|
function stepZoom(direction) {
|
|
if (!lightboxState.isOpen) {
|
|
return;
|
|
}
|
|
|
|
const activeScale = zoomed ? lightboxState.zoomScale : 1;
|
|
const nextScale = clampZoomScale(activeScale + (direction * LIGHTBOX_ZOOM_STEP));
|
|
zoomed = nextScale > 1;
|
|
setZoomScale(nextScale);
|
|
|
|
if (!zoomed && imageEl) {
|
|
lightboxState.zoomOriginX = 50;
|
|
lightboxState.zoomOriginY = 50;
|
|
}
|
|
|
|
if (!zoomed && overlayImageEl) {
|
|
lightboxState.zoomOriginX = 50;
|
|
lightboxState.zoomOriginY = 50;
|
|
}
|
|
}
|
|
|
|
function isPanUpKey(event) {
|
|
return event.code === "KeyW" || String(event.key || "").toLowerCase() === "w";
|
|
}
|
|
|
|
function isPanLeftKey(event) {
|
|
return event.code === "KeyA" || String(event.key || "").toLowerCase() === "a";
|
|
}
|
|
|
|
function isPanDownKey(event) {
|
|
return event.code === "KeyS" || String(event.key || "").toLowerCase() === "s";
|
|
}
|
|
|
|
function isPanRightKey(event) {
|
|
return event.code === "KeyD" || String(event.key || "").toLowerCase() === "d";
|
|
}
|
|
|
|
function stepPan(deltaX, deltaY) {
|
|
if (!lightboxState.isOpen || !zoomed || !imageEl) {
|
|
return;
|
|
}
|
|
|
|
lightboxState.zoomOriginX = Math.min(100, Math.max(0, lightboxState.zoomOriginX + deltaX));
|
|
lightboxState.zoomOriginY = Math.min(100, Math.max(0, lightboxState.zoomOriginY + deltaY));
|
|
applyTransformOrigins();
|
|
}
|
|
|
|
function toggleRotation() {
|
|
if (!lightboxState.isOpen) {
|
|
return;
|
|
}
|
|
|
|
if (lightboxState.compareMode && hasSecondaryCard()) {
|
|
lightboxState.overlayRotated = !lightboxState.overlayRotated;
|
|
} else {
|
|
lightboxState.primaryRotated = !lightboxState.primaryRotated;
|
|
}
|
|
|
|
applyZoomTransform();
|
|
}
|
|
|
|
function createCompareGroupElement(group) {
|
|
const sectionEl = document.createElement("section");
|
|
sectionEl.style.display = "flex";
|
|
sectionEl.style.flexDirection = "column";
|
|
sectionEl.style.gap = "5px";
|
|
sectionEl.style.paddingTop = "8px";
|
|
sectionEl.style.borderTop = "1px solid rgba(148, 163, 184, 0.14)";
|
|
|
|
const titleEl = document.createElement("div");
|
|
titleEl.textContent = group.title;
|
|
titleEl.style.font = "600 10px/1.2 sans-serif";
|
|
titleEl.style.letterSpacing = "0.1em";
|
|
titleEl.style.textTransform = "uppercase";
|
|
titleEl.style.color = "rgba(148, 163, 184, 0.92)";
|
|
|
|
const valuesEl = document.createElement("div");
|
|
valuesEl.style.display = "flex";
|
|
valuesEl.style.flexDirection = "column";
|
|
valuesEl.style.gap = "3px";
|
|
|
|
group.items.forEach((item) => {
|
|
const itemEl = document.createElement("div");
|
|
itemEl.textContent = item;
|
|
itemEl.style.font = "500 12px/1.35 sans-serif";
|
|
itemEl.style.color = "#f8fafc";
|
|
valuesEl.appendChild(itemEl);
|
|
});
|
|
|
|
sectionEl.append(titleEl, valuesEl);
|
|
return sectionEl;
|
|
}
|
|
|
|
function renderComparePanel(panelEl, titleEl, groupsEl, hintEl, cardRequest, roleLabel, hintText, isVisible) {
|
|
if (!panelEl || !titleEl || !groupsEl || !hintEl) {
|
|
return;
|
|
}
|
|
|
|
if (!isVisible || !cardRequest?.label) {
|
|
panelEl.style.display = "none";
|
|
titleEl.textContent = "";
|
|
hintEl.textContent = "";
|
|
groupsEl.replaceChildren();
|
|
return;
|
|
}
|
|
|
|
panelEl.style.display = "flex";
|
|
titleEl.textContent = `${roleLabel}: ${cardRequest.label}`;
|
|
groupsEl.replaceChildren();
|
|
|
|
if (Array.isArray(cardRequest.compareDetails) && cardRequest.compareDetails.length) {
|
|
cardRequest.compareDetails.forEach((group) => {
|
|
groupsEl.appendChild(createCompareGroupElement(group));
|
|
});
|
|
} else {
|
|
const emptyEl = document.createElement("div");
|
|
emptyEl.textContent = "No compare metadata available.";
|
|
emptyEl.style.font = "500 12px/1.35 sans-serif";
|
|
emptyEl.style.color = "rgba(226, 232, 240, 0.8)";
|
|
groupsEl.appendChild(emptyEl);
|
|
}
|
|
|
|
hintEl.textContent = hintText;
|
|
hintEl.style.display = hintText ? "block" : "none";
|
|
}
|
|
|
|
function syncComparePanels() {
|
|
const isComparing = lightboxState.compareMode;
|
|
const overlaySelected = hasSecondaryCard();
|
|
const showPrimaryPanel = Boolean(lightboxState.isOpen && lightboxState.allowOverlayCompare && lightboxState.primaryCard?.label && !zoomed);
|
|
const showSecondaryPanel = Boolean(isComparing && overlaySelected && lightboxState.secondaryCard?.label && !zoomed);
|
|
|
|
renderComparePanel(
|
|
primaryInfoEl,
|
|
primaryTitleEl,
|
|
primaryGroupsEl,
|
|
primaryHintEl,
|
|
lightboxState.primaryCard,
|
|
"Base",
|
|
isComparing
|
|
? (overlaySelected
|
|
? "Use Left and Right arrows to move through the overlaid card."
|
|
: "Click another House card behind the dock, or use Left and Right arrows to pick the first overlay card.")
|
|
: "Use Left and Right arrows to move through cards. Click Overlay to compare.",
|
|
showPrimaryPanel
|
|
);
|
|
|
|
renderComparePanel(
|
|
secondaryInfoEl,
|
|
secondaryTitleEl,
|
|
secondaryGroupsEl,
|
|
secondaryHintEl,
|
|
lightboxState.secondaryCard,
|
|
"Overlay",
|
|
overlaySelected ? "Use Left and Right arrows to swap the overlay card." : "",
|
|
showSecondaryPanel
|
|
);
|
|
}
|
|
|
|
function syncOpacityControl() {
|
|
if (!opacityControlEl) {
|
|
return;
|
|
}
|
|
|
|
opacityControlEl.style.display = lightboxState.compareMode && hasSecondaryCard() && !zoomed ? "flex" : "none";
|
|
setOverlayOpacity(lightboxState.overlayOpacity);
|
|
}
|
|
|
|
function syncHelpUi() {
|
|
if (!helpButtonEl || !helpPanelEl) {
|
|
return;
|
|
}
|
|
|
|
const canShow = lightboxState.isOpen && !zoomed;
|
|
helpButtonEl.style.display = canShow ? "inline-flex" : "none";
|
|
helpPanelEl.style.display = canShow && lightboxState.helpOpen ? "flex" : "none";
|
|
helpButtonEl.textContent = lightboxState.helpOpen ? "Hide Help" : "Help";
|
|
}
|
|
|
|
function syncZoomControl() {
|
|
if (!zoomControlEl) {
|
|
return;
|
|
}
|
|
|
|
zoomControlEl.style.display = lightboxState.isOpen && !zoomed ? "flex" : "none";
|
|
if (zoomSliderEl) {
|
|
zoomSliderEl.value = String(Math.round(lightboxState.zoomScale * 100));
|
|
}
|
|
|
|
if (zoomValueEl) {
|
|
zoomValueEl.textContent = `${Math.round(lightboxState.zoomScale * 100)}%`;
|
|
}
|
|
}
|
|
|
|
function applyComparePresentation() {
|
|
if (!overlayEl || !backdropEl || !toolbarEl || !stageEl || !frameEl || !imageEl || !overlayImageEl || !compareButtonEl) {
|
|
return;
|
|
}
|
|
|
|
compareButtonEl.hidden = zoomed || !lightboxState.allowOverlayCompare || (lightboxState.compareMode && !hasSecondaryCard());
|
|
compareButtonEl.textContent = lightboxState.compareMode ? "Done Overlay" : "Overlay";
|
|
syncHelpUi();
|
|
syncZoomControl();
|
|
syncOpacityControl();
|
|
|
|
if (!lightboxState.compareMode) {
|
|
overlayEl.style.pointerEvents = "none";
|
|
backdropEl.style.display = "block";
|
|
backdropEl.style.pointerEvents = "auto";
|
|
backdropEl.style.background = "rgba(0, 0, 0, 0.82)";
|
|
toolbarEl.style.top = "24px";
|
|
toolbarEl.style.right = "24px";
|
|
toolbarEl.style.left = "auto";
|
|
stageEl.style.top = "0";
|
|
stageEl.style.right = "0";
|
|
stageEl.style.bottom = "0";
|
|
stageEl.style.left = "0";
|
|
stageEl.style.width = "auto";
|
|
stageEl.style.height = "auto";
|
|
stageEl.style.transform = "none";
|
|
stageEl.style.pointerEvents = "auto";
|
|
frameEl.style.position = "relative";
|
|
frameEl.style.width = "100%";
|
|
frameEl.style.height = "100%";
|
|
frameEl.style.maxWidth = "none";
|
|
frameEl.style.maxHeight = "none";
|
|
frameEl.style.borderRadius = "0";
|
|
frameEl.style.background = "transparent";
|
|
frameEl.style.boxShadow = "none";
|
|
frameEl.style.overflow = "hidden";
|
|
primaryInfoEl.style.left = "auto";
|
|
primaryInfoEl.style.right = "18px";
|
|
primaryInfoEl.style.top = "50%";
|
|
primaryInfoEl.style.bottom = "auto";
|
|
primaryInfoEl.style.width = "clamp(220px, 20vw, 320px)";
|
|
primaryInfoEl.style.transform = "translateY(-50%)";
|
|
imageEl.style.width = "100%";
|
|
imageEl.style.height = "100%";
|
|
imageEl.style.maxWidth = "none";
|
|
imageEl.style.maxHeight = "none";
|
|
imageEl.style.objectFit = "contain";
|
|
overlayImageEl.style.display = "none";
|
|
secondaryInfoEl.style.display = "none";
|
|
syncComparePanels();
|
|
applyZoomTransform();
|
|
return;
|
|
}
|
|
|
|
overlayEl.style.pointerEvents = "none";
|
|
backdropEl.style.display = "none";
|
|
backdropEl.style.pointerEvents = "none";
|
|
toolbarEl.style.top = "18px";
|
|
toolbarEl.style.right = "18px";
|
|
toolbarEl.style.left = "auto";
|
|
stageEl.style.pointerEvents = "auto";
|
|
frameEl.style.position = "relative";
|
|
frameEl.style.width = "100%";
|
|
frameEl.style.height = "100%";
|
|
frameEl.style.maxWidth = "none";
|
|
frameEl.style.maxHeight = "none";
|
|
frameEl.style.overflow = "hidden";
|
|
imageEl.style.width = "100%";
|
|
imageEl.style.height = "100%";
|
|
imageEl.style.maxWidth = "none";
|
|
imageEl.style.maxHeight = "none";
|
|
imageEl.style.objectFit = "contain";
|
|
updateImageCursor();
|
|
|
|
if (zoomed && hasSecondaryCard()) {
|
|
stageEl.style.top = "0";
|
|
stageEl.style.right = "0";
|
|
stageEl.style.bottom = "0";
|
|
stageEl.style.left = "0";
|
|
stageEl.style.width = "auto";
|
|
stageEl.style.height = "auto";
|
|
stageEl.style.transform = "none";
|
|
frameEl.style.borderRadius = "0";
|
|
frameEl.style.background = "transparent";
|
|
frameEl.style.boxShadow = "none";
|
|
} else if (!hasSecondaryCard()) {
|
|
stageEl.style.top = "auto";
|
|
stageEl.style.right = "18px";
|
|
stageEl.style.bottom = "18px";
|
|
stageEl.style.left = "auto";
|
|
stageEl.style.width = "clamp(180px, 18vw, 280px)";
|
|
stageEl.style.height = "min(44vh, 520px)";
|
|
stageEl.style.transform = "none";
|
|
frameEl.style.borderRadius = "22px";
|
|
frameEl.style.background = "rgba(13, 13, 20, 0.9)";
|
|
frameEl.style.boxShadow = "0 24px 64px rgba(0, 0, 0, 0.5)";
|
|
primaryInfoEl.style.left = "auto";
|
|
primaryInfoEl.style.right = "calc(100% + 16px)";
|
|
primaryInfoEl.style.top = "50%";
|
|
primaryInfoEl.style.bottom = "auto";
|
|
primaryInfoEl.style.width = "clamp(220px, 22vw, 320px)";
|
|
primaryInfoEl.style.transform = "translateY(-50%)";
|
|
} else {
|
|
stageEl.style.top = "50%";
|
|
stageEl.style.right = "auto";
|
|
stageEl.style.bottom = "auto";
|
|
stageEl.style.left = "50%";
|
|
stageEl.style.width = "min(44vw, 560px)";
|
|
stageEl.style.height = "min(92vh, 1400px)";
|
|
stageEl.style.transform = "translate(-50%, -50%)";
|
|
frameEl.style.borderRadius = "28px";
|
|
frameEl.style.background = "rgba(11, 15, 26, 0.88)";
|
|
frameEl.style.boxShadow = "0 30px 90px rgba(0, 0, 0, 0.56)";
|
|
primaryInfoEl.style.left = "auto";
|
|
primaryInfoEl.style.right = "calc(100% + 10px)";
|
|
primaryInfoEl.style.top = "50%";
|
|
primaryInfoEl.style.bottom = "auto";
|
|
primaryInfoEl.style.width = "clamp(180px, 15vw, 220px)";
|
|
primaryInfoEl.style.transform = "translateY(-50%)";
|
|
secondaryInfoEl.style.left = "calc(100% + 10px)";
|
|
secondaryInfoEl.style.right = "auto";
|
|
secondaryInfoEl.style.top = "50%";
|
|
secondaryInfoEl.style.bottom = "auto";
|
|
secondaryInfoEl.style.width = "clamp(180px, 15vw, 220px)";
|
|
secondaryInfoEl.style.transform = "translateY(-50%)";
|
|
}
|
|
|
|
if (hasSecondaryCard()) {
|
|
overlayImageEl.style.display = "block";
|
|
} else {
|
|
overlayImageEl.style.display = "none";
|
|
secondaryInfoEl.style.display = "none";
|
|
}
|
|
|
|
syncComparePanels();
|
|
applyZoomTransform();
|
|
setOverlayOpacity(lightboxState.overlayOpacity);
|
|
}
|
|
|
|
function resetZoom() {
|
|
if (!imageEl && !overlayImageEl) {
|
|
return;
|
|
}
|
|
|
|
lightboxState.zoomOriginX = 50;
|
|
lightboxState.zoomOriginY = 50;
|
|
applyTransformOrigins();
|
|
|
|
zoomed = false;
|
|
applyZoomTransform();
|
|
}
|
|
|
|
function updateZoomOrigin(clientX, clientY) {
|
|
if (!zoomed || !imageEl) {
|
|
return;
|
|
}
|
|
|
|
const rect = imageEl.getBoundingClientRect();
|
|
if (!rect.width || !rect.height) {
|
|
return;
|
|
}
|
|
|
|
const x = Math.min(100, Math.max(0, ((clientX - rect.left) / rect.width) * 100));
|
|
const y = Math.min(100, Math.max(0, ((clientY - rect.top) / rect.height) * 100));
|
|
lightboxState.zoomOriginX = x;
|
|
lightboxState.zoomOriginY = y;
|
|
applyTransformOrigins();
|
|
}
|
|
|
|
function isPointOnCard(clientX, clientY) {
|
|
if (!imageEl) {
|
|
return false;
|
|
}
|
|
|
|
const rect = imageEl.getBoundingClientRect();
|
|
const naturalWidth = imageEl.naturalWidth;
|
|
const naturalHeight = imageEl.naturalHeight;
|
|
|
|
if (!rect.width || !rect.height || !naturalWidth || !naturalHeight) {
|
|
return true;
|
|
}
|
|
|
|
const frameAspect = rect.width / rect.height;
|
|
const imageAspect = naturalWidth / naturalHeight;
|
|
|
|
let renderWidth = rect.width;
|
|
let renderHeight = rect.height;
|
|
if (imageAspect > frameAspect) {
|
|
renderHeight = rect.width / imageAspect;
|
|
} else {
|
|
renderWidth = rect.height * imageAspect;
|
|
}
|
|
|
|
const left = rect.left + (rect.width - renderWidth) / 2;
|
|
const top = rect.top + (rect.height - renderHeight) / 2;
|
|
const right = left + renderWidth;
|
|
const bottom = top + renderHeight;
|
|
|
|
return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom;
|
|
}
|
|
|
|
function ensure() {
|
|
if (overlayEl && imageEl && overlayImageEl) {
|
|
return;
|
|
}
|
|
|
|
overlayEl = document.createElement("div");
|
|
overlayEl.setAttribute("aria-hidden", "true");
|
|
overlayEl.setAttribute("role", "dialog");
|
|
overlayEl.setAttribute("aria-modal", "true");
|
|
overlayEl.tabIndex = -1;
|
|
overlayEl.style.position = "fixed";
|
|
overlayEl.style.inset = "0";
|
|
overlayEl.style.display = "none";
|
|
overlayEl.style.zIndex = "9999";
|
|
overlayEl.style.pointerEvents = "none";
|
|
|
|
helpButtonEl = document.createElement("button");
|
|
helpButtonEl.type = "button";
|
|
helpButtonEl.textContent = "Help";
|
|
helpButtonEl.style.position = "fixed";
|
|
helpButtonEl.style.top = "24px";
|
|
helpButtonEl.style.left = "24px";
|
|
helpButtonEl.style.display = "none";
|
|
helpButtonEl.style.alignItems = "center";
|
|
helpButtonEl.style.justifyContent = "center";
|
|
helpButtonEl.style.border = "1px solid rgba(255, 255, 255, 0.2)";
|
|
helpButtonEl.style.background = "rgba(15, 23, 42, 0.84)";
|
|
helpButtonEl.style.color = "#f8fafc";
|
|
helpButtonEl.style.borderRadius = "999px";
|
|
helpButtonEl.style.padding = "10px 14px";
|
|
helpButtonEl.style.font = "600 13px/1.1 sans-serif";
|
|
helpButtonEl.style.cursor = "pointer";
|
|
helpButtonEl.style.backdropFilter = "blur(12px)";
|
|
helpButtonEl.style.pointerEvents = "auto";
|
|
helpButtonEl.style.zIndex = "2";
|
|
|
|
helpPanelEl = document.createElement("div");
|
|
helpPanelEl.style.position = "fixed";
|
|
helpPanelEl.style.top = "72px";
|
|
helpPanelEl.style.left = "24px";
|
|
helpPanelEl.style.display = "none";
|
|
helpPanelEl.style.flexDirection = "column";
|
|
helpPanelEl.style.gap = "8px";
|
|
helpPanelEl.style.width = "min(320px, calc(100vw - 48px))";
|
|
helpPanelEl.style.padding = "14px 16px";
|
|
helpPanelEl.style.borderRadius = "18px";
|
|
helpPanelEl.style.background = "rgba(2, 6, 23, 0.88)";
|
|
helpPanelEl.style.border = "1px solid rgba(148, 163, 184, 0.16)";
|
|
helpPanelEl.style.color = "#f8fafc";
|
|
helpPanelEl.style.boxShadow = "0 16px 42px rgba(0, 0, 0, 0.34)";
|
|
helpPanelEl.style.backdropFilter = "blur(12px)";
|
|
helpPanelEl.style.pointerEvents = "auto";
|
|
helpPanelEl.style.zIndex = "2";
|
|
|
|
const helpTitleEl = document.createElement("div");
|
|
helpTitleEl.textContent = "Lightbox Shortcuts";
|
|
helpTitleEl.style.font = "700 13px/1.3 sans-serif";
|
|
|
|
const helpListEl = document.createElement("div");
|
|
helpListEl.style.display = "flex";
|
|
helpListEl.style.flexDirection = "column";
|
|
helpListEl.style.gap = "6px";
|
|
helpListEl.style.font = "500 12px/1.4 sans-serif";
|
|
helpListEl.style.color = "rgba(226, 232, 240, 0.92)";
|
|
|
|
[
|
|
"Click card: toggle zoom at the clicked point",
|
|
"Left / Right: move cards, or move overlay card in compare mode",
|
|
"Overlay: pick a second card to compare",
|
|
"Space: swap base and overlay cards",
|
|
"R: rotate base card, or rotate overlay card in compare mode",
|
|
"+ / -: zoom in or out in steps",
|
|
"W A S D: pan while zoomed",
|
|
"Escape or backdrop click: close"
|
|
].forEach((line) => {
|
|
const lineEl = document.createElement("div");
|
|
lineEl.textContent = line;
|
|
helpListEl.appendChild(lineEl);
|
|
});
|
|
|
|
helpPanelEl.append(helpTitleEl, helpListEl);
|
|
|
|
backdropEl = document.createElement("button");
|
|
backdropEl.type = "button";
|
|
backdropEl.setAttribute("aria-label", "Close enlarged tarot card");
|
|
backdropEl.style.position = "absolute";
|
|
backdropEl.style.inset = "0";
|
|
backdropEl.style.border = "none";
|
|
backdropEl.style.padding = "0";
|
|
backdropEl.style.margin = "0";
|
|
backdropEl.style.background = "rgba(0, 0, 0, 0.82)";
|
|
backdropEl.style.cursor = "pointer";
|
|
|
|
toolbarEl = document.createElement("div");
|
|
toolbarEl.style.position = "fixed";
|
|
toolbarEl.style.top = "24px";
|
|
toolbarEl.style.right = "24px";
|
|
toolbarEl.style.display = "flex";
|
|
toolbarEl.style.flexDirection = "column";
|
|
toolbarEl.style.alignItems = "flex-end";
|
|
toolbarEl.style.gap = "8px";
|
|
toolbarEl.style.pointerEvents = "auto";
|
|
toolbarEl.style.zIndex = "2";
|
|
|
|
compareButtonEl = document.createElement("button");
|
|
compareButtonEl.type = "button";
|
|
compareButtonEl.textContent = "Overlay";
|
|
compareButtonEl.style.border = "1px solid rgba(255, 255, 255, 0.2)";
|
|
compareButtonEl.style.background = "rgba(15, 23, 42, 0.84)";
|
|
compareButtonEl.style.color = "#f8fafc";
|
|
compareButtonEl.style.borderRadius = "999px";
|
|
compareButtonEl.style.padding = "10px 14px";
|
|
compareButtonEl.style.font = "600 13px/1.1 sans-serif";
|
|
compareButtonEl.style.cursor = "pointer";
|
|
compareButtonEl.style.backdropFilter = "blur(12px)";
|
|
|
|
zoomControlEl = document.createElement("label");
|
|
zoomControlEl.style.display = "flex";
|
|
zoomControlEl.style.alignItems = "center";
|
|
zoomControlEl.style.gap = "8px";
|
|
zoomControlEl.style.padding = "10px 14px";
|
|
zoomControlEl.style.border = "1px solid rgba(255, 255, 255, 0.2)";
|
|
zoomControlEl.style.borderRadius = "999px";
|
|
zoomControlEl.style.background = "rgba(15, 23, 42, 0.84)";
|
|
zoomControlEl.style.color = "#f8fafc";
|
|
zoomControlEl.style.font = "600 12px/1.1 sans-serif";
|
|
zoomControlEl.style.backdropFilter = "blur(12px)";
|
|
|
|
const zoomTextEl = document.createElement("span");
|
|
zoomTextEl.textContent = "Zoom";
|
|
|
|
zoomSliderEl = document.createElement("input");
|
|
zoomSliderEl.type = "range";
|
|
zoomSliderEl.min = "100";
|
|
zoomSliderEl.max = String(Math.round(LIGHTBOX_ZOOM_SCALE * 100));
|
|
zoomSliderEl.step = "10";
|
|
zoomSliderEl.value = String(Math.round(LIGHTBOX_ZOOM_SCALE * 100));
|
|
zoomSliderEl.style.width = "110px";
|
|
zoomSliderEl.style.cursor = "pointer";
|
|
|
|
zoomValueEl = document.createElement("span");
|
|
zoomValueEl.textContent = `${Math.round(LIGHTBOX_ZOOM_SCALE * 100)}%`;
|
|
zoomValueEl.style.minWidth = "42px";
|
|
zoomValueEl.style.textAlign = "right";
|
|
|
|
zoomControlEl.append(zoomTextEl, zoomSliderEl, zoomValueEl);
|
|
|
|
opacityControlEl = document.createElement("label");
|
|
opacityControlEl.style.display = "none";
|
|
opacityControlEl.style.alignItems = "center";
|
|
opacityControlEl.style.gap = "8px";
|
|
opacityControlEl.style.padding = "10px 14px";
|
|
opacityControlEl.style.border = "1px solid rgba(255, 255, 255, 0.2)";
|
|
opacityControlEl.style.borderRadius = "999px";
|
|
opacityControlEl.style.background = "rgba(15, 23, 42, 0.84)";
|
|
opacityControlEl.style.color = "#f8fafc";
|
|
opacityControlEl.style.font = "600 12px/1.1 sans-serif";
|
|
opacityControlEl.style.backdropFilter = "blur(12px)";
|
|
|
|
const opacityTextEl = document.createElement("span");
|
|
opacityTextEl.textContent = "Overlay";
|
|
|
|
opacitySliderEl = document.createElement("input");
|
|
opacitySliderEl.type = "range";
|
|
opacitySliderEl.min = "5";
|
|
opacitySliderEl.max = "100";
|
|
opacitySliderEl.step = "5";
|
|
opacitySliderEl.value = String(Math.round(LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY * 100));
|
|
opacitySliderEl.style.width = "110px";
|
|
opacitySliderEl.style.cursor = "pointer";
|
|
|
|
opacityValueEl = document.createElement("span");
|
|
opacityValueEl.textContent = `${Math.round(LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY * 100)}%`;
|
|
opacityValueEl.style.minWidth = "34px";
|
|
opacityValueEl.style.textAlign = "right";
|
|
|
|
opacityControlEl.append(opacityTextEl, opacitySliderEl, opacityValueEl);
|
|
|
|
toolbarEl.append(compareButtonEl, zoomControlEl, opacityControlEl);
|
|
|
|
stageEl = document.createElement("div");
|
|
stageEl.style.position = "fixed";
|
|
stageEl.style.top = "0";
|
|
stageEl.style.right = "0";
|
|
stageEl.style.bottom = "0";
|
|
stageEl.style.left = "0";
|
|
stageEl.style.pointerEvents = "auto";
|
|
stageEl.style.overflow = "visible";
|
|
stageEl.style.transition = "top 220ms ease, right 220ms ease, bottom 220ms ease, left 220ms ease, width 220ms ease, height 220ms ease, transform 220ms ease";
|
|
stageEl.style.transform = "none";
|
|
stageEl.style.zIndex = "1";
|
|
|
|
frameEl = document.createElement("div");
|
|
frameEl.style.position = "relative";
|
|
frameEl.style.width = "100%";
|
|
frameEl.style.height = "100%";
|
|
frameEl.style.overflow = "hidden";
|
|
frameEl.style.transition = "border-radius 220ms ease, background 220ms ease, box-shadow 220ms ease";
|
|
|
|
baseLayerEl = document.createElement("div");
|
|
baseLayerEl.style.position = "absolute";
|
|
baseLayerEl.style.inset = "0";
|
|
baseLayerEl.style.transform = "scale(1)";
|
|
baseLayerEl.style.transformOrigin = "50% 50%";
|
|
baseLayerEl.style.transition = "transform 120ms ease-out";
|
|
|
|
overlayLayerEl = document.createElement("div");
|
|
overlayLayerEl.style.position = "absolute";
|
|
overlayLayerEl.style.inset = "0";
|
|
overlayLayerEl.style.transform = "scale(1)";
|
|
overlayLayerEl.style.transformOrigin = "50% 50%";
|
|
overlayLayerEl.style.transition = "transform 120ms ease-out";
|
|
overlayLayerEl.style.pointerEvents = "none";
|
|
|
|
imageEl = document.createElement("img");
|
|
imageEl.alt = "Tarot card enlarged image";
|
|
imageEl.style.width = "100%";
|
|
imageEl.style.height = "100%";
|
|
imageEl.style.objectFit = "contain";
|
|
imageEl.style.cursor = "zoom-in";
|
|
imageEl.style.transform = "rotate(0deg)";
|
|
imageEl.style.transformOrigin = "center center";
|
|
imageEl.style.transition = "transform 120ms ease-out, opacity 180ms ease";
|
|
imageEl.style.userSelect = "none";
|
|
|
|
overlayImageEl = document.createElement("img");
|
|
overlayImageEl.alt = "Tarot card overlay image";
|
|
overlayImageEl.style.width = "100%";
|
|
overlayImageEl.style.height = "100%";
|
|
overlayImageEl.style.objectFit = "contain";
|
|
overlayImageEl.style.opacity = String(LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY);
|
|
overlayImageEl.style.pointerEvents = "none";
|
|
overlayImageEl.style.display = "none";
|
|
overlayImageEl.style.transform = "rotate(0deg)";
|
|
overlayImageEl.style.transformOrigin = "center center";
|
|
overlayImageEl.style.transition = "opacity 180ms ease";
|
|
|
|
function createInfoPanel() {
|
|
const panelEl = document.createElement("div");
|
|
panelEl.style.position = "absolute";
|
|
panelEl.style.display = "none";
|
|
panelEl.style.flexDirection = "column";
|
|
panelEl.style.gap = "10px";
|
|
panelEl.style.padding = "14px 16px";
|
|
panelEl.style.borderRadius = "18px";
|
|
panelEl.style.background = "rgba(2, 6, 23, 0.8)";
|
|
panelEl.style.border = "1px solid rgba(148, 163, 184, 0.16)";
|
|
panelEl.style.color = "#f8fafc";
|
|
panelEl.style.backdropFilter = "blur(12px)";
|
|
panelEl.style.boxShadow = "0 16px 42px rgba(0, 0, 0, 0.34)";
|
|
panelEl.style.transition = "opacity 180ms ease, transform 180ms ease";
|
|
panelEl.style.transform = "translateY(-50%)";
|
|
panelEl.style.pointerEvents = "none";
|
|
panelEl.style.maxHeight = "min(78vh, 760px)";
|
|
panelEl.style.overflowY = "auto";
|
|
|
|
const titleEl = document.createElement("div");
|
|
titleEl.style.font = "700 13px/1.3 sans-serif";
|
|
titleEl.style.color = "#f8fafc";
|
|
|
|
const groupsEl = document.createElement("div");
|
|
groupsEl.style.display = "flex";
|
|
groupsEl.style.flexDirection = "column";
|
|
groupsEl.style.gap = "0";
|
|
|
|
const hintEl = document.createElement("div");
|
|
hintEl.style.font = "500 11px/1.35 sans-serif";
|
|
hintEl.style.color = "rgba(226, 232, 240, 0.82)";
|
|
|
|
panelEl.append(titleEl, groupsEl, hintEl);
|
|
return { panelEl, titleEl, groupsEl, hintEl };
|
|
}
|
|
|
|
const primaryPanel = createInfoPanel();
|
|
primaryInfoEl = primaryPanel.panelEl;
|
|
primaryTitleEl = primaryPanel.titleEl;
|
|
primaryGroupsEl = primaryPanel.groupsEl;
|
|
primaryHintEl = primaryPanel.hintEl;
|
|
|
|
const secondaryPanel = createInfoPanel();
|
|
secondaryInfoEl = secondaryPanel.panelEl;
|
|
secondaryTitleEl = secondaryPanel.titleEl;
|
|
secondaryGroupsEl = secondaryPanel.groupsEl;
|
|
secondaryHintEl = secondaryPanel.hintEl;
|
|
baseLayerEl.appendChild(imageEl);
|
|
overlayLayerEl.appendChild(overlayImageEl);
|
|
frameEl.append(baseLayerEl, overlayLayerEl);
|
|
stageEl.append(frameEl, primaryInfoEl, secondaryInfoEl);
|
|
overlayEl.append(backdropEl, stageEl, toolbarEl, helpButtonEl, helpPanelEl);
|
|
|
|
const close = () => {
|
|
if (!overlayEl || !imageEl || !overlayImageEl) {
|
|
return;
|
|
}
|
|
|
|
lightboxState.isOpen = false;
|
|
lightboxState.compareMode = false;
|
|
lightboxState.allowOverlayCompare = false;
|
|
lightboxState.primaryCard = null;
|
|
lightboxState.secondaryCard = null;
|
|
lightboxState.sequenceIds = [];
|
|
lightboxState.resolveCardById = null;
|
|
lightboxState.onSelectCardId = null;
|
|
lightboxState.overlayOpacity = LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY;
|
|
lightboxState.zoomScale = LIGHTBOX_ZOOM_SCALE;
|
|
lightboxState.helpOpen = false;
|
|
overlayEl.style.display = "none";
|
|
overlayEl.setAttribute("aria-hidden", "true");
|
|
imageEl.removeAttribute("src");
|
|
imageEl.alt = "Tarot card enlarged image";
|
|
overlayImageEl.removeAttribute("src");
|
|
overlayImageEl.alt = "";
|
|
overlayImageEl.style.display = "none";
|
|
resetZoom();
|
|
syncHelpUi();
|
|
syncComparePanels();
|
|
syncOpacityControl();
|
|
|
|
if (previousFocusedEl instanceof HTMLElement) {
|
|
previousFocusedEl.focus({ preventScroll: true });
|
|
}
|
|
previousFocusedEl = null;
|
|
};
|
|
|
|
function toggleCompareMode() {
|
|
if (!lightboxState.allowOverlayCompare || !lightboxState.primaryCard) {
|
|
return;
|
|
}
|
|
|
|
lightboxState.compareMode = !lightboxState.compareMode;
|
|
if (!lightboxState.compareMode) {
|
|
clearSecondaryCard();
|
|
}
|
|
applyComparePresentation();
|
|
}
|
|
|
|
function setSecondaryCard(cardRequest, syncSelection = false) {
|
|
const normalizedCard = normalizeCardRequest(cardRequest);
|
|
if (!normalizedCard.src || !normalizedCard.cardId || normalizedCard.cardId === lightboxState.primaryCard?.cardId) {
|
|
return false;
|
|
}
|
|
|
|
lightboxState.secondaryCard = normalizedCard;
|
|
overlayImageEl.src = normalizedCard.src;
|
|
overlayImageEl.alt = normalizedCard.altText;
|
|
overlayImageEl.style.display = "block";
|
|
overlayImageEl.style.opacity = String(lightboxState.overlayOpacity);
|
|
if (syncSelection && typeof lightboxState.onSelectCardId === "function") {
|
|
lightboxState.onSelectCardId(normalizedCard.cardId);
|
|
}
|
|
applyComparePresentation();
|
|
return true;
|
|
}
|
|
|
|
function stepSecondaryCard(direction) {
|
|
const sequence = Array.isArray(lightboxState.sequenceIds) ? lightboxState.sequenceIds : [];
|
|
if (!lightboxState.compareMode || sequence.length < 2 || typeof lightboxState.resolveCardById !== "function") {
|
|
return;
|
|
}
|
|
|
|
const anchorId = lightboxState.secondaryCard?.cardId || lightboxState.primaryCard?.cardId;
|
|
const startIndex = sequence.indexOf(anchorId);
|
|
if (startIndex < 0) {
|
|
return;
|
|
}
|
|
|
|
for (let offset = 1; offset <= sequence.length; offset += 1) {
|
|
const nextIndex = (startIndex + direction * offset + sequence.length) % sequence.length;
|
|
const nextCardId = sequence[nextIndex];
|
|
if (!nextCardId || nextCardId === lightboxState.primaryCard?.cardId) {
|
|
continue;
|
|
}
|
|
|
|
const nextCard = resolveCardRequestById(nextCardId);
|
|
if (nextCard && setSecondaryCard(nextCard, true)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function stepPrimaryCard(direction) {
|
|
const sequence = Array.isArray(lightboxState.sequenceIds) ? lightboxState.sequenceIds : [];
|
|
if (lightboxState.compareMode || sequence.length < 2 || typeof lightboxState.resolveCardById !== "function") {
|
|
return;
|
|
}
|
|
|
|
const startIndex = sequence.indexOf(lightboxState.primaryCard?.cardId);
|
|
if (startIndex < 0) {
|
|
return;
|
|
}
|
|
|
|
const nextIndex = (startIndex + direction + sequence.length) % sequence.length;
|
|
const nextCardId = sequence[nextIndex];
|
|
const nextCard = resolveCardRequestById(nextCardId);
|
|
if (!nextCard?.src) {
|
|
return;
|
|
}
|
|
|
|
lightboxState.primaryCard = nextCard;
|
|
imageEl.src = nextCard.src;
|
|
imageEl.alt = nextCard.altText;
|
|
resetZoom();
|
|
clearSecondaryCard();
|
|
if (typeof lightboxState.onSelectCardId === "function") {
|
|
lightboxState.onSelectCardId(nextCard.cardId);
|
|
}
|
|
applyComparePresentation();
|
|
}
|
|
|
|
function swapCompareCards() {
|
|
if (!lightboxState.compareMode || !lightboxState.primaryCard?.src || !lightboxState.secondaryCard?.src) {
|
|
return;
|
|
}
|
|
|
|
const nextPrimaryCard = lightboxState.secondaryCard;
|
|
const nextSecondaryCard = lightboxState.primaryCard;
|
|
|
|
lightboxState.primaryCard = nextPrimaryCard;
|
|
lightboxState.secondaryCard = nextSecondaryCard;
|
|
|
|
imageEl.src = nextPrimaryCard.src;
|
|
imageEl.alt = nextPrimaryCard.altText;
|
|
overlayImageEl.src = nextSecondaryCard.src;
|
|
overlayImageEl.alt = nextSecondaryCard.altText;
|
|
overlayImageEl.style.display = "block";
|
|
overlayImageEl.style.opacity = String(lightboxState.overlayOpacity);
|
|
|
|
if (typeof lightboxState.onSelectCardId === "function") {
|
|
lightboxState.onSelectCardId(nextPrimaryCard.cardId);
|
|
}
|
|
|
|
applyComparePresentation();
|
|
}
|
|
|
|
function shouldIgnoreGlobalKeydown(event) {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLElement)) {
|
|
return false;
|
|
}
|
|
|
|
if (!overlayEl?.contains(target)) {
|
|
return false;
|
|
}
|
|
|
|
return target instanceof HTMLInputElement
|
|
|| target instanceof HTMLTextAreaElement
|
|
|| target instanceof HTMLSelectElement
|
|
|| target instanceof HTMLButtonElement;
|
|
}
|
|
|
|
function restoreLightboxFocus() {
|
|
if (!overlayEl || !lightboxState.isOpen) {
|
|
return;
|
|
}
|
|
|
|
requestAnimationFrame(() => {
|
|
if (overlayEl && lightboxState.isOpen) {
|
|
overlayEl.focus({ preventScroll: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
backdropEl.addEventListener("click", close);
|
|
helpButtonEl.addEventListener("click", () => {
|
|
lightboxState.helpOpen = !lightboxState.helpOpen;
|
|
syncHelpUi();
|
|
restoreLightboxFocus();
|
|
});
|
|
compareButtonEl.addEventListener("click", () => {
|
|
toggleCompareMode();
|
|
restoreLightboxFocus();
|
|
});
|
|
zoomSliderEl.addEventListener("input", () => {
|
|
setZoomScale(Number(zoomSliderEl.value) / 100);
|
|
});
|
|
zoomSliderEl.addEventListener("change", restoreLightboxFocus);
|
|
zoomSliderEl.addEventListener("pointerup", restoreLightboxFocus);
|
|
opacitySliderEl.addEventListener("input", () => {
|
|
setOverlayOpacity(Number(opacitySliderEl.value) / 100);
|
|
});
|
|
opacitySliderEl.addEventListener("change", restoreLightboxFocus);
|
|
opacitySliderEl.addEventListener("pointerup", restoreLightboxFocus);
|
|
|
|
imageEl.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
if (!isPointOnCard(event.clientX, event.clientY)) {
|
|
if (lightboxState.compareMode) {
|
|
return;
|
|
}
|
|
|
|
close();
|
|
return;
|
|
}
|
|
|
|
if (!zoomed) {
|
|
zoomed = true;
|
|
applyZoomTransform();
|
|
updateZoomOrigin(event.clientX, event.clientY);
|
|
applyComparePresentation();
|
|
return;
|
|
}
|
|
|
|
resetZoom();
|
|
applyComparePresentation();
|
|
});
|
|
|
|
imageEl.addEventListener("mousemove", (event) => {
|
|
updateZoomOrigin(event.clientX, event.clientY);
|
|
});
|
|
|
|
imageEl.addEventListener("mouseleave", () => {
|
|
if (zoomed) {
|
|
lightboxState.zoomOriginX = 50;
|
|
lightboxState.zoomOriginY = 50;
|
|
applyTransformOrigins();
|
|
}
|
|
});
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape") {
|
|
close();
|
|
return;
|
|
}
|
|
|
|
if (shouldIgnoreGlobalKeydown(event)) {
|
|
return;
|
|
}
|
|
|
|
if (lightboxState.isOpen && event.code === "Space" && lightboxState.compareMode && hasSecondaryCard()) {
|
|
event.preventDefault();
|
|
swapCompareCards();
|
|
return;
|
|
}
|
|
|
|
if (lightboxState.isOpen && isRotateKey(event)) {
|
|
event.preventDefault();
|
|
toggleRotation();
|
|
return;
|
|
}
|
|
|
|
if (lightboxState.isOpen && isZoomInKey(event)) {
|
|
event.preventDefault();
|
|
stepZoom(1);
|
|
return;
|
|
}
|
|
|
|
if (lightboxState.isOpen && isZoomOutKey(event)) {
|
|
event.preventDefault();
|
|
stepZoom(-1);
|
|
return;
|
|
}
|
|
|
|
if (lightboxState.isOpen && zoomed && isPanUpKey(event)) {
|
|
event.preventDefault();
|
|
stepPan(0, -LIGHTBOX_PAN_STEP);
|
|
return;
|
|
}
|
|
|
|
if (lightboxState.isOpen && zoomed && isPanLeftKey(event)) {
|
|
event.preventDefault();
|
|
stepPan(-LIGHTBOX_PAN_STEP, 0);
|
|
return;
|
|
}
|
|
|
|
if (lightboxState.isOpen && zoomed && isPanDownKey(event)) {
|
|
event.preventDefault();
|
|
stepPan(0, LIGHTBOX_PAN_STEP);
|
|
return;
|
|
}
|
|
|
|
if (lightboxState.isOpen && zoomed && isPanRightKey(event)) {
|
|
event.preventDefault();
|
|
stepPan(LIGHTBOX_PAN_STEP, 0);
|
|
return;
|
|
}
|
|
|
|
if (!lightboxState.isOpen || !LIGHTBOX_COMPARE_SEQUENCE_STEP_KEYS.has(event.key)) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
if (lightboxState.compareMode) {
|
|
stepSecondaryCard(event.key === "ArrowRight" ? 1 : -1);
|
|
return;
|
|
}
|
|
|
|
stepPrimaryCard(event.key === "ArrowRight" ? 1 : -1);
|
|
});
|
|
|
|
document.body.appendChild(overlayEl);
|
|
|
|
overlayEl.closeLightbox = close;
|
|
overlayEl.setSecondaryCard = setSecondaryCard;
|
|
overlayEl.applyComparePresentation = applyComparePresentation;
|
|
}
|
|
|
|
function open(srcOrOptions, altText, extraOptions) {
|
|
const request = normalizeOpenRequest(srcOrOptions, altText, extraOptions);
|
|
const normalizedPrimary = normalizeCardRequest(request);
|
|
|
|
if (!normalizedPrimary.src) {
|
|
return;
|
|
}
|
|
|
|
ensure();
|
|
if (!overlayEl || !imageEl || !overlayImageEl) {
|
|
return;
|
|
}
|
|
|
|
const canCompare = Boolean(
|
|
request.allowOverlayCompare
|
|
&& normalizedPrimary.cardId
|
|
&& Array.isArray(request.sequenceIds)
|
|
&& request.sequenceIds.length > 1
|
|
&& typeof request.resolveCardById === "function"
|
|
);
|
|
|
|
if (lightboxState.isOpen && lightboxState.compareMode && lightboxState.allowOverlayCompare && canCompare && normalizedPrimary.cardId) {
|
|
if (normalizedPrimary.cardId === lightboxState.primaryCard?.cardId) {
|
|
return;
|
|
}
|
|
overlayEl.setSecondaryCard?.(normalizedPrimary, false);
|
|
return;
|
|
}
|
|
|
|
lightboxState.isOpen = true;
|
|
lightboxState.compareMode = false;
|
|
lightboxState.allowOverlayCompare = canCompare;
|
|
lightboxState.primaryCard = normalizedPrimary;
|
|
lightboxState.sequenceIds = canCompare ? [...request.sequenceIds] : [];
|
|
lightboxState.resolveCardById = canCompare ? request.resolveCardById : null;
|
|
lightboxState.onSelectCardId = canCompare && typeof request.onSelectCardId === "function"
|
|
? request.onSelectCardId
|
|
: null;
|
|
lightboxState.overlayOpacity = LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY;
|
|
lightboxState.zoomScale = LIGHTBOX_ZOOM_SCALE;
|
|
lightboxState.helpOpen = false;
|
|
|
|
imageEl.src = normalizedPrimary.src;
|
|
imageEl.alt = normalizedPrimary.altText;
|
|
clearSecondaryCard();
|
|
resetZoom();
|
|
previousFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
overlayEl.style.display = "block";
|
|
overlayEl.setAttribute("aria-hidden", "false");
|
|
overlayEl.applyComparePresentation?.();
|
|
overlayEl.focus({ preventScroll: true });
|
|
}
|
|
|
|
window.TarotUiLightbox = {
|
|
...(window.TarotUiLightbox || {}),
|
|
open
|
|
};
|
|
})(); |