常见颜色混合/复合算法(按类别组织)
1. alpha 复合(Compositing / Porter-Duff)
用途:最基础的“前景覆盖背景”逻辑,所有 UI 半透明和图层合成的基础。 核心思想:把前景(source)按其 alpha 覆盖在背景(destination)上,常见操作是 source-over(即 CSS/HTML 的默认)。
概念
src = (r_s,g_s,b_s,α_s),dst = (r_d,g_d,b_d,α_d)输出 alpha:
α_out = α_s + α_d*(1-α_s)输出颜色(非 premultiplied):
C_out = (C_s * α_s + C_d * α_d * (1 - α_s)) / α_out如果背景是完全不透明(α_d = 1)则简化为:
C_out = C_s * α_s + C_d * (1 - α_s)(这里 C 表示通道值,注意:应在线性光 (linear-light) 空间中进行)
Premultiplied alpha(重要)
涉及将颜色值与透明度值提前相乘,这样可以提高图像操作性能
用 premultiplied 表示:
C_s' = C_s * α_s、C_d' = C_d * α_d,这样合成为:C_out' = C_s' + C_d' * (1 - α_s) α_out = α_s + α_d*(1-α_s)避免边缘晕影(halo)、更稳定的数值,GPU 通常使用 premultiplied alpha。
- 合成公式中没有除法(除非最后要把预乘颜色还原为非预乘),数值更稳定,避免除以很小的 α 导致噪声。
简化并加速合成(compositing)运算:合成公式在预乘下变得更简单、更稳定(无除法或少除法)。
更适合 GPU 的硬件混合模式(硬件一般以 premultiplied 为优先/默认实现)。
转换
- 从非预乘到预乘
// srgb或linear都适用(注意:转换应在同一色彩空间下)
function premultiply([r, g, b, a]) {
return [r * a, g * a, b * a, a];
}- 从预乘到非预乘
function unpremultiply([r_p, g_p, b_p, a]) {
if (a === 0) return [0, 0, 0, 0];
return [r_p / a, g_p / a, b_p / a, a];
}WARNING
注意:当 a 非常小/0 时除法会造成数值不稳定或 NaN,需要做分支处理或 epsilon 限制。
2. 线性插值(LERP)与 gamma/linear 的差别
用途:最简单的“在两个颜色之间做插值”——常用于渐变、动画。
线性插值(在数值通道上):
C = C0 * (1 - t) + C1 * t关键:在 sRGB(gamma 编码)上直接 LERP 会产生视觉错误(看起来并非线性亮度变化)。正确方法是:
- sRGB → linear-light(解 gamma)
- 在 linear 空间 LERP
- linear → sRGB(编码)
更感知一致的方法:在感知空间(如 Oklab)中插值可得到更自然的色彩过渡。
3. 基本算术混合(Channel-wise operators)
这些是图像工具中最常见的图层混合模式(Photoshop、CSS mix-blend-mode 部分实现)。多数按每通道计算(通常在 linear-light 或 gamma 空间,取决于实现)。
Multiply(正片叠底)
C = C_a * C_b效果:更暗,类似滤镜把光减掉。物理上近似把透光率相乘。
Screen(滤色)
C = 1 - (1 - C_a) * (1 - C_b)效果:更亮,Multiply 的互补。
Overlay(叠加)
组合 multiply 和 screen:以背景亮度决定使用哪一式:
if C_b <= 0.5:
C = 2 * C_a * C_b
else:
C = 1 - 2 * (1 - C_a) * (1 - C_b)Darken / Lighten
Darken: C = min(C_a, C_b)
Lighten: C = max(C_a, C_b)Color Dodge / Color Burn
- Color Dodge:
C = C_b / (1 - C_a)(带阈值/夹紧) - Color Burn:
C = 1 - (1 - C_b) / C_a(需防除零) 效果非常强烈,用于高亮/阴影增强。
Difference / Exclusion
- Difference:
abs(C_a - C_b) - Exclusion:
C_a + C_b - 2*C_a*C_b(更柔和)
Additive(线性加法)
C = C_a + C_b常见于光叠加(stage lights、HDR 加法合成),通常需要裁剪或色域映射。
4. 几何/感知空间混合(HSL/HSV / Lab / Oklab)
为何? 在不同空间插值得到的感知效果差异巨大。
在 HSL/HSV 上插值
- 插值色相时要注意环绕(360° wrap-around)。
- 插值饱和度与亮度通常不能保证感知线性(会看到色相漂移或亮度不均)。
在 CIE Lab / Lch / Oklab 上插值
- 这些是“感知”或“接近感知线性”的空间,适合做颜色过渡/渐变。
- Oklab 被认为对人眼更均匀、更稳定(近年来推荐用于 UI 渐变与色彩插值)。
流程:sRGB -> linear -> RGB-> XYZ -> Lab/Oklab -> LERP -> 回转5. 光学/物理混合(Additive vs Subtractive vs Spectral)
- Additive(加色):RGB 屏幕、光源叠加是相加(或更准确是能量叠加,在线性光下加)。因此光学合成常用线性加法。
- Subtractive(减色):印刷/颜料混合(CMY)是减色,混合规则与 RGB 直接相乘或减法不同,通常用 Kubelka–Munk / CMYK 转换或模拟吸收谱。
- Spectral(光谱)渲染:最准确的颜色混合模式,逐波长计算光的相互作用,代价高但适用于科学/高保真渲染。
6. 色彩空间与 gamut mapping(色域映射)
当混合后的颜色落在目标设备不可显示区域时,必须做 gamut mapping。常见策略:
- 剪裁(clip to gamut)——简单但可能丢失细节/饱和度
- 压缩(perceptual mapping)——保持总体外观但改变全部分量
- 相对色度图(relative colorimetric)等 ICC 渠道策略
7. 数值实现细节(精度/premultiplied/8-bit vs float)
- Premultiplied alpha 推荐:避免边缘问题、便于 GPU 或快速合成。
- 浮点 vs 8-bit:在 8-bit 下每步量化会累积误差。对物理正确要求高的场景尽量使用浮点缓冲(WebGL / offscreen canvas / linear-srgb)。
- 混合顺序:不是可交换的,
A over B≠B over A。 - 颜色空间:务必明确是在 sRGB(gamma 编码)还是 linear-light 下计算。
8. GPU 固定功能混合与可编程混合
- GPU 中存在固定的硬件混合(blendfunc),例如 OpenGL 的
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)对应 premultipliedsource-over行为。 - 若需更复杂的 blend-modes(像 Photoshop 的 overlay/dodge),通常用片元着色器实现(或在现代管线用扩展支持)。
9. CSS 中的混合(实践与标准)
- CSS(Color 4)规定:
mix-blend-mode、opacity等复合在 linear-light 下执行(这就是与许多历史实现差异的根源)。 background-blend-mode,mix-blend-mode提供多种算术混合(multiply, screen, overlay, etc.)。
公式汇总(常用一览,假设通道值归一化到 [0,1])
Source-over(α_b = 1 简化):
C = C_s * α_s + C_b * (1 - α_s)LERP (t in [0,1]):
C = (1 - t) * C0 + t * C1Multiply:
C = C_a * C_bScreen:
C = 1 - (1 - C_a) * (1 - C_b) = C_a + C_b - C_a*C_bOverlay:
C = C_b <= 0.5 ? (2 * C_a * C_b) : (1 - 2*(1-C_a)*(1-C_b))Additive:
C = C_a + C_b (后续 clamp 到 [0,1] 或做 HDR tone mapping)Difference / Exclusion:
Difference: C = abs(C_a - C_b) Exclusion: C = C_a + C_b - 2*C_a*C_bColor interpolation in Oklab(流程):
sRGB -> linear -> to Oklab -> LERP -> from Oklab -> linear -> sRGB
实用建议(什么时候用哪种算法)
- UI/网页颜色合成(遵循规范):尽量遵守 CSS Color 4(线性光混合);在实现自定义混合时优先使用 linear-light + premultiplied alpha。
- 动画渐变 / 渐变色带:优先在 Oklab 或 linear-light 中插值以获得视觉上更匀的过渡。
- 图像特效(Photoshop 风格):使用 channel-wise blend modes(multiply, screen, overlay…),但注意执行空间(最好在 linear)。
- 光与亮度叠加(比如灯光、光斑):使用 additive(线性加法),并使用 HDR / tone-mapping 输出。
- 印刷/颜料混合:使用减色模型或谱方法,简单 RGB 模拟会很不真实。
- 高保真颜色处理:使用浮点缓冲、线性空间、并用色彩管理(ICC profile)做 gamut mapping。
常见陷阱(实战必看)
- 在 sRGB(编码)空间直接做 LERP 或 blend,会导致视觉误差(偏暗或偏亮)。
- 不使用 premultiplied alpha 容易导致边缘错误、采样伪影。
- 用 HSL 直接插值容易使色相突然跳动(环绕问题)。
- 8-bit 每步量化会累积误差;长链路合成优先用浮点。
- 不同浏览器/Canvas 的默认行为不同(Canvas 默认在 sRGB gamma 做混合;CSS 按 linear-light),所以视觉上可能出现不一致。
- 色域问题:超出显示色域的颜色需要做 gamut mapping,否则看起来会失真或被夹紧。
代码示例(关键实现片段)
Premultiplied source-over(线性空间)
// 输入通道都假设已线性化且在 [0,1]
function compositePremult(srcPremul, srcA, dstPremul, dstA) {
const outA = srcA + dstA * (1 - srcA);
return {
r: srcPremul.r + dstPremul.r * (1 - srcA),
g: srcPremul.g + dstPremul.g * (1 - srcA),
b: srcPremul.b + dstPremul.b * (1 - srcA),
a: outA,
};
}
// 如果需要非 premult 输出,除以 outA 得到非 premult colorMultiply(伪码,通道在 [0,1])
function multiply(a, b) {
return a * b;
}linear <-→ sRGB(精确)
function srgbToLinear(c) {
c = c / 255;
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}
function linearToSrgb(l) {
const c = l <= 0.0031308 ? l * 12.92 : 1.055 * Math.pow(l, 1 / 2.4) - 0.055;
return Math.round(Math.max(0, Math.min(1, c)) * 255);
}Oklab 插值(流程示意)
- 具体转换公式较长,但思路是:sRGB → linear → 3x3 矩阵→非线性 -> Oklab,插值后逆转回来。建议引用可靠实现或库(如
color.js/culori/tinycolor支持 Oklab)。
小结(一句话)
- 要物理/视觉正确就在线性光或感知空间混合,并使用 premultiplied alpha 与浮点计算;
- 要兼容历史实现或性能优先可使用 sRGB 混合与 8-bit,但会产生视觉偏差;
- 选择何种算法取决于目标(视觉一致、物理正确、性能、跨平台一致性)。
如果你想,我可以:
- 为你把上述若干算法(source-over, multiply, screen, overlay, additive, Oklab 插值)写成一个小 TypeScript 库和 demo 页,支持「选择色彩空间(sRGB/linear/Oklab)」、「是否使用 premultiplied」并能实时对比结果。 要我直接生成 demo/代码吗?

