annotate src/app/visualisations/waves-base.component.ts @ 389:29b817e49a22

Introduce interface VerticallyBounded, which describes a component with a vertical range and the ability to render a scale on itself. VerticallyBoundedWavesComponent partially implements this interface, in that it adds a ScaleLayer to itself - derived instances provide the means of obtaining the scale.
author Lucas Thompson <dev@lucas.im>
date Thu, 01 Jun 2017 10:04:41 +0100
parents 2ec1e795f46c
children 7ef6c8089801
rev   line source
dev@354 1 /**
dev@354 2 * Created by lucast on 26/05/2017.
dev@354 3 */
dev@384 4 import {AfterViewInit, ElementRef, Input, ViewChild} from '@angular/core';
dev@354 5 import {OnSeekHandler} from '../playhead/PlayHeadHelpers';
dev@354 6 import {attachTouchHandlerBodges} from './WavesJunk';
dev@354 7 import Waves from 'waves-ui-piper';
dev@376 8 import {countingIdProvider} from 'piper/client-stubs/WebWorkerStreamingClient';
dev@383 9 import {ShapedFeatureData} from './FeatureUtilities';
dev@376 10
dev@376 11 const trackIdGenerator = countingIdProvider(0);
dev@354 12
dev@389 13 // has to be an abstract class vs as interface for Angular's DI
dev@389 14 export abstract class VerticallyBounded {
dev@389 15 abstract get range(): [number, number];
dev@389 16 abstract renderScale(range: [number, number]): void;
dev@389 17 }
dev@389 18
dev@383 19 export abstract class WavesComponent<T extends ShapedFeatureData | AudioBuffer>
dev@383 20 implements AfterViewInit {
dev@384 21 @ViewChild('track') trackContainer: ElementRef;
dev@354 22 @Input() set width(width: number) {
dev@354 23 if (this.timeline) {
dev@354 24 requestAnimationFrame(() => {
dev@354 25 this.timeline.timeContext.visibleWidth = width;
dev@354 26 this.timeline.tracks.update();
dev@354 27 });
dev@354 28 }
dev@354 29 }
dev@354 30 @Input() timeline: Timeline;
dev@354 31 @Input() onSeek: OnSeekHandler;
dev@379 32 @Input() colour: string;
dev@383 33 @Input() set feature(feature: T) {
dev@383 34 this.mFeature = feature;
dev@383 35 this.update();
dev@383 36 }
dev@383 37
dev@383 38 get feature(): T {
dev@383 39 return this.mFeature;
dev@383 40 }
dev@354 41
dev@387 42 private layers: Layer[];
dev@387 43 private zoomOnMouseDown: number;
dev@387 44 private offsetOnMouseDown: number;
dev@387 45 private waveTrack: Track;
dev@387 46 private mFeature: T;
dev@387 47 private id: string;
dev@383 48 protected abstract get featureLayers(): Layer[];
dev@383 49 protected postAddMap: (value: Layer, index: number, array: Layer[]) => void;
dev@383 50 protected height: number;
dev@383 51 protected duration: number;
dev@354 52
dev@354 53 constructor() {
dev@354 54 this.layers = [];
dev@376 55 this.id = trackIdGenerator.next().value;
dev@354 56 }
dev@354 57
dev@383 58 ngAfterViewInit(): void {
dev@384 59 this.height =
dev@384 60 this.trackContainer.nativeElement.getBoundingClientRect().height;
dev@386 61 this.renderTimeline();
dev@383 62 this.update();
dev@383 63 }
dev@383 64
dev@386 65 private update(): void {
dev@383 66 if (!this.waveTrack || !this.mFeature) {
dev@383 67 return;
dev@383 68 }
dev@386 69 this.clearTimeline();
dev@383 70 const layers = this.featureLayers;
dev@383 71 for (const layer of layers) {
dev@389 72 this.addLayer(layer);
dev@383 73 }
dev@383 74 if (this.postAddMap) {
dev@383 75 layers.forEach(this.postAddMap);
dev@383 76 }
dev@383 77 }
dev@383 78
dev@383 79
dev@386 80 private renderTimeline(): Timeline {
dev@386 81 const track: HTMLElement = this.trackContainer.nativeElement;
dev@354 82 track.innerHTML = '';
dev@383 83 if (this.duration >= 0) {
dev@364 84 const width: number = track.getBoundingClientRect().width;
dev@383 85 this.timeline.pixelsPerSecond = width / this.duration;
dev@364 86 this.timeline.visibleWidth = width;
dev@364 87 }
dev@354 88 this.waveTrack = this.timeline.createTrack(
dev@354 89 track,
dev@383 90 this.height,
dev@376 91 this.id
dev@354 92 );
dev@354 93
dev@354 94 if ('ontouchstart' in window) {
dev@354 95 attachTouchHandlerBodges(
dev@386 96 track,
dev@354 97 this.timeline
dev@354 98 );
dev@354 99 }
dev@383 100 this.resetTimelineState();
dev@354 101 }
dev@354 102
dev@354 103 // TODO can likely be removed, or use waves-ui methods
dev@386 104 private clearTimeline(): void {
dev@354 105 // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
dev@354 106 const timeContextChildren = this.timeline.timeContext._children;
dev@354 107 for (const track of this.timeline.tracks) {
dev@354 108 if (track.layers.length === 0) { continue; }
dev@354 109 const trackLayers: Layer[] = Array.from(track.layers as Layer[]);
dev@354 110 while (trackLayers.length) {
dev@354 111 const layer: Layer = trackLayers.pop();
dev@354 112 if (this.layers.includes(layer)) {
dev@354 113 track.remove(layer);
dev@354 114 this.layers.splice(this.layers.indexOf(layer), 1);
dev@354 115 const index = timeContextChildren.indexOf(layer.timeContext);
dev@354 116 if (index >= 0) {
dev@354 117 timeContextChildren.splice(index, 1);
dev@354 118 }
dev@354 119 layer.destroy();
dev@354 120 }
dev@354 121 }
dev@354 122 }
dev@383 123 this.resetTimelineState();
dev@356 124 }
dev@356 125
dev@383 126 private resetTimelineState(): void {
dev@356 127 // time axis
dev@356 128 const timeAxis = new Waves.helpers.TimeAxisLayer({
dev@384 129 height: this.height,
dev@356 130 color: '#b0b0b0'
dev@356 131 });
dev@389 132 this.addLayer(timeAxis, true);
dev@356 133 this.timeline.state = new Waves.states.CenteredZoomState(this.timeline);
dev@364 134 this.timeline.tracks.update(); // TODO this is problematic, shared state across components
dev@354 135 }
dev@354 136
dev@354 137
dev@354 138 // TODO can likely use methods in waves-ui directly
dev@389 139 protected addLayer(layer: Layer,
dev@389 140 isAxis: boolean = false): void {
dev@389 141 const timeContext = this.timeline.timeContext;
dev@354 142 if (!layer.timeContext) {
dev@354 143 layer.setTimeContext(isAxis ?
dev@354 144 timeContext : new Waves.core.LayerTimeContext(timeContext));
dev@354 145 }
dev@389 146 this.waveTrack.add(layer);
dev@354 147 this.layers.push(layer);
dev@354 148 layer.render();
dev@354 149 layer.update();
dev@354 150 }
dev@354 151
dev@354 152 seekStart(): void {
dev@354 153 this.zoomOnMouseDown = this.timeline.timeContext.zoom;
dev@354 154 this.offsetOnMouseDown = this.timeline.timeContext.offset;
dev@354 155 }
dev@354 156
dev@354 157 seekEnd(x: number): void {
dev@354 158 const hasSameZoom: boolean = this.zoomOnMouseDown ===
dev@354 159 this.timeline.timeContext.zoom;
dev@354 160 const hasSameOffset: boolean = this.offsetOnMouseDown ===
dev@354 161 this.timeline.timeContext.offset;
dev@354 162 if (hasSameZoom && hasSameOffset) {
dev@354 163 this.seek(x);
dev@354 164 }
dev@354 165 }
dev@354 166
dev@354 167 seek(x: number): void {
dev@354 168 if (this.timeline) {
dev@354 169 const timeContext: any = this.timeline.timeContext;
dev@354 170 if (this.onSeek) {
dev@354 171 this.onSeek(timeContext.timeToPixel.invert(x) - timeContext.offset);
dev@354 172 }
dev@354 173 }
dev@354 174 }
dev@354 175 }
dev@389 176
dev@389 177 export abstract class VerticallyBoundedWavesComponent
dev@389 178 <T extends ShapedFeatureData> extends WavesComponent<T>
dev@389 179 implements VerticallyBounded {
dev@389 180 abstract range: [number, number];
dev@389 181
dev@389 182 renderScale(range: [number, number]): void {
dev@389 183 this.addLayer(new Waves.helpers.ScaleLayer({
dev@389 184 tickColor: this.colour,
dev@389 185 textColor: this.colour,
dev@389 186 height: this.height,
dev@389 187 yDomain: range
dev@389 188 }));
dev@389 189 }
dev@389 190 }