@checksub_team/peaks_timeline 1.4.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @file
3
+ *
4
+ * Defines the {@link TimelineAxis} class.
5
+ *
6
+ * @module timeline-axis
7
+ */
8
+
9
+ define([
10
+ './utils',
11
+ 'konva'
12
+ ], function(Utils, Konva) {
13
+ 'use strict';
14
+
15
+ var LEFT_PADDING = 4;
16
+
17
+ /**
18
+ * Creates the timeline axis shapes and adds them to the given view layer.
19
+ *
20
+ * @class
21
+ * @alias TimelineAxis
22
+ *
23
+ * @param {WaveformOverview|TimelineZoomView} view
24
+ * @param {Object} options
25
+ * @param {String} options.axisGridlineColor
26
+ * @param {String} options.axisLabelColor
27
+ */
28
+
29
+ function TimelineAxis(peaks, view, options) {
30
+ var self = this;
31
+
32
+ peaks.on('playhead.moved', this._onPlayheadMoved.bind(this));
33
+ peaks.on('playhead.hidden', this._onPlayheadHidden.bind(this));
34
+
35
+ self._axisGridlineColor = options.axisGridlineColor;
36
+ self._axisLabelColor = options.axisLabelColor;
37
+
38
+ self._backLayer = new Konva.Layer({
39
+ listening: false
40
+ });
41
+
42
+ self._frontLayer = new Konva.Layer({
43
+ listening: false
44
+ });
45
+
46
+ self._axisShape = new Konva.Shape({
47
+ sceneFunc: function(context) {
48
+ self.drawAxis(context, view);
49
+ }
50
+ });
51
+ self._backLayer.add(self._axisShape);
52
+
53
+ self._timesShape = new Konva.Shape({
54
+ sceneFunc: function(context) {
55
+ self.drawTimes(context, view);
56
+ }
57
+ });
58
+ self._frontLayer.add(self._timesShape);
59
+ }
60
+
61
+ TimelineAxis.prototype._onPlayheadMoved = function(playheadX, playheadWidth) {
62
+ this._maskStart = playheadX;
63
+ this._maskEnd = playheadX + playheadWidth;
64
+ this._frontLayer.draw();
65
+ };
66
+
67
+ TimelineAxis.prototype._onPlayheadHidden = function() {
68
+ this._maskStart = null;
69
+ this._maskEnd = null;
70
+ this._frontLayer.draw();
71
+ };
72
+
73
+ TimelineAxis.prototype.draw = function() {
74
+ this._backLayer.draw();
75
+ this._frontLayer.draw();
76
+ };
77
+
78
+ TimelineAxis.prototype.addBackToStage = function(stage) {
79
+ stage.add(this._backLayer);
80
+ };
81
+
82
+ TimelineAxis.prototype.addFrontToStage = function(stage) {
83
+ stage.add(this._frontLayer);
84
+ };
85
+
86
+ /**
87
+ * Returns number of seconds for each x-axis marker, appropriate for the
88
+ * current zoom level, ensuring that markers are not too close together
89
+ * and that markers are placed at intuitive time intervals (i.e., every 1,
90
+ * 2, 5, 10, 20, 30 seconds, then every 1, 2, 5, 10, 20, 30 minutes, then
91
+ * every 1, 2, 5, 10, 20, 30 hours).
92
+ *
93
+ * @param {WaveformOverview|WaveformZoomView} view
94
+ * @returns {Number}
95
+ */
96
+
97
+ TimelineAxis.prototype.getAxisLabelScale = function(view) {
98
+ var baseSecs = 1; // seconds
99
+ var steps = [0.1, 0.5, 1, 2, 5, 10, 20, 30];
100
+ var minSpacing = 60;
101
+ var index = 0;
102
+
103
+ var secs;
104
+
105
+ for (;;) {
106
+ secs = baseSecs * steps[index];
107
+ var pixels = view.timeToPixels(secs);
108
+
109
+ if (pixels < minSpacing) {
110
+ if (++index === steps.length) {
111
+ baseSecs *= 60; // seconds -> minutes -> hours
112
+ index = 0;
113
+ }
114
+ }
115
+ else {
116
+ break;
117
+ }
118
+ }
119
+
120
+ return secs;
121
+ };
122
+
123
+ /**
124
+ * Draws the time axis and labels onto a view.
125
+ *
126
+ * @param {Konva.Context} context The context to draw on.
127
+ * @param {WaveformOverview|WaveformZoomView} view
128
+ */
129
+
130
+ TimelineAxis.prototype.drawAxis = function(context, view) {
131
+ var currentFrameStartTime = view.pixelsToTime(view.getFrameOffset());
132
+
133
+ // Time interval between axis markers (seconds)
134
+ var axisLabelIntervalSecs = this.getAxisLabelScale(view);
135
+
136
+ // Time of first axis marker (seconds)
137
+ var firstAxisLabelSecs = Utils.roundUpToNearest(currentFrameStartTime, axisLabelIntervalSecs);
138
+
139
+ // Distance between waveform start time and first axis marker (seconds)
140
+ var axisLabelOffsetSecs = firstAxisLabelSecs - currentFrameStartTime;
141
+
142
+ // Distance between waveform start time and first axis marker (pixels)
143
+ var axisLabelOffsetPixels = view.timeToPixels(axisLabelOffsetSecs);
144
+
145
+ context.setAttr('strokeStyle', this._axisGridlineColor);
146
+ context.setAttr('lineWidth', 1);
147
+
148
+ // Set text style
149
+ context.setAttr('font', '11px sans-serif');
150
+ context.setAttr('fillStyle', this._axisLabelColor);
151
+ context.setAttr('textAlign', 'left');
152
+ context.setAttr('textBaseline', 'bottom');
153
+
154
+ var secs = firstAxisLabelSecs;
155
+ var x;
156
+
157
+ var width = view.getWidth();
158
+ var height = view.getHeight();
159
+
160
+ for (;;) {
161
+ // Position of axis marker (pixels)
162
+ x = axisLabelOffsetPixels + view.timeToPixels(secs - firstAxisLabelSecs);
163
+ if (x >= width) {
164
+ break;
165
+ }
166
+
167
+ context.beginPath();
168
+ context.moveTo(x + 0.5, 0);
169
+ context.lineTo(x + 0.5, height);
170
+ context.stroke();
171
+
172
+ secs += axisLabelIntervalSecs;
173
+ }
174
+ };
175
+
176
+ TimelineAxis.prototype.drawTimes = function(context, view) {
177
+ var currentFrameStartTime = view.pixelsToTime(view.getFrameOffset());
178
+
179
+ // Time interval between axis markers (seconds)
180
+ var axisLabelIntervalSecs = this.getAxisLabelScale(view);
181
+
182
+ // Time of first axis marker (seconds)
183
+ var firstAxisLabelSecs = Utils.roundUpToNearestPositive(
184
+ currentFrameStartTime - 1,
185
+ axisLabelIntervalSecs
186
+ );
187
+
188
+ // Distance between waveform start time and first axis marker (seconds)
189
+ var axisLabelOffsetSecs = firstAxisLabelSecs - currentFrameStartTime;
190
+
191
+ // Distance between waveform start time and first axis marker (pixels)
192
+ var axisLabelOffsetPixels = view.timeToPixels(axisLabelOffsetSecs);
193
+
194
+ // Set text style
195
+ context.setAttr('font', '11px sans-serif');
196
+ context.setAttr('fillStyle', this._axisLabelColor);
197
+ context.setAttr('textAlign', 'left');
198
+ context.setAttr('textBaseline', 'bottom');
199
+
200
+ var secs = firstAxisLabelSecs;
201
+ var x;
202
+
203
+ var width = view.getWidth();
204
+
205
+ for (;;) {
206
+ // Position of axis marker (pixels)
207
+ x = axisLabelOffsetPixels + view.timeToPixels(secs - firstAxisLabelSecs);
208
+
209
+ if (x >= width) {
210
+ break;
211
+ }
212
+
213
+ var label = Utils.formatTime(secs, false);
214
+ var labelHeight = context.measureText(label).actualBoundingBoxAscent
215
+ - context.measureText(label).actualBoundingBoxDescent;
216
+ var labelWidth = context.measureText(label).width;
217
+ var labelX = x + LEFT_PADDING;
218
+ var labelY = labelHeight;
219
+
220
+ if (!this._labelIsMasked(labelX, labelX + labelWidth)) {
221
+ context.fillText(label, labelX, labelY);
222
+ }
223
+
224
+ secs += axisLabelIntervalSecs;
225
+ }
226
+ };
227
+
228
+ TimelineAxis.prototype._labelIsMasked = function(labelStart, labelEnd) {
229
+ if (this._maskStart && this._maskEnd) {
230
+ if (labelEnd > this._maskStart && labelStart < this._maskEnd) {
231
+ return true;
232
+ }
233
+ }
234
+ return false;
235
+ };
236
+
237
+ return TimelineAxis;
238
+ });
@@ -0,0 +1,389 @@
1
+ /**
2
+ * @file
3
+ *
4
+ * Defines the {@link TimelineSegments} class.
5
+ *
6
+ * @module timeline-segments
7
+ */
8
+
9
+ define([
10
+ 'colors.css',
11
+ './segment',
12
+ './utils'
13
+ ], function(Colors, Segment, Utils) {
14
+ 'use strict';
15
+
16
+ /**
17
+ * Segment parameters.
18
+ *
19
+ * @typedef {Object} SegmentOptions
20
+ * @global
21
+ * @property {Number} startTime Segment start time, in seconds.
22
+ * @property {Number} endTime Segment end time, in seconds.
23
+ * @property {Boolean=} editable If <code>true</code> the segment start and
24
+ * end times can be adjusted via the user interface.
25
+ * Default: <code>false</code>.
26
+ * @property {String=} color Segment waveform color.
27
+ * Default: a random color.
28
+ * @property {String=} labelText Segment label text.
29
+ * Default: an empty string.
30
+ * @property {String=} id A unique segment identifier.
31
+ * Default: an automatically generated identifier.
32
+ */
33
+
34
+ /**
35
+ * Handles all functionality related to the adding, removing and manipulation
36
+ * of segments.
37
+ *
38
+ * @class
39
+ * @alias TimelineSegments
40
+ *
41
+ * @param {Peaks} peaks The parent Peaks object.
42
+ */
43
+
44
+ function TimelineSegments(peaks) {
45
+ this._peaks = peaks;
46
+ this._segments = [];
47
+ this._segmentsById = {};
48
+ this._segmentIdCounter = 0;
49
+ this._colorIndex = 0;
50
+ }
51
+
52
+ /**
53
+ * Returns a new unique segment id value.
54
+ *
55
+ * @private
56
+ * @returns {String}
57
+ */
58
+
59
+ TimelineSegments.prototype._getNextSegmentId = function() {
60
+ return 'peaks.segment.' + this._segmentIdCounter++;
61
+ };
62
+
63
+ var colors = [
64
+ Colors.navy,
65
+ Colors.blue,
66
+ Colors.aqua,
67
+ Colors.teal,
68
+ // Colors.olive,
69
+ // Colors.lime,
70
+ // Colors.green,
71
+ Colors.yellow,
72
+ Colors.orange,
73
+ Colors.red,
74
+ Colors.maroon,
75
+ Colors.fuchsia,
76
+ Colors.purple
77
+ // Colors.black,
78
+ // Colors.gray,
79
+ // Colors.silver
80
+ ];
81
+
82
+ /**
83
+ * @private
84
+ * @returns {String}
85
+ */
86
+
87
+ TimelineSegments.prototype._getSegmentColor = function() {
88
+ if (this._peaks.options.randomizeSegmentColor) {
89
+ if (++this._colorIndex === colors.length) {
90
+ this._colorIndex = 0;
91
+ }
92
+
93
+ return colors[this._colorIndex];
94
+ }
95
+ else {
96
+ return this._peaks.options.segmentColor;
97
+ }
98
+ };
99
+
100
+ /**
101
+ * Adds a new segment object.
102
+ *
103
+ * @private
104
+ * @param {Segment} segment
105
+ */
106
+
107
+ TimelineSegments.prototype._addSegment = function(segment) {
108
+ this._segments.push(segment);
109
+
110
+ this._segmentsById[segment.id] = segment;
111
+ };
112
+
113
+ /**
114
+ * Creates a new segment object.
115
+ *
116
+ * @private
117
+ * @param {SegmentOptions} options
118
+ * @return {Segment}
119
+ */
120
+
121
+ TimelineSegments.prototype._createSegment = function(options) {
122
+ // Watch for anyone still trying to use the old
123
+ // createSegment(startTime, endTime, ...) API
124
+ if (!Utils.isObject(options)) {
125
+ // eslint-disable-next-line max-len
126
+ throw new TypeError('peaks.segments.add(): expected a Segment object parameter');
127
+ }
128
+
129
+ var segment = new Segment(
130
+ this._peaks,
131
+ Utils.isNullOrUndefined(options.id) ? this._getNextSegmentId() : options.id,
132
+ options.startTime,
133
+ options.endTime,
134
+ options.labelText,
135
+ options.color || this._getSegmentColor(),
136
+ options.textColor || '#000000',
137
+ options.handleTextColor || '#000000',
138
+ options.opacity || 1,
139
+ true // editable
140
+ );
141
+
142
+ return segment;
143
+ };
144
+
145
+ /**
146
+ * Returns all segments.
147
+ *
148
+ * @returns {Array<Segment>}
149
+ */
150
+
151
+ TimelineSegments.prototype.getSegments = function() {
152
+ return this._segments;
153
+ };
154
+
155
+ /**
156
+ * Add segments to the given line so they can be displayed.
157
+ */
158
+
159
+ TimelineSegments.prototype.addSegmentsToPosition = function(position) {
160
+ this._peaks.emit('segments.show', position);
161
+ };
162
+
163
+ /**
164
+ * Returns the segment with the given id, or <code>null</code> if not found.
165
+ *
166
+ * @param {String} id
167
+ * @returns {Segment|null}
168
+ */
169
+
170
+ TimelineSegments.prototype.getSegment = function(id) {
171
+ return this._segmentsById[id] || null;
172
+ };
173
+
174
+ /**
175
+ * Returns all segments that overlap a given point in time.
176
+ *
177
+ * @param {Number} time
178
+ * @returns {Array<Segment>}
179
+ */
180
+
181
+ TimelineSegments.prototype.getSegmentsAtTime = function(time) {
182
+ return this._segments.filter(function(segment) {
183
+ return time >= segment.startTime && time < segment.endTime;
184
+ });
185
+ };
186
+
187
+ TimelineSegments.prototype.setMagnetizing = function(bool) {
188
+ this._peaks.emit('segments.setMagnetizing', bool);
189
+ };
190
+
191
+ /**
192
+ * Returns all segments that overlap a given time region.
193
+ *
194
+ * @param {Number} startTime The start of the time region, in seconds.
195
+ * @param {Number} endTime The end of the time region, in seconds.
196
+ *
197
+ * @returns {Array<Segment>}
198
+ */
199
+
200
+ TimelineSegments.prototype.find = function(startTime, endTime) {
201
+ return this._segments.filter(function(segment) {
202
+ return segment.isVisible(startTime, endTime);
203
+ });
204
+ };
205
+
206
+ /**
207
+ * Adds one or more segments to the timeline.
208
+ *
209
+ * @param {SegmentOptions|Array<SegmentOptions>} segmentOrSegments
210
+ */
211
+
212
+ TimelineSegments.prototype.add = function(/* segmentOrSegments */) {
213
+ var self = this;
214
+
215
+ var segments = Array.isArray(arguments[0]) ?
216
+ arguments[0] :
217
+ Array.prototype.slice.call(arguments);
218
+
219
+ segments = segments.map(function(segmentOptions) {
220
+ var segment = self._createSegment(segmentOptions);
221
+
222
+ if (Utils.objectHasProperty(self._segmentsById, segment.id)) {
223
+ var seg = Object.values(self._segmentsById)[0];
224
+
225
+ throw new Error('peaks.segments.add(): duplicate id. \n\n Segment I = ' +
226
+ JSON.stringify(
227
+ {
228
+ id: seg.id,
229
+ startTime: seg.startTime,
230
+ endTime: seg.endTime
231
+ }
232
+ ) + ' Segment II = ' +
233
+ JSON.stringify(
234
+ {
235
+ id: segment.id,
236
+ startTime: segment.startTime,
237
+ endTime: segment.endTime
238
+ }
239
+ )
240
+ );
241
+ }
242
+
243
+ return segment;
244
+ });
245
+
246
+ segments.forEach(function(segment) {
247
+ self._addSegment(segment);
248
+ });
249
+
250
+ this._peaks.emit('segments.add', segments);
251
+ };
252
+
253
+ /**
254
+ * Returns the indexes of segments that match the given predicate.
255
+ *
256
+ * @private
257
+ * @param {Function} predicate Predicate function to find matching segments.
258
+ * @returns {Array<Number>} An array of indexes into the segments array of
259
+ * the matching elements.
260
+ */
261
+
262
+ TimelineSegments.prototype._findSegment = function(predicate) {
263
+ var indexes = [];
264
+
265
+ for (var i = 0, length = this._segments.length; i < length; i++) {
266
+ if (predicate(this._segments[i])) {
267
+ indexes.push(i);
268
+ }
269
+ }
270
+
271
+ return indexes;
272
+ };
273
+
274
+ /**
275
+ * Removes the segments at the given array indexes.
276
+ *
277
+ * @private
278
+ * @param {Array<Number>} indexes The array indexes to remove.
279
+ * @returns {Array<Segment>} The removed {@link Segment} objects.
280
+ */
281
+
282
+ TimelineSegments.prototype._removeIndexes = function(indexes) {
283
+ var removed = [];
284
+
285
+ for (var i = 0; i < indexes.length; i++) {
286
+ var index = indexes[i] - removed.length;
287
+
288
+ var itemRemoved = this._segments.splice(index, 1)[0];
289
+
290
+ delete this._segmentsById[itemRemoved.id];
291
+
292
+ removed.push(itemRemoved);
293
+ }
294
+
295
+ return removed;
296
+ };
297
+
298
+ /**
299
+ * Removes all segments that match a given predicate function.
300
+ *
301
+ * After removing the segments, this function also emits a
302
+ * <code>segments.remove</code> event with the removed {@link Segment}
303
+ * objects.
304
+ *
305
+ * @private
306
+ * @param {Function} predicate A predicate function that identifies which
307
+ * segments to remove.
308
+ * @returns {Array<Segment>} The removed {@link Segment} objects.
309
+ */
310
+
311
+ TimelineSegments.prototype._removeSegments = function(predicate) {
312
+ var indexes = this._findSegment(predicate);
313
+
314
+ var removed = this._removeIndexes(indexes);
315
+
316
+ this._peaks.emit('segments.remove', removed);
317
+
318
+ return removed;
319
+ };
320
+
321
+ /**
322
+ * Removes the given segment.
323
+ *
324
+ * @param {Segment} segment The segment to remove.
325
+ * @returns {Array<Segment>} The removed segment.
326
+ */
327
+
328
+ TimelineSegments.prototype.remove = function(segment) {
329
+ return this._removeSegments(function(s) {
330
+ return s === segment;
331
+ });
332
+ };
333
+
334
+ /**
335
+ * Removes any segments with the given id.
336
+ *
337
+ * @param {String} id
338
+ * @returns {Array<Segment>} The removed {@link Segment} objects.
339
+ */
340
+
341
+ TimelineSegments.prototype.removeById = function(segmentId) {
342
+ return this._removeSegments(function(segment) {
343
+ return segment.id === segmentId;
344
+ });
345
+ };
346
+
347
+ /**
348
+ * Removes any segments with the given start time, and optional end time.
349
+ *
350
+ * @param {Number} startTime Segments with this start time are removed.
351
+ * @param {Number?} endTime If present, only segments with both the given
352
+ * start time and end time are removed.
353
+ * @returns {Array<Segment>} The removed {@link Segment} objects.
354
+ */
355
+
356
+ TimelineSegments.prototype.removeByTime = function(startTime, endTime) {
357
+ endTime = (typeof endTime === 'number') ? endTime : 0;
358
+
359
+ var fnFilter;
360
+
361
+ if (endTime > 0) {
362
+ fnFilter = function(segment) {
363
+ return segment.startTime === startTime && segment.endTime === endTime;
364
+ };
365
+ }
366
+ else {
367
+ fnFilter = function(segment) {
368
+ return segment.startTime === startTime;
369
+ };
370
+ }
371
+
372
+ return this._removeSegments(fnFilter);
373
+ };
374
+
375
+ /**
376
+ * Removes all segments.
377
+ *
378
+ * After removing the segments, this function emits a
379
+ * <code>segments.remove_all</code> event.
380
+ */
381
+
382
+ TimelineSegments.prototype.removeAll = function() {
383
+ this._segments = [];
384
+ this._segmentsById = {};
385
+ this._peaks.emit('segments.remove_all');
386
+ };
387
+
388
+ return TimelineSegments;
389
+ });