猫工具站
帮助中心 猫Blog 关于我们 广告合作 EN

颜色三件套:转换器怎么读十六进制、提取器看多少像素、渐变器什么时候给出坏 CSS

颜色更新于 2026-09-30

三个页面都挂着「颜色」两个字,但里面的机器毫无关系。颜色转换器是一串手写解析加位运算,它的输出是确定的、可复现的,风险全在边界写法上。配色提取器是一台抽样聚类机:它只看极小一部分像素,中心点靠随机数选,还会主动合并和丢弃颜色,所以它给的是「一次结果」而不是「那个结果」。CSS 渐变生成器则是一个状态机,而它的状态组合里有一条会把无效 CSS 递给你。

这三件事的防法也不一样:转换器要防的是「它没报错」;提取器要防的是「它报的数字和你以为的不是一回事」;渐变器要防的是「预览是对的,代码是错的」。

下面每一条都是从三个页面自己的脚本里读出来,再在页面上实际跑一遍确认过的。

转换器:它不是在解析颜色,是在做位运算

颜色转换器的十六进制入口只有四行:去掉 #,三个字符就把每位复制一遍,然后 parseInt(h, 16),最后 (n>>16)&255、(n>>8)&255、n&255 取三个通道。问题全在这条移位链上——JS 的位运算先把数折成 32 位有符号整数,再从低位往回读三个字节。

你输入 页面读到的 为什么
#ff7e5f rgb(255,126,95) 正常
ff7e5f rgb(255,126,95) 它先删 #,没有 # 也照样吃
#fff rgb(255,255,255) 三位扩展
#FF0000ff rgb(0,0,255) 折成 32 位后只剩 0000ff,红色和alpha一起没了
#1234567889aabb rgb(137,170,187) 12 位照样折回 32 位,只有末尾六个十六进制位参与结果
#12g456 rgb(0,0,18) parseInt 在 g 处停下,12 当成 0x12
#ggzz 不更新 一位都读不出来,isNaN 才成立
`` (空) 不更新 同上

最后一列那一句是最要紧的:这个页面没有错误提示。每个输入框的处理函数在解析失败时直接 return,屏幕上留着上一个颜色,不闪红字、不变边框、不弹提示。所以判断「我刚才粘贴的东西它到底收没收」的唯一办法是看输出有没有变。而 #12g456 这种打错一个字母的输入比失败更糟:它成功了,输出一个 #000012 的近黑色,三个框看起来完全合法。

从设计工具里带 alpha 的颜色贴进来之前,先把末尾两位删掉。这是这个页面唯一没写在界面上的使用条件。

HSL 那一格用的是严格正则:hsl 加括号,三段整数,后两段必须带 %,逗号分隔。实测之下 hsl(0, 100%, 50%) 能用,hsl(0 100% 50%)(现代空格写法)、hsl(0, 100.5%, 50%)(小数)、hsl(0deg, 100%, 50%)(带单位)都无声失败。

HSL 是显示值,不是颜色本身

页面把 rgb 换算成 HSL 时,三个通道各除以 255 算出 h、s、l,然后 Math.round 成整数——色相 0–360、饱和度 0–100、明度 0–100。这三个整数是拿来显示的,但滑块和 HSL 输入框读的就是它们,于是「抄下 HSL 再贴回来」并不等于原颜色。

把页面自己的两个函数原样跑一遍全部 16,777,216 个 RGB 值,结果是:

  • 89.4% 的颜色(15,002,160 个)往返会变,剩下 10.6% 恰好是整数 HSL 能精确表示的。
  • 单通道最大偏移 5。实测到的极端例子:rgb(2,228,230) → hsl(181, 98%, 45%) → 回来是 rgb(2,223,227),绿色掉 5、蓝色掉 3。
  • 整数 HSL 三元组总共只能到达 2,037,576 个颜色,占全空间的 12.1%。也就是说 361×101×101 = 3,685,481 个位置里,有三分之一是重复的,而剩下的 87.9% 颜色你用滑块根本调不出来。

结论很短:要精确就用 HEX。HSL 框和三个滑块适合找方向、找深浅,不适合定稿。

反过来的路是干净的:rgbToHex 对每个通道 Math.round 再夹到 0–255,所以 HEX 那一格永远不会出现 #00000e 之类的意外——HEX 是这个页面上唯一可信的输出。复制按钮点下去会变成 ✓ 一秒钟;剪贴板被拒绝时 .catch 是空的,按钮纹丝不动,也不告诉你为什么(非 HTTPS 环境、页面不在焦点、浏览器不允许都会走这条路)。

