view src/app/visualisations/waves-base.component.ts @ 394:f45a916eb5b1

Use the cross hair layer for notes, tracks and curve. This involved bodging in unit to ShapedFeatureData, which isn't particularly easy to do because this isn't an encapsulated type. Need to come back to improving this, as I am monkey-patching a unit property onto Arrays etc.
author Lucas Thompson <dev@lucas.im>
date Thu, 01 Jun 2017 18:55:55 +0100
parents 7ef6c8089801
children 3eab26a629e1
line wrap: on
line source
/**
 * Created by lucast on 26/05/2017.
 */
import {AfterViewInit, ElementRef, Input, ViewChild} from '@angular/core';
import {OnSeekHandler} from '../playhead/PlayHeadHelpers';
import {attachTouchHandlerBodges} from './WavesJunk';
import Waves from 'waves-ui-piper';
import {countingIdProvider} from 'piper/client-stubs/WebWorkerStreamingClient';
import {ShapedFeatureData} from './FeatureUtilities';

const trackIdGenerator = countingIdProvider(0);

// has to be an abstract class vs as interface for Angular's DI
export abstract class VerticallyBounded {
  abstract get range(): [number, number];
}

export abstract class VerticalScaleRenderer extends VerticallyBounded {
  abstract renderScale(range: [number, number]): void;
}

export abstract class VerticalValueInspectorRenderer
  extends VerticalScaleRenderer {
  // TODO how do I know these layers are actually 'describable'?
  abstract renderInspector(range: [number, number], unit?: string): void;
}

