/** * UI component that lets the use control auto-slide * playback via play/pause. */ export default class Playback { /** * @param {HTMLElement} container The component will append * itself to this * @param {function} progressCheck A method which will be * called frequently to get the current playback progress on * a range of 0-1 */ constructor( container, progressCheck ) { // Cosmetics this.diameter = 100; this.diameter2 = this.diameter/2; this.thickness = 6; // Flags if we are currently playing this.playing = false; // Current progress on a 0-1 range this.progress = 0; // Used to loop the animation smoothly this.progressOffset = 1; this.container = container; this.progressCheck = progressCheck; this.canvas = document.createElement( 'canvas' ); this.canvas.className = 'playback'; this.canvas.width = this.diameter; this.canvas.height = this.diameter; this.canvas.style.width = this.diameter2 + 'px'; this.canvas.style.height = this.diameter2 + 'px'; this.context = this.canvas.getContext( '2d' ); this.container.appendChild( this.canvas ); this.render(); } setPlaying( value ) { const wasPlaying = this.playing; this.playing = value; // Start repainting if we weren't already if( !wasPlaying && this.playing ) { this.animate(); } else { this.render(); } } animate() { const progressBefore = this.progress; this.progress = this.progressCheck(); // When we loop, offset the progress so that it eases // smoothly rather than immediately resetting if( progressBefore > 0.8 && this.progress < 0.2 ) { this.progressOffset = this.progress; } this.render(); if( this.playing ) { requestAnimationFrame( this.animate.bind( this ) ); } } /** * Renders the current progress and playback state. */ render() { let progress = this.playing ? this.progress : 0, radius = ( this.diameter2 ) - this.thickness, x = this.diameter2, y = this.diameter2, iconSize = 28; // Ease towards 1 this.progressOffset += ( 1 - this.progressOffset ) * 0.1; const endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) ); const startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) ); this.context.save(); this.context.clearRect( 0, 0, this.diameter, this.diameter ); // Solid background color this.context.beginPath(); this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false ); this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )'; this.context.fill(); // Draw progress track this.context.beginPath(); this.context.arc( x, y, radius, 0, Math.PI * 2, false ); this.context.lineWidth = this.thickness; this.context.strokeStyle = 'rgba( 255, 255, 255, 0.2 )'; this.context.stroke(); if( this.playing ) { // Draw progress on top of track this.context.beginPath(); this.context.arc( x, y, radius, startAngle, endAngle, false ); this.context.lineWidth = this.thickness; this.context.strokeStyle = '#fff'; this.context.stroke(); } this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) ); // Draw play/pause icons if( this.playing ) { this.context.fillStyle = '#fff'; this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize ); this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize ); } else { this.context.beginPath(); this.context.translate( 4, 0 ); this.context.moveTo( 0, 0 ); this.context.lineTo( iconSize - 4, iconSize / 2 ); this.context.lineTo( 0, iconSize ); this.context.fillStyle = '#fff'; this.context.fill(); } this.context.restore(); } on( type, listener ) { this.canvas.addEventListener( type, listener, false ); } off( type, listener ) { this.canvas.removeEventListener( type, listener, false ); } destroy() { this.playing = false; if( this.canvas.parentNode ) { this.container.removeChild( this.canvas ); } } }