预设色板(点击取色)里是 24 个固定色值,从 #FF6B6B 到 #CED4DA,点了直接填色。它们是装饰用的调色板,不是任何算法的结果。

提取器:它只看你图片的千分之几

配色提取器的流程写在它自己的常见问题里,而且和代码对得上:取 alpha ≥ 128 的像素 → K-means++ 初始化中心 → 迭代 12 轮 → 合并欧氏距离 < 24 的相似色 → 压制低饱和度背景色 → 按频率和饱和度评分排序。真正需要知道的细节都在数字上。

它读全尺寸图,但只抽检 5000 个像素。 页面先按图片原始宽高建 canvas、getImageData 拿全分辨率的 RGBA 缓冲——1200 万像素就是约 48 MB 内存,这也是这个工具真正的成本(不是隐私成本,图片不上传)。然后进入聚类之前有一段抽样:像素数超过 5000 时,step = floor(pixels.length / 5000),从下标 0 开始每隔 step 个取一个,取满 5000 就停。

1200 万像素的图:floor(12000000 / 5000) = 2400,即每 2400 个不透明像素里取 1 个。这是一条固定的等距扫描线,不是随机抽样。它有两个后果:

  • 等距步长在二维网格上会走出图案。步数和图片宽度有公因子时,抽样点会集中在若干条竖列上,其余整列一个都不看。Logo 里那条细细的品牌色如果正好落在没被抽到的列上,就永远不会出现在结果里。
  • 抽检的前提是「不透明像素」列表。透明区域先被剔掉,所以一张四周透明的大 PNG,抽样密度和一张同尺寸的实底 JPG 完全不同。

想让某个颜色稳定进结果,最省事的办法不是提高图片质量,而是把图缩小——几百像素宽的时候 pixels.length 常常不到 5000,抽样整段被跳过,每个像素都被分析。或者裁到你真正要取色的那块区域。

聚类中心每轮重掷。 K-means++ 的初始化里,第一个中心是写死的第一像素,之后的每一个都按「到已选中心的最近距离平方」做加权随机(Math.random() * totalDist 累加选取)。这意味着同一张图点两次「重新提取」,中心起点不同,12 轮迭代的落点也不同。实测:纯色平涂图每次都完全一样;照片会在 1–2 个通道单位内漂移。第一像素被钉住这点也有可见后果——如果你的图片左上角是背景色,那个颜色已经预选了一个中心。

「颜色数量 5–9」是上限,不是配额。 滑块默认 7,范围 5 到 9,改完立刻重算。但结果数量在两道工序之后才定:先按 K 聚类,再把欧氏距离 < 24 的簇合并(RGB 空间里 24 大约是肉眼难辨的差别),然后压制饱和度——cutoff = 0.18 × 这张图里最高的饱和度,低于这条线的颜色直接丢掉;如果整张图最大饱和度低于 0.05(近黑、近白、纯灰),这一步整体跳过,不然灰图会被清空。所以「我要 7 个色,它给我 4 个」是正常输出:要么有两簇挨得太近被并了,要么有颜色被判成背景压掉了。灰色、米白、浅蓝大面积背景最容易在这里消失。

那个百分比不是占图片的比例。 评分阶段对每个存活的簇,把 colorDist(像素, 簇中心) < 24 的像素都数进去——一个像素离两个中心都小于 24 时,两个簇都算它一份。最后这些计数被分摊成总和 100% 的比例。所以它们只能互相比较,不能拿去说「这个色占了画面 30%」。

「显示分布」画的是聚类归属,不是遮罩。 它按预览的 CSS 尺寸建一块离屏 canvas,对每个显示像素做最近邻取值(srcX = floor(x * scaleX)),把「赢了这个像素」的颜色填进去,再以 alpha 180/255 叠回原图。它不判断颜色合不合适,只判断谁离得近——所以大片区域被染上一个勉强接近的颜色,恰恰是它在如实告诉你聚类边界长什么样。

渐变器:2 到 4 个停止点、没有 alpha、只有一个圆

