@qooxdoo/framework
Version:
The JS Framework for Coders
929 lines (845 loc) • 26.7 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
* Andreas Ecker (ecker)
* Christian Hagendorn (cs)
************************************************************************ */
/**
* Methods to convert colors between different color spaces.
*
* @ignore(qx.theme.*)
* @ignore(qx.Class)
* @ignore(qx.Class.*)
*/
qx.Bootstrap.define("qx.util.ColorUtil", {
statics: {
/**
* Regular expressions for color strings
*/
REGEXP: {
hexShort: /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])?$/,
hexLong:
/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})?$/,
hex3: /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/,
hex6: /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/,
rgb: /^rgb\(\s*([0-9]{1,3}\.{0,1}[0-9]*)\s*,\s*([0-9]{1,3}\.{0,1}[0-9]*)\s*,\s*([0-9]{1,3}\.{0,1}[0-9]*)\s*\)$/,
rgba: /^rgba\(\s*([0-9]{1,3}\.{0,1}[0-9]*)\s*,\s*([0-9]{1,3}\.{0,1}[0-9]*)\s*,\s*([0-9]{1,3}\.{0,1}[0-9]*)\s*,\s*([0-9]{1,3}\.{0,2}[0-9]*)\s*\)$/
},
/**
* CSS3 system color names.
*/
SYSTEM: {
activeborder: true,
activecaption: true,
appworkspace: true,
background: true,
buttonface: true,
buttonhighlight: true,
buttonshadow: true,
buttontext: true,
captiontext: true,
graytext: true,
highlight: true,
highlighttext: true,
inactiveborder: true,
inactivecaption: true,
inactivecaptiontext: true,
infobackground: true,
infotext: true,
menu: true,
menutext: true,
scrollbar: true,
threeddarkshadow: true,
threedface: true,
threedhighlight: true,
threedlightshadow: true,
threedshadow: true,
window: true,
windowframe: true,
windowtext: true
},
/**
* Named colors, only the 16 basic colors plus the following ones:
* transparent, grey, magenta, orange and brown
*/
NAMED: {
black: [0, 0, 0],
silver: [192, 192, 192],
gray: [128, 128, 128],
white: [255, 255, 255],
maroon: [128, 0, 0],
red: [255, 0, 0],
purple: [128, 0, 128],
fuchsia: [255, 0, 255],
green: [0, 128, 0],
lime: [0, 255, 0],
olive: [128, 128, 0],
yellow: [255, 255, 0],
navy: [0, 0, 128],
blue: [0, 0, 255],
teal: [0, 128, 128],
aqua: [0, 255, 255],
// Additional values
transparent: [-1, -1, -1],
magenta: [255, 0, 255], // alias for fuchsia
orange: [255, 165, 0],
brown: [165, 42, 42]
},
/**
* Whether the incoming value is a named color.
*
* @param value {String} the color value to test
* @return {Boolean} true if the color is a named color
*/
isNamedColor(value) {
return this.NAMED[value] !== undefined;
},
/**
* Whether the incoming value is a system color.
*
* @param value {String} the color value to test
* @return {Boolean} true if the color is a system color
*/
isSystemColor(value) {
return this.SYSTEM[value] !== undefined;
},
/**
* Whether the color theme manager is loaded. Generally
* part of the GUI of qooxdoo.
*
* @return {Boolean} <code>true</code> when color theme support is ready.
**/
supportsThemes() {
if (qx.Class) {
return qx.Class.isDefined("qx.theme.manager.Color");
}
return false;
},
/**
* Whether the incoming value is a themed color.
*
* @param value {String} the color value to test
* @return {Boolean} true if the color is a themed color
*/
isThemedColor(value) {
if (!this.supportsThemes()) {
return false;
}
if (qx.theme && qx.theme.manager && qx.theme.manager.Color) {
return qx.theme.manager.Color.getInstance().isDynamic(value);
}
return false;
},
/**
* Try to convert an incoming string to an RGBA array.
* Supports themed, named and system colors, but also RGBA strings,
* hex[3468] values.
*
* @param str {String} any string
* @return {Array} returns an array of red, green, blue and optional alpha on a successful transformation
* @throws {Error} if the string could not be parsed
*/
stringToRgb(str) {
if (this.supportsThemes() && this.isThemedColor(str)) {
str = qx.theme.manager.Color.getInstance().resolveDynamic(str);
}
return this.cssStringToRgb(str);
},
/**
* Try to convert an incoming string to an RGB array with optional alpha.
* Support named colors, RGB strings, RGBA strings, hex[3468] values.
*
* @param str {String} any string
* @return {Array} returns an array of red, green, blue on a successful transformation
* @throws {Error} if the string could not be parsed
*/
cssStringToRgb(str) {
var color;
if (this.isNamedColor(str)) {
color = this.NAMED[str].concat();
} else if (this.isSystemColor(str)) {
throw new Error("Could not convert system colors to RGB: " + str);
} else if (this.isRgbaString(str)) {
color = this.__rgbaStringToRgb(str);
} else if (this.isRgbString(str)) {
color = this.__rgbStringToRgb();
} else if (this.ishexShortString(str)) {
color = this.__hexShortStringToRgb();
} else if (this.ishexLongString(str)) {
color = this.__hexLongStringToRgb();
}
if (color) {
// don't mention alpha if the color is opaque
if (color.length === 3 && color[3] == 1) {
color.pop();
}
return color;
}
throw new Error("Could not parse color: " + str);
},
/**
* Try to convert an incoming string to an RGB string, which can be used
* for all color properties.
* Supports themed, named and system colors, but also RGB strings,
* hexShort and hexLong values.
*
* @param str {String} any string
* @return {String} a RGB string
* @throws {Error} if the string could not be parsed
*/
stringToRgbString(str) {
return this.rgbToRgbString(this.stringToRgb(str));
},
/**
* Converts a RGB array to an RGB string
*
* @param rgb {Array} an array with red, green and blue values and optionally
* an alpha value
* @return {String} an RGB string
*/
rgbToRgbString(rgb) {
return (
"rgb" +
(rgb.length === 4 ? "a" : "") +
"(" +
rgb
.map(function (v) {
return Math.round(v * 1000) / 1000;
})
.join(",") +
")"
);
},
/**
* Converts a RGB array to a hex[68] string
*
* @param rgb {Array} an array with red, green, blue and optional alpha
* @return {String} a hex[68] string (#xxxxxx)
*/
rgbToHexString(rgb) {
return (
"#" +
qx.lang.String.pad(rgb[0].toString(16).toUpperCase(), 2) +
qx.lang.String.pad(rgb[1].toString(16).toUpperCase(), 2) +
qx.lang.String.pad(rgb[2].toString(16).toUpperCase(), 2) +
(rgb.length === 4 && rgb[3] !== 1
? qx.lang.String.pad(
Math.round(rgb[3] * 255)
.toString(16)
.toUpperCase(),
2
)
: "")
);
},
/**
* Detects if a string is a valid qooxdoo color
*
* @param str {String} any string
* @return {Boolean} true when the incoming value is a valid qooxdoo color
*/
isValidPropertyValue(str) {
return (
this.isThemedColor(str) ||
this.isNamedColor(str) ||
this.ishexShortString(str) ||
this.ishexLongString(str) ||
this.isRgbString(str) ||
this.isRgbaString(str)
);
},
/**
* Detects if a string is a valid CSS color string
*
* @param str {String} any string
* @return {Boolean} true when the incoming value is a valid CSS color string
*/
isCssString(str) {
return (
this.isSystemColor(str) ||
this.isNamedColor(str) ||
this.ishexShortString(str) ||
this.ishexLongString(str) ||
this.isRgbString(str) ||
this.isRgbaString(str)
);
},
/**
* Detects if a string is a valid hexShort string
*
* @param str {String} any string
* @return {Boolean} true when the incoming value is a valid hexShort string
*/
ishexShortString(str) {
return this.REGEXP.hexShort.test(str);
},
/**
* Detects if a string is a valid hex3 string
*
* @param str {String} any string
* @return {Boolean} true when the incoming value is a valid hex3 string
*/
isHex3String(str) {
return this.REGEXP.hex3.test(str);
},
/**
* Detects if a string is a valid hex6 string
*
* @param str {String} any string
* @return {Boolean} true when the incoming value is a valid hex6 string
*/
isHex6String(str) {
return this.REGEXP.hex6.test(str);
},
/**
* Detects if a string is a valid hex6/8 string
*
* @param str {String} any string
* @return {Boolean} true when the incoming value is a valid hex8 string
*/
ishexLongString(str) {
return this.REGEXP.hexLong.test(str);
},
/**
* Detects if a string is a valid RGB string
*
* @param str {String} any string
* @return {Boolean} true when the incoming value is a valid RGB string
*/
isRgbString(str) {
return this.REGEXP.rgb.test(str);
},
/**
* Detects if a string is a valid RGBA string
*
* @param str {String} any string
* @return {Boolean} true when the incoming value is a valid RGBA string
*/
isRgbaString(str) {
return this.REGEXP.rgba.test(str);
},
/**
* Converts a regexp object match of a rgb string to an RGBA array.
*
* @return {Array} an array with red, green, blue
*/
__rgbStringToRgb() {
var red = parseInt(RegExp.$1, 10);
var green = parseInt(RegExp.$2, 10);
var blue = parseInt(RegExp.$3, 10);
return [red, green, blue];
},
/**
* Converts a regexp object match of a rgba string to an RGB array.
*
* @return {Array} an array with red, green, blue
*/
__rgbaStringToRgb() {
var red = parseInt(RegExp.$1, 10);
var green = parseInt(RegExp.$2, 10);
var blue = parseInt(RegExp.$3, 10);
var alpha = parseFloat(RegExp.$4, 10);
if (red === 0 && (green === 0) & (blue === 0) && alpha === 0) {
// this is the (pre-alpha) representation of transparency
// in qooxdoo
return [-1, -1, -1];
}
return alpha == 1 ? [red, green, blue] : [red, green, blue, alpha];
},
/**
* Converts a regexp object match of a hexShort string to an RGB array.
*
* @return {Array} an array with red, green, blue
*/
__hexShortStringToRgb() {
var red = parseInt(RegExp.$1, 16) * 17;
var green = parseInt(RegExp.$2, 16) * 17;
var blue = parseInt(RegExp.$3, 16) * 17;
var alpha =
Math.round((parseInt(RegExp.$4 || "f", 16) / 15) * 1000) / 1000;
return alpha == 1 ? [red, green, blue] : [red, green, blue, alpha];
},
/**
* Converts a regexp object match of a hex3 string to an RGB array.
*
* @return {Array} an array with red, green, blue
*/
__hex3StringToRgb() {
var red = parseInt(RegExp.$1, 16) * 17;
var green = parseInt(RegExp.$2, 16) * 17;
var blue = parseInt(RegExp.$3, 16) * 17;
return [red, green, blue];
},
/**
* Converts a regexp object match of a hex6 string to an RGB array.
*
* @return {Array} an array with red, green, blue
*/
__hex6StringToRgb() {
var red = parseInt(RegExp.$1, 16) * 16 + parseInt(RegExp.$2, 16);
var green = parseInt(RegExp.$3, 16) * 16 + parseInt(RegExp.$4, 16);
var blue = parseInt(RegExp.$5, 16) * 16 + parseInt(RegExp.$6, 16);
return [red, green, blue];
},
/**
* Converts a regexp object match of a hexLong string to an RGB array.
*
* @return {Array} an array with red, green, blue
*/
__hexLongStringToRgb() {
var red = parseInt(RegExp.$1, 16);
var green = parseInt(RegExp.$2, 16);
var blue = parseInt(RegExp.$3, 16);
var alpha =
Math.round((parseInt(RegExp.$4 || "ff", 16) / 255) * 1000) / 1000;
return alpha == 1 ? [red, green, blue] : [red, green, blue, alpha];
},
/**
* Converts a hex3 string to an RGB array
*
* @param value {String} a hex3 (#xxx) string
* @return {Array} an array with red, green, blue
*/
hex3StringToRgb(value) {
if (this.isHex3String(value)) {
return this.__hex3StringToRgb(value);
}
throw new Error("Invalid hex3 value: " + value);
},
/**
* Converts a hex3 (#xxx) string to a hex6 (#xxxxxx) string.
*
* @param value {String} a hex3 (#xxx) string
* @return {String} The hex6 (#xxxxxx) string or the passed value when the
* passed value is not an hex3 (#xxx) value.
*/
hex3StringToHex6String(value) {
if (this.isHex3String(value)) {
return this.rgbToHexString(this.hex3StringToRgb(value));
}
return value;
},
/**
* Converts a hex6 string to an RGB array
*
* @param value {String} a hex6 (#xxxxxx) string
* @return {Array} an array with red, green, blue
*/
hex6StringToRgb(value) {
if (this.isHex6String(value)) {
return this.__hex6StringToRgb(value);
}
throw new Error("Invalid hex6 value: " + value);
},
/**
* Converts a hex string to an RGB array
*
* @param value {String} a hexShort (#rgb/#rgba) or hexLong (#rrggbb/#rrggbbaa) string
* @return {Array} an array with red, green, blue and alpha
*/
hexStringToRgb(value) {
if (this.ishexShortString(value)) {
return this.__hexShortStringToRgb(value);
}
if (this.ishexLongString(value)) {
return this.__hexLongStringToRgb(value);
}
throw new Error("Invalid hex value: " + value);
},
/**
* Convert RGB colors to HSB/HSV
*
* @param rgb {Number[]} red, blue and green as array
* @return {Array} an array with hue, saturation and brightness/value
*/
rgbToHsb(rgb) {
var hue, saturation, brightness;
var red = rgb[0];
var green = rgb[1];
var blue = rgb[2];
var cmax = red > green ? red : green;
if (blue > cmax) {
cmax = blue;
}
var cmin = red < green ? red : green;
if (blue < cmin) {
cmin = blue;
}
brightness = cmax / 255.0;
if (cmax != 0) {
saturation = (cmax - cmin) / cmax;
} else {
saturation = 0;
}
if (saturation == 0) {
hue = 0;
} else {
var redc = (cmax - red) / (cmax - cmin);
var greenc = (cmax - green) / (cmax - cmin);
var bluec = (cmax - blue) / (cmax - cmin);
if (red == cmax) {
hue = bluec - greenc;
} else if (green == cmax) {
hue = 2.0 + redc - bluec;
} else {
hue = 4.0 + greenc - redc;
}
hue = hue / 6.0;
if (hue < 0) {
hue = hue + 1.0;
}
}
return [
Math.round(hue * 360),
Math.round(saturation * 100),
Math.round(brightness * 100)
];
},
/**
* Convert HSB/HSV colors to RGB
*
* @param hsb {Number[]} an array with hue, saturation and brightness/value
* @return {Integer[]} an array with red, green, blue
*/
hsbToRgb(hsb) {
var i, f, p, r, t;
var hue = hsb[0] / 360;
var saturation = hsb[1] / 100;
var brightness = hsb[2] / 100;
if (hue >= 1.0) {
hue %= 1.0;
}
if (saturation > 1.0) {
saturation = 1.0;
}
if (brightness > 1.0) {
brightness = 1.0;
}
var tov = Math.floor(255 * brightness);
var rgb = {};
if (saturation == 0.0) {
rgb.red = rgb.green = rgb.blue = tov;
} else {
hue *= 6.0;
i = Math.floor(hue);
f = hue - i;
p = Math.floor(tov * (1.0 - saturation));
r = Math.floor(tov * (1.0 - saturation * f));
t = Math.floor(tov * (1.0 - saturation * (1.0 - f)));
switch (i) {
case 0:
rgb.red = tov;
rgb.green = t;
rgb.blue = p;
break;
case 1:
rgb.red = r;
rgb.green = tov;
rgb.blue = p;
break;
case 2:
rgb.red = p;
rgb.green = tov;
rgb.blue = t;
break;
case 3:
rgb.red = p;
rgb.green = r;
rgb.blue = tov;
break;
case 4:
rgb.red = t;
rgb.green = p;
rgb.blue = tov;
break;
case 5:
rgb.red = tov;
rgb.green = p;
rgb.blue = r;
break;
}
}
return [rgb.red, rgb.green, rgb.blue];
},
/**
* Convert RGB colors to HSL
*
* @param rgb {Number[]} red, blue and green as array
* @return {Array} an array with hue, saturation and lightness
*/
rgbToHsl(rgb) {
var r = rgb[0] / 255;
var g = rgb[1] / 255;
var b = rgb[2] / 255;
// implementation from
// https://stackoverflow.com/questions/2348597/why-doesnt-this-javascript-rgb-to-hsl-code-work/54071699#54071699
var a = Math.max(r, g, b);
var n = a - Math.min(r, g, b);
var f = 1 - Math.abs(a + a - n - 1);
var h =
n &&
(a == r ? (g - b) / n : a == g ? 2 + (b - r) / n : 4 + (r - g) / n);
return [
60 * (h < 0 ? h + 6 : h),
100 * (f ? n / f : 0),
(100 * (a + a - n)) / 2
];
},
/**
* Convert HSL colors to RGB
*
* @param hsl {Number[]} an array with hue, saturation and lightness
* @return {Integer[]} an array with red, green, blue
*/
hslToRgb(hsl) {
var h = hsl[0];
var s = hsl[1] / 100;
var l = hsl[2] / 100;
// implementation from
// https://stackoverflow.com/questions/36721830/convert-hsl-to-rgb-and-hex/54014428#54014428
var a = s * Math.min(l, 1 - l);
var f = function (n) {
var k = (n + h / 30) % 12;
return l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
};
return [f(0), f(8), f(4)].map(function (v) {
return Math.round(v * 2550) / 10;
});
},
/**
* Creates a random color.
*
* @return {String} a valid qooxdoo/CSS rgb color string.
*/
randomColor() {
var r = Math.round(Math.random() * 255);
var g = Math.round(Math.random() * 255);
var b = Math.round(Math.random() * 255);
return this.rgbToRgbString([r, g, b]);
},
/**
* Tune a color string according to the tuneMap
*
* @param color {String} a valid qooxdoo/CSS rgb color string
* @param scaleMap {Map} as described above
* @param tuner {Function} function
* @param hue_tuner {Function} function
* @return {String} a valid CSS rgb color string.*
*/
__tuner(color, tuneMap, tuner, hue_tuner) {
var rgba = this.stringToRgb(color);
for (var key in tuneMap) {
if (tuneMap[key] == 0) {
continue;
}
switch (key) {
case "red":
rgba[0] = tuner(rgba[0], tuneMap[key], 255);
break;
case "green":
rgba[1] = tuner(rgba[1], tuneMap[key], 255);
break;
case "blue":
rgba[2] = tuner(rgba[2], tuneMap[key], 255);
break;
case "alpha":
rgba[3] = tuner(rgba[3] || 1, tuneMap[key], 1);
break;
case "hue":
if (hue_tuner) {
var hsb = this.rgbToHsb(rgba);
hsb[0] = hue_tuner(hsb[0], tuneMap[key]);
var rgb = this.hsbToRgb(hsb);
rgb[3] = rgba[3];
rgba = rgb;
} else {
throw new Error("Invalid key in map: " + key);
}
break;
case "saturation":
var hsb = this.rgbToHsb(rgba);
hsb[1] = tuner(hsb[1], tuneMap[key], 100);
rgb = this.hsbToRgb(hsb);
rgb[3] = rgba[3];
rgba = rgb;
break;
case "brightness":
var hsb = this.rgbToHsb(rgba);
hsb[2] = tuner(hsb[2], tuneMap[key], 100);
rgb = this.hsbToRgb(hsb);
rgb[3] = rgba[3];
rgba = rgb;
break;
case "lightness":
var hsl = this.rgbToHsl(rgba);
hsl[2] = tuner(hsl[2], tuneMap[key], 100);
rgb = this.hslToRgb(hsl);
rgb[3] = rgba[3];
rgba = rgb;
break;
default:
throw new Error("Invalid key in tune map: " + key);
}
}
if (rgba.length === 4) {
if (rgba[3] === undefined || rgba[3] >= 1) {
rgba.pop();
} else if (rgba[3] < 0) {
rgba[3] = 0;
}
}
[0, 1, 2].forEach(function (i) {
if (rgba[i] < 0) {
rgba[i] = 0;
return;
}
if (rgba[i] > 255) {
rgba[i] = 255;
return;
}
});
return this.rgbToRgbString(rgba);
},
/**
* Scale
*
* Scale the given properties of the input color according to the
* configuration given in the `scaleMap`. Each key argument must point to a
* number between -100% and 100% (inclusive). This indicates how far the
* corresponding property should be moved from its original position
* towards the maximum (if the argument is positive) or the minimum (if the
* argument is negative). This means that, for example, `lightness: "50%"`
* will make all colors 50% closer to maximum lightness without making them
* fully white.
*
* Supported keys are:
* `red`, `green`, `blue`, `alpha`, `saturation`,
* `brightness`, `value`, `lightness`.
*
* @param color {String} a valid qooxdoo/CSS rgb color string
* @param scaleMap {Map} as described above
* @return {String} a valid CSS rgb color string.
*/
scale(color, scaleMap) {
return this.__tuner(color, scaleMap, function (value, scale, max) {
if (value > max) {
value = max;
}
if (scale > 0) {
if (scale > 100) {
scale = 100;
}
return value + ((max - value) * scale) / 100;
}
// scale < 0
if (scale < -100) {
scale = -100;
}
return value + (value * scale) / 100;
});
},
/**
* Adjust
*
* Increases or decreases one or more properties of the input color
* by fixed amounts according to the configuration given in the
* `adjustMap`. The value of the corresponding key is added to the
* original value and the final result is adjusted to stay within legal
* bounds. Hue values can go full circle.a1
*
* Supported keys are:
* `red`, `green`, `blue`, `alpha`, `hue`, `saturation`, `brightness`,
* `lightness`
*
* @param color {String} a valid qooxdoo/CSS rgb color string
* @param scaleMap {Map} as described above
* @return {String} a valid CSS rgb color string.
*/
adjust(color, adjustMap) {
return this.__tuner(
color,
adjustMap,
function (value, offset, max) {
value += offset;
if (value > max) {
return max;
}
if (value < 0) {
return 0;
}
return value;
},
function (value, offset) {
value += offset;
while (value >= 360) {
value -= 360;
}
while (value < 0) {
value += 360;
}
return value;
}
);
},
/**
* RgbToLuminance
*
* Calculate the [luminance](https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests) of the given rgb color.
*
* @param color {String} a valid qooxdoo/CSS rgb color string
* @return {Number} luminance
*/
luminance(color) {
var rgb = this.stringToRgb(color);
var lum = function (i) {
var c = rgb[i] / 255;
return c < 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
return 0.2126 * lum(0) + 0.7152 * lum(1) + 0.0722 * lum(2);
},
/**
* contrast
*
* Calculate the contrast of two given rgb colors.
*
* @param back {String} a valid qooxdoo/CSS rgb color string
* @param front {String} a valid qooxdoo/CSS rgb color string
* @return {Number} contrast
*/
contrast(back, front) {
var bl = this.luminance(back) + 0.05;
var fl = this.luminance(front) + 0.5;
return Math.max(bl, fl) / Math.min(bl, fl);
},
/**
* Picks a contrasting color
*
* @param rgb {Number[]|String} the color, either as a string or as an RGB array of 3 numbers
* @param threshold {Number?} the threshold between light and dark outputs, where the range is 0-255, defaults to 128
* @param dark {String?} the colour to use for "dark", defaults to black
* @param light {String?} the colour to use for "light", defaults to white
* @return {String} colour string
*/
chooseContrastingColor(rgb, threshold, dark, light) {
if (typeof rgb == "string") {
rgb = qx.util.ColorUtil.stringToRgb(rgb);
}
var r = rgb[0];
var g = rgb[1];
var b = rgb[2];
if (!threshold) {
threshold = 128;
}
// Combine into the YIQ color space (which gives us a handy scale we can use with a threshold)
var yiq = (r * 299 + g * 587 + b * 114) / 1000;
return yiq >= threshold ? dark || "#000" : light || "#fff";
}
}
});