MediaWiki:Common.js

From Skateboarding on the Pacific Ocean
Revision as of 11:00, 16 April 2026 by King of the colours (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
 * Willoughby Tucker, I'll Always Love You — MediaWiki Skin JS
 * Ethel Cain, 2025
 *
 * Load via MediaWiki:Common.js or skin template
 * Requires: vanilla JS, no dependencies
 *
 * Effects:
 *  1. Amber loading bar
 *  2. Dust particle system
 *  3. Magnetic cursor (dot + ring)
 *  4. Scroll-reveal observer
 *  5. Sidebar link ripples
 *  6. Title ambient pulse
 *  7. Search placeholder cycling
 *  8. VHS glitch on title
 *  9. Table row glow
 * 10. Fog parallax on sidebar
 */

/* ═══════════════════════════════════════════════════════
   JS EFFECTS
   ═══════════════════════════════════════════════════════ */

/* 1. Amber loading bar */
(function() {
  const bar = document.getElementById('loading-bar');
  if (!bar) return;
  let w = 0;
  const iv = setInterval(() => {
    w += Math.random() * 18 + 4;
    if (w >= 95) { clearInterval(iv); w = 95; }
    bar.style.width = w + '%';
  }, 80);
  window.addEventListener('load', () => {
    clearInterval(iv);
    bar.style.width = '100%';
    setTimeout(() => { bar.style.opacity = '0'; bar.style.transition = 'opacity 0.5s'; }, 300);
  });
})();

/* 2. Dust particle system */
(function() {
  const canvas = document.getElementById('dust-canvas');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');
  let W, H, particles = [];

  function resize() {
    W = canvas.width = window.innerWidth;
    H = canvas.height = window.innerHeight;
  }

  function createParticle() {
    return {
      x: Math.random() * W,
      y: H + Math.random() * 20,
      vx: (Math.random() - 0.5) * 0.4,
      vy: -(Math.random() * 0.6 + 0.1),
      size: Math.random() * 2 + 0.3,
      opacity: Math.random() * 0.5 + 0.1,
      life: 0,
      maxLife: Math.random() * 600 + 200,
      hue: Math.random() * 20 + 25
    };
  }

  for (let i = 0; i < 80; i++) {
    const p = createParticle();
    p.y = Math.random() * H;
    p.life = Math.random() * p.maxLife;
    particles.push(p);
  }

  function draw() {
    ctx.clearRect(0, 0, W, H);
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx + Math.sin(p.life * 0.01) * 0.15;
      p.y += p.vy;
      p.life++;

      const fade = p.life < 60 ? p.life / 60 : p.life > p.maxLife - 60 ? (p.maxLife - p.life) / 60 : 1;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
      ctx.fillStyle = `hsla(${p.hue}, 55%, 60%, ${p.opacity * fade})`;
      ctx.fill();

      if (p.life >= p.maxLife || p.y < -10) {
        particles[i] = createParticle();
      }
    }

    // Occasional larger dust mote
    if (Math.random() < 0.01 && particles.length < 120) {
      const mote = createParticle();
      mote.size = Math.random() * 3 + 1.5;
      mote.opacity = 0.08;
      mote.vy = -(Math.random() * 0.2 + 0.05);
      particles.push(mote);
    }

    requestAnimationFrame(draw);
  }

  resize();
  window.addEventListener('resize', resize);
  draw();
})();

/* 3. Magnetic cursor */
(function() {
  const dot = document.getElementById('cursor-dot');
  const ring = document.getElementById('cursor-ring');
  if (!dot || !ring) return;

  let mx = 0, my = 0, rx = 0, ry = 0;
  let entered = false;

  document.addEventListener('mouseenter', () => {
    entered = true;
    dot.style.opacity = '1';
    ring.style.opacity = '1';
  });

  document.addEventListener('mouseleave', () => {
    entered = false;
    dot.style.opacity = '0';
    ring.style.opacity = '0';
  });

  document.addEventListener('mousemove', e => {
    mx = e.clientX; my = e.clientY;
    dot.style.left = mx + 'px';
    dot.style.top  = my + 'px';
    if (!entered) { dot.style.opacity = '1'; ring.style.opacity = '0.7'; entered = true; }
  });

  // Links make cursor expand
  document.querySelectorAll('a, button').forEach(el => {
    el.addEventListener('mouseenter', () => {
      ring.style.width  = '44px';
      ring.style.height = '44px';
      ring.style.borderColor = 'rgba(200,132,58,0.8)';
      dot.style.transform = 'translate(-50%,-50%) scale(1.8)';
    });
    el.addEventListener('mouseleave', () => {
      ring.style.width  = '28px';
      ring.style.height = '28px';
      ring.style.borderColor = 'rgba(200,132,58,0.4)';
      dot.style.transform = 'translate(-50%,-50%) scale(1)';
    });
  });

  // Smooth ring follow
  function lerp(a, b, t) { return a + (b - a) * t; }
  function animate() {
    rx = lerp(rx, mx, 0.12);
    ry = lerp(ry, my, 0.12);
    ring.style.left = rx + 'px';
    ring.style.top  = ry + 'px';
    requestAnimationFrame(animate);
  }
  animate();
})();

