Skip to content

Color Utils

颜色的工具函数。

代码实现

将下列代码复制到src/utils/colorUtils.ts文件中:

tsx
export type ColorType = 'rgb' | 'hsl' | 'hex' | 'hsv' | 'rgba'

export const reg = /\d+/g

// 随机颜色
const randomColor = () => '#' + Math.random().toString(16).slice(2, 8).padEnd(6, '0')

// rgb转16进制(包括rgba)
const rgb2hex = (rgb: string) => {
    const rgbArr = rgb.match(reg)

    const to16 = (num: string | number) =>
        Number(num).toString(16).padStart(2, '0')

    const rgbToHex = (rgbArr: string[]) =>
        rgbArr.reduce((pre, cur) => pre + to16(cur), '#')

    const rgbLength4 = (rgba: (string | number)[]) => {
        const rgb = rgba
        const op =
            Number(rgba[rgba.length - 1]) !== 1 ? Number(rgba[rgba.length - 1]) : 100
        rgb.pop()
        return rgbToHex(rgb as string[]) + to16(op * 2.56)
    }

    if (rgbArr!.length === 3) return rgbToHex(rgbArr!)

    if (rgbArr!.length === 5) {
        const newRgbArr = [
            rgbArr![0],
            rgbArr![1],
            rgbArr![2],
            Number(rgbArr![rgbArr!.length - 1]) * 10
        ]

        return rgbLength4(newRgbArr)
    }

    return rgbLength4(rgbArr!)
}

// 16进制转rgb(包括rgba)
const hex2rgb = (hex: string) => {
    const hexVal = hex.split('#')[1]

    const hexLength6 = (hex: string) => {
        const r = parseInt(hex.slice(0, 2), 16)
        const g = parseInt(hex.slice(2, 4), 16)
        const b = parseInt(hex.slice(4, 6), 16)

        return [r, g, b]
    }

    if (hexVal.length === 8) {
        const op = parseInt(hexVal.slice(-2), 16) / 256
        const val = hexVal.slice(0, -2)

        const arr = hexLength6(val)
        arr.push(op)

        return 'rgba(' + arr.join(', ') + ')'
    }

    return 'rgb(' + hexLength6(hexVal).join(', ') + ')'
}

const rgb2rgba = (bg: string, op?: number) => {
    if (!op) return bg
    if (bg.includes('rgba')) {
        const arr = bg.split(',')
        arr.pop()
        arr.push(op + ')')
        const newBg = arr.join(',')
        return newBg
    }
    if (bg.includes('rgb')) {
        const arr = bg.split(',')
        const rgb = 'rgba(' + arr[0].split('(')[1]
        const b = arr[arr.length - 1].split(')')[0]
        const newBg = [rgb, arr[1], b, ' ' + op + ')'].join(',')
        return newBg
    }
}

