如果你在做前端组件库、可视化大屏、无障碍辅助工具,或者任何一个需要“把颜色翻译成人话”的场景,你迟早要面对一个名字叫“色彩命名工具”的小模块。它的活儿很简单:用户从拾色器里拖出一个#3498DB,你要告诉他这叫“天蓝色”而不是“蓝色”;屏幕上一堆图表,你要给红色系、橙色系、紫色系自动打上人类能看懂的分类标签。
这个需求听起来不复杂,真正动手写核心JS实现的时候,坑比想象中多:颜色距离怎么算?色名数据从哪来?命名结果为什么总在“青”和“绿”之间摇摆?RGB空间里看起来差不多的两个颜色,在人眼里一个偏紫一个偏灰,到底怎么权衡?
这篇博文就聚焦在“核心JS实现”上,把我实际做这个工具时的设计思路、算法选型、代码细节、性能优化和踩坑记录完整复盘一遍。不堆概念,不贴大段框架代码,只在最关键的地方给出可直接复用的实现,适合刚接触色彩计算的前端同学,也适合想把命名模块从“能用”优化到“好用”的进阶开发者。
1. 命名这件事,本质是一个“最近邻查找”问题
1.1 机器不知道什么是“好看的天蓝”
色彩命名工具的核心逻辑,其实就是三步:把用户输入的颜色解析成标准色值、计算这个色值跟色名库里每个已知颜色的“距离”、取距离最近的那个名字作为输出。
听起来简单,但这里有一个关键认知:机器根本不知道什么是“天蓝”,它只知道自己算出来的Lab色彩空间距离。你给它一个RGB(52, 152, 219),它只能在预设的色名表里找一找,哪个已知色跟这个值的空间距离最小。所谓“天蓝”,本质上就是预先整理好的一个Lab坐标点而已。所以整个工具的准确度上限,取决于两件事:色名表数据的质量、距离算法对“视觉感知”的模拟程度。
先聊数据。色名表长什么样?我最初用的方案是直接拿CSS命名颜色那146个颜色来凑。这数据够标准,浏览器原生支持,但问题是它太稀疏了。它把整个色域打碎成一百多个点,很多中间色根本找不到合适的映射,比如#00AACC这种“青绿色”,在CSS标准命名里既不是cyan也不是teal,结果是命名的准确度和自然度都不好。
后来我换成了开源色名库,里面动辄上千个名字,像“海洋泡沫”“雾霾蓝”“脏粉”这类更接近人类真实语言习惯的词,确实覆盖得全了。但问题也跟着来:数据质量参差不齐。有的库把“浅灰色”和“银灰色”设置的距离非常近,导致一个小数点的RGB波动就能让命名结果跳跃。最终我实际跑出来的结论是:数据源选择必须首先保证“命名空间均匀覆盖”和“最小间距可控”,绝不能贪多。推荐的做法是自建一个小而精的基础色名库,以CSS标准名为底座,向上扩充到300-500个常用颜色词,每条数据包含中文名、英文名、十六进制值和对应的Lab坐标。核心JS逻辑只依赖Lab,不依赖RGB,这是后面所有算法的基础。
1.2 为什么RGB空间距离不靠谱
如果直接在RGB空间里算欧几里德距离,比如sqrt((r1-r2)^2 + (g1-g2)^2 + (b1-b2)^2),会有一个很烦人的问题:人眼对绿色之间的差异不敏感,对蓝色之间的差异也不是很敏感,但对红色之间微微的偏移却非常敏感。这导致RGB距离算出来“差不多”的两个颜色,在人看来差异极大;反过来,RGB距离大的两个颜色,人眼反而觉得挺像。
这就好比你用一把尺子去测量温度,尺子的刻度跟温度的物理变化根本不挂钩,量出来的数值自然不反映真实温度。
所以成熟一点的做法,是把颜色转到Lab色彩空间再计算距离。Lab空间是国际照明委员会定义的颜色空间,设计核心目标就是“感知均匀”——在Lab空间里距离相等的两个颜色点,人眼主观感知的差异也基本相等。我们在实际项目里验证下来也确实是这么回事:同一个色名库,RGB距离和Lab距离跑出来的命名结果,在描述性上差别非常明显,Lab方案明显更贴近人的第一反应。
所以核心JS实现的第一步是一个可靠的RGB到Lab的转换函数,再配合一个“加权欧氏距离”来判断最近邻——这里不用CIEDE2000这个高精度色差公式,原因后面会讲。对于命名这个需求来说,Lab加权距离已经是性价比最高的方案。
2. 核心JS实现:从RGB到Lab的可靠转换
2.1 先做输入预处理,别让脏数据进来
不管用户给的是#RRGGBB、#RGB、rgb(r,g,b)还是hsl字符串,第一步都要统一解析为[0,255]范围内的RGB三元组。这个函数看似没什么技术含量,但有一个细节特别容易翻车:透明通道的兼容问题。
rgba(0, 0, 0, 0.5)这种输入,如果你直接忽略了alpha值,命中的名字可能完全不是用户想表达的。比如一个半透明白色,用户看到的“浅灰白”,如果你的解析把它当纯白来处理,那命名就完全走偏了。我的处理方式是:对带alpha的输入,先做背景混合(默认白色背景),输出一个不透明的混合色,再进入命名流程。这个处理相当于把透明色当成一个物理涂在纸上的颜色,更符合直觉。
预处理的代码我用了一个非常轻的解析器:
function parseColor(input) { if (typeof input !== 'string') return null; let str = input.trim().toLowerCase(); if (str[0] === '#') { return parseHex(str); } if (str.startsWith('rgb')) { return parseRgbOrRgba(str); } if (str.startsWith('hsl')) { const { h, s, l, a } = parseHsl(str); const rgb = hslToRgb(h, s, l); if (typeof a === 'number' && a < 1) { return blendWithWhite(rgb, a); } return rgb; } return null; }这里值得说一说的,是hsl转rgb再混合的逻辑。我们平时用CSS写颜色,hsl(210, 100%, 50%)是那个标准的亮蓝色,但它跟用户心里想的“蓝”有细微的感知差异。所以色名表里的基准色我统一存成sRGB值,任何形式的输入都先归一化到sRGB三元组,再转Lab。归一化这一步保证了计算基准完全一致,不会因为输入格式不同而产生“同样颜色两种命名”的问题。
2.2 RGB转Lab:记住三个关键细节
RGB到Lab的转换,网上能找到一堆简化版本,但很多简化版在临界区域会有偏差。完整的转换公式分三步:sRGB通道线性化、RGB到XYZ、XYZ到Lab。我这里给出一个在我项目里跑了很久、误差非常小的实现:
function srgbToLinear(c) { c = c / 255; return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); } function rgbToXyz(r, g, b) { r = srgbToLinear(r); g = srgbToLinear(g); b = srgbToLinear(b); return { x: 0.4124 * r + 0.3576 * g + 0.1805 * b, y: 0.2126 * r + 0.7152 * g + 0.0722 * b, z: 0.0193 * r + 0.1192 * g + 0.9505 * b }; } function xyzToLab(x, y, z) { const refX = 95.047; const refY = 100.0; const refZ = 108.883; x = x / refX; y = y / refY; z = z / refZ; const e = 0.008856; const k = 903.3; const fx = x > e ? Math.cbrt(x) : (k * x + 16) / 116; const fy = y > e ? Math.cbrt(y) : (k * y + 16) / 116; const fz = z > e ? Math.cbrt(z) : (k * z + 16) / 116; return { l: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) }; } function rgbToLab(r, g, b) { const { x, y, z } = rgbToXyz(r, g, b); return xyzToLab(x, y, z); }有几点必须说明:
- D65标准光源的参考白点值(refX, refY, refZ)必须是(95.047, 100, 108.883),不能用其他光源的白点。有的文章用D50,那会引入一致的偏色偏移,看起来不明显,但会让部分临界颜色的命名漂移。
- 线性化时必须用2.4的gamma指数和0.04045的阈值,不能看网上的旧代码直接套 2.2。sRGB标准就是这样定义的,偏离它会让Lab值在高光部分有偏差。
- 在Lab公式里,小数值的处理用的是线性分段,也就是对于非常暗的颜色,用k*x + 16代替Math.cbrt(x),否则暗部颜色会出现不连续的跳变。
这套实现跑出来的Lab值,跟浏览器内置的color.js以及主流色彩工具对比,误差极小。我在做回归测试的时候专门对比过几十个颜色,最大偏差在L轴不超过0.5,这已经完全足够用于命名判断了。
2.3 色差计算:为什么选Lab加权欧氏距离而不是CIEDE2000
很多文章一上来就推CIEDE2000色差公式,说它是行业标准,算出来的色差最符合人眼感知。这话没错,但我实际对比之后发现,在色彩命名这个场景里,CIEDE2000有三个致命问题:
第一,公式复杂,迭代计算多。CIEDE2000包含色调旋转修正项、亮度加权函数,还涉及反正切修正,代码实现大概有上百行。命名工具的核心JS如果做得太厚重,打包体积上会有压力。
第二,CIEDE2000的计算结果对临界颜色区分度太高。什么意思呢?它会把两个视觉上非常接近的颜色算出差异较大的色差,这在工业质检、印刷对色领域是优势,但命名工具恰恰相反——我们需要在海量候选色里找“最接近的那个”,如果一个候选色和另一个候选色本来就肉眼难分,那么谁排第一谁排第二其实没多大关系,强行挤掉反而会让名字在微小抖动时来回跳。
第三,命名工具必须快。现在的色名表动辄上千条,如果在核心循环里对每一对颜色算一次CIEDE2000,单次查询可能要到几十毫秒,这点时间虽然不长,但如果你要在大批量像素上做颜色标注(比如图像处理、主题色提取),性能就会被拖垮。
所以我的实际选择是:Lab空间里直接把欧氏距离算出来,然后对L、a、b三个通道分别做加权。具体权重我是靠一组视觉评测调出来的:
function colorDistance(lab1, lab2) { const dL = lab1.l - lab2.l; const da = lab1.a - lab2.a; const db = lab1.b - lab2.b; return 1.0 * dL * dL + 1.2 * da * da + 0.8 * db * db; }调出来的经验是:b通道(蓝黄轴)的权重应该略低,a通道(绿红轴)的权重略高。人眼对黄蓝变化确实不如对红绿变化敏感,这跟色彩科学的结论一致。这个加权欧氏距离的好处是:计算快、稳定、可解释,而且只用一个参数就能在“光线的偏蓝判断”和“纯色的红绿判断”之间做平衡。
3. 用分层桶索引优化最近邻查找
3.1 暴力遍历的问题
如果你的色名表只有146个CSS颜色,那直接在查询时全部遍历一遍's fine。但一旦数据规模翻到上千甚至上万,每次都过一遍全量计算,就会在低端设备上出现肉眼可感知的卡顿。尤其在某些图表组件里,颜色标注可能发生在滚动过程中,那对性能的要求就严格得多。
暴力遍历的核心问题在于:它把查询当作一个“全局问题”来处理,而忽略了颜色空间的局部性。实际上,色名表是静态数据,只要一次预处理,就能把颜色空间切割成很多小区块,让查询只在一个小区块里寻找。
3.2 按L桶划分,把查询时间压到个位数毫秒
我实际用的方案是构建一个基于L通道的分层桶索引——把色名表按明度(L)切成多个桶,建议切20个桶,每个桶覆盖5个L单位。这样每个桶里的候选颜色数量大概降到全量的二十分之一。
为什么按L切而不是按a、b切?因为人眼对明度的变化最敏感。一个高亮度的颜色,绝不会和一个低亮度的颜色是同一个名字。切L通道能保证同桶内颜色的明度范围很窄,从而把命名判断聚焦在明度立刻匹配的候选上。
索引构建逻辑很简单:
class ColorNameIndex { constructor(colorNameArray) { this.buckets = new Array(20); for (let i = 0; i < 20; i++) this.buckets[i] = []; for (const item of colorNameArray) { const lab = rgbToLab(item.rgb[0], item.rgb[1], item.rgb[2]); item.lab = lab; const bucketIndex = Math.min(19, Math.floor(lab.l / 5)); this.buckets[bucketIndex].push(item); } } findNearest(lab) { const bucketIndex = Math.min(19, Math.max(0, Math.floor(lab.l / 5))); let best = null; let bestDist = Infinity; const candidates = this.buckets[bucketIndex]; for (const item of candidates) { const d = colorDistance(lab, item.lab); if (d < bestDist) { bestDist = d; best = item; } } if (bestDist < 900) return best; // 如果当前桶没找到足够近的,需要去相邻桶找 const neighbors = [bucketIndex - 1, bucketIndex + 1]; for (const idx of neighbors) { if (idx < 0 || idx >= 20) continue; for (const item of this.buckets[idx]) { const d = colorDistance(lab, item.lab); if (d < bestDist) { bestDist = d; best = item; } } } return best; } }这里有一个很大的坑:如果当前桶里找不到足够近的颜色,直接返回best会得到一个错误的命名。比如输入一个L=48的颜色,当前L分区是45-50,但那个区域色名表恰好没有覆盖,而真正的最近邻在L=51的桶里,那就要到相邻桶去找,否则结果会明显跳偏。
这个回退策略一开始我没加,结果遇到了“荧光绿”被识别成“浅灰”的怪现象。原理是:L=70的颜色落到了高亮区,但高亮区里没有绿色词,最近的灰色在相邻桶,距离虽然有点远但在全量的最近邻里已经是第一了。把这个边界问题解决后,命名稳定性有了明显提升。
3.3 死区判定:避免“硬凑”出错误名字
还有一个小case可能很多人会忽略:当输入色跟色名表里所有已知色都距离很远时,硬取最近的那个名字是不合理的。比如一个输入色落在高饱和的UV区域,而你的色名表主要覆盖常见颜色,那它离所有已知色的距离都很大。这种情况下强行命名,会得到“勉强接近某某色”,用户一看就觉得不对劲。
我的处理方式是设置一个“死区阈值”。当最近距离超过某个值时,返回的不是单一命,而是返回“糟糕,无法匹配任何已知颜色”,让上层组件决定是否展示原始色值或者提示用户手动输入名字。这个阈值不要定太死,我跑出来的推荐值是加权距离超过1200就判定为死区。这个值不是绝对的,跟你色名表的数量和分布有关,最好在数据集上实际跑一遍统计直方图再定。
4. 命名结果的排序与候选展示
4.1 多候选比单选更实用
刚开始做这个工具的时候,我天真地以为只要返回一个字符串就完事了。实际跑了一些真实用户反馈之后发现,很多情况下用户对一个颜色的描述是模糊的,即使程序认为它最接近“天蓝”,用户心里想的可能是“湖蓝”或“亮蓝”。单项命名的结果,用户要么接受,要么强烈反对,没有任何中间地带。
换位思考一下:如果是人工给颜色命名,你会不会犹豫“这个到底是绿还是青”?当然会。所以实际产品里,更好的策略是返回Top 3候选名字,并附上各自的距离值。这样用户可以在有歧义的时候看到“这个颜色其实也接近某某色”,这比一句生硬的“天蓝”更贴近人类的认知方式。
我在核心接口上这么设计:
function findNearestCandidates(index, lab, topN = 3) { const bucketIndex = Math.min(19, Math.max(0, Math.floor(lab.l / 5))); const startIdx = Math.max(0, bucketIndex - 1); const endIdx = Math.min(19, bucketIndex + 1); const results = []; for (let i = startIdx; i <= endIdx; i++) { for (const item of this.buckets[i]) { const d = colorDistance(lab, item.lab); if (results.length < topN) { results.push({ item, d }); results.sort((a, b) => a.d - b.d); } else if (d < results[results.length - 1].d) { results[results.length - 1] = { item, d }; results.sort((a, b) => a.d - b.d); } } } return results.map((r) => r.item.name); }在设计这个接口的时候,还有一个细节是值的返回格式。建议不要直接返回字符串,而是返回一个结构体:{ name: '天蓝', hex: '#87CEEB', distance: 23.4, lab: {...} }。这样上层组件既能展示名字,也能展示色块,还能拿到距离进行置信度判断。别小看这个设计,它让组件从“万能命名器”变成了“可以被灵活组合的颜色基础设施”。
4.2 候选词的同义归并与词频排序
候选排序看着简单,真正跑起来会有个问题:色名表里存在大量同义词,比如“天蓝”和“天空蓝”,它们在Lab坐标上几乎重叠。如果你不处理同义词,Top 3可能会给出“天蓝、天空蓝、蔚蓝”这种三个用户读起来基本一样的结果,实际上没有任何信息量。
所以预处理阶段还需要做一次同义归并:把距离相似的词按优先级排序,优先展示更大众化、更口语化的词。这一步可以用一个简单的“展示优先级”字段来做,人工为每个名字打一个1-5的优先级分数。如果两个候选名之间的Lab距离小于0.5且优先级不同,排序时直接让高分词排前面。这样做出来的Top 3不再是一堆近义词,而是真正覆盖“这个词”、“另一个角度的词”和“更细致的描述词”。
5. 性能实测与工程选型
5.1 不同数据量级下的策略选择
我在实际项目里跑过三组不同量级的色名表,结论直接决定你用哪种索引策略:
| 色名表规模 | 查询方式 | 预处理耗时 | 单次查询耗时 |
|---|---|---|---|
| 146个CSS色 | 全量遍历 | 0ms | 0.02ms |
| 1200个开源色名 | 全量遍历 | 0ms | 0.24ms |
| 1200个开源色名 | 20桶索引 | 1.5ms | 0.05ms |
| 5000个扩展色名 | 20桶索引 | 6ms | 0.11ms |
| 50000个采样色名 | 20桶索引 | 60ms | 0.48ms |
从这个表可以看出一个规律:只要色名表数量在几百量级,全量遍历完全够用,不需要任何索引;但到了几千量级,索引结构带来的提升非常明显,尤其当你需要描边大量像素时。对于一个纯单次调用的场景,全量遍历和索引在体感上几乎没有区别,但如果你把它用在一个循环里处理一整个像素矩阵,那0.2ms和0.05ms的区别就被放大了40倍。
5.2 包体积和依赖取舍
很多库喜欢引入完整的颜色转换库来处理这套逻辑。可在我的项目里,核心JS实现只有一个文件、120行代码,没有任何第三方依赖。为什么?因为颜色转换是一个相对封闭的数学问题,完全可以自实现。引入依赖反而会带来几个问题:包体积从2KB膨胀到20KB、依赖的版本升级可能改变转换精度导致命名结果漂移、构建工具里还要额外处理ESM和CJS的兼容。
我最终把文件结构组织的很干净:
color-namer/ ├── index.js // 主入口 ├── color-convert.js // RGB/HEX/HSL/Lab转换 ├── distance.js // 加权色差计算 ├── color-name-index.js // 分层桶索引 ├── color-names.json // 色名数据 └── index.d.ts // TypeScript类型定义index.js暴露的API少而精:
export function colorName(colorInput, options) { const rgb = parseColor(colorInput); if (!rgb) return null; const lab = rgbToLab(rgb[0], rgb[1], rgb[2]); const index = getIndex(); return index.findNearest(lab); }options里可以传入自定义色名表、命名数量、死区阈值。默认的色名表通过JSON文件导入,运行时以懒加载方式初始化索引,不会拖慢页面首屏加载。
6. 调试方法与雷区排查实录
6.1 常见异常现象与根因
排查过程中我记录了几个高频问题的根因,整理成速查表:
| 异常现象 | 根因分析 | 处理方式 |
|---|---|---|
| 同一个色值两次命名结果不同 | 索引构建时机不同,色名表部分数据是异步加载 | 确保索引初始化完成后再调命名接口,初始化加锁 |
| 浅蓝色被命名为“灰色” | 色名表缺少浅蓝区域的候选词 | 扩充色名数据,或调低b通道权重 |
| 荧光色硬匹配到“暗绿” | 所有候选距离都极大,死区阈值未触发 | 精确计算死区阈值,改为返回null或带降级文案 |
| 输入#FFF和#FEFEFE命出不同名字 | Lab距离极小但阈值切分在索引边界两侧 | 增加相邻桶回退机制,或增大桶重叠度 |
| 移动端低端机上耗时突增 | 色名表未预构建索引,每次查询全量遍历 | 首次查询前构建索引,或将索引封装在Worker线程 |
录几个特别值得展开的:
第一个“同一个颜色两次命名不同”的问题,根源其实不是算法,而是我的库在初始化时如果色名数据还没加载完成,就先用了一个瘦版空数据建索引,导致首次查询命中空桶。后来改成了Promise模式,通过getIndex().then()调用,彻底消灭了异步竞态。
第三个“荧光色硬匹配”的问题很有代表性。只要你在接口设计上做了死区阈值判断,就能兜住这类异常。不过阈值定位有说法,我用了一组人眼评测:随机抽30个颜色,人工标出“这颜色有合适的名字吗”,并记录对应距离值,画出分布直方图之后,阈值就定在距离分布的右侧拐点。这个数据驱动的定参方式比拍脑袋靠谱得多。
6.2 调试时的视觉校验法
一个特别有效的方法,是把色名表和查询结果渲染成色块,然后人工快速扫视校验。具体做法是把每个命名结果对应的色块和原输入色块并排,在一行列表里展示色块、原色值、候选名、距离值。不需要复杂的UI,一个简单的HTML页面就够用。
我来实际用一下这个逻辑:用户拖入一个#3498DB,页面把输入的色块和Top1命名的色块渲染出来,旁边标注“天蓝, 距离23.4”。如果色块看起来明显不像,那肯定不是命名结果的问题,而是色名表数据设置有问题——比如这个“天蓝”基准色在Lab空间里偏了几个单位,或者色名表的覆盖有空洞。
这个调试方法比断点调试高效很多,因为色彩感知本身就是视觉性的问题,用视觉来验证逻辑闭环,是最直接的方式。
6.3 浏览器环境下的集成注意事项
如果你在浏览器里做集成,还要留意CSS环境的影响。比如你要在canvas上做像素级颜色标注,那么canvas的color space默认是sRGB,这没问题;但如果你在CSS中用了color-mix或者将颜色转换成display-p3色域,那就不能直接用sRGB转换逻辑,得先把显示色域统一转换回sRGB再进Lab计算。
此外,如果命名工具在页面初始化阶段就要被调用,建议把JSON数据解析和索引构建放到requestIdleCallback或者setTimeout(0)里,避免阻塞首屏渲染。如果命名请求会频繁触发,比如跟随鼠标取色,最好把查询函数设计成纯函数,不做任何DOM操作,保留在数据层,这样就不会产生浏览器布局抖动问题。
7. 测试用例与扩展方向
7.1 回归测试用例集
核心JS写好后,一定要建一组固定的回归测试用例。我的用例集由三部分组成:
第一部分是标准色验证,直接从CSS颜色名取一部分作为输入,断言返回的第一个名字就是该标准名本身。比如输入#FF6347,结果第一个必须是“番茄红”。这部分用例能确保转换链路不漂移。
第二部分是临界色验证,专门挑一些跨色相边界的颜色,比如#00A96D(介于青和绿之间)之类。这类用例主要验证死区阈值和候选排序是否稳定。
第三部分是随机色模糊测试,用脚本随机生成1000个颜色,不指定预期值,只要求结果不为null且匹配距离在合理范围内,这是防回归的兜底方案。
回归测试的最佳实践,是把“色名表数据”和“算法代码”分开测试。因为算法一旦写错,所有用例都会失败;而色名表如果被人工改坏了,通常只会集中失败一批用例,这能帮你快速定位到底是数据问题还是逻辑问题。
7.2 后续扩展:从单点命名到批量分析
核心JS做完之后,扩展方向其实不少。最直接的是做成批量处理接口,输入一个颜色数组,返回对应的名字数组。这个用法在数据可视化里非常常见,比如给图表里的系列数据自动分配可读标签。
另一个方向是“颜色语义分析”。你可以把命名结果跟一些常见的场景关键词关联起来,比如“冷淡”“温暖”“清新”“复古”,从而快速判断一组颜色大概是什么情感倾向。这个思路虽然不能直接通过Lab距离做到,但能借助命名结果作为中间层,映射到更高层的语义空间。
个人建议是不要在一开始就把这层语义分析加进去,先把底层命名做到可信,语义分析才有意义。
8. 最后分享一个小技巧:别迷信“标准色卡”
我在这个项目里踩过最大的坑,是过度相信某些权威色卡的数据。色卡本身没错,但放在数字屏幕上会有偏差:屏幕的色域、色温、甚至不同操作系统的色彩管理都会改变显示颜色。你是为数字产品做的命名工具,那色名表参考颜色就应该在数字环境下被重新采样和校正。
我在做数据校准时,会把每个基础颜色的色块渲染到屏幕正中央,旁边放一个中性灰背景,然后目测调它的RGB值,直到“看起来”符合那个名字给人的直觉。这个工作听起来很主观,但它恰恰是最贴近真实使用场景的优化方式。毕竟,工具最终是要服务人的直觉,而不是服务色度计的读数。
色彩命名的乐趣,就在于游走在客观测量和主观感知之间。核心JS实现只是一层薄薄的外壳,外壳下面的数据打磨和算法选择才是真正的价值所在。
希望这篇复盘能帮你把命名模块做顺,祝编码愉快。