Move the visualizer to a React component (#181)

This commit is contained in:
Jordan Eldredge 2016-08-01 19:30:22 -07:00 committed by GitHub
parent 72734c6f04
commit bc1ffb29c3
8 changed files with 220 additions and 218 deletions

View file

@ -67,7 +67,7 @@ const MainWindow = (props) => {
<div id='play-pause'></div>
<div id='work-indicator' className={props.display.working ? 'selected' : ''}></div>
<Time />
<Visualizer />
<Visualizer analyser={props.mediaPlayer._analyser}/>
</div>
<div className='media-info'>
<Marquee />

View file

@ -1,23 +1,201 @@
import React from 'react';
import {connect} from 'react-redux';
const OSCILLOSCOPE = 1;
const BAR = 2;
// Return the average value in a slice of dataArray
function sliceAverage(dataArray, sliceWidth, sliceNumber) {
var start = sliceWidth * sliceNumber;
var end = start + sliceWidth;
var sum = 0;
for (var i = start; i < end; i++) {
sum += dataArray[i];
}
return sum / sliceWidth;
}
class Visualizer extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.props.dispatch({type: 'TOGGLE_VISUALIZER'});
componentDidMount() {
this.canvasCtx = this.refs.canvas.getContext('2d');
this.canvasCtx.imageSmoothingEnabled = false;
this.width = this.refs.canvas.width * 1; // Cast to int
this.height = this.refs.canvas.height * 1; // Cast to int
// Off-screen canvas for pre-rendering the background
this.bgCanvas = document.createElement('canvas');
this.bgCanvas.width = this.width;
this.bgCanvas.height = this.height;
// Off-screen canvas for pre-rendering a single bar gradient
this.barCanvas = document.createElement('canvas');
this.barCanvas.width = 6;
this.barCanvas.height = 32;
this.setStyle();
// Kick off the animation loop
const loop = () => {
if (this.props.status === 'PLAYING') {
this.paintFrame();
}
window.requestAnimationFrame(loop);
};
loop();
}
componentDidUpdate() {
this.setStyle();
}
setStyle() {
// TODO: Split this into to methods. One for skin update, one for style
// update.
this.preRenderBg();
this.preRenderBar();
if (this.props.style === OSCILLOSCOPE) {
this.props.analyser.fftSize = 2048;
this.bufferLength = this.props.analyser.fftSize;
this.dataArray = new Uint8Array(this.bufferLength);
} else if (this.props.style === BAR) {
this.props.analyser.fftSize = 64; // Must be a power of two
// Number of bins/bars we get
this.bufferLength = this.props.analyser.frequencyBinCount;
this.dataArray = new Uint8Array(this.bufferLength);
}
// If we are paused when the skin changes, we will keep the vis colors
// until we paint again. For now we can just clear the current frame so
// we don't end up with a clashing visual.
// TODO: Once this is split, alwasy clear on skin change
if (this.props.status === 'PLAYING') {
this.clear();
}
}
clear() {
this.canvasCtx.drawImage(this.bgCanvas, 0, 0);
}
// Pre-render the background grid
preRenderBg() {
var bgCanvasCtx = this.bgCanvas.getContext('2d');
bgCanvasCtx.fillStyle = this.props.colors[0];
bgCanvasCtx.fillRect(0, 0, this.width, this.height);
bgCanvasCtx.fillStyle = this.props.colors[1];
for (var x = 0; x < this.width; x += 4) {
for (var y = 2; y < this.height; y += 4) {
bgCanvasCtx.fillRect(x, y, 2, 2);
}
}
}
// Pre-render the bar gradient
preRenderBar() {
var barCanvasCtx = this.barCanvas.getContext('2d');
barCanvasCtx.fillStyle = this.props.colors[23];
barCanvasCtx.fillRect(0, 0, 6, 2);
for (var i = 0; i <= 15; i++) {
var colorNumber = 17 - i;
barCanvasCtx.fillStyle = this.props.colors[colorNumber];
var y = 32 - (i * 2);
barCanvasCtx.fillRect(0, y, 6, 2);
}
}
paintFrame() {
this.clear();
if (this.props.style === OSCILLOSCOPE) {
this._paintOscilloscopeFrame();
} else if (this.props.style === BAR) {
this._paintBarFrame();
}
}
_paintOscilloscopeFrame() {
this.props.analyser.getByteTimeDomainData(this.dataArray);
// 2 because we're shrinking the canvas by 2
this.canvasCtx.lineWidth = 2;
// Just use one of the viscolors for now
this.canvasCtx.strokeStyle = this.props.colors[18];
// Since dataArray has more values than we have pixels to display, we
// have to average several dataArray values per pixel. We call these
// groups slices.
//
// We use the 2x scale here since we only want to plot values for
// "real" pixels.
var sliceWidth = Math.floor(this.bufferLength / this.width) * 2;
// The max amplitude is half the height
var h = this.height / 2;
this.canvasCtx.beginPath();
// Iterate over the width of the canvas in "real" pixels.
for (var j = 0; j <= this.width / 2; j++) {
var amplitude = sliceAverage(this.dataArray, sliceWidth, j);
var percentAmplitude = amplitude / 128; // dataArray gives us bytes
var y = percentAmplitude * h;
var x = j * 2;
// Canvas coordinates are in the middle of the pixel by default.
// When we want to draw pixel perfect lines, we will need to
// account for that here
if (x === 0) {
this.canvasCtx.moveTo(x, y);
} else {
this.canvasCtx.lineTo(x, y);
}
}
this.canvasCtx.stroke();
}
_printBar(x, height) {
height = Math.round(height) * 2;
if (height > 0) {
var y = 32 - height;
// Draw the gray peak line
this.canvasCtx.drawImage(this.barCanvas, 0, 0, 6, 2, x, y - 2, 6, 2);
// Draw the gradient
this.canvasCtx.drawImage(this.barCanvas, 0, y, 6, height, x, y, 6, height);
}
}
_paintBarFrame() {
this.props.analyser.getByteFrequencyData(this.dataArray);
for (var j = 0; j < this.bufferLength; j++) {
var height = this.dataArray[j] * (14 / 256);
this._printBar(j * 8, height);
}
}
handleClick() {
this.props.dispatch({type: 'TOGGLE_VISUALIZER_STYLE'});
}
render() {
// TODO: Don't rerender DOM on style updates
return <canvas
onClick={this.handleClick}
id='visualizer'
ref='canvas'
width='152'
height='32'
onClick={this.handleClick}
/>;
}
}
module.exports = connect((state) => state)(Visualizer);
const mapStateToProps = (state) => {
return {...state.visualizer, status: state.media.status};
};
module.exports = connect(mapStateToProps)(Visualizer);

View file

@ -4,10 +4,10 @@ export function play(mediaPlayer) {
return (dispatch, getState) => {
if (getState().media.status === 'PLAYING') {
mediaPlayer.stop();
dispatch({type: 'MEDIA_IS_STOPPED'});
dispatch({type: 'SET_MEDIA_STATUS', status: 'STOPPED'});
} else {
mediaPlayer.play();
dispatch({type: 'MEDIA_IS_PLAYING'});
dispatch({type: 'SET_MEDIA_STATUS', status: 'PLAYING'});
}
};
}
@ -18,11 +18,11 @@ export function pause(mediaPlayer) {
switch (status) {
case 'PAUSED':
mediaPlayer.play();
dispatch({type: 'MEDIA_IS_PLAYING'});
dispatch({type: 'SET_MEDIA_STATUS', status: 'PLAYING'});
break;
case 'PLAYING':
mediaPlayer.pause();
dispatch({type: 'MEDIA_IS_PAUSED'});
dispatch({type: 'SET_MEDIA_STATUS', status: 'PAUSED'});
break;
}
};
@ -31,7 +31,7 @@ export function pause(mediaPlayer) {
export function stop(mediaPlayer) {
return (dispatch) => {
mediaPlayer.stop();
dispatch({type: 'MEDIA_IS_STOPPED'});
dispatch({type: 'SET_MEDIA_STATUS', status: 'STOPPED'});
};
}

View file

@ -232,11 +232,6 @@ module.exports = {
if (this._playing) {
this._updatePosition();
this._callbacks.timeupdate();
// _updatePosition might have stopped the playing
if (this._playing) {
this._callbacks.visualizerupdate(this._analyser);
}
}
window.requestAnimationFrame(this._draw.bind(this));
},

View file

@ -19,6 +19,23 @@ const userInput = (state, action) => {
}
};
const visualizer = (state, action) => {
if (!state) {
return {
colors: [],
style: 2
};
}
switch (action.type) {
case 'SET_VISUALIZATION_COLORS':
return {...state, colors: action.colors};
case 'TOGGLE_VISUALIZER_STYLE':
return {...state, style: (state.style + 1) % 3};
default:
return state;
}
};
const display = (state, action) => {
if (!state) {
return {
@ -114,12 +131,8 @@ const media = (state, action) => {
return {...state, repeat: !state.repeat};
case 'TOGGLE_SHUFFLE':
return {...state, shuffle: !state.shuffle};
case 'MEDIA_IS_PLAYING':
return {...state, status: 'PLAYING'};
case 'MEDIA_IS_PAUSED':
return {...state, status: 'PAUSED'};
case 'MEDIA_IS_STOPPED':
return {...state, status: 'STOPPED'};
case 'SET_MEDIA_STATUS':
return {...state, status: action.status};
default:
return state;
}
@ -130,6 +143,7 @@ const createReducer = (winamp) => {
const reducer = combineReducers({
userInput,
display,
visualizer,
contextMenu,
media
});
@ -154,9 +168,6 @@ const createReducer = (winamp) => {
case 'TOGGLE_SHUFFLE':
winamp.toggleShuffle();
return state;
case 'TOGGLE_VISUALIZER':
winamp.toggleVisualizer();
return state;
default:
return state;
}

View file

@ -1,12 +1,10 @@
// Dynamically set the css background images for all the sprites
import SKIN_SPRITES from './skin-sprites';
import Visualizer from './visualizer';
import JSZip from '../node_modules/jszip/dist/jszip'; // Hack
module.exports = {
init: function(visualizerNode, analyser) {
init: function() {
this._createNewStyleNode();
this.visualizer = Visualizer.init(visualizerNode, analyser);
return this;
},
@ -38,7 +36,7 @@ module.exports = {
this.styleNode.appendChild(document.createTextNode(cssRules));
this._parseVisColors(zip);
if (this.completedCallback !== void 0) {
this.completedCallback();
this.completedCallback(this.colors);
}
}.bind(this));
},
@ -58,7 +56,7 @@ module.exports = {
console.error('Error in VISCOLOR.TXT on line', i);
}
}
this.visualizer.setColors(colors);
this.colors = colors;
},
_findFileInZip: function(name, zip) {

View file

@ -1,163 +0,0 @@
/* Use Canvas to recreate the simple Winamp visualizer */
module.exports = {
init: function(canvasNode, analyser) {
this.canvas = canvasNode;
this.analyser = analyser;
this.canvasCtx = this.canvas.getContext('2d');
this.canvasCtx.imageSmoothingEnabled = false;
this.width = this.canvas.width * 1; // Cast to int
this.height = this.canvas.height * 1; // Cast to int
this.colors = []; // skin.js fills this from viscolors.txt
this.NONE = 0;
this.OSCILLOSCOPE = 1;
this.BAR = 2;
this.bufferLength = null;
this.dataArray = null;
this.setStyle(this.BAR);
// Off-screen canvas for pre-rendering the background
this.bgCanvas = document.createElement('canvas');
this.bgCanvas.width = this.width;
this.bgCanvas.height = this.height;
this.bgCanvasCtx = this.bgCanvas.getContext('2d');
// Off-screen canvas for pre-rendering a single bar gradient
this.barCanvas = document.createElement('canvas');
this.barCanvas.width = 6;
this.barCanvas.height = 32;
this.barCanvasCtx = this.barCanvas.getContext('2d');
return this;
},
clear: function() {
this.canvasCtx.drawImage(this.bgCanvas, 0, 0);
},
setColors: function(colors) {
this.colors = colors;
this.preRenderBg();
this.preRenderBar();
},
// Pre-render the background grid
preRenderBg: function() {
this.bgCanvasCtx.fillStyle = this.colors[0];
this.bgCanvasCtx.fillRect(0, 0, this.width, this.height);
this.bgCanvasCtx.fillStyle = this.colors[1];
for (var x = 0; x < this.width; x += 4) {
for (var y = 2; y < this.height; y += 4) {
this.bgCanvasCtx.fillRect(x, y, 2, 2);
}
}
},
// Pre-render the bar gradient
preRenderBar: function() {
this.barCanvasCtx.fillStyle = this.colors[23];
this.barCanvasCtx.fillRect(0, 0, 6, 2);
for (var i = 0; i <= 15; i++) {
var colorNumber = 17 - i;
this.barCanvasCtx.fillStyle = this.colors[colorNumber];
var y = 32 - (i * 2);
this.barCanvasCtx.fillRect(0, y, 6, 2);
}
// If we are paused when the skin changes, we will keep the vis colors
// until we paint again. For now we can just clear the current frame so
// we don't end up with a clashing visual.
this.clear();
},
setStyle: function(style) {
this.style = style;
if (this.style === this.OSCILLOSCOPE) {
this.analyser.fftSize = 2048;
this.bufferLength = this.analyser.fftSize;
this.dataArray = new Uint8Array(this.bufferLength);
} else if (this.style === this.BAR) {
this.analyser.fftSize = 64; // Must be a power of two
// Number of bins/bars we get
this.bufferLength = this.analyser.frequencyBinCount;
this.dataArray = new Uint8Array(this.bufferLength);
}
},
paintFrame: function() {
this.clear();
if (this.style === this.OSCILLOSCOPE) {
return this._paintOscilloscopeFrame();
} else if (this.style === this.BAR) {
return this._paintBarFrame();
}
},
_paintOscilloscopeFrame: function() {
// Return the average value in a slice of dataArray
function sliceAverage(dataArray, sliceWidth, sliceNumber) {
var start = sliceWidth * sliceNumber;
var end = start + sliceWidth;
var sum = 0;
for (var i = start; i < end; i++) {
sum += dataArray[i];
}
return sum / sliceWidth;
}
this.analyser.getByteTimeDomainData(this.dataArray);
// 2 because we're shrinking the canvas by 2
this.canvasCtx.lineWidth = 2;
// Just use one of the viscolors for now
this.canvasCtx.strokeStyle = this.colors[18];
// Since dataArray has more values than we have pixels to display, we
// have to average several dataArray values per pixel. We call these
// groups slices.
//
// We use the 2x scale here since we only want to plot values for
// "real" pixels.
var sliceWidth = Math.floor(this.bufferLength / this.width) * 2;
// The max amplitude is half the height
var h = this.height / 2;
this.canvasCtx.beginPath();
// Iterate over the width of the canvas in "real" pixels.
for (var j = 0; j <= this.width / 2; j++) {
var amplitude = sliceAverage(this.dataArray, sliceWidth, j);
var percentAmplitude = amplitude / 128; // dataArray gives us bytes
var y = percentAmplitude * h;
var x = j * 2;
// Canvas coordinates are in the middle of the pixel by default.
// When we want to draw pixel perfect lines, we will need to
// account for that here
if (x === 0) {
this.canvasCtx.moveTo(x, y);
} else {
this.canvasCtx.lineTo(x, y);
}
}
this.canvasCtx.stroke();
},
_paintBarFrame: function() {
var printBar = function(x, height) {
height = Math.round(height) * 2;
if (height > 0) {
var y = 32 - height;
// Draw the gray peak line
this.canvasCtx.drawImage(this.barCanvas, 0, 0, 6, 2, x, y - 2, 6, 2);
// Draw the gradient
this.canvasCtx.drawImage(this.barCanvas, 0, y, 6, height, x, y, 6, height);
}
}.bind(this);
this.analyser.getByteFrequencyData(this.dataArray);
for (var j = 0; j < this.bufferLength; j++) {
var height = this.dataArray[j] * (14 / 256);
printBar(j * 8, height);
}
}
};

View file

@ -12,8 +12,7 @@ module.exports = {
this.fileInput.type = 'file';
this.fileInput.style.display = 'none';
this.skin = Skin.init(document.getElementById('visualizer'), this.media._analyser);
this.state = '';
this.skin = Skin.init(this.media._analyser);
this.events = {
timeUpdated: new Event('timeUpdated')
@ -37,13 +36,8 @@ module.exports = {
window.dispatchEvent(this.events.timeUpdated);
});
this.media.addEventListener('visualizerupdate', (analyser) => {
this.skin.visualizer.paintFrame(this.visualizerStyle, analyser);
});
this.media.addEventListener('ended', () => {
this.skin.visualizer.clear();
this.dispatch({type: 'MEDIA_IS_STOPPED'});
this.dispatch({type: 'SET_MEDIA_STATUS', status: 'STOPPED'});
});
this.media.addEventListener('waiting', () => {
@ -54,10 +48,6 @@ module.exports = {
this.dispatch({type: 'STOP_WORKING'});
});
this.media.addEventListener('playing', () => {
this.dispatch({type: 'MEDIA_IS_PLAYING'});
});
this.fileInput.onchange = (e) => {
this.loadFromFileReference(e.target.files[0]);
};
@ -97,7 +87,6 @@ module.exports = {
close: function() {
this.media.stop();
this.dispatch({type: 'MEDIA_IS_STOPPED'});
},
openFileDialog: function() {
@ -108,7 +97,9 @@ module.exports = {
const file = new MyFile();
file.setFileReference(fileReference);
if (new RegExp('(wsz|zip)$', 'i').test(fileReference.name)) {
this.skin.setSkinByFile(file);
this.skin.setSkinByFile(file, (colors) => {
this.dispatch({type: 'SET_VISUALIZATION_COLORS', colors});
});
} else {
this.media.autoPlay = true;
this.fileName = fileReference.name;
@ -130,18 +121,10 @@ module.exports = {
setSkin: function(file) {
this.dispatch({type: 'START_LOADING'});
this.skin.setSkinByFile(file, () => this.dispatch({type: 'STOP_LOADING'}));
},
toggleVisualizer: function() {
if (this.skin.visualizer.style === this.skin.visualizer.NONE) {
this.skin.visualizer.setStyle(this.skin.visualizer.BAR);
} else if (this.skin.visualizer.style === this.skin.visualizer.BAR) {
this.skin.visualizer.setStyle(this.skin.visualizer.OSCILLOSCOPE);
} else if (this.skin.visualizer.style === this.skin.visualizer.OSCILLOSCOPE) {
this.skin.visualizer.setStyle(this.skin.visualizer.NONE);
}
this.skin.visualizer.clear();
this.skin.setSkinByFile(file, (colors) => {
this.dispatch({type: 'STOP_LOADING'});
this.dispatch({type: 'SET_VISUALIZATION_COLORS', colors});
});
},
/* Listeners */