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) { const start = sliceWidth * sliceNumber; const end = start + sliceWidth; let sum = 0; for (let 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); } 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() { if (!this.props.colors) { return; } // 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() { const 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 (let x = 0; x < this.width; x += 4) { for (let y = 2; y < this.height; y += 4) { bgCanvasCtx.fillRect(x, y, 2, 2); } } } // Pre-render the bar gradient preRenderBar() { const barCanvasCtx = this.barCanvas.getContext('2d'); barCanvasCtx.fillStyle = this.props.colors[23]; barCanvasCtx.fillRect(0, 0, 6, 2); for (let i = 0; i <= 15; i++) { const colorNumber = 17 - i; barCanvasCtx.fillStyle = this.props.colors[colorNumber]; const 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. const sliceWidth = Math.floor(this.bufferLength / this.width) * 2; // The max amplitude is half the height const h = this.height / 2; this.canvasCtx.beginPath(); // Iterate over the width of the canvas in "real" pixels. for (let j = 0; j <= this.width / 2; j++) { const amplitude = sliceAverage(this.dataArray, sliceWidth, j); const percentAmplitude = amplitude / 128; // dataArray gives us bytes const y = percentAmplitude * h; const 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) { const 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 (let j = 0; j < this.bufferLength; j++) { const 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 ; } } const mapStateToProps = (state) => { return { colors: state.display.skinColors, style: state.display.visualizerStyle, status: state.media.status }; }; module.exports = connect(mapStateToProps)(Visualizer);