// rgb转hsl
const rgb2hsl = (rgb: string) => {
    const rbgArr = rgb.match(reg)!
    const r = Number(rbgArr[0]) / 255
    const g = Number(rbgArr[1]) / 255
    const b = Number(rbgArr[2]) / 255

    const min = Math.min(r, g, b), max = Math.max(r, g, b)

    let h = 0, s = 0, l = (max + min) / 2

    if (max === min) h = s = 0 // achromatic
    else {
        const d = max - min
        s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
        switch (max) {
            case r: h = (g - b) / d + (g < b ? 6 : 0); break
            case g: h = (b - r) / d + 2; break
            case b: h = (r - g) / d + 4; break
        }
        h /= 6
    }

    return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`
}

// hsl转rgb
const hsl2rgb = (hsl: string) => {
    const hslArr = hsl.match(reg)!
    const h = Number(hslArr[0]) / 360
    const s = Number(hslArr[1]) / 100
    const l = Number(hslArr[2]) / 100

    let r, g, b

    function hue2Rgb(p: number, q: number, t: number) {
        if (t < 0) t += 1;
        if (t > 1) t -= 1;
        if (t < 1 / 6) return p + (q - p) * 6 * t;
        if (t < 1 / 2) return q;
        if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
        return p;
    }

    let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    let p = 2 * l - q;
    r = hue2Rgb(p, q, h + 1 / 3);
    g = hue2Rgb(p, q, h);
    b = hue2Rgb(p, q, h - 1 / 3);

    return `rgb(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)})`
}

// 16进制转hsl
const hex2hsl = (hex: string) => rgb2hsl(hex2rgb(hex))

// hsl转16进制
const hsl2hex = (hsl: string) => rgb2hex(hsl2rgb(hsl))

// rgb转hsv
const rgb2hsv = (rgb: string) => {
    const rbgArr = rgb.match(reg)!
    let r = Number(rbgArr[0]) / 255,
        g = Number(rbgArr[1]) / 255,
        b = Number(rbgArr[2]) / 255;

    const max = Math.max(r, g, b);
    const min = Math.min(r, g, b);
    let h: number = 0,
        s: number = 0;
    let v = max;

    const d = max - min;
    s = max === 0 ? 0 : d / max;

    if (max === min) {
        h = 0; // achromatic
    } else {
        switch (max) {
            case r:
                h = (g - b) / d + (g < b ? 6 : 0);
                break;
            case g:
                h = (b - r) / d + 2;
                break;
            case b:
                h = (r - g) / d + 4;
                break;
        }
        h /= 6;
    }

    return `hsv(${h * 360}, ${s * 100}, ${v * 100})`;
}

// hsv转rgb (未完成)
const hsv2rgb = (hsv: string) => {
    const hsvArr = hsv.match(reg)!
    let h = Number(hsvArr[0]) / 360,
        s = Number(hsvArr[1]) / 100,
        v = Number(hsvArr[2]) / 100;
    let r, g, b, i, f, p, q, t;
    if (h && s === 0) {
        r = g = b = v; //  achromatic
    } else {
        h /= 60; //  sector 0 to 5
        i = Math.floor(h);
        f = h - i; //  factorial part of h
        p = v * (1 - s);
        q = v * (1 - s * f);
        t = v * (1 - s * (1 - f));
        switch (i) {
            case 0:
                r = v;
                g = t;
                b = p;
                break;
            case 1:
                r = q;
                g = v;
                b = p;
                break;
            case 2:
                r = p;
                g = v;
                b = t;
                break;
            case 3:
                r = p;
                g = q;
                b = v;
                break;
            case 4:
                r = t;
                g = p;
                b = v;
                break;
            default: // case 5:
                r = v;
                g = p;
                b = q;
        }
    }
    return `rgb(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)})`
}

// 16进制转hsv
const hex2hsv = (hex: string) => rgb2hsv(hex2rgb(hex))

// hsv转16进制
const hsv2hex = (hsv: string) => rgb2hex(hsv2rgb(hsv))

export const formatColor = (
    rgb: string,
    a: number,
    type: ColorType = 'rgba'
): string => {
    const rbgArr = rgb.match(reg)!
    let r = Number(rbgArr[0]) / 255,
        g = Number(rbgArr[1]) / 255,
        b = Number(rbgArr[2]) / 255;
    if (type === 'rgba') {
        return `rgba(${r},${g},${b},${a})`;
    } else if (type === 'rgb') {
        return rgb;
    } else if (type === 'hex') {
        return rgb2hex(rgb);
    } else if (type === 'hsl') {
        return rgb2hsl(`rgba(${r},${g},${b},${a})`);
    }
    return `rgba(${r},${g},${b},${a})`;
};

// 判断字符串是否为十六进制或rgb格式的字符串
const isValidColor = (clr: string) => {
    // 十六进制颜色格式
    const hexPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$/
    // rgb颜色格式
    const rgbPattern = /^rgb\((1?\d{1,3},\s*1?\d{1,3},\s*1?\d{1,3})\)$/

    if (hexPattern.test(clr) || rgbPattern.test(clr)) return true
    else return false
}

// 判断颜色字符串格式
const getColorType = (clr: string) => {
    // 十六进制颜色格式
    const hexPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$/
    // rgb颜色格式
    const rgbPattern = /^rgb\((1?\d{1,3},\s*1?\d{1,3},\s*1?\d{1,3})\)$/

    if (hexPattern.test(clr)) return 'hex'
    else if (rgbPattern.test(clr)) return 'rgb'
    else return 'unknown'
}

// 向一个函数传入一个16进制颜色值,一个类型(lighten或darken)和一个百分比值,并返回一个调整亮度或暗度后的颜色值
const adjustColor = (color: string, type: 'lighten' | 'darken', percent: number) => {
    const colorType = getColorType(color)
    if (colorType === 'hex') {
        const hex = color.replace('#', '')
        const r = parseInt(hex.slice(0, 2), 16)
        const g = parseInt(hex.slice(2, 4), 16)
        const b = parseInt(hex.slice(4, 6), 16)
        if (type === 'lighten') {
            const r2 = Math.min(255, Math.max(0, r + (255 - r) * percent / 100))
            const g2 = Math.min(255, Math.max(0, g + (255 - g) * percent / 100))
            const b2 = Math.min(255, Math.max(0, b + (255 - b) * percent / 100))
            return `#${r2.toString(16).padStart(2, '0')}${g2.toString(16).padStart(2, '0')}${b2.toString(16).padStart(2, '0')}`
        } else if (type === 'darken') {
            const r2 = Math.min(255, Math.max(0, r - r * percent / 100))
            const g2 = Math.min(255, Math.max(0, g - g * percent / 100))
            const b2 = Math.min(255, Math.max(0, b - b * percent / 100))
            return `#${r2.toString(16).padStart(2, '0')}${g2.toString(16).padStart(2, '0')}${b2.toString(16).padStart(2, '0')}`
        }
    } else if (colorType === 'rgb') {
        const rgb = color.match(/\d+/g)!
        const r = parseInt(rgb[0])
        const g = parseInt(rgb[1])
        const b = parseInt(rgb[2])
        if (type === 'lighten') {
            const r2 = Math.min(255, Math.max(0, r + (255 - r) * percent / 100))
            const g2 = Math.min(255, Math.max(0, g + (255 - g) * percent / 100))
            const b2 = Math.min(255, Math.max(0, b + (255 - b) * percent / 100))
            return `rgb(${r2},${g2},${b2})`
        } else if (type === 'darken') {
            const r2 = Math.min(255, Math.max(0, r - r * percent / 100))
            const g2 = Math.min(255, Math.max(0, g - g * percent / 100))
            const b2 = Math.min(255, Math.max(0, b - b * percent / 100))
            return `rgb(${r2},${g2},${b2})`
        }
    }
    return color
}

