diff --git a/src/mappings/coloring.js b/src/mappings/coloring.js index 3bbec1b..8882f40 100644 --- a/src/mappings/coloring.js +++ b/src/mappings/coloring.js @@ -1,4 +1,4 @@ -import { parseColor, rgbaToHexRgba } from "../support/colors.js"; +import { hslaToCssHsla, hslaToRgba, parseColorToHsla, parseColorToRgba, rgbaToCssRgba, rgbaToHexRgba, rgbaToHsla } from "../support/colors.js"; import VisUpdateRouter from "../visualization/VisUpdateRouter.js"; import { numerical } from "./numeric.js"; @@ -10,7 +10,7 @@ import { numerical } from "./numeric.js"; * * @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 {string} 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. @@ -26,12 +26,20 @@ export function backgroundColorRgba({ element, color, lowerBin, visUpdateRouter, 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]; + return parseColorToRgba(element.style.backgroundColor)[color]; }; const setter = (value) => { - const changed = parseColor(element.style.backgroundColor); + const changed = parseColorToRgba(element.style.backgroundColor); changed[color] = value; - element.style.backgroundColor = rgbaToHexRgba(changed); + if (element.style.backgroundColor.startsWith("rgb")) { + element.style.backgroundColor = rgbaToCssRgba(changed); + } else if (element.style.backgroundColor.startsWith("hsl")) { + element.style.backgroundColor = hslaToCssHsla(rgbaToHsla(changed)); + } else if (element.style.backgroundColor.startsWith("#")) { + element.style.backgroundColor = rgbaToHexRgba(changed); + } else { + element.style.backgroundColor = rgbaToHexRgba(changed); + } }; upperVal = Math.min(Math.max(0, upperVal), 255); lowerVal = Math.min(Math.max(0, lowerVal), upperVal); @@ -49,12 +57,71 @@ export function backgroundColorRgba({ element, color, lowerBin, visUpdateRouter, return numerical(conf); } +/** + * 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. + * @param {number} conf.select Where h for hue, s, for saturation, l for lightness, 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 360 if conf.color is "h", 1 if conf.color is "s" or "l", and 255 if conf.color is "a"). + * @returns {{lower: number, upper: number, listener: VisUpdateRouter~visualizerRangedUpdateListener}} The ranged listener that was added. + */ +export function backgroundColorHsla({ element, select, lowerBin, visUpdateRouter, interpolator, upperBin = undefined, reversed = false, lowerVal = 0, upperVal = undefined }) { + const rgbaStr = "hsla"; + select = rgbaStr.indexOf(select); + if (select < 0) throw new Error("Invalid color parameter provided."); + + const getter = () => { + if (!element.style.backgroundColor) element.style.backgroundColor = "hsl(0, 50%, 50%, 255)"; + return parseColorToHsla(element.style.backgroundColor)[select]; + }; + const setter = (value) => { + const changed = parseColorToHsla(element.style.backgroundColor); + changed[select] = value; + if (element.style.backgroundColor.startsWith("hsl")) { + element.style.backgroundColor = hslaToCssHsla(changed); + } else if (element.style.backgroundColor.startsWith("rgb")) { + element.style.backgroundColor = rgbaToCssRgba(hslaToRgba(changed)); + } else if (element.style.backgroundColor.startsWith("#")) { + element.style.backgroundColor = rgbaToHexRgba(hslaToRgba(changed)); + } else { + element.style.backgroundColor = rgbaToHexRgba(hslaToRgba(changed)); // If we don't recognize the currently used color function, then just use hex. + } + }; + let upperBound = 360; + if (select === "s" || select === "l") { + upperBound = 1; + } else if (select === "a") { + upperBound = 255; + } + upperVal = Math.min(Math.max(0, upperVal), upperBound); + 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 {string} 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. @@ -70,12 +137,20 @@ export function fontColorRgba({ element, color, lowerBin, visUpdateRouter, inter if (color < 0) throw new Error("Invalid color parameter provided."); const getter = () => { if (!element.style.color) element.style.color = "rgba(0, 0, 0, 255)"; - return parseColor(element.style.color)[color]; + return parseColorToRgba(element.style.color)[color]; }; const setter = (value) => { - const changed = parseColor(element.style.color); + const changed = parseColorToRgba(element.style.color); changed[color] = value; - element.style.color = rgbaToHexRgba(changed); + if (element.style.color.startsWith("rgb")) { + element.style.color = rgbaToCssRgba(changed); + } else if (element.style.color.startsWith("hsl")) { + element.style.color = hslaToCssHsla(rgbaToHsla(changed)); + } else if (element.style.color.startsWith("#")) { + element.style.color = rgbaToHexRgba(changed); + } else { + element.style.color = rgbaToHexRgba(changed); + } }; upperVal = Math.min(Math.max(0, upperVal), 255); lowerVal = Math.min(Math.max(0, lowerVal), upperVal); diff --git a/src/support/colors.js b/src/support/colors.js index 20a8381..0bd528a 100644 --- a/src/support/colors.js +++ b/src/support/colors.js @@ -45,7 +45,7 @@ export function rgbHexToRgba(hex) { */ export function cssRgbaToRgba(rgba) { rgba = rgba.replaceAll(" ", ""); - const cssRgbaRegex = /rgba\((\d+),(\d+),(\d+),(\d+)\)/; + const cssRgbaRegex = /rgba\((\d+)[, ] ?(\d+)[, ] ?(\d+)[, ] ?(\d+)\)/; try { const matches = rgba.match(cssRgbaRegex); return [parseInt(matches[1]), parseInt(matches[2]), parseInt(matches[3]), parseInt(matches[4])]; @@ -61,8 +61,7 @@ export function cssRgbaToRgba(rgba) { * @returns {number[]} the rgba components. */ export function cssRgbToRgba(rgb) { - rgb = rgb.replaceAll(" ", ""); - const cssRgbRegex = /rgb\((\d+),(\d+),(\d+)\)/; + const cssRgbRegex = /rgb\((\d+)[, ] ?(\d+)[, ] ?(\d+)\)/; try { const matches = rgb.match(cssRgbRegex); return [parseInt(matches[1]), parseInt(matches[2]), parseInt(matches[3]), 255]; @@ -84,14 +83,14 @@ export function rgbaToHexRgba(rgba) { /** - * Converts a css rgb, rgba, hex, or rgba hex to a rgba array. + * Converts a css rgb, rgba, hex, hsl(a), or rgba hex to a rgba array. * * For hex, we assume there is no alpha channel unless the hex value is not minimized. * * @param {string} color The string that contains the color information. * @returns {number[]} an array that contains the r, g, b and a components. */ -export function parseColor(color) { +export function parseColorToRgba(color) { if (color.startsWith("rgba(")) { return cssRgbaToRgba(color); } else if (color.startsWith("rgb(")) { @@ -102,6 +101,124 @@ export function parseColor(color) { } else { return rgbHexToRgba(color); } + } else if (color.startsWith("hsl(")) { + return hslaToRgba(cssHslaToHsla(color)); } throw new Error("Could not parse to an rgba value."); } + +/** + * Converts a css rgb, rgba, hex, hsl(a), or rgba hex to an hsla array. + * + * @param {string} color The string that contains the color information. + * @returns {number[]} An array that contains the h, s, l and alpha channel components. + */ +export function parseColorToHsla(color) { + if (color.startsWith("rgba(")) { + return rgbaToHsla(cssRgbaToRgba(color)); + } else if (color.startsWith("rgb(")) { + return rgbaToHsla(cssRgbToRgba(color)); + } else if (color.startsWith("#")) { + if (color.length === 9) { + return rgbaToHsla(rgbaHexToRgba(color)); + } else { + return rgbaToHsla(rgbHexToRgba(color)); + } + } else if (color.startsWith("hsl(")) { + return cssHslaToHsla(color); + } +} + +/** + * Converts a given rgb value to hsla. Alpha value is simply passed through. + * + * @param {number[]} rgba An array that contains numbers representing the r, g, b, and a values respectively. + * @returns {number[]} An array that contains the h, s, l, and a values. + */ +export function rgbaToHsla(rgba) { // Used GeeksForGeeks implementation + let r = rgba[0] / 255; + let g = rgba[1] / 255; + let b = rgba[2] / 255; + + const cmax = Math.max(r, g, b); + const cmin = Math.min(r, g, b); + const delta = cmax - cmin; + + let h = 0; + if (delta === 0) { + h = 0; + } else if (cmax === r) { + h = 60 * (((g - b) / delta) % 6); + } else if (cmax === g) { + h = 60 * (2 + (b - r) / delta); + } else if (cmax === b) { + h = 60 * (4 + (r - g) / delta); + } + + let l = (cmax + cmin) / 2; + + let s = 0; + if (delta === 0) { + s = 0; + } else { + s = delta / (1 - Math.abs(2 * l - 1)); + } + + return [h, s, l, rgba[3]]; +} + +/** + * Converts a given hsv value to rgb. The alpha channel value is just passed through. + * + * @param {number[]} hsla The HSL and alpha component array. + * @returns {number[]} An array comprised of r, g, b and a values converted from the given HSL. + */ +export function hslaToRgba(hsla) { + const h = hsla[0]; + const s = hsla[1]; + const l = hsla[2]; + + const fn = (n) => { + const k = ((n + (h / 30)) % 12); + const a = s * Math.min(l, 1 - l); + return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); + }; + + return [Math.round(fn(0) * 255), Math.round(fn(8) * 255), Math.round(fn(4) * 255), hsla[3]]; +} + +/** + * Parses a CSS color property coded in hsl. + * + * @param {string} cssHsla The style property value in HSL(A). + * @returns {number[]} An array that contains the parsed H, S, L and alpha channel. + */ +export function cssHslaToHsla(cssHsla) { + const cssHslaRegex = /hsl\((\d+)[, ] ?(\d+)%[, ] ?(\d+)% ?[, /]? ?(\d+)?\)/; + try { + const matches = cssHsla.match(cssHslaRegex); + return [parseInt(matches[1]), parseInt(matches[2]) / 100, parseInt(matches[3]) / 100, isNaN(matches[4]) ? 255 : parseInt(matches[4])]; + } catch (error) { + throw new Error("Could not parse the given HSL(a) value: " + error.message); + } +} + +/** + * Converts an array containing the H, S, L and alpha values to a css hsl function call. + * + * @param {number[]} hsla An array containing the H, S, L and alpha channel values. + * @returns {string} The css hsl function call. + */ +export function hslaToCssHsla(hsla) { + return "hsl(" + hsla[0] + ", " + (hsla[1] * 100) + "%, " + (hsla[2] * 100) + "%, " + hsla[3]; +} + +/** + * Converts an array containing the R, G, B and alpha values to a css rgba function call. + * + * @param {number[]} rgba An array whose elements are the rgba components to encode into a css rgba function call. + * @returns {string} The css rgba function call. + */ +export function rgbaToCssRgba(rgba) { + return "rgba(" + Math.round(rgba[0]) + ", " + Math.round(rgba[1]) + ", " + Math.round(rgba[2]) + ", " + Math.round(rgba[3]) + ")"; +} \ No newline at end of file diff --git a/tests/testColors.js b/tests/testColors.js index 0e8289d..107856a 100644 --- a/tests/testColors.js +++ b/tests/testColors.js @@ -1,7 +1,7 @@ /* eslint-disable no-undef */ import { expect } from "chai"; import { describe } from "mocha"; -import { cssRgbaToRgba, cssRgbToRgba, rgbaHexToRgba, parseColor, rgbaToHexRgba, rgbHexToRgba } from "../src/support/colors.js"; +import { cssRgbaToRgba, cssRgbToRgba, rgbaHexToRgba, parseColorToRgba, rgbaToHexRgba, rgbHexToRgba, rgbaToHsla, hslaToRgba, cssHslaToHsla, hslaToCssHsla } from "../src/support/colors.js"; describe("Color utilities", function () { describe("the hex to rgba conversion function", function () { @@ -83,6 +83,11 @@ describe("Color utilities", function () { const rgba = cssRgbToRgba("rgb(1,1,1)"); expect(rgba).to.have.ordered.members([1, 1, 1, 255]); }); + + it("returns 1, 1 and 1 for r, g and b given \"rgb(1, 1, 1)\" (contains spaces)", function () { + const rgba = cssRgbToRgba("rgb(1, 1, 1)"); + expect(rgba).to.have.ordered.members([1, 1, 1, 255]); + }); }); describe("The function to convert r, g, b, and a represented as an array to hexadecimals", function () { @@ -106,22 +111,72 @@ describe("Color utilities", function () { describe("The function that automatically converts a string containing an rgb(a) value to an rgba array.", function () { it("returns 1, 1, 1 and 1 for r, g, b and a given \"rgba(1,1,1,1)\"", function () { - const rgba = parseColor("rgba(1,1,1,1)"); + const rgba = parseColorToRgba("rgba(1,1,1,1)"); expect(rgba).to.have.ordered.members([1, 1, 1, 1]); }); it("returns 0, 1, 0 and 1 for r, g, b and a given \"#00010001\"", function () { - const rgb = parseColor("#00010001"); + const rgb = parseColorToRgba("#00010001"); expect(rgb).to.have.ordered.members([0, 1, 0, 1]); }); it("returns 1, 0, 1 and 255 for r, g, b and a given \"#10001\"", function () { - const rgb = parseColor("#10001"); + const rgb = parseColorToRgba("#10001"); expect(rgb).to.have.ordered.members([1, 0, 1, 255]); }); it("returns 1, 1, 1 and 255 for r, g, b and a given \"rgb(1,1,1)\"", function () { - const rgba = parseColor("rgb(1,1,1)"); + const rgba = parseColorToRgba("rgb(1,1,1)"); expect(rgba).to.have.ordered.members([1, 1, 1, 255]); }); + it("returns 1, 1, 1 and 1 for r, g, b and a given \"rgba(1, 1, 1, 1)\" (contains spaces)", function () { + const rgba = parseColorToRgba("rgba(1, 1, 1, 1)"); + expect(rgba).to.have.ordered.members([1, 1, 1, 1]); + }); }); + describe("The function that converts a rgba array to hsl and a values.", function () { + it("returns (approx.) 193, 100% and 50% for H, S, and V components given [0, 200, 255, 255]", function () { + const [h, s, v, a] = rgbaToHsla([0, 200, 255, 255]); + expect(h).to.be.closeTo(193, 0.1); + expect(s).to.equal(1); + expect(v).to.equal(0.5); + expect(a).to.equal(255); + }); + it("returns (approx.) 120, 100% and 70% for H, S, and V components given [102, 255, 102, 255]", function () { + const [h, s, v, a] = rgbaToHsla([102, 255, 102, 255]); + expect(h).to.be.closeTo(120, 0.1); + expect(s).to.be.closeTo(1, 0.0001); + expect(v).to.equal(0.7); + expect(a).to.equal(255); + }); + }); + describe("The function that converts a hsva array to rgba values.", function () { + it("Returns (approx.) 0, 200, 255 and 255 for r, g, b and a given 193, 100%, 50% and 255.", function () { + const [r, g, b, a] = hslaToRgba([193, 1, 0.5, 255]); + expect(r).to.be.closeTo(0, 1); + expect(g).to.be.closeTo(200, 1); + expect(b).to.be.closeTo(255, 1); + expect(a).to.equal(255); + }); + it("Returns 61, 71, 77 and 255 for r, g, b and a given 200, 20%, 30% and 255.", function () { + const [r, g, b, a] = hslaToRgba([203, 0.116, 0.271, 255]); + expect(r).to.be.closeTo(61, 0.2); + expect(g).to.be.closeTo(71, 0.2); + expect(b).to.be.closeTo(77, 0.2); + expect(a).to.equal(255); + }); + }); + + describe("The function that converts an css hsl function call to an array containing the individual h, s, l and a components.", function () { + it("Returns [1, 0.01, 0.01, 1] given \"hsl(1, 1%, 1%, 1)\"", function () { + const hsla = cssHslaToHsla("hsl(1, 1%, 1%, 1)"); + expect(hsla).to.have.ordered.members([1, 0.01, 0.01, 1]); + }); + }); + + describe("The function that converts an array representing hsla values to a css hsl function call.", function () { + it("Returns \"hsl(1, 1%, 1%, 1)\" given [1, 0.01, 0.01, 1].", function () { + const cssHsla = hslaToCssHsla([1, 0.01, 0.01, 1]); + expect(cssHsla).to.equal("hsl(1, 1%, 1%, 1"); + }); + }); }); \ No newline at end of file