drawer.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. import * as util from './util';
  2. /**
  3. * Parent class for renderers
  4. *
  5. * @extends {Observer}
  6. */
  7. export default class Drawer extends util.Observer {
  8. /**
  9. * @param {HTMLElement} container The container node of the wavesurfer instance
  10. * @param {WavesurferParams} params The wavesurfer initialisation options
  11. */
  12. constructor(container, params) {
  13. super();
  14. /** @private */
  15. this.container = container;
  16. /**
  17. * @type {WavesurferParams}
  18. * @private
  19. */
  20. this.params = params;
  21. /**
  22. * The width of the renderer
  23. * @type {number}
  24. */
  25. this.width = 0;
  26. /**
  27. * The height of the renderer
  28. * @type {number}
  29. */
  30. this.height = params.height * this.params.pixelRatio;
  31. /** @private */
  32. this.lastPos = 0;
  33. /**
  34. * The `<wave>` element which is added to the container
  35. * @type {HTMLElement}
  36. */
  37. this.wrapper = null;
  38. }
  39. /**
  40. * Alias of `util.style`
  41. *
  42. * @param {HTMLElement} el The element that the styles will be applied to
  43. * @param {Object} styles The map of propName: attribute, both are used as-is
  44. * @return {HTMLElement} el
  45. */
  46. style(el, styles) {
  47. return util.style(el, styles);
  48. }
  49. /**
  50. * Create the wrapper `<wave>` element, style it and set up the events for
  51. * interaction
  52. */
  53. createWrapper() {
  54. this.wrapper = this.container.appendChild(
  55. document.createElement('wave')
  56. );
  57. this.style(this.wrapper, {
  58. display: 'block',
  59. position: 'relative',
  60. userSelect: 'none',
  61. webkitUserSelect: 'none',
  62. height: this.params.height + 'px'
  63. });
  64. if (this.params.fillParent || this.params.scrollParent) {
  65. this.style(this.wrapper, {
  66. width: '100%',
  67. overflowX: this.params.hideScrollbar ? 'hidden' : 'auto',
  68. overflowY: 'hidden'
  69. });
  70. }
  71. this.setupWrapperEvents();
  72. }
  73. /**
  74. * Handle click event
  75. *
  76. * @param {Event} e Click event
  77. * @param {?boolean} noPrevent Set to true to not call `e.preventDefault()`
  78. * @return {number} Playback position from 0 to 1
  79. */
  80. handleEvent(e, noPrevent) {
  81. !noPrevent && e.preventDefault();
  82. const clientX = e.targetTouches ? e.targetTouches[0].clientX : e.clientX;
  83. const bbox = this.wrapper.getBoundingClientRect();
  84. const nominalWidth = this.width;
  85. const parentWidth = this.getWidth();
  86. let progress;
  87. if (!this.params.fillParent && nominalWidth < parentWidth) {
  88. progress = ((clientX - bbox.left) * this.params.pixelRatio / nominalWidth) || 0;
  89. if (progress > 1) {
  90. progress = 1;
  91. }
  92. } else {
  93. progress = ((clientX - bbox.left + this.wrapper.scrollLeft) / this.wrapper.scrollWidth) || 0;
  94. }
  95. return progress;
  96. }
  97. /**
  98. * @private
  99. */
  100. setupWrapperEvents() {
  101. this.wrapper.addEventListener('click', e => {
  102. const scrollbarHeight = this.wrapper.offsetHeight - this.wrapper.clientHeight;
  103. if (scrollbarHeight != 0) {
  104. // scrollbar is visible. Check if click was on it
  105. const bbox = this.wrapper.getBoundingClientRect();
  106. if (e.clientY >= bbox.bottom - scrollbarHeight) {
  107. // ignore mousedown as it was on the scrollbar
  108. return;
  109. }
  110. }
  111. if (this.params.interact) {
  112. this.fireEvent('click', e, this.handleEvent(e));
  113. }
  114. });
  115. this.wrapper.addEventListener('scroll', e => this.fireEvent('scroll', e));
  116. }
  117. /**
  118. * Draw peaks on the canvas
  119. *
  120. * @param {number[]|number[][]} peaks Can also be an array of arrays for split channel
  121. * rendering
  122. * @param {number} length The width of the area that should be drawn
  123. * @param {number} start The x-offset of the beginning of the area that
  124. * should be rendered
  125. * @param {number} end The x-offset of the end of the area that should be
  126. * rendered
  127. */
  128. drawPeaks(peaks, length, start, end) {
  129. if (!this.setWidth(length)) {
  130. this.clearWave();
  131. }
  132. this.params.barWidth ?
  133. this.drawBars(peaks, 0, start, end) :
  134. this.drawWave(peaks, 0, start, end);
  135. }
  136. /**
  137. * Scroll to the beginning
  138. */
  139. resetScroll() {
  140. if (this.wrapper !== null) {
  141. this.wrapper.scrollLeft = 0;
  142. }
  143. }
  144. /**
  145. * Recenter the viewport at a certain percent of the waveform
  146. *
  147. * @param {number} percent Value from 0 to 1 on the waveform
  148. */
  149. recenter(percent) {
  150. const position = this.wrapper.scrollWidth * percent;
  151. this.recenterOnPosition(position, true);
  152. }
  153. /**
  154. * Recenter the viewport on a position, either scroll there immediately or
  155. * in steps of 5 pixels
  156. *
  157. * @param {number} position X-offset in pixels
  158. * @param {boolean} immediate Set to true to immediately scroll somewhere
  159. */
  160. recenterOnPosition(position, immediate) {
  161. const scrollLeft = this.wrapper.scrollLeft;
  162. const half = ~~(this.wrapper.clientWidth / 2);
  163. const maxScroll = this.wrapper.scrollWidth - this.wrapper.clientWidth;
  164. let target = position - half;
  165. let offset = target - scrollLeft;
  166. if (maxScroll == 0) {
  167. // no need to continue if scrollbar is not there
  168. return;
  169. }
  170. // if the cursor is currently visible...
  171. if (!immediate && -half <= offset && offset < half) {
  172. // we'll limit the "re-center" rate.
  173. const rate = 5;
  174. offset = Math.max(-rate, Math.min(rate, offset));
  175. target = scrollLeft + offset;
  176. }
  177. // limit target to valid range (0 to maxScroll)
  178. target = Math.max(0, Math.min(maxScroll, target));
  179. // no use attempting to scroll if we're not moving
  180. if (target != scrollLeft) {
  181. this.wrapper.scrollLeft = target;
  182. }
  183. }
  184. /**
  185. * Get the current scroll position in pixels
  186. *
  187. * @return {number}
  188. */
  189. getScrollX() {
  190. return Math.round(this.wrapper.scrollLeft * this.params.pixelRatio);
  191. }
  192. /**
  193. * Get the width of the container
  194. *
  195. * @return {number}
  196. */
  197. getWidth() {
  198. return Math.round(this.container.clientWidth * this.params.pixelRatio);
  199. }
  200. /**
  201. * Set the width of the container
  202. *
  203. * @param {number} width
  204. */
  205. setWidth(width) {
  206. if (this.width == width) {
  207. return false;
  208. }
  209. this.width = width;
  210. if (this.params.fillParent || this.params.scrollParent) {
  211. this.style(this.wrapper, {
  212. width: ''
  213. });
  214. } else {
  215. this.style(this.wrapper, {
  216. width: ~~(this.width / this.params.pixelRatio) + 'px'
  217. });
  218. }
  219. this.updateSize();
  220. return true;
  221. }
  222. /**
  223. * Set the height of the container
  224. *
  225. * @param {number} height
  226. */
  227. setHeight(height) {
  228. if (height == this.height) {
  229. return false;
  230. }
  231. this.height = height;
  232. this.style(this.wrapper, {
  233. height: ~~(this.height / this.params.pixelRatio) + 'px'
  234. });
  235. this.updateSize();
  236. return true;
  237. }
  238. /**
  239. * Called by wavesurfer when progress should be renderered
  240. *
  241. * @param {number} progress From 0 to 1
  242. */
  243. progress(progress) {
  244. const minPxDelta = 1 / this.params.pixelRatio;
  245. const pos = Math.round(progress * this.width) * minPxDelta;
  246. if (pos < this.lastPos || pos - this.lastPos >= minPxDelta) {
  247. this.lastPos = pos;
  248. if (this.params.scrollParent && this.params.autoCenter) {
  249. const newPos = ~~(this.wrapper.scrollWidth * progress);
  250. this.recenterOnPosition(newPos);
  251. }
  252. this.updateProgress(pos);
  253. }
  254. }
  255. /**
  256. * This is called when wavesurfer is destroyed
  257. */
  258. destroy() {
  259. this.unAll();
  260. if (this.wrapper) {
  261. if (this.wrapper.parentNode == this.container) {
  262. this.container.removeChild(this.wrapper);
  263. }
  264. this.wrapper = null;
  265. }
  266. }
  267. /* Renderer-specific methods */
  268. /**
  269. * Called when the size of the container changes so the renderer can adjust
  270. *
  271. * @abstract
  272. */
  273. updateSize() {}
  274. /**
  275. * Draw a waveform with bars
  276. *
  277. * @abstract
  278. * @param {number[]|number[][]} peaks Can also be an array of arrays for split channel
  279. * rendering
  280. * @param {number} channelIndex The index of the current channel. Normally
  281. * should be 0
  282. * @param {number} start The x-offset of the beginning of the area that
  283. * should be rendered
  284. * @param {number} end The x-offset of the end of the area that should be
  285. * rendered
  286. */
  287. drawBars(peaks, channelIndex, start, end) {}
  288. /**
  289. * Draw a waveform
  290. *
  291. * @abstract
  292. * @param {number[]|number[][]} peaks Can also be an array of arrays for split channel
  293. * rendering
  294. * @param {number} channelIndex The index of the current channel. Normally
  295. * should be 0
  296. * @param {number} start The x-offset of the beginning of the area that
  297. * should be rendered
  298. * @param {number} end The x-offset of the end of the area that should be
  299. * rendered
  300. */
  301. drawWave(peaks, channelIndex, start, end) {}
  302. /**
  303. * Clear the waveform
  304. *
  305. * @abstract
  306. */
  307. clearWave() {}
  308. /**
  309. * Render the new progress
  310. *
  311. * @abstract
  312. * @param {number} position X-Offset of progress position in pixels
  313. */
  314. updateProgress(position) {}
  315. }