UNPKG

wavesurfer.js

Version:

Interactive navigable audio visualization using Web Audio and Canvas

654 lines (583 loc) 23 kB
/*! * wavesurfer.js 2.0.5 (Sun Mar 04 2018 20:09:58 GMT+0100 (CET)) * https://github.com/katspaugh/wavesurfer.js * @license BSD-3-Clause */ (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define("timeline", [], factory); else if(typeof exports === 'object') exports["timeline"] = factory(); else root["WaveSurfer"] = root["WaveSurfer"] || {}, root["WaveSurfer"]["timeline"] = factory(); })(typeof self !== 'undefined' ? self : this, function() { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = "localhost:8080/dist/plugin/"; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * @typedef {Object} TimelinePluginParams * @desc Extends the `WavesurferParams` wavesurfer was initialised with * @property {!string|HTMLElement} container CSS selector or HTML element where * the timeline should be drawn. This is the only required parameter. * @property {number} notchPercentHeight=90 Height of notches in percent * @property {string} primaryColor='#000' The colour of the main notches * @property {string} secondaryColor='#c0c0c0' The colour of the secondary * notches * @property {string} primaryFontColor='#000' The colour of the labels next to * the main notches * @property {string} secondaryFontColor='#000' The colour of the labels next to * the secondary notches * @property {number} labelPadding=5 The padding between the label and the notch * @property {?number} zoomDebounce A debounce timeout to increase rendering * performance for large files * @property {string} fontFamily='Arial' * @property {number} fontSize=10 Font size of labels in pixels * @property {function} formatTimeCallback=→00:00 * @property {?boolean} deferInit Set to true to manually call * `initPlugin('timeline')` */ /** * Adds a timeline to the waveform. * * @implements {PluginClass} * @extends {Observer} * @example * // es6 * import TimelinePlugin from 'wavesurfer.timeline.js'; * * // commonjs * var TimelinePlugin = require('wavesurfer.timeline.js'); * * // if you are using <script> tags * var TimelinePlugin = window.WaveSurfer.timeline; * * // ... initialising wavesurfer with the plugin * var wavesurfer = WaveSurfer.create({ * // wavesurfer options ... * plugins: [ * TimelinePlugin.create({ * // plugin options ... * }) * ] * }); */ var TimelinePlugin = function () { _createClass(TimelinePlugin, null, [{ key: 'create', /** * Timeline plugin definition factory * * This function must be used to create a plugin definition which can be * used by wavesurfer to correctly instantiate the plugin. * * @param {TimelinePluginParams} params parameters use to initialise the plugin * @return {PluginDefinition} an object representing the plugin */ value: function create(params) { return { name: 'timeline', deferInit: params && params.deferInit ? params.deferInit : false, params: params, instance: TimelinePlugin }; } // event handlers /** @private */ /** @private */ /** @private */ /** @private */ }]); /** * Creates an instance of TimelinePlugin. * * You probably want to use TimelinePlugin.create() * * @param {TimelinePluginParams} params Plugin parameters * @param {object} ws Wavesurfer instance */ function TimelinePlugin(params, ws) { var _this = this; _classCallCheck(this, TimelinePlugin); _initialiseProps.call(this); /** @private */ this.container = 'string' == typeof params.container ? document.querySelector(params.container) : params.container; if (!this.container) { throw new Error('No container for wavesurfer timeline'); } /** @private */ this.wavesurfer = ws; /** @private */ this.util = ws.util; /** @private */ this.params = this.util.extend({}, { height: 20, notchPercentHeight: 90, labelPadding: 5, primaryColor: '#000', secondaryColor: '#c0c0c0', primaryFontColor: '#000', secondaryFontColor: '#000', fontFamily: 'Arial', fontSize: 10, zoomDebounce: false, formatTimeCallback: function formatTimeCallback(seconds) { if (seconds / 60 > 1) { // calculate minutes and seconds from seconds count var minutes = parseInt(seconds / 60, 10); seconds = parseInt(seconds % 60, 10); // fill up seconds with zeroes seconds = seconds < 10 ? '0' + seconds : seconds; return minutes + ':' + seconds; } return Math.round(seconds * 1000) / 1000; }, timeInterval: function timeInterval(pxPerSec) { if (pxPerSec >= 25) { return 1; } else if (pxPerSec * 5 >= 25) { return 5; } else if (pxPerSec * 15 >= 25) { return 15; } return Math.ceil(0.5 / pxPerSec) * 60; }, primaryLabelInterval: function primaryLabelInterval(pxPerSec) { if (pxPerSec >= 25) { return 10; } else if (pxPerSec * 5 >= 25) { return 6; } else if (pxPerSec * 15 >= 25) { return 4; } return 4; }, secondaryLabelInterval: function secondaryLabelInterval(pxPerSec) { if (pxPerSec >= 25) { return 5; } else if (pxPerSec * 5 >= 25) { return 2; } else if (pxPerSec * 15 >= 25) { return 2; } return 2; } }, params); /** @private */ this.canvases = []; /** @private */ this.wrapper = null; /** @private */ this.drawer = null; /** @private */ this.pixelRatio = null; /** @private */ this.maxCanvasWidth = null; /** @private */ this.maxCanvasElementWidth = null; /** * This event handler has to be in the constructor function because it * relies on the debounce function which is only available after * instantiation * * Use a debounced function if zoomDebounce is defined * * @private */ this._onZoom = this.params.zoomDebounce ? this.wavesurfer.util.debounce(function () { return _this.render(); }, this.params.zoomDebounce) : function () { return _this.render(); }; } /** * Initialisation function used by the plugin API */ _createClass(TimelinePlugin, [{ key: 'init', value: function init() { this.wavesurfer.on('ready', this._onReady); // Check if ws is ready if (this.wavesurfer.isReady) { this._onReady(); } } /** * Destroy function used by the plugin API */ }, { key: 'destroy', value: function destroy() { this.unAll(); this.wavesurfer.un('redraw', this._onRedraw); this.wavesurfer.un('zoom', this._onZoom); this.wavesurfer.un('ready', this._onReady); this.wavesurfer.drawer.wrapper.removeEventListener('scroll', this._onScroll); if (this.wrapper && this.wrapper.parentNode) { this.wrapper.removeEventListener('click', this._onWrapperClick); this.wrapper.parentNode.removeChild(this.wrapper); this.wrapper = null; } } /** * Create a timeline element to wrap the canvases drawn by this plugin * * @private */ }, { key: 'createWrapper', value: function createWrapper() { var wsParams = this.wavesurfer.params; this.container.innerHTML = ''; this.wrapper = this.container.appendChild(document.createElement('timeline')); this.util.style(this.wrapper, { display: 'block', position: 'relative', userSelect: 'none', webkitUserSelect: 'none', height: this.params.height + 'px' }); if (wsParams.fillParent || wsParams.scrollParent) { this.util.style(this.wrapper, { width: '100%', overflowX: 'hidden', overflowY: 'hidden' }); } this.wrapper.addEventListener('click', this._onWrapperClick); } /** * Render the timeline (also updates the already rendered timeline) * * @private */ }, { key: 'render', value: function render() { if (!this.wrapper) { this.createWrapper(); } this.updateCanvases(); this.updateCanvasesPositioning(); this.renderCanvases(); } /** * Make sure the correct of timeline canvas elements exist and are cached in * this.canvases * * @private */ }, { key: 'updateCanvases', value: function updateCanvases() { var _this2 = this; var addCanvas = function addCanvas() { var canvas = _this2.wrapper.appendChild(document.createElement('canvas')); _this2.canvases.push(canvas); _this2.util.style(canvas, { position: 'absolute', zIndex: 4 }); }; var removeCanvas = function removeCanvas() { var canvas = _this2.canvases.pop(); canvas.parentElement.removeChild(canvas); }; var totalWidth = Math.round(this.drawer.wrapper.scrollWidth); var requiredCanvases = Math.ceil(totalWidth / this.maxCanvasElementWidth); while (this.canvases.length < requiredCanvases) { addCanvas(); } while (this.canvases.length > requiredCanvases) { removeCanvas(); } } /** * Update the dimensions and positioning style for all the timeline canvases * * @private */ }, { key: 'updateCanvasesPositioning', value: function updateCanvasesPositioning() { var _this3 = this; // cache length for perf var canvasesLength = this.canvases.length; this.canvases.forEach(function (canvas, i) { // canvas width is the max element width, or if it is the last the // required width var canvasWidth = i === canvasesLength - 1 ? _this3.drawer.wrapper.scrollWidth - _this3.maxCanvasElementWidth * (canvasesLength - 1) : _this3.maxCanvasElementWidth; // set dimensions and style canvas.width = canvasWidth * _this3.pixelRatio; // on certain pixel ratios the canvas appears cut off at the bottom, // therefore leave 1px extra canvas.height = (_this3.params.height + 1) * _this3.pixelRatio; _this3.util.style(canvas, { width: canvasWidth + 'px', height: _this3.params.height + 'px', left: i * _this3.maxCanvasElementWidth + 'px' }); }); } /** * Render the timeline labels and notches * * @private */ }, { key: 'renderCanvases', value: function renderCanvases() { var _this4 = this; var duration = this.wavesurfer.backend.getDuration(); if (duration <= 0) { return; } var wsParams = this.wavesurfer.params; var fontSize = this.params.fontSize * wsParams.pixelRatio; var totalSeconds = parseInt(duration, 10) + 1; var width = wsParams.fillParent && !wsParams.scrollParent ? this.drawer.getWidth() : this.drawer.wrapper.scrollWidth * wsParams.pixelRatio; var height1 = this.params.height * this.pixelRatio; var height2 = this.params.height * (this.params.notchPercentHeight / 100) * this.pixelRatio; var pixelsPerSecond = width / duration; var formatTime = this.params.formatTimeCallback; // if parameter is function, call the function with // pixelsPerSecond, otherwise simply take the value as-is var intervalFnOrVal = function intervalFnOrVal(option) { return typeof option === 'function' ? option(pixelsPerSecond) : option; }; var timeInterval = intervalFnOrVal(this.params.timeInterval); var primaryLabelInterval = intervalFnOrVal(this.params.primaryLabelInterval); var secondaryLabelInterval = intervalFnOrVal(this.params.secondaryLabelInterval); var curPixel = 0; var curSeconds = 0; var i = void 0; // build an array of position data with index, second and pixel data, // this is then used multiple times below var positioning = []; for (i = 0; i < totalSeconds / timeInterval; i++) { positioning.push([i, curSeconds, curPixel]); curSeconds += timeInterval; curPixel += pixelsPerSecond * timeInterval; } // iterate over each position var renderPositions = function renderPositions(cb) { positioning.forEach(function (pos) { cb(pos[0], pos[1], pos[2]); }); }; // render primary labels this.setFillStyles(this.params.primaryColor); this.setFonts(fontSize + 'px ' + this.params.fontFamily); this.setFillStyles(this.params.primaryFontColor); renderPositions(function (i, curSeconds, curPixel) { if (i % primaryLabelInterval === 0) { _this4.fillRect(curPixel, 0, 1, height1); _this4.fillText(formatTime(curSeconds), curPixel + _this4.params.labelPadding * _this4.pixelRatio, height1); } }); // render secondary labels this.setFillStyles(this.params.secondaryColor); this.setFonts(fontSize + 'px ' + this.params.fontFamily); this.setFillStyles(this.params.secondaryFontColor); renderPositions(function (i, curSeconds, curPixel) { if (i % secondaryLabelInterval === 0) { _this4.fillRect(curPixel, 0, 1, height1); _this4.fillText(formatTime(curSeconds), curPixel + _this4.params.labelPadding * _this4.pixelRatio, height1); } }); // render the actual notches (when no labels are used) this.setFillStyles(this.params.secondaryColor); renderPositions(function (i, curSeconds, curPixel) { if (i % secondaryLabelInterval !== 0 && i % primaryLabelInterval !== 0) { _this4.fillRect(curPixel, 0, 1, height2); } }); } /** * Set the canvas fill style * * @param {DOMString|CanvasGradient|CanvasPattern} fillStyle * @private */ }, { key: 'setFillStyles', value: function setFillStyles(fillStyle) { this.canvases.forEach(function (canvas) { canvas.getContext('2d').fillStyle = fillStyle; }); } /** * Set the canvas font * * @param {DOMString} font * @private */ }, { key: 'setFonts', value: function setFonts(font) { this.canvases.forEach(function (canvas) { canvas.getContext('2d').font = font; }); } /** * Draw a rectangle on the canvases * * (it figures out the offset for each canvas) * * @param {number} x * @param {number} y * @param {number} width * @param {number} height * @private */ }, { key: 'fillRect', value: function fillRect(x, y, width, height) { var _this5 = this; this.canvases.forEach(function (canvas, i) { var leftOffset = i * _this5.maxCanvasWidth; var intersection = { x1: Math.max(x, i * _this5.maxCanvasWidth), y1: y, x2: Math.min(x + width, i * _this5.maxCanvasWidth + canvas.width), y2: y + height }; if (intersection.x1 < intersection.x2) { canvas.getContext('2d').fillRect(intersection.x1 - leftOffset, intersection.y1, intersection.x2 - intersection.x1, intersection.y2 - intersection.y1); } }); } /** * Fill a given text on the canvases * * @param {string} text * @param {number} x * @param {number} y * @private */ }, { key: 'fillText', value: function fillText(text, x, y) { var textWidth = void 0; var xOffset = 0; this.canvases.forEach(function (canvas) { var context = canvas.getContext('2d'); var canvasWidth = context.canvas.width; if (xOffset > x + textWidth) { return; } if (xOffset + canvasWidth > x) { textWidth = context.measureText(text).width; context.fillText(text, x - xOffset, y); } xOffset += canvasWidth; }); } }]); return TimelinePlugin; }(); var _initialiseProps = function _initialiseProps() { var _this6 = this; this._onScroll = function () { if (_this6.wrapper && _this6.drawer.wrapper) { _this6.wrapper.scrollLeft = _this6.drawer.wrapper.scrollLeft; } }; this._onRedraw = function () { return _this6.render(); }; this._onReady = function () { var ws = _this6.wavesurfer; _this6.drawer = ws.drawer; _this6.pixelRatio = ws.drawer.params.pixelRatio; _this6.maxCanvasWidth = ws.drawer.maxCanvasWidth || ws.drawer.width; _this6.maxCanvasElementWidth = ws.drawer.maxCanvasElementWidth || Math.round(_this6.maxCanvasWidth / _this6.pixelRatio); ws.drawer.wrapper.addEventListener('scroll', _this6._onScroll); ws.on('redraw', _this6._onRedraw); ws.on('zoom', _this6._onZoom); _this6.render(); }; this._onWrapperClick = function (e) { e.preventDefault(); var relX = 'offsetX' in e ? e.offsetX : e.layerX; _this6.fireEvent('click', relX / _this6.wrapper.scrollWidth || 0); }; }; exports.default = TimelinePlugin; module.exports = exports['default']; /***/ }) /******/ ]); }); //# sourceMappingURL=wavesurfer.timeline.js.map