/* 4. Scroll-reveal observer */
(function() {
  const els = document.querySelectorAll('.scroll-reveal');
  const obs = new IntersectionObserver(entries => {
    entries.forEach(e => {
      if (e.isIntersecting) {
        e.target.classList.add('revealed');
        obs.unobserve(e.target);
      }
    });
  }, { threshold: 0.12 });
  els.forEach(el => obs.observe(el));
})();

/* 5. Sidebar link ripple */
(function() {
  document.querySelectorAll('.portal a').forEach(link => {
    link.addEventListener('click', function(e) {
      const ripple = document.createElement('span');
      ripple.style.cssText = `
        position: absolute;
        border-radius: 50%;
        background: rgba(200,132,58,0.25);
        width: 6px; height: 6px;
        top: 50%; left: ${e.offsetX}px;
        transform: translate(-50%,-50%) scale(0);
        animation: ripple-out 0.5s ease forwards;
        pointer-events: none;
      `;
      link.appendChild(ripple);
      setTimeout(() => ripple.remove(), 500);
    });
  });

  const style = document.createElement('style');
  style.textContent = `
    @keyframes ripple-out {
      to { transform: translate(-50%,-50%) scale(20); opacity: 0; }
    }
  `;
  document.head.appendChild(style);
})();

/* 6. Ambient audio visualizer hint (subtle amber pulse on #firstHeading) */
(function() {
  const title = document.getElementById('firstHeading');
  if (!title) return;
  let frame = 0;
  function pulse() {
    frame++;
    const s = 1 + Math.sin(frame * 0.025) * 0.003;
    const g = 0.3 + Math.sin(frame * 0.018) * 0.15;
    title.style.textShadow = `0 2px ${30 + Math.sin(frame*0.02)*20}px rgba(200,132,58,${g})`;
    requestAnimationFrame(pulse);
  }
  pulse();
})();

/* 7. Search bar typewriter placeholder cycling */
(function() {
  const input = document.getElementById('searchInput');
  if (!input) return;
  const phrases = [
    'search the dust…',
    'find Willoughby…',
    'janie, please stay…',
    'nettles and ghosts…',
    'waco, texas, 1986…',
    'i\'ll always love you…'
  ];
  let i = 0;
  setInterval(() => {
    if (document.activeElement !== input) {
      i = (i + 1) % phrases.length;
      input.placeholder = phrases[i];
    }
  }, 3200);
})();

/* 8. VHS glitch flicker on title — rare, occasional */
(function() {
  const title = document.getElementById('firstHeading');
  if (!title) return;
  function maybeGlitch() {
    if (Math.random() < 0.3) {
      title.style.transition = 'none';
      title.style.transform = `translateX(${(Math.random()-0.5)*3}px)`;
      title.style.filter = 'brightness(1.3) saturate(0.5)';
      setTimeout(() => {
        title.style.transform = 'translateX(0)';
        title.style.filter = '';
      }, 60 + Math.random() * 80);
    }
    setTimeout(maybeGlitch, 4000 + Math.random() * 8000);
  }
  setTimeout(maybeGlitch, 5000);
})();

/* 9. Table row glow on hover */
(function() {
  document.querySelectorAll('.wikitable tbody tr').forEach(row => {
    row.addEventListener('mouseenter', () => {
      row.style.boxShadow = 'inset 0 0 40px rgba(200,132,58,0.07)';
    });
    row.addEventListener('mouseleave', () => {
      row.style.boxShadow = '';
    });
  });
})();

/* 10. Fog drifting parallax on sidebar */
(function() {
  let ticking = false;
  window.addEventListener('scroll', () => {
    if (!ticking) {
      requestAnimationFrame(() => {
        const offset = window.scrollY * 0.15;
        const panel = document.getElementById('mw-panel');
        if (panel) panel.style.backgroundPositionY = offset + 'px';
        ticking = false;
      });
      ticking = true;
    }
  });
})();