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