|
|
| Zeile 396: |
Zeile 396: |
|
| |
|
| }); | | }); |
|
| |
|
| |
|
| |
|
| |
|
| |
|
|
| |
|
| Zeile 458: |
Zeile 454: |
|
| |
|
| /* ========== Kategorien rekursiv einsammeln (inkl. Subkats, Namespaces) ========== */ | | /* ========== Kategorien rekursiv einsammeln (inkl. Subkats, Namespaces) ========== */ |
| // NIMMT nsStr (z. B. "*", "0|102|14"). Bei "*" wird cmnamespace NICHT gesetzt.
| |
| function fetchCategoryMembersRecursiveSingleResolved(api, catTitle, limit, outSet, pages, nsStr){ | | function fetchCategoryMembersRecursiveSingleResolved(api, catTitle, limit, outSet, pages, nsStr){ |
| var visited = {}, queue = [catTitle]; | | var visited = {}, queue = [catTitle]; |
| Zeile 468: |
Zeile 463: |
| list: 'categorymembers', | | list: 'categorymembers', |
| cmtitle: title, | | cmtitle: title, |
| // cmnamespace nur setzen, wenn NICHT "*"
| |
| cmtype: 'page|subcat', | | cmtype: 'page|subcat', |
| cmlimit: Math.min(200, limit), | | cmlimit: Math.min(200, limit), |
| Zeile 577: |
Zeile 571: |
| /* ========== Render ========== */ | | /* ========== Render ========== */ |
| function renderTopN(container, rows, N, minVotes) { | | function renderTopN(container, rows, N, minVotes) { |
| // Statusbox parken, falls keep aktiv
| |
| var keep = (container.getAttribute && container.getAttribute('data-keep-status') === 'true'); | | var keep = (container.getAttribute && container.getAttribute('data-keep-status') === 'true'); |
| var statusBox = keep ? container.querySelector('.whisky-top5__status') : null; | | var statusBox = keep ? container.querySelector('.whisky-top5__status') : null; |
| Zeile 637: |
Zeile 630: |
| container.setAttribute('data-top5-init','1'); | | container.setAttribute('data-top5-init','1'); |
|
| |
|
| // Kategorien (Zeilenumbruch ODER Semikolon getrennt)
| |
| var rawCats = container.getAttribute('data-categories') || container.getAttribute('data-category') || ''; | | var rawCats = container.getAttribute('data-categories') || container.getAttribute('data-category') || ''; |
| var parts = rawCats.split(/\n|;/), rootCats = [], i; | | var parts = rawCats.split(/\n|;/), rootCats = [], i; |
| Zeile 647: |
Zeile 639: |
| var minVotes = parseInt(container.getAttribute('data-min-votes') || '1', 10); | | var minVotes = parseInt(container.getAttribute('data-min-votes') || '1', 10); |
| var includeHidden = (container.getAttribute('data-include-hidden') === 'true'); | | var includeHidden = (container.getAttribute('data-include-hidden') === 'true'); |
| var nsStr = container.getAttribute('data-namespaces') || '0|14'; // z. B. "*", "0|102|14" | | var nsStr = container.getAttribute('data-namespaces') || '0|14'; |
|
| |
|
| var rawC = container.getAttribute('data-contests') || 'NASE,GESCHMACK,ABGANG,GESAMTEINDRUCK'; | | var rawC = container.getAttribute('data-contests') || 'NASE,GESCHMACK,ABGANG,GESAMTEINDRUCK'; |
| Zeile 658: |
Zeile 650: |
| status('Sammle Seiten …'); | | status('Sammle Seiten …'); |
|
| |
|
|
| |
| // 1) Artikel einsammeln
| |
| fetchCategoryMembersRecursiveMulti(rootCats, lim, status, nsStr).then(function(members){ | | fetchCategoryMembersRecursiveMulti(rootCats, lim, status, nsStr).then(function(members){ |
| status('Gefundene Seiten gesamt: ' + (members ? members.length : 0) + ' – lade Bewertungen …', true); | | status('Gefundene Seiten gesamt: ' + (members ? members.length : 0) + ' – lade Bewertungen …', true); |
| Zeile 703: |
Zeile 693: |
|
| |
|
| }); | | }); |
|
| |
|
| |
|
| |
|
|
| |
|
| Zeile 712: |
Zeile 700: |
| var el = this, val = parseFloat(el.getAttribute('data-rating') || '0'); | | var el = this, val = parseFloat(el.getAttribute('data-rating') || '0'); |
| if (isNaN(val)) val = 0; | | if (isNaN(val)) val = 0; |
| // clamp 0..5
| |
| val = Math.max(0, Math.min(5, val)); | | val = Math.max(0, Math.min(5, val)); |
| // set CSS variable for width percentage (0..5 -> 0..5 stars)
| |
| el.style.setProperty('--stars', (val).toString()); | | el.style.setProperty('--stars', (val).toString()); |
| el.setAttribute('aria-label', val + ' von 5 Sternen'); | | el.setAttribute('aria-label', val + ' von 5 Sternen'); |
| Zeile 721: |
Zeile 707: |
| }); | | }); |
|
| |
|
|
| |
| // --------------------------------
| |
|
| |
|
| // Force light color-scheme at document level (helps Mobile Safari) | | // Force light color-scheme at document level (helps Mobile Safari) |
| Zeile 736: |
Zeile 720: |
| } | | } |
| }); | | }); |
|
| |
| // -------------------------------------------------
| |
|
| |
|
|
| |
|
| Zeile 751: |
Zeile 733: |
| if (!chart || !block) return; | | if (!chart || !block) return; |
|
| |
|
| // Summe aller Werte berechnen
| |
| const total = chart.data.datasets.reduce((sum, ds) => | | const total = chart.data.datasets.reduce((sum, ds) => |
| sum + (ds.data || []).reduce((a, b) => a + (parseFloat(b) || 0), 0) | | sum + (ds.data || []).reduce((a, b) => a + (parseFloat(b) || 0), 0) |
| , 0); | | , 0); |
|
| |
|
| // Bestehende Anzeige entfernen (falls Neurender)
| |
| const oldInfo = block.querySelector(':scope > .chart-total-info'); | | const oldInfo = block.querySelector(':scope > .chart-total-info'); |
| if (oldInfo) oldInfo.remove(); | | if (oldInfo) oldInfo.remove(); |
|
| |
|
| // Neue Anzeige einfügen
| |
| const info = document.createElement('div'); | | const info = document.createElement('div'); |
| info.className = 'chart-total-info'; | | info.className = 'chart-total-info'; |
| Zeile 775: |
Zeile 754: |
| } | | } |
|
| |
|
|
| |
| // -------------------------------------
| |
|
| |
|
| /* === ADOS Multi-Serien-Chart (Chart.js) ============================= * | | /* === ADOS Multi-Serien-Chart (Chart.js) ============================= * |
| Zeile 784: |
Zeile 761: |
|
| |
|
| (function () { | | (function () { |
| // 1) Chart.js nur 1x laden und erst dann rendern
| |
| var _chartReady = null; | | var _chartReady = null; |
| function ensureChartJS() { | | function ensureChartJS() { |
| Zeile 791: |
Zeile 767: |
| if (window.Chart) return resolve(); | | if (window.Chart) return resolve(); |
| var s = document.createElement('script'); | | var s = document.createElement('script'); |
| s.src = 'https://cdn.jsdelivr.net/npm/chart.js'; // UMD-Bundle | | s.src = 'https://cdn.jsdelivr.net/npm/chart.js'; |
| s.async = true; | | s.async = true; |
| s.onload = function(){ resolve(); }; | | s.onload = function(){ resolve(); }; |
| Zeile 800: |
Zeile 776: |
| } | | } |
|
| |
|
| // 2) Farben je Serie (gut unterscheidbar)
| |
| var ADOS_COLORS = { | | var ADOS_COLORS = { |
| 'A Dream of Scotland': '#C2410C', // Kupferbraun | | 'A Dream of Scotland': '#C2410C', |
| 'A Dream of Ireland': '#15803D', // Flaschengrün | | 'A Dream of Ireland': '#15803D', |
| 'A Dream of... – Der Rest der Welt': '#1D4ED8', // Mittelblau | | 'A Dream of... – Der Rest der Welt': '#1D4ED8', |
| 'Friendly Mr. Z': '#9333EA', // Violett | | 'Friendly Mr. Z': '#9333EA', |
| 'Die Whisky Elfen': '#0891B2', // Türkis | | 'Die Whisky Elfen': '#0891B2', |
| 'The Fine Art of Whisky': '#CA8A04' // Goldgelb | | 'The Fine Art of Whisky': '#CA8A04' |
| }; | | }; |
| var COLOR_CYCLE = ['#2563eb','#16a34a','#f97316','#dc2626','#a855f7','#0ea5e9','#f59e0b','#10b981']; | | var COLOR_CYCLE = ['#2563eb','#16a34a','#f97316','#dc2626','#a855f7','#0ea5e9','#f59e0b','#10b981']; |
| Zeile 822: |
Zeile 797: |
| } | | } |
|
| |
|
| // 3) Tabelle (Jahr | Serie | Anzahl) -> {labels, datasets}
| |
| function buildDatasetsFromTable(tbl){ | | function buildDatasetsFromTable(tbl){ |
| var rows = Array.from(tbl.querySelectorAll('tr')); | | var rows = Array.from(tbl.querySelectorAll('tr')); |
| Zeile 828: |
Zeile 802: |
|
| |
|
| var yearsSet = new Set(); | | var yearsSet = new Set(); |
| var bySeries = new Map(); // serie -> Map(year -> count) | | var bySeries = new Map(); |
|
| |
|
| rows.slice(1).forEach(function(tr){ | | rows.slice(1).forEach(function(tr){ |
| Zeile 863: |
Zeile 837: |
| } | | } |
|
| |
|
| // 4) Einen Chart-Container rendern: nimmt die NÄCHSTE Tabelle als Datenquelle | | function renderOne(block){ |
| function renderOne(block){ | | if (block.dataset.rendered === '1') return; |
| // schon gerendert? (z. B. durch AJAX/Minerva-Reloads)
| |
| if (block.dataset.rendered === '1') return;
| |
|
| |
|
| // nächste Tabelle (auch wenn sie in einem Wrapper steckt) finden
| | var el = block.nextElementSibling, tbl = null, wrapToHide = null; |
| var el = block.nextElementSibling, tbl = null, wrapToHide = null;
| | while (el) { |
| while (el) {
| | if (/^H[1-6]$/.test(el.tagName) || (el.classList && el.classList.contains('ados-chart-multi'))) break; |
| if (/^H[1-6]$/.test(el.tagName) || (el.classList && el.classList.contains('ados-chart-multi'))) break;
| | if (el.tagName === 'TABLE') { |
| if (el.tagName === 'TABLE') {
| | tbl = el; |
| tbl = el;
| | } else if (el.querySelector) { |
| } else if (el.querySelector) {
| | var t = el.querySelector('table'); |
| var t = el.querySelector('table');
| | if (t) tbl = t; |
| if (t) tbl = t;
| | } |
| }
| | if (tbl) { |
| if (tbl) {
| | wrapToHide = tbl.parentElement; |
| // Wrapper merken – aber später nur verstecken, wenn er "quasi nur" die Tabelle enthält
| | break; |
| wrapToHide = tbl.parentElement;
| | } |
| break; | | el = el.nextElementSibling; |
| } | | } |
| el = el.nextElementSibling; | | if (!tbl) return; |
| }
| |
| if (!tbl) return;
| |
|
| |
|
| var out = buildDatasetsFromTable(tbl);
| | var out = buildDatasetsFromTable(tbl); |
| if (!out.labels.length || !out.datasets.length) return;
| | if (!out.labels.length || !out.datasets.length) return; |
|
| |
|
| // Tabelle verstecken, wenn gewünscht (nur die Tabelle – oder den Wrapper, falls er sonst leer ist)
| | var hide = (block.dataset.hideTable || '').toLowerCase() === 'true'; |
| var hide = (block.dataset.hideTable || '').toLowerCase() === 'true';
| | if (hide) { |
| if (hide) {
| | var onlyTable = wrapToHide && wrapToHide.children.length === 1 && wrapToHide.firstElementChild === tbl; |
| // Hat der Wrapper außer der Tabelle noch sichtbaren Inhalt?
| | if (onlyTable) { |
| var onlyTable = wrapToHide && wrapToHide.children.length === 1 && wrapToHide.firstElementChild === tbl;
| | wrapToHide.setAttribute('aria-hidden','true'); |
| if (onlyTable) {
| | wrapToHide.style.display = 'none'; |
| wrapToHide.setAttribute('aria-hidden','true');
| | } else { |
| wrapToHide.style.display = 'none';
| | tbl.setAttribute('aria-hidden','true'); |
| } else {
| | tbl.style.display = 'none'; |
| tbl.setAttribute('aria-hidden','true');
| | } |
| tbl.style.display = 'none';
| |
| } | | } |
| }
| |
|
| |
|
| // Zeichenfläche einsetzen (mobil/desktop)
| | var wrap = document.createElement('div'); |
| var wrap = document.createElement('div');
| | wrap.style.position = 'relative'; |
| wrap.style.position = 'relative';
| | wrap.style.width = '100%'; |
| wrap.style.width = '100%';
| | wrap.style.height = block.dataset.height || (window.matchMedia('(min-width:768px)').matches ? '450px' : '300px'); |
| wrap.style.height = block.dataset.height || (window.matchMedia('(min-width:768px)').matches ? '450px' : '300px');
| | var canvas = document.createElement('canvas'); |
| var canvas = document.createElement('canvas');
| | wrap.appendChild(canvas); |
| wrap.appendChild(canvas);
| | block.innerHTML = ''; |
| block.innerHTML = '';
| | block.appendChild(wrap); |
| block.appendChild(wrap);
| |
|
| |
|
| var type = (block.dataset.type || 'line').toLowerCase();
| | var type = (block.dataset.type || 'line').toLowerCase(); |
| var title = block.dataset.title || '';
| | var title = block.dataset.title || ''; |
| var cumulative = (block.dataset.cumulative || '').toLowerCase() === 'true';
| | var cumulative = (block.dataset.cumulative || '').toLowerCase() === 'true'; |
|
| |
|
| // optional: kumulative Werte pro Serie bauen
| | if (cumulative) { |
| if (cumulative) {
| | out.datasets = out.datasets.map(function(ds){ |
| out.datasets = out.datasets.map(function(ds){
| | var acc = 0; |
| var acc = 0;
| | return Object.assign({}, ds, { |
| return Object.assign({}, ds, {
| | data: ds.data.map(function(v){ acc += v; return acc; }) |
| data: ds.data.map(function(v){ acc += v; return acc; })
| | }); |
| }); | | }); |
| }); | | } |
| }
| |
|
| |
|
| ensureChartJS().then(function(){ | | ensureChartJS().then(function(){ |
| const chart = new Chart(canvas.getContext('2d'), {
| | const chart = new Chart(canvas.getContext('2d'), { |
| type: type,
| | type: type, |
| data: { labels: out.labels, datasets: out.datasets },
| | data: { labels: out.labels, datasets: out.datasets }, |
| options: {
| | options: { |
| responsive: true,
| | responsive: true, |
| maintainAspectRatio: false,
| | maintainAspectRatio: false, |
| interaction: { mode: 'nearest', intersect: false },
| | interaction: { mode: 'nearest', intersect: false }, |
| plugins: {
| | plugins: { |
| legend: { position: 'bottom', labels: { font: { size: 13 }, boxWidth: 20 } },
| | legend: { position: 'bottom', labels: { font: { size: 13 }, boxWidth: 20 } }, |
| title: { display: !!title, text: title, font: { size: 16 } },
| | title: { display: !!title, text: title, font: { size: 16 } }, |
| tooltip:{ backgroundColor: 'rgba(0,0,0,0.8)', titleFont: {size:14}, bodyFont: {size:13} }
| | tooltip:{ backgroundColor: 'rgba(0,0,0,0.8)', titleFont: {size:14}, bodyFont: {size:13} } |
| },
| | }, |
| scales: {
| | scales: { |
| x: { ticks: { font: { size: 12 } } },
| | x: { ticks: { font: { size: 12 } } }, |
| y: { beginAtZero: true, ticks: { precision: 0, font: { size: 12 } } }
| | y: { beginAtZero: true, ticks: { precision: 0, font: { size: 12 } } } |
| }
| | } |
| }
| | } |
| });
| | }); |
|
| |
|
| // --- NEU ---
| | const hideTotal = (block.dataset.hideTotal || '').toLowerCase() === 'true'; |
| const hideTotal = (block.dataset.hideTotal || '').toLowerCase() === 'true';
| | const oldInfo = block.querySelector(':scope > .chart-total-info'); |
| | if (oldInfo) oldInfo.remove(); |
|
| |
|
| // existierende Anzeige entfernen (falls Seite neu gerendert wird)
| | if (!hideTotal) { |
| const oldInfo = block.querySelector(':scope > .chart-total-info');
| | addTotalBelowLegend(chart, block); |
| if (oldInfo) oldInfo.remove();
| |
|
| |
|
| if (!hideTotal) {
| | if (window.ResizeObserver) { |
| // Gesamtzahl einfügen
| | const obs = new ResizeObserver(() => addTotalBelowLegend(chart, block)); |
| addTotalBelowLegend(chart, block);
| | obs.observe(chart.canvas); |
| | chart.$adosTotalObserver = obs; |
| | } |
| | } |
|
| |
|
| // bei Größenänderung (mobil <-> desktop) neu berechnen
| | block.dataset.rendered = '1'; |
| if (window.ResizeObserver) {
| | }); |
| const obs = new ResizeObserver(() => addTotalBelowLegend(chart, block)); | |
| obs.observe(chart.canvas);
| |
| chart.$adosTotalObserver = obs;
| |
| } | |
| } | | } |
|
| |
|
| block.dataset.rendered = '1';
| |
| });
| |
|
| |
| }
| |
|
| |
| // 5) Start: erst wenn DOM fertig, dann Chart.js laden, dann rendern
| |
| function boot($scope){ | | function boot($scope){ |
| var blocks = ($scope && $scope[0] ? $scope[0] : document).querySelectorAll('.ados-chart-multi'); | | var blocks = ($scope && $scope[0] ? $scope[0] : document).querySelectorAll('.ados-chart-multi'); |
| Zeile 982: |
Zeile 941: |
| mw.hook('wikipage.content').add(boot); | | mw.hook('wikipage.content').add(boot); |
| } else { | | } else { |
| // Fallback
| |
| (document.readyState === 'loading') | | (document.readyState === 'loading') |
| ? document.addEventListener('DOMContentLoaded', function(){ boot(); }) | | ? document.addEventListener('DOMContentLoaded', function(){ boot(); }) |
| Zeile 988: |
Zeile 946: |
| } | | } |
| })(); | | })(); |
|
| |
|
| |
|
|
| |
|
| // ==========================Scan================================== | | // ==========================Scan================================== |
|
| |
| mw.loader.using('mediawiki.util').then(function () { | | mw.loader.using('mediawiki.util').then(function () { |
| if (mw.config.get('wgPageName') !== 'LabelScan') return; | | if (mw.config.get('wgPageName') !== 'LabelScan') return; |
| Zeile 998: |
Zeile 954: |
| mw.loader.load('/index.php?title=MediaWiki:Gadget-LabelScan.css&action=raw&ctype=text/css', 'text/css'); | | mw.loader.load('/index.php?title=MediaWiki:Gadget-LabelScan.css&action=raw&ctype=text/css', 'text/css'); |
| }); | | }); |
|
| |
|
| |
|
| |
|
|
| |
|
| // ==========================ScanApp================================== | | // ==========================ScanApp================================== |
|
| |
| /* ==== PWA: Manifest + Service Worker + Install-Button (ES5) ==== */ | | /* ==== PWA: Manifest + Service Worker + Install-Button (ES5) ==== */ |
|
| |
|
| Zeile 1.032: |
Zeile 985: |
| }); | | }); |
|
| |
|
| // Sicherstellen, dass DOM existiert, bevor wir Handler setzen | | function onReady(fn){ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", fn); else fn(); } |
| function onReady(fn){ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", fn); else fn(); }
| |
| onReady(function () { | | onReady(function () { |
| var btn = document.getElementById("ados-install"); | | var btn = document.getElementById("ados-install"); |
| Zeile 1.045: |
Zeile 997: |
| }); | | }); |
| })(); | | })(); |
|
| |
|
| |
|
| if ('serviceWorker' in navigator) { | | if ('serviceWorker' in navigator) { |
| navigator.serviceWorker.register('/app/labelscan/sw.js').catch(function(){}); | | navigator.serviceWorker.register('/app/labelscan/sw.js').catch(function(){}); |
| } | | } |
|
| |
|
| |
|
| |
|
| |
|
| |
| /* =========================================================
| |
| ADOS XMAS: Timer-Bar & Snow (nur im Dezember)
| |
| ========================================================= */
| |
| (function () {
| |
| 'use strict';
| |
|
| |
| if (typeof window === 'undefined' || typeof document === 'undefined') return;
| |
|
| |
| var now = new Date();
| |
|
| |
| // Nur im Dezember aktiv (Monat 11 = Dezember)
| |
| if (now.getMonth() !== 11) {
| |
| return;
| |
| }
| |
|
| |
| // Wenn der Nutzer reduzierte Animationen bevorzugt → Schneefall aus
| |
| var prefersReducedMotion = false;
| |
| try {
| |
| prefersReducedMotion = window.matchMedia &&
| |
| window.matchMedia('(prefers-reduced-motion: reduce)').matches;
| |
| } catch (e) {}
| |
|
| |
| /* -----------------------------
| |
| 1) Nur bestehenden Timer „weihnachtlich anziehen“
| |
| ----------------------------- */
| |
| function decorateXmasTimer() {
| |
| var bar = document.getElementById('ados-timer-bar');
| |
| if (!bar) return; // Dein eigener Timer erzeugt dieses Element
| |
|
| |
| // Weihnachts-Design via CSS-Klasse aktivieren
| |
| bar.classList.add('ados-xmas');
| |
|
| |
| // Optional: Nachricht dezent mit 🎄 ergänzen (ohne alles zu überschreiben)
| |
| var msgEl = document.getElementById('ados-timer-message');
| |
| if (msgEl && !msgEl.dataset.xmasDecorated) {
| |
| msgEl.textContent = '🎄 ' + msgEl.textContent;
| |
| msgEl.dataset.xmasDecorated = '1';
| |
| }
| |
| }
| |
|
| |
| /* -----------------------------
| |
| 2) Schneefall-Effekt
| |
| ----------------------------- */
| |
| function initSnow() {
| |
| if (prefersReducedMotion) return;
| |
|
| |
| // Optional: auf sehr kleinen Geräten deaktivieren
| |
| if (window.innerWidth < 600) return;
| |
|
| |
| // Container anlegen
| |
| var container = document.createElement('div');
| |
| container.id = 'ados-snow';
| |
| document.body.appendChild(container);
| |
|
| |
| var flakeChars = ['❄', '✻', '✼', '✥', '✶'];
| |
| var flakeCount = 60; // dezent halten
| |
|
| |
| for (var i = 0; i < flakeCount; i++) {
| |
| var flake = document.createElement('span');
| |
| flake.className = 'ados-snowflake';
| |
| flake.textContent = flakeChars[Math.floor(Math.random() * flakeChars.length)];
| |
|
| |
| // Zufällige Start-Parameter
| |
| var startX = Math.random() * 100; // vw
| |
| var drift = (Math.random() * 40) - 20; // -20 bis +20 vw
| |
| var dur = 12 + Math.random() * 10; // 12–22 Sekunden
| |
| var delay = Math.random() * 20; // bis zu 20 Sekunden
| |
|
| |
| flake.style.left = startX + 'vw';
| |
| flake.style.setProperty('--x-start', '0vw');
| |
| flake.style.setProperty('--x-end', drift.toFixed(1) + 'vw');
| |
| flake.style.animationDuration = dur.toFixed(1) + 's';
| |
| flake.style.animationDelay = delay.toFixed(1) + 's';
| |
|
| |
| container.appendChild(flake);
| |
| }
| |
| }
| |
|
| |
| /* -----------------------------
| |
| 3) Init nach DOM-Ready
| |
| ----------------------------- */
| |
| function onReady(fn) {
| |
| if (document.readyState === 'loading') {
| |
| document.addEventListener('DOMContentLoaded', fn, { once: true });
| |
| } else {
| |
| fn();
| |
| }
| |
| }
| |
|
| |
| onReady(function () {
| |
| // nur dekorieren, NICHT neu bauen
| |
| decorateXmasTimer();
| |
| initSnow();
| |
| });
| |
|
| |
| })();
| |
|
| |
|
| |
| /* === ADOS – Winter-Schneefall (Desktop dezent, mobil deutlich sichtbar) === */
| |
| (function() {
| |
|
| |
| // Rücksicht auf Nutzer mit "Bewegung reduzieren"
| |
| if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
| |
|
| |
| function initSnow() {
| |
| const isMobile = window.innerWidth < 700;
| |
|
| |
| const container = document.createElement('div');
| |
| container.id = 'ados-snow';
| |
| container.style.position = 'fixed';
| |
| container.style.top = '0';
| |
| container.style.left = '0';
| |
| container.style.width = '100%';
| |
| container.style.height = '100%';
| |
| container.style.pointerEvents = 'none';
| |
| container.style.zIndex = '9999';
| |
| container.style.overflow = 'hidden';
| |
| document.body.appendChild(container);
| |
|
| |
| const flakeChars = ['•', '·', '∙'];
| |
|
| |
| // Desktop dezent, mobil kräftig
| |
| const flakeCount = isMobile ? 60 : 30;
| |
|
| |
| for (let i = 0; i < flakeCount; i++) {
| |
| const flake = document.createElement('div');
| |
| flake.textContent = flakeChars[Math.floor(Math.random() * flakeChars.length)];
| |
| flake.style.position = 'absolute';
| |
|
| |
| // Größe: mobil deutlich größer
| |
| const size = isMobile
| |
| ? (12 + Math.random() * 10) // Mobil: 12–22 px
| |
| : (5 + Math.random() * 4); // Desktop: 5–9 px
| |
|
| |
| flake.style.fontSize = size + 'px';
| |
|
| |
| // Sichtbarkeit (Opacity): mobil deutlich höher
| |
| const opacity = isMobile
| |
| ? (0.45 + Math.random() * 0.4) // Mobil: 0.45–0.85
| |
| : (0.15 + Math.random() * 0.18); // Desktop: 0.15–0.33
| |
|
| |
| flake.style.opacity = opacity.toFixed(2);
| |
| flake.style.color = '#ffffff';
| |
|
| |
| // Startposition
| |
| flake.style.left = Math.random() * 100 + 'vw';
| |
| flake.style.top = -(Math.random() * 20) + 'vh';
| |
|
| |
| // Geschwindigkeit: mobil etwas schneller, aber nicht hektisch
| |
| const duration = isMobile
| |
| ? (10 + Math.random() * 12) // Mobil: 10–22 s
| |
| : (18 + Math.random() * 22); // Desktop: 18–40 s
| |
|
| |
| const drift = isMobile ? 18 : 10; // seitliche Drift in px
| |
|
| |
| flake.style.animation = `adosSnowSoft ${duration}s linear infinite`;
| |
| flake.style.setProperty('--ados-snow-drift', drift + 'px');
| |
| flake.style.animationDelay = (-Math.random() * duration) + 's';
| |
|
| |
| container.appendChild(flake);
| |
| }
| |
| }
| |
|
| |
| // Keyframes: benutzen CSS-Variable für Drift
| |
| const style = document.createElement('style');
| |
| style.textContent = `
| |
| @keyframes adosSnowSoft {
| |
| 0% { transform: translateY(-12vh) translateX(0); }
| |
| 100% { transform: translateY(110vh) translateX(var(--ados-snow-drift, 10px)); }
| |
| }
| |
| `;
| |
| document.head.appendChild(style);
| |
|
| |
| if (document.readyState === 'loading') {
| |
| document.addEventListener('DOMContentLoaded', initSnow);
| |
| } else {
| |
| initSnow();
| |
| }
| |
|
| |
| })();
| |
|
| |
| /* ============================================================
| |
| ADOS – Feuerwerk (Raketen hoch + Explosion + optional "2026")
| |
| ES5 | dauerhaft | kein Abdunkeln/Overlay | Canvas transparent
| |
| ============================================================ */
| |
| (function () {
| |
| 'use strict';
| |
|
| |
| // true = nur 31.12/01.01, false = immer
| |
| var onlyOnNewYears = false;
| |
|
| |
| // Wahrscheinlichkeit, dass statt normaler Explosion "2026" erscheint
| |
| var YEAR_PROB = 0.15; // 0.10 = seltener, 0.25 = häufiger
| |
|
| |
| // Raketen-Geschwindigkeit (langsamer = kleinere Beträge)
| |
| var ROCKET_VY_MIN = -6.5;
| |
| var ROCKET_VY_MAX = -9.0;
| |
| var ROCKET_VX_MIN = -0.6;
| |
| var ROCKET_VX_MAX = 0.6;
| |
|
| |
| // Schwerkraft für Rakete (kleiner = ruhiger)
| |
| var ROCKET_GRAVITY = 0.003;
| |
|
| |
| function isNewYears() {
| |
| var d = new Date();
| |
| var m = d.getMonth() + 1;
| |
| var day = d.getDate();
| |
| return (m === 12 && day === 31) || (m === 1 && day === 1);
| |
| }
| |
| if (onlyOnNewYears && !isNewYears()) return;
| |
|
| |
| function createCanvas() {
| |
| var old = document.getElementById('ados-fireworks-canvas');
| |
| if (old && old.parentNode) old.parentNode.removeChild(old);
| |
|
| |
| var c = document.createElement('canvas');
| |
| c.id = 'ados-fireworks-canvas';
| |
| c.style.position = 'fixed';
| |
| c.style.left = '0';
| |
| c.style.top = '0';
| |
| c.style.width = '100%';
| |
| c.style.height = '100%';
| |
| c.style.pointerEvents = 'none';
| |
| c.style.zIndex = '9999';
| |
| c.style.opacity = '1';
| |
| document.body.appendChild(c);
| |
| return c;
| |
| }
| |
|
| |
| function fitCanvas(c) {
| |
| var dpr = window.devicePixelRatio || 1;
| |
| c.width = Math.floor(window.innerWidth * dpr);
| |
| c.height = Math.floor(window.innerHeight * dpr);
| |
| var ctx = c.getContext('2d');
| |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
| |
| // Glow/Leuchteffekt
| |
| ctx.globalCompositeOperation = 'lighter';
| |
| return ctx;
| |
| }
| |
|
| |
| function rand(min, max) { return min + Math.random() * (max - min); }
| |
| function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
| |
|
| |
| var canvas, ctx;
| |
| var rockets = [];
| |
| var particles = [];
| |
| var last = 0;
| |
| var running = true;
| |
|
| |
| // ------------------------------------------------------------
| |
| // "2026" Punkt-Matrix (5x7)
| |
| // ------------------------------------------------------------
| |
| function getDigitPoints(digit) {
| |
| var map = {
| |
| '0': [
| |
| "01110",
| |
| "10001",
| |
| "10011",
| |
| "10101",
| |
| "11001",
| |
| "10001",
| |
| "01110"
| |
| ],
| |
| '2': [
| |
| "01110",
| |
| "10001",
| |
| "00001",
| |
| "00010",
| |
| "00100",
| |
| "01000",
| |
| "11111"
| |
| ],
| |
| '6': [
| |
| "00110",
| |
| "01000",
| |
| "10000",
| |
| "11110",
| |
| "10001",
| |
| "10001",
| |
| "01110"
| |
| ]
| |
| };
| |
| return map[digit] || [];
| |
| }
| |
|
| |
| // ------------------------------------------------------------
| |
| // Rakete erzeugen
| |
| // ------------------------------------------------------------
| |
| function spawnRocket() {
| |
| var w = window.innerWidth;
| |
| var h = window.innerHeight;
| |
|
| |
| var x = rand(60, w - 60);
| |
| var y = h + rand(20, 120);
| |
|
| |
| // Explosionshöhe
| |
| var targetY = rand(h * 0.12, h * 0.48);
| |
|
| |
| // LANGSAMERES Hochschießen (konfig oben)
| |
| var vy = rand(ROCKET_VY_MIN, ROCKET_VY_MAX);
| |
| var vx = rand(ROCKET_VX_MIN, ROCKET_VX_MAX);
| |
|
| |
| // warmes Leuchten
| |
| var r = Math.floor(rand(220, 255));
| |
| var g = Math.floor(rand(170, 240));
| |
| var b = Math.floor(rand(80, 170));
| |
|
| |
| rockets.push({
| |
| x: x, y: y,
| |
| vx: vx, vy: vy,
| |
| targetY: targetY,
| |
| age: 0,
| |
| life: rand(2400, 3400),
| |
| r: r, g: g, b: b,
| |
| trail: [],
| |
| showYear: (Math.random() < YEAR_PROB)
| |
| });
| |
| }
| |
|
| |
| // ------------------------------------------------------------
| |
| // Normale Explosion
| |
| // ------------------------------------------------------------
| |
| function explode(x, y) {
| |
| var count = Math.floor(rand(55, 95));
| |
| var i;
| |
|
| |
| for (i = 0; i < count; i++) {
| |
| var angle = rand(0, Math.PI * 2);
| |
| var speed = rand(2.8, 6.8);
| |
|
| |
| var rr = Math.floor(rand(120, 255));
| |
| var gg = Math.floor(rand(80, 240));
| |
| var bb = Math.floor(rand(120, 255));
| |
|
| |
| // Gold/Amber Bias
| |
| if (Math.random() < 0.35) {
| |
| rr = Math.floor(rand(220, 255));
| |
| gg = Math.floor(rand(150, 230));
| |
| bb = Math.floor(rand(30, 120));
| |
| }
| |
|
| |
| particles.push({
| |
| x: x, y: y,
| |
| vx: Math.cos(angle) * speed,
| |
| vy: Math.sin(angle) * speed,
| |
| age: 0,
| |
| life: rand(900, 1500),
| |
| size: rand(1.8, 3.4),
| |
| r: rr, g: gg, b: bb,
| |
| type: 'normal'
| |
| });
| |
| }
| |
| }
| |
|
| |
| // ------------------------------------------------------------
| |
| // "2026" Explosion (Punkte ziehen sich kurz zur Zahl)
| |
| // ------------------------------------------------------------
| |
| function explode2026(cx, cy) {
| |
| var digits = ['2', '0', '2', '6'];
| |
|
| |
| // Optik-Parameter
| |
| var spacing = 34; // Abstand zwischen Ziffern
| |
| var pixel = 6; // Punktabstand innerhalb Ziffer
| |
| var rowsH = 7 * pixel;
| |
|
| |
| // Gesamtbreite grob berechnen: 4 Ziffern * (5*pixel) + 3*spacing
| |
| var totalW = (4 * (5 * pixel)) + (3 * spacing);
| |
| var startX = cx - (totalW / 2);
| |
| var baseY = cy - (rowsH / 2);
| |
|
| |
| var dx = startX;
| |
| var d, y, x, mat;
| |
|
| |
| for (d = 0; d < digits.length; d++) {
| |
| mat = getDigitPoints(digits[d]);
| |
|
| |
| for (y = 0; y < mat.length; y++) {
| |
| for (x = 0; x < mat[y].length; x++) {
| |
| if (mat[y].charAt(x) === '1') {
| |
| // Partikel startet am Explosionspunkt und „zieht“ zur Zielposition
| |
| particles.push({
| |
| x: cx,
| |
| y: cy,
| |
| vx: rand(-1.2, 1.2),
| |
| vy: rand(-1.2, 1.2),
| |
| tx: dx + x * pixel,
| |
| ty: baseY + y * pixel,
| |
| age: 0,
| |
| life: 1800,
| |
| size: 2.6,
| |
| r: 255,
| |
| g: 200,
| |
| b: 80,
| |
| type: 'digit'
| |
| });
| |
| }
| |
| }
| |
| }
| |
| dx += (5 * pixel) + spacing;
| |
| }
| |
| }
| |
|
| |
| // ------------------------------------------------------------
| |
| // Frame
| |
| // ------------------------------------------------------------
| |
| function tick(ts) {
| |
| if (!running) return;
| |
| if (!last) last = ts;
| |
| var dt = ts - last;
| |
| last = ts;
| |
|
| |
| var w = window.innerWidth;
| |
| var h = window.innerHeight;
| |
|
| |
| // Transparent löschen: kein Abdunkeln, kein Weiß-Schleier
| |
| ctx.globalCompositeOperation = 'source-over';
| |
| ctx.clearRect(0, 0, w, h);
| |
| ctx.globalCompositeOperation = 'lighter';
| |
|
| |
| // ---------------- Rockets ----------------
| |
| var i, r;
| |
| for (i = rockets.length - 1; i >= 0; i--) {
| |
| r = rockets[i];
| |
| r.age += dt;
| |
|
| |
| // Trail
| |
| r.trail.push({ x: r.x, y: r.y });
| |
| if (r.trail.length > 16) r.trail.shift();
| |
|
| |
| // Physik (langsamer & ruhiger)
| |
| r.vy += ROCKET_GRAVITY * (dt / 16);
| |
| r.x += r.vx * (dt / 16);
| |
| r.y += r.vy * (dt / 16);
| |
|
| |
| // Explodieren
| |
| if (r.y <= r.targetY) {
| |
| if (r.showYear) explode2026(r.x, r.y);
| |
| else explode(r.x, r.y);
| |
| rockets.splice(i, 1);
| |
| continue;
| |
| }
| |
|
| |
| // Safety
| |
| if (r.age >= r.life || r.y < -200 || r.x < -200 || r.x > w + 200) {
| |
| rockets.splice(i, 1);
| |
| continue;
| |
| }
| |
|
| |
| // Trail zeichnen
| |
| ctx.beginPath();
| |
| ctx.lineWidth = 2.6;
| |
| ctx.strokeStyle = 'rgba(' + r.r + ',' + r.g + ',' + r.b + ',0.30)';
| |
| var t;
| |
| for (t = 0; t < r.trail.length; t++) {
| |
| var pt = r.trail[t];
| |
| if (t === 0) ctx.moveTo(pt.x, pt.y);
| |
| else ctx.lineTo(pt.x, pt.y);
| |
| }
| |
| ctx.stroke();
| |
|
| |
| // Kopf
| |
| ctx.beginPath();
| |
| ctx.fillStyle = 'rgba(' + r.r + ',' + r.g + ',' + r.b + ',1)';
| |
| ctx.arc(r.x, r.y, 2.7, 0, Math.PI * 2, false);
| |
| ctx.fill();
| |
| }
| |
|
| |
| // ---------------- Particles ----------------
| |
| var p;
| |
| for (i = particles.length - 1; i >= 0; i--) {
| |
| p = particles[i];
| |
| p.age += dt;
| |
|
| |
| if (p.age >= p.life) {
| |
| particles.splice(i, 1);
| |
| continue;
| |
| }
| |
|
| |
| // "2026"-Partikel: zuerst zur Zielposition ziehen, dann zerfallen lassen
| |
| if (p.type === 'digit' && p.age < 900) {
| |
| // sanftes „Anziehen“ zur Zahl
| |
| p.x += (p.tx - p.x) * 0.085;
| |
| p.y += (p.ty - p.y) * 0.085;
| |
| } else {
| |
| // normales Partikel-Verhalten
| |
| p.vy += 0.018 * (dt / 16);
| |
| p.vx *= Math.pow(0.986, dt / 16);
| |
| p.vy *= Math.pow(0.986, dt / 16);
| |
| p.x += p.vx * (dt / 16);
| |
| p.y += p.vy * (dt / 16);
| |
| }
| |
|
| |
| var alpha = clamp(1 - (p.age / p.life), 0, 1);
| |
|
| |
| ctx.beginPath();
| |
| ctx.fillStyle = 'rgba(' + p.r + ',' + p.g + ',' + p.b + ',' + alpha + ')';
| |
| ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2, false);
| |
| ctx.fill();
| |
| }
| |
|
| |
| requestAnimationFrame(tick);
| |
| }
| |
|
| |
| // ------------------------------------------------------------
| |
| // Spawn-Frequenz (klassisch, aber nicht Dauerfeuer)
| |
| // ------------------------------------------------------------
| |
| function scheduleRockets() {
| |
| function loop() {
| |
| if (!running) return;
| |
|
| |
| // meist 1 Rakete, manchmal 2
| |
| spawnRocket();
| |
| if (Math.random() < 0.28) spawnRocket();
| |
|
| |
| setTimeout(loop, Math.floor(rand(1200, 2200)));
| |
| }
| |
| loop();
| |
| }
| |
|
| |
| function init() {
| |
| canvas = createCanvas();
| |
| ctx = fitCanvas(canvas);
| |
|
| |
| window.addEventListener('resize', function () {
| |
| if (!canvas) return;
| |
| ctx = fitCanvas(canvas);
| |
| });
| |
|
| |
| scheduleRockets();
| |
| requestAnimationFrame(tick);
| |
| }
| |
|
| |
| if (document.readyState === 'loading') {
| |
| document.addEventListener('DOMContentLoaded', init, false);
| |
| } else {
| |
| init();
| |
| }
| |
| })();
| |