Home Manual Reference Source Repository

src/shapes/waveform.js

import BaseShape from './base-shape';


const xhtmlNS = 'http://www.w3.org/1999/xhtml';

/**
 * A shape to display a waveform. (for entity data)
 *
 * [example usage](./examples/layer-waveform.html)
 *
 * @todo - fix problems with canvas strategy.
 */
export default class Waveform extends BaseShape {
  getClassName() { return 'waveform'; }

  _getAccessorList() {
    // return { y: 0 };
    return {};
  }

  _getDefaults() {
    return {
      sampleRate: 44100,
      color: '#000000',
      opacity: 1,
      // renderingStrategy: 'svg' // canvas is bugged (translation, etc...)
    };
  }

  render(renderingContext) {
    if (this.$el) { return this.$el; }

    // if (this.params.renderingStrategy === 'svg') {

      this.$el = document.createElementNS(this.ns, 'path');
      this.$el.setAttributeNS(null, 'fill', 'none');
      this.$el.setAttributeNS(null, 'shape-rendering', 'crispEdges');
      this.$el.setAttributeNS(null, 'stroke', this.params.color);
      this.$el.style.opacity = this.params.opacity;

    // } else if (this.params.renderingStrategy === 'canvas') {

    //   this.$el = document.createElementNS(this.ns, 'foreignObject');
    //   this.$el.setAttributeNS(null, 'width', renderingContext.width);
    //   this.$el.setAttributeNS(null, 'height', renderingContext.height);

    //   const canvas = document.createElementNS(xhtmlNS, 'xhtml:canvas');

    //   this._ctx = canvas.getContext('2d');
    //   this._ctx.canvas.width = renderingContext.width;
    //   this._ctx.canvas.height = renderingContext.height;

    //   this.$el.appendChild(canvas);
    // }

    return this.$el;
  }

  update(renderingContext, datum) {
    // define nbr of samples per pixels
    const sliceMethod = datum instanceof Float32Array ? 'subarray' : 'slice';
    const nbrSamples = datum.length;
    const duration = nbrSamples / this.params.sampleRate;
    const width = renderingContext.timeToPixel(duration);
    const samplesPerPixel = nbrSamples / width;

    if (!samplesPerPixel || datum.length < samplesPerPixel) { return; }

    // compute/draw visible area only
    // @TODO refactor this ununderstandable mess
    let minX = Math.max(-renderingContext.offsetX, 0);
    let trackDecay = renderingContext.trackOffsetX + renderingContext.startX;
    if (trackDecay < 0) { minX = -trackDecay; }

    let maxX = minX;
    maxX += (renderingContext.width - minX < renderingContext.visibleWidth) ?
      renderingContext.width : renderingContext.visibleWidth;

    // get min/max per pixels, clamped to the visible area
    const invert = renderingContext.timeToPixel.invert;
    const sampleRate = this.params.sampleRate;
    const minMax = [];

    for (let px = minX; px < maxX; px++) {
      const startTime = invert(px);
      const startSample = startTime * sampleRate;
      const extract = datum[sliceMethod](startSample, startSample + samplesPerPixel);

      let min = Infinity;
      let max = -Infinity;

      for (let j = 0, l = extract.length; j < l; j++) {
        let sample = extract[j];
        if (sample < min) { min = sample; }
        if (sample > max) { max = sample; }
      }
      // disallow Infinity
      min = !isFinite(min) ? 0 : min;
      max = !isFinite(max) ? 0 : max;
      if (min === 0 && max === 0) { continue; }

      minMax.push([px, min, max]);
    }

    if (!minMax.length) { return; }

    const PIXEL = 0;
    const MIN   = 1;
    const MAX   = 2;
    const ZERO  = renderingContext.valueToPixel(0);
    // rendering strategies
    // if (this.params.renderingStrategy === 'svg') {

      let instructions = minMax.map((datum, index) => {
        const x  = datum[PIXEL];
        let y1 = Math.round(renderingContext.valueToPixel(datum[MIN]));
        let y2 = Math.round(renderingContext.valueToPixel(datum[MAX]));
        // return `${x},${ZERO}L${x},${y1}L${x},${y2}L${x},${ZERO}`;
        return `${x},${y1}L${x},${y2}`;
      });

      const d = 'M' + instructions.join('L');
      this.$el.setAttributeNS(null, 'd', d);

    // } else if (this.params.renderingStrategy === 'canvas') {

    //   this._ctx.canvas.width = width;
    //   this.$el.setAttribute('width', width);
    //   // fix chrome bug with translate
    //   if (navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
    //     this.$el.setAttribute('x', renderingContext.offsetX);
    //   }

    //   this._ctx.strokeStyle = this.params.color;
    //   this._ctx.globalAlpha = this.params.opacity;
    //   this._ctx.moveTo(renderingContext.timeToPixel(0), renderingContext.valueToPixel(0));

    //   minMax.forEach((datum) => {
    //     const x  = datum[PIXEL];
    //     let y1 = Math.round(renderingContext.valueToPixel(datum[MIN]));
    //     let y2 = Math.round(renderingContext.valueToPixel(datum[MAX]));

    //     this._ctx.moveTo(x, y1);
    //     this._ctx.lineTo(x, y2);
    //   });

    //   this._ctx.stroke();
    // }
  }
}