色彩这个东西,做久了会发现它特别像做饭。盐放多少、火开多大,菜谱上写的“适量”“中火”对新手毫无意义,但对老师傅来说,脑子里有一套自己的刻度。hue、saturation、brightness 这三个词,就是颜色的那套刻度——色调决定你往哪个方向走,饱和度决定这味道浓不浓,亮度决定整盘菜是清淡还是重口。我接触这三个概念是从一次配色返工开始的:一套界面改了七版,主色被反复微调,每次都靠肉眼在取色器上瞎拖,直到有人提醒我“你把色相锁死,只动饱和度和亮度试试”,问题半小时就解决了。从那天起我才明白,RGB 是给机器看的,HSB 才是给人用的。
这篇内容想解决的是三件事:搞懂这三个参数各自管什么、怎么在代码和工具里精确控制它们、以及在实际项目里怎么用它们把配色从“凭感觉”变成“有章法”。适合刚入行的界面设计、前端开发、图像处理方向的朋友,也适合那些已经用了一两年取色器但一直没搞明白 V 和 L 到底差在哪的人。下面所有例子都可以直接抄,参数也是我自己项目里跑过的,不是理论值。
1. 先把三个词摆平:这套模型到底在描述什么
1.1 用调色盘的直觉理解三个参数
先说一个最容易被忽略的事实:HSB 不是一个“客观的物理量”,它是按人的感知习惯重新排列的一组坐标。物理上颜色是不同波长的光,但人眼和大脑并不这么处理,我们描述颜色时最自然的三句话是——“这是啥颜色”“这个颜色正不正”“这个颜色亮不亮”。HSB 就是这三句话的数学化。
**色调(Hue)**回答“是啥颜色”,用 0 到 360 度的角度表示,本质是你在色环上转到了哪个位置。0 度附近是红,60 度附近是黄,120 度附近是绿,180 度附近是青,240 度附近是蓝,300 度附近是品红,转回 360 度又回到红。这个环形的排列顺序不是随便定的,它对应的是可见光谱绕一圈后首尾相接的结果。记住这个顺序很重要,后面讲色相偏移做主题换肤时,全靠这个环的连续性。
**饱和度(Saturation)**回答“这个颜色正不正”,取值 0 到 100%。可以理解成往一罐纯色颜料里掺灰:0% 就是纯灰,100% 就是未经稀释的纯色。饱和度越低,颜色越“闷”、越收敛,越接近灰色的世界;饱和度越高,颜色越“跳”、越刺眼。日常口语里说的“高级灰”“莫兰迪色”“颜色有点脏”,绝大多数情况说的是饱和度偏低这件事。
**亮度(Brightness)**回答“这个颜色亮不亮”,取值 0 到 100%。它控制的是往颜色里掺黑还是掺白——更准确地说,是控制这个颜色反射出的光量。0% 是纯黑,100% 是这条色相线上能达到的最亮状态。这里有个必须提前说清的坑:亮度到 100% 不等于变成白色。白色是亮度和饱和度同时作用的结果——饱和度必须是 0,亮度拉满,才是白。如果饱和度保持 100%,亮度拉到 100%,你得到的是那条色相线上最鲜艳的纯色,不是白。这个误解我在新人身上见过太多次。
三者合起来,就是一套圆柱坐标:角度是色调,半径是饱和度,高度是亮度。这个几何图像在心里建立起来之后,你在取色器上拖鼠标就不再是碰运气,而是在一个三维空间里做定向移动。
1.2 HSB、HSV、HSL 这三个名字到底是不是一回事
搜这三个词,会看到一堆互相矛盾的说法,这里给一个能直接用的结论:HSB 和 HSV 是同一个东西的两种叫法,B 和 V 都指 Brightness 或 Value;HSL 是另一套模型,和它们不一样。
区别就在第三个参数。HSV 里的 V 定义是“这个颜色的最大分量在 255 中占多少”,所以纯色(比如纯红 R=255, G=0, B=0)的 V 是 100%,白色(R=G=B=255)的 V 也是 100%。也就是说,V 拉满时,从纯色到白色这一整条路都叫 V=100%,靠饱和度来区分。
HSL 里的 L(Lightness)则是“在纯黑和纯白之间居中”,纯色的 L 固定是 50%。要得到白色,L 必须拉到 100%,此时饱和度归零。要得到黑色,L 压到 0%。
| 模型 | 第三参数含义 | 纯红色的值 | 纯白色的值 | 纯黑色的值 |
|---|---|---|---|---|
| HSB / HSV | 最大分量占比 | H=0, S=100%, V=100% | H=0, S=0%, V=100% | S 任意, V=0% |
| HSL | 黑白之间的中点位置 | H=0, S=100%, L=50% | S=0%, L=100% | S 任意, L=0% |
这个差异带来的直接影响是:在 HSL 里想做一个“浅色调”,你调的是 L;在 HSB 里想做一个浅色调,你调的往往是 S。很多前端同学在 CSS 里用hsl()生成浅色背景,习惯性地把 L 拉到 90%,结果发现颜色发灰、发脏,就是因为 L 高了之后模型会自动往白里掺,色彩的“色感”被冲淡了。而在 HSB 体系里做同样的事,通常是保持 V 在 90% 上下,把 S 从 100% 压到 20%–30%,得到的是干净的浅粉、浅蓝,不会发灰。
一句话总结怎么选:做视觉判断、调色、看效果,用 HSB;写代码里做颜色变换,看你的库给的什么接口,别混用两套的心算逻辑。这是后面所有实操的前提。
1.3 为什么设计端偏爱 HSB,工程端却常给 HSL
这不是谁对谁错的问题,是两边的工作目标不同。
设计端调色的动作本质是“在保持色感的前提下微调”。你在做一套主色为蓝的品牌色时,按钮的 hover 态、禁用态、边框线,都希望是同一个“蓝的感觉”,只是深浅不同。在 HSB 里这非常自然:H 锁死在 210 度,S 从 100% 降到 85%、V 从 95% 降到 80%,得到的所有颜色都还是那个蓝,用户看不出换了色系。而在 HSL 里做同样的事,L 一动,颜色的“色相感”会被明显削弱,稍微过头就变成了另一个颜色。
工程端更常用 HSL,原因很朴素:它的数学形式更接近 RGB,在 CSS 里直接支持hsl()和hwb(),网页上写起来不用换算;而且 HSL 在做“整体提亮/压暗”这类粗暴操作时行为更可预测,不会出现 HSB 那种“V 拉满但颜色没变亮”的意外。
我的做法是两套都用,但分工明确:设计稿和色彩决策阶段用 HSB 思考,落代码时如果只能用 HSL,就先把 HSB 的意图转成 HSL 的具体值,而不是在 HSL 里重新凭感觉调。转换不是心算的,用工具或者几行代码跑一下,下面第 2 章会给转换的算法。
2. 数值背后:三要素的几何与感知规律
2.1 色相环那 360 度是怎么切出来的
很多人以为色相是个连续光谱,随便切多少份都行,其实不是。色相环的六段主色(红、黄、绿、青、蓝、品红)之间的距离不是均匀的,人眼对黄绿区域的敏感度远高于蓝紫区域。这就是为什么很多配色工具在色环上标刻度时会标出“感知不均匀”这件事。
在具体数值上,业内常见的做法是把色环切成六个 60 度的区间,每个区间对应两个相邻主色之间的过渡。这个划分方式来自 RGB 立方体的几何结构——把 RGB 立方体沿对角线(黑到白的那条轴)投影到平面上,六个顶点正好落在圆周上,间隔就是 60 度。所以你在代码里看到的那些色相计算公式,全都是围绕 60 度这个常数在转。
实际用的时候,记住几个关键角度就够了:0/360 红、30 橙、60 黄、120 绿、180 青、240 蓝、270 靛紫、300 品红。这八个点足够覆盖日常配色 90% 的需求。想做近似色的微调,在 ±15 度以内动;想做对比色,加 180 度取反;想做三角配色,间隔 120 度取三个点。这些都是死规则,不涉及审美,先把位置找准再谈好不好看。
2.2 饱和度:为什么 80% 往往比 100% 更耐看
我刚做界面时有个执念,觉得颜色越纯越“正”,所有主色都拉到 S=100%。结果上线后被反馈“看着累”“像十年前的设计”。后来才明白,饱和度拉满意味着这个颜色在屏幕上几乎是纯光谱输出,眼睛长时间盯着会疲劳。
这里有个经验区间可以直接用:大面积背景和长时间停留的界面,饱和度控制在 15%–45%;需要引起注意的按钮和强调元素,55%–85%;需要极度突出的告警或瞬时提示,可以到 90%–100%,但面积一定要小,最好不超过屏幕的 5%。
这套区间的逻辑是“色彩面积和饱和度成反比”。一块 5% 面积的高饱和色是视觉焦点,一块 80% 面积的高饱和色是视觉暴力。你去看那些成熟产品的界面,主按钮的颜色往往是 60%–70% 饱和度的品牌色,而不是 100%。原因很简单,按钮背后是大面积的浅色背景,主色如果满饱和,对比过度,反而显得廉价。
还有一个实用判断法:把一张设计稿缩小到 30%,如果高饱和区域看起来像一块块突兀的色斑,说明饱和度整体偏高了。这个方法比盯着原图看准得多,因为缩小后细节被过滤,只剩下大块的色彩关系。
2.3 亮度和明度:V 和 L 的换算要过一道手
前面说过 HSL 的 L 和 HSB 的 V 不是一回事,这里给出换算关系,方便你在两套体系之间迁移。
对于同一组 RGB 值,设最大分量为 max、最小分量为 min(都归一化到 0–1):
- HSB 的 V = max
- HSB 的 S = (max - min) / max(max 为 0 时 S 记为 0)
- HSL 的 L = (max + min) / 2
- HSL 的 S = (max - min) / (1 - |2L - 1|)
举个具体的例子,颜色 RGB(255, 128, 0),也就是橙色:
- max = 255/255 = 1.0,min = 0/255 = 0
- HSB:V = 1.0 即 100%,S = (1.0 - 0) / 1.0 = 100%
- HSL:L = (1.0 + 0) / 2 = 0.5 即 50%,S = (1.0 - 0) / (1 - |1 - 1|) = 100%
这个例子刚好两套都是 S=100%,看不出差别。换一个,RGB(180, 120, 120) 这种偏暗的粉红:
- max = 180/255 ≈ 0.706,min = 120/255 ≈ 0.471
- HSB:V ≈ 70.6%,S = (0.706-0.471)/0.706 ≈ 33.3%
- HSL:L = (0.706+0.471)/2 ≈ 58.8%,S = (0.706-0.471)/(1-|1.176-1|) = 0.235/0.824 ≈ 28.5%
同一颜色,HSB 说“饱和度 33%、亮度 71%”,HSL 说“饱和度 28%、亮度 59%”。如果你拿 HSB 的数值直接填进hsl(),出来的颜色会明显偏暗、偏灰。这就是混用两套体系最典型的翻车现场。
2.4 手算一遍:从 RGB 到 HSB 的完整流程
知道公式之后,最好手算一次,把直觉建立起来。以 RGB(200, 60, 100) 为例,这是一个偏玫红的颜色。
第一步,归一化并找极值。R=200/255≈0.784,G=60/255≈0.235,B=100/255≈0.392。max = 0.784(R),min = 0.235(G),delta = 0.549。
第二步,算亮度。V = max = 0.784,也就是 78.4%。
第三步,算饱和度。S = delta / max = 0.549 / 0.784 ≈ 0.700,即 70%。
第四步,算色相。因为 max 是 R,用公式 H = 60 × ((G - B) / delta mod 6)。代入:(0.235 - 0.392) / 0.549 ≈ -0.286,mod 6 后约 5.714,乘以 60 得 342.9 度。342.9 度落在 300 到 360 之间,靠近品红往红的过渡区,符合“偏玫红”的直觉。
这四步手算一次之后就明白了:色相的计算最麻烦,也是唯一需要分段讨论的,所以实际项目中不要手算,交给函数。但要记住一点:色相的计算依赖于“哪个通道最大”,一旦某个颜色接近灰色(delta 很小),色相会变得极不稳定——两三个灰阶的差别就可能让色相从 0 度跳到 200 度。这就是为什么给低饱和度的颜色做色相偏移时,效果经常莫名其妙,因为它的色相本身就没有明确意义。
3. 落地实操:把三要素用进真实工作流
3.1 用固定色相搭一套可扩展的色板
这套方法我在三个项目里用过,稳定可靠。核心思路是:主色的色相锁死,用饱和度和亮度做二维阶梯,生成整套色板。
假设品牌主色定为 H=212 度(一个偏冷的蓝),先确定主色的 S 和 V。我一般从 S=82%、V=88% 起步,这个组合出来的蓝既有存在感,又不刺眼。然后按下面的规则生成:
| 用途 | 色相 H | 饱和度 S | 亮度 V | 说明 |
|---|---|---|---|---|
| 深色按下态 | 212 | 88% | 62% | 只压 V,保持色感 |
| 主色 | 212 | 82% | 88% | 基准 |
| 悬停态 | 212 | 76% | 94% | 提 V 降 S,变亮但不发白 |
| 浅色背景 | 212 | 22% | 97% | 大幅降 S,得到干净的浅蓝 |
| 边框线 | 212 | 35% | 78% | 中等偏低的 S,避免线条扎眼 |
| 文字禁用态 | 212 | 12% | 55% | 低饱和低亮度,读得出但不抢戏 |
关键在于同一行里只动一个参数。做深浅变化时动 V,做“浓淡”变化时动 S,两者不要同时大幅改。这样出来的色板,无论怎么组合,视觉上都属于同一个色系,不会出现“这个蓝和那个蓝不是一家人”的情况。
这套色板还有个好处:扩展性极强。项目后期要加深色模式,直接把 V 的整体区间从 70–97 映射到 12–40,H 和 S 基本不动,整站就能平滑过渡,不会出现深色模式下颜色全变味的问题。这个技巧比重新配一套深色板省事太多。
3.2 代码里的三种典型写法
场景一:前端 CSS 里做主题色阶。现代 CSS 支持hsl(),虽然模型不同,但用来写阶梯足够。写法上建议用 CSS 变量把 H、S、L 拆开,方便整体换肤:
:root { --brand-h: 212; --brand-s: 78%; --brand-l: 52%; --brand-base: hsl(var(--brand-h) var(--brand-s) var(--brand-l)); --brand-hover: hsl(var(--brand-h) var(--brand-s) calc(var(--brand-l) + 6%)); --brand-active: hsl(var(--brand-h) var(--brand-s) calc(var(--brand-l) - 8%)); --brand-soft: hsl(var(--brand-h) 24% 96%); --brand-line: hsl(var(--brand-h) 36% 86%); } .button { background: var(--brand-base); border: 1px solid var(--brand-line); } .button:hover { background: var(--brand-hover); } .button:active { background: var(--brand-active); }只改变量--brand-h一个值,整套按钮、边框、背景会一起变色,主题切换就是这么做的。注意 hover 用的是calc()加 L,而不是改 S——因为在 HSL 里改 L 的行为更稳定,不会出现色相偏移的意外。
场景二:Python 里做颜色计算。标准库colorsys直接提供rgb_to_hsv和hsv_to_rgb,注意它的输入输出都是 0–1 的浮点数,不是 0–255:
import colorsys def hex_to_hsb(hex_color): hex_color = hex_color.lstrip('#') r, g, b = (int(hex_color[i:i+2], 16) / 255 for i in (0, 2, 4)) h, s, v = colorsys.rgb_to_hsv(r, g, b) return round(h * 360, 1), round(s * 100, 1), round(v * 100, 1) def hsb_to_hex(h, s, v): r, g, b = colorsys.hsv_to_rgb(h / 360, s / 100, v / 100) return '#{:02X}{:02X}{:02X}'.format( round(r * 255), round(g * 255), round(b * 255) ) print(hex_to_hsb('#C83C64')) # (342.9, 70.0, 78.4) print(hsb_to_hex(342.9, 70, 78.4)) # #C83C64这段代码我经常用来批量检查一套色板是不是“同一家族”——把所有颜色转成 HSB,看 H 值的散布范围,超过 ±20 度就说明色相不统一,需要收敛。
场景三:图像处理里按 HSB 做区域筛选。多数计算机视觉库提供的 HSV 接口有个反直觉的设定:H 通道的量程被压到了 0–179,不是 0–360,因为常用的 8 位无符号整型存不下 360 这个值,所以统一做了折半处理;S 和 V 则是 0–255。这意味着你写阈值时,色相要除以 2。要筛出红色的物体,参数大概是 H 在 0–10 和 170–179 两段(红色在环上跨了 0 度)、S 在 80–255、V 在 70–255。红色被拆成两段是必须处理的细节,很多人卡在这里,以为阈值写一段就够了,结果只能抓到一半的红。
3.3 单通道调色:只动一个参数的效果差异
拿一张普通的产品照片做实验,只改一个参数,效果差别非常明显,这也是理解三要素最快的路径。
只动色相。把 H 整体偏移 +40 度,蓝的衣服变紫,绿的背景变黄,整张图像换了滤镜。这个操作适合做“一键换季配色”,但要注意红色区域跨越 0 度时会出现断层——直接对 H 加上偏移量,0 度附近的部分会绕回 360 度,产生一条明显的色带。正确做法是转成极坐标做环形插值,或者对近红区域单独处理。这个问题在纯色块上不容易发现,在渐变和照片上特别明显。
只动饱和度。S 统一压到 30%,整张图立刻变成“低饱和胶片风”。这个操作对画面结构的破坏最小,因为所有元素的相对明暗关系没变,只是色彩浓度降低了。想要“高级感”又不想动构图,压饱和度是最安全的一步。压到 0% 就是纯黑白,但要注意纯黑白和“压到 10%”看起来完全是两回事,后者还留有一点点色偏,观感更舒服。
只动亮度。V 整体提 15%,画面变亮但对比度降低,暗部细节会被提起来,同时高光容易溢出。这个操作要小心,因为 V 是逐像素的,暗部提亮后噪点也会一起提亮,噪点在低亮度区域占比大,一提就暴露。做这一步之前先做降噪,或者用曲线而不是线性拉伸。
我个人的习惯顺序是:先定色相方向,再定饱和度浓淡,最后用亮度找层次。这个顺序的原因是色相变化对画面结构的破坏最大,先定它最省返工;亮度放最后,因为它最容易被反复微调。
3.4 图表和可视化里的配色规则
数据可视化最容易犯的错是给每个类别随便挑一个高饱和颜色,结果十根柱子十种颜色,像打翻的调色盘。用 HSB 来管这件事就很有条理。
分类数据(无顺序关系的类别):固定 S 和 V,只按等角度间隔取 H。比如五类数据,H 取 0、72、144、216、288,S=72%、V=85%。这样所有颜色的“浓淡”一致,视觉权重相同,不会让某一类看起来更重要。
有序数据(从低到高的连续量):固定 H,只沿 V 做阶梯,S 可以略微配合变化。比如五个等级,V 取 40%、55%、70%、85%、97%,S 取 90%、85%、78%、68%、55%。这个组合出来的效果是从深到浅同时略微变淡,符合人对“量级递增”的直觉。很多默认色板是反的——高值用深色,低值用浅色,这在地图类图表里还行,但在柱状图里会让人误以为深的那根更重要。
发散数据(围绕中间值的正负分布):用两端色相 + 中间低饱和。比如负值用 H=210 的蓝,正值用 H=25 的橙,中间值 S 压到 8%、V 拉到 96%,接近白。这个做法比“深红到深绿”那种默认方案友好得多,因为红绿对色觉障碍人群不友好,蓝橙组合的辨识度明显更高。
4. 踩过的坑和排查思路
4.1 常见问题速查表
| 现象 | 最可能的原因 | 排查与解决 |
|---|---|---|
| 浅色背景发灰发脏 | 在 HSL 里拉高 L 生成浅色 | 改用 HSB 思路,保持 V 在 90% 上下、压低 S 到 20%–30% |
| 深色态颜色变味 | 同时大改 S 和 V | 深色态优先压 V,S 只做小幅调整(±10% 内) |
| 红色区域出现色带断层 | 色相偏移未处理 0/360 环绕 | 用环形插值,或对 H<20 与 H>340 单独处理 |
| 低饱和色的色相乱跳 | delta 太小,色相计算不稳定 | 低饱和区域不要用色相做判断,改用亮度分层 |
| 图像筛选红色只抓到一半 | 色相阈值只写了一段 | 红色跨 0 度,需要 0–10 与 170–179 两段 |
| 代码里 HSV 阈值全不生效 | 色相量程是 0–179 而非 0–360 | 把设计值除以 2 再填进去 |
| 深浅模式的对比度不达标 | 用 HSV 的 V 直接算对比度 | 对比度要用线性化的相对亮度,不能直接用 V |
| 主题换色后阴影变怪 | 阴影色写死了 RGB | 阴影用同色相的低 S 低 V 值,跟随主题变量 |
表里第 7 条值得单独展开。HSB 的亮度 V 不能直接用来算对比度。原因是对比度标准用的是“相对亮度”,需要先把 RGB 值做伽马校正的线性化,再加权求和(红色权重约 0.2126、绿色 0.7152、蓝色 0.0722)。而 HSV 的 V 只是三个通道里的最大值,是一个纯粹的几何量,和感知亮度不是一回事。举个极端例子:纯黄 RGB(255,255,0) 的 V 是 100%,纯蓝 RGB(0,0,255) 的 V 也是 100%,但两者的相对亮度差了近十倍。如果你按 V 来判断黑白文字该配在哪一边,一定会出错。
4.2 几条不太会写进文档的经验
第一,色板不要一次做十几个色阶。我最早给项目做了 10 个色阶,从 50 到 900,看着很专业,实际用起来只用到 4 个——主色、hover、浅背景、边框。剩下六个没人记得住,还增加了维护成本。现在我的做法是先做 4 个,用着不够再加,加的时候也只在现有的两个之间插值,不重新设计。
第二,判断两个颜色是不是“一个家族”,看 H 的差值,不要看肉眼。肉眼看久了会失准,尤其在低饱和区域。把两个颜色转成 HSB,H 差在 15 度以内基本安全,20 到 40 度要看具体场景,超过 60 度基本就是两个色系了。这个量化标准在多人协作时特别有用,能避免来回扯皮。
第三,饱和度整体偏高时,先降 S 再考虑改色相。很多“配色不好看”的问题,本质是饱和度过高,而不是色相选错。把所有颜色的 S 统一乘 0.75,往往立刻顺眼。这个操作成本极低,值得作为排查的第一步。
第四,做图像调色时,把三个通道分开看。单独把 S 通道导出来看,你会发现画面的“色彩丰富度”分布一目了然;单独看 V 通道,等于看一张灰度图,构图和层次全在里面。养成这个习惯之后,很多“为什么这张图不够通透”的问题能直接定位到是暗部堆在一起还是高光缺失。
第五,别在 8 位色深里做反复的 HSB 往返转换。每次转换都要取整,来回几次之后误差会累积,尤其在低饱和的暗色区域,一次往返可能就丢掉一两个色阶的差别,多次之后会出现明显的色块。需要反复调的时候,中间结果保留浮点,最后一步再取整。
我自己现在的做法是,任何一套新配色,先在 HSB 空间里把 H、S、V 三个值写成一条明确的规则,比如“H 固定 212,S 取主色的 0.3/0.6/1.0 三档,V 取 0.55/0.8/0.95 三档”,然后直接让代码生成。这样出来的色板有内在逻辑,后续无论是扩展深色模式还是做无障碍对比度校正,都能在这套规则上继续推导,而不是每改一个颜色就重新凭感觉调一次。配色从手艺变成规则,是我这几年在色彩这件事上最大的收获。