import { internalAssertColorUnit } from "../internal.ts" import type { LinearRgbColorValue, SrgbColorValue, XyChromaticityValue } from "../types.ts" import { xyzColorValueToRelativeLuminance, xyzColorValueToTristimulusSum, xyzColorValueToXyChromaticityValue, } from "../xyz/analyze.ts" import { linearRgbColorValueToXyzColorValue } from "../xyz/convert.ts" import { createSrgbColorValue } from "./construct.ts" import { srgbColorValueToLinearRgbColorValue } from "./convert.ts" const internalDefaultDarkSrgbColorValue: SrgbColorValue = { red: 0, green: 0, blue: 0, alpha: 1 } const internalDefaultLightSrgbColorValue: SrgbColorValue = { red: 255, green: 255, blue: 255, alpha: 1, } const internalNormalizeContrastRatio = (value: number): number => { if (Number.isFinite(value) === false || value < 1) { throw new RangeError("Contrast ratio must be a finite number greater than or equal to 1") } return value } /** * @description 读取 Linear RGB 颜色值的相对亮度。 * * 该函数会沿合法路径 `Linear RGB -> XYZ -> luminance` 计算。 * * @example * ``` * // Expect: 0.212673 * const example1 = linearRgbColorValueToRelativeLuminance({ red: 1, green: 0, blue: 0, alpha: 1 }) * * // Expect: 1 * const example2 = linearRgbColorValueToRelativeLuminance({ red: 1, green: 1, blue: 1, alpha: 1 }) * ``` */ export const linearRgbColorValueToRelativeLuminance = (color: LinearRgbColorValue): number => { return xyzColorValueToRelativeLuminance(linearRgbColorValueToXyzColorValue(color)) } /** * @description 读取 sRGB 颜色值的相对亮度。 * * 该函数会沿合法路径 `sRGB -> Linear RGB -> XYZ -> luminance` 计算。 * * @example * ``` * // Expect: 0.125053 * const example1 = srgbColorValueToRelativeLuminance({ red: 51, green: 102, blue: 153, alpha: 1 }) * * // Expect: 1 * const example2 = srgbColorValueToRelativeLuminance({ red: 255, green: 255, blue: 255, alpha: 1 }) * ``` */ export const srgbColorValueToRelativeLuminance = (color: SrgbColorValue): number => { return linearRgbColorValueToRelativeLuminance(srgbColorValueToLinearRgbColorValue(color)) } /** * @description 读取两个 sRGB 颜色值之间的对比度。 * * 该函数假定输入已经是最终显示颜色;若存在透明叠加,应先完成合成。 * * @example * ``` * // Expect: 21 * const example1 = srgbColorValuesToContrastRatio( * { red: 0, green: 0, blue: 0, alpha: 1 }, * { red: 255, green: 255, blue: 255, alpha: 1 }, * ) * * // Expect: 1 * const example2 = srgbColorValuesToContrastRatio( * { red: 51, green: 102, blue: 153, alpha: 1 }, * { red: 51, green: 102, blue: 153, alpha: 1 }, * ) * ``` */ export const srgbColorValuesToContrastRatio = ( first: SrgbColorValue, second: SrgbColorValue, ): number => { const firstLuminance = srgbColorValueToRelativeLuminance(first) const secondLuminance = srgbColorValueToRelativeLuminance(second) const lighterLuminance = Math.max(firstLuminance, secondLuminance) const darkerLuminance = Math.min(firstLuminance, secondLuminance) return lighterLuminance === darkerLuminance ? 1 : Number(((lighterLuminance + 0.05) / (darkerLuminance + 0.05)).toFixed(6)) } /** * @description 判断两个 sRGB 颜色值是否达到指定对比度阈值。 * * 该函数假定输入已经是最终显示颜色;若存在透明叠加,应先完成合成。 * * @example * ``` * // Expect: true * const example1 = doSrgbColorValuesMeetContrastRatio( * { red: 0, green: 0, blue: 0, alpha: 1 }, * { red: 255, green: 255, blue: 255, alpha: 1 }, * 4.5, * ) * * // Expect: false * const example2 = doSrgbColorValuesMeetContrastRatio( * { red: 120, green: 120, blue: 120, alpha: 1 }, * { red: 255, green: 255, blue: 255, alpha: 1 }, * 4.5, * ) * ``` */ export const doSrgbColorValuesMeetContrastRatio = ( first: SrgbColorValue, second: SrgbColorValue, minimumContrastRatio = 4.5, ): boolean => { return ( srgbColorValuesToContrastRatio(first, second) >= internalNormalizeContrastRatio(minimumContrastRatio) ) } /** * @description 为给定背景色选择更可读的黑色或白色文本颜色。 * * 该函数假定背景色已经是最终显示颜色。 * * @example * ``` * // Expect: { red: 255, green: 255, blue: 255, alpha: 1 } * const example1 = pickReadableSrgbTextColorValue({ red: 0, green: 102, blue: 204, alpha: 1 }) * * // Expect: { red: 0, green: 0, blue: 0, alpha: 1 } * const example2 = pickReadableSrgbTextColorValue({ red: 250, green: 240, blue: 210, alpha: 1 }) * ``` */ export const pickReadableSrgbTextColorValue = ( backgroundColor: SrgbColorValue, options: { darkColor?: SrgbColorValue; lightColor?: SrgbColorValue } = {}, ): SrgbColorValue => { const normalizedDarkColor = createSrgbColorValue( options.darkColor ?? internalDefaultDarkSrgbColorValue, ) const normalizedLightColor = createSrgbColorValue( options.lightColor ?? internalDefaultLightSrgbColorValue, ) const darkContrastRatio = srgbColorValuesToContrastRatio(normalizedDarkColor, backgroundColor) const lightContrastRatio = srgbColorValuesToContrastRatio(normalizedLightColor, backgroundColor) return lightContrastRatio > darkContrastRatio ? normalizedLightColor : normalizedDarkColor } /** * @description 读取 Linear RGB 颜色值沿合法路径得到的三刺激总量。 * * 该函数会沿合法路径 `Linear RGB -> XYZ -> tristimulus sum` 计算。 * * @example * ``` * // Expect: 0.644463 * const example1 = linearRgbColorValueToTristimulusSum({ red: 1, green: 0, blue: 0, alpha: 1 }) * * // Expect: 3.0393 * const example2 = linearRgbColorValueToTristimulusSum({ red: 1, green: 1, blue: 1, alpha: 1 }) * ``` */ export const linearRgbColorValueToTristimulusSum = (color: LinearRgbColorValue): number => { return xyzColorValueToTristimulusSum(linearRgbColorValueToXyzColorValue(color)) } /** * @description 读取 sRGB 颜色值沿合法路径得到的三刺激总量。 * * 该函数会沿合法路径 `sRGB -> Linear RGB -> XYZ -> tristimulus sum` 计算。 * * @example * ``` * // Expect: 0.562889 * const example1 = srgbColorValueToTristimulusSum({ red: 51, green: 102, blue: 153, alpha: 1 }) * * // Expect: 3.0393 * const example2 = srgbColorValueToTristimulusSum({ red: 255, green: 255, blue: 255, alpha: 1 }) * ``` */ export const srgbColorValueToTristimulusSum = (color: SrgbColorValue): number => { return linearRgbColorValueToTristimulusSum(srgbColorValueToLinearRgbColorValue(color)) } /** * @description 读取 Linear RGB 颜色值沿合法路径得到的 `xy` 色度坐标。 * * 该函数会沿合法路径 `Linear RGB -> XYZ -> xy chromaticity` 计算。 * * @example * ``` * // Expect: { x: 0.64, y: 0.33 } * const example1 = linearRgbColorValueToXyChromaticityValue({ red: 1, green: 0, blue: 0, alpha: 1 }) * * // Expect: { x: 0.312727, y: 0.329023 } * const example2 = linearRgbColorValueToXyChromaticityValue({ red: 1, green: 1, blue: 1, alpha: 1 }) * ``` */ export const linearRgbColorValueToXyChromaticityValue = ( color: LinearRgbColorValue, ): XyChromaticityValue => { return xyzColorValueToXyChromaticityValue(linearRgbColorValueToXyzColorValue(color)) } /** * @description 读取 sRGB 颜色值沿合法路径得到的 `xy` 色度坐标。 * * 该函数会沿合法路径 `sRGB -> Linear RGB -> XYZ -> xy chromaticity` 计算。 * * @example * ``` * // Expect: { x: 0.210775, y: 0.222163 } * const example1 = srgbColorValueToXyChromaticityValue({ red: 51, green: 102, blue: 153, alpha: 1 }) * * // Expect: { x: 0.312727, y: 0.329023 } * const example2 = srgbColorValueToXyChromaticityValue({ red: 255, green: 255, blue: 255, alpha: 1 }) * ``` */ export const srgbColorValueToXyChromaticityValue = (color: SrgbColorValue): XyChromaticityValue => { return linearRgbColorValueToXyChromaticityValue(srgbColorValueToLinearRgbColorValue(color)) } /** * @description 判断一个 sRGB 颜色值是否可视为亮色。 * * 阈值位于 `0` 到 `1` 之间,判断依据是相对亮度。 * * @example * ``` * // Expect: true * const example1 = isLightSrgbColorValue({ red: 255, green: 255, blue: 255, alpha: 1 }) * * // Expect: false * const example2 = isLightSrgbColorValue({ red: 0, green: 0, blue: 0, alpha: 1 }) * ``` */ export const isLightSrgbColorValue = (color: SrgbColorValue, threshold = 0.5): boolean => { internalAssertColorUnit(threshold, "threshold") return srgbColorValueToRelativeLuminance(createSrgbColorValue(color)) >= threshold }