/* ========================================================================
   LUMINA — Trippy / Rave visualizers (more tunnel/glow/glitch)
   ======================================================================== */
(function () {
  const TAU = Math.PI * 2;
  const hsla = (h, s = 80, l = 60, a = 1) => `hsla(${h}, ${s}%, ${l}%, ${a})`;
  const bg = (ctx, w, h, P) => {
    ctx.fillStyle = `rgba(8,8,12,${P.bgAlpha ?? 0.18})`;
    ctx.fillRect(0, 0, w, h);
  };

  /* ============ FRACTAL ZOOM (Mandelbrot-ish recursive squares) ============ */
  const FractalZoom = {
    id: "fractalZoom", name: "Fractal Zoom", tag: "RECURSE",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <rect x="2" y="2" width="20" height="20"/>
      <rect x="6" y="6" width="12" height="12"/>
      <rect x="9" y="9" width="6" height="6"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const cx = w/2, cy = h/2;
      const N = 16;
      const rot = t * 0.5 * P.motion + A.bass * 2;
      const baseScale = 1.18 + A.bass * 0.15;
      ctx.shadowBlur = P.glow * 12;
      for (let i = N; i > 0; i--) {
        const s = Math.pow(baseScale, -i) * Math.min(w, h) * 0.5 * Math.pow(baseScale, N);
        const angle = rot * (i / N);
        const hue = (P.hue + i * 22 + t * 60) % 360;
        ctx.save();
        ctx.translate(cx, cy);
        ctx.rotate(angle);
        ctx.strokeStyle = hsla(hue, 100, 60, 1 - i / N * 0.6);
        ctx.shadowColor = hsla(hue, 100, 60);
        ctx.lineWidth = P.lineWidth + (i % 3 === 0 ? 2 : 0);
        ctx.strokeRect(-s, -s, s * 2, s * 2);
        ctx.restore();
      }
    },
  };

  /* ============ MIRROR BALL / DISCO ============ */
  const MirrorBall = {
    id: "mirrorBall", name: "Disco Ball", tag: "RAVE",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <circle cx="12" cy="12" r="9"/>
      <path d="M3 12h18M12 3v18M5 5l14 14M19 5L5 19"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const cx = w/2, cy = h/2;
      const r = Math.min(w, h) * 0.3 * (1 + A.bass * 0.15);
      // Disco ball body
      const tiles = 18;
      for (let lat = 0; lat < tiles; lat++) {
        for (let lon = 0; lon < tiles * 2; lon++) {
          const phi = (lat / tiles) * Math.PI;
          const theta = (lon / (tiles * 2)) * TAU + t * 0.3 * P.motion;
          const sinPhi = Math.sin(phi);
          const x = cx + r * sinPhi * Math.cos(theta);
          const y = cy + r * Math.cos(phi);
          const depth = sinPhi * Math.sin(theta) * 0.5 + 0.5;
          const sparkle = Math.random() > 0.95 ? 1 : 0;
          const hue = (P.hue + lat * 10 + lon * 5) % 360;
          ctx.fillStyle = hsla(hue, 100, 50 + depth * 40 + sparkle * 30,
            0.4 + depth * 0.6);
          if (sparkle) {
            ctx.shadowColor = "#fff";
            ctx.shadowBlur = 8;
          } else ctx.shadowBlur = 0;
          const sz = r / tiles * 1.3;
          ctx.fillRect(x - sz / 2, y - sz / 2, sz, sz);
        }
      }
      ctx.shadowBlur = 0;
      // Light beams
      const beams = 24;
      ctx.globalCompositeOperation = "lighter";
      for (let i = 0; i < beams; i++) {
        const a = (i / beams) * TAU + t * 0.4 * P.motion;
        const hue = (P.hue + i * 15) % 360;
        const len = Math.max(w, h) * (0.6 + A.energy * 0.5);
        const grd = ctx.createLinearGradient(cx, cy, cx + Math.cos(a) * len, cy + Math.sin(a) * len);
        grd.addColorStop(0, hsla(hue, 100, 70, 0.5 * P.intensity));
        grd.addColorStop(1, hsla(hue, 100, 70, 0));
        ctx.fillStyle = grd;
        const spread = 0.05;
        ctx.beginPath();
        ctx.moveTo(cx, cy);
        ctx.lineTo(cx + Math.cos(a - spread) * len, cy + Math.sin(a - spread) * len);
        ctx.lineTo(cx + Math.cos(a + spread) * len, cy + Math.sin(a + spread) * len);
        ctx.closePath();
        ctx.fill();
      }
      ctx.globalCompositeOperation = "source-over";
    },
  };

  /* ============ STROBE ROOM (full screen color flashes) ============ */
  const StrobeRoom = {
    id: "strobeRoom", name: "Strobe Room", tag: "BEAT",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
      <path d="M13 2L4 14h7v8l9-12h-7z"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      // Background full color shifted by beat
      const onBeat = A.beat > 0.4;
      const hue = (P.hue + Math.floor(t * 4) * 40) % 360;
      ctx.fillStyle = onBeat ? hsla(hue, 100, 70) : hsla(hue, 60, 8);
      ctx.fillRect(0, 0, w, h);
      // Concentric pulse rings on beat
      if (onBeat) {
        const cx = w/2, cy = h/2;
        const N = 12;
        for (let i = 0; i < N; i++) {
          const r = (i / N) * Math.max(w, h) * 0.7;
          ctx.strokeStyle = `rgba(255,255,255,${1 - i / N})`;
          ctx.lineWidth = 4;
          ctx.beginPath(); ctx.arc(cx, cy, r, 0, TAU); ctx.stroke();
        }
      }
      // Bass-reactive geometric shapes
      const N = 8;
      ctx.shadowBlur = P.glow * 20;
      for (let i = 0; i < N; i++) {
        const a = (i / N) * TAU + t * P.motion;
        const r = w * 0.25 * (1 + A.bass * 0.5);
        const x = w/2 + Math.cos(a) * r;
        const y = h/2 + Math.sin(a) * r;
        const sz = 30 + A.energy * 80 * P.intensity;
        const ch = (P.hue + i * 45) % 360;
        ctx.fillStyle = hsla(ch, 100, 60, 0.8);
        ctx.shadowColor = hsla(ch, 100, 60);
        ctx.fillRect(x - sz/2, y - sz/2, sz, sz);
      }
      ctx.shadowBlur = 0;
    },
  };

  /* ============ LASER GRID ============ */
  const LaserGrid = {
    id: "laserGrid", name: "Laser Grid", tag: "BEAM",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M2 12h20M12 2v20M5 5l14 14M19 5L5 19M2 7h20M2 17h20M7 2v20M17 2v20"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const cx = w/2, cy = h/2;
      const beams = 32;
      const sweep = Math.sin(t * 0.7 * P.motion) * 0.5 + 0.5;
      ctx.globalCompositeOperation = "lighter";
      ctx.shadowBlur = P.glow * 30;
      for (let i = 0; i < beams; i++) {
        const baseAng = (i / beams) * TAU;
        const ang = baseAng + sweep * 0.4 + (i % 2 ? -1 : 1) * t * 0.3;
        const fi = Math.floor((i / beams) * (A.freq.length * 0.5));
        const v = A.freq[fi] / 255;
        const hue = (P.hue + i * 11 + t * 80) % 360;
        const len = Math.max(w, h) * (0.4 + v * 0.7 * P.intensity);
        ctx.strokeStyle = hsla(hue, 100, 60, 0.9);
        ctx.shadowColor = hsla(hue, 100, 60);
        ctx.lineWidth = 1 + v * 4 * P.intensity;
        ctx.beginPath();
        ctx.moveTo(cx, cy);
        ctx.lineTo(cx + Math.cos(ang) * len, cy + Math.sin(ang) * len);
        ctx.stroke();
      }
      ctx.globalCompositeOperation = "source-over";
      ctx.shadowBlur = 0;
      // Source dot
      const dotR = 8 + A.bass * 20 * P.intensity;
      ctx.fillStyle = "#fff";
      ctx.shadowColor = "#fff"; ctx.shadowBlur = 30;
      ctx.beginPath(); ctx.arc(cx, cy, dotR, 0, TAU); ctx.fill();
      ctx.shadowBlur = 0;
    },
  };

  /* ============ FEEDBACK LOOP (self-referencing canvas) ============ */
  const Feedback = {
    id: "feedbackLoop", name: "Feedback Loop", tag: "TRAIL",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <circle cx="12" cy="12" r="9"/>
      <circle cx="12" cy="12" r="6"/>
      <circle cx="12" cy="12" r="3"/>
      <circle cx="12" cy="12" r="1" fill="currentColor"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      // Feedback: redraw existing canvas with slight scale + rotate + fade
      if (!Feedback._buf) Feedback._buf = document.createElement("canvas");
      const buf = Feedback._buf;
      if (buf.width !== w || buf.height !== h) { buf.width = w; buf.height = h; }
      const bctx = buf.getContext("2d");
      // Copy old frame
      bctx.save();
      bctx.globalCompositeOperation = "destination-out";
      bctx.fillStyle = `rgba(0,0,0,${0.08 + (1 - P.bgAlpha) * 0.05})`;
      bctx.fillRect(0, 0, w, h);
      bctx.restore();
      bctx.save();
      bctx.translate(w/2, h/2);
      bctx.rotate(0.005 + A.high * 0.04 * P.motion);
      const sc = 1.02 + A.bass * 0.04 * P.intensity;
      bctx.scale(sc, sc);
      bctx.drawImage(ctx.canvas, -w/2, -h/2, w, h);
      bctx.restore();
      // Draw new shapes
      const cx = w/2, cy = h/2;
      const hue = (P.hue + t * 80) % 360;
      bctx.fillStyle = hsla(hue, 100, 60, 0.9);
      bctx.shadowColor = hsla(hue, 100, 60);
      bctx.shadowBlur = P.glow * 20;
      const sz = 20 + A.bass * 80 * P.intensity;
      bctx.beginPath();
      bctx.arc(cx + Math.cos(t * 2) * 100, cy + Math.sin(t * 1.7) * 80, sz, 0, TAU);
      bctx.fill();
      // Copy back
      ctx.clearRect(0, 0, w, h);
      ctx.drawImage(buf, 0, 0);
    },
  };

  /* ============ SPECTRUM RIBBONS ============ */
  const Ribbons = {
    id: "ribbons", name: "Ribbon Flow", tag: "FLOW",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M2 12 Q8 4 12 12 T22 12"/>
      <path d="M2 16 Q8 8 12 16 T22 16" opacity="0.7"/>
      <path d="M2 8 Q8 0 12 8 T22 8" opacity="0.4"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const N = 8;
      ctx.globalCompositeOperation = "lighter";
      for (let r = 0; r < N; r++) {
        const phase = (r / N) * TAU;
        const hue = (P.hue + r * 30 + t * 50) % 360;
        ctx.beginPath();
        const amp = h * 0.15 * P.intensity * (1 + A.bass * 0.4);
        for (let x = 0; x <= w; x += 6) {
          const u = x / w;
          const y =
            h/2 +
            Math.sin(u * TAU * 2 + t * P.motion + phase) * amp +
            Math.sin(u * TAU * 5 - t * P.motion * 1.6 + phase * 1.5) * amp * 0.4 +
            (A.wave[Math.floor(u * A.wave.length)] - 128) * 0.4 * P.intensity;
          if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        }
        ctx.strokeStyle = hsla(hue, 100, 60, 0.5);
        ctx.shadowBlur = P.glow * 14;
        ctx.shadowColor = hsla(hue, 100, 60);
        ctx.lineWidth = P.lineWidth + 1;
        ctx.stroke();
      }
      ctx.globalCompositeOperation = "source-over";
      ctx.shadowBlur = 0;
    },
  };

  /* ============ NEON RUSH (line streaks) ============ */
  const NeonRush = {
    id: "neonRush", name: "Neon Rush", tag: "STREAK",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M2 4l20 8M2 12l20 8M2 20l20-12"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const cx = w/2, cy = h/2;
      const N = Math.max(60, Math.floor(P.density * 220));
      const sp = 0.6 + A.bass * 4 * P.intensity;
      ctx.globalCompositeOperation = "lighter";
      ctx.shadowBlur = P.glow * 16;
      for (let i = 0; i < N; i++) {
        const seed = i * 12.9898;
        const ang = (Math.sin(seed) * 0.5 + 0.5) * TAU;
        const dist = ((t * sp + (Math.sin(seed * 2) * 0.5 + 0.5) * 10) % 10) / 10;
        const len = 40 + dist * 200;
        const x1 = cx + Math.cos(ang) * dist * Math.hypot(w, h) * 0.7;
        const y1 = cy + Math.sin(ang) * dist * Math.hypot(w, h) * 0.7;
        const x2 = x1 - Math.cos(ang) * len;
        const y2 = y1 - Math.sin(ang) * len;
        const hue = (P.hue + i * 7 + t * 100) % 360;
        ctx.strokeStyle = hsla(hue, 100, 60, dist);
        ctx.shadowColor = hsla(hue, 100, 60);
        ctx.lineWidth = (1 - dist) * P.lineWidth * 3 + 0.5;
        ctx.beginPath();
        ctx.moveTo(x1, y1); ctx.lineTo(x2, y2);
        ctx.stroke();
      }
      ctx.globalCompositeOperation = "source-over";
      ctx.shadowBlur = 0;
    },
  };

  /* ============ KALEIDO X (multi-layer mirror with feedback) ============ */
  const KaleidoX = {
    id: "kaleidoX", name: "Kaleido X", tag: "MIRROR · 12",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <polygon points="12,2 22,8 22,16 12,22 2,16 2,8"/>
      <path d="M12 2v20M2 8l20 8M2 16l20-8"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const cx = w/2, cy = h/2;
      const segs = P.kaleidoSegs || 12;
      const R = Math.min(w, h) * 0.55;
      ctx.save();
      ctx.translate(cx, cy);
      for (let s = 0; s < segs; s++) {
        ctx.save();
        ctx.rotate((s / segs) * TAU + t * 0.15 * P.motion);
        if (s % 2) ctx.scale(-1, 1);
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.arc(0, 0, R, -Math.PI / segs, Math.PI / segs);
        ctx.closePath(); ctx.clip();
        // Layer 1: rotating shapes
        const N = 18 + Math.floor(P.density * 24);
        ctx.shadowBlur = P.glow * 18;
        for (let i = 0; i < N; i++) {
          const fi = Math.floor((i / N) * A.freq.length * 0.5);
          const v = A.freq[fi] / 255;
          const ang = (i / N) * Math.PI / segs + t * 0.4 * P.motion;
          const d = (i / N) * R * (0.4 + v * 0.7 * P.intensity);
          const sz = 4 + v * 60 * P.intensity;
          const hue = (P.hue + s * 30 + i * 6 + t * 60) % 360;
          ctx.fillStyle = hsla(hue, 100, 60, 0.75);
          ctx.shadowColor = hsla(hue, 100, 60);
          ctx.beginPath();
          ctx.arc(Math.cos(ang) * d, Math.sin(ang) * d, sz * 0.5, 0, TAU);
          ctx.fill();
        }
        // Layer 2: radial lines
        const L = 20;
        ctx.strokeStyle = hsla((P.hue + s * 30 + 180) % 360, 100, 70, 0.5);
        ctx.lineWidth = 1;
        for (let i = 0; i < L; i++) {
          const a = (i / L) * Math.PI / segs;
          ctx.beginPath();
          ctx.moveTo(0, 0);
          ctx.lineTo(Math.cos(a) * R, Math.sin(a) * R);
          ctx.stroke();
        }
        ctx.restore();
      }
      ctx.restore();
    },
  };

  /* ============ MATRIX RAIN ============ */
  const MatrixRain = {
    id: "matrixRain", name: "Matrix Rain", tag: "CYBER",
    icon: <svg viewBox="0 0 24 24" fill="currentColor">
      <rect x="3" y="3" width="2" height="6"/><rect x="3" y="12" width="2" height="4"/>
      <rect x="8" y="6" width="2" height="8"/><rect x="13" y="3" width="2" height="6"/>
      <rect x="13" y="15" width="2" height="4"/><rect x="18" y="9" width="2" height="6"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      // Fade-out gives trails
      ctx.fillStyle = `rgba(0, 5, 0, ${0.15 + (1 - P.bgAlpha) * 0.05})`;
      ctx.fillRect(0, 0, w, h);
      const charW = 14;
      const cols = Math.floor(w / charW);
      if (!MatrixRain._drops || MatrixRain._drops.length !== cols) {
        MatrixRain._drops = Array.from({ length: cols }, () => Math.random() * h);
      }
      const drops = MatrixRain._drops;
      ctx.font = "bold 14px monospace";
      ctx.textAlign = "center";
      for (let i = 0; i < cols; i++) {
        const ch = String.fromCharCode(0x30A0 + Math.random() * 96 | 0);
        const speed = 5 + A.bass * 30 * P.intensity;
        drops[i] += speed * (0.5 + A.energy);
        if (drops[i] > h + 30 && Math.random() > 0.97) drops[i] = -Math.random() * 200;
        const y = drops[i];
        // Head bright
        ctx.fillStyle = hsla(P.hue, 100, 80, 1);
        ctx.shadowColor = hsla(P.hue, 100, 60);
        ctx.shadowBlur = 12 * P.glow;
        ctx.fillText(ch, i * charW + charW / 2, y);
        // Tail
        ctx.shadowBlur = 0;
        for (let j = 1; j < 12; j++) {
          const ty = y - j * 14;
          if (ty < 0) break;
          const alpha = (1 - j / 12) * 0.6;
          ctx.fillStyle = hsla(P.hue, 100, 50, alpha);
          ctx.fillText(String.fromCharCode(0x30A0 + Math.random() * 96 | 0),
            i * charW + charW / 2, ty);
        }
      }
    },
  };

  /* ============ DROP DETECTOR (explosive beat reaction) ============ */
  const DropMode = {
    id: "dropMode", name: "DROP Mode", tag: "EXPLODE",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M12 2v9l-3-3M12 11l3-3M5 14h14M3 18h18M5 22h14"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      // Reactive dark BG
      ctx.fillStyle = `rgba(8,8,12,${0.25 - A.bass * 0.18})`;
      ctx.fillRect(0, 0, w, h);
      const cx = w/2, cy = h/2;
      // Persistent ring particles
      if (!DropMode._rings) DropMode._rings = [];
      const rings = DropMode._rings;
      if (A.beat > 0.5 && rings.length < 12) {
        for (let i = 0; i < 4; i++) {
          rings.push({
            r: 30, a: 1,
            hue: (P.hue + Math.random() * 60) % 360,
            speed: 6 + Math.random() * 8,
          });
        }
      }
      ctx.globalCompositeOperation = "lighter";
      for (let i = rings.length - 1; i >= 0; i--) {
        const r = rings[i];
        r.r += r.speed * (0.5 + A.energy);
        r.a -= 0.012;
        if (r.a <= 0) { rings.splice(i, 1); continue; }
        ctx.strokeStyle = hsla(r.hue, 100, 65, r.a);
        ctx.shadowColor = hsla(r.hue, 100, 60);
        ctx.shadowBlur = P.glow * 26 * r.a;
        ctx.lineWidth = (1 - r.a) * 8 * P.intensity + 1;
        ctx.beginPath();
        ctx.arc(cx, cy, r.r, 0, TAU);
        ctx.stroke();
      }
      ctx.globalCompositeOperation = "source-over";
      ctx.shadowBlur = 0;
      // Center bass core
      const coreR = 20 + A.bass * 60 * P.intensity;
      const grd = ctx.createRadialGradient(cx, cy, 0, cx, cy, coreR);
      grd.addColorStop(0, hsla(P.hue, 100, 90, 1));
      grd.addColorStop(0.6, hsla(P.hue, 100, 60, 0.6));
      grd.addColorStop(1, hsla(P.hue, 100, 50, 0));
      ctx.fillStyle = grd;
      ctx.beginPath(); ctx.arc(cx, cy, coreR, 0, TAU); ctx.fill();
      // Audio frequency burst lines
      const N = 64;
      ctx.shadowBlur = P.glow * 8;
      for (let i = 0; i < N; i++) {
        const ang = (i / N) * TAU + t * 0.2;
        const fi = Math.floor((i / N) * A.freq.length * 0.55);
        const v = A.freq[fi] / 255;
        const len = coreR + v * Math.min(w, h) * 0.35 * P.intensity;
        const hue = (P.hue + i * 5 + t * 80) % 360;
        ctx.strokeStyle = hsla(hue, 100, 60, 0.85);
        ctx.shadowColor = hsla(hue, 100, 60);
        ctx.lineWidth = 1.5 + v * 3;
        ctx.beginPath();
        ctx.moveTo(cx + Math.cos(ang) * coreR, cy + Math.sin(ang) * coreR);
        ctx.lineTo(cx + Math.cos(ang) * len, cy + Math.sin(ang) * len);
        ctx.stroke();
      }
      ctx.shadowBlur = 0;
    },
  };

  /* ============ RGB GLITCH ============ */
  const RgbGlitch = {
    id: "rgbGlitch", name: "RGB Glitch", tag: "BROKEN",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M4 4h16v4M4 10h16v4M4 16h16v4"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      // Rapid color blocks
      const slices = 20;
      for (let i = 0; i < slices; i++) {
        const y = (i / slices) * h;
        const sh = h / slices;
        const off = Math.sin(t * 5 + i * 0.7) * 30 * P.intensity * (0.3 + A.energy);
        const hue = (P.hue + i * 18 + Math.floor(t * 6) * 40) % 360;
        // RGB stripes
        ctx.fillStyle = hsla(hue, 100, 50, 0.7);
        ctx.fillRect(off, y, w, sh - 1);
        if (A.beat > 0.4 && Math.random() > 0.7) {
          ctx.fillStyle = hsla((hue + 180) % 360, 100, 70, 0.5);
          ctx.fillRect(-off * 1.5, y, w, sh * 0.5);
        }
      }
      // Vertical scan line
      const sx = ((t * 200) % w);
      ctx.fillStyle = "rgba(255,255,255,0.3)";
      ctx.fillRect(sx, 0, 2, h);
      // Text glitch
      if (A.beat > 0.5) {
        const text = "DATA_CORRUPTED";
        const fontSize = h * 0.15;
        ctx.font = `900 ${fontSize}px monospace`;
        ctx.textAlign = "center"; ctx.textBaseline = "middle";
        ctx.fillStyle = "rgba(255,40,40,0.85)";
        ctx.fillText(text, w/2 - 4, h/2);
        ctx.fillStyle = "rgba(40,200,255,0.85)";
        ctx.fillText(text, w/2 + 4, h/2);
        ctx.fillStyle = "#fff";
        ctx.fillText(text, w/2, h/2);
      }
    },
  };

  window.VISUALIZERS.push(FractalZoom, MirrorBall, StrobeRoom, LaserGrid,
                          Feedback, Ribbons, NeonRush, KaleidoX,
                          MatrixRain, DropMode, RgbGlitch);
})();
