CSS中的混合算法
在CSS中前景是默认覆盖背景色的,例如,在父元素中设置背景色A,在子元素中设置背景色B,那么最终呈现的颜色永远是子类的背景色B,当然这是在没有设置opacity的前提下
如果设置了opacity,则会应用颜色混合算法。
根据CSS Color 4规范,正确的alpha合成公式为:
C_out = C_f * α_f + C_b * α_b * (1 - α_f)
α_out = α_f + α_b * (1 - α_f)
C_final = C_out / α_outTIP
CSS中的颜色都是sRGB空间下的,在应用混合算法时,需要首先将它转换为线性空间下,计算后再转换为sRGB空间
计算步骤:sRGB -> linear sRGB -> sRGB
半透明前景混合非透明背景
对于非透明背景(α_b = 1),公式简化为:
r = (foreground.r * alpha_f) + (background.r * (1.0 - alpha_f));
g = (foreground.g * alpha_f) + (background.g * (1.0 - alpha_f));
b = (foreground.b * alpha_f) + (background.b * (1.0 - alpha_f));- foreground指前景颜色
- background指背景颜色
- α_f指前景alpha值
- α_b指背景alpha值
<div class="bg-#ff0000 w-full h-100px">
<div class="w-full h-full bg-#00ff00 opacity-40 color-#ffffff"></div>
</div>计算步骤:
首先从sRGB转换为linear rgb空间下
- 父层颜色:红色 → #ff0000 = rgb(255, 0, 0)
- linear rgb: r-1 g-0 b-0
- 子层颜色:绿色 → #00ff00 = rgb(0, 255, 0)
- linear rgb: r-0 g-1 b-0
- 子层透明度:opacity = 0.4
在linear rgb空间中计算:
r = (0 × 0.4) + 1.0 × (1.0 - 0.4) = 0.6
g = (1.0 × 0.4) + 0 × (1.0 - 0.4) = 0.4
b = (0 × 0.4) + 0 × (1.0 - 0.4) = 0结果:rgb(153, 102, 0)
再转换为sRGB空间下:
r = 0.78
g = 0.641
b = 0结果:rgb(199, 163, 0)
TIP
为什么浏览器最终渲染出的颜色与计算出的背景色不一样,浏览器渲染出的颜色为#A77211,我们计算出的颜色为#C7A300?
这和计算公式本身无关,是显示/色彩管理链路的问题
半透明前景混合半透明背景
具体步骤:
计算输出颜色:
textr_out = (foreground.r * alpha_f) + (background.r * alpha_b * (1.0 - alpha_f)); g_out = (foreground.g * alpha_f) + (background.g * alpha_b * (1.0 - alpha_f)); b_out = (foreground.b * alpha_f) + (background.b * alpha_b * (1.0 - alpha_f));计算输出alpha:
textalpha_out = alpha_f + alpha_b * (1.0 - alpha_f)最终颜色:
textr_final = r_out / alpha_out g_final = g_out / alpha_out b_final = b_out / alpha_out
举例
<div class="w-full h-100px bg-#ffffff">
<div class="w-full h-full bg-#ff0000 opacity-40">
<div class="w-full h-full bg-#00ff00 opacity-20"></div>
</div>
</div>计算步骤:
第一层:红色(40%) + 白色背景(Linear RGB)
a. 转换到Linear RGB:
- 白色(255,255,255) → (1.0, 1.0, 1.0)
- 红色(255,0,0) → (1.0, 0.0, 0.0)
b. Linear RGB混合:
r1_linear = 1.0 × 0.4 + 1.0 × 0.6 = 1.0
g1_linear = 0.0 × 0.4 + 1.0 × 0.6 = 0.6
b1_linear = 0.0 × 0.4 + 1.0 × 0.6 = 0.6第二层:绿色(20%) + 第一层结果(Linear RGB)
a. 转换到Linear RGB:
- 第一层结果(1.0, 0.6, 0.6)
- 绿色(0,255,0) → (0.0, 1.0, 0.0)
b. Linear RGB混合:
r2_linear = 0.0 × 0.2 + 1.0 × 0.8 = 0.8
g2_linear = 1.0 × 0.2 + 0.6 × 0.8 = 0.68
b2_linear = 0.0 × 0.2 + 0.6 × 0.8 = 0.48c. 转回sRGB:
r2 = 1.055 × (0.8^(1/2.4)) - 0.055 ≈ 0.906 → 231
g2 = 1.055 × (0.68^(1/2.4)) - 0.055 ≈ 0.843 → 215
b2 = 1.055 × (0.48^(1/2.4)) - 0.055 ≈ 0.722 → 184结果:rgb(231, 215, 184) = #E7D7B8
这个结果 #E7D7B8 与您观察到的实际渲染结果 #EFB7AB 有一定差异,但这是正常的
CSS 的真实混合过程
浏览器中的CSS混合标准流程(符合 CSS Color 4 规范(color interpolation & compositing)):
"Interpolation and compositing are done in linear-light (unencoded) color space, not gamma-encoded sRGB space."
CSS 所有颜色插值、混合(如 opacity、mix-blend-mode、color-mix() 等)都必须在 linear-light 空间中进行。
取出 RGB 值(0–255)并转为 sRGB 归一化 [0–1]
应用 sRGB → LinearRGB 转换(伽马解码)
在 Linear 空间中做 alpha 混合
再转回 sRGB 空间显示
这两次 γ 转换(解码 + 编码)会让结果变亮。
1. 实算示例:#ff0000 背景 + #00ff00@0.4 前景
按照CSS Color 4规范在Linear RGB空间计算:
a. 转换为 0–1:
| 颜色 | R | G | B |
|---|---|---|---|
| 背景红 | 1.0 | 0 | 0 |
| 前景绿 | 0 | 1.0 | 0 |
b. 解码到 Linear RGB:
| 通道 | sRGB→Linear |
|---|---|
| 1.0 | 1.0 |
| 0 | 0 |
(0和1在sRGB→Linear转换中保持不变)
c. 在Linear空间混合(α=0.4):
d. 转回 sRGB:
e. 转换为8位值:
注意:实际浏览器渲染结果 #A77211 可能与理论计算有差异,这是由于:
- 渲染引擎的具体实现细节
- 抗锯齿算法的影响
- 显示器色彩配置文件
- 浏览器的优化策略
WARNING
- 内层颜色与外层颜色放置顺序不同,产生的最终效果也不同
背景色:bg-#ffffff
背景色:bg-#ff0000 opacity-40
<div class="w-full h-100px bg-#ffffff mt-20px">
<div class="w-full h-full bg-#ff0000 opacity-40"></div>
</div>canvas中混合流程
浏览器中canvas的标准混合流程Canvas 2D 规范
"The default color space of the 2D context is 'srgb'." "All drawing operations and compositing are performed in that color space, using 8-bit per channel precision."
也就是说:
- Canvas 默认工作在 sRGB 颜色空间;
- 混合直接在 gamma 编码的 sRGB 通道 上进行;
- 不做解码到线性空间的步骤;
- 结果保存在 sRGB(8-bit)帧缓冲中。
// 在页面上放两个相同的层结构然后读取中心像素
const getMixColor = () => {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
// 背景红
ctx.fillStyle = '#ff0000';
ctx.fillRect(0, 0, 1, 1);
// 前景半透明绿 (alpha=0.4)
ctx.fillStyle = 'rgba(0,255,0,0.4)';
ctx.fillRect(0, 0, 1, 1);
// 返回的是非预乘 (unpremultiplied) sRGBA 值(按规范,API 给开发者的是解 premult 后的常见表示)。(实现细节可能有差异,但这是规范行为)
// 如果你在 JS 中拿到 getImageData() 并在 CPU 上做合成,推荐把值转换为 线性空间并预乘 alpha 做进一步处理(特别是多步处理或插值),然后在最后编码回 sRGB 并写回 canvas
const p = ctx.getImageData(0, 0, 1, 1).data;
console.log('canvas pixel (r,g,b,a):', p[0], p[1], p[2], p[3]);
// 打印 hex
const hex =
'#' +
[p[0], p[1], p[2]].map((v) => v.toString(16).padStart(2, '0')).join('');
console.log('canvas hex:', hex);
};=> canvas pixel (r,g,b,a): 153 102 0 255
=> canvas hex: #996600
为什么 Canvas 会这样设计?
历史原因 + 性能考量:
- Canvas API 设计于 2005 年左右,那时没有统一的线性色彩规范;
- 早期 GPU 合成管线都在 8-bit gamma 空间中操作;
- 为了性能与兼容性,Canvas 沿用了 sRGB 通道混合;
- 直到近几年才通过 { colorSpace: 'linear-srgb' } 选项补上线性支持。
参考:
【1】CSS Color 4
【2】canvas

