Compare commits
2 Commits
8246906a76
...
bf3014337e
| Author | SHA1 | Date | |
|---|---|---|---|
|
bf3014337e
|
|||
|
6182b8f943
|
@@ -329,14 +329,24 @@ const hasMysteryImages = $derived(showImages && selectedMystery === 'schmerzhaft
|
|||||||
// Mystery image scroll target based on active section
|
// Mystery image scroll target based on active section
|
||||||
function getMysteryScrollTarget(section) {
|
function getMysteryScrollTarget(section) {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
|
case 'cross':
|
||||||
|
return 'before';
|
||||||
|
case 'lbead2':
|
||||||
|
return 'garden';
|
||||||
|
case 'secret1':
|
||||||
|
return 'garden';
|
||||||
case 'secret1_transition':
|
case 'secret1_transition':
|
||||||
|
return 'flagellation';
|
||||||
case 'secret2':
|
case 'secret2':
|
||||||
|
return 'flagellation';
|
||||||
case 'secret2_transition':
|
case 'secret2_transition':
|
||||||
|
return 'mocking';
|
||||||
case 'secret3':
|
case 'secret3':
|
||||||
return 'mocking';
|
return 'mocking';
|
||||||
case 'secret3_transition':
|
case 'secret3_transition':
|
||||||
|
return 'carry';
|
||||||
case 'secret4':
|
case 'secret4':
|
||||||
return 'between';
|
return 'carry';
|
||||||
case 'secret4_transition':
|
case 'secret4_transition':
|
||||||
case 'secret5':
|
case 'secret5':
|
||||||
return 'crucifixion';
|
return 'crucifixion';
|
||||||
@@ -352,13 +362,19 @@ function getMysteryScrollTarget(section) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mobile PiP: which image to show (null = hide)
|
// 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)
|
||||||
function getMysteryImage(mystery, section) {
|
function getMysteryImage(mystery, section) {
|
||||||
if (mystery !== 'schmerzhaften') return null;
|
if (mystery !== 'schmerzhaften') return null;
|
||||||
const target = getMysteryScrollTarget(section);
|
const target = getMysteryScrollTarget(section);
|
||||||
if (target === 'mocking') return '/glaube/sorrowful/2-3.mocking.webp';
|
return mysteryImages.get(target)?.src ?? null;
|
||||||
if (target === 'crucifixion') return '/glaube/sorrowful/5.crucification.webp';
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
const mysteryPipSrc = $derived(getMysteryImage(selectedMystery, activeSection));
|
const mysteryPipSrc = $derived(getMysteryImage(selectedMystery, activeSection));
|
||||||
|
|
||||||
@@ -413,7 +429,8 @@ $effect(() => {
|
|||||||
const targetName = getMysteryScrollTarget(activeSection);
|
const targetName = getMysteryScrollTarget(activeSection);
|
||||||
const targetEl = mysteryImageContainer.querySelector(`[data-target="${targetName}"]`);
|
const targetEl = mysteryImageContainer.querySelector(`[data-target="${targetName}"]`);
|
||||||
if (targetEl) {
|
if (targetEl) {
|
||||||
scrollMysteryImage(targetEl.offsetTop);
|
const padding = parseFloat(getComputedStyle(mysteryImageContainer).paddingTop) || 0;
|
||||||
|
scrollMysteryImage(Math.max(0, targetEl.offsetTop - padding));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -530,10 +547,13 @@ onMount(() => {
|
|||||||
// Now allow saving to localStorage
|
// Now allow saving to localStorage
|
||||||
hasLoadedFromStorage = true;
|
hasLoadedFromStorage = true;
|
||||||
|
|
||||||
// PiP resize handler
|
// PiP resize handler — show/hide when crossing the breakpoint
|
||||||
const onPipResize = () => {
|
const onPipResize = () => {
|
||||||
if (rosaryPipEl && isMobilePip() && mysteryPipSrc) {
|
if (!rosaryPipEl) return;
|
||||||
pip.reposition();
|
if (isMobilePip() && mysteryPipSrc) {
|
||||||
|
pip.show(rosaryPipEl);
|
||||||
|
} else if (!isMobilePip()) {
|
||||||
|
pip.hide();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('resize', onPipResize);
|
window.addEventListener('resize', onPipResize);
|
||||||
@@ -641,40 +661,13 @@ onMount(() => {
|
|||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting && scrollLock !== 'svg' && scrollLock !== 'click') {
|
if (entry.isIntersecting && scrollLock !== 'svg' && scrollLock !== 'click') {
|
||||||
|
// Skip observer updates when at the top — handleWindowScroll handles this
|
||||||
|
const scrollY = window.scrollY || window.pageYOffset;
|
||||||
|
if (scrollY < 50) return;
|
||||||
|
|
||||||
const section = entry.target.dataset.section;
|
const section = entry.target.dataset.section;
|
||||||
activeSection = section;
|
activeSection = section;
|
||||||
|
|
||||||
// Don't auto-scroll if we're at the absolute top of the page and viewing the first section
|
|
||||||
const scrollY = window.scrollY || window.pageYOffset;
|
|
||||||
if (scrollY < 50 && section === 'cross') {
|
|
||||||
// User is at the very top - don't trigger auto-scroll, just update SVG
|
|
||||||
if (svgContainer && sectionPositions[activeSection] !== undefined) {
|
|
||||||
const svg = svgContainer.querySelector('svg');
|
|
||||||
if (!svg) return;
|
|
||||||
|
|
||||||
const svgYPosition = sectionPositions[activeSection];
|
|
||||||
const viewBox = svg.viewBox.baseVal;
|
|
||||||
const svgHeight = svg.clientHeight;
|
|
||||||
const viewBoxHeight = viewBox.height;
|
|
||||||
|
|
||||||
// Get CSS transform scale (3.5 on mobile, 1 on desktop)
|
|
||||||
const computedStyle = window.getComputedStyle(svg);
|
|
||||||
const matrix = new DOMMatrix(computedStyle.transform);
|
|
||||||
const cssScale = matrix.a || 1;
|
|
||||||
|
|
||||||
// Convert SVG coordinates to pixel coordinates
|
|
||||||
const scale = (svgHeight / viewBoxHeight) * cssScale;
|
|
||||||
const pixelPosition = svgYPosition * scale;
|
|
||||||
|
|
||||||
// Position with some padding to show context above
|
|
||||||
const targetScroll = pixelPosition - 100;
|
|
||||||
|
|
||||||
setScrollLock('prayer');
|
|
||||||
smoothScrollElement(svgContainer, Math.max(0, targetScroll));
|
|
||||||
}
|
|
||||||
return; // Skip the page scroll
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll SVG to keep active section visible at top
|
// Scroll SVG to keep active section visible at top
|
||||||
if (svgContainer && sectionPositions[activeSection] !== undefined) {
|
if (svgContainer && sectionPositions[activeSection] !== undefined) {
|
||||||
const svg = svgContainer.querySelector('svg');
|
const svg = svgContainer.querySelector('svg');
|
||||||
@@ -727,10 +720,14 @@ onMount(() => {
|
|||||||
|
|
||||||
// Check if we're at the absolute top of the page
|
// Check if we're at the absolute top of the page
|
||||||
if (scrollY < 50) {
|
if (scrollY < 50) {
|
||||||
// Scroll SVG to top
|
activeSection = 'cross';
|
||||||
if (svgContainer.scrollTop > 10) { // Only if not already at top
|
// Snap SVG and images to top instantly
|
||||||
|
if (svgContainer.scrollTop > 10) {
|
||||||
setScrollLock('prayer');
|
setScrollLock('prayer');
|
||||||
smoothScrollElement(svgContainer, 0);
|
svgContainer.scrollTop = 0;
|
||||||
|
}
|
||||||
|
if (mysteryImageContainer && mysteryImageContainer.scrollTop > 10) {
|
||||||
|
mysteryImageContainer.scrollTop = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check if we're at the absolute bottom of the page
|
// Check if we're at the absolute bottom of the page
|
||||||
@@ -749,6 +746,9 @@ onMount(() => {
|
|||||||
setScrollLock('prayer');
|
setScrollLock('prayer');
|
||||||
smoothScrollElement(svgContainer, 0);
|
smoothScrollElement(svgContainer, 0);
|
||||||
}
|
}
|
||||||
|
if (mysteryImageContainer && mysteryImageContainer.scrollTop > 10) {
|
||||||
|
smoothScrollElement(mysteryImageContainer, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Check if we've scrolled past the final section (it's completely above viewport)
|
// Check if we've scrolled past the final section (it's completely above viewport)
|
||||||
else if (finalSectionRect.bottom < viewportHeight * 0.4) {
|
else if (finalSectionRect.bottom < viewportHeight * 0.4) {
|
||||||
@@ -1413,9 +1413,10 @@ h1 {
|
|||||||
.mystery-image-column {
|
.mystery-image-column {
|
||||||
display: block;
|
display: block;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 6rem;
|
top: 0;
|
||||||
|
padding-top: 6rem;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
max-height: calc(100vh - 7rem);
|
max-height: 100vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
@@ -1426,15 +1427,29 @@ h1 {
|
|||||||
.mystery-image-pad {
|
.mystery-image-pad {
|
||||||
height: calc(100vh - 5rem);
|
height: calc(100vh - 5rem);
|
||||||
}
|
}
|
||||||
|
.mystery-image-column figure {
|
||||||
|
margin: 0;
|
||||||
|
margin-right: 2rem;
|
||||||
|
}
|
||||||
.mystery-image-column img {
|
.mystery-image-column img {
|
||||||
max-height: calc(100vh - 5rem);
|
max-height: calc(100vh - 5rem);
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 25vw;
|
max-width: 25vw;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-right: 2rem;
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.mystery-image-column figcaption {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--nord4);
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
max-width: 25vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) and (prefers-color-scheme: light) {
|
||||||
|
.mystery-image-column figcaption {
|
||||||
|
color: var(--nord2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile PiP for mystery images */
|
/* Mobile PiP for mystery images */
|
||||||
@@ -1869,9 +1884,14 @@ h1 {
|
|||||||
<div class="mystery-image-column" bind:this={mysteryImageContainer}>
|
<div class="mystery-image-column" bind:this={mysteryImageContainer}>
|
||||||
{#if hasMysteryImages}
|
{#if hasMysteryImages}
|
||||||
<div class="mystery-image-pad" data-target="before"></div>
|
<div class="mystery-image-pad" data-target="before"></div>
|
||||||
<img src="/glaube/sorrowful/2-3.mocking.webp" alt={isEnglish ? 'Mocking of Christ' : 'Verspottung Christi'} data-target="mocking">
|
{#each ["garden", "flagellation", "mocking", "carry", "crucifixion"] as target, i}
|
||||||
<div class="mystery-image-pad" data-target="between"></div>
|
{@const img = mysteryImages.get(target)}
|
||||||
<img src="/glaube/sorrowful/5.crucification.webp" alt={isEnglish ? 'Crucifixion of Christ' : 'Kreuzigung Christi'} data-target="crucifixion">
|
{#if i > 0}<div class="mystery-image-pad" data-target="between{i}"></div>{/if}
|
||||||
|
<figure data-target={target}>
|
||||||
|
<img src={img.src} alt="{img.artist} — {isEnglish ? img.title : img.titleDe}">
|
||||||
|
<figcaption>{img.artist}, <em>{isEnglish ? img.title : img.titleDe}</em>, {img.year}</figcaption>
|
||||||
|
</figure>
|
||||||
|
{/each}
|
||||||
<div class="mystery-image-pad" data-target="after"></div>
|
<div class="mystery-image-pad" data-target="after"></div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -1890,7 +1910,7 @@ h1 {
|
|||||||
onpointerup={pip.onpointerup}
|
onpointerup={pip.onpointerup}
|
||||||
>
|
>
|
||||||
{#if lastPipSrc}
|
{#if lastPipSrc}
|
||||||
<img src={lastPipSrc} alt="">
|
<img src={lastPipSrc} alt="" onload={() => pip.reposition()}>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
BIN
static/glaube/sorrowful/1.carl-bloch.gethsemane.webp
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
static/glaube/sorrowful/2.wiliam-bouguereau.flagellation.webp
Normal file
|
After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
BIN
static/glaube/sorrowful/4.lorenzo-lotto.carrying-the-cross.webp
Normal file
|
After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |