diff --git a/src/colors.ts b/src/colors.ts index 6c04fda..b266787 100644 --- a/src/colors.ts +++ b/src/colors.ts @@ -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 = (r: number, g: number, b: number) => T; +type ColorProcessingFunc = (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 = (r, g, b) => +const rgbToHex: ColorProcessingFunc = (r, g, b) => "#" + [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join(""); -const rgbToRatios: RgbProcessingFunc = (rIN, gIN, bIN) => ({ +const rgbToRatios: ColorProcessingFunc = (rIN, gIN, bIN) => ({ r: byteToRatio(rIN), g: byteToRatio(gIN), b: byteToRatio(bIN), }); -const rgbToHsl: RgbProcessingFunc = (rIN, gIN, bIN) => { +const rgbToHsl: ColorProcessingFunc = (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 = (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 = (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); }); diff --git a/src/math.ts b/src/math.ts index 7d0a407..a032d21 100644 --- a/src/math.ts +++ b/src/math.ts @@ -16,4 +16,6 @@ const round = (v: number, digits = ROUND_PRECISION) => { return Math.round(v * factor) / factor; }; -export { byteToRatio, round }; +const clamp = (v: number, a = 0, b = 1) => Math.min(b, Math.max(a, v)); + +export { byteToRatio, round, clamp, BYTE_MAX };