// 3位十六进制转6位十六进制
const hex3To6 = (hex: string) => {
    const hexArr = hex.replace('#', '').split('')
    return `#${hexArr[0] + hexArr[0] + hexArr[1] + hexArr[1] + hexArr[2] + hexArr[2]}`
}

// 根据背景颜色获取文字颜色(黑 / 白)
const getContrastColor = (backgroundColor: string) => {
    // 将颜色字符串转换为标准的 RGB 格式
    function normalizeColor(color: string) {
        if (color.startsWith('#')) {
            // 将 3 位简写的颜色值扩展为 6 位
            if (color.length === 4) {
                color = `#${color.slice(1).repeat(2)}`
            }
            // 提取各个颜色分量的值
            const r = parseInt(color.substr(1, 2), 16)
            const g = parseInt(color.substr(3, 2), 16)
            const b = parseInt(color.substr(5, 2), 16)
            return [r, g, b];
        } else if (color.startsWith('rgb')) {
            // 提取各个颜色分量的值
            const rgb = color.match(/\d+/g)!
            const r = parseInt(rgb[0])
            const g = parseInt(rgb[1])
            const b = parseInt(rgb[2])
            return [r, g, b]
        }
        return null; // 非法颜色格式
    }

    // 计算亮度值
    const getBrightness = (color: number[]) =>
        (color[0] * 299 + color[1] * 587 + color[2] * 114) / 1000

    // 选择合适的文字颜色
    const chooseTextColor = (brightness: number) =>
        brightness > 128 ? '#000' : '#fff'

    const normalizedColor = normalizeColor(backgroundColor)
    if (normalizedColor === null) {
        return null; // 非法颜色格式
    }

    const brightness = getBrightness(normalizedColor)
    const textColor = chooseTextColor(brightness)

    return textColor
}

// 调节颜色亮度
/**
 * 
 * @param clr 颜色值 16进制 / rgb / hsl
 * @param type 类型(light / dark)
 * @param percentage 百分比值(0 - 100)
 * @returns 颜色值 16进制
 */
export const adjustingColors = (
    clr: string,
    type: 'light' | 'dark',
    percentage: number
) => {
    let clrArr: string[] = []
    if(clr.includes('#')) clrArr = hex2hsl(clr).match(reg)!
    else if(clr.includes('rgb')) clrArr = rgb2hsl(clr).match(reg)!
    else if(clr.includes('hsl')) clrArr = clr.match(reg)!
    else throw new Error('颜色格式错误')

    const adjustingLuminance = () => {
        if (type === 'light')
            return Number(clrArr[2]) + percentage > 100
                ? 100
                : Number(clrArr[2]) + percentage
        else
            return Number(clrArr[2]) - percentage < 0
                ? 0
                : Number(clrArr[2]) - percentage
    }

    return hsl2hex(
        `${clrArr[0]}, ${clrArr[1]}%, ${adjustingLuminance()}%`
    )
}

// 系统吸管
const myDropper = async (onSuccess: (clr: string) => void) => {
    const dropper = new EyeDropper()
    try {
        const result = await dropper.open()
        onSuccess(result.sRGBHex)
    } catch {
        console.log('user canceled')
    }
}

export default { randomColor, rgb2hex, hex2rgb, rgb2rgba, rgb2hsl, hsl2rgb, hex2hsl, hsl2hex, rgb2hsv, hsv2rgb, hex2hsv, hsv2hex, formatColor, getContrastColor, adjustColor, isValidColor, getColorType, hex3To6, adjustingColors, myDropper }