CSS 渐变生成器是三个页面里约束最多的,而约束一条都没写在界面上。

  • 只有线性与径向。 没有 conic。径向输出被写死成 radial-gradient(circle, …)——没有 ellipse,也没有 at 30% 70% 的定位。要这两种就得复制完手工改。
  • 停止点最少 2 个、最多 4 个。 到 4 个时「+ 添加颜色停止点」直接从页面上消失(display: none),处理函数也会提前 return,两条路都堵死。只剩 2 个时,删除用的 ✕ 根本不会渲染,原位置是一个 28px 的空占位块。五个色的彩虹这页做不出来。
  • 新加的颜色永远是 #888888,位置是 min(100, 上一个 + 20)。 这条公式不管碰撞:如果最后一个停止点已经在 100%,新的也落在 100%。同一位置叠两个色,后者分到的带宽是零——实测「显示分布」之外更直接的办法是看预览:加之前加之后 computed background 一模一样,一个字节都不差。唯一说明它生效了的,是计数变成「3 个颜色」以及代码里多出 #888888 100%。所以加停止点之前,先把最后那个从 100% 挪开。
  • 没有 alpha,但十六进制框每敲一个字都校验。 颜色来自 <input type="color"> 加一个十六进制文本框,后者要求 # 加 3 位或 6 位十六进制:#abc、#ff7e5f 收;ff7e5f、#ff7e5f80、rgb(1,2,3)、red 全部无声忽略,你的文字还留在框里,预览还是旧颜色。因为 3 位本身就是合法颜色,输入 #a1b2c3 的过程是这样的:#a 不收、#a1 不收、#a1b 收——渐变在半路跳成那个灰蓝色,#a1b2、#a1b2c 都不合法所以它保持,直到第六位落下才变成 #a1b2c3。如果你就停在三位,代码里写出的也是 #a1b,不会展开成 #aaaabbcc。要淡出到透明没有入口,只能复制出来自己加 transparent 或八位色值。
  • 方向只有五个按钮,其中三个是关键字。 网格是「上到下」(to bottom,进页面时选中)、「左到右」(to right)、「对角线」(to bottom right)、「径向」、「自定义」,上面还有两个类型按钮(线性渐变 / 径向渐变)。没有「上」「左」「左上」这些按钮,要它们只能走「自定义」写角度。角度滑条只在自定义模式出现,state.customAngle 默认 45 但静静躺在数据里,进页面的代码框显示的是 background: linear-gradient(to bottom, #ff7e5f, #feb47b 100%);,只有你按下「自定义」那一行才变成 linear-gradient(45deg, …)。
你想要的 自定义里该写
to top 0deg
to top right 45deg(默认值)
to right 90deg
to bottom right 135deg
to bottom 180deg
to bottom left 225deg
to left 270deg
to top left 315deg

注意 CSS 的角度是 0 度朝上、顺时针增长,所以默认 45° 指向右上角,和「对角线」按钮的 to bottom right 是相反的两个角。从「对角线」切到「自定义」又不碰滑条,渐变就翻到对角去了——粘贴过去发现「方向反了」通常就是这个。四个正交方向和关键字是严格等价的;四个对角不等价,也不可能等价:关键字让渐变线指向真实拐角(角度随盒子长宽比变),度数固定角度(非正方形盒子上就碰不到拐角)。要线必须过拐角就用关键字,要角度本身是设计决定就用度数。

「径向渐变」和「线性渐变」这两个按钮里,只有一个是干净的。 「径向」方向按钮和「径向渐变」类型按钮设的是同一个状态,而处于径向时点任意一个关键字方向会把类型翻回线性——这两条路都对。唯独「线性渐变」类型按钮不清理方向值:它改了函数名,state.dir 还留着 radial,于是拼接出

background: linear-gradient(radial, #ff7e5f, #feb47b 100%);

这不是合法 CSS,任何浏览器读到这里都会整条忽略。更难受的是页面看起来一切正常:给预览赋无效值是静默失败,所以上一次的径向渐变继续留在画面上,代码框安安静静躺着那行坏 CSS,你按「复制」它还会变 ✓ 已复制。再点一次「线性渐变」也救不回来(类型早就是 linear 了)。唯一的出路是点任意一个方向按钮。中文页和英文页这段逻辑逐字相同,所以两边都有这个问题。习惯上:收尾永远点方向按钮,不要点类型按钮,并且贴之前扫一眼代码里有没有那个不该出现的 radial。

代码框里永远是一整条声明:background: + 函数 + ;。位置写法也不对称——0% 的停止点只写颜色不带 %,大于 0 的都带,所以默认输出是 #ff7e5f, #feb47b 100% 而不是 0%, … 100%;把两个停止点都拖到 0,那行就成了 linear-gradient(to right, #ff7e5f, #feb47b),一个位置都没有。预览下面那行小字是去掉 background: 前缀的裸表达式,需要值而非声明时看它。复制按钮失败同样是空 .catch:不提示、不帮你选中文字,按钮上还是「复制」。

十二个预设模板——日落、海洋、极光、暮色、火焰、森林、霓虹、晨曦、薰衣草、草莓、冰雪、沙漠——全是两色的 to right 线性渐变。点一个会把你手工排好的停止点整个换掉;但它只换颜色,不换形状:每个预设自带一份 stops 数组和一份独立的 CSS 字符串,那份字符串只用来画模板条上的小色块。于是色块永远是左到右的线性条,而你的渐变还停在当前模式里。实测:径向模式下点「日落」得到 background: radial-gradient(circle, #ff7e5f, #feb47b 100%);——一个径向的日落,不是你点的那个样子;自定义 200° 下点「海洋」得到 background: linear-gradient(200deg, #2193b0, #6dd5ed);,角度原封不动。以代码框为准;想要预设原样,先按一个方向按钮。

哪个页面干哪件事

你手上有 用 要当心
一种记法要换成另一种 转换器 HSL 是取整后的显示值,定稿看 HEX
带 alpha 的设计稿颜色 转换器,先删末两位 8 位十六进制从低位倒着读
打错的十六进制 没有提示 #12g456 会变成一个近黑色并「成功」
从照片/Logo 抽品牌色 提取器 等距抽检可能整列跳过,小色块会消失
同一张图要稳定结果 提取器——平涂图稳定,照片每轮漂 1–2 中心点是随机掷的
要 7 个色却只给 4 个 提取器的正常行为 距离 24 合并 + 0.18 饱和度压制
一段能直接粘的 CSS 背景 渐变器 4 个停止点封顶、无 alpha、只有 circle、类型按钮能产出无效 CSS
透明渐变、conic 三个都不行 自己写

常见问题

我粘的 #FF0000ff 怎么变蓝了? 因为那个框是 parseInt(str, 16) 之后用 32 位移位取通道的,32 位之外的部分被折掉,留下的正是 0000ff。粘之前删掉末尾的 alpha 两位。

转换器是不是算错了?我的颜色是不是错了? 都不是。HSL 那格是颜色的取整显示,拿它反推回来可能一个通道偏 1 到 5。全空间实测 89.4% 的颜色会动,最大偏 5。

我能用滑块调出任何颜色吗? 不能。整数 HSL 位置覆盖 16,777,216 个颜色里的 2,037,576 个,12.1%。要精确就直接写 HEX。

页面为什么不告诉我输入非法? 它没有错误分支。每个框的处理函数在解析失败时直接返回,屏幕上保留上一个颜色。粘贴后什么都没变,就是被拒了——对照上面那张表看写法。

我 Logo 的品牌色为什么从来不进配色结果? 最可能是步长。1200 万像素的图只看不透明像素的每 2400 个之一,小色块很可能压根没被抽到。裁出你要取色的那块,或把图缩到几百像素宽,小颜色的存活率会高很多。

我选了 7 个颜色,它只给 4 个,是坏了吗? 不是。RGB 距离 24 以内的簇会合并,饱和度低于本图峰值 18% 的颜色会在截断前被丢掉(整图最大饱和度低于 0.05 时这一步跳过)。数量滑块是上限,不是保证。

那些百分比是占图片的面积吗? 不是。它们分摊的是存活簇认领到的像素数,而一个像素可以被多个簇同时认领。它们永远加起来 100%,而且只适合彼此比较。

我动了滑块又调回 7,配色为什么变了? K-means++ 的第二及以后的中心用 Math.random() 选。每次提取都是一次新的抽取,所以结果会漂;第一个中心钉在第一个不透明像素上,其余不是。

渐变能做透明到实色的淡出吗? 界面上不行,颜色输入不带 alpha,十六进制框拒绝 8 位值。复制出来把其中一个停止点改成 transparent 或 #RRGGBB00。

为什么加不出第五个停止点? 处理函数到 4 就返回,按钮同时被隐藏。这是页面里的硬上限,不是渲染问题。

我按了「线性渐变」,粘过去的 CSS 没反应,怎么回事? 你当时在径向模式。那个按钮只改函数名,方向值还是 radial,于是写出来的是 linear-gradient(radial, …),浏览器不认。预览继续画上一个径向渐变,所以唯一线索是 radial 出现在本该是方向的位置上。点任意方向按钮即可恢复,再点「线性渐变」没用。中文页英文页都有这个缺陷。

我点的预设和色块长得不一样? 预设只给颜色,不给形状。色块用预设自己写死的 to right 串画,生成器沿用当前的类型与角度。看代码框;想要色块那个样子,先按方向按钮。

这些工具会把我的图片传上去吗? 不会。提取器用 FileReader 读文件,在同一个标签页的 canvas 上分析,什么都不发出去。代价是内存而不是隐私:它运行期间一直握着全分辨率的 RGBA 缓冲,1200 万像素约 48 MB。

本指南用到的工具

广告