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