export abstract class WavesComponent<T extends ShapedFeatureData | AudioBuffer>
  implements AfterViewInit {
  @ViewChild('track') trackContainer: ElementRef;
  @Input() set width(width: number) {
    if (this.timeline) {
      requestAnimationFrame(() => {
        this.timeline.timeContext.visibleWidth = width;
        this.timeline.tracks.update();
      });
    }
  }
  @Input() timeline: Timeline;
  @Input() onSeek: OnSeekHandler;
  @Input() colour: string;
  @Input() set feature(feature: T) {
    this.mFeature = feature;
    this.update();
  }

  get feature(): T {
    return this.mFeature;
  }

  private layers: Layer[];
  private zoomOnMouseDown: number;
  private offsetOnMouseDown: number;
  private waveTrack: Track;
  private mFeature: T;
  private id: string;
  protected abstract get featureLayers(): Layer[];
  protected cachedFeatureLayers: Layer[];
  protected postAddMap: (value: Layer, index: number, array: Layer[]) => void;
  protected height: number;
  protected duration: number;

  constructor() {
    this.layers = [];
    this.id = trackIdGenerator.next().value;
  }

  ngAfterViewInit(): void {
    this.height =
      this.trackContainer.nativeElement.getBoundingClientRect().height;
    this.renderTimeline();
    this.update();
  }

  private update(): void {
    if (!this.waveTrack || !this.mFeature) {
      return;
    }
    this.clearTimeline();
    this.cachedFeatureLayers = this.featureLayers;
    for (const layer of this.cachedFeatureLayers) {
      this.addLayer(layer);
    }
    if (this.postAddMap) {
      this.cachedFeatureLayers.forEach(this.postAddMap);
    }
  }


  private renderTimeline(): Timeline {
    const track: HTMLElement = this.trackContainer.nativeElement;
    track.innerHTML = '';
    if (this.duration >= 0) {
      const width: number = track.getBoundingClientRect().width;
      this.timeline.pixelsPerSecond = width / this.duration;
      this.timeline.visibleWidth = width;
    }
    this.waveTrack = this.timeline.createTrack(
      track,
      this.height,
      this.id
    );

    if ('ontouchstart' in window) {
      attachTouchHandlerBodges(
        track,
        this.timeline
      );
    }
    this.resetTimelineState();
  }

  // TODO can likely be removed, or use waves-ui methods
  private clearTimeline(): void {
    // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
    const timeContextChildren = this.timeline.timeContext._children;
    for (const track of this.timeline.tracks) {
      if (track.layers.length === 0) { continue; }
      const trackLayers: Layer[] = Array.from(track.layers as Layer[]);
      while (trackLayers.length) {
        const layer: Layer = trackLayers.pop();
        if (this.layers.includes(layer)) {
          track.remove(layer);
          this.layers.splice(this.layers.indexOf(layer), 1);
          const index = timeContextChildren.indexOf(layer.timeContext);
          if (index >= 0) {
            timeContextChildren.splice(index, 1);
          }
          layer.destroy();
        }
      }
    }
    this.resetTimelineState();
  }

  private resetTimelineState(): void {
    // time axis
    const timeAxis = new Waves.helpers.TimeAxisLayer({
      height: this.height,
      color: '#b0b0b0'
    });
    this.addLayer(timeAxis, true);
    this.timeline.state = new Waves.states.CenteredZoomState(this.timeline);
    this.timeline.tracks.update(); // TODO this is problematic, shared state across components
  }


  // TODO can likely use methods in waves-ui directly
  protected addLayer(layer: Layer,
                     isAxis: boolean = false): void {
    const timeContext = this.timeline.timeContext;
    if (!layer.timeContext) {
      layer.setTimeContext(isAxis ?
        timeContext : new Waves.core.LayerTimeContext(timeContext));
    }
    this.waveTrack.add(layer);
    this.layers.push(layer);
    layer.render();
    layer.update();
  }

  seekStart(): void {
    this.zoomOnMouseDown = this.timeline.timeContext.zoom;
    this.offsetOnMouseDown = this.timeline.timeContext.offset;
  }

  seekEnd(x: number): void {
    const hasSameZoom: boolean = this.zoomOnMouseDown ===
      this.timeline.timeContext.zoom;
    const hasSameOffset: boolean = this.offsetOnMouseDown ===
      this.timeline.timeContext.offset;
    if (hasSameZoom && hasSameOffset) {
      this.seek(x);
    }
  }

  seek(x: number): void {
    if (this.timeline) {
      const timeContext: any = this.timeline.timeContext;
      if (this.onSeek) {
        this.onSeek(timeContext.timeToPixel.invert(x) - timeContext.offset);
      }
    }
  }
}

export abstract class VerticallyBoundedWavesComponent
<T extends ShapedFeatureData> extends WavesComponent<T>
  implements VerticalScaleRenderer {
  abstract range: [number, number];

  renderScale(range: [number, number]): void {
    this.addLayer(new Waves.helpers.ScaleLayer({
      tickColor: this.colour,
      textColor: this.colour,
      height: this.height,
      yDomain: range
    }));
  }
}

export abstract class InspectableVerticallyBoundedComponent
<T extends ShapedFeatureData> extends VerticallyBoundedWavesComponent<T>
  implements VerticalValueInspectorRenderer {

  private wrappedSeekHandler: OnSeekHandler;
  private highlight: HighlightLayer;

  @Input() set onSeek(handler: OnSeekHandler) {
    this.wrappedSeekHandler = (x: number) => {
      handler(x);
      if (this.highlight) {
        this.highlight.currentPosition = x;
        this.highlight.update();
      }
    };
  }

  get onSeek(): OnSeekHandler {
    return this.wrappedSeekHandler;
  }


  renderInspector(range: [number, number], unit?: string): void {
    if (range) {
      this.highlight = new Waves.helpers.HighlightLayer(
        this.cachedFeatureLayers,
        {
          opacity: 0.7,
          height: this.height,
          color: '#c33c54', // TODO pass in?
          labelOffset: 38,
          yDomain: range,
          unit: unit || this.feature.unit || ''
        }
      );
      this.addLayer(this.highlight);
    }
  }
}