playback.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. /**
  2. * UI component that lets the use control auto-slide
  3. * playback via play/pause.
  4. */
  5. export default class Playback {
  6. /**
  7. * @param {HTMLElement} container The component will append
  8. * itself to this
  9. * @param {function} progressCheck A method which will be
  10. * called frequently to get the current playback progress on
  11. * a range of 0-1
  12. */
  13. constructor( container, progressCheck ) {
  14. // Cosmetics
  15. this.diameter = 100;
  16. this.diameter2 = this.diameter/2;
  17. this.thickness = 6;
  18. // Flags if we are currently playing
  19. this.playing = false;
  20. // Current progress on a 0-1 range
  21. this.progress = 0;
  22. // Used to loop the animation smoothly
  23. this.progressOffset = 1;
  24. this.container = container;
  25. this.progressCheck = progressCheck;
  26. this.canvas = document.createElement( 'canvas' );
  27. this.canvas.className = 'playback';
  28. this.canvas.width = this.diameter;
  29. this.canvas.height = this.diameter;
  30. this.canvas.style.width = this.diameter2 + 'px';
  31. this.canvas.style.height = this.diameter2 + 'px';
  32. this.context = this.canvas.getContext( '2d' );
  33. this.container.appendChild( this.canvas );
  34. this.render();
  35. }
  36. setPlaying( value ) {
  37. const wasPlaying = this.playing;
  38. this.playing = value;
  39. // Start repainting if we weren't already
  40. if( !wasPlaying && this.playing ) {
  41. this.animate();
  42. }
  43. else {
  44. this.render();
  45. }
  46. }
  47. animate() {
  48. const progressBefore = this.progress;
  49. this.progress = this.progressCheck();
  50. // When we loop, offset the progress so that it eases
  51. // smoothly rather than immediately resetting
  52. if( progressBefore > 0.8 && this.progress < 0.2 ) {
  53. this.progressOffset = this.progress;
  54. }
  55. this.render();
  56. if( this.playing ) {
  57. requestAnimationFrame( this.animate.bind( this ) );
  58. }
  59. }
  60. /**
  61. * Renders the current progress and playback state.
  62. */
  63. render() {
  64. let progress = this.playing ? this.progress : 0,
  65. radius = ( this.diameter2 ) - this.thickness,
  66. x = this.diameter2,
  67. y = this.diameter2,
  68. iconSize = 28;
  69. // Ease towards 1
  70. this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
  71. const endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) );
  72. const startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) );
  73. this.context.save();
  74. this.context.clearRect( 0, 0, this.diameter, this.diameter );
  75. // Solid background color
  76. this.context.beginPath();
  77. this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false );
  78. this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
  79. this.context.fill();
  80. // Draw progress track
  81. this.context.beginPath();
  82. this.context.arc( x, y, radius, 0, Math.PI * 2, false );
  83. this.context.lineWidth = this.thickness;
  84. this.context.strokeStyle = 'rgba( 255, 255, 255, 0.2 )';
  85. this.context.stroke();
  86. if( this.playing ) {
  87. // Draw progress on top of track
  88. this.context.beginPath();
  89. this.context.arc( x, y, radius, startAngle, endAngle, false );
  90. this.context.lineWidth = this.thickness;
  91. this.context.strokeStyle = '#fff';
  92. this.context.stroke();
  93. }
  94. this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) );
  95. // Draw play/pause icons
  96. if( this.playing ) {
  97. this.context.fillStyle = '#fff';
  98. this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize );
  99. this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize );
  100. }
  101. else {
  102. this.context.beginPath();
  103. this.context.translate( 4, 0 );
  104. this.context.moveTo( 0, 0 );
  105. this.context.lineTo( iconSize - 4, iconSize / 2 );
  106. this.context.lineTo( 0, iconSize );
  107. this.context.fillStyle = '#fff';
  108. this.context.fill();
  109. }
  110. this.context.restore();
  111. }
  112. on( type, listener ) {
  113. this.canvas.addEventListener( type, listener, false );
  114. }
  115. off( type, listener ) {
  116. this.canvas.removeEventListener( type, listener, false );
  117. }
  118. destroy() {
  119. this.playing = false;
  120. if( this.canvas.parentNode ) {
  121. this.container.removeChild( this.canvas );
  122. }
  123. }
  124. }