UNPKG

@checksub_team/peaks_timeline

Version:

JavaScript UI component for displaying audio waveforms

256 lines (203 loc) 7.03 kB
/** * @file * * Defines the {@link Axis} class. * * @module axis */ define([ '../utils', './invoker', 'konva' ], function(Utils, Invoker, Konva) { 'use strict'; var LEFT_PADDING = 4; /** * Creates the timeline axis shapes and adds them to the given view layer. * * @class * @alias Axis * * @param {WaveformOverview|View} view * @param {Object} options * @param {String} options.axisGridlineColor * @param {String} options.axisLabelColor */ function Axis(peaks, view, options) { this._view = view; this._invoker = new Invoker(); var self = this; peaks.on('playhead.moved', this._onPlayheadMoved.bind(this)); peaks.on('playhead.hidden', this._onPlayheadHidden.bind(this)); this._axisGridlineColor = options.axisGridlineColor; this._axisLabelColor = options.axisLabelColor; this._backLayer = new Konva.Layer({ listening: false }); this._frontLayer = new Konva.Layer({ listening: false }); this._axisShape = new Konva.Shape({ sceneFunc: function(context) { self.drawAxis(context, view); } }); this._backLayer.add(this._axisShape); this._timesShape = new Konva.Shape({ sceneFunc: function(context) { self.drawTimes(context, view); } }); this._frontLayer.add(this._timesShape); // Throttled draws to prevent excessive redraws. this._throttledBackDraw = this._invoker.throttleTrailing( this._backLayer.batchDraw.bind(this._backLayer) ); this._throttledFrontDraw = this._invoker.throttleTrailing( this._frontLayer.batchDraw.bind(this._frontLayer) ); } Axis.prototype._onPlayheadMoved = function(playheadX, playheadWidth) { this._maskStart = playheadX + this._view.getFrameOffset(); this._maskEnd = playheadX + this._view.getFrameOffset() + playheadWidth; this._throttledFrontDraw(); }; Axis.prototype._onPlayheadHidden = function() { this._maskStart = null; this._maskEnd = null; this._throttledFrontDraw(); }; Axis.prototype.batchDraw = function() { this._throttledBackDraw(); this._throttledFrontDraw(); }; Axis.prototype.addBackToStage = function(stage) { stage.add(this._backLayer); }; Axis.prototype.addFrontToStage = function(stage) { stage.add(this._frontLayer); }; /** * Returns number of seconds for each x-axis marker, appropriate for the * current zoom level, ensuring that markers are not too close together * and that markers are placed at intuitive time intervals (i.e., every 1, * 2, 5, 10, 20, 30 seconds, then every 1, 2, 5, 10, 20, 30 minutes, then * every 1, 2, 5, 10, 20, 30 hours). * * @param {WaveformOverview|WaveformZoomView} view * @returns {Number} */ Axis.prototype.getAxisLabelScale = function(view) { var baseSecs = 1; // seconds var steps = [0.1, 0.5, 1, 2, 5, 10, 20, 30]; var minSpacing = 60; var index = 0; var secs; for (;;) { secs = baseSecs * steps[index]; var pixels = view.timeToPixels(secs); if (pixels < minSpacing) { if (++index === steps.length) { baseSecs *= 60; // seconds -> minutes -> hours index = 0; } } else { break; } } return secs; }; /** * Draws the time axis and labels onto a view. * * @param {Konva.Context} context The context to draw on. * @param {WaveformOverview|WaveformZoomView} view */ Axis.prototype.drawAxis = function(context, view) { var currentFrameStartTime = view.pixelsToTime(view.getFrameOffset()); // Time interval between axis markers (seconds) var axisLabelIntervalSecs = this.getAxisLabelScale(view); // Time of first axis marker (seconds) var firstAxisLabelSecs = Utils.roundUpToNearest(currentFrameStartTime, axisLabelIntervalSecs); // Distance between waveform start time and first axis marker (seconds) var axisLabelOffsetSecs = firstAxisLabelSecs - currentFrameStartTime; // Distance between waveform start time and first axis marker (pixels) var axisLabelOffsetPixels = view.timeToPixels(axisLabelOffsetSecs); context.setAttr('strokeStyle', this._axisGridlineColor); context.setAttr('lineWidth', 1); // Set text style context.setAttr('font', '11px sans-serif'); context.setAttr('fillStyle', this._axisLabelColor); context.setAttr('textAlign', 'left'); context.setAttr('textBaseline', 'bottom'); var secs = firstAxisLabelSecs; var x; var width = view.getWidth(); var height = view.getHeight(); for (;;) { // Position of axis marker (pixels) x = axisLabelOffsetPixels + view.timeToPixels(secs - firstAxisLabelSecs); if (x >= width) { break; } context.beginPath(); context.moveTo(x + 0.5, 0); context.lineTo(x + 0.5, height); context.stroke(); secs += axisLabelIntervalSecs; } }; Axis.prototype.drawTimes = function(context, view) { var currentFrameStartTime = view.pixelsToTime(view.getFrameOffset()); // Time interval between axis markers (seconds) var axisLabelIntervalSecs = this.getAxisLabelScale(view); // Time of first axis marker (seconds) var firstAxisLabelSecs = Utils.roundUpToNearestPositive( currentFrameStartTime - 1, axisLabelIntervalSecs ); // Distance between waveform start time and first axis marker (seconds) var axisLabelOffsetSecs = firstAxisLabelSecs - currentFrameStartTime; // Distance between waveform start time and first axis marker (pixels) var axisLabelOffsetPixels = view.timeToPixels(axisLabelOffsetSecs); // Set text style context.setAttr('font', '11px sans-serif'); context.setAttr('fillStyle', this._axisLabelColor); context.setAttr('textAlign', 'left'); context.setAttr('textBaseline', 'bottom'); var secs = firstAxisLabelSecs; var x; var width = view.getWidth(); for (;;) { // Position of axis marker (pixels) x = axisLabelOffsetPixels + view.timeToPixels(secs - firstAxisLabelSecs); if (x >= width) { break; } var label = Utils.formatTime(secs, false); var measures = context.measureText(label); var labelHeight = measures.actualBoundingBoxAscent - measures.actualBoundingBoxDescent; var labelWidth = measures.width; var labelX = x + LEFT_PADDING; var labelY = labelHeight; if (!this._labelIsMasked( labelX + view.getFrameOffset(), labelX + view.getFrameOffset() + labelWidth )) { context.fillText(label, labelX, labelY); } secs += axisLabelIntervalSecs; } }; Axis.prototype._labelIsMasked = function(labelStart, labelEnd) { if (this._maskStart && this._maskEnd) { if (labelEnd > this._maskStart && labelStart < this._maskEnd) { return true; } } return false; }; return Axis; });