Removed single bin listeners.

Upper bin bound now defaults to the lower bound if left undefined for numerical function.

Renamed mapping functions to be less wordy.

Fixed some JSDoc typos.
This commit is contained in:
Harrison Deng 2022-04-21 02:29:03 -05:00
parent ea56601126
commit 4dc34ef2f9
5 changed files with 101 additions and 163 deletions

View File

@ -1,12 +1,12 @@
import { parseColor, rgbaToHexRgba } from "../support/colors.js";
import VisUpdateRouter from "../visualization/VisUpdateRouter.js";
import { mapBinNumerical, mapRangedAvgNumerical } from "./numeric.js";
import { numerical } from "./numeric.js";
/**@module */
/**
* Maps the red component of the text color to a certain range of bins.
* Maps a color component of an element background color to a certain range of frequency bins.
*
* @param {object} conf The configuration of the mapping.
* @param {HTMLElement} conf.element The element whose text's red value should be mapped.
@ -18,9 +18,53 @@ import { mapBinNumerical, mapRangedAvgNumerical } from "./numeric.js";
* @param {boolean} [conf.reversed=true] If true, then the quieter, the greater the red value.
* @param {number} [conf.lowerVal=0] The lower boundary of possible values for the color component (0 to upperVal inclusive).
* @param {number} [conf.upperVal=0] The upper boundary of possible values for the color component (0 to 255 inclusive).
* @returns {{bin: number, listener: VisUpdateRouter~visualizerBinUpdateListener}|{lower: number, upper: number, listener: VisUpdateRouter~visualizerRangedUpdateListener}} The ranged listener that was added.
* @returns {{lower: number, upper: number, listener: VisUpdateRouter~visualizerRangedUpdateListener}} The ranged listener that was added.
*/
export function mapFontRgba({ element, color, lowerBin, visUpdateRouter, interpolator, upperBin = undefined, reversed = false, lowerVal = 0, upperVal = 255 }) {
export function backgroundColorRgba({ element, color, lowerBin, visUpdateRouter, interpolator, upperBin = undefined, reversed = false, lowerVal = 0, upperVal = 255 }) {
const rgbaStr = "rgba";
color = rgbaStr.indexOf(color);
if (color < 0) throw new Error("Invalid color parameter provided.");
const getter = () => {
if (!element.style.backgroundColor) element.style.backgroundColor = "rgba(0, 0, 0, 255)";
return parseColor(element.style.backgroundColor)[color];
};
const setter = (value) => {
const changed = parseColor(element.style.backgroundColor);
changed[color] = value;
element.style.backgroundColor = rgbaToHexRgba(changed);
};
upperVal = Math.min(Math.max(0, upperVal), 255);
lowerVal = Math.min(Math.max(0, lowerVal), upperVal);
const conf = {
minVal: lowerVal,
maxVal: upperVal,
lowerBin: lowerBin,
upperBin: upperBin,
getter: getter,
setter: setter,
interpolator: interpolator,
visUpdateRouter: visUpdateRouter,
reversed: reversed
};
return numerical(conf);
}
/**
* Maps a color component of the text color to a certain range of frequency bins.
*
* @param {object} conf The configuration of the mapping.
* @param {HTMLElement} conf.element The element whose text's red value should be mapped.
* @param {number} conf.color Where r for red, g, for green, b for blue, and a for alpha.
* @param {number} conf.lowerBin The lower bound of the bins to be mapped.
* @param {VisUpdateRouter} conf.visUpdateRouter The visualizer update manager associated with the audio playback you would like the mapping with.
* @param {Function} conf.interpolator The interpolation function to use.
* @param {number} [conf.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} [conf.reversed=true] If true, then the quieter, the greater the red value.
* @param {number} [conf.lowerVal=0] The lower boundary of possible values for the color component (0 to upperVal inclusive).
* @param {number} [conf.upperVal=0] The upper boundary of possible values for the color component (0 to 255 inclusive).
* @returns {{lower: number, upper: number, listener: VisUpdateRouter~visualizerRangedUpdateListener}} The ranged listener that was added.
*/
export function fontColorRgba({ element, color, lowerBin, visUpdateRouter, interpolator, upperBin = undefined, reversed = false, lowerVal = 0, upperVal = 255 }) {
const rgbaStr = "rgba";
color = rgbaStr.indexOf(color);
if (color < 0) throw new Error("Invalid color parameter provided.");
@ -38,18 +82,13 @@ export function mapFontRgba({ element, color, lowerBin, visUpdateRouter, interpo
const conf = {
minVal: lowerVal,
maxVal: upperVal,
bin: lowerBin,
lowerBin: lowerBin,
upperBin: upperBin,
getter: getter,
setter: setter,
interpolator: interpolator,
visUpdateRouter: visUpdateRouter,
reversed: reversed
};
if (upperBin) {
conf.bin = undefined;
conf.lowerBin = lowerBin;
conf.upperBin = upperBin;
return mapRangedAvgNumerical(conf);
}
return mapBinNumerical(conf);
return numerical(conf);
}

View File

@ -1,5 +1,5 @@
import VisUpdateRouter from "../visualization/VisUpdateRouter.js";
import { mapBinNumerical, mapRangedAvgNumerical } from "./numeric.js";
import { numerical } from "./numeric.js";
/**@module */
@ -18,27 +18,21 @@ import { mapBinNumerical, mapRangedAvgNumerical } from "./numeric.js";
* @param {boolean} [conf.reversed=false] If true, then high amplitudes are mapped to lower values and vice versa.
* @returns {{bin: number, listener: VisUpdateRouter~visualizerBinUpdateListener}|{lower: number, upper: number, listener: VisUpdateRouter.visualizerRangedUpdateListener}} The listener that was added (ranged if an upper bound was provided, binned otherwise).
*/
export function mapWidth({ element, growLower, growUpper, unit, lowerBin, visUpdateRouter, interpolator, upperBin = undefined, reversed = false }) {
export function width({ element, growLower, growUpper, unit, lowerBin, visUpdateRouter, interpolator, upperBin = undefined, reversed = false }) {
const getter = () => parseInt(element.style.width) || 0;
const setter = (value) => element.style.width = value + unit;
const conf = {
minVal: growLower,
maxVal: growUpper,
bin: lowerBin,
lowerBin: lowerBin,
upperBin: upperBin,
getter: getter,
setter: setter,
interpolator: interpolator,
visUpdateRouter: visUpdateRouter,
reversed: reversed
};
if (upperBin) {
conf.bin = undefined;
conf.lowerBin = lowerBin;
conf.upperBin = upperBin;
return mapRangedAvgNumerical(conf);
}
return mapBinNumerical(conf);
return numerical(conf);
}
/**
@ -56,27 +50,21 @@ export function mapWidth({ element, growLower, growUpper, unit, lowerBin, visUpd
* @param {boolean} [conf.reversed=false] If true, then high amplitudes are mapped to lower values and vice versa.
* @returns {{bin: number, listener: VisUpdateRouter~visualizerBinUpdateListener}|{lower: number, upper: number, listener: VisUpdateRouter.visualizerRangedUpdateListener}} The listener that was added (ranged if an upper bound was provided, binned otherwise).
*/
export function mapHeight({ element, growLower, growUpper, unit, lowerBin, visUpdateRouter, interpolator, upperBin = undefined, reversed = false }) {
export function height({ element, growLower, growUpper, unit, lowerBin, visUpdateRouter, interpolator, upperBin = undefined, reversed = false }) {
const getter = () => parseInt(element.style.height) || 0;
const setter = (value) => element.style.height = value + unit;
const conf = {
minVal: growLower,
maxVal: growUpper,
bin: lowerBin,
lowerBin: lowerBin,
upperBin: upperBin,
getter: getter,
setter: setter,
interpolator: interpolator,
visUpdateRouter: visUpdateRouter,
reversed: reversed
};
if (upperBin) {
conf.bin = undefined;
conf.lowerBin = lowerBin;
conf.upperBin = upperBin;
return mapRangedAvgNumerical(conf);
}
return mapBinNumerical(conf);
return numerical(conf);
}
/**
@ -93,24 +81,19 @@ export function mapHeight({ element, growLower, growUpper, unit, lowerBin, visUp
* @param {boolean} [fontSizeMapConfiguration.reversed=false] If true, then high amplitudes are mapped to lower values and vice versa.
* @returns {{bin: number, listener: VisUpdateRouter~visualizerBinUpdateListener}|{lower: number, upper: number, listener: VisUpdateRouter~visualizerRangedUpdateListener}} The listener that was added (ranged if an upper bound was provided, binned otherwise).
*/
export function mapFontSize({ element, growLower, growUpper, unit, lowerBin, visUpdateRouter, interpolator, upperBin = undefined, reversed = false }) {
export function fontSize({ element, growLower, growUpper, unit, lowerBin, visUpdateRouter, interpolator, upperBin = undefined, reversed = false }) {
const getter = () => parseInt(element.style.fontSize);
const setter = (value) => element.style.fontSize = value + unit;
const conf = {
minVal: growLower,
maxVal: growUpper,
bin: lowerBin,
lowerBin: lowerBin,
upperBin: upperBin,
getter: getter,
setter: setter,
interpolator: interpolator,
visUpdateRouter: visUpdateRouter,
reversed: reversed
};
if (upperBin) {
conf.bin = undefined;
conf.lowerBin = lowerBin;
conf.upperBin = upperBin;
return mapRangedAvgNumerical(conf);
}
return mapBinNumerical(conf);
return numerical(conf);
}

View File

@ -22,7 +22,7 @@ import VisUpdateRouter from "../visualization/VisUpdateRouter.js";
* @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 {number} [conf.upperBin=undefined] The upper bin of the range of bins this value si to be mapped to.
* @param {module:mappings/numeric~numericalGetter} conf.getter The getter callback to be used to get the current number.
* @param {module:mappings/numeric~numericalSetter} conf.setter The setter callback to be used to set the new number.
* @param {module:support/easings~interpolator} conf.interpolator The interpolation function to use.
@ -30,10 +30,10 @@ import VisUpdateRouter from "../visualization/VisUpdateRouter.js";
* @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: VisUpdateRouter.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, visUpdateRouter, reversed = false }) {
export function numerical({ minVal, maxVal, lowerBin, upperBin = undefined, getter, setter, interpolator, visUpdateRouter, reversed = false }) {
const rangedListener = {
lower: lowerBin,
upper: upperBin,
upper: upperBin ? upperBin : lowerBin,
listener: (timeDelta, bins) => {
const normalBins = [...bins];
// TODO: Future: add weighting / distribution system?
@ -50,38 +50,8 @@ export function mapRangedAvgNumerical({ minVal, maxVal, lowerBin = undefined, up
setter(minVal + range * interpolated);
}
};
if (visUpdateRouter.addVisualizerRangedUpdateListener(rangedListener)) {
if (visUpdateRouter.addUpdateListener(rangedListener)) {
return rangedListener;
}
return null; // Technically doesn't occur since the functions are anonymous?
}
/**
* 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 {module:mappings/numeric~numericalGetter} conf.getter The callback to be used to get the current number.
* @param {module:mappings/numeric~numericalSetter} conf.setter The callback to be used to set the new number.
* @param {module:support/easings~interpolator} conf.interpolator The interpolation function to use.
* @param {VisUpdateRouter} conf.visUpdateRouter 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: VisUpdateRouter~visualizerBinUpdateListener}} The bin listener that was added.
*/
export function mapBinNumerical({ minVal, maxVal, bin, getter, setter, interpolator, visUpdateRouter, reversed = false }) {
const listener = {
bin: bin,
listener: (timeDelta, amp) => {
const range = maxVal - minVal;
let interpolated = interpolator(getter(), amp, Math.min(timeDelta, 1));
if (reversed) interpolated = 1 - interpolated;
setter(minVal + range * interpolated);
}
};
if (visUpdateRouter.AddVisualizerBinUpdateListener(listener)) {
return listener;
}
return null; // Technically doesn't occur since the functions are anonymous?
}

View File

@ -11,8 +11,7 @@ import Visualizer from "./Visualizer.js";
* In the rare event that a bin has not changed, then it will not receive an update call.
*/
export default class VisUpdateRouter {
#binnedListeners = [];
#rangedListeners = [];
listeners = [];
#lastBins;
#visualizer;
@ -34,31 +33,23 @@ export default class VisUpdateRouter {
* @param {Visualizer} visualizer the fft window size of the visualizer this manager obtains data from.
*/
constructor(visualizer) {
this.#binnedListeners = [];
this.#rangedListeners = [];
const bins = visualizer.numberOfBins; // TODO: Clean this up.
for (let i = 0; i < bins; i++) {
this.#binnedListeners.push([]);
}
this.#lastBins = new Uint8Array(this.#binnedListeners.length);
this.listeners = [];
this.#lastBins = new Uint8Array(visualizer.numberOfBins);
this.#visualizer = visualizer;
this.#visualizer.addUpdateListener(this.#visualizerListener);
}
#visualizerListener = (delta, bins) => {
const sortedCopyOfRangedListeners = [... this.#rangedListeners]; // We assume this is sorted properly. A priority queue could be better.
const copyOfRangedListeners = [... this.listeners]; // 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]) {
this.#binnedListeners[binInd].forEach(listener => {
listener(delta, bins[binInd], bins[binInd] - lastBin);
});
for (let rangedInd = 0; rangedInd < sortedCopyOfRangedListeners.length; rangedInd++) { // Could switch to a while loop.
const { lower, upper, listener } = sortedCopyOfRangedListeners[rangedInd];
for (let rangedInd = 0; rangedInd < copyOfRangedListeners.length; rangedInd++) { // Could switch to a while loop.
const { lower, upper, listener } = copyOfRangedListeners[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();
copyOfRangedListeners.shift();
rangedInd--;
}
}
@ -67,21 +58,6 @@ export default class VisUpdateRouter {
}
};
/**
* Adds a listener to a specific frequency bin.
*
* @param {object} freqBinListener the listener for a specific frequency bin.
* @param {number} freqBinListener.freqBin the frequency bin this update listener should listen to.
* @param {VisUpdateRouter~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 }) {
if (this.#binnedListeners[freqBin].includes(listener)) return false;
this.#binnedListeners[freqBin].push(listener);
return true;
}
/**
* Similar to {@link VisUpdateRouter#AddVisualizerBinUpdateListener}, this method adds a listener for to a range of bins.
*
@ -91,29 +67,15 @@ export default class VisUpdateRouter {
* @param {VisUpdateRouter~visualizerRangedUpdateListener} rangedUpdateListener.listener The listener to register to the range.
* @returns {boolean} True if and only if the ranged listener was added successfully.
*/
addVisualizerRangedUpdateListener({ lower, upper, listener }) {
addUpdateListener({ 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 {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 {VisUpdateRouter~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 }) {
const removeIndex = this.#binnedListeners[freqBin].indexOf(listener);
if (removeIndex < 0) return false;
this.#binnedListeners[freqBin].splice(removeIndex, 1);
if (this.listeners.includes(rangedListener)) return false;
this.listeners.push(rangedListener);
this.listeners.sort((a, b) => a.lower - b.lower);
return true;
}
@ -126,52 +88,36 @@ export default class VisUpdateRouter {
* @param {VisUpdateRouter~visualizerRangedUpdateListener} rangedListener.listener The update listener to remove from the bins.
* @returns {boolean} True if and only if the given listener was removed.
*/
removeVisualizerRangedUpdateListener({ lower, upper, listener }) {
removeUpdateListener({ lower, upper, listener }) {
const rangedListener = {
lower: lower,
upper: upper,
listener: listener
};
const removeIndex = this.#rangedListeners.indexOf(rangedListener);
const removeIndex = this.listeners.indexOf(rangedListener);
if (removeIndex < 0) return false;
this.#binnedListeners.splice(removeIndex, 1);
this.listeners.splice(removeIndex, 1);
return true;
}
/**
* @param {object} listeners The listeners that this visualizer update manager should be set to use.
* @param {visualizerBinUpdateListener[][]} listeners.binned The bin listeners.
* @param {object[]} listeners.ranged The array of ranged listeners.
* @param {number} listeners.ranged[].lower The lower bound of the listener.
* @param {number} listeners.ranged[].upper The upper bound of the listener.
* @param {VisUpdateRouter~visualizerRangedUpdateListener} listeners.ranged[].listener The listener for the previously mentioned ranges.
* @param {object[]} listeners The listeners that this visualizer update manager should be set to use.
* @param {number} listeners[].lower The lower bound of the listener.
* @param {number} listeners[].upper The upper bound of the listener.
* @param {VisUpdateRouter~visualizerRangedUpdateListener} listeners[].listener The listener for the previously mentioned ranges.
* @returns {boolean} true if and only if successfully loaded the new listeners.
*/
loadListeners(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);
this.listeners = listeners.sort((a, b) => a.lower - b.lower);
return true;
}
/**
*
* @returns {object} All the listeners, both for binned, and ranged listeners. See {@link VisUpdateRouter#loadListeners} to see the structure of the returned object.
* @returns {object[]} All the listeners, both for binned, and ranged listeners. See {@link VisUpdateRouter#loadListeners} to see the structure of the returned object.
*/
retrieveListeners() {
return {
binned: this.#binnedListeners,
ranged: this.#rangedListeners
};
}
/**
* Clears this manager of all listeners.
*/
clearBinnedListeners() {
this.#binnedListeners.forEach(bin => {
bin.length = 0;
});
return this.listeners;
}
/**

View File

@ -7,9 +7,9 @@
<p>This is the fun part. We can use a {@link VisualizedMusicPlayer} and a {@link MusicPlaylist} to create a music player that is like {@link MusicPlayer} but with the ability to automatically fetch the current {@link Visualizer}. On top of that, it then routes that visualizer data to {@link VisualizerUpdateManager} which can be to make much more refined mappings.</p>
This library comes with a variety of mapping tools:
<ul>
<li>Want to map ranges of frequency bins to a plethora of element style properties? Take a look at {@link module:mappings/numeric} and {@link module:mappings/dimensions}!</li>
<li>Want to map ranges of frequency bins to width, height, and other dimensions related properties? Take a look at {@link module:mappings/numeric} and {@link module:mappings/dimensions}!</li>
<li>Check out {@link module:patterns/canvas} for built in canvas patterns.</li>
<li>We even do font size and color with the {@link module:mappings/text} module!</li>
<li>We even do font color in the {@link module:mappings/coloring} module!</li>
</ul>
</div>
<div class="part">
@ -41,7 +41,7 @@
<p>To do this, we need to perform what's called a <strong>mapping</strong> between a range of frequency bins, or a single frequency bin, and the width property of the <code>div</code> element. We can then define a multitude of parameters to specify how the mapping will work. Following is the code that produced the example above with comment annotations.</p>
<pre><code class="language-js">
const widthElem = document.getElementById("width-map-demo"); // selecting an element.
ask.mappings.dimensions.mapWidth({ // the mapping function for width.
ask.mappings.dimensions.width({ // the mapping function for width.
element: widthElem, // the element to map.
growLower: 2, // the minimum width
growUpper: 8, // the maximum width
@ -61,7 +61,7 @@
</div>
<pre><code class="language-js">
const heightElem = document.getElementById("height-map-demo"); // Only big difference is the function being called.
ask.mappings.dimensions.mapHeight({
ask.mappings.dimensions.height({
element: heightElem,
growLower: 2, // height smallest can be 2 rem, tallest can be 8 rem.
growUpper: 8,
@ -91,8 +91,8 @@
visUpdateRouter: player.visUpdateRouter,
interpolator: ask.support.easings.createEaseLinear(2.5)
}
ask.mappings.dimensions.mapWidth(squareElemConf); // Apply them easily!
ask.mappings.dimensions.mapHeight(squareElemConf);
ask.mappings.dimensions.width(squareElemConf); // Apply them easily!
ask.mappings.dimensions.height(squareElemConf);
</code></pre>
</div>
<div class="part">
@ -105,7 +105,7 @@
</div>
<pre><code class="language-js">
const fontColorElem = document.getElementById("font-color-map-demo");
ask.mappings.coloring.mapFontRgba({ // Under mappings, the text module. We just want to map one of the RGBA color components...
ask.mappings.coloring.fontColorRgba({ // Under mappings, the text module. We just want to map one of the RGBA color components...
element: fontColorElem, // The element to map (same as above examples).
color: "r", // Choose the red component.
lowerBin: 128, // All other values are what we've seen above.
@ -135,7 +135,7 @@
playbackCtrlsElem.appendChild(nextElem);
const widthElem = document.getElementById("width-map-demo");
ask.mappings.dimensions.mapWidth({
ask.mappings.dimensions.width({
element: widthElem,
growLower: 2,
growUpper: 8,
@ -147,7 +147,7 @@
});
const heightElem = document.getElementById("height-map-demo");
ask.mappings.dimensions.mapHeight({
ask.mappings.dimensions.height({
element: heightElem,
growLower: 2,
growUpper: 8,
@ -169,11 +169,11 @@
visUpdateRouter: player.visUpdateRouter,
interpolator: ask.support.easings.createEaseLinear(2.5)
}
ask.mappings.dimensions.mapWidth(squareElemConf);
ask.mappings.dimensions.mapHeight(squareElemConf);
ask.mappings.dimensions.width(squareElemConf);
ask.mappings.dimensions.height(squareElemConf);
const fontColorElem = document.getElementById("font-color-map-demo");
ask.mappings.coloring.mapFontRgba({
ask.mappings.coloring.fontColorRgba({
element: fontColorElem,
color: "r",
lowerBin: 128,