MediaWiki:Common.js: Unterschied zwischen den Versionen

Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
Zeile 739: Zeile 739:
// -------------------------------------
// -------------------------------------


/* Whisky News – robustes Popup (v2)
/* Whisky News – World of Whisky (Mannheim) – Popup (v3, Wave-Fix)
   - Fixe: ResizeObserver + sichtbarkeitsgesteuertes Start/Stop
   - Flüssigkeit wird als Wellen-Path gefüllt (keine harte Kante mehr)
   - HiDPI korrekt
   - HiDPI & ResizeObserver wie gehabt
  - CSS/SVG-Fallback falls Canvas nicht verfügbar oder reduce-motion aktiv
*/
*/
mw.loader.using(['mediawiki.util','jquery']).then(function(){
mw.loader.using(['mediawiki.util','jquery']).then(function(){
Zeile 750: Zeile 749:
     var CONFIG = {
     var CONFIG = {
       enabled: true,
       enabled: true,
       id: 'wow_mannheim_whisky_news_v2', // Version erhöhen
       id: 'wow_mannheim_whisky_news_v3',
       title: 'Whisky News: Messeabfüllungen – World of Whisky (Mannheim)',
       title: 'Whisky News: Messeabfüllungen – World of Whisky (Mannheim)',
       introHTML: '<p>Frisch zur Messe in Mannheim: Zwei limitierte Abfüllungen. Schau sie dir an und bewerte sie im Wiki!</p>',
       introHTML: '<p>Frisch zur Messe in Mannheim: Zwei limitierte Abfüllungen. Schau sie dir an und bewerte sie im Wiki!</p>',
       images: [
       images: [
         {
         {
          // TODO: durch finale Bild-URL ersetzen
           src: 'https://example.com/path/South_Islay_13_Sherry_Octave.jpg', // ← deine finale URL
           src: 'https://ados-wiki.de/images/2/2f/South_Islay_Single_Malt_13_year-old_%28Sherry_Octave_Cask_Finish%29.single.jpg',
           alt: 'South Islay 13y – Sherry Octave Cask Finish',
           alt: 'South Islay 13y – Sherry Octave Cask Finish',
           link: 'https://ados-wiki.de/wiki/South_Islay_Single_Malt_13_year-old_(Sherry_Octave_Cask_Finish)',
           link: 'https://ados-wiki.de/wiki/South_Islay_Single_Malt_13_year-old_(Sherry_Octave_Cask_Finish)',
Zeile 762: Zeile 760:
         },
         },
         {
         {
          // TODO: durch finale Bild-URL ersetzen
           src: 'https://example.com/path/Tullibardine_13_Shiraz_Octave.jpg', // ← deine finale URL
           src: 'https://ados-wiki.de/images/9/95/Tullibardine_13_year-old_%28Shiraz_Wine_Octave_Cask_Finish%29.single.jpg',
           alt: 'Tullibardine 13y – Shiraz Wine Octave Cask Finish',
           alt: 'Tullibardine 13y – Shiraz Wine Octave Cask Finish',
           link: 'https://ados-wiki.de/wiki/Tullibardine_13_year-old',
           link: 'https://ados-wiki.de/wiki/Tullibardine_13_year-old',
Zeile 770: Zeile 767:
       ],
       ],
       showOnNamespaces: 'all',
       showOnNamespaces: 'all',
      dailyLimit: 1,
       escToClose: true,
       escToClose: true,
       clickBackdropToClose: true
       clickBackdropToClose: true
Zeile 782: Zeile 778:
         $.inArray(ns, CONFIG.showOnNamespaces) === -1) return;
         $.inArray(ns, CONFIG.showOnNamespaces) === -1) return;


     // 1× pro Tag
     // 1×/Tag
     var isAnon = (mw.config.get('wgUserName') === null);
     var isAnon = (mw.config.get('wgUserName') === null);
     function LSget(k){ try { return localStorage.getItem(k); } catch(e){ return null; } }
     function LSget(k){ try { return localStorage.getItem(k); } catch(e){ return null; } }
Zeile 792: Zeile 788:


     $(function(){
     $(function(){
      // Grundgerüst
       var $overlay = $('<div>', {'class':'mw-popup-overlay'});
       var $overlay = $('<div>', {'class':'mw-popup-overlay'});
       var $modal = $('<div>', {'class':'mw-popup-modal','role':'dialog','aria-modal':'true','aria-labelledby':'mw-news-title'});
       var $modal = $('<div>', {'class':'mw-popup-modal','role':'dialog','aria-modal':'true','aria-labelledby':'mw-news-title'});


      // Bühne
       var $stage = $('<div>', {'class':'mw-fw-canvas-wrap'});
       var $stage = $('<div>', {'class':'mw-fw-canvas-wrap'});
       var $canvas = $('<canvas>', {'class':'mw-fw-canvas','aria-hidden':'true'});
       var $canvas = $('<canvas>', {'class':'mw-fw-canvas','aria-hidden':'true'});
Zeile 804: Zeile 798:
       var $intro = $('<div>', {'class':'mw-popup-content'}).html(CONFIG.introHTML);
       var $intro = $('<div>', {'class':'mw-popup-content'}).html(CONFIG.introHTML);


      // Karten
       var $cards = $('<div>', {'class':'mw-wnews-cards'});
       var $cards = $('<div>', {'class':'mw-wnews-cards'});
       CONFIG.images.forEach(function(img){
       CONFIG.images.forEach(function(img){
Zeile 831: Zeile 824:
         $(document).off('keydown.mwwnews visibilitychange');
         $(document).off('keydown.mwwnews visibilitychange');
       }
       }
      if (CONFIG.clickBackdropToClose) $overlay.on('click', close);
       $ok.on('click', close);
       $ok.on('click', close);
      if (CONFIG.clickBackdropToClose) $overlay.on('click', close);
       if (CONFIG.escToClose){
       if (CONFIG.escToClose){
         $(document).on('keydown.mwwnews', function(e){
         $(document).on('keydown.mwwnews', function(e){
Zeile 840: Zeile 833:
       }
       }


       // ===== Animation (Canvas) – Whisky-Glas =====
       // ===== Canvas-Animation (Wave-Fix) =====
       var canvas = $canvas[0];
       var canvas = $canvas[0], ctx = canvas.getContext && canvas.getContext('2d');
      var ctx = canvas.getContext && canvas.getContext('2d');
       var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
       var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
       var dpr = 1, width=0, height=0, raf=null, started=false, bubbles=[], t0=0;
       var dpr=1, cw=0, ch=0, raf=null, t0=0, started=false, bubbles=[], ro;
      var ro;


       function setSize(){
       function setSize(){
         var rect = $stage[0].getBoundingClientRect();
         var r = $stage[0].getBoundingClientRect();
         if (rect.width <= 0 || rect.height <= 0) return false;
         if (r.width <= 0 || r.height <= 0) return false;
         var newDpr = Math.max(1, window.devicePixelRatio || 1);
         var newDpr = Math.max(1, window.devicePixelRatio || 1);
         if (width !== rect.width || height !== rect.height || dpr !== newDpr){
         if (cw !== r.width || ch !== r.height || dpr !== newDpr){
           dpr = newDpr;
           dpr = newDpr; cw = r.width; ch = r.height;
          width = rect.width; height = rect.height;
           canvas.width = Math.floor(cw*dpr);
           canvas.width = Math.floor(width*dpr);
           canvas.height = Math.floor(ch*dpr);
           canvas.height = Math.floor(height*dpr);
           canvas.style.width = cw+'px';
           canvas.style.width = width+'px';
           canvas.style.height = ch+'px';
           canvas.style.height = height+'px';
         }
         }
         return true;
         return true;
       }
       }
      function rand(min,max){ return Math.random()*(max-min)+min; }
      function hsla(h,s,l,a){ return 'hsla('+h+','+s+'%,'+l+'%,'+a+')'; }


       function drawGlass(){
       function drawGlass(){
Zeile 879: Zeile 866:
         ctx.closePath();
         ctx.closePath();
         ctx.stroke();
         ctx.stroke();
 
         // kleiner Glanz
         // leichter Glanz
         ctx.beginPath();
         ctx.beginPath();
         ctx.moveTo(gx+gw*0.15, gy+gh*0.1);
         ctx.moveTo(gx+gw*0.15, gy+gh*0.1);
Zeile 889: Zeile 875:
         return {x:gx,y:gy,w:gw,h:gh,r:r};
         return {x:gx,y:gy,w:gw,h:gh,r:r};
       }
       }
      function rand(min,max){ return Math.random()*(max-min)+min; }


       function resetBubble(b, liquidTop, glass){
       function resetBubble(b, liquidTop, glass){
Zeile 898: Zeile 886:
       }
       }


       function frame(ts){
       function drawLiquid(glass, t){
        if (!t0) t0 = ts;
         var base = glass.y + glass.h*0.58;             // mittlere Füllhöhe
        var t = (ts - t0)/1000;
         var amp  = Math.min(14*dpr, canvas.height*0.03);// Amplitude
 
        // Hintergrund
        ctx.globalCompositeOperation = 'source-over';
        ctx.fillStyle = 'rgba(5,10,20,0.18)';
        ctx.fillRect(0,0,canvas.width,canvas.height);
 
        // Glas
        var glass = drawGlass();
 
        // Flüssigkeit
         var base = glass.y + glass.h*0.58;
         var amp  = Math.min(14*dpr, canvas.height*0.03);
         var wave = Math.sin(t*2.2)*amp;
         var wave = Math.sin(t*2.2)*amp;
         var liquidTop = base + wave;
         var topY = base + wave;


         // Whisky
         // Farbverlauf
         var grd = ctx.createLinearGradient(0, liquidTop-30*dpr, 0, glass.y+glass.h);
         var grd = ctx.createLinearGradient(0, topY-30*dpr, 0, glass.y+glass.h);
         grd.addColorStop(0, 'rgba(255,190,90,0.95)');
         grd.addColorStop(0, 'rgba(255,190,90,0.96)');
         grd.addColorStop(1, 'rgba(170,85,20,0.98)');
         grd.addColorStop(1, 'rgba(170,85,20,0.98)');


Zeile 932: Zeile 908:
         ctx.clip();
         ctx.clip();


         // Füllung
         // === Wellen-PATH als komplette Flüssigkeit (kein Rechteck!) ===
        ctx.fillStyle = grd;
        ctx.fillRect(glass.x, liquidTop, glass.w, glass.y+glass.h - liquidTop);
 
        // Wellenkamm
         ctx.beginPath();
         ctx.beginPath();
         ctx.moveTo(glass.x, liquidTop);
         ctx.moveTo(glass.x, topY);
         for (var x=0; x<=glass.w; x+=6*dpr){
         for (var x=0; x<=glass.w; x+=6*dpr){
           var y = liquidTop + Math.sin((x*0.05) + t*3.2)*amp*0.2;
           var y = topY + Math.sin((x*0.05) + t*3.2) * amp * 0.22;
           ctx.lineTo(glass.x + x, y);
           ctx.lineTo(glass.x + x, y);
         }
         }
         ctx.lineTo(glass.x+glass.w, glass.y+glass.h);
        // Seiten + Boden schließen
         ctx.lineTo(glass.x, glass.y+glass.h);
         ctx.lineTo(glass.x + glass.w, glass.y + glass.h);
         ctx.lineTo(glass.x,             glass.y + glass.h);
         ctx.closePath();
         ctx.closePath();
         ctx.fillStyle = 'rgba(255,165,70,0.3)';
         ctx.fillStyle = grd;
         ctx.fill();
         ctx.fill();


         // Bubbles
         // dezente helle Gischt auf dem Kamm
        ctx.beginPath();
        ctx.moveTo(glass.x, topY);
        for (var x2=0; x2<=glass.w; x2+=6*dpr){
          var y2 = topY + Math.sin((x2*0.05) + t*3.2) * amp * 0.22;
          ctx.lineTo(glass.x + x2, y2);
        }
        ctx.strokeStyle = 'rgba(255,215,120,0.35)';
        ctx.lineWidth = Math.max(1, 1*dpr);
        ctx.stroke();
 
        // Blasen (nur innerhalb der Flüssigkeit)
         if (bubbles.length === 0){
         if (bubbles.length === 0){
           for (var i=0;i<120;i++){ bubbles.push({}); resetBubble(bubbles[i], liquidTop, glass); }
           for (var i=0;i<120;i++){ bubbles.push({}); resetBubble(bubbles[i], topY, glass); }
         }
         }
         ctx.globalCompositeOperation = 'lighter';
         ctx.globalCompositeOperation = 'lighter';
         bubbles.forEach(function(b){
         bubbles.forEach(function(b){
           if (b.y < liquidTop + 3*dpr) {
           if (b.y < topY + 3*dpr) resetBubble(b, topY, glass);
            resetBubble(b, liquidTop, glass);
           else {
           } else {
             b.y -= b.speed * (1 + Math.sin(t*3 + b.x*0.02)*0.2);
             b.y -= b.speed * (1 + Math.sin(t*3 + b.x*0.02)*0.2);
             b.x += Math.sin(t*2 + b.y*0.02)*b.wobble*0.15;
             b.x += Math.sin(t*2 + b.y*0.02)*b.wobble*0.15;
Zeile 972: Zeile 955:
         });
         });


         ctx.restore();
         ctx.restore(); // Clip
      }
 
      function frame(ts){
        if (!t0) t0 = ts;
        var t = (ts - t0)/1000;
 
        // weiches Redraw
        ctx.globalCompositeOperation = 'source-over';
        ctx.fillStyle = 'rgba(5,10,20,0.16)';
        ctx.fillRect(0,0,canvas.width,canvas.height);
 
        var glass = drawGlass();
        drawLiquid(glass, t);


         // kleiner Randglanz
         // kleiner Randglanz
Zeile 988: Zeile 984:


       function startAnim(){
       function startAnim(){
         if (!ctx || reduce) { renderFallback(); return; }
         if (!ctx || (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches)) return;
         if (!setSize()) {
         if (!setSize()) { setTimeout(startAnim, 50); return; }
          // falls noch 0×0 (z. B. wegen Layout), später versuchen
          setTimeout(startAnim, 50);
          return;
        }
         if (started) return;
         if (started) return;
         started = true;
         started = true; t0 = 0;
        t0 = 0;
         raf = requestAnimationFrame(frame);
         raf = requestAnimationFrame(frame);


        // weiter auf Größenänderungen reagieren
         if ('ResizeObserver' in window) {
         if ('ResizeObserver' in window) {
           ro = new ResizeObserver(function(){
           ro = new ResizeObserver(setSize);
            setSize();
          });
           ro.observe($stage[0]);
           ro.observe($stage[0]);
         } else {
         } else {
           $(window).on('resize.mwwnews', setSize);
           $(window).on('resize.mwwnews', setSize);
         }
         }
        // Tab-Visibility
         document.addEventListener('visibilitychange', onVis);
         document.addEventListener('visibilitychange', onVis);
       }
       }
 
       function stopAnim(remove){
       function stopAnim(removeListeners){
         if (raf){ cancelAnimationFrame(raf); raf=null; }
         if (raf){ cancelAnimationFrame(raf); raf=null; }
         started = false;
         started = false;
         if (removeListeners){
         if (remove){
           if (ro){ ro.disconnect(); ro=null; } else { $(window).off('resize.mwwnews'); }
           if (ro){ ro.disconnect(); ro=null; } else { $(window).off('resize.mwwnews'); }
           document.removeEventListener('visibilitychange', onVis);
           document.removeEventListener('visibilitychange', onVis);
         }
         }
       }
       }
      function onVis(){ if (document.hidden) stopAnim(false); else startAnim(); }


      function onVis(){
        if (document.hidden) stopAnim(false);
        else startAnim();
      }
      // Fallback: statisches SVG mit CSS-Blasen, falls kein Canvas oder reduce-motion
      function renderFallback(){
        var svg = '' +
          '<svg viewBox="0 0 600 230" xmlns="http://www.w3.org/2000/svg" class="mw-fw-fallback">' +
          ' <defs>' +
          '  <linearGradient id="whisky" x1="0" y1="0" x2="0" y2="1">' +
          '    <stop offset="0%" stop-color="#FFBE5A"/>' +
          '    <stop offset="100%" stop-color="#AA5514"/>' +
          '  </linearGradient>' +
          ' </defs>' +
          ' <rect x="120" y="40" width="360" height="150" rx="18" ry="18" fill="none" stroke="rgba(255,255,255,0.35)" stroke-width="3"/>' +
          ' <rect x="120" y="115" width="360" height="75" rx="18" ry="18" fill="url(#whisky)"/>' +
          ' <g class="bubbles">' +
          '  <circle cx="180" cy="180" r="5" fill="rgba(255,220,140,0.9)"/>' +
          '  <circle cx="260" cy="190" r="4" fill="rgba(255,220,140,0.9)"/>' +
          '  <circle cx="340" cy="185" r="6" fill="rgba(255,220,140,0.9)"/>' +
          '  <circle cx="420" cy="195" r="4" fill="rgba(255,220,140,0.9)"/>' +
          ' </g>' +
          '</svg>';
        $stage.html(svg);
      }
      // Start etwas verzögert, damit Layout steht
       setTimeout(startAnim, 0);
       setTimeout(startAnim, 0);
       markSeen();
       markSeen();
     });
     });
   })(jQuery, mw);
   })(jQuery, mw);
});
});