|
|
|
|
@ -1,31 +1,32 @@
|
|
|
|
|
import { byteToRatio, round } from "./math";
|
|
|
|
|
import { BYTE_MAX, byteToRatio, round, clamp } from "./math";
|
|
|
|
|
|
|
|
|
|
type Color = { r: number; g: number; b: number };
|
|
|
|
|
type RGB = { r: number; g: number; b: number };
|
|
|
|
|
type HSL = { h: number; s: number; l: number };
|
|
|
|
|
type RgbProcessingFunc<T> = (r: number, g: number, b: number) => T;
|
|
|
|
|
type ColorProcessingFunc<T> = (r: number, g: number, b: number) => T;
|
|
|
|
|
|
|
|
|
|
const hexToRgb = (hex: string) => {
|
|
|
|
|
const DEGREES_IN_CIRCLE = 360;
|
|
|
|
|
const PERCENT_FACTOR = 100;
|
|
|
|
|
|
|
|
|
|
const hexToRgb = (hex: string): RGB => {
|
|
|
|
|
const h = hex.replace("#", "");
|
|
|
|
|
return [
|
|
|
|
|
parseInt(h.slice(0, 2), 16),
|
|
|
|
|
parseInt(h.slice(2, 4), 16),
|
|
|
|
|
parseInt(h.slice(4, 6), 16),
|
|
|
|
|
];
|
|
|
|
|
return {
|
|
|
|
|
r: parseInt(h.slice(0, 2), 16),
|
|
|
|
|
g: parseInt(h.slice(2, 4), 16),
|
|
|
|
|
b: parseInt(h.slice(4, 6), 16),
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const rgbToHex: RgbProcessingFunc<string> = (r, g, b) =>
|
|
|
|
|
const rgbToHex: ColorProcessingFunc<string> = (r, g, b) =>
|
|
|
|
|
"#" + [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
|
|
|
|
|
|
|
|
const rgbToRatios: RgbProcessingFunc<Color> = (rIN, gIN, bIN) => ({
|
|
|
|
|
const rgbToRatios: ColorProcessingFunc<RGB> = (rIN, gIN, bIN) => ({
|
|
|
|
|
r: byteToRatio(rIN),
|
|
|
|
|
g: byteToRatio(gIN),
|
|
|
|
|
b: byteToRatio(bIN),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const rgbToHsl: RgbProcessingFunc<HSL> = (rIN, gIN, bIN) => {
|
|
|
|
|
const rgbToHsl: ColorProcessingFunc<HSL> = (rIN, gIN, bIN) => {
|
|
|
|
|
const HUE_SEGMENTS = 6;
|
|
|
|
|
const DEGREES_IN_CIRCLE = 360;
|
|
|
|
|
const PERCENT_FACTOR = 100;
|
|
|
|
|
|
|
|
|
|
const { r, g, b } = rgbToRatios(rIN, gIN, bIN);
|
|
|
|
|
const max = Math.max(r, g, b);
|
|
|
|
|
@ -60,62 +61,55 @@ const rgbToHsl: RgbProcessingFunc<HSL> = (rIN, gIN, bIN) => {
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const hslToRgb = (h: number, s: number, l: number) => {
|
|
|
|
|
s /= 100;
|
|
|
|
|
l /= 100;
|
|
|
|
|
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
|
|
|
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
|
|
|
const m = l - c / 2;
|
|
|
|
|
let r = 0,
|
|
|
|
|
g = 0,
|
|
|
|
|
b = 0;
|
|
|
|
|
if (h < 60) {
|
|
|
|
|
r = c;
|
|
|
|
|
g = x;
|
|
|
|
|
b = 0;
|
|
|
|
|
} else if (h < 120) {
|
|
|
|
|
r = x;
|
|
|
|
|
g = c;
|
|
|
|
|
b = 0;
|
|
|
|
|
} else if (h < 180) {
|
|
|
|
|
r = 0;
|
|
|
|
|
g = c;
|
|
|
|
|
b = x;
|
|
|
|
|
} else if (h < 240) {
|
|
|
|
|
r = 0;
|
|
|
|
|
g = x;
|
|
|
|
|
b = c;
|
|
|
|
|
} else if (h < 300) {
|
|
|
|
|
r = x;
|
|
|
|
|
g = 0;
|
|
|
|
|
b = c;
|
|
|
|
|
} else {
|
|
|
|
|
r = c;
|
|
|
|
|
g = 0;
|
|
|
|
|
b = x;
|
|
|
|
|
}
|
|
|
|
|
return [
|
|
|
|
|
Math.round((r + m) * 255),
|
|
|
|
|
Math.round((g + m) * 255),
|
|
|
|
|
Math.round((b + m) * 255),
|
|
|
|
|
/**
|
|
|
|
|
* Конвертация HSL -> RGB
|
|
|
|
|
* Возвращает объект {r,g,b} с целыми 0..255.
|
|
|
|
|
*/
|
|
|
|
|
export const hslToRgb: ColorProcessingFunc<RGB> = (h, s, l) => {
|
|
|
|
|
// Нормализация
|
|
|
|
|
const hue = ((h % DEGREES_IN_CIRCLE) + DEGREES_IN_CIRCLE) % DEGREES_IN_CIRCLE;
|
|
|
|
|
const sat = clamp(s / PERCENT_FACTOR);
|
|
|
|
|
const light = clamp(l / PERCENT_FACTOR);
|
|
|
|
|
|
|
|
|
|
const c = (1 - Math.abs(2 * light - 1)) * sat;
|
|
|
|
|
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
|
|
|
|
|
const m = light - c / 2;
|
|
|
|
|
|
|
|
|
|
const sector = Math.floor(hue / 60) | 0;
|
|
|
|
|
|
|
|
|
|
// Пары (v1,v2) соответствуют комбинациям c,x,0 в порядке r,g,b
|
|
|
|
|
// Используем массив, чтобы избежать ветвлений
|
|
|
|
|
const table: Array<[number, number, number]> = [
|
|
|
|
|
[c, x, 0], // 0..60
|
|
|
|
|
[x, c, 0], // 60..120
|
|
|
|
|
[0, c, x], // 120..180
|
|
|
|
|
[0, x, c], // 180..240
|
|
|
|
|
[x, 0, c], // 240..300
|
|
|
|
|
[c, 0, x], // 300..360
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const [rp, gp, bp] = table[sector];
|
|
|
|
|
|
|
|
|
|
const r = round(clamp(rp + m, 0, 1) * BYTE_MAX);
|
|
|
|
|
const g = round(clamp(gp + m, 0, 1) * BYTE_MAX);
|
|
|
|
|
const b = round(clamp(bp + m, 0, 1) * BYTE_MAX);
|
|
|
|
|
|
|
|
|
|
return { r, g, b };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const hslToHex = (h: number, s: number, l: number) => {
|
|
|
|
|
const [r, g, b] = hslToRgb(h, s, l);
|
|
|
|
|
const { r, g, b } = hslToRgb(h, s, l);
|
|
|
|
|
return rgbToHex(r, g, b);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const hexToHsl = (hex: string) => {
|
|
|
|
|
const [r, g, b] = hexToRgb(hex);
|
|
|
|
|
const { r, g, b } = hexToRgb(hex);
|
|
|
|
|
return rgbToHsl(r, g, b);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const clamp = (v: number, a = 0, b = 100) => Math.min(Math.max(v, a), b);
|
|
|
|
|
|
|
|
|
|
// luminance & contrast
|
|
|
|
|
export const luminance = (hex: string) => {
|
|
|
|
|
const [r, g, b] = hexToRgb(hex).map((v) => {
|
|
|
|
|
const [r, g, b] = Array.from(Object.values(hexToRgb(hex))).map((v) => {
|
|
|
|
|
v /= 255;
|
|
|
|
|
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
|
|
|
});
|
|
|
|
|
|