annotate src/app/visualisations/waves-base.component.ts @ 407:6e14dd416e12

A play-head which attaches itself to WavesComponents.
author Lucas Thompson <dev@lucas.im>
date Sun, 04 Jun 2017 20:17:43 +0100
parents 0554b1af47f6
children 89674c064cda
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@392 16 }
dev@392 17
dev@392 18 export abstract class VerticalScaleRenderer extends VerticallyBounded {
dev@389 19 abstract renderScale(range: [number, number]): void;
dev@389 20 }
dev@389 21
dev@392 22 export abstract class VerticalValueInspectorRenderer
dev@392 23 extends VerticalScaleRenderer {
dev@392 24 // TODO how do I know these layers are actually 'describable'?
dev@394 25 abstract renderInspector(range: [number, number], unit?: string): void;
dev@397 26 abstract get updatePosition(): OnSeekHandler;
dev@392 27 }
dev@392 28
dev@406 29 export type LayerRemover = () => void;
dev@406 30
dev@383 31 export abstract class WavesComponent<T extends ShapedFeatureData | AudioBuffer>
dev@383 32 implements AfterViewInit {
dev@384 33 @ViewChild('track') trackContainer: ElementRef;
dev@354 34 @Input() set width(width: number) {
dev@354 35 if (this.timeline) {
dev@354 36 requestAnimationFrame(() => {
dev@354 37 this.timeline.timeContext.visibleWidth = width;
dev@354 38 this.timeline.tracks.update();
dev@354 39 });
dev@354 40 }
dev@354 41 }
dev@354 42 @Input() timeline: Timeline;
dev@354 43 @Input() onSeek: OnSeekHandler;
dev@379 44 @Input() colour: string;
dev@383 45 @Input() set feature(feature: T) {
dev@383 46 this.mFeature = feature;
dev@383 47 this.update();
dev@383 48 }
dev@383 49
dev@383 50 get feature(): T {
dev@383 51 return this.mFeature;
dev@383 52 }
dev@354 53
dev@387 54 private layers: Layer[];
dev@387 55 private zoomOnMouseDown: number;
dev@387 56 private offsetOnMouseDown: number;
dev@387 57 private waveTrack: Track;
dev@387 58 private mFeature: T;
dev@387 59 private id: string;
dev@383 60 protected abstract get featureLayers(): Layer[];
dev@392 61 protected cachedFeatureLayers: Layer[];
dev@383 62 protected postAddMap: (value: Layer, index: number, array: Layer[]) => void;
dev@383 63 protected duration: number;
dev@406 64 height: number;
dev@354 65
dev@354 66 constructor() {
dev@354 67 this.layers = [];
dev@376 68 this.id = trackIdGenerator.next().value;
dev@354 69 }
dev@354 70
dev@383 71 ngAfterViewInit(): void {
dev@384 72 this.height =
dev@384 73 this.trackContainer.nativeElement.getBoundingClientRect().height;
dev@386 74 this.renderTimeline();
dev@383 75 this.update();
dev@383 76 }
dev@383 77
dev@386 78 private update(): void {
dev@383 79 if (!this.waveTrack || !this.mFeature) {
dev@383 80 return;
dev@383 81 }
dev@386 82 this.clearTimeline();
dev@392 83 this.cachedFeatureLayers = this.featureLayers;
dev@392 84 for (const layer of this.cachedFeatureLayers) {
dev@389 85 this.addLayer(layer);
dev@383 86 }
dev@383 87 if (this.postAddMap) {
dev@392 88 this.cachedFeatureLayers.forEach(this.postAddMap);
dev@383 89 }
dev@383 90 }
dev@383 91
dev@383 92
dev@386 93 private renderTimeline(): Timeline {
dev@386 94 const track: HTMLElement = this.trackContainer.nativeElement;
dev@354 95 track.innerHTML = '';
dev@383 96 if (this.duration >= 0) {
dev@364 97 const width: number = track.getBoundingClientRect().width;
dev@383 98 this.timeline.pixelsPerSecond = width / this.duration;
dev@364 99 this.timeline.visibleWidth = width;
dev@364 100 }
dev@354 101 this.waveTrack = this.timeline.createTrack(
dev@354 102 track,
dev@383 103 this.height,
dev@376 104 this.id
dev@354 105 );
dev@354 106
dev@354 107 if ('ontouchstart' in window) {
dev@354 108 attachTouchHandlerBodges(
dev@386 109 track,
dev@354 110 this.timeline
dev@354 111 );
dev@354 112 }
dev@383 113 this.resetTimelineState();
dev@354 114 }
dev@354 115
dev@354 116 // TODO can likely be removed, or use waves-ui methods
dev@386 117 private clearTimeline(): void {
dev@354 118 // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
dev@406 119 const track = this.waveTrack;
dev@406 120 if (track.layers.length === 0) { return; }
dev@406 121 const trackLayers: Layer[] = Array.from(track.layers as Layer[]);
dev@406 122 while (trackLayers.length) {
dev@406 123 this.removeLayer(trackLayers.pop());
dev@354 124 }
dev@383 125 this.resetTimelineState();
dev@356 126 }
dev@356 127
dev@406 128 private removeLayer(layer: Layer) {
dev@406 129 if (this.layers.includes(layer) && this.waveTrack) {
dev@406 130 const timeContextChildren = this.timeline.timeContext._children;
dev@406 131 this.waveTrack.remove(layer);
dev@406 132 this.layers.splice(this.layers.indexOf(layer), 1);
dev@406 133 const index = timeContextChildren.indexOf(layer.timeContext);
dev@406 134 if (index >= 0) {
dev@406 135 timeContextChildren.splice(index, 1);
dev@406 136 }
dev@406 137 layer.destroy();
dev@406 138 }
dev@406 139 }
dev@406 140
dev@383 141 private resetTimelineState(): void {
dev@356 142 // time axis
dev@356 143 const timeAxis = new Waves.helpers.TimeAxisLayer({
dev@384 144 height: this.height,
dev@356 145 color: '#b0b0b0'
dev@356 146 });
dev@389 147 this.addLayer(timeAxis, true);
dev@356 148 this.timeline.state = new Waves.states.CenteredZoomState(this.timeline);
dev@364 149 this.timeline.tracks.update(); // TODO this is problematic, shared state across components
dev@354 150 }
dev@354 151
dev@354 152
dev@354 153 // TODO can likely use methods in waves-ui directly
dev@406 154 addLayer(layer: Layer,
dev@406 155 isAxis: boolean = false): LayerRemover {
dev@389 156 const timeContext = this.timeline.timeContext;
dev@354 157 if (!layer.timeContext) {
dev@354 158 layer.setTimeContext(isAxis ?
dev@354 159 timeContext : new Waves.core.LayerTimeContext(timeContext));
dev@354 160 }
dev@389 161 this.waveTrack.add(layer);
dev@354 162 this.layers.push(layer);
dev@354 163 layer.render();
dev@354 164 layer.update();
dev@406 165 return () => this.removeLayer(layer);
dev@354 166 }
dev@354 167
dev@354 168 seekStart(): void {
dev@354 169 this.zoomOnMouseDown = this.timeline.timeContext.zoom;
dev@354 170 this.offsetOnMouseDown = this.timeline.timeContext.offset;
dev@354 171 }
dev@354 172
dev@354 173 seekEnd(x: number): void {
dev@354 174 const hasSameZoom: boolean = this.zoomOnMouseDown ===
dev@354 175 this.timeline.timeContext.zoom;
dev@354 176 const hasSameOffset: boolean = this.offsetOnMouseDown ===
dev@354 177 this.timeline.timeContext.offset;
dev@354 178 if (hasSameZoom && hasSameOffset) {
dev@354 179 this.seek(x);
dev@354 180 }
dev@354 181 }
dev@354 182
dev@354 183 seek(x: number): void {
dev@354 184 if (this.timeline) {
dev@354 185 const timeContext: any = this.timeline.timeContext;
dev@354 186 if (this.onSeek) {
dev@354 187 this.onSeek(timeContext.timeToPixel.invert(x) - timeContext.offset);
dev@354 188 }
dev@354 189 }
dev@354 190 }
dev@354 191 }
dev@389 192
dev@389 193 export abstract class VerticallyBoundedWavesComponent
dev@389 194 <T extends ShapedFeatureData> extends WavesComponent<T>
dev@392 195 implements VerticalScaleRenderer {
dev@389 196 abstract range: [number, number];
dev@389 197
dev@389 198 renderScale(range: [number, number]): void {
dev@389 199 this.addLayer(new Waves.helpers.ScaleLayer({
dev@389 200 tickColor: this.colour,
dev@389 201 textColor: this.colour,
dev@389 202 height: this.height,
dev@389 203 yDomain: range
dev@389 204 }));
dev@389 205 }
dev@389 206 }
dev@392 207
dev@392 208 export abstract class InspectableVerticallyBoundedComponent
dev@392 209 <T extends ShapedFeatureData> extends VerticallyBoundedWavesComponent<T>
dev@392 210 implements VerticalValueInspectorRenderer {
dev@392 211
dev@392 212 private wrappedSeekHandler: OnSeekHandler;
dev@392 213 private highlight: HighlightLayer;
dev@392 214
dev@392 215 @Input() set onSeek(handler: OnSeekHandler) {
dev@392 216 this.wrappedSeekHandler = (x: number) => {
dev@392 217 handler(x);
dev@397 218 this.updatePosition(x);
dev@397 219 };
dev@397 220 }
dev@397 221
dev@397 222 get updatePosition() {
dev@397 223 return (currentTime: number): void => {
dev@394 224 if (this.highlight) {
dev@397 225 this.highlight.currentPosition = currentTime;
dev@394 226 this.highlight.update();
dev@394 227 }
dev@392 228 };
dev@392 229 }
dev@392 230
dev@392 231 get onSeek(): OnSeekHandler {
dev@392 232 return this.wrappedSeekHandler;
dev@392 233 }
dev@392 234
dev@392 235
dev@394 236 renderInspector(range: [number, number], unit?: string): void {
dev@394 237 if (range) {
dev@394 238 this.highlight = new Waves.helpers.HighlightLayer(
dev@394 239 this.cachedFeatureLayers,
dev@394 240 {
dev@394 241 opacity: 0.7,
dev@394 242 height: this.height,
dev@394 243 color: '#c33c54', // TODO pass in?
dev@394 244 labelOffset: 38,
dev@394 245 yDomain: range,
dev@396 246 unit: unit || ''
dev@394 247 }
dev@394 248 );
dev@394 249 this.addLayer(this.highlight);
dev@394 250 }
dev@392 251 }
dev@392 252 }