@texel/color
这是一个简洁现代的JavaScript颜色库。特别适用于实时应用、生成艺术和网页图形。
- 功能:快速颜色转换、色差计算、色域映射和序列化
- 速度优化:比Colorjs.io快约5-125倍(参见基准测试)
- 内存和分配优化:转换和色域映射函数内不创建数组或对象
- 优化打包体积:零依赖,未使用的色彩空间可自动被树摇除去,保持小体积(例如,如果只需要OKLCH到sRGB的转换,压缩后约3.5kb)
- 精确度优化:高精度色彩空间矩阵
- 专注于最小化和现代化的色彩空间集合:
- xyz (D65)、xyz-d50、oklab、oklch、okhsv、okhsl、srgb、srgb-linear、display-p3、display-p3-linear、rec2020、rec2020-linear、a98-rgb、a98-rgb-linear、prophoto-rgb、prophoto-rgb-linear
安装
使用npm安装并导入模块。
npm install @texel/color --save
示例
将OKLCH(OKLab的圆柱形式)转换为sRGB:
import { convert, OKLCH, sRGB } from "@texel/color";
// L = 0 .. 1
// C = 0 .. 0.4
// H = 0 .. 360(度)
const rgb = convert([0.5, 0.15, 30], OKLCH, sRGB);
// 注意sRGB输出范围为0 .. 1
// -> [ 0.658, 0.217, 0.165 ]
你也可以使用通配符导入:
import * as colors from "@texel/color";
const rgb = colors.convert([0.5, 0.15, 30], colors.OKLCH, colors.sRGB);
:bulb: 现代打包工具(esbuild、vite)会应用树摇,移除未使用的功能,如代码中未引用的色彩空间和色域映射函数。上述脚本使用esbuild打包后约3.5kb。
另一个使用色域映射和序列化的宽色域Canvas2D示例:
import { gamutMapOKLCH, DisplayP3Gamut, sRGBGamut, serialize } from "@texel/color";
// 可能在或不在sRGB色域内的值
const oklch = [ 0.15, 0.425, 30 ];
// 决定要映射到哪个色域
const isDisplayP3Supported = /* 检查环境 */;
const gamut = isDisplayP3Supported ? DisplayP3Gamut : sRGBGamut;
// 将输入的OKLCH映射到R,G,B空间(sRGB或DisplayP3)
const rgb = gamutMapOKLCH(oklch, gamut);
// 获取输出空间的CSS颜色字符串
const color = serialize(rgb, gamut.space);
// 在Canvas2D上下文中绘制颜色
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d', {
colorSpace: gamut.id
});
context.fillStyle = color;
context.fillRect(0,0, canvas.width, canvas.height);
API
output = convert(coords, fromSpace, toSpace, output = [0, 0, 0])
将coords
(通常为[r,g,b]
或[l,c,h]
等)从fromSpace
转换到指定的toSpace
。from和to空间是spaces接口之一。
你可以传入output
,一个三维向量,结果将存储在其中。这可用于避免在热代码路径中分配新内存。
返回值是目标空间的新坐标;如sRGB
空间的目标为[r,g,b]
。注意,大多数空间使用归一化且无界的坐标;所以RGB空间的范围是0..1,可能会超出范围(即超出色域)。你可能需要结合gamutMapOKLCH
使用,见下文。
output = gamutMapOKLCH(oklch, gamut = sRGBGamut, targetSpace = gamut.space, out = [0, 0, 0], mapping = MapToCuspL, [cusp])
执行Björn Ottoson描述的(2021)快速OKLCH色域映射。接受OKLCH空间的输入[l,c,h]
坐标,确保最终结果在指定的颜色gamut
内(默认sRGBGamut
)。你可以进一步指定不同的目标空间(默认为色域的空间),例如获取线性光sRGB并避免传递函数,或保持结果在OKLCH中:
import { gamutMapOKLCH, sRGBGamut, sRGBLinear, OKLCH } from "@texel/color";
// 映射到sRGB但返回线性sRGB
const lrgb = gamutMapOKLCH(oklch, sRGBGamut, sRGBLinear);
// 或映射到sRGB但返回OKLCH(不执行RGB裁剪)
const lch = gamutMapOKLCH(oklch, sRGBGamut, OKLCH);
你可以指定out
数组以避免分配,结果将存储在该数组中。你还可以指定mapping
函数,决定色域映射时使用的策略,可以是以下之一:
import {
// 可能的映射
MapToL,
MapToGray,
MapToCuspL,
MapToAdaptiveGray,
MapToAdaptiveCuspL,
} from "@texel/color";
// 执行sRGB色域映射时保持亮度
const rgb = [0, 0, 0];
gamutMapOKLCH(oklch, sRGBGamut, sRGB, rgb, MapToL);
cusp
也可作为最后一个参数传入,允许对已知色相进行更快的评估。见下文计算cusp。
注意: 如果映射到基于OKLab的目标(OKLCH、OKHSL等),将跳过最后的RGB裁剪步骤。这会产生更可预测的OKLab和OKLCH结果,但在转换为可显示颜色时,你可能需要执行最后的clampedRGB()步骤。
LC = findCuspOKLCH(a, b, gamut, out = [0, 0])
找到给定OKLab色相平面(用OKLab空间中的归一化a
和b
值表示)的'cusp',返回[L, C]
(亮度和色度)。当你在已知色相上工作时,这对预计算色域映射的某些方面很有用:
import {
sRGBGamut,
findCuspOKLCH,
gamutMapOKLCH,
degToRad,
MapToCuspL,
} from "@texel/color";
const gamut = sRGBGamut;
// 为这个色相计算一次cusp
const H = 30; // 例如30º色相
const hueAngle = degToRad(H);
const a = Math.cos(hueAngle);
const b = Math.sin(hueAngle);
const cuspLC = findCuspOKLCH(a, b, gamut);
// ... 在程序的其他地方 ...
// 传入'cusp'参数以加快评估
// 假设你的OKLCH坐标与cusp有相同的色相(H)
gamutMapOKLCH(oklch, gamut, gamut.space, out, MapToCuspL, cuspLC);
a
和b
也可以来自OKLab坐标,但必须归一化使a^2 + b^2 == 1
。
str = serialize(coords, inputSpace, outputSpace = inputSpace)
将指定的coords
(假定在inputSpace
中)转换为字符串,如果需要,先转换到指定的outputSpace
。如果空间是sRGB,为了浏览器兼容性和性能,将使用普通的rgb(r,g,b)
字符串(字节形式),否则将返回CSS颜色字符串。注意,并非所有空间(如某些线性空间)目前都受CSS支持。你可以选择在coords
数组中传入第四个元素作为alpha
分量(0..1范围)。
import { serialize, sRGB, DisplayP3, OKLCH } from "@texel/color";
serialize([0, 0.5, 1], sRGB); // "rgb(0, 128, 255)"
serialize([0, 0.5, 1, 0.5], sRGB); // "rgba(0, 128, 255, 0.5)"
serialize([0, 0.5, 1], DisplayP3); // "color(display-p3 0 0.5 1)"
serialize([0, 0.5, 1, 0.35], DisplayP3); // "color(display-p3 0 0.5 1 / 0.35)"
serialize([1, 0, 0], OKLCH, sRGB); // "rgb(255, 255, 255)"
serialize([1, 0, 0], OKLCH); // "oklch(1 0 0)"
info = deserialize(colorString)
这是serialize
的反向操作,它会接收一个字符串,确定它引用的色彩空间id
,以及3个或4个(含透明度)coords
。这个功能有意地限制了范围,只支持十六进制RGB、rgb()
和rgba()
字节值,以及oklch()
、oklab()
和不带修饰符的普通color()
函数。
import { deserialize } from "@texel/color";
const { coords, id } = deserialize("color(display-p3 0 0.5 1 / 0.35)");
console.log(id); // "display-p3"
console.log(coords); // [ 0, 0.5, 1, 0.35 ]
注意: 解析仍然是API设计中正在进行的工作,复杂的CSS颜色字符串处理不在本库的范围内。
delta = deltaEOK(oklabA, oklabB)
在OKLab空间中计算两个坐标之间的颜色差异。由于这是一个感知均匀的色彩空间,改进了CIELAB及其缺陷,因此在许多情况下它应该可以适当地替代CIEDE2000颜色差异方程。
[utils]
该模块还导出了许多其他实用工具。
色彩空间
该模块导出了一组色彩空间:
import {
XYZ, // 使用D65白点
XYZD50, // 使用D50白点
sRGB,
sRGBLinear,
DisplayP3,
DisplayP3Linear,
Rec2020,
Rec2020Linear,
A98RGB, // Adobe® 1998 RGB
A98RGBLinear,
ProPhotoRGB,
ProPhotoRGBLinear,
OKLab,
OKLCH,
OKHSL, // 在sRGB色域内
OKHSV, // 在sRGB色域内
// 列出所有空间的函数
listColorSpaces,
} from "@texel/color";
console.log(listColorSpaces()); // [XYZ, sRGB, sRGBLinear, ...]
console.log(sRGBLinear.id); // "srgb-linear"
console.log(sRGB.base); // -> sRGBLinear
console.log(sRGB.fromBase(someLinearRGB)); // -> [gamma编码的sRGB...]
console.log(sRGB.toBase(someGammaRGB)); // -> [线性sRGB...]
注意,并非所有空间都有base
字段;如果未指定,则假定色彩空间可以通过OKLab或XYZ作为根进行传递。
色域
该模块导出了一组"色域",这些色域是由OKLab空间中的近似值定义的边界,允许快速进行色域映射。这些接口主要由gamutMapOKLCH
函数使用。
import {
sRGBGamut,
DisplayP3Gamut,
Rec2020Gamut,
A98RGBGamut,
// 列出所有色域的函数
listColorGamuts,
} from "@texel/color";
console.log(listColorGamuts()); // [sRGBGamut, ...]
console.log(sRGBGamut.space); // sRGB空间
console.log(sRGBGamut.space.id); // 'srgb'
注意:目前尚不支持ProPhoto色域,我欢迎通过Python脚本修复它的PR。
实用工具
除了核心API外,该模块还导出了许多实用工具:
b = floatToByte(f)
将0..1范围内的浮点数转换为0..255范围内的字节,进行四舍五入并限制在范围内。
out = XYZ_to_xyY(xyz, out=[0,0,0])
将XYZ坐标转换为xyY形式,如果指定了out
,则将结果存储在其中后返回。
out = xyY_to_XYZ(xyY, out=[0,0,0])
将xyY坐标转换为XYZ形式,如果指定了out
,则将结果存储在其中后返回。
v = lerp(min, max, t)
使用因子t
在min和max之间执行线性插值。
v = lerpAngle(min, max, t)
使用因子t
在min和max之间执行圆形线性插值,其中min和max被视为角度(以度为单位),允许值在0到360之间环绕,插值以创建最短的弧。
c = clamp(value, min, max)
将value
限制在min和max之间,并返回结果。
out = clampedRGB(inRGB, out=[0,0,0])
将RGB限制(即裁剪)在0..1范围内,如果指定了out
,则将结果存储在其中后返回。
inside = isRGBInGamut(rgb, epsilon = 0.000075)
如果给定的rgb
在其0..1色域边界内,则返回true
,阈值为epsilon
。
rgb = hexToRGB(hex, out=[0,0,0])
将指定的十六进制字符串(带或不带前导#
)转换为0..1范围内的浮点RGB三元组,如果指定了out
,则将结果存储在其中后返回。
hex = RGBToHex(rgb)
将指定的RGB三元组(0..1范围内的浮点数)转换为带前导#
的6字符十六进制颜色字符串。
angle = constrainAngle(angle)
将angle
(以度为单位)限制在0..360范围内,必要时进行环绕。
degAngle = radToDeg(radAngle)
将角度(以弧度为单位)转换为度。
radAngle = degToRad(degAngle)
将角度(以度为单位)转换为弧度。
变换矩阵
您还可以导入更低级别的函数和矩阵;这可能对细粒度转换有用,或者例如将缓冲区上传到WebGPU以进行计算着色器。
import {
OKLab_to,
OKLab_from,
transform,
XYZ_to_linear_sRGB_M,
LMS_to_XYZ_M,
XYZ_to_LMS_M,
sRGB,
OKHSLToOKLab,
DisplayP3Gamut,
} from "@texel/color";
console.log(XYZ_to_linear_sRGB_M); // [ [a,b,c], ... ]
OKLab_to(oklab, LMS_to_XYZ_M); // OKLab -> XYZ D65
OKLab_from(xyzD65, XYZ_to_LMS_M); // XYZ D65 -> OKLab
transform(xyzD65, XYZ_to_linear_sRGB_M); // XYZ D65 -> sRGBLinear
sRGB.fromBase(in_linear_sRGB, out_sRGB); // 线性到gamma传递函数
sRGB.toBase(in_sRGB, out_linear_sRGB); // gamma到线性传递函数
// 非sRGB色域中的OKHSL
// 另见OKHSVToOKLab及其反函数
OKHSLToOKLab([h, s, l], DisplayP3Gamut, optionalOutVec);
插值
该库目前只公开了{ lerp, lerpAngle }
函数。要插值颜色,您需要构建一些额外的逻辑,例如,请参见example-interpolation.js脚本,该脚本在Canvas2D中创建颜色渐变。
自定义色彩空间
您可以构建自定义色彩空间对象来扩展这个库,例如添加对CIELab和HSL的支持。有关示例,请参见test/spaces/lab.js和test/spaces/hsl.js。其中一些空间可能在以后添加到库中,尽管当前重点是"现代"空间(如OKLab,它在很大程度上使CIELab和HSL过时)。自定义色彩空间的文档正在编写中。
注意事项
为什么要开发另一个库?
Colorjs非常出色,可能是JavaScript中当前领先的标准,但它对于创意编码和实时Web应用程序来说并不是很实用,在这些应用程序中,要求通常是(1)更精简的代码库,(2)高度优化,以及(3)最小化GC抖动。
Colorjs,以及类似的Culori,专注于匹配CSS规范,这意味着它很可能会随着时间的推移继续增加复杂性,性能通常会受到影响(例如,@texel/color
的尖点交叉色域映射比Colorjs快约125倍,比culori快约60倍)。
还有许多其他选项,如 color-space 或 color-convert,但这些不支持现代色彩空间,如 OKLab 和 OKHSL,和/或具有可疑的准确度(例如,许多库不区分 XYZ 中的 D50 和 D65 白点)。
支持的色彩空间
本库并不旨在涵盖所有色彩空间;它只专注于有限的"现代"集合,即 OKLab、OKHSL 和 DeltaEOK 在许多实际用途中已经取代了 CIELab、HSL 和 CIEDE2000,使得本库更简单、更精简。请注意,其他空间如 CIELab 和 HSL 可通过"自定义色彩空间"得到支持。
改进与技术
该模块使用了以下一些做法,以显著优化性能并减小打包体积:
- 循环、闭包、解构和其他语法糖被替换为更优化的代码路径和简单的数组访问。
- 移除了热点代码路径中的内存分配,必要时重复使用临时数组。
- 某些转换,如 OKLab 到 sRGB,不需要先通过 XYZ,可以使用已知矩阵直接转换。
- API 设计的结构使得色彩空间通常不在内部引用,允许它们自动进行树摇。
准确度
所有转换都经过测试,与 Colorjs 转换近似相等,误差在 2-33(10 位小数)以内,在某些情况下甚至更准确。
本库使用 coloraide 及其 Python 工具计算转换矩阵和 OKLab 色域近似值。一些矩阵已硬编码到脚本中,并尽可能使用有理数(如 CSS 颜色模块工作草案规范 中 建议 的那样)。
如果您认为矩阵或准确度可以改进,请提交 PR。
基准测试
test 目录中有几个基准测试:
- bench-colorjs.js - 运行
npm run bench
与 colorjs 进行比较 - bench-culori.js - 用 node 运行以与 culori 进行比较
- bench-node.js - 运行
npm run bench:node
获取 node 性能分析 - bench-size.js - 运行
npm run bench:size
使用 esbuild 获取小型打包大小
以下结果基于 MacBook Air M2。请注意,Colorjs 的性能取决于您使用的 API(默认的基于类的 API 比过程式 API 慢得多)。
与 Colorjs.io 的基准对比
转换(Colorjs.io 过程式 API)--
Colorjs.io:2955.88 毫秒
我们的:457.86 毫秒
加速:快 6.5 倍
转换(Colorjs.io 主要 API)--
Colorjs.io:10034.38 毫秒
我们的:452.11 毫秒
加速:快 22.2 倍
OKLCH - sRGB 色域映射(Colorjs.io 过程式 API)--
Colorjs.io:5602.46 毫秒
我们的:49.10 毫秒
加速:快 114.1 倍
OKLCH - sRGB 色域映射(Colorjs.io 主要 API)--
Colorjs.io:5913.80 毫秒
我们的:44.91 毫秒
加速:快 131.7 倍
所有空间到 P3 的色域映射(Colorjs.io 过程式 API)--
Colorjs.io:4693.43 毫秒
我们的:150.16 毫秒
加速:快 31.3 倍
所有空间到 P3 的色域映射(Colorjs.io 主要 API)--
Colorjs.io:5478.16 毫秒
我们的:145.88 毫秒
加速:快 37.6 倍
与 Culori 的基准对比
使用输入类型:OKLab L 平面的随机采样进行测试
OKLCH 到 P3 的转换 --
Culori:43.30 毫秒
我们的:12.83 毫秒
加速:快 3.4 倍
OKLCH 到 P3 色域的色域映射 --
Culori:1588.62 毫秒
我们的:23.05 毫秒
加速:快 68.9 倍
本地运行
克隆,npm install
,然后运行 npm run
列出可用的脚本,或运行 npm t
执行测试。
致谢
这个库的实现得益于许多开发者和工程师的出色前期工作:
许可
MIT 许可,详见 LICENSE.md。