diff --git a/src/lib/components/BenedictusMedal.svelte b/src/lib/components/BenedictusMedal.svelte
new file mode 100644
index 0000000..2da7412
--- /dev/null
+++ b/src/lib/components/BenedictusMedal.svelte
@@ -0,0 +1,72 @@
+
+
+
diff --git a/src/lib/components/CounterButton.svelte b/src/lib/components/CounterButton.svelte
new file mode 100644
index 0000000..7ec19c4
--- /dev/null
+++ b/src/lib/components/CounterButton.svelte
@@ -0,0 +1,62 @@
+
+
+
+
+
diff --git a/src/routes/glaube/rosenkranz/+page.svelte b/src/routes/glaube/rosenkranz/+page.svelte
index 87c55dc..f3c1adc 100644
--- a/src/routes/glaube/rosenkranz/+page.svelte
+++ b/src/routes/glaube/rosenkranz/+page.svelte
@@ -9,6 +9,8 @@ import AveMaria from "$lib/components/prayers/AveMaria.svelte";
import GloriaPatri from "$lib/components/prayers/GloriaPatri.svelte";
import FatimaGebet from "$lib/components/prayers/FatimaGebet.svelte";
import SalveRegina from "$lib/components/prayers/SalveRegina.svelte";
+import BenedictusMedal from "$lib/components/BenedictusMedal.svelte";
+import CounterButton from "$lib/components/CounterButton.svelte";
// Mystery variations for each type of rosary
const mysteries = {
@@ -73,15 +75,107 @@ const mysteriesLatin = {
]
};
-// Determine which mystery to use (for now using schmerzhaften)
-let currentMysteries = mysteries.schmerzhaften;
-let currentMysteriesLatin = mysteriesLatin.schmerzhaften;
+// Toggle for including Luminous mysteries
+let includeLuminous = true;
+
+// Function to get the appropriate mystery for a given weekday
+function getMysteryForWeekday(date, includeLuminous) {
+ const dayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, etc.
+
+ if (includeLuminous) {
+ // With Luminous mysteries schedule
+ const schedule = {
+ 0: 'glorreichen', // Sunday
+ 1: 'freudenreich', // Monday
+ 2: 'schmerzhaften', // Tuesday
+ 3: 'glorreichen', // Wednesday
+ 4: 'lichtreichen', // Thursday
+ 5: 'schmerzhaften', // Friday
+ 6: 'freudenreich' // Saturday
+ };
+ return schedule[dayOfWeek];
+ } else {
+ // Without Luminous mysteries schedule
+ const schedule = {
+ 0: 'glorreichen', // Sunday
+ 1: 'freudenreich', // Monday
+ 2: 'schmerzhaften', // Tuesday
+ 3: 'glorreichen', // Wednesday
+ 4: 'freudenreich', // Thursday
+ 5: 'schmerzhaften', // Friday
+ 6: 'glorreichen' // Saturday
+ };
+ return schedule[dayOfWeek];
+ }
+}
+
+// Determine which mystery to use based on current weekday
+let selectedMystery = getMysteryForWeekday(new Date(), includeLuminous);
+let currentMysteries = mysteries[selectedMystery];
+let currentMysteriesLatin = mysteriesLatin[selectedMystery];
+
+// Function to switch mysteries
+function selectMystery(mysteryType) {
+ selectedMystery = mysteryType;
+ currentMysteries = mysteries[mysteryType];
+ currentMysteriesLatin = mysteriesLatin[mysteryType];
+}
+
+// Function to handle toggle change
+function handleToggleChange() {
+ // Recalculate the default mystery for today
+ const todaysMystery = getMysteryForWeekday(new Date(), includeLuminous);
+ // Update to today's mystery
+ selectMystery(todaysMystery);
+}
// Active section tracking
let activeSection = "cross";
let sectionElements = {};
let svgContainer;
+// Counter for tracking Ave Maria progress in each decade (0-10 for each)
+let decadeCounters = {
+ secret1: 0,
+ secret2: 0,
+ secret3: 0,
+ secret4: 0,
+ secret5: 0
+};
+
+// Function to advance the counter for a specific decade
+function advanceDecade(decadeNum) {
+ const key = `secret${decadeNum}`;
+ if (decadeCounters[key] < 10) {
+ decadeCounters[key] += 1;
+
+ // When we reach 10, auto-scroll to next section after a brief delay
+ // and reset the counter
+ if (decadeCounters[key] === 10) {
+ setTimeout(() => {
+ // Reset counter to clear highlighting
+ decadeCounters[key] = 0;
+
+ // Determine next section
+ let nextSection;
+ if (decadeNum < 5) {
+ nextSection = `secret${decadeNum}_transition`;
+ } else {
+ nextSection = 'final_transition';
+ }
+
+ // Scroll to next section
+ const nextElement = sectionElements[nextSection];
+ if (nextElement) {
+ const elementTop = nextElement.getBoundingClientRect().top + window.scrollY;
+ const offset = parseFloat(getComputedStyle(document.documentElement).fontSize) * 3;
+ window.scrollTo({ top: elementTop - offset, behavior: 'smooth' });
+ }
+ }, 500);
+ }
+ }
+}
+
// Map sections to their vertical positions in the SVG
const sectionPositions = {
cross: 35,
@@ -90,7 +184,7 @@ const sectionPositions = {
start2: 135,
start3: 160,
lbead2: 200,
- secret1: 280,
+ secret1: 270,
secret1_transition: 520,
secret2: 560,
secret2_transition: 800,
@@ -103,7 +197,100 @@ const sectionPositions = {
};
onMount(() => {
- // Set up Intersection Observer for scroll tracking
+ let scrollLock = null; // Track which side initiated the scroll: 'prayer', 'svg', or 'click'
+ let scrollLockTimeout = null;
+
+ const setScrollLock = (source, duration = 1000) => {
+ scrollLock = source;
+ clearTimeout(scrollLockTimeout);
+ scrollLockTimeout = setTimeout(() => {
+ scrollLock = null;
+ }, duration);
+ };
+
+ // Check if browser supports smooth scrolling
+ // Test both CSS property and actual scrollTo API support
+ const supportsNativeSmoothScroll = (() => {
+ if (!('scrollBehavior' in document.documentElement.style)) {
+ return false;
+ }
+ // Additional check: some browsers have the CSS property but not the JS API
+ try {
+ const testElement = document.createElement('div');
+ testElement.scrollTo({ top: 0, behavior: 'smooth' });
+ return true;
+ } catch (e) {
+ return false;
+ }
+ })();
+
+ // Smooth scroll polyfill for window scrolling
+ const smoothScrollTo = (targetY, duration = 500) => {
+ if (supportsNativeSmoothScroll) {
+ try {
+ window.scrollTo({ top: targetY, behavior: 'smooth' });
+ return;
+ } catch (e) {
+ // Fall through to polyfill
+ }
+ }
+
+ const startY = window.scrollY || window.pageYOffset;
+ const distance = targetY - startY;
+ const startTime = performance.now();
+
+ const easeInOutQuad = (t) => {
+ return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
+ };
+
+ const scroll = (currentTime) => {
+ const elapsed = currentTime - startTime;
+ const progress = Math.min(elapsed / duration, 1);
+ const ease = easeInOutQuad(progress);
+ window.scrollTo(0, startY + distance * ease);
+
+ if (progress < 1) {
+ requestAnimationFrame(scroll);
+ }
+ };
+
+ requestAnimationFrame(scroll);
+ };
+
+ // Smooth scroll polyfill for element scrolling (for SVG container)
+ const smoothScrollElement = (element, targetY, duration = 500) => {
+ if (supportsNativeSmoothScroll) {
+ try {
+ element.scrollTo({ top: targetY, behavior: 'smooth' });
+ return;
+ } catch (e) {
+ // Fall through to polyfill
+ }
+ }
+
+ const startY = element.scrollTop;
+ const distance = targetY - startY;
+ const startTime = performance.now();
+
+ const easeInOutQuad = (t) => {
+ return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
+ };
+
+ const scroll = (currentTime) => {
+ const elapsed = currentTime - startTime;
+ const progress = Math.min(elapsed / duration, 1);
+ const ease = easeInOutQuad(progress);
+ element.scrollTop = startY + distance * ease;
+
+ if (progress < 1) {
+ requestAnimationFrame(scroll);
+ }
+ };
+
+ requestAnimationFrame(scroll);
+ };
+
+ // Set up Intersection Observer for scroll tracking (prayers -> SVG)
const options = {
root: null,
rootMargin: "-20% 0px -60% 0px", // Trigger when section is near top
@@ -112,7 +299,7 @@ onMount(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
- if (entry.isIntersecting) {
+ if (entry.isIntersecting && scrollLock !== 'svg' && scrollLock !== 'click') {
activeSection = entry.target.dataset.section;
// Scroll SVG to keep active section visible at top
@@ -125,17 +312,20 @@ onMount(() => {
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;
+ const scale = (svgHeight / viewBoxHeight) * cssScale;
const pixelPosition = svgYPosition * scale;
// Position with some padding to show context above
const targetScroll = pixelPosition - 100;
- svgContainer.scrollTo({
- top: Math.max(0, targetScroll),
- behavior: 'smooth'
- });
+ setScrollLock('prayer');
+ smoothScrollElement(svgContainer, Math.max(0, targetScroll));
}
}
});
@@ -146,7 +336,165 @@ onMount(() => {
if (el) observer.observe(el);
});
- return () => observer.disconnect();
+ // Detect when user scrolls past all prayers and snap SVG to bottom or top
+ const handleWindowScroll = () => {
+ if (scrollLock === 'svg' || scrollLock === 'click' || !svgContainer) return;
+
+ const viewportHeight = window.innerHeight;
+
+ // Get the first and final prayer sections
+ const firstSection = sectionElements.cross;
+ const finalSection = sectionElements.final_transition;
+ if (!firstSection || !finalSection) return;
+
+ const firstSectionRect = firstSection.getBoundingClientRect();
+ const finalSectionRect = finalSection.getBoundingClientRect();
+
+ // Check if we've scrolled above the first section (it's completely below viewport)
+ if (firstSectionRect.top > viewportHeight * 0.6) {
+ // Scroll SVG to top
+ if (svgContainer.scrollTop > 10) { // Only if not already at top
+ smoothScrollElement(svgContainer, 0);
+ }
+ }
+ // Check if we've scrolled past the final section (it's completely above viewport)
+ else if (finalSectionRect.bottom < viewportHeight * 0.4) {
+ // Scroll SVG to bottom
+ const maxScroll = svgContainer.scrollHeight - svgContainer.clientHeight;
+ if (svgContainer.scrollTop < maxScroll - 10) { // Only if not already at bottom
+ smoothScrollElement(svgContainer, maxScroll);
+ }
+ }
+ };
+
+ window.addEventListener('scroll', handleWindowScroll, { passive: true });
+
+ // Debounce SVG scroll handler to avoid excessive updates
+ let svgScrollTimeout = null;
+ const handleSvgScroll = () => {
+ if (scrollLock === 'prayer' || scrollLock === 'click' || !svgContainer) return;
+
+ clearTimeout(svgScrollTimeout);
+ svgScrollTimeout = setTimeout(() => {
+ const svg = svgContainer.querySelector('svg');
+ if (!svg) return;
+
+ const scrollTop = svgContainer.scrollTop;
+ 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;
+
+ const scale = (svgHeight / viewBoxHeight) * cssScale;
+
+ // Convert scroll position back to SVG coordinates
+ const svgY = scrollTop / scale;
+
+ // Find the closest section based on scroll position
+ let closestSection = 'cross';
+ let closestDistance = Infinity;
+
+ for (const [section, position] of Object.entries(sectionPositions)) {
+ const distance = Math.abs(svgY - position);
+ if (distance < closestDistance) {
+ closestDistance = distance;
+ closestSection = section;
+ }
+ }
+
+ // Scroll to the corresponding prayer section
+ if (closestSection !== activeSection && sectionElements[closestSection]) {
+ activeSection = closestSection;
+ setScrollLock('svg');
+
+ // Calculate scroll position with offset for spacing at top
+ const element = sectionElements[closestSection];
+ const elementTop = element.getBoundingClientRect().top + window.scrollY;
+ const offset = parseFloat(getComputedStyle(document.documentElement).fontSize) * 3; // 3em in pixels
+
+ smoothScrollTo(elementTop - offset);
+ }
+ }, 150); // Debounce by 150ms
+ };
+
+ if (svgContainer) {
+ svgContainer.addEventListener('scroll', handleSvgScroll);
+ }
+
+ // Handle clicks on SVG elements to jump to prayers
+ const handleSvgClick = (e) => {
+ // Find the clicked element or its parent with a data-section attribute
+ let target = e.target;
+ while (target && target !== svgContainer) {
+ const section = target.dataset.section;
+ if (section && sectionElements[section]) {
+ // Update active section immediately
+ activeSection = section;
+
+ // Lock scrolling for clicks
+ setScrollLock('click', 1500);
+
+ // Scroll the SVG visualization to the clicked section
+ if (sectionPositions[section] !== undefined) {
+ const svg = svgContainer.querySelector('svg');
+ if (svg) {
+ const svgYPosition = sectionPositions[section];
+ 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;
+
+ smoothScrollElement(svgContainer, Math.max(0, targetScroll));
+ }
+ }
+
+ // Scroll the prayers to the corresponding section
+ const element = sectionElements[section];
+ const elementTop = element.getBoundingClientRect().top + window.scrollY;
+ const offset = parseFloat(getComputedStyle(document.documentElement).fontSize) * 3;
+
+ smoothScrollTo(elementTop - offset);
+
+ break;
+ }
+ target = target.parentElement;
+ }
+ };
+
+ const svg = svgContainer?.querySelector('svg');
+ if (svg) {
+ svg.addEventListener('click', handleSvgClick);
+ // Make it clear elements are clickable
+ svg.style.cursor = 'pointer';
+ }
+
+ return () => {
+ observer.disconnect();
+ clearTimeout(scrollLockTimeout);
+ clearTimeout(svgScrollTimeout);
+ window.removeEventListener('scroll', handleWindowScroll);
+ if (svgContainer) {
+ svgContainer.removeEventListener('scroll', handleSvgScroll);
+ }
+ if (svg) {
+ svg.removeEventListener('click', handleSvgClick);
+ }
+ };
});
Rosenkranz - Interaktiv
@@ -362,86 +941,153 @@ h1 {
Interaktiver Rosenkranz
+
+
+
+
+ Die Geheimnisse werden automatisch nach dem Wochenplan ausgewählt.
+ {#if includeLuminous}
+ Mit lichtreichen Geheimnissen: Do=Lichtreich, andere Tage folgen dem traditionellen Plan.
+ {:else}
+ Traditioneller Plan ohne lichtreiche Geheimnisse.
+ {/if}
+ Sie können jederzeit manuell ein anderes Geheimnis wählen.
+