From 4a5483ab592cac44689a215a10ac014a0fe85828 Mon Sep 17 00:00:00 2001 From: Harrison Deng Date: Sun, 17 Apr 2022 14:05:41 -0500 Subject: [PATCH] Added generic numeric mapping functions. Changed library code to favour use of objects over arrays. Implemented changes in text.js. --- src/mapping/generic.js | 104 +++++++++++++++++ src/mapping/text.js | 117 ++++++++++++++++--- src/visualization/VisualizerUpdateManager.js | 78 ++++++++----- 3 files changed, 254 insertions(+), 45 deletions(-) create mode 100644 src/mapping/generic.js diff --git a/src/mapping/generic.js b/src/mapping/generic.js new file mode 100644 index 0000000..7a41f20 --- /dev/null +++ b/src/mapping/generic.js @@ -0,0 +1,104 @@ +// TODO: add for width +// TODO: add for height +// TODO: add for background + +import VisualizerUpdateManager from "../visualization/VisualizerUpdateManager.js"; + +// TODO: Figure out how to not need this defined multiple times. +/** + * @callback interpolator + * @param {number} current the current value. + * @param {number} dest the destination value. + * @param {number} frameDelta the time elapsed since the last update. + * @returns {number} the new current value. + */ + +/** + * @callback VisualizerUpdateManager.visualizerRangedUpdateListener + * @param {number} timeDelta elapsed time since last update. + * @param {number} bins the bins of the range. + */ + +/** + * @callback VisualizerUpdateManager.visualizerBinUpdateListener + * @param {number} timeDelta elapsed time since last update. + * @param {number} amplitude The amplitude of the associated bin. + * @param {number} ampDelta change in amplitude of the frequency bin. + */ + +/** + * + * @callback numericalGetter + * @returns {number} The number this value currently represents. + */ + +/** + * @callback numericalSetter + * @param {number} value The new numerical value to update to. + */ + +/** + * Maps the average of a range of bins to numerical value defined by a getter and a setter. + * + * @param {object} conf A configuration for how to map a numerical value. + * @param {number} conf.minVal The minimum value of the numerical value. + * @param {number} conf.maxVal The maximum value of the numerical value. + * @param {number} conf.lowerBin The lower bin of the range of bins this value is to be mapped to. + * @param {number} conf.upperBin The upper bin of the range of bins this value si to be mapped to. + * @param {numericalGetter} conf.getter The getter callback to be used to get the current number. + * @param {numericalSetter} conf.setter The setter callback to be used to set the new number. + * @param {interpolator} conf.interpolator The interpolation function to use. + * @param {VisualizerUpdateManager} conf.visUpdateManager the visualizer update manager this mapping corresponds with. + * @param {boolean} [conf.reversed = false] If true, then high amplitudes will be mapped to low values and vice versa. + * @returns {{lower: number, upper: number, listener: VisualizerUpdateManager.visualizerRangedUpdateListener}} An object containing the lower and upper bounds for the range of a listener, which is also in the object. + */ +export function mapRangedAvgNumerical({ minVal, maxVal, lowerBin = undefined, upperBin, getter, setter, interpolator, visUpdateManager, reversed = false }) { + const rangedListener = { + lower: lowerBin, + upper: upperBin, + listener: (timeDelta, bins) => { + const normalBins = [...bins]; + // TODO: Future: add weighting / distribution system? + let average = 0; + for (let i = 0; i < normalBins.length; i++) { + normalBins[i] = normalBins[i] / 255.0; + average += normalBins[i]; + } + average /= normalBins.length; + const range = maxVal - minVal; + let interpolated = interpolator(getter(), average, timeDelta); // TODO: May be optimized by allowing for transferring of previous style state. + if (reversed) interpolated = 1 - interpolated; + setter(minVal + range * interpolated); + } + }; + visUpdateManager.addVisualizerRangedUpdateListener(rangedListener); + return rangedListener; +} + +/** + * Maps a single bin of frequency amplitudes to a numerical value defined by a getter and a setter. + * + * @param {object} conf The configuration for mapping a single bin to a numerical value. + * @param {number} conf.minVal The minimum value the mapping can produce. + * @param {number} conf.maxVal The maximum value the mapping can produce. + * @param {number} conf.bin The bin to map this number to. + * @param {numericalGetter} conf.getter The callback to be used to get the current number. + * @param {numericalSetter} conf.setter The callback to be used to set the new number. + * @param {interpolator} conf.interpolator The interpolation function to use. + * @param {VisualizerUpdateManager} conf.visUpdateManager The update manager to map to. + * @param {boolean} [conf.reversed] If true, then high amplitudes will be mapped to lower values and vice versa. + * @returns {bin: number, listener: VisualizerUpdateManager.visualizerBinUpdateListener} + */ +export function mapBinNumerical({ minVal, maxVal, bin, getter, setter, interpolator, visUpdateManager, reversed = false }) { + const listener = { + bin: bin, + listener: (timeDelta, amp) => { + const range = maxVal - minVal; + let interpolated = interpolator(getter(), amp, timeDelta); + if (reversed) interpolated = 1 - interpolated; + setter(minVal + range * interpolated); + } + }; + visUpdateManager.AddVisualizerBinUpdateListener(listener); + return listener; +} \ No newline at end of file diff --git a/src/mapping/text.js b/src/mapping/text.js index 2c7d275..e6d46a7 100644 --- a/src/mapping/text.js +++ b/src/mapping/text.js @@ -1,7 +1,8 @@ import { parseColor, rgbaToHexRgba } from "./colors.js"; import VisualizerUpdateManager from "../visualization/VisualizerUpdateManager.js"; +import { mapBinNumerical, mapRangedAvgNumerical } from "./generic.js"; -// TODO: Should be able to remove this since already declared in easings.js, but don't know how to link. +// TODO: Future: Should be able to remove this since already declared in easings.js, but don't know how to link. /** * @callback interpolator * @param {number} current the current value. @@ -10,26 +11,112 @@ import VisualizerUpdateManager from "../visualization/VisualizerUpdateManager.js * @returns {number} the new current value. */ +/** + * @callback VisualizerUpdateManager.visualizerRangedUpdateListener + * @param {number} timeDelta elapsed time since last update. + * @param {number} bins the bins of the range. + */ + +/** + * @callback VisualizerUpdateManager.visualizerBinUpdateListener + * @param {number} timeDelta elapsed time since last update. + * @param {number} amplitude The amplitude of the associated bin. + * @param {number} ampDelta change in amplitude of the frequency bin. + */ + /** * Maps the red component of the text color to a certain range of bins. * - * @param {HTMLElement} element The element whose text's red value should be mapped. - * @param {number} color Where r for red, g, for green, b for blue, and a for alpha. - * @param {number} lowerBin The lower bound of the bins to be mapped. - * @param {VisualizerUpdateManager} visUpdateManager The visualizer update manager associated with the audio playback you would like the mapping with. - * @param {interpolator} interpolator The interpolation function to use. - * @param {number} [upperBin=undefined] The upper bound of the bins to be mapped. If left undefined, then only the bin defined by the min value is used. - * @param {boolean} [reversed=true] If true, then the quieter, the greater the red value. + * @param {object} rgbaMapConfiguration The configuration of the mapping. + * @param {HTMLElement} rgbaMapConfiguration.element The element whose text's red value should be mapped. + * @param {number} rgbaMapConfiguration.color Where r for red, g, for green, b for blue, and a for alpha. + * @param {number} rgbaMapConfiguration.lowerBin The lower bound of the bins to be mapped. + * @param {VisualizerUpdateManager} rgbaMapConfiguration.visUpdateManager The visualizer update manager associated with the audio playback you would like the mapping with. + * @param {interpolator} rgbaMapConfiguration.interpolator The interpolation function to use. + * @param {number} [rgbaMapConfiguration.upperBin=undefined] The upper bound of the bins to be mapped. If left undefined, then only the bin defined by the min value is used. + * @param {boolean} [rgbaMapConfiguration.reversed=true] If true, then the quieter, the greater the red value. + * @returns {{bin: number, listener: VisualizerUpdateManager.visualizerBinUpdateListener}|{lower: number, upper: number, listener: VisualizerUpdateManager.visualizerRangedUpdateListener}} The ranged listener that was added. */ -export function mapColor(element, color, lowerBin, visUpdateManager, interpolator, upperBin = undefined, reversed = false) { +export function mapRgba({ element, color, lowerBin, visUpdateManager, interpolator, upperBin = undefined, reversed = false }) { const rgbaStr = "rgba"; color = rgbaStr.indexOf(color); if (color < 0) throw new Error("Invalid color parameter provided."); - if (!upperBin) upperBin = lowerBin; - visUpdateManager.addVisualizerRangeUpdateListener(lowerBin, upperBin, (timeDelta, amp) => { - const rgba = parseColor(element.style.color); - rgba[color] = interpolator(rgba[color], amp, timeDelta); - if (reversed) rgba[color] = 255 - rgba[color]; - element.style.color = rgbaToHexRgba(rgba); + const getter = () => parseColor(element.style.color)[color]; + const setter = (value) => { + const changed = parseColor(element.style.color); + changed[color] = value; + element.style.color = rgbaToHexRgba(changed); + }; + if (!upperBin) { + return mapBinNumerical({ + minVal: 0, + maxVal: 255, + bin: lowerBin, + getter: getter, + setter: setter, + interpolator: interpolator, + visUpdateManager: visUpdateManager, + reversed: reversed + }); + } + // TODO: Future: Could use the same object and modify + return mapRangedAvgNumerical({ + minVal: 0, + maxVal: 255, + lowerBin: lowerBin, + upperBin: upperBin, + getter: getter, + setter: setter, + interpolator: interpolator, + visUpdateManager: visUpdateManager, + reversed: reversed }); } + +/** + * + * @param {object} fontSizeMapConfiguration The configuration of the font size mapping. + * @param {HTMLElement} fontSizeMapConfiguration.element The element whose font sizes will be mapped to the amplitude. + * @param {number} fontSizeMapConfiguration.growLower The lower limit of the font size. + * @param {number} fontSizeMapConfiguration.growUpper The upper limit of the font size. + * @param {string} fontSizeMapConfiguration.unit The unit the upper and lower bounds are measured in. + * @param {number} fontSizeMapConfiguration.lowerBin The lower boundary of bins to be mapped (or the single bin to be mapped if the upper bin is undefined). + * @param {VisualizerUpdateManager} fontSizeMapConfiguration.visUpdateManager the visualizer update manager to be mapped to. + * @param {interpolator} fontSizeMapConfiguration.interpolator The interpolation function to be used to transition from one value to the next. + * @param {number} [fontSizeMapConfiguration.upperBin=undefined] The upper bin, or undefined, which results in mapping to a single bin. + * @param {boolean} [fontSizeMapConfiguration.reversed=false] If true, then high amplitudes are mapped to lower values and vice versa. + * @returns {{bin: number, listener: VisualizerUpdateManager.visualizerBinUpdateListener}|{lower: number, upper: number, listener: VisualizerUpdateManager.visualizerRangedUpdateListener}} The ranged listener that was added. + */ +export function mapFontSize({ element, growLower, growUpper, unit, lowerBin, visUpdateManager, interpolator, upperBin = undefined, reversed = false }) { + const getter = () => { + return parseInt(element.style.fontSize); + }; + const setter = (value) => { + element.style.fontSize = value + unit; + }; + if (!upperBin) { + return mapBinNumerical({ + minVal: growLower, + maxVal: growUpper, + bin: lowerBin, + getter: getter, + setter: setter, + interpolator: interpolator, + visUpdateManager: visUpdateManager, + reversed: reversed + }); + } + return mapRangedAvgNumerical({ + minVal: growLower, + maxVal: growUpper, + lowerBin: lowerBin, + upperBin: upperBin, + getter: getter, + setter: setter, + interpolator: interpolator, + visUpdateManager: visUpdateManager, + reversed: reversed + }); +} + +// TODO: Future: map hue \ No newline at end of file diff --git a/src/visualization/VisualizerUpdateManager.js b/src/visualization/VisualizerUpdateManager.js index a8d67bf..9e6b521 100644 --- a/src/visualization/VisualizerUpdateManager.js +++ b/src/visualization/VisualizerUpdateManager.js @@ -21,7 +21,7 @@ export default class VisualizerUpdateManager { this._lastBins = new Uint8Array(this._binnedListeners.length); this._visualizer = visualizer; this._visualizerListener = (delta, bins) => { - const sortedCopyOfRangedListeners = [... this._rangedListeners].sort((a, b) => a[0] - b[0]); // Priority queue could be better. + const sortedCopyOfRangedListeners = [... this._rangedListeners]; // We assume this is sorted properly. A priority queue could be better. for (let binInd = 0; binInd < this._lastBins.length; binInd++) { const lastBin = this._lastBins[binInd]; if (lastBin !== bins[binInd]) { @@ -29,10 +29,10 @@ export default class VisualizerUpdateManager { listener(delta, bins[binInd], bins[binInd] - lastBin); }); for (let rangedInd = 0; rangedInd < sortedCopyOfRangedListeners.length; rangedInd++) { // Could switch to a while loop. - const [min, max, listener] = sortedCopyOfRangedListeners[rangedInd]; - if (min > binInd) break; // Don't need to check the rest if the current lowest minimum is greater than the current bin index. - if (binInd <= max) { - listener(delta, bins.slice(min, max)); + const { lower, upper, listener } = sortedCopyOfRangedListeners[rangedInd]; + if (lower > binInd) break; // Don't need to check the rest if the current lowest minimum is greater than the current bin index. + if (binInd <= upper) { + listener(delta, bins.slice(lower, upper)); sortedCopyOfRangedListeners.shift(); rangedInd--; } @@ -45,26 +45,28 @@ export default class VisualizerUpdateManager { } /** - * @callback visualizerBinUpdateListener + * @callback VisualizerUpdateManager.visualizerBinUpdateListener * @param {number} timeDelta elapsed time since last update. * @param {number} amplitude The amplitude of the associated bin. * @param {number} ampDelta change in amplitude of the frequency bin. */ /** - * @callback visualizerRangeUpdateListener + * @callback VisualizerUpdateManager.visualizerRangedUpdateListener * @param {number} timeDelta elapsed time since last update. * @param {number} bins the bins of the range. */ /** + * Adds a listener to a specific frequency bin. * - * @param {number} freqBin the frequency bin this update listener should listen to. - * @param {visualizerBinUpdateListener} listener the listener itself that will be called upon the bin updating. + * @param {object} freqBinListener the listener for a specific frequency bin. + * @param {number} freqBinListener.freqBin the frequency bin this update listener should listen to. + * @param {VisualizerUpdateManager.visualizerBinUpdateListener} freqBinListener.listener the listener itself that will be called upon the bin updating. * @returns {boolean} true if and only if the updater was added successfully. */ - AddVisualizerBinUpdateListener(freqBin, listener) { + AddVisualizerBinUpdateListener({ freqBin, listener }) { if (this._binnedListeners[freqBin].includes(listener)) return false; this._binnedListeners[freqBin].push(listener); return true; @@ -73,25 +75,32 @@ export default class VisualizerUpdateManager { /** * Similar to {@link VisualizerUpdateManager#AddVisualizerBinUpdateListener}, this method adds a listener for to a range of bins. * - * @param {number} min The lower bound of the bins to listen to (inclusive). - * @param {number} max The upper bound of the bins to listen to (inclusive). - * @param {visualizerRangeUpdateListener} listener The listener to register to the range. + * @param {object} rangedUpdateListener The ranged update listener to add. + * @param {number} rangedUpdateListener.lower The lower bound of the bins to listen to (inclusive). + * @param {number} rangedUpdateListener.upper The upper bound of the bins to listen to (inclusive). + * @param {VisualizerUpdateManager.visualizerRangedUpdateListener} rangedUpdateListener.listener The listener to register to the range. * @returns {boolean} True if and only if the ranged listener was added successfully. */ - addVisualizerRangeUpdateListener(min, max, listener) { - const rangedListener = [min, max, listener]; + addVisualizerRangedUpdateListener({ lower, upper, listener }) { + const rangedListener = { + lower: lower, + upper: upper, + listener: listener + }; if (this._rangedListeners.includes(rangedListener)) return false; this._rangedListeners.push(rangedListener); + this._rangedListeners.sort((a, b) => a.lower - b.lower); return true; } /** * - * @param {number} freqBin the frequency bin the update listener to be removed from is in. - * @param {visualizerBinUpdateListener} listener the listener that is to be removed. + * @param {object} binFreqListener The bin frequency listener to remove. + * @param {number} binFreqListener.freqBin the frequency bin the update listener to be removed from is in. + * @param {VisualizerUpdateManager.visualizerBinUpdateListener} binFreqListener.listener the listener that is to be removed. * @returns {boolean} true if and only if the listener was successfully removed. */ - removeVisualizerBinUpdateListener(freqBin, listener) { + removeVisualizerBinUpdateListener({ freqBin, listener }) { const removeIndex = this._binnedListeners[freqBin].indexOf(listener); if (removeIndex < 0) return false; this._binnedListeners[freqBin].splice(removeIndex, 1); @@ -101,13 +110,18 @@ export default class VisualizerUpdateManager { /** * Similar to {@link removeVisualizerBinUpdateListener}, this method removes the given listener from a range of bins. * - * @param {number} min The lower bound of bins to remove the listener from (inclusive). - * @param {number} max The upper bound of bin to remove the listener from (inclusive.) - * @param {visualizerBinUpdateListener} listener The update listener to remove from the bins. + * @param {object} rangedListener The ranged listener to remove. + * @param {number} rangedListener.lower The lower bound of bins to remove the listener from (inclusive). + * @param {number} rangedListener.upper The upper bound of bin to remove the listener from (inclusive.) + * @param {VisualizerUpdateManager.visualizerRangedUpdateListener} rangedListener.listener The update listener to remove from the bins. * @returns {boolean} True if and only if the given listener was removed. */ - removeVisualizerRangeUpdateListener(min, max, listener) { - const rangedListener = [min, max, listener]; + removeVisualizerRangedUpdateListener({ lower, upper, listener }) { + const rangedListener = { + lower: lower, + upper: upper, + listener: listener + }; const removeIndex = this._rangedListeners.indexOf(rangedListener); if (removeIndex < 0) return false; this._binnedListeners.splice(removeIndex, 1); @@ -116,21 +130,25 @@ export default class VisualizerUpdateManager { /** * - * @param {visualizerBinUpdateListener[][]} binnedListeners an array of the same length as the number of bins where each element is another array containing the listeners for that bin. + * @param {{binned: VisualizerUpdateManager.visualizerBinUpdateListener[][], ranged: {lower: number, upper: number, listener: VisualizerUpdateManager.visualizerRangedUpdateListener}[]}} listeners an object containing both the binned, and ranged listeners that this update manager should use. * @returns {boolean} true if and only if successfully loaded the new listeners. */ - setBinnedListeners(binnedListeners) { - if (binnedListeners.length !== this._binnedListeners.length) return false; - this._binnedListeners = binnedListeners; + setBinnedListeners(listeners) { + if (listeners.binned.length !== this._binnedListeners.length) return false; + this._binnedListeners = listeners.binned; + this._rangedListeners = listeners.ranged.sort((a, b) => a.lower - b.lower); return true; } /** * - * @returns {visualizerBinUpdateListener[][]} an array of the same length as the number of bins where each element is another array containing the listeners for that bin. + * @returns {{binned: VisualizerUpdateManager.visualizerBinUpdateListener[][], ranged: {lower: number, upper: number, listener: VisualizerUpdateManager.visualizerRangedUpdateListener}[]}} All the listeners, both for binned, and ranged listeners. */ - getBinnedListeners() { - return this._binnedListeners; + getAllListeners() { + return { + binned: this._binnedListeners, + ranged: this._rangedListeners + }; } /**