Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[system] Add color manipulators #26668

Merged
merged 2 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/material-ui-system/src/colorManipulator.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type ColorFormat = 'rgb' | 'rgba' | 'hsl' | 'hsla';
export interface ColorObject {
type: ColorFormat;
values: [number, number, number] | [number, number, number, number];
colorSpace?: 'srgb' | 'display-p3' | 'a98-rgb' | 'prophoto-rgb' | 'rec-2020';
}

export function hexToRgb(hex: string): string;
export function rgbToHex(color: string): string;
export function hslToRgb(color: string): string;
export function decomposeColor(color: string): ColorObject;
export function recomposeColor(color: ColorObject): string;
export function getContrastRatio(foreground: string, background: string): number;
export function getLuminance(color: string): number;
export function emphasize(color: string, coefficient?: number): string;
export function alpha(color: string, value: number): string;
export function darken(color: string, coefficient: number): string;
export function lighten(color: string, coefficient: number): string;
283 changes: 283 additions & 0 deletions packages/material-ui-system/src/colorManipulator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
import MuiError from '@material-ui/utils/macros/MuiError.macro';
/* eslint-disable @typescript-eslint/no-use-before-define */

/**
* Returns a number whose value is limited to the given range.
* @param {number} value The value to be clamped
* @param {number} min The lower boundary of the output range
* @param {number} max The upper boundary of the output range
* @returns {number} A number in the range [min, max]
*/
function clamp(value, min = 0, max = 1) {
if (process.env.NODE_ENV !== 'production') {
if (value < min || value > max) {
console.error(`Material-UI: The value provided ${value} is out of range [${min}, ${max}].`);
}
}

return Math.min(Math.max(min, value), max);
}

/**
* Converts a color from CSS hex format to CSS rgb format.
* @param {string} color - Hex color, i.e. #nnn or #nnnnnn
* @returns {string} A CSS rgb color string
*/
export function hexToRgb(color) {
color = color.substr(1);

const re = new RegExp(`.{1,${color.length >= 6 ? 2 : 1}}`, 'g');
let colors = color.match(re);

if (colors && colors[0].length === 1) {
colors = colors.map((n) => n + n);
}

return colors
? `rgb${colors.length === 4 ? 'a' : ''}(${colors
.map((n, index) => {
return index < 3 ? parseInt(n, 16) : Math.round((parseInt(n, 16) / 255) * 1000) / 1000;
})
.join(', ')})`
: '';
}

function intToHex(int) {
const hex = int.toString(16);
return hex.length === 1 ? `0${hex}` : hex;
}

/**
* Converts a color from CSS rgb format to CSS hex format.
* @param {string} color - RGB color, i.e. rgb(n, n, n)
* @returns {string} A CSS rgb color string, i.e. #nnnnnn
*/
export function rgbToHex(color) {
// Idempotent
if (color.indexOf('#') === 0) {
return color;
}

const { values } = decomposeColor(color);
return `#${values.map((n, i) => intToHex(i === 3 ? Math.round(255 * n) : n)).join('')}`;
}

/**
* Converts a color from hsl format to rgb format.
* @param {string} color - HSL color values
* @returns {string} rgb color values
*/
export function hslToRgb(color) {
color = decomposeColor(color);
const { values } = color;
const h = values[0];
const s = values[1] / 100;
const l = values[2] / 100;
const a = s * Math.min(l, 1 - l);
const f = (n, k = (n + h / 30) % 12) => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);

let type = 'rgb';
const rgb = [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];

if (color.type === 'hsla') {
type += 'a';
rgb.push(values[3]);
}

return recomposeColor({ type, values: rgb });
}

/**
* Returns an object with the type and values of a color.
*
* Note: Does not support rgb % values.
* @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()
* @returns {object} - A MUI color object: {type: string, values: number[]}
*/
export function decomposeColor(color) {
// Idempotent
if (color.type) {
return color;
}

if (color.charAt(0) === '#') {
return decomposeColor(hexToRgb(color));
}

const marker = color.indexOf('(');
const type = color.substring(0, marker);

if (['rgb', 'rgba', 'hsl', 'hsla', 'color'].indexOf(type) === -1) {
throw new MuiError(
'Material-UI: Unsupported `%s` color.\n' +
'The following formats are supported: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color().',
color,
);
}

let values = color.substring(marker + 1, color.length - 1);
let colorSpace;

if (type === 'color') {
values = values.split(' ');
colorSpace = values.shift();
if (values.length === 4 && values[3].charAt(0) === '/') {
values[3] = values[3].substr(1);
}
if (['srgb', 'display-p3', 'a98-rgb', 'prophoto-rgb', 'rec-2020'].indexOf(colorSpace) === -1) {
throw new MuiError(
'Material-UI: unsupported `%s` color space.\n' +
'The following color spaces are supported: srgb, display-p3, a98-rgb, prophoto-rgb, rec-2020.',
colorSpace,
);
}
} else {
values = values.split(',');
}
values = values.map((value) => parseFloat(value));

return { type, values, colorSpace };
}

/**
* Converts a color object with type and values to a string.
* @param {object} color - Decomposed color
* @param {string} color.type - One of: 'rgb', 'rgba', 'hsl', 'hsla'
* @param {array} color.values - [n,n,n] or [n,n,n,n]
* @returns {string} A CSS color string
*/
export function recomposeColor(color) {
const { type, colorSpace } = color;
let { values } = color;

if (type.indexOf('rgb') !== -1) {
// Only convert the first 3 values to int (i.e. not alpha)
values = values.map((n, i) => (i < 3 ? parseInt(n, 10) : n));
} else if (type.indexOf('hsl') !== -1) {
values[1] = `${values[1]}%`;
values[2] = `${values[2]}%`;
}
if (type.indexOf('color') !== -1) {
values = `${colorSpace} ${values.join(' ')}`;
} else {
values = `${values.join(', ')}`;
}

return `${type}(${values})`;
}

