rosary: add mystery images for all four mystery types with PiP fullscreen
All checks were successful
CI / update (push) Successful in 1m29s
Generalize mystery images from sorrowful-only to all mystery types (joyful, sorrowful, glorious, luminous). Add PiP fullscreen mode with tap-to-show controls and double-tap to toggle enlarged/fullscreen.
@@ -10,6 +10,7 @@ interface PipOptions {
|
|||||||
initialCorner?: Corner;
|
initialCorner?: Corner;
|
||||||
smallHeight?: number;
|
smallHeight?: number;
|
||||||
largeHeight?: number;
|
largeHeight?: number;
|
||||||
|
fullscreenEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
||||||
@@ -17,13 +18,16 @@ type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|||||||
export function createPip(opts: PipOptions = {}) {
|
export function createPip(opts: PipOptions = {}) {
|
||||||
const margin = opts.margin ?? 16;
|
const margin = opts.margin ?? 16;
|
||||||
const tapThreshold = opts.tapThreshold ?? 10;
|
const tapThreshold = opts.tapThreshold ?? 10;
|
||||||
const doubleTapMs = opts.doubleTapMs ?? 400;
|
const doubleTapMs = opts.doubleTapMs ?? 250;
|
||||||
const smallVh = opts.smallHeight ?? 25;
|
const smallVh = opts.smallHeight ?? 25;
|
||||||
const largeVh = opts.largeHeight ?? 37.5;
|
const largeVh = opts.largeHeight ?? 37.5;
|
||||||
|
const fullscreenEnabled = opts.fullscreenEnabled ?? false;
|
||||||
|
|
||||||
let corner: Corner = $state(opts.initialCorner ?? 'bottom-right');
|
let corner: Corner = $state(opts.initialCorner ?? 'bottom-right');
|
||||||
let dragging = $state(false);
|
let dragging = $state(false);
|
||||||
let enlarged = $state(false);
|
let enlarged = $state(false);
|
||||||
|
let fullscreen = $state(false);
|
||||||
|
let showControls = $state(false);
|
||||||
|
|
||||||
let dragOffset = { x: 0, y: 0 };
|
let dragOffset = { x: 0, y: 0 };
|
||||||
let dragPos = { x: 0, y: 0 };
|
let dragPos = { x: 0, y: 0 };
|
||||||
@@ -59,7 +63,7 @@ export function createPip(opts: PipOptions = {}) {
|
|||||||
const pos = getCornerPos(c, target);
|
const pos = getCornerPos(c, target);
|
||||||
corner = c;
|
corner = c;
|
||||||
dragPos = pos;
|
dragPos = pos;
|
||||||
target.style.transition = 'transform 0.25s ease';
|
target.style.transition = 'transform 0.25s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||||
target.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
target.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||||
target.addEventListener('transitionend', () => { target.style.transition = ''; }, { once: true });
|
target.addEventListener('transitionend', () => { target.style.transition = ''; }, { once: true });
|
||||||
}
|
}
|
||||||
@@ -82,15 +86,77 @@ export function createPip(opts: PipOptions = {}) {
|
|||||||
if (corner.includes('bottom')) newY = rect.bottom - newH;
|
if (corner.includes('bottom')) newY = rect.bottom - newH;
|
||||||
|
|
||||||
dragPos = { x: newX, y: newY };
|
dragPos = { x: newX, y: newY };
|
||||||
el.style.transition = 'transform 0.25s ease';
|
el.style.transition = 'transform 0.25s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||||
el.style.transform = `translate(${newX}px, ${newY}px)`;
|
el.style.transform = `translate(${newX}px, ${newY}px)`;
|
||||||
el.addEventListener('transitionend', () => {
|
el.addEventListener('transitionend', () => {
|
||||||
el!.style.transition = '';
|
el!.style.transition = '';
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleFullscreen() {
|
||||||
|
if (!el) return;
|
||||||
|
showControls = false;
|
||||||
|
lastTapTime = 0; // cancel any pending single-tap timeout
|
||||||
|
const img = el.querySelector('img') as HTMLImageElement | null;
|
||||||
|
|
||||||
|
if (!fullscreen) {
|
||||||
|
// Enter fullscreen: dark bg appears, image grows + moves to center
|
||||||
|
fullscreen = true;
|
||||||
|
el.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||||
|
el.style.transform = 'translate(0px, 0px)';
|
||||||
|
if (img) {
|
||||||
|
img.style.transition = 'height 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||||
|
img.style.height = '90vh';
|
||||||
|
img.style.maxWidth = '95vw';
|
||||||
|
img.style.maxHeight = '90vh';
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
if (el) el.style.transition = '';
|
||||||
|
if (img) img.style.transition = '';
|
||||||
|
}, 350);
|
||||||
|
} else {
|
||||||
|
// Exit fullscreen: image shrinks + moves to corner, then bg removed
|
||||||
|
const vh = window.innerHeight;
|
||||||
|
const vw = window.innerWidth;
|
||||||
|
const pipH = vh * (smallVh / 100);
|
||||||
|
let pipW = pipH;
|
||||||
|
if (img && img.naturalHeight > 0) {
|
||||||
|
pipW = pipH * (img.naturalWidth / img.naturalHeight);
|
||||||
|
}
|
||||||
|
const pos = {
|
||||||
|
x: corner.includes('right') ? vw - pipW - margin : margin,
|
||||||
|
y: corner.includes('bottom') ? vh - pipH - margin : margin,
|
||||||
|
};
|
||||||
|
dragPos = pos;
|
||||||
|
|
||||||
|
el.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||||
|
el.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||||
|
if (img) {
|
||||||
|
img.style.transition = 'height 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||||
|
img.style.height = `${smallVh}vh`;
|
||||||
|
img.style.maxWidth = '';
|
||||||
|
img.style.maxHeight = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fullscreen = false;
|
||||||
|
enlarged = false;
|
||||||
|
if (el) el.style.transition = '';
|
||||||
|
if (img) {
|
||||||
|
img.style.transition = '';
|
||||||
|
img.style.height = '';
|
||||||
|
}
|
||||||
|
}, 350);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function show(target: HTMLElement) {
|
function show(target: HTMLElement) {
|
||||||
el = target;
|
el = target;
|
||||||
|
showControls = false;
|
||||||
|
if (fullscreen) {
|
||||||
|
target.style.opacity = '1';
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pos = getCornerPos(corner, target);
|
const pos = getCornerPos(corner, target);
|
||||||
dragPos = pos;
|
dragPos = pos;
|
||||||
target.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
target.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||||
@@ -98,11 +164,25 @@ export function createPip(opts: PipOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
|
if (fullscreen) {
|
||||||
|
fullscreen = false;
|
||||||
|
showControls = false;
|
||||||
|
if (el) {
|
||||||
|
const img = el.querySelector('img') as HTMLElement | null;
|
||||||
|
if (img) {
|
||||||
|
img.style.transition = '';
|
||||||
|
img.style.height = '';
|
||||||
|
img.style.maxWidth = '';
|
||||||
|
img.style.maxHeight = '';
|
||||||
|
}
|
||||||
|
el.style.transition = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
if (el) el.style.opacity = '0';
|
if (el) el.style.opacity = '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
function reposition() {
|
function reposition() {
|
||||||
if (!el) return;
|
if (!el || fullscreen) return;
|
||||||
const pos = getCornerPos(corner, el);
|
const pos = getCornerPos(corner, el);
|
||||||
dragPos = pos;
|
dragPos = pos;
|
||||||
el.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
el.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||||
@@ -112,15 +192,19 @@ export function createPip(opts: PipOptions = {}) {
|
|||||||
if (!el) return;
|
if (!el) return;
|
||||||
dragging = true;
|
dragging = true;
|
||||||
dragMoved = false;
|
dragMoved = false;
|
||||||
|
|
||||||
|
if (!fullscreen) {
|
||||||
const r = el.getBoundingClientRect();
|
const r = el.getBoundingClientRect();
|
||||||
dragOffset = { x: e.clientX - r.left, y: e.clientY - r.top };
|
dragOffset = { x: e.clientX - r.left, y: e.clientY - r.top };
|
||||||
el.setPointerCapture(e.pointerId);
|
|
||||||
el.style.transition = '';
|
el.style.transition = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
el.setPointerCapture(e.pointerId);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onpointermove(e: PointerEvent) {
|
function onpointermove(e: PointerEvent) {
|
||||||
if (!dragging || !el) return;
|
if (!dragging || !el || fullscreen) return;
|
||||||
const x = e.clientX - dragOffset.x;
|
const x = e.clientX - dragOffset.x;
|
||||||
const y = e.clientY - dragOffset.y;
|
const y = e.clientY - dragOffset.y;
|
||||||
if (!dragMoved) {
|
if (!dragMoved) {
|
||||||
@@ -140,23 +224,40 @@ export function createPip(opts: PipOptions = {}) {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastTapTime < doubleTapMs) {
|
if (now - lastTapTime < doubleTapMs) {
|
||||||
lastTapTime = 0;
|
lastTapTime = 0;
|
||||||
|
if (fullscreen) {
|
||||||
|
toggleFullscreen(); // exit fullscreen
|
||||||
|
} else {
|
||||||
toggleEnlarged();
|
toggleEnlarged();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastTapTime = now;
|
lastTapTime = now;
|
||||||
|
if (fullscreenEnabled) {
|
||||||
|
// Delayed single tap: toggle controls (skipped if double-tap follows)
|
||||||
|
const tapTime = now;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (lastTapTime === tapTime) showControls = !showControls;
|
||||||
|
}, doubleTapMs);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!fullscreen) {
|
||||||
const r = el.getBoundingClientRect();
|
const r = el.getBoundingClientRect();
|
||||||
snapToCorner(el, nearestCorner(r.left, r.top, el));
|
snapToCorner(el, nearestCorner(r.left, r.top, el));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get corner() { return corner; },
|
get corner() { return corner; },
|
||||||
get dragging() { return dragging; },
|
get dragging() { return dragging; },
|
||||||
get enlarged() { return enlarged; },
|
get enlarged() { return enlarged; },
|
||||||
|
get fullscreen() { return fullscreen; },
|
||||||
|
get showControls() { return showControls; },
|
||||||
show,
|
show,
|
||||||
hide,
|
hide,
|
||||||
reposition,
|
reposition,
|
||||||
|
toggleFullscreen,
|
||||||
onpointerdown,
|
onpointerdown,
|
||||||
onpointermove,
|
onpointermove,
|
||||||
onpointerup
|
onpointerup
|
||||||
|
|||||||
@@ -323,63 +323,65 @@ let activeSection = $state("cross");
|
|||||||
let sectionElements = {};
|
let sectionElements = {};
|
||||||
let svgContainer;
|
let svgContainer;
|
||||||
|
|
||||||
|
// Mystery images with captions (per mystery type, keyed by decade number 1-5)
|
||||||
|
const allMysteryImages = {
|
||||||
|
freudenreich: new Map([
|
||||||
|
[1, { src: "/glaube/joyful/1-murilllo-annunciation.webp", artist: "Bartolomé Esteban Murillo", title: "The Annunciation", titleDe: "Die Verkündigung" }],
|
||||||
|
[2, { src: "/glaube/joyful/2-carl-bloch.the-visitation.1866.webp", artist: "Carl Bloch", title: "The Visitation", titleDe: "Die Heimsuchung", year: 1866 }],
|
||||||
|
[3, { src: "/glaube/joyful/3-adoration-of-the-shepards.webp", title: "Adoration of the Shepherds", titleDe: "Die Anbetung der Hirten" }],
|
||||||
|
[4, { src: "/glaube/joyful/4-vouet.presentation-in-the-temple.webp", artist: "Simon Vouet", title: "The Presentation in the Temple", titleDe: "Die Darstellung im Tempel" }],
|
||||||
|
[5, { src: "/glaube/joyful/5-carl-bloch.the-twelve-year-old-jesus-in-the-temple.1869.webp", artist: "Carl Bloch", title: "The Twelve-Year-Old Jesus in the Temple", titleDe: "Der zwölfjährige Jesus im Tempel", year: 1869 }],
|
||||||
|
]),
|
||||||
|
schmerzhaften: new Map([
|
||||||
|
[1, { src: "/glaube/sorrowful/1.carl-bloch.gethsemane.webp", artist: "Carl Bloch", title: "Gethsemane", titleDe: "Gethsemane", year: 1873 }],
|
||||||
|
[2, { src: "/glaube/sorrowful/2.wiliam-bouguereau.flagellation.webp", artist: "William-Adolphe Bouguereau", title: "The Flagellation of Our Lord Jesus Christ", titleDe: "Die Geisselung unseres Herrn Jesus Christus", year: 1880 }],
|
||||||
|
[3, { src: "/glaube/sorrowful/3.carl-bloch.mocking.webp", artist: "Carl Bloch", title: "The Mocking of Christ", titleDe: "Die Verspottung Christi", year: 1880 }],
|
||||||
|
[4, { src: "/glaube/sorrowful/4.lorenzo-lotto.carrying-the-cross.webp", artist: "Lorenzo Lotto", title: "Carrying the Cross", titleDe: "Die Kreuztragung", year: 1526 }],
|
||||||
|
[5, { src: "/glaube/sorrowful/5.alonso-cano.the-crucifixion.webp", artist: "Diego Velázquez", title: "Christ Crucified", titleDe: "Der gekreuzigte Christus", year: 1632 }],
|
||||||
|
]),
|
||||||
|
glorreichen: new Map([
|
||||||
|
[1, { src: "/glaube/glorious/1-carl-bloch.resurrection.webp", artist: "Carl Bloch", title: "The Resurrection", titleDe: "Die Auferstehung" }],
|
||||||
|
[2, { src: "/glaube/glorious/2-ascension.webp", title: "The Ascension", titleDe: "Die Himmelfahrt" }],
|
||||||
|
[3, { src: "/glaube/glorious/3-pentecost.webp", title: "Pentecost", titleDe: "Die Geistsendung" }],
|
||||||
|
[4, { src: "/glaube/glorious/4-giovanni-tiepolo.the-immaculate-conception.webp", artist: "Giovanni Battista Tiepolo", title: "The Immaculate Conception", titleDe: "Die Aufnahme Mariens in den Himmel" }],
|
||||||
|
[5, { src: "/glaube/glorious/5-diego-veazquez.coronation-mary.webp", artist: "Diego Velázquez", title: "Coronation of the Virgin", titleDe: "Die Krönung der Jungfrau", year: 1641 }],
|
||||||
|
]),
|
||||||
|
lichtreichen: new Map([
|
||||||
|
[1, { src: "/glaube/luminous/1-carl-bloch.the-baptism-of-christ.1870.webp", artist: "Carl Bloch", title: "The Baptism of Christ", titleDe: "Die Taufe Christi", year: 1870 }],
|
||||||
|
[2, { src: "/glaube/luminous/2-carl-bloch.the-wedding-at-cana.1870.webp", artist: "Carl Bloch", title: "The Wedding at Cana", titleDe: "Die Hochzeit zu Kana", year: 1870 }],
|
||||||
|
[3, { src: "/glaube/luminous/3-carl-bloch.the-sermon-on-the-mount.1877.jpg", artist: "Carl Bloch", title: "The Sermon on the Mount", titleDe: "Die Bergpredigt", year: 1877 }],
|
||||||
|
[4, { src: "/glaube/luminous/4-carl-bloch.transfiguration-of-christ.webp", artist: "Carl Bloch", title: "Transfiguration of Christ", titleDe: "Die Verklärung Christi" }],
|
||||||
|
[5, { src: "/glaube/luminous/5-carl-bloch.the-last-supper.webp", artist: "Carl Bloch", title: "The Last Supper", titleDe: "Das letzte Abendmahl" }],
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
// Whether the rosary has mystery images (stable, doesn't change during scroll)
|
// Whether the rosary has mystery images (stable, doesn't change during scroll)
|
||||||
const hasMysteryImages = $derived(showImages && selectedMystery === 'schmerzhaften');
|
const hasMysteryImages = $derived(showImages && (allMysteryImages[selectedMystery]?.size ?? 0) > 0);
|
||||||
|
|
||||||
// Mystery image scroll target based on active section
|
// Mystery image scroll target based on active section (returns decade number 1-5, or 'before'/'after')
|
||||||
function getMysteryScrollTarget(section) {
|
function getMysteryScrollTarget(section) {
|
||||||
switch (section) {
|
if (section === 'lbead2') return 1;
|
||||||
case 'cross':
|
const secretMatch = section.match(/^secret(\d)/);
|
||||||
return 'before';
|
if (secretMatch) {
|
||||||
case 'lbead2':
|
const num = parseInt(secretMatch[1]);
|
||||||
return 'garden';
|
return section.includes('_transition') ? num + 1 : num;
|
||||||
case 'secret1':
|
|
||||||
return 'garden';
|
|
||||||
case 'secret1_transition':
|
|
||||||
return 'flagellation';
|
|
||||||
case 'secret2':
|
|
||||||
return 'flagellation';
|
|
||||||
case 'secret2_transition':
|
|
||||||
return 'mocking';
|
|
||||||
case 'secret3':
|
|
||||||
return 'mocking';
|
|
||||||
case 'secret3_transition':
|
|
||||||
return 'carry';
|
|
||||||
case 'secret4':
|
|
||||||
return 'carry';
|
|
||||||
case 'secret4_transition':
|
|
||||||
case 'secret5':
|
|
||||||
return 'crucifixion';
|
|
||||||
case 'final_transition':
|
|
||||||
case 'final_salve':
|
|
||||||
case 'final_schlussgebet':
|
|
||||||
case 'final_michael':
|
|
||||||
case 'final_paternoster':
|
|
||||||
case 'final_cross':
|
|
||||||
return 'after';
|
|
||||||
default:
|
|
||||||
return 'before';
|
|
||||||
}
|
}
|
||||||
|
if (section.startsWith('final_')) return 'after';
|
||||||
|
return 'before';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mystery images with captions
|
|
||||||
const mysteryImages = new Map([
|
|
||||||
["garden", { src: "/glaube/sorrowful/1.carl-bloch.gethsemane.webp", artist: "Carl Bloch", title: "Gethsemane", titleDe: "Gethsemane", year: 1873 }],
|
|
||||||
["flagellation", { src: "/glaube/sorrowful/2.wiliam-bouguereau.flagellation.webp", artist: "William-Adolphe Bouguereau", title: "The Flagellation of Our Lord Jesus Christ", titleDe: "Die Geisselung unseres Herrn Jesus Christus", year: 1880 }],
|
|
||||||
["mocking", { src: "/glaube/sorrowful/3.carl-bloch.mocking.webp", artist: "Carl Bloch", title: "The Mocking of Christ", titleDe: "Die Verspottung Christi", year: 1880 }],
|
|
||||||
["carry", { src: "/glaube/sorrowful/4.lorenzo-lotto.carrying-the-cross.webp", artist: "Lorenzo Lotto", title: "Carrying the Cross", titleDe: "Die Kreuztragung", year: 1526 }],
|
|
||||||
["crucifixion", { src: "/glaube/sorrowful/5.alonso-cano.the-crucifixion.webp", artist: "Diego Velázquez", title: "Christ Crucified", titleDe: "Der gekreuzigte Christus", year: 1632 }]
|
|
||||||
]);
|
|
||||||
// Mobile PiP: which image src to show (null = hide)
|
// Mobile PiP: which image src to show (null = hide)
|
||||||
function getMysteryImage(mystery, section) {
|
function getMysteryImage(mystery, section) {
|
||||||
if (mystery !== 'schmerzhaften') return null;
|
const images = allMysteryImages[mystery];
|
||||||
|
if (!images || images.size === 0) return null;
|
||||||
const target = getMysteryScrollTarget(section);
|
const target = getMysteryScrollTarget(section);
|
||||||
return mysteryImages.get(target)?.src ?? null;
|
if (target === 'before' || target === 'after') return null;
|
||||||
|
return images.get(target)?.src ?? null;
|
||||||
}
|
}
|
||||||
const mysteryPipSrc = $derived(getMysteryImage(selectedMystery, activeSection));
|
const mysteryPipSrc = $derived(getMysteryImage(selectedMystery, activeSection));
|
||||||
|
|
||||||
// Mobile PiP drag/enlarge
|
// Mobile PiP drag/enlarge
|
||||||
const pip = createPip();
|
const pip = createPip({ fullscreenEnabled: true });
|
||||||
let rosaryPipEl = $state(null);
|
let rosaryPipEl = $state(null);
|
||||||
let lastPipSrc = $state(null);
|
let lastPipSrc = $state(null);
|
||||||
|
|
||||||
@@ -1487,6 +1489,54 @@ h1 {
|
|||||||
.mystery-pip.enlarged img {
|
.mystery-pip.enlarged img {
|
||||||
height: 37.5vh;
|
height: 37.5vh;
|
||||||
}
|
}
|
||||||
|
.mystery-pip.fullscreen {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba(0, 0, 0, 0.95);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.mystery-pip.fullscreen img {
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.pip-fullscreen-btn {
|
||||||
|
all: unset;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: transparent;
|
||||||
|
filter: drop-shadow(0 0 1px black);
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
outline: none;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
.pip-fullscreen-btn:hover,
|
||||||
|
.pip-fullscreen-btn:active {
|
||||||
|
transform: translate(-50%, -50%) scale(1.2);
|
||||||
|
}
|
||||||
|
.mystery-pip.fullscreen .pip-fullscreen-btn {
|
||||||
|
top: auto;
|
||||||
|
left: auto;
|
||||||
|
bottom: 10vw;
|
||||||
|
right: 10vw;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
.mystery-pip.fullscreen .pip-fullscreen-btn:hover,
|
||||||
|
.mystery-pip.fullscreen .pip-fullscreen-btn:active {
|
||||||
|
transform: scale(0.85);
|
||||||
|
}
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1200px) {
|
||||||
.mystery-pip {
|
.mystery-pip {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -1891,13 +1941,13 @@ h1 {
|
|||||||
<!-- Third column: Mystery images (desktop scrollable sticky) -->
|
<!-- Third column: Mystery images (desktop scrollable sticky) -->
|
||||||
<div class="mystery-image-column" bind:this={mysteryImageContainer}>
|
<div class="mystery-image-column" bind:this={mysteryImageContainer}>
|
||||||
{#if hasMysteryImages}
|
{#if hasMysteryImages}
|
||||||
|
{@const images = allMysteryImages[selectedMystery]}
|
||||||
<div class="mystery-image-pad" data-target="before"></div>
|
<div class="mystery-image-pad" data-target="before"></div>
|
||||||
{#each ["garden", "flagellation", "mocking", "carry", "crucifixion"] as target, i}
|
{#each [...images.entries()] as [num, img], i}
|
||||||
{@const img = mysteryImages.get(target)}
|
|
||||||
{#if i > 0}<div class="mystery-image-pad" data-target="between{i}"></div>{/if}
|
{#if i > 0}<div class="mystery-image-pad" data-target="between{i}"></div>{/if}
|
||||||
<figure data-target={target}>
|
<figure data-target={num}>
|
||||||
<img src={img.src} alt="{img.artist} — {isEnglish ? img.title : img.titleDe}">
|
<img src={img.src} alt="{img.artist ? `${img.artist} — ` : ''}{isEnglish ? img.title : img.titleDe}">
|
||||||
<figcaption>{img.artist}, <em>{isEnglish ? img.title : img.titleDe}</em>, {img.year}</figcaption>
|
<figcaption>{#if img.artist}{img.artist}, {/if}<em>{isEnglish ? img.title : img.titleDe}</em>{#if img.year}, {img.year}{/if}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="mystery-image-pad" data-target="after"></div>
|
<div class="mystery-image-pad" data-target="after"></div>
|
||||||
@@ -1912,6 +1962,7 @@ h1 {
|
|||||||
class="mystery-pip"
|
class="mystery-pip"
|
||||||
class:visible={!!mysteryPipSrc}
|
class:visible={!!mysteryPipSrc}
|
||||||
class:enlarged={pip.enlarged}
|
class:enlarged={pip.enlarged}
|
||||||
|
class:fullscreen={pip.fullscreen}
|
||||||
bind:this={rosaryPipEl}
|
bind:this={rosaryPipEl}
|
||||||
onpointerdown={pip.onpointerdown}
|
onpointerdown={pip.onpointerdown}
|
||||||
onpointermove={pip.onpointermove}
|
onpointermove={pip.onpointermove}
|
||||||
@@ -1920,6 +1971,20 @@ h1 {
|
|||||||
{#if lastPipSrc}
|
{#if lastPipSrc}
|
||||||
<img src={lastPipSrc} alt="" onload={() => pip.reposition()}>
|
<img src={lastPipSrc} alt="" onload={() => pip.reposition()}>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if pip.showControls}
|
||||||
|
<button
|
||||||
|
class="pip-fullscreen-btn"
|
||||||
|
onpointerdown={(e) => e.stopPropagation()}
|
||||||
|
onclick={(e) => { e.stopPropagation(); pip.toggleFullscreen(); }}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="8 3 3 3 3 8"/>
|
||||||
|
<polyline points="16 3 21 3 21 8"/>
|
||||||
|
<polyline points="8 21 3 21 3 16"/>
|
||||||
|
<polyline points="16 21 21 21 21 16"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
BIN
static/glaube/glorious/1-carl-bloch.resurrection.webp
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
static/glaube/glorious/2-ascension.webp
Normal file
|
After Width: | Height: | Size: 347 KiB |
BIN
static/glaube/glorious/3-pentecost.webp
Normal file
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 110 KiB |
BIN
static/glaube/glorious/5-diego-veazquez.coronation-mary.webp
Normal file
|
After Width: | Height: | Size: 434 KiB |
BIN
static/glaube/joyful/1-murilllo-annunciation.webp
Normal file
|
After Width: | Height: | Size: 274 KiB |
BIN
static/glaube/joyful/2-carl-bloch.the-visitation.1866.webp
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
static/glaube/joyful/3-adoration-of-the-shepards.webp
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
static/glaube/joyful/4-vouet.presentation-in-the-temple.webp
Normal file
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 77 KiB |
BIN
static/glaube/luminous/5-carl-bloch.the-last-supper.webp
Normal file
|
After Width: | Height: | Size: 126 KiB |