Source: src/visualizations/Needles.js

import { p5 } from "../../index.js";
import Visualization from "../classes/Visualization.js";
import { fourier } from "../globals.js";

/**
 * needles moving by different frequency groups
 * @extends Visualization
 * @memberof Visualization
 */
class Needles extends Visualization {
  constructor() {
    super("needles");
    // set initial values when the object is created
    this.onResize();
  }

  // the number of plots in columns
  #PLOTS_COLUMN = 2;

  // the number of plots in rows
  #PLOTS_ROW = 2;

  // the min arc of the needle plot
  #MIN_ANGLE = p5.PI + p5.PI / 10;

  // the max arc of the needle plot
  #MAX_ANGLE = p5.TWO_PI - p5.PI / 10;

  // frquencies used by the energyfunction to retrieve a value for each plot.
  frequencyGroups = ["bass", "lowMid", "highMid", "treble"];

  draw() {
    // create an array amplitude values from the fft.
    fourier.analyze();

    // iterator for selecting frequency bin.
    let currentBin = 0;
    const width = p5.width;
    const height = p5.height;

    p5.push();
    p5.angleMode(p5.RADIANS);
    // nested for loop to place plots in 2 X 2 grid
    for (let i = 0; i < this.#PLOTS_ROW; i++) {
      for (let j = 0; j < this.#PLOTS_COLUMN; j++) {
        // calculate the size of the plots
        const cGap = width / 25; // gap b/w rows
        const rGap = height / 15; // gap b/w columns
        const halfWidth = width / 2;
        const halfHeight = height / 2;

        // calculate the size of the plots
        const x = j * (halfWidth - 0.6 * cGap) + cGap;
        const y = i * (halfHeight - 0.5 * rGap) + rGap;
        const w = halfWidth - cGap * 1.6;
        const h = halfHeight - rGap * 1.5;

        //draw a rectangle at that location and size
        p5.fill(240);
        p5.rect(x, y, w, h);

        // draw ticks
        this.drawTicks(x + w / 2, y + h, this.frequencyGroups[currentBin]);

        // draw needles
        const amp = fourier.getEnergy(this.frequencyGroups[currentBin]);
        this.#drawNeedle(amp, x + w / 2, y + h);

        currentBin++;
      }
    }

    p5.pop();
  }

  /**
   * resize the plots sizes when the screen is resized.
   * readjust pad, plotWidth, plotHeight, and dialRadius.
   */
  onResize = () => {
    /**
     * pad
     * @type {number}
     */
    this.pad = p5.width / 20;

    /**
     * pad width
     * @type {number}
     */
    this.plotWidth = (p5.width - this.pad) / this.#PLOTS_COLUMN;

    /**
     * plot height
     * @type {number}
     */
    this.plotHeight = (p5.height - this.pad) / this.#PLOTS_ROW;

    /**
     * dial radius
     * @type {number}
     */
    this.dialRadius = (this.plotWidth - this.pad) / 2 - 5;
  };

  /**
   * draws a needle to an individual plot
   * @param {number[]} amplitude - the amplitude for the current frequency
   * @param {number} centreX - central x coordinate of the plot rectangle
   * @param {number} bottomY - The bottom y coordinate of the plot rectangle
   */
  #drawNeedle(amplitude, centreX, bottomY) {
    p5.push();
    p5.stroke("#333333");

    // translate so 0 is at the bottom of the needle
    p5.translate(centreX, bottomY);

    // map the energy to the angle for the plot
    const theta = p5.map(amplitude, 0, 255, this.#MIN_ANGLE, this.#MAX_ANGLE);
    //calculate x and y coorindates from angle for the length of needle
    const x = this.dialRadius * p5.cos(theta);
    const y = this.dialRadius * p5.sin(theta);

    //draw the needle
    p5.line(0, 0, x, y);
    p5.pop();
  }

  /**
   * draw the graph ticks on an indivisual plot
   * @param {number} centreX - central x coordinate of the plot rectangle
   * @param {number} bottomY - The bottom y coordinate of the plot rectangle
   * @param {string} freqLabel - Label denoting the frequency of the plot
   */
  drawTicks(centreX, bottomY, freqLabel) {
    // 8 ticks from pi to 2pi
    let nextTickAngle = this.#MIN_ANGLE;
    p5.push();
    p5.stroke("#333333");
    p5.fill("#333333");
    p5.translate(centreX, bottomY);

    // draw the semi circle for the botttom of the needle
    p5.arc(0, 0, 20, 20, p5.PI, 2 * p5.PI);
    p5.textAlign(p5.CENTER);
    p5.textSize(12);
    p5.text(freqLabel, 0, -(this.plotHeight / 2));

    for (let i = 0; i < 9; i++) {
      // for each tick work out the start and end coordinates of based on its angle from the needle's origin
      let x1 = this.dialRadius * p5.cos(nextTickAngle);
      let x2 = (this.dialRadius - 5) * p5.cos(nextTickAngle);

      let y1 = this.dialRadius * p5.sin(nextTickAngle);
      let y2 = (this.dialRadius - 5) * p5.sin(nextTickAngle);

      p5.line(x1, y1, x2, y2);
      nextTickAngle += p5.PI / 10;
    }
    p5.pop();
  }
}

export default Needles;