MediaWiki:Common.js: Unterschied zwischen den Versionen
Admin (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung Markierung: Zurückgesetzt |
Admin (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung Markierung: Manuelle Zurücksetzung |
||
| Zeile 1.050: | Zeile 1.050: | ||
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(); | |||
} | |||
})(); | })(); | ||