仅作用于Alpha通道的SVG滤镜为何会同时修改RGB通道?
这问题我刚看到的时候也懵了——明明只动了Alpha通道的映射,怎么RGB跟着变了?而且还跟Alpha值非线性相关,浏览器表现还略有差异,这背后确实有容易被忽略的渲染细节。
核心原因:预乘Alpha(Premultiplied Alpha)的隐形影响
你可能没意识到,浏览器在处理带透明度的颜色时,默认会用预乘Alpha格式存储和计算。简单说,就是RGB通道的值已经和Alpha值提前相乘过了:
预乘后R = 原始R × Alpha 预乘后G = 原始G × Alpha 预乘后B = 原始B × Alpha
比如你写background: rgb(from var(--c) r g b/ var(--a))时,浏览器已经把--c的原始RGB值和--a(比如0.07)相乘,得到了低亮度的预乘RGB值,再加上Alpha值存储起来。
当你用滤镜修改Alpha时,到底发生了什么?
你的滤镜通过tableValues='1.25 0'把Alpha区间[0,1]映射成[1.25,0](超出1的部分会被强制截断到1),但关键是:滤镜是在预乘Alpha的颜色空间上操作的。
当你把Alpha从0.07拉到1的时候,浏览器不会自动“反推”原始RGB值——它只会保持当前预乘后的RGB不变,直接把Alpha设置为1。这就导致最终显示的RGB是预乘后的低亮度值(原始RGB×原Alpha),而不是你期待的原始--c颜色。
举几个直观的例子:
- 当
--a=0.01时,预乘后的RGB几乎是0(原始RGB×0.01),把Alpha拉到1后,显示的就是接近黑色的颜色; - 当
--a=0.2时,预乘后的RGB是原始RGB×0.2,Alpha拉到1后,就接近原始蓝色但亮度明显偏低; - 当
--a=0.05时,预乘RGB是原始×0.05,比0.01亮一些,但还是远低于原始颜色。
为什么RGB的变化是非线性的?
这和你的滤镜Alpha映射曲线+预乘转非预乘的双重计算有关:
你的feFuncA是线性映射:输入Alpha从0到1,输出Alpha从1.25降到0。但输出Alpha会被截断在[0,1]区间内:
- 当输入Alpha ≤ 0.2时,输出Alpha=1.25*(1-输入Alpha) ≥1,被截断为1,此时显示的RGB就是预乘RGB(原始×输入Alpha),输入Alpha越小,颜色越暗;
- 当输入Alpha>0.2时,输出Alpha=1.25*(1-输入Alpha) <1,此时浏览器会把预乘RGB除以输出Alpha,得到显示用的非预乘RGB:
显示RGB = (原始RGB×输入Alpha) / 输出Alpha。
比如输入Alpha=0.3时,输出Alpha=1.25*(1-0.3)=0.875,显示RGB=原始RGB×0.3/0.875≈原始×0.3429,这和输入Alpha的线性关系完全不同,视觉上就体现为非线性的颜色变化。
为什么Chrome和Firefox表现略有差异?
两大浏览器对滤镜颜色空间的处理细节、预乘Alpha的截断时机有细微差别:
- Chrome在设置
color-interpolation-filters='sRGB'后,会切换到非预乘的sRGB空间处理滤镜,所以当你用filter: opacity(var(--a)) url(#extract)时,opacity()滤镜会先保留原始RGB,再设置Alpha,后续滤镜修改Alpha时就能保持原始RGB不变; - Firefox对
color-interpolation-filters的兼容逻辑和Chrome不同,即使设置了sRGB,它可能还是会在预乘空间处理后续滤镜,所以结果和直接在背景上设Alpha的情况一致。
怎么解决这个问题,恢复原始RGB?
要拿到原始RGB,你需要在滤镜中把预乘后的RGB“还原”——也就是乘以1/原Alpha,结合CSS动态计算这个值再传给滤镜就行:
步骤1:用CSS计算Alpha的倒数
div { --a: 0.07; --inv-a: calc(1 / var(--a)); /* 计算1/原Alpha,用于还原RGB */ background: rgb(from var(--c) r g b/ var(--a)); }
步骤2:用feColorMatrix同步修改RGB和Alpha
把原来的feComponentTransfer换成feColorMatrix,同时完成RGB还原和Alpha映射:
<svg height='0'> <filter id='extract' x='0' y='0' width='1' height='1' color-interpolation-filters='sRGB'> <feColorMatrix type='matrix' values=' var(--inv-a) 0 0 0 0 0 var(--inv-a) 0 0 0 0 0 var(--inv-a) 0 0 0 0 0 -1.25 1.25 '/> </filter> </svg>
- 前三行的
var(--inv-a):把预乘RGB乘以1/原Alpha,还原成原始RGB; - 最后一行:实现你需要的Alpha映射
输出Alpha=1.25 -1.25×输入Alpha。
这样修改后,无论你调整--a到什么值,都能得到原始的--c颜色了。
最后总结
这个坑的本质是预乘Alpha在浏览器渲染中的隐形存在——平时我们用CSS写颜色时感知不到,但一旦涉及滤镜、Canvas等底层颜色操作,它就会跳出来搞事情。浏览器的差异则是因为不同厂商对渲染标准的细节实现略有不同,但核心逻辑是一致的。




