audio_player-1.0.x-dev/js/equalizer-effects.js

js/equalizer-effects.js
// equalizer-effects.js

/**
 * @file
 * Contains all equalizer drawing functions.
 * These functions require a pre-initialized AudioContext, AnalyserNode,
 * and a canvas context (canvasCtx) and data arrays (timeDomainDataArray, frequencyDataArray).
 */

(function (exports) {
  // Common variables that each drawing function needs.
  // These will be passed in from the main script.
  let equalizerCanvas = undefined;
  let canvasCtx = undefined;
  let analyser = undefined;
  let bufferLength = 0;
  let timeDomainDataArray = undefined;
  let frequencyDataArray = undefined;
  let animationFrameId = undefined;
  let equalizerColorData = undefined;

  // Variables for specific effects that need persistent state
  let rotationAngle = 0; // For Rotating Ring and Vortex Spectrum
  let particles = [];    // For Raindrop Effect
  let fireflies = [];    // For Fireflies / Swarm
  let tunnelOffset = 0;  // For Tunnel Effect
  let vortexAngle = 0;   // For Vortex Spectrum
  let lightningParticles = []; // For Rain and Lightning

  /**
   * Initializes the common variables required by all drawing functions.
   * This should be called once when the equalizer is set up.
   */
  function initEqualizerEffects(canvas, ctx, an, bufLen, timeData, freqData, animId, colorData) {
    equalizerCanvas = canvas;
    canvasCtx = ctx;
    analyser = an;
    bufferLength = bufLen;
    timeDomainDataArray = timeData;
    frequencyDataArray = freqData;
    animationFrameId = animId; // We'll update this by returning the new ID
    equalizerColorData = colorData; // We'll update this by returning the new ID
  }

  /**
   * Helper to request animation frame and recursively call the draw function.
   * @param {Function} drawFn The actual drawing function (e.g., drawWaveform)
   */
  function animate(drawFn) {
    if (!equalizerCanvas || !canvasCtx || !analyser) {
      return undefined; // Ensure essentials exist
    }
    // If there's an ongoing animation, cancel it before starting a new one.
    if (animationFrameId) {
      cancelAnimationFrame(animationFrameId);
    }

    animationFrameId = requestAnimationFrame(() => animate(drawFn)); // Update global ID
    drawFn(); // Execute the actual drawing logic for the chosen effect
    return animationFrameId; // Return the new ID to the main script
  }

  // --- Drawing Functions (same as before, but now within this module) ---

  // 1. Waveform (Line)
  function drawWaveform() {
    analyser.getByteTimeDomainData(timeDomainDataArray);

    // var colorValue = getComputedStyle(document.documentElement).getPropertyValue('--playlist-accent-color').trim();
    var colorValue = equalizerColorData.backgroundColor;

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);
    canvasCtx.lineWidth = 2;
    canvasCtx.strokeStyle = colorValue; // Blue
    canvasCtx.beginPath();

    const sliceWidth = equalizerCanvas.width * 1.0 / bufferLength;
    let x = 0;

    for (let i = 0; i < bufferLength; i++) {
      const v = timeDomainDataArray[i] / 128.0; // Normalize 0-255 to 0-2
      const y = v * equalizerCanvas.height / 2; // Scale to canvas height

      if (i === 0) {
        canvasCtx.moveTo(x, y);
      } else {
        canvasCtx.lineTo(x, y);
      }
      x += sliceWidth;
    }
    canvasCtx.lineTo(equalizerCanvas.width, equalizerCanvas.height / 2);
    canvasCtx.stroke();
  }

  // 2. Frequency Bars
  function drawFrequencyBars() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const barWidth = (equalizerCanvas.width / bufferLength) * 2.5;
    let x = 0;

    for (let i = 0; i < bufferLength; i++) {
      let barHeight = frequencyDataArray[i] / 255 * equalizerCanvas.height;

      const gradient = canvasCtx.createLinearGradient(0, equalizerCanvas.height, 0, 0);
      gradient.addColorStop(0, 'rgba(26, 115, 232, 0.2)');
      gradient.addColorStop(0.5, 'rgba(26, 115, 232, 0.7)');
      gradient.addColorStop(1, '#1a73e8');
      canvasCtx.fillStyle = gradient;

      canvasCtx.fillRect(x, equalizerCanvas.height - barHeight, barWidth, barHeight);
      x += barWidth + 1;
    }
  }

  // 3. Circular Waveform
  function drawCircularWaveform() {
    analyser.getByteTimeDomainData(timeDomainDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const centerX = equalizerCanvas.width / 2;
    const centerY = equalizerCanvas.height / 2;
    const radius = Math.min(centerX, centerY) * 0.4; // Base radius

    canvasCtx.lineWidth = 2;
    canvasCtx.strokeStyle = '#1a73e8';
    canvasCtx.beginPath();

    for (let i = 0; i < bufferLength; i++) {
      const v = timeDomainDataArray[i] / 128.0; // Normalized value 0-2
      const r = radius + (v - 1) * radius * 0.8; // Modulate radius based on amplitude (-1 to 1 range)

      const angle = (i / bufferLength) * Math.PI * 2; // Full circle
      const x = centerX + r * Math.cos(angle);
      const y = centerY + r * Math.sin(angle);

      if (i === 0) {
        canvasCtx.moveTo(x, y);
      } else {
        canvasCtx.lineTo(x, y);
      }
    }
    canvasCtx.closePath();
    canvasCtx.stroke();
  }

  // 4. Dot Waveform
  function drawDotWaveform() {
    analyser.getByteTimeDomainData(timeDomainDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const sliceWidth = equalizerCanvas.width * 1.0 / bufferLength;
    let x = 0;

    for (let i = 0; i < bufferLength; i++) {
      const v = timeDomainDataArray[i] / 128.0;
      const y = v * equalizerCanvas.height / 2;

      canvasCtx.beginPath();
      canvasCtx.arc(x, y, 2, 0, Math.PI * 2, 0); // Draw a small circle (dot)
      canvasCtx.fillStyle = '#1a73e8';
      canvasCtx.fill();
      x += sliceWidth;
    }
  }

  // 5. Simple Blob/Pulse Effect (based on overall frequency energy)
  function drawBlobEffect() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const centerX = equalizerCanvas.width / 2;
    const centerY = equalizerCanvas.height / 2;
    const baseRadius = Math.min(centerX, centerY) * 0.2; // Base size of the blob

    // Calculate average frequency energy
    let sum = 0;
    for (let i = 0; i < bufferLength; i++) {
      sum += frequencyDataArray[i];
    }
    const averageFrequency = sum / bufferLength;
    const scale = (averageFrequency / 255) * 0.8 + 0.2; // Scale from 0.2 to 1 for subtle movement

    const currentRadius = baseRadius * scale * 2; // Make it more pronounced

    canvasCtx.beginPath();
    canvasCtx.arc(centerX, centerY, currentRadius, 0, Math.PI * 2, 0);
    canvasCtx.fillStyle = 'rgba(26, 115, 232, 0.7)'; // Semi-transparent blue
    canvasCtx.fill();
    canvasCtx.strokeStyle = '#1a73e8';
    canvasCtx.lineWidth = 3;
    canvasCtx.stroke();
  }

  // 6. Audio Spectrum Visualizer (Enhanced Frequency Bars)
  function drawAudioSpectrumVisualizer() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const barsToDraw = 128;
    const barWidth = (equalizerCanvas.width / barsToDraw) * 0.9;
    const sliceSize = Math.floor(bufferLength / barsToDraw);

    for (let i = 0; i < barsToDraw; i++) {
      let sum = 0;
      for (let j = 0; j < sliceSize; j++) {
        sum += frequencyDataArray[i * sliceSize + j];
      }
      const averageFrequency = sum / sliceSize;

      let barHeight = (averageFrequency / 255) * equalizerCanvas.height;

      const x = i * (barWidth + (equalizerCanvas.width * 0.01 / barsToDraw));
      const y = equalizerCanvas.height - barHeight;

      const gradient = canvasCtx.createLinearGradient(x, equalizerCanvas.height, x, y);
      gradient.addColorStop(0, 'rgba(26, 115, 232, 0.1)');
      gradient.addColorStop(0.5, 'rgba(26, 115, 232, 0.7)');
      gradient.addColorStop(1, '#1a73e8');
      canvasCtx.fillStyle = gradient;

      canvasCtx.fillRect(x, y, barWidth, barHeight);

      canvasCtx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
      canvasCtx.lineWidth = 0.5;
      canvasCtx.strokeRect(x, y, barWidth, barHeight);
    }
  }

  // 7. Soundwave (Filled Waveform)
  function drawSoundwave() {
    analyser.getByteTimeDomainData(timeDomainDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    canvasCtx.lineWidth = 2;
    canvasCtx.strokeStyle = '#1a73e8';
    canvasCtx.fillStyle = 'rgba(26, 115, 232, 0.3)';

    canvasCtx.beginPath();
    const sliceWidth = equalizerCanvas.width * 1.0 / bufferLength;
    let x = 0;

    canvasCtx.moveTo(0, equalizerCanvas.height);

    for (let i = 0; i < bufferLength; i++) {
      const v = timeDomainDataArray[i] / 128.0;
      const y = v * equalizerCanvas.height / 2;
      canvasCtx.lineTo(x, y);
      x += sliceWidth;
    }

    canvasCtx.lineTo(equalizerCanvas.width, equalizerCanvas.height);
    canvasCtx.closePath();

    canvasCtx.fill();
    canvasCtx.stroke();
  }

  // 8. Visual Equalizer (Circular Frequency Bars)
  function drawVisualEqualizer() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const centerX = equalizerCanvas.width / 2;
    const centerY = equalizerCanvas.height / 2;
    const maxRadius = Math.min(centerX, centerY) * 0.8;
    const minRadius = maxRadius * 0.2;

    const barCount = 60;
    const angleStep = (Math.PI * 2) / barCount;
    const sliceSize = Math.floor(bufferLength / barCount);

    for (let i = 0; i < barCount; i++) {
      let sum = 0;
      for (let j = 0; j < sliceSize; j++) {
        sum += frequencyDataArray[i * sliceSize + j];
      }
      const averageFrequency = sum / sliceSize;

      const barHeight = (averageFrequency / 255) * (maxRadius - minRadius);
      const currentRadius = minRadius + barHeight;

      const startAngle = i * angleStep;
      const endAngle = (i + 1) * angleStep - 0.02;

      const x1 = centerX + minRadius * Math.cos(startAngle);
      const y1 = centerY + minRadius * Math.sin(startAngle);

      const x2 = centerX + currentRadius * Math.cos(startAngle);
      const y2 = centerY + currentRadius * Math.sin(startAngle);

      const x3 = centerX + currentRadius * Math.cos(endAngle);
      const y3 = centerY + currentRadius * Math.sin(endAngle);

      const x4 = centerX + minRadius * Math.cos(endAngle);
      const y4 = centerY + minRadius * Math.sin(endAngle);

      const gradient = canvasCtx.createLinearGradient(x1, y1, x2, y2);
      gradient.addColorStop(0, 'rgba(26, 115, 232, 0.3)');
      gradient.addColorStop(1, '#1a73e8');
      canvasCtx.fillStyle = gradient;

      canvasCtx.beginPath();
      canvasCtx.moveTo(x1, y1);
      canvasCtx.lineTo(x2, y2);
      canvasCtx.arc(centerX, centerY, currentRadius, startAngle, endAngle, 0);
      canvasCtx.lineTo(x4, y4);
      canvasCtx.arc(centerX, centerY, minRadius, endAngle, startAngle, 1);
      canvasCtx.closePath();
      canvasCtx.fill();

      canvasCtx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
      canvasCtx.lineWidth = 0.5;
      canvasCtx.stroke();
    }
  }

  // 9. WaveSurfer-like (Amplitude Peaks - Vertical Lines)
  function drawWavesurferLike() {
    analyser.getByteTimeDomainData(timeDomainDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const centerY = equalizerCanvas.height / 2;
    const barWidth = 2;
    const gap = 1;
    let x = 0;

    const numberOfBars = equalizerCanvas.width / (barWidth + gap);
    const step = Math.ceil(bufferLength / numberOfBars);

    for (let i = 0; i < bufferLength; i += step) {
      const v = (timeDomainDataArray[i] / 128.0) - 1;
      const height = Math.abs(v) * equalizerCanvas.height * 0.8;

      const topY = centerY - (height / 2);

      canvasCtx.fillStyle = '#1a73e8';
      canvasCtx.fillRect(x, topY, barWidth, height);
      x += barWidth + gap;
    }
  }

  // 10. Audio Sound Graphic (Reactive Circle)
  function drawAudioSoundGraphic() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const centerX = equalizerCanvas.width / 2;
    const centerY = equalizerCanvas.height / 2;
    const baseRadius = Math.min(centerX, centerY) * 0.15;

    let sum = 0;
    for (let i = 0; i < bufferLength; i++) {
      sum += frequencyDataArray[i];
    }
    const averageFrequency = sum / bufferLength;

    const scale = (averageFrequency / 255);
    const currentRadius = baseRadius + (baseRadius * 1.5 * scale);

    const r = Math.floor(26 + (255 - 26) * scale);
    const g = Math.floor(115 + (255 - 115) * scale);
    const b = Math.floor(232 + (255 - 232) * scale);
    canvasCtx.fillStyle = `rgba(${r}, ${g}, ${b}, 0.8)`;
    canvasCtx.strokeStyle = `rgb(${r}, ${g}, ${b})`;
    canvasCtx.lineWidth = 3;

    canvasCtx.beginPath();
    canvasCtx.arc(centerX, centerY, currentRadius, 0, Math.PI * 2, 0);
    canvasCtx.fill();
    canvasCtx.stroke();

    canvasCtx.shadowBlur = 10 + (20 * scale);
    canvasCtx.shadowColor = `rgba(${r}, ${g}, ${b}, 0.5)`;
    canvasCtx.fill();
    canvasCtx.shadowBlur = 0;
  }

  // 11. Vertical Symmetrical Bars
  function drawVerticalSymmetricalBars() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const barWidth = equalizerCanvas.width / bufferLength * 1.5;
    let x = 0;
    const centerY = equalizerCanvas.height / 2;

    for (let i = 0; i < bufferLength; i++) {
      const barHeight = frequencyDataArray[i] / 255 * (equalizerCanvas.height / 2);

      const gradient = canvasCtx.createLinearGradient(x, centerY - barHeight, x, centerY + barHeight);
      gradient.addColorStop(0, 'rgba(26, 115, 232, 0.1)');
      gradient.addColorStop(0.5, '#1a73e8');
      gradient.addColorStop(1, 'rgba(26, 115, 232, 0.1)');
      canvasCtx.fillStyle = gradient;

      canvasCtx.fillRect(x, centerY - barHeight, barWidth, barHeight * 2);
      x += barWidth + 1;
    }
  }

  // 12. Particle Cloud
  function drawParticleCloud() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const centerX = equalizerCanvas.width / 2;
    const centerY = equalizerCanvas.height / 2;
    const maxParticleSize = 5;
    const minParticleSize = 1;

    for (let i = 0; i < bufferLength; i += 5) {
      const data = frequencyDataArray[i];
      const normalizedData = data / 255;

      const angle = (i / bufferLength) * Math.PI * 2;
      const radius = Math.min(centerX, centerY) * normalizedData;

      const x = centerX + radius * Math.cos(angle);
      const y = centerY + radius * Math.sin(angle);

      const size = minParticleSize + (maxParticleSize - minParticleSize) * normalizedData;

      canvasCtx.beginPath();
      canvasCtx.arc(x, y, size, 0, Math.PI * 2, 0);
      canvasCtx.fillStyle = `rgba(26, 115, 232, ${0.2 + normalizedData * 0.8})`;
      canvasCtx.fill();
    }
  }

  // 13. Rotating Ring
  function drawRotatingRing() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const centerX = equalizerCanvas.width / 2;
    const centerY = equalizerCanvas.height / 2;
    const baseRadius = Math.min(centerX, centerY) * 0.3;
    const numSegments = 120;
    const segmentWidth = (Math.PI * 2) / numSegments;

    rotationAngle += 0.005;

    canvasCtx.save();
    canvasCtx.translate(centerX, centerY);
    canvasCtx.rotate(rotationAngle);

    for (let i = 0; i < numSegments; i++) {
      const dataIndex = Math.floor(i * (bufferLength / numSegments));
      const barHeight = (frequencyDataArray[dataIndex] / 255) * baseRadius * 1.5;

      const angle = i * segmentWidth;
      const x1 = baseRadius * Math.cos(angle);
      const y1 = baseRadius * Math.sin(angle);

      const x2 = (baseRadius + barHeight) * Math.cos(angle);
      const y2 = (baseRadius + barHeight) * Math.sin(angle);

      canvasCtx.beginPath();
      canvasCtx.moveTo(x1, y1);
      canvasCtx.lineTo(x2, y2);
      canvasCtx.strokeStyle = "hsl(" + (i * (360 / numSegments)) + ", 80%, 60 %)";
      canvasCtx.lineWidth = 2;
      canvasCtx.stroke();
    }
    canvasCtx.restore();
  }

  // 14. Abstract Lines
  function drawAbstractLines() {
    analyser.getByteTimeDomainData(timeDomainDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    canvasCtx.lineWidth = 1.5;
    canvasCtx.strokeStyle = '#66CCFF';

    const sliceWidth = equalizerCanvas.width * 1.0 / bufferLength;
    let x = 0;

    for (let i = 0; i < bufferLength; i++) {
      const v = timeDomainDataArray[i] / 128.0;
      const y = v * equalizerCanvas.height / 2;

      canvasCtx.beginPath();
      canvasCtx.moveTo(x, equalizerCanvas.height / 2);
      canvasCtx.lineTo(x, y);
      canvasCtx.stroke();

      x += sliceWidth;
    }
  }

  // 15. Sphere Pulse
  function drawSpherePulse() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const centerX = equalizerCanvas.width / 2;
    const centerY = equalizerCanvas.height / 2;
    const baseRadius = Math.min(centerX, centerY) * 0.1;

    let sum = 0;
    for (let i = 0; i < bufferLength; i++) {
      sum += frequencyDataArray[i];
    }
    const averageFrequency = sum / bufferLength;
    const scale = (averageFrequency / 255);

    const currentRadius = baseRadius + (baseRadius * 2 * scale);
    const alpha = 0.5 + (scale * 0.5);

    const r = Math.floor(26 + (255 - 26) * scale);
    const g = Math.floor(115 + (180 - 115) * scale);
    const b = Math.floor(232 + (255 - 232) * scale);

    canvasCtx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
    canvasCtx.strokeStyle = `rgb(${r}, ${g}, ${b})`;
    canvasCtx.lineWidth = 5 + (scale * 5);

    canvasCtx.beginPath();
    canvasCtx.arc(centerX, centerY, currentRadius, 0, Math.PI * 2, 0);
    canvasCtx.fill();
    canvasCtx.stroke();

    canvasCtx.shadowBlur = 15 + (scale * 20);
    canvasCtx.shadowColor = `rgba(${r}, ${g}, ${b}, ${alpha * 0.7})`;
    canvasCtx.fill();
    canvasCtx.shadowBlur = 0;
  }

  // 16. Line Grid
  function drawLineGrid() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const gridSize = 20;
    const numX = Math.ceil(equalizerCanvas.width / gridSize);
    const numY = Math.ceil(equalizerCanvas.height / gridSize);

    canvasCtx.strokeStyle = 'rgba(255, 255, 255, 0.1)';

    for (let i = 0; i < numX; i++) {
      for (let j = 0; j < numY; j++) {
        const x = i * gridSize;
        const y = j * gridSize;

        const dataIndex = Math.floor((i / numX) * bufferLength);
        const intensity = frequencyDataArray[dataIndex] / 255;

        const offsetX = (intensity - 0.5) * gridSize * 0.5;
        const offsetY = (intensity - 0.5) * gridSize * 0.5;

        canvasCtx.beginPath();
        canvasCtx.moveTo(x + offsetX, y);
        canvasCtx.lineTo(x + gridSize + offsetX, y);
        canvasCtx.lineTo(x + gridSize, y + gridSize + offsetY);
        canvasCtx.lineTo(x, y + gridSize + offsetY);
        canvasCtx.closePath();
        canvasCtx.stroke();
      }
    }
  }

  // 17. Growing Circles
  function drawGrowingCircles() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const centerX = equalizerCanvas.width / 2;
    const centerY = equalizerCanvas.height / 2;
    const maxRadius = Math.min(centerX, centerY) * 0.9;
    const numCircles = 5;

    for (let i = 0; i < numCircles; i++) {
      const dataIndex = Math.floor((i / numCircles) * bufferLength);
      const intensity = frequencyDataArray[dataIndex] / 255;

      const radius = maxRadius * (0.2 + intensity * 0.8 * (1 - (i / numCircles)));
      const alpha = 0.1 + intensity * 0.4;

      canvasCtx.beginPath();
      canvasCtx.arc(centerX, centerY, radius, 0, Math.PI * 2, 0);
      canvasCtx.strokeStyle = `rgba(26, 115, 232, ${alpha})`;
      canvasCtx.lineWidth = 1 + (intensity * 3);
      canvasCtx.stroke();
    }
  }

  // 18. Raindrop Effect
  function drawRaindropEffect() {
    analyser.getByteFrequencyData(frequencyDataArray);

    // Initialize particles if they are empty (happens on effect switch)
    if (particles.length === 0) {
      const maxParticles = 100;
      for (let i = 0; i < maxParticles; i++) {
        particles.push({
          x: Math.random() * equalizerCanvas.width,
          y: Math.random() * equalizerCanvas.height,
          size: Math.random() * 2 + 1,
          speed: Math.random() * 1 + 0.5,
          alpha: Math.random()
        });
      }
    }

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    let sum = 0;
    for (let i = 0; i < bufferLength; i++) {
      sum += frequencyDataArray[i];
    }
    const averageFrequency = sum / bufferLength;
    const pulseScale = (averageFrequency / 255);

    for (let i = 0; i < particles.length; i++) {
      const p = particles[i];

      p.y += p.speed * (1 + pulseScale * 2);
      p.alpha -= 0.005;

      if (p.y > equalizerCanvas.height || p.alpha <= 0) {
        p.x = Math.random() * equalizerCanvas.width;
        p.y = -p.size;
        p.size = Math.random() * 2 + 1;
        p.speed = Math.random() * 1 + 0.5;
        p.alpha = 1;
      }

      canvasCtx.beginPath();
      canvasCtx.arc(p.x, p.y, p.size * (1 + pulseScale), 0, Math.PI * 2, 0);
      canvasCtx.fillStyle = `rgba(26, 115, 232, ${p.alpha})`;
      canvasCtx.fill();
    }
  }

  // 19. Tunnel Effect
  function drawTunnelEffect() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const centerX = equalizerCanvas.width / 2;
    const centerY = equalizerCanvas.height / 2;
    const maxDimension = Math.max(equalizerCanvas.width, equalizerCanvas.height);
    const numRings = 20;

    tunnelOffset = (tunnelOffset + 0.5) % maxDimension;

    for (let i = 0; i < numRings; i++) {
      const dataIndex = Math.floor((i / numRings) * bufferLength);
      const intensity = frequencyDataArray[dataIndex] / 255;

      const radius = (maxDimension * (i / numRings)) + tunnelOffset;
      const actualRadius = radius % maxDimension;

      const pulseStrength = (intensity * 0.5) + 0.5;
      const finalRadius = actualRadius * pulseStrength;

      const alpha = 1 - (actualRadius / maxDimension);

      canvasCtx.beginPath();
      canvasCtx.arc(centerX, centerY, finalRadius, 0, Math.PI * 2, 0);
      canvasCtx.strokeStyle = `rgba(26, 115, 232, ${alpha})`;
      canvasCtx.lineWidth = 1 + (intensity * 2);
      canvasCtx.stroke();
    }
  }

  // 20. Fireflies / Swarm
  function drawFirefliesSwarm() {
    analyser.getByteFrequencyData(frequencyDataArray);

    // Initialize fireflies if they are empty
    if (fireflies.length === 0) {
      const numFireflies = 80;
      for (let i = 0; i < numFireflies; i++) {
        fireflies.push({
          x: Math.random() * equalizerCanvas.width,
          y: Math.random() * equalizerCanvas.height,
          vx: (Math.random() - 0.5) * 2,
          vy: (Math.random() - 0.5) * 2,
          size: Math.random() * 1.5 + 0.5,
          life: Math.random() * 100,
          maxLife: 100
        });
      }
    }

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    let sum = 0;
    for (let i = 0; i < bufferLength; i++) {
      sum += frequencyDataArray[i];
    }
    const averageEnergy = sum / bufferLength;
    const influence = averageEnergy / 255;

    for (let i = 0; i < fireflies.length; i++) {
      const f = fireflies[i];

      f.x += f.vx * (1 + influence);
      f.y += f.vy * (1 + influence);

      f.vx += (Math.random() - 0.5) * 0.5 * influence;
      f.vy += (Math.random() - 0.5) * 0.5 * influence;

      if (f.x < 0 || f.x > equalizerCanvas.width) {
        f.vx *= -1;
      }
      if (f.y < 0 || f.y > equalizerCanvas.height) {
        f.vy *= -1;
      }

      f.life--;
      if (f.life <= 0) {
        f.x = Math.random() * equalizerCanvas.width;
        f.y = Math.random() * equalizerCanvas.height;
        f.vx = (Math.random() - 0.5) * 2;
        f.vy = (Math.random() - 0.5) * 2;
        f.size = Math.random() * 1.5 + 0.5;
        f.life = f.maxLife;
      }

      const alpha = (f.life / f.maxLife) * (0.5 + influence * 0.5);
      canvasCtx.beginPath();
      canvasCtx.arc(f.x, f.y, f.size * (1 + influence), 0, Math.PI * 2, 0);
      canvasCtx.fillStyle = `rgba(255, 255, 100, ${alpha})`;
      canvasCtx.fill();

      canvasCtx.shadowBlur = f.size * 5 * influence;
      canvasCtx.shadowColor = `rgba(255, 255, 100, ${alpha})`;
      canvasCtx.fill();
      canvasCtx.shadowBlur = 0;
    }
  }

  // 21. Pixel Grid / Heatmap
  function drawPixelGridHeatmap() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const pixelSize = 10;
    const numXPixels = Math.ceil(equalizerCanvas.width / pixelSize);
    const numYPixels = Math.ceil(equalizerCanvas.height / pixelSize);

    for (let i = 0; i < numXPixels; i++) {
      for (let j = 0; j < numYPixels; j++) {
        const x = i * pixelSize;
        const y = j * pixelSize;

        const dataIndex = Math.floor((i / numXPixels) * bufferLength);
        const intensity = frequencyDataArray[dataIndex] / 255;

        const r = Math.floor(26 + (255 - 26) * intensity);
        const g = Math.floor(115 + (0 - 115) * intensity);
        const b = Math.floor(232 + (0 - 232) * intensity);

        canvasCtx.fillStyle = `rgb(${r}, ${g}, ${b})`;
        canvasCtx.fillRect(x, y, pixelSize, pixelSize);
      }
    }
  }

  // 22. Lissajous Curve (simplified with audio influence)
  function drawLissajousCurve() {
    analyser.getByteTimeDomainData(timeDomainDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const centerX = equalizerCanvas.width / 2;
    const centerY = equalizerCanvas.height / 2;
    const amplitudeX = centerX * 0.8;
    const amplitudeY = centerY * 0.8;

    canvasCtx.beginPath();
    canvasCtx.strokeStyle = '#FF66FF';
    canvasCtx.lineWidth = 1.5;

    const freqA = 2;
    const freqB = 3;
    const phaseShift = Math.PI / 2;

    for (let i = 0; i < bufferLength; i++) {
      const v1 = timeDomainDataArray[i] / 128.0 - 1;
      const v2 = timeDomainDataArray[(i + Math.floor(bufferLength / 4)) % bufferLength] / 128.0 - 1;

      const x = centerX + amplitudeX * Math.cos(i * 0.05 + v1 * freqA);
      const y = centerY + amplitudeY * Math.sin(i * 0.05 + v2 * freqB + phaseShift);

      if (i === 0) {
        canvasCtx.moveTo(x, y);
      } else {
        canvasCtx.lineTo(x, y);
      }
    }
    canvasCtx.stroke();
  }

  // 23. Ripple Effect (Concentric Expanding Circles)
  function drawRippleEffect() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const centerX = equalizerCanvas.width / 2;
    const centerY = equalizerCanvas.height / 2;
    const maxRadius = Math.min(centerX, centerY) * 0.9;

    let sum = 0;
    for (let i = 0; i < bufferLength; i++) {
      sum += frequencyDataArray[i];
    }
    const averageFrequency = sum / bufferLength;
    const intensity = averageFrequency / 255; // Normalize to 0-1

    const numRipples = 5;
    for (let i = 0; i < numRipples; i++) {
      // Calculate radius based on intensity and a phase offset
      const phaseOffset = (performance.now() * 0.001 * 0.1) % 1; // Slow, continuous expansion
      let currentRadius = maxRadius * ((i / numRipples) + phaseOffset) % maxRadius;

      // Apply intensity for a 'pulse' effect
      currentRadius += currentRadius * intensity * 0.5;

      const alpha = 1 - (currentRadius / maxRadius) * 0.8; // Fade out as it expands

      canvasCtx.beginPath();
      canvasCtx.arc(centerX, centerY, currentRadius, 0, Math.PI * 2, 0);
      canvasCtx.strokeStyle = `rgba(26, 115, 232, ${alpha})`;
      canvasCtx.lineWidth = 2 + (intensity * 3); // Thicker line with more intensity
      canvasCtx.stroke();
    }
  }

  // 24. Glow Trails (Smooth, Fading Lines)
  function drawGlowTrails() {
    analyser.getByteTimeDomainData(timeDomainDataArray);

    // Partial clear for trails
    canvasCtx.fillStyle = 'rgba(0, 0, 0, 0.08)'; // Adjust transparency for longer/shorter trails
    canvasCtx.fillRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    canvasCtx.lineWidth = 2;
    canvasCtx.strokeStyle = '#FF6600'; // Orange glow color
    canvasCtx.shadowBlur = 10;
    canvasCtx.shadowColor = '#FF6600';

    const sliceWidth = equalizerCanvas.width * 1.0 / bufferLength;
    let x = 0;

    canvasCtx.beginPath();
    canvasCtx.moveTo(0, equalizerCanvas.height / 2);

    for (let i = 0; i < bufferLength; i++) {
      const v = timeDomainDataArray[i] / 128.0;
      const y = v * equalizerCanvas.height / 2;
      canvasCtx.lineTo(x, y);
      x += sliceWidth;
    }
    canvasCtx.stroke();

    canvasCtx.shadowBlur = 0; // Reset shadow for other drawings
  }

  // 25. Rain and Lightning (Particles + Impulse Lines)
  function initializeLightningParticles() {
    lightningParticles = [];
    const maxLightningParticles = 150; // More particles for a rain effect
    for (let i = 0; i < maxLightningParticles; i++) {
      lightningParticles.push({
        x: Math.random() * equalizerCanvas.width,
        y: Math.random() * equalizerCanvas.height,
        size: Math.random() * 1.5 + 0.5,
        speed: Math.random() * 2 + 1,
        alpha: Math.random() * 0.5 + 0.3 // Slightly less opaque
      });
    }
  }

  function drawRainAndLightning() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    let sum = 0;
    for (let i = 0; i < bufferLength; i++) {
      sum += frequencyDataArray[i];
    }
    const averageFrequency = sum / bufferLength;
    const pulseThreshold = 150; // Adjust for sensitivity of lightning
    const isLightningPulse = averageFrequency > pulseThreshold;
    const intensity = averageFrequency / 255;

    // Draw rain particles
    for (let i = 0; i < lightningParticles.length; i++) {
      const p = lightningParticles[i];

      p.y += p.speed * (1 + intensity * 0.5); // Rain speed slightly influenced by audio
      p.alpha -= 0.003; // Fade out

      if (p.y > equalizerCanvas.height + p.size || p.alpha <= 0) {
        p.x = Math.random() * equalizerCanvas.width;
        p.y = -p.size;
        p.size = Math.random() * 1.5 + 0.5;
        p.speed = Math.random() * 2 + 1;
        p.alpha = Math.random() * 0.5 + 0.3;
      }

      canvasCtx.beginPath();
      canvasCtx.arc(p.x, p.y, p.size, 0, Math.PI * 2, 0);
      canvasCtx.fillStyle = `rgba(150, 200, 255, ${p.alpha})`; // Light blue for rain
      canvasCtx.fill();
    }

    // Draw lightning if pulse is detected
    if (isLightningPulse) {
      const lightningCount = Math.floor(intensity * 5); // More lightning for stronger pulses
      for (let i = 0; i < lightningCount; i++) {
        const lightningX = Math.random() * equalizerCanvas.width;
        const lightningWidth = Math.random() * 3 + 1; // Random width
        const lightningHeight = equalizerCanvas.height * (0.5 + Math.random() * 0.5); // Random height

        canvasCtx.beginPath();
        canvasCtx.moveTo(lightningX, 0);
        canvasCtx.lineTo(lightningX + lightningWidth / 2, lightningHeight / 2);
        canvasCtx.lineTo(lightningX - lightningWidth / 2, lightningHeight * 0.75);
        canvasCtx.lineTo(lightningX + lightningWidth / 4, equalizerCanvas.height);
        canvasCtx.strokeStyle = 'rgba(255, 255, 255, 0.9)'; // Bright white
        canvasCtx.lineWidth = 2 + (Math.random() * 3);
        canvasCtx.shadowBlur = 20 + (intensity * 30);
        canvasCtx.shadowColor = 'rgba(200, 200, 255, 0.8)';
        canvasCtx.stroke();
        canvasCtx.shadowBlur = 0;
      }
    }
  }

  // 26. Data Stream (Vertical Lines with Horizontal Flow)
  function drawDataStream() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const segmentHeight = 10;
    const gap = 2;
    const numSegmentsPerColumn = Math.floor(equalizerCanvas.height / (segmentHeight + gap));
    const columnWidth = 5;
    const columnGap = 10;

    // Add a slight horizontal shift over time for flow effect
    const flowOffset = (performance.now() * 0.001 * 5) % (columnWidth + columnGap); // Faster flow

    for (let i = 0; i < bufferLength; i += 5) { // Sample less data for wider columns
      const data = frequencyDataArray[i];
      const intensity = data / 255;

      let x = i * (columnWidth + columnGap) / 5 + flowOffset; // Adjust x with flowOffset

      if (x > equalizerCanvas.width + columnWidth) {
        // Wrap around
        x -= (equalizerCanvas.width + columnWidth + columnGap); // Reset to beginning for smooth loop
      }

      // Draw multiple vertical segments in each column
      for (let j = 0; j < numSegmentsPerColumn; j++) {
        let y = j * (segmentHeight + gap);

        // Make segments appear or disappear based on intensity
        if (Math.random() < intensity * 0.8) { // Higher intensity, more likely to draw
          const gradient = canvasCtx.createLinearGradient(x, y, x + columnWidth, y + segmentHeight);
          gradient.addColorStop(0, 'rgba(0, 200, 255, 0.2)');
          gradient.addColorStop(0.5, 'rgba(0, 200, 255, 0.8)');
          gradient.addColorStop(1, 'rgba(0, 200, 255, 0.2)');
          canvasCtx.fillStyle = gradient;

          canvasCtx.fillRect(x, y, columnWidth, segmentHeight);
        }
      }
    }
  }

  // 27. Vortex Spectrum (Rotating, Expanding Bars)
  function drawVortexSpectrum() {
    analyser.getByteFrequencyData(frequencyDataArray);

    canvasCtx.clearRect(0, 0, equalizerCanvas.width, equalizerCanvas.height);

    const centerX = equalizerCanvas.width / 2;
    const centerY = equalizerCanvas.height / 2;
    const maxRadius = Math.min(centerX, centerY) * 0.9;
    const baseInnerRadius = maxRadius * 0.1; // Inner circle where bars start

    const barCount = 90; // Number of bars in the vortex
    const angleStep = (Math.PI * 2) / barCount;
    const sliceSize = Math.floor(bufferLength / barCount);

    vortexAngle += 0.008; // Continuous rotation speed

    canvasCtx.save();
    canvasCtx.translate(centerX, centerY);
    canvasCtx.rotate(vortexAngle);

    for (let i = 0; i < barCount; i++) {
      let sum = 0;
      for (let j = 0; j < sliceSize; j++) {
        sum += frequencyDataArray[i * sliceSize + j];
      }
      const averageFrequency = sum / sliceSize;
      const barLength = (averageFrequency / 255) * (maxRadius - baseInnerRadius);

      const startAngle = i * angleStep;
      const endAngle = (i + 1) * angleStep - 0.01; // Small gap between bars

      // Radial gradient for each bar
      const gradient = canvasCtx.createRadialGradient(0, 0, baseInnerRadius, 0, 0, baseInnerRadius + barLength);
      gradient.addColorStop(0, 'rgba(0, 255, 255, 0.2)'); // Cyan starting
      gradient.addColorStop(0.5, 'rgba(0, 255, 255, 0.8)');
      gradient.addColorStop(1, 'rgba(0, 100, 255, 1)'); // Darker blue at end

      canvasCtx.fillStyle = gradient;

      canvasCtx.beginPath();
      canvasCtx.arc(0, 0, baseInnerRadius + barLength, startAngle, endAngle, 0); // Outer arc
      canvasCtx.arc(0, 0, baseInnerRadius, endAngle, startAngle, 1); // Inner arc (reversed)
      canvasCtx.closePath();
      canvasCtx.fill();
    }
    canvasCtx.restore();
  }

  /**
   * Resets the internal state of effects that manage their own particles/arrays.
   * This should be called when switching between effects that have persistent state.
   */
  function resetEffectStates() {
    rotationAngle = 0;
    particles = [];
    fireflies = [];
    tunnelOffset = 0;
    vortexAngle = 0;
    lightningParticles = []; // Ensure this is also reset
  }

  // Expose functions
  exports.EqualizerEffects = {
    init: initEqualizerEffects,
    resetStates: resetEffectStates,
    animate: animate, // This will now handle the requestAnimationFrame loop
    effects: {
      waveform: drawWaveform,
      frequency: drawFrequencyBars,
      circular: drawCircularWaveform,
      dots: drawDotWaveform,
      blob: drawBlobEffect,
      audioSpectrumVisualizer: drawAudioSpectrumVisualizer,
      soundwave: drawSoundwave,
      visualEqualizer: drawVisualEqualizer,
      wavesurferLike: drawWavesurferLike,
      audioSoundGraphic: drawAudioSoundGraphic,
      verticalSymmetricalBars: drawVerticalSymmetricalBars,
      particleCloud: drawParticleCloud,
      rotatingRing: drawRotatingRing,
      abstractLines: drawAbstractLines,
      spherePulse: drawSpherePulse,
      lineGrid: drawLineGrid,
      growingCircles: drawGrowingCircles,
      raindropEffect: drawRaindropEffect,
      tunnelEffect: drawTunnelEffect,
      firefliesSwarm: drawFirefliesSwarm,
      pixelGridHeatmap: drawPixelGridHeatmap,
      lissajousCurve: drawLissajousCurve,
      rippleEffect: drawRippleEffect,
      glowTrails: drawGlowTrails,
      rainAndLightning: drawRainAndLightning,
      dataStream: drawDataStream,
      vortexSpectrum: drawVortexSpectrum
    }
  };

})(window); // Expose EqualizerEffects globally or within a specific namespace

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc