|
|
| 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();
| |
| }
| |
| })(); | | })(); |