/**
* Calculates the contrast ratio between two colors.
*
* Formula: https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
* @param {string} foreground - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()
* @param {string} background - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()
* @returns {number} A contrast ratio value in the range 0 - 21.
*/
export function getContrastRatio(foreground, background) {
const lumA = getLuminance(foreground);
const lumB = getLuminance(background);
return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05);
}

/**
* The relative brightness of any point in a color space,
* normalized to 0 for darkest black and 1 for lightest white.
*
* Formula: https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
* @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
* @returns {number} The relative brightness of the color in the range 0 - 1
*/
export function getLuminance(color) {
color = decomposeColor(color);

let rgb = color.type === 'hsl' ? decomposeColor(hslToRgb(color)).values : color.values;
rgb = rgb.map((val) => {
if (color.type !== 'color') {
val /= 255; // normalized
}
return val <= 0.03928 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4;
});

// Truncate at 3 digits
return Number((0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]).toFixed(3));
}

/**
* Darken or lighten a color, depending on its luminance.
* Light colors are darkened, dark colors are lightened.
* @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
* @param {number} coefficient=0.15 - multiplier in the range 0 - 1
* @returns {string} A CSS color string. Hex input values are returned as rgb
*/
export function emphasize(color, coefficient = 0.15) {
return getLuminance(color) > 0.5 ? darken(color, coefficient) : lighten(color, coefficient);
}

/**
* Set the absolute transparency of a color.
* Any existing alpha values are overwritten.
* @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
* @param {number} value - value to set the alpha channel to in the range 0 - 1
* @returns {string} A CSS color string. Hex input values are returned as rgb
*/
export function alpha(color, value) {
color = decomposeColor(color);
value = clamp(value);

if (color.type === 'rgb' || color.type === 'hsl') {
color.type += 'a';
}
if (color.type === 'color') {
color.values[3] = `/${value}`;
} else {
color.values[3] = value;
}

return recomposeColor(color);
}

/**
* Darkens a color.
* @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
* @param {number} coefficient - multiplier in the range 0 - 1
* @returns {string} A CSS color string. Hex input values are returned as rgb
*/
export function darken(color, coefficient) {
color = decomposeColor(color);
coefficient = clamp(coefficient);

if (color.type.indexOf('hsl') !== -1) {
color.values[2] *= 1 - coefficient;
} else if (color.type.indexOf('rgb') !== -1 || color.type.indexOf('color') !== -1) {
for (let i = 0; i < 3; i += 1) {
color.values[i] *= 1 - coefficient;
}
}
return recomposeColor(color);
}

/**
* Lightens a color.
* @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
* @param {number} coefficient - multiplier in the range 0 - 1
* @returns {string} A CSS color string. Hex input values are returned as rgb
*/
export function lighten(color, coefficient) {
color = decomposeColor(color);
coefficient = clamp(coefficient);

if (color.type.indexOf('hsl') !== -1) {
color.values[2] += (100 - color.values[2]) * coefficient;
} else if (color.type.indexOf('rgb') !== -1) {
for (let i = 0; i < 3; i += 1) {
color.values[i] += (255 - color.values[i]) * coefficient;
}
} else if (color.type.indexOf('color') !== -1) {
for (let i = 0; i < 3; i += 1) {
color.values[i] += (1 - color.values[i]) * coefficient;
}
}

return recomposeColor(color);
}
2 changes: 2 additions & 0 deletions packages/material-ui-system/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,5 @@ export { SpacingOptions, Spacing } from './createTheme/createSpacing';

export { default as shape } from './createTheme/shape';
export * from './createTheme/shape';

export * from './colorManipulator';
1 change: 1 addition & 0 deletions packages/material-ui-system/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ export { default as styled } from './styled';
export { default as createTheme } from './createTheme';
export { default as createBreakpoints } from './createTheme/createBreakpoints';
export { default as shape } from './createTheme/shape';
export * from './colorManipulator';
33 changes: 15 additions & 18 deletions packages/material-ui/src/styles/colorManipulator.d.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
export type ColorFormat = 'rgb' | 'rgba' | 'hsl' | 'hsla';
export interface ColorObject {
type: ColorFormat;
values: [number, number, number] | [number, number, number, number];
colorSpace?: 'srgb' | 'display-p3' | 'a98-rgb' | 'prophoto-rgb' | 'rec-2020';
}

export function hexToRgb(hex: string): string;
export function rgbToHex(color: string): string;
export function hslToRgb(color: string): string;
export function decomposeColor(color: string): ColorObject;
export function recomposeColor(color: ColorObject): string;
export function getContrastRatio(foreground: string, background: string): number;
export function getLuminance(color: string): number;
export function emphasize(color: string, coefficient?: number): string;
export function alpha(color: string, value: number): string;
export function darken(color: string, coefficient: number): string;
export function lighten(color: string, coefficient: number): string;
export {
hexToRgb,
rgbToHex,
hslToRgb,
decomposeColor,
recomposeColor,
getContrastRatio,
getLuminance,
emphasize,
alpha,
darken,
lighten,
ColorFormat,
ColorObject,
} from '@material-ui/system';
Loading