diff --git a/js/components/Visualizer.js b/js/components/Visualizer.js index 8be609e6..911321b8 100644 --- a/js/components/Visualizer.js +++ b/js/components/Visualizer.js @@ -4,102 +4,17 @@ import { connect } from "react-redux"; import { toggleVisualizerStyle } from "../actionCreators"; import { getWindowShade, getVisualizerStyle } from "../selectors"; import { VISUALIZERS, MEDIA_STATUS } from "../constants"; - -const PIXEL_DENSITY = 2; -const NUM_BARS = 20; -const BAR_WIDTH = 3 * PIXEL_DENSITY; -const BAR_PEAK_DROP_RATE = 0.01; -const GRADIENT_COLOR_COUNT = 16; -const PEAK_COLOR_INDEX = 23; - -// 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; -} - -function octaveBucketsForBufferLength(bufferLength) { - const octaveBuckets = new Array(NUM_BARS).fill(0); - const minHz = 200; - const maxHz = 22050; - const octaveStep = Math.pow(maxHz / minHz, 1 / NUM_BARS); - - octaveBuckets[0] = 0; - octaveBuckets[1] = minHz; - for (let i = 2; i < NUM_BARS - 1; i++) { - octaveBuckets[i] = octaveBuckets[i - 1] * octaveStep; - } - octaveBuckets[NUM_BARS - 1] = maxHz; - - for (let i = 0; i < NUM_BARS; i++) { - const octaveIdx = Math.floor((octaveBuckets[i] / maxHz) * bufferLength); - octaveBuckets[i] = octaveIdx; - } - - return octaveBuckets; -} - -// Pre-render the background grid -function preRenderBg(width, height, bgColor, fgColor, windowShade) { - // Off-screen canvas for pre-rendering the background - const bgCanvas = document.createElement("canvas"); - bgCanvas.width = width; - bgCanvas.height = height; - const distance = 2 * PIXEL_DENSITY; - - const bgCanvasCtx = bgCanvas.getContext("2d"); - bgCanvasCtx.fillStyle = bgColor; - bgCanvasCtx.fillRect(0, 0, width, height); - if (!windowShade) { - bgCanvasCtx.fillStyle = fgColor; - for (let x = 0; x < width; x += distance) { - for (let y = PIXEL_DENSITY; y < height; y += distance) { - bgCanvasCtx.fillRect(x, y, PIXEL_DENSITY, PIXEL_DENSITY); - } - } - } - return bgCanvas; -} - -function preRenderBar(height, colors, renderHeight) { - /** - * The order of the colours is commented in the file: the fist two colours - * define the background and dots (check it to see what are the dots), the - * next 16 colours are the analyzer's colours from top to bottom, the next - * 5 colours are the oscilloscope's ones, from center to top/bottom, the - * last colour is for the analyzer's peak markers. - */ - - // Off-screen canvas for pre-rendering a single bar gradient - const barCanvas = document.createElement("canvas"); - barCanvas.width = BAR_WIDTH; - barCanvas.height = height; - - const offset = 2; // The first two colors are for the background; - const gradientColors = colors.slice(offset, offset + GRADIENT_COLOR_COUNT); - - const barCanvasCtx = barCanvas.getContext("2d"); - const multiplier = GRADIENT_COLOR_COUNT / renderHeight; - // In shade mode, the five colors are, from top to bottom: - // 214, 102, 0 -- 3 - // 222, 165, 24 -- 6 - // 148, 222, 33 -- 9 - // 57, 181, 16 -- 12 - // 24, 132, 8 -- 15 - // TODO: This could probably be improved by iterating backwards - for (let i = 0; i < renderHeight; i++) { - const colorIndex = GRADIENT_COLOR_COUNT - 1 - Math.floor(i * multiplier); - barCanvasCtx.fillStyle = gradientColors[colorIndex]; - const y = height - i * PIXEL_DENSITY; - barCanvasCtx.fillRect(0, y, BAR_WIDTH, PIXEL_DENSITY); - } - return barCanvas; -} +import { + preRenderBg, + preRenderBar, + sliceAverage, + octaveBucketsForBufferLength, + NUM_BARS, + BAR_WIDTH, + PIXEL_DENSITY, + PEAK_COLOR_INDEX, + BAR_PEAK_DROP_RATE, +} from "./visualizerUtils"; class Visualizer extends React.Component { componentDidMount() { diff --git a/js/components/visualizerUtils.ts b/js/components/visualizerUtils.ts new file mode 100644 index 00000000..9bb482aa --- /dev/null +++ b/js/components/visualizerUtils.ts @@ -0,0 +1,109 @@ +export const PIXEL_DENSITY = 2; +export const NUM_BARS = 20; +export const BAR_WIDTH = 3 * PIXEL_DENSITY; +export const BAR_PEAK_DROP_RATE = 0.01; +export const GRADIENT_COLOR_COUNT = 16; +export const PEAK_COLOR_INDEX = 23; + +// Pre-render the background grid +export function preRenderBg( + width: number, + height: number, + bgColor: string, + fgColor: string, + windowShade: boolean +): HTMLCanvasElement { + // Off-screen canvas for pre-rendering the background + const bgCanvas = document.createElement("canvas"); + bgCanvas.width = width; + bgCanvas.height = height; + const distance = 2 * PIXEL_DENSITY; + + const bgCanvasCtx = bgCanvas.getContext("2d")!; + bgCanvasCtx.fillStyle = bgColor; + bgCanvasCtx.fillRect(0, 0, width, height); + if (!windowShade) { + bgCanvasCtx.fillStyle = fgColor; + for (let x = 0; x < width; x += distance) { + for (let y = PIXEL_DENSITY; y < height; y += distance) { + bgCanvasCtx.fillRect(x, y, PIXEL_DENSITY, PIXEL_DENSITY); + } + } + } + return bgCanvas; +} + +export function preRenderBar( + height: number, + colors: string[], + renderHeight: number +): HTMLCanvasElement { + /** + * The order of the colours is commented in the file: the fist two colours + * define the background and dots (check it to see what are the dots), the + * next 16 colours are the analyzer's colours from top to bottom, the next + * 5 colours are the oscilloscope's ones, from center to top/bottom, the + * last colour is for the analyzer's peak markers. + */ + + // Off-screen canvas for pre-rendering a single bar gradient + const barCanvas = document.createElement("canvas"); + barCanvas.width = BAR_WIDTH; + barCanvas.height = height; + + const offset = 2; // The first two colors are for the background; + const gradientColors = colors.slice(offset, offset + GRADIENT_COLOR_COUNT); + + const barCanvasCtx = barCanvas.getContext("2d")!; + const multiplier = GRADIENT_COLOR_COUNT / renderHeight; + // In shade mode, the five colors are, from top to bottom: + // 214, 102, 0 -- 3 + // 222, 165, 24 -- 6 + // 148, 222, 33 -- 9 + // 57, 181, 16 -- 12 + // 24, 132, 8 -- 15 + // TODO: This could probably be improved by iterating backwards + for (let i = 0; i < renderHeight; i++) { + const colorIndex = GRADIENT_COLOR_COUNT - 1 - Math.floor(i * multiplier); + barCanvasCtx.fillStyle = gradientColors[colorIndex]; + const y = height - i * PIXEL_DENSITY; + barCanvasCtx.fillRect(0, y, BAR_WIDTH, PIXEL_DENSITY); + } + return barCanvas; +} + +// Return the average value in a slice of dataArray +export function sliceAverage( + dataArray: Uint8Array, + sliceWidth: number, + sliceNumber: number +) { + const start = sliceWidth * sliceNumber; + const end = start + sliceWidth; + let sum = 0; + for (let i = start; i < end; i++) { + sum += dataArray[i]; + } + return sum / sliceWidth; +} + +export function octaveBucketsForBufferLength(bufferLength: number): number[] { + const octaveBuckets = new Array(NUM_BARS).fill(0); + const minHz = 200; + const maxHz = 22050; + const octaveStep = Math.pow(maxHz / minHz, 1 / NUM_BARS); + + octaveBuckets[0] = 0; + octaveBuckets[1] = minHz; + for (let i = 2; i < NUM_BARS - 1; i++) { + octaveBuckets[i] = octaveBuckets[i - 1] * octaveStep; + } + octaveBuckets[NUM_BARS - 1] = maxHz; + + for (let i = 0; i < NUM_BARS; i++) { + const octaveIdx = Math.floor((octaveBuckets[i] / maxHz) * bufferLength); + octaveBuckets[i] = octaveIdx; + } + + return octaveBuckets; +}