Source: utils/BaseComponent.js

class BaseComponent {


  /** @summary BaseComponent is the bedrock of any visualisation here. It must be inherited from Mono or Stereo component abstractions.
   * @author Arthur Beaulieu
   * @since 2020
   * @description <blockquote>Store all base method, mostly to handle events, other processing methods needs to be overridden.</blockquote> **/
  constructor() {
    /** @private
     * @member {string} - The component type. See supported componenets in AudioVisualizer factory */
    this._type = null;
    /** @private
     * @member {object} - The audio source (HTML audio player) */
    this._player = null;
    /** @private
     * @member {object} - Target div to render module in */
    this._renderTo = null;
    /** @private
     * @member {number} - FFT size used to analyse audio stream. Must be a power of 2 */
    this._fftSize = null;
    /** @private
     * @member {object} - The audio context */
    this._audioCtx = null;
    /** @private
     * @member {object} - The source node to chain from ; it will ignore the output of HTML audio player */
    this._inputNode = null;
    /** @private
     * @member {boolean} - The playing state of the player */
    this._isPlaying = false;
    /** @private
     * @member {object} - Contains all useful DOM objects */
    this._dom = {
      container: null
    };
    /** @private
     * @member {object} - Save container dimension to restore when closing fullscreen */
    this._parentDimension = {
      position: null,
      height: null,
      width: null,
      zIndex: null
    };
    /** @private
     * @member {object} - Resize observable to watch for any resize change */
    this._resizeObserver = null;
    // Event binding
    this._onResize = this._onResize.bind(this);
    this._play = this._play.bind(this);
    this._pause = this._pause.bind(this);
    this._dblClick = this._dblClick.bind(this);
    // Bind process audio bin for add and remove event on demand
    this._processAudioBin = this._processAudioBin.bind(this);
  }


  /** @method
   * @name destroy
   * @public
   * @memberof BaseComponent
   * @author Arthur Beaulieu
   * @since 2020
   * @description <blockquote>The destroy method to clear events and remove all component properties.</blockquote> **/
  destroy() {
    this._removeEvents();
    Object.keys(this).forEach(key => { delete this[key]; });
  }


  /** @method
   * @name _fillAttributes
   * @private
   * @memberof BaseComponent
   * @author Arthur Beaulieu
   * @since 2020
   * @description <blockquote>Build component properties from options. Must be implemented in sub class.</blockquote> **/
  _fillAttributes() {
    // Must be implemented in sub class
  }


  /** @method
   * @name _buildUI
   * @private
   * @memberof BaseComponent
   * @author Arthur Beaulieu
   * @since 2020
   * @description <blockquote>Create, configure and append UI in DOM. Must be implemented in sub class.</blockquote> **/
  _buildUI() {
    // Must be implemented in sub class
  }


  /** @method
   * @name _setAudioNodes
   * @private
   * @memberof BaseComponent
   * @author Arthur Beaulieu
   * @since 2020
   * @description <blockquote>Build audio chain with source. Must be implemented in sub class.</blockquote> **/
  _setAudioNodes() {
    // Must be implemented in sub class
  }


  /** @method
   * @name _processAudioBin
   * @private
   * @memberof BaseComponent
   * @author Arthur Beaulieu
   * @since 2020
   * @description <blockquote>Real time audio analysis using PCM data from WebAudioAPI. Must be implemented in sub class.</blockquote> **/
  _processAudioBin() {
    // Must be implemented in sub class
  }


  /** @method
   * @name _addEvents
   * @private
   * @memberof BaseComponent
   * @author Arthur Beaulieu
   * @since 2020
   * @description <blockquote>Add component events (resize, play, pause, dbclick).</blockquote> **/
  _addEvents() {
    // Put observer on renderTo and callback onResize at each action
    this._resizeObserver = new ResizeObserver(this._onResize);
    this._resizeObserver.observe(this._renderTo);
    // Playback events
    this._player.addEventListener('play', this._play, false);
    this._player.addEventListener('pause', this._pause, false);
    // Double click handler (fullscreen for most components)
    this._dom.container.addEventListener('dblclick', this._dblClick, false);
  }


  /** @method
   * @name _removeEvents
   * @private
   * @memberof BaseComponent
   * @author Arthur Beaulieu
   * @since 2020
   * @description <blockquote>Remove component events (resize, play, pause, dbclick).</blockquote> **/
  _removeEvents() {
    // Clear observable
    this._resizeObserver.disconnect();
    // Clear playback events
    this._player.removeEventListener('play', this._play, false);
    this._player.removeEventListener('pause', this._pause, false);
    // Remove double click listener
    this._dom.container.removeEventListener('dblclick', this._dblClick, false);
  }


  /** @method
   * @name _play
   * @private
   * @memberof BaseComponent
   * @author Arthur Beaulieu
   * @since 2020
   * @description <blockquote>On play event callback.</blockquote> **/
  _play() {
    this._audioCtx.resume();
    this._isPlaying = true;
    this._processAudioBin();
  }


  /** @method
   * @name _pause
   * @private
   * @memberof BaseComponent
   * @author Arthur Beaulieu
   * @since 2020
   * @description <blockquote>On pause event callback.</blockquote> **/
  _pause() {
    this._audioCtx.suspend();
    this._isPlaying = false;
  }


  /** @method
   * @name _onResize
   * @private
   * @memberof BaseComponent
   * @author Arthur Beaulieu
   * @since 2020
   * @description <blockquote>On resize event callback. Must be implemented in sub class.</blockquote> **/
  _onResize() {
    // Resize must be handled in each sub class
  }


  /** @method
   * @name _dblClick
   * @private
   * @memberof BaseComponent
   * @author Arthur Beaulieu
   * @since 2020
   * @description <blockquote>On double click event callback (toggle fullscreen).</blockquote> **/
  _dblClick() {
    if (document.fullscreenElement) {
      document.exitFullscreen().then(() => {
        // Restore renderTo initial style
        this._renderTo.style.position = this._parentDimension.position;
        this._renderTo.style.height = this._parentDimension.height;
        this._renderTo.style.width = this._parentDimension.width;
        this._renderTo.style.zIndex = this._parentDimension.zIndex;
        this._parentDimension = {
          position: null,
          height: null,
          width: null,
          zIndex: null
        };
      });
    } else {
      document.documentElement.requestFullscreen().then(() => {
        // Update renderTo dimension (canvas will be automatically rescaled)
        this._parentDimension = {
          position: this._renderTo.style.position,
          height: this._renderTo.style.height,
          width: this._renderTo.style.width,
          zIndex: this._renderTo.style.zIndex || ''
        };
        // Alter render to style to make it fullscreen
        this._renderTo.style.position = 'fixed';
        this._renderTo.style.height = '100vh';
        this._renderTo.style.width = '100vw';
        this._renderTo.style.zIndex = '999';
      });
    }
  }


  /** @method
   * @name _clearCanvas
   * @private
   * @memberof BaseComponent
   * @author Arthur Beaulieu
   * @since 2020
   * @description <blockquote>Clear component canvas contexts from their content. Must be implemented in sub class.</blockquote> **/
  _clearCanvas() {
    // Clear canvas must be handled in Mono/Stereo sub class depending on amount of canvas
  }


}


export default BaseComponent;