drawer.multicanvas.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. import Drawer from './drawer';
  2. import * as util from './util';
  3. /**
  4. * @typedef {Object} CanvasEntry
  5. * @private
  6. * @property {HTMLElement} wave The wave node
  7. * @property {CanvasRenderingContext2D} waveCtx The canvas rendering context
  8. * @property {?HTMLElement} progress The progress wave node
  9. * @property {?CanvasRenderingContext2D} progressCtx The progress wave canvas
  10. * rendering context
  11. * @property {?number} start Start of the area the canvas should render, between 0 and 1
  12. * @property {?number} end End of the area the canvas should render, between 0 and 1
  13. */
  14. /**
  15. * MultiCanvas renderer for wavesurfer. Is currently the default and sole built
  16. * in renderer.
  17. */
  18. export default class MultiCanvas extends Drawer {
  19. /**
  20. * @param {HTMLElement} container The container node of the wavesurfer instance
  21. * @param {WavesurferParams} params The wavesurfer initialisation options
  22. */
  23. constructor(container, params) {
  24. super(container, params);
  25. /**
  26. * @type {number}
  27. * @private
  28. */
  29. this.maxCanvasWidth = params.maxCanvasWidth;
  30. /**
  31. * @private
  32. * @type {number}
  33. */
  34. this.maxCanvasElementWidth = Math.round(params.maxCanvasWidth / params.pixelRatio);
  35. /**
  36. * Whether or not the progress wave is renderered. If the `waveColor`
  37. * and `progressColor` are the same colour it is not.
  38. * @type {boolean}
  39. */
  40. this.hasProgressCanvas = params.waveColor != params.progressColor;
  41. /**
  42. * @private
  43. * @type {number}
  44. */
  45. this.halfPixel = 0.5 / params.pixelRatio;
  46. /**
  47. * @private
  48. * @type {Array}
  49. */
  50. this.canvases = [];
  51. /** @private */
  52. this.progressWave = null;
  53. }
  54. /**
  55. * Initialise the drawer
  56. */
  57. init() {
  58. this.createWrapper();
  59. this.createElements();
  60. }
  61. /**
  62. * Create the canvas elements and style them
  63. *
  64. * @private
  65. */
  66. createElements() {
  67. this.progressWave = this.wrapper.appendChild(
  68. this.style(document.createElement('wave'), {
  69. position: 'absolute',
  70. zIndex: 3,
  71. left: 0,
  72. top: 0,
  73. bottom: 0,
  74. overflow: 'hidden',
  75. width: '0',
  76. display: 'none',
  77. boxSizing: 'border-box',
  78. borderRightStyle: 'solid',
  79. borderRightWidth: this.params.cursorWidth + 'px',
  80. borderRightColor: this.params.cursorColor,
  81. pointerEvents: 'none'
  82. })
  83. );
  84. this.addCanvas();
  85. }
  86. /**
  87. * Adjust to the updated size by adding or removing canvases
  88. */
  89. updateSize() {
  90. const totalWidth = Math.round(this.width / this.params.pixelRatio);
  91. const requiredCanvases = Math.ceil(totalWidth / this.maxCanvasElementWidth);
  92. while (this.canvases.length < requiredCanvases) {
  93. this.addCanvas();
  94. }
  95. while (this.canvases.length > requiredCanvases) {
  96. this.removeCanvas();
  97. }
  98. this.canvases.forEach((entry, i) => {
  99. // Add some overlap to prevent vertical white stripes, keep the width even for simplicity.
  100. let canvasWidth = this.maxCanvasWidth + 2 * Math.ceil(this.params.pixelRatio / 2);
  101. if (i == this.canvases.length - 1) {
  102. canvasWidth = this.width - (this.maxCanvasWidth * (this.canvases.length - 1));
  103. }
  104. this.updateDimensions(entry, canvasWidth, this.height);
  105. this.clearWaveForEntry(entry);
  106. });
  107. }
  108. /**
  109. * Add a canvas to the canvas list
  110. *
  111. * @private
  112. */
  113. addCanvas() {
  114. const entry = {};
  115. const leftOffset = this.maxCanvasElementWidth * this.canvases.length;
  116. entry.wave = this.wrapper.appendChild(
  117. this.style(document.createElement('canvas'), {
  118. position: 'absolute',
  119. zIndex: 2,
  120. left: leftOffset + 'px',
  121. top: 0,
  122. bottom: 0,
  123. height: '100%',
  124. pointerEvents: 'none'
  125. })
  126. );
  127. entry.waveCtx = entry.wave.getContext('2d');
  128. if (this.hasProgressCanvas) {
  129. entry.progress = this.progressWave.appendChild(
  130. this.style(document.createElement('canvas'), {
  131. position: 'absolute',
  132. left: leftOffset + 'px',
  133. top: 0,
  134. bottom: 0,
  135. height: '100%'
  136. })
  137. );
  138. entry.progressCtx = entry.progress.getContext('2d');
  139. }
  140. this.canvases.push(entry);
  141. }
  142. /**
  143. * Pop one canvas from the list
  144. *
  145. * @private
  146. */
  147. removeCanvas() {
  148. const lastEntry = this.canvases.pop();
  149. lastEntry.wave.parentElement.removeChild(lastEntry.wave);
  150. if (this.hasProgressCanvas) {
  151. lastEntry.progress.parentElement.removeChild(lastEntry.progress);
  152. }
  153. }
  154. /**
  155. * Update the dimensions of a canvas element
  156. *
  157. * @private
  158. * @param {CanvasEntry} entry
  159. * @param {number} width The new width of the element
  160. * @param {number} height The new height of the element
  161. */
  162. updateDimensions(entry, width, height) {
  163. const elementWidth = Math.round(width / this.params.pixelRatio);
  164. const totalWidth = Math.round(this.width / this.params.pixelRatio);
  165. // Where the canvas starts and ends in the waveform, represented as a decimal between 0 and 1.
  166. entry.start = (entry.waveCtx.canvas.offsetLeft / totalWidth) || 0;
  167. entry.end = entry.start + elementWidth / totalWidth;
  168. entry.waveCtx.canvas.width = width;
  169. entry.waveCtx.canvas.height = height;
  170. this.style(entry.waveCtx.canvas, { width: elementWidth + 'px'});
  171. this.style(this.progressWave, { display: 'block'});
  172. if (this.hasProgressCanvas) {
  173. entry.progressCtx.canvas.width = width;
  174. entry.progressCtx.canvas.height = height;
  175. this.style(entry.progressCtx.canvas, { width: elementWidth + 'px'});
  176. }
  177. }
  178. /**
  179. * Clear the whole waveform
  180. */
  181. clearWave() {
  182. this.canvases.forEach(entry => this.clearWaveForEntry(entry));
  183. }
  184. /**
  185. * Clear one canvas
  186. *
  187. * @private
  188. * @param {CanvasEntry} entry
  189. */
  190. clearWaveForEntry(entry) {
  191. entry.waveCtx.clearRect(0, 0, entry.waveCtx.canvas.width, entry.waveCtx.canvas.height);
  192. if (this.hasProgressCanvas) {
  193. entry.progressCtx.clearRect(0, 0, entry.progressCtx.canvas.width, entry.progressCtx.canvas.height);
  194. }
  195. }
  196. /**
  197. * Draw a waveform with bars
  198. *
  199. * @param {number[]|number[][]} peaks Can also be an array of arrays for split channel
  200. * rendering
  201. * @param {number} channelIndex The index of the current channel. Normally
  202. * should be 0. Must be an integer.
  203. * @param {number} start The x-offset of the beginning of the area that
  204. * should be rendered
  205. * @param {number} end The x-offset of the end of the area that should be
  206. * rendered
  207. */
  208. drawBars(peaks, channelIndex, start, end) {
  209. return this.prepareDraw(peaks, channelIndex, start, end, ({
  210. absmax,
  211. hasMinVals,
  212. height,
  213. offsetY,
  214. halfH
  215. }) => {
  216. // if drawBars was called within ws.empty we don't pass a start and
  217. // don't want anything to happen
  218. if (start === undefined) {
  219. return;
  220. }
  221. // Skip every other value if there are negatives.
  222. const peakIndexScale = hasMinVals ? 2 : 1;
  223. const length = peaks.length / peakIndexScale;
  224. const bar = this.params.barWidth * this.params.pixelRatio;
  225. const gap = Math.max(this.params.pixelRatio, ~~(bar / 2));
  226. const step = bar + gap;
  227. const scale = length / this.width;
  228. const first = start;
  229. const last = end;
  230. let i;
  231. for (i = first; i < last; i += step) {
  232. const peak = peaks[Math.floor(i * scale * peakIndexScale)] || 0;
  233. const h = Math.round(peak / absmax * halfH);
  234. this.fillRect(i + this.halfPixel, halfH - h + offsetY, bar + this.halfPixel, h * 2);
  235. }
  236. });
  237. }
  238. /**
  239. * Draw a waveform
  240. *
  241. * @param {number[]|number[][]} peaks Can also be an array of arrays for split channel
  242. * rendering
  243. * @param {number} channelIndex The index of the current channel. Normally
  244. * should be 0
  245. * @param {number?} start The x-offset of the beginning of the area that
  246. * should be rendered (If this isn't set only a flat line is rendered)
  247. * @param {number?} end The x-offset of the end of the area that should be
  248. * rendered
  249. */
  250. drawWave(peaks, channelIndex, start, end) {
  251. return this.prepareDraw(peaks, channelIndex, start, end, ({
  252. absmax,
  253. hasMinVals,
  254. height,
  255. offsetY,
  256. halfH
  257. }) => {
  258. if (!hasMinVals) {
  259. const reflectedPeaks = [];
  260. const len = peaks.length;
  261. let i;
  262. for (i = 0; i < len; i++) {
  263. reflectedPeaks[2 * i] = peaks[i];
  264. reflectedPeaks[2 * i + 1] = -peaks[i];
  265. }
  266. peaks = reflectedPeaks;
  267. }
  268. // if drawWave was called within ws.empty we don't pass a start and
  269. // end and simply want a flat line
  270. if (start !== undefined) {
  271. this.drawLine(peaks, absmax, halfH, offsetY, start, end);
  272. }
  273. // Always draw a median line
  274. this.fillRect(0, halfH + offsetY - this.halfPixel, this.width, this.halfPixel);
  275. });
  276. }
  277. /**
  278. * Tell the canvas entries to render their portion of the waveform
  279. *
  280. * @private
  281. * @param {number[]} peaks Peak data
  282. * @param {number} absmax Maximum peak value (absolute)
  283. * @param {number} halfH Half the height of the waveform
  284. * @param {number} offsetY Offset to the top
  285. * @param {number} start The x-offset of the beginning of the area that
  286. * should be rendered
  287. * @param {number} end The x-offset of the end of the area that
  288. * should be rendered
  289. */
  290. drawLine(peaks, absmax, halfH, offsetY, start, end) {
  291. this.canvases.forEach(entry => {
  292. this.setFillStyles(entry);
  293. this.drawLineToContext(entry, entry.waveCtx, peaks, absmax, halfH, offsetY, start, end);
  294. this.drawLineToContext(entry, entry.progressCtx, peaks, absmax, halfH, offsetY, start, end);
  295. });
  296. }
  297. /**
  298. * Render the actual waveform line on a canvas
  299. *
  300. * @private
  301. * @param {CanvasEntry} entry
  302. * @param {Canvas2DContextAttributes} ctx Essentially `entry.[wave|progress]Ctx`
  303. * @param {number[]} peaks
  304. * @param {number} absmax Maximum peak value (absolute)
  305. * @param {number} halfH Half the height of the waveform
  306. * @param {number} offsetY Offset to the top
  307. * @param {number} start The x-offset of the beginning of the area that
  308. * should be rendered
  309. * @param {number} end The x-offset of the end of the area that
  310. * should be rendered
  311. */
  312. drawLineToContext(entry, ctx, peaks, absmax, halfH, offsetY, start, end) {
  313. if (!ctx) { return; }
  314. const length = peaks.length / 2;
  315. const scale = (this.params.fillParent && this.width != length)
  316. ? this.width / length
  317. : 1;
  318. const first = Math.round(length * entry.start);
  319. // Use one more peak value to make sure we join peaks at ends -- unless,
  320. // of course, this is the last canvas.
  321. const last = Math.round(length * entry.end) + 1;
  322. if (first > end || last < start) { return; }
  323. const canvasStart = Math.min(first, start);
  324. const canvasEnd = Math.max(last, end);
  325. let i;
  326. let j;
  327. ctx.beginPath();
  328. ctx.moveTo((canvasStart - first) * scale + this.halfPixel, halfH + offsetY);
  329. for (i = canvasStart; i < canvasEnd; i++) {
  330. const peak = peaks[2 * i] || 0;
  331. const h = Math.round(peak / absmax * halfH);
  332. ctx.lineTo((i - first) * scale + this.halfPixel, halfH - h + offsetY);
  333. }
  334. // Draw the bottom edge going backwards, to make a single
  335. // closed hull to fill.
  336. for (j = canvasEnd - 1; j >= canvasStart; j--) {
  337. const peak = peaks[2 * j + 1] || 0;
  338. const h = Math.round(peak / absmax * halfH);
  339. ctx.lineTo((j - first) * scale + this.halfPixel, halfH - h + offsetY);
  340. }
  341. ctx.closePath();
  342. ctx.fill();
  343. }
  344. /**
  345. * Draw a rectangle on the waveform
  346. *
  347. * @param {number} x
  348. * @param {number} y
  349. * @param {number} width
  350. * @param {number} height
  351. */
  352. fillRect(x, y, width, height) {
  353. const startCanvas = Math.floor(x / this.maxCanvasWidth);
  354. const endCanvas = Math.min(
  355. Math.ceil((x + width) / this.maxCanvasWidth) + 1,
  356. this.canvases.length
  357. );
  358. let i;
  359. for (i = startCanvas; i < endCanvas; i++) {
  360. const entry = this.canvases[i];
  361. const leftOffset = i * this.maxCanvasWidth;
  362. const intersection = {
  363. x1: Math.max(x, i * this.maxCanvasWidth),
  364. y1: y,
  365. x2: Math.min(x + width, i * this.maxCanvasWidth + entry.waveCtx.canvas.width),
  366. y2: y + height
  367. };
  368. if (intersection.x1 < intersection.x2) {
  369. this.setFillStyles(entry);
  370. this.fillRectToContext(entry.waveCtx,
  371. intersection.x1 - leftOffset,
  372. intersection.y1,
  373. intersection.x2 - intersection.x1,
  374. intersection.y2 - intersection.y1);
  375. this.fillRectToContext(entry.progressCtx,
  376. intersection.x1 - leftOffset,
  377. intersection.y1,
  378. intersection.x2 - intersection.x1,
  379. intersection.y2 - intersection.y1);
  380. }
  381. }
  382. }
  383. /**
  384. * Performs preparation tasks and calculations which are shared by drawBars and drawWave
  385. *
  386. * @private
  387. * @param {number[]|number[][]} peaks Can also be an array of arrays for split channel
  388. * rendering
  389. * @param {number} channelIndex The index of the current channel. Normally
  390. * should be 0
  391. * @param {number?} start The x-offset of the beginning of the area that
  392. * should be rendered (If this isn't set only a flat line is rendered)
  393. * @param {number?} end The x-offset of the end of the area that should be
  394. * rendered
  395. * @param {function} fn The render function to call
  396. */
  397. prepareDraw(peaks, channelIndex, start, end, fn) {
  398. return util.frame(() => {
  399. // Split channels and call this function with the channelIndex set
  400. if (peaks[0] instanceof Array) {
  401. const channels = peaks;
  402. if (this.params.splitChannels) {
  403. this.setHeight(channels.length * this.params.height * this.params.pixelRatio);
  404. channels.forEach((channelPeaks, i) => this.prepareDraw(channelPeaks, i, start, end, fn));
  405. return;
  406. }
  407. peaks = channels[0];
  408. }
  409. // calculate maximum modulation value, either from the barHeight
  410. // parameter or if normalize=true from the largest value in the peak
  411. // set
  412. let absmax = 1 / this.params.barHeight;
  413. if (this.params.normalize) {
  414. const max = util.max(peaks);
  415. const min = util.min(peaks);
  416. absmax = -min > max ? -min : max;
  417. }
  418. // Bar wave draws the bottom only as a reflection of the top,
  419. // so we don't need negative values
  420. const hasMinVals = [].some.call(peaks, val => val < 0);
  421. const height = this.params.height * this.params.pixelRatio;
  422. const offsetY = height * channelIndex || 0;
  423. const halfH = height / 2;
  424. return fn({
  425. absmax: absmax,
  426. hasMinVals: hasMinVals,
  427. height: height,
  428. offsetY: offsetY,
  429. halfH: halfH
  430. });
  431. })();
  432. }
  433. /**
  434. * Draw the actual rectangle on a canvas
  435. *
  436. * @private
  437. * @param {Canvas2DContextAttributes} ctx
  438. * @param {number} x
  439. * @param {number} y
  440. * @param {number} width
  441. * @param {number} height
  442. */
  443. fillRectToContext(ctx, x, y, width, height) {
  444. if (!ctx) { return; }
  445. ctx.fillRect(x, y, width, height);
  446. }
  447. /**
  448. * Set the fill styles for a certain entry (wave and progress)
  449. *
  450. * @private
  451. * @param {CanvasEntry} entry
  452. */
  453. setFillStyles(entry) {
  454. entry.waveCtx.fillStyle = this.params.waveColor;
  455. if (this.hasProgressCanvas) {
  456. entry.progressCtx.fillStyle = this.params.progressColor;
  457. }
  458. }
  459. /**
  460. * Return image data of the waveform
  461. *
  462. * @param {string} type='image/png' An optional value of a format type.
  463. * @param {number} quality=0.92 An optional value between 0 and 1.
  464. * @return {string|string[]} images A data URL or an array of data URLs
  465. */
  466. getImage(type, quality) {
  467. const images = this.canvases.map(entry => entry.wave.toDataURL(type, quality));
  468. return images.length > 1 ? images : images[0];
  469. }
  470. /**
  471. * Render the new progress
  472. *
  473. * @param {number} position X-Offset of progress position in pixels
  474. */
  475. updateProgress(position) {
  476. this.style(this.progressWave, { width: position + 'px' });
  477. }
  478. }