vuetify
Version:
Vue Material Component Framework
321 lines (315 loc) • 8.48 kB
JavaScript
// Utilities
import { APCAcontrast } from "./color/APCA.js";
import { consoleWarn } from "./console.js";
import { chunk, has, padEnd } from "./helpers.js";
import * as CIELAB from "./color/transformCIELAB.js";
import * as sRGB from "./color/transformSRGB.js"; // Types
export function isCssColor(color) {
return !!color && /^(#|var\(--|(rgb|hsl)a?\()/.test(color);
}
export function isParsableColor(color) {
return isCssColor(color) && !/^((rgb|hsl)a?\()?var\(--/.test(color);
}
const cssColorRe = /^(?<fn>(?:rgb|hsl)a?)\((?<values>.+)\)/;
const mappers = {
rgb: (r, g, b, a) => ({
r,
g,
b,
a
}),
rgba: (r, g, b, a) => ({
r,
g,
b,
a
}),
hsl: (h, s, l, a) => HSLtoRGB({
h,
s,
l,
a
}),
hsla: (h, s, l, a) => HSLtoRGB({
h,
s,
l,
a
}),
hsv: (h, s, v, a) => HSVtoRGB({
h,
s,
v,
a
}),
hsva: (h, s, v, a) => HSVtoRGB({
h,
s,
v,
a
})
};
export function parseColor(color) {
if (typeof color === 'number') {
if (isNaN(color) || color < 0 || color > 0xFFFFFF) {
// int can't have opacity
consoleWarn(`'${color}' is not a valid hex color`);
}
return {
r: (color & 0xFF0000) >> 16,
g: (color & 0xFF00) >> 8,
b: color & 0xFF
};
} else if (typeof color === 'string' && cssColorRe.test(color)) {
const {
groups
} = color.match(cssColorRe);
const {
fn,
values
} = groups;
const realValues = values.split(/,\s*|\s*\/\s*|\s+/).map((v, i) => {
if (v.endsWith('%') ||
// unitless slv are %
i > 0 && i < 3 && ['hsl', 'hsla', 'hsv', 'hsva'].includes(fn)) {
return parseFloat(v) / 100;
} else {
return parseFloat(v);
}
});
return mappers[fn](...realValues);
} else if (typeof color === 'string') {
let hex = color.startsWith('#') ? color.slice(1) : color;
if ([3, 4].includes(hex.length)) {
hex = hex.split('').map(char => char + char).join('');
} else if (![6, 8].includes(hex.length)) {
consoleWarn(`'${color}' is not a valid hex(a) color`);
}
const int = parseInt(hex, 16);
if (isNaN(int) || int < 0 || int > 0xFFFFFFFF) {
consoleWarn(`'${color}' is not a valid hex(a) color`);
}
return HexToRGB(hex);
} else if (typeof color === 'object') {
if (has(color, ['r', 'g', 'b'])) {
return color;
} else if (has(color, ['h', 's', 'l'])) {
return HSVtoRGB(HSLtoHSV(color));
} else if (has(color, ['h', 's', 'v'])) {
return HSVtoRGB(color);
}
}
throw new TypeError(`Invalid color: ${color == null ? color : String(color) || color.constructor.name}\nExpected #hex, #hexa, rgb(), rgba(), hsl(), hsla(), object or number`);
}
export function RGBToInt(color) {
return (color.r << 16) + (color.g << 8) + color.b;
}
export function classToHex(color, colors, currentTheme) {
const [colorName, colorModifier] = color.toString().trim().replace('-', '').split(' ', 2);
let hexColor = '';
if (colorName && colorName in colors) {
if (colorModifier && colorModifier in colors[colorName]) {
hexColor = colors[colorName][colorModifier];
} else if ('base' in colors[colorName]) {
hexColor = colors[colorName].base;
}
} else if (colorName && colorName in currentTheme) {
hexColor = currentTheme[colorName];
}
return hexColor;
}
/** Converts HSVA to RGBA. Based on formula from https://en.wikipedia.org/wiki/HSL_and_HSV */
export function HSVtoRGB(hsva) {
const {
h,
s,
v,
a
} = hsva;
const f = n => {
const k = (n + h / 60) % 6;
return v - v * s * Math.max(Math.min(k, 4 - k, 1), 0);
};
const rgb = [f(5), f(3), f(1)].map(v => Math.round(v * 255));
return {
r: rgb[0],
g: rgb[1],
b: rgb[2],
a
};
}
export function HSLtoRGB(hsla) {
return HSVtoRGB(HSLtoHSV(hsla));
}
/** Converts RGBA to HSVA. Based on formula from https://en.wikipedia.org/wiki/HSL_and_HSV */
export function RGBtoHSV(rgba) {
if (!rgba) return {
h: 0,
s: 1,
v: 1,
a: 1
};
const r = rgba.r / 255;
const g = rgba.g / 255;
const b = rgba.b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0;
if (max !== min) {
if (max === r) {
h = 60 * (0 + (g - b) / (max - min));
} else if (max === g) {
h = 60 * (2 + (b - r) / (max - min));
} else if (max === b) {
h = 60 * (4 + (r - g) / (max - min));
}
}
if (h < 0) h = h + 360;
const s = max === 0 ? 0 : (max - min) / max;
const hsv = [h, s, max];
return {
h: hsv[0],
s: hsv[1],
v: hsv[2],
a: rgba.a
};
}
export function HSVtoHSL(hsva) {
const {
h,
s,
v,
a
} = hsva;
const l = v - v * s / 2;
const sprime = l === 1 || l === 0 ? 0 : (v - l) / Math.min(l, 1 - l);
return {
h,
s: sprime,
l,
a
};
}
export function HSLtoHSV(hsl) {
const {
h,
s,
l,
a
} = hsl;
const v = l + s * Math.min(l, 1 - l);
const sprime = v === 0 ? 0 : 2 - 2 * l / v;
return {
h,
s: sprime,
v,
a
};
}
export function RGBtoCSS(_ref) {
let {
r,
g,
b,
a
} = _ref;
return a === undefined ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a})`;
}
export function HSVtoCSS(hsva) {
return RGBtoCSS(HSVtoRGB(hsva));
}
function toHex(v) {
const h = Math.round(v).toString(16);
return ('00'.substr(0, 2 - h.length) + h).toUpperCase();
}
export function RGBtoHex(_ref2) {
let {
r,
g,
b,
a
} = _ref2;
return `#${[toHex(r), toHex(g), toHex(b), a !== undefined ? toHex(Math.round(a * 255)) : ''].join('')}`;
}
export function HexToRGB(hex) {
hex = parseHex(hex);
let [r, g, b, a] = chunk(hex, 2).map(c => parseInt(c, 16));
a = a === undefined ? a : a / 255;
return {
r,
g,
b,
a
};
}
export function HexToHSV(hex) {
const rgb = HexToRGB(hex);
return RGBtoHSV(rgb);
}
export function HSVtoHex(hsva) {
return RGBtoHex(HSVtoRGB(hsva));
}
export function parseHex(hex) {
if (hex.startsWith('#')) {
hex = hex.slice(1);
}
hex = hex.replace(/([^0-9a-f])/gi, 'F');
if (hex.length === 3 || hex.length === 4) {
hex = hex.split('').map(x => x + x).join('');
}
if (hex.length !== 6) {
hex = padEnd(padEnd(hex, 6), 8, 'F');
}
return hex;
}
export function parseGradient(gradient, colors, currentTheme) {
return gradient.replace(/([a-z]+(\s[a-z]+-[1-5])?)(?=$|,)/gi, x => {
return classToHex(x, colors, currentTheme) || x;
}).replace(/(rgba\()#[0-9a-f]+(?=,)/gi, x => {
return 'rgba(' + Object.values(HexToRGB(parseHex(x.replace(/rgba\(/, '')))).slice(0, 3).join(',');
});
}
export function lighten(value, amount) {
const lab = CIELAB.fromXYZ(sRGB.toXYZ(value));
lab[0] = lab[0] + amount * 10;
return sRGB.fromXYZ(CIELAB.toXYZ(lab));
}
export function darken(value, amount) {
const lab = CIELAB.fromXYZ(sRGB.toXYZ(value));
lab[0] = lab[0] - amount * 10;
return sRGB.fromXYZ(CIELAB.toXYZ(lab));
}
/**
* Calculate the relative luminance of a given color
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
*/
export function getLuma(color) {
const rgb = parseColor(color);
return sRGB.toXYZ(rgb)[1];
}
/**
* Returns the contrast ratio (1-21) between two colors.
* @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef
*/
export function getContrast(first, second) {
const l1 = getLuma(first);
const l2 = getLuma(second);
const light = Math.max(l1, l2);
const dark = Math.min(l1, l2);
return (light + 0.05) / (dark + 0.05);
}
export function getForeground(color) {
const blackContrast = Math.abs(APCAcontrast(parseColor(0), parseColor(color)));
const whiteContrast = Math.abs(APCAcontrast(parseColor(0xffffff), parseColor(color)));
// TODO: warn about poor color selections
// const contrastAsText = Math.abs(APCAcontrast(colorVal, colorToInt(theme.colors.background)))
// const minContrast = Math.max(blackContrast, whiteContrast)
// if (minContrast < 60) {
// consoleInfo(`${key} theme color ${color} has poor contrast (${minContrast.toFixed()}%)`)
// } else if (contrastAsText < 60 && !['background', 'surface'].includes(color)) {
// consoleInfo(`${key} theme color ${color} has poor contrast as text (${contrastAsText.toFixed()}%)`)
// }
// Prefer white text if both have an acceptable contrast ratio
return whiteContrast > Math.min(blackContrast, 50) ? '#fff' : '#000';
}
//# sourceMappingURL=colorUtils.js.map