如何仅使用CSS滤镜将黑色转换为任何给定的颜色

我的问题是:给定目标RGB颜色,仅使用CSS过滤器将黑色(#000)重新着色为该颜色的公式是什么?

对于被接受的答案,它需要提供一个函数(任何语言),该函数将接受目标颜色作为参数,并返回相应的CSSfilter字符串。

其上下文是需要对background-image内的SVG重新着色。在这种情况下,它将支持KATEX中的某些TeX数学功能:https://github.com/Khan/KaTeX/issues/587

例子

如果目标颜色#ffff00(黄色),则一个正确的解决方案是:

filter: invert(100%) sepia() saturate(10000%) hue-rotate(0deg)

演示

非目标

  • 动画。
  • 非CSS过滤器解决方案。
  • 从黑色以外的颜色开始。
  • 关心黑色以外的颜色会发生什么。

目前为止的结果

您仍然可以通过提交非强力解决方案来获得已接受答案!

资源

  • 如何计算hue-rotatesepia: https://stackoverflow.com/a/29521147/181228. Ruby实现示例:

    LUM_R = 0.2126; LUM_G = 0.7152; LUM_B = 0.0722
    HUE_R = 0.1430; HUE_G = 0.1400; HUE_B = 0.2830
    
    
    def clamp(num)
    [0, [255, num].min].max.round
    end
    
    
    def hue_rotate(r, g, b, angle)
    angle = (angle % 360 + 360) % 360
    cos = Math.cos(angle * Math::PI / 180)
    sin = Math.sin(angle * Math::PI / 180)
    [clamp(
    r * ( LUM_R  +  (1 - LUM_R) * cos  -  LUM_R * sin       ) +
    g * ( LUM_G  -  LUM_G * cos        -  LUM_G * sin       ) +
    b * ( LUM_B  -  LUM_B * cos        +  (1 - LUM_B) * sin )),
    clamp(
    r * ( LUM_R  -  LUM_R * cos        +  HUE_R * sin       ) +
    g * ( LUM_G  +  (1 - LUM_G) * cos  +  HUE_G * sin       ) +
    b * ( LUM_B  -  LUM_B * cos        -  HUE_B * sin       )),
    clamp(
    r * ( LUM_R  -  LUM_R * cos        -  (1 - LUM_R) * sin ) +
    g * ( LUM_G  -  LUM_G * cos        +  LUM_G * sin       ) +
    b * ( LUM_B  +  (1 - LUM_B) * cos  +  LUM_B * sin       ))]
    end
    
    
    def sepia(r, g, b)
    [r * 0.393 + g * 0.769 + b * 0.189,
    r * 0.349 + g * 0.686 + b * 0.168,
    r * 0.272 + g * 0.534 + b * 0.131]
    end
    

    注意,上述clamp使得hue-rotate为非线性函数。

    浏览器实现:火狐

  • 演示:从灰度颜色到非灰度颜色: https://stackoverflow.com/a/25524145/181228

  • 几乎起作用的
  • 公式(来自类似的问题):
    https://stackoverflow.com/a/29958459/181228

    详细解释为什么上面的公式是错误的(CSShue-rotate不是真正的色调旋转而是线性近似):
    https://stackoverflow.com/a/19325417/2441511

74245 次浏览

注意:OP要求我取消删除,但奖金将归Dave的答案所有。


我知道这不是问题正文中所问的内容,当然也不是我们一直在等待的内容,但有一个CSS过滤器可以做到这一点: drop-shadow()

注意事项:

  • 阴影绘制在现有内容的后面。这意味着我们必须做一些绝对的定位技巧。
  • 所有的像素都将被同等对待,但是op说[我们不应该]";关心黑色以外的颜色会发生什么变化。";
  • 浏览器支持。(我不确定,只在最新的FF和Chrome下测试过)。

/* the container used to hide the original bg */


.icon {
width: 60px;
height: 60px;
overflow: hidden;
}




/* the content */


.icon.green>span {
-webkit-filter: drop-shadow(60px 0px green);
filter: drop-shadow(60px 0px green);
}


.icon.red>span {
-webkit-filter: drop-shadow(60px 0px red);
filter: drop-shadow(60px 0px red);
}


.icon>span {
-webkit-filter: drop-shadow(60px 0px black);
filter: drop-shadow(60px 0px black);
background-position: -100% 0;
margin-left: -60px;
display: block;
width: 61px; /* +1px for chrome bug...*/
height: 60px;
background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgOTAgOTAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDkwIDkwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48Zz48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTYxLjUxMSwyNi4xNWMtMC43MTQtMS43MzgtMS43MjMtMy4yOTgtMy4wMjYtNC42NzkgICBjLTEuMzAzLTEuMzY2LTIuODA5LTIuNDUyLTQuNTE1LTMuMjU5Yy0xLjc1NC0wLjgyMi0zLjYwMS0xLjI4OC01LjU0LTEuMzk2Yy0wLjI4LTAuMDMxLTAuNTUyLTAuMDQ3LTAuODE0LTAuMDQ3ICAgYy0wLjAxOCwwLTAuMDMxLDAtMC4wNDcsMGMtMC4zMjcsMC4wMTYtMC41NzQsMC4wMjMtMC43NDUsMC4wMjNjLTEuOTcxLDAuMTA4LTMuODQxLDAuNTc0LTUuNjA5LDEuMzk3ICAgYy0xLjcwOCwwLjgwNy0zLjIxMiwxLjg5My00LjUxNywzLjI1OWMtMS4zMTgsMS4zODEtMi4zMjcsMi45NDgtMy4wMjYsNC43MDJ2LTAuMDIzYy0wLjc0NCwxLjgxNS0xLjExOCwzLjcxNi0xLjExOCw1LjcwMiAgIGMtMC4wMTUsMi4wNjQsMC41MzcsNC4xODIsMS42NTQsNi4zNTVjMC41NzQsMS4xMzMsMS4yOTUsMi4yNSwyLjE2NCwzLjM1MmMwLjQ4MiwwLjYwNSwxLjAwMiwxLjIxLDEuNTYsMS44MTYgICBjMC4wMzEsMC4wMTYsMC4wNTUsMC4wMzksMC4wNzEsMC4wN2MwLjUyNywwLjQ5NiwwLjg5MiwwLjk3OCwxLjA5MywxLjQ0M2MwLjEwOCwwLjIzMywwLjE3OSwwLjUyLDAuMjEsMC44NjIgICBjMC4wNDYsMC4zNzEsMC4wNjksMC44MjIsMC4wNjksMS4zNXYxLjA0OGMwLDAuNjIsMC4xMTcsMS4yMTgsMC4zNDksMS43OTJjMC4yMzQsMC41NDMsMC41NiwxLjAyNCwwLjk3OCwxLjQ0M2gwLjAyNSAgIGMwLjQxOCwwLjQxOSwwLjg5MiwwLjc0NSwxLjQyLDAuOTc3aDAuMDIzYzAuNTU4LDAuMjQ5LDEuMTQ4LDAuMzczLDEuNzY5LDAuMzczaDcuMjg3YzAuNjIsMCwxLjIwOS0wLjEyNCwxLjc2OS0wLjM3MyAgIGMwLjU0My0wLjIzMSwxLjAyMy0wLjU1OCwxLjQ0My0wLjk3N2MwLjQxOC0wLjQxOSwwLjc0My0wLjksMC45NzgtMS40NDNjMC4yNDgtMC41NzQsMC4zNzEtMS4xNzIsMC4zNzEtMS43OTJ2LTEuMDQ4ICAgYzAtMC41MjcsMC4wMjMtMC45NzksMC4wNzEtMS4zNWMwLjAyOS0wLjM0MiwwLjA5Mi0wLjYzNywwLjE4Ni0wLjg4NWMwLjEwOC0wLjIzMywwLjI2NC0wLjQ3MywwLjQ2Ni0wLjcyMnYtMC4wMjMgICBjMC4xODctMC4yMzMsMC40MDMtMC40NjYsMC42NTEtMC42OTljMC4wMTYtMC4wMTYsMC4wMzEtMC4wMywwLjA0Ny0wLjA0NmMwLjU3NC0wLjYwNSwxLjEwMy0xLjIxLDEuNTgzLTEuODE2ICAgYzAuODY4LTEuMTAyLDEuNTkxLTIuMjE5LDIuMTY1LTMuMzUyYzEuMTE3LTIuMTczLDEuNjY3LTQuMjkxLDEuNjUyLTYuMzU1QzYyLjYwNSwyOS44NTksNjIuMjQsMjcuOTY2LDYxLjUxMSwyNi4xNXogICAgTTgxLjc4NSw0My4xNDJjMCw2Ljg3NS0xLjc1MywxMy4wMi01LjI2MSwxOC40MzZjLTEuMzgxLDIuMTQxLTMuMDMyLDQuMTY3LTQuOTU4LDYuMDc1Yy02Ljc1LDYuNzk3LTE0LjkxMywxMC4xOTUtMjQuNDg2LDEwLjE5NSAgIGMtNi40NTcsMC0xMi4yOTItMS41NDQtMTcuNTA1LTQuNjMyYy0wLjI0OSwwLjI5NS0wLjU2LDAuNTI3LTAuOTMyLDAuNjk4bC0xNi4xMzEsNy42NThjLTAuNTEyLDAuMjMzLTEuMDQ3LDAuMzAzLTEuNjA2LDAuMjEgICBjLTAuNTU5LTAuMDk0LTEuMDQtMC4zNDItMS40NDMtMC43NDVjLTAuNDA0LTAuNDAzLTAuNjUyLTAuODg2LTAuNzQ2LTEuNDQzYy0wLjA5My0wLjU2LTAuMDIzLTEuMDk0LDAuMjEtMS42MDVsNy42NTgtMTYuMjcxICAgYzAuMTQtMC4zMTEsMC4zMzQtMC41NzQsMC41ODMtMC43OTJjLTMuMTk3LTUuMjYxLTQuNzk2LTExLjE4OC00Ljc5Ni0xNy43ODRjMC05LjYyMSwzLjM3Ni0xNy44MDcsMTAuMTI1LTI0LjU1OCAgIGMwLjUyOC0wLjUyNywxLjA3MS0xLjA0LDEuNjMtMS41MzZjMi4yMDQtMS45NTYsNC41MzktMy41Nyw3LjAwNi00Ljg0MkMzNS45NDUsOS42OTIsNDEuMjYsOC40MzYsNDcuMDgsOC40MzYgICBjOS41NzMsMCwxNy43MzYsMy4zODIsMjQuNDg2LDEwLjE0OGM2LjQyNiw2LjM3OCw5LjgyNCwxNC4wMjksMTAuMTk1LDIyLjk1MkM4MS43NzgsNDIuMDYzLDgxLjc4NSw0Mi41OTksODEuNzg1LDQzLjE0MnogICAgTTUxLjM4NiwyNS4yNjZjLTAuNzE0LTAuMzI2LTEuNDU5LTAuNTEzLTIuMjM1LTAuNTU5Yy0wLjQ4LTAuMDMxLTAuODc2LTAuMjI1LTEuMTg4LTAuNTgzYy0wLjMxMS0wLjM0LTAuNDU3LTAuNzUyLTAuNDQxLTEuMjMzICAgYzAuMDMxLTAuNDY2LDAuMjI1LTAuODU0LDAuNTgyLTEuMTY1YzAuMzU3LTAuMzEsMC43NjktMC40NTcsMS4yMzQtMC40NDFjMS4yMjYsMC4wNzcsMi4zOTcsMC4zOCwzLjUxNSwwLjkwNyAgIGMxLjA2OSwwLjQ5NywyLjAxOCwxLjE3OSwyLjg0LDIuMDQ5YzAuODA3LDAuODY5LDEuNDM1LDEuODU0LDEuODg0LDIuOTU2YzAuNDY2LDEuMTMzLDAuNjk5LDIuMzIsMC42OTksMy41NjIgICBjMCwwLjQ2NS0wLjE3MSwwLjg2OS0wLjUxMiwxLjIxYy0wLjMyNSwwLjMyNi0wLjcyMiwwLjQ4OS0xLjE4OCwwLjQ4OWMtMC40OCwwLTAuODg0LTAuMTYzLTEuMjEtMC40ODkgICBjLTAuMzQyLTAuMzQxLTAuNTEzLTAuNzQ2LTAuNTEzLTEuMjFjMC0wLjc5Mi0wLjE0Ni0xLjU1Mi0wLjQ0MS0yLjI4MWMtMC4yNzktMC42OTktMC42ODMtMS4zMjctMS4yMTEtMS44ODYgICBTNTIuMDY3LDI1LjU5MSw1MS4zODYsMjUuMjY2eiBNNTcuNzg3LDM1LjM2OGMwLDAuNTEyLTAuMTg4LDAuOTU0LTAuNTYsMS4zMjZjLTAuMzU2LDAuMzU3LTAuOCwwLjUzNi0xLjMyNiwwLjUzNiAgIGMtMC41MTIsMC0wLjk0Ni0wLjE3OS0xLjMwMy0wLjUzNmMtMC4zNzQtMC4zNzItMC41Ni0wLjgxNC0wLjU2LTEuMzI2YzAtMC41MTMsMC4xODYtMC45NTYsMC41Ni0xLjMyNyAgI
GMwLjM1Ni0wLjM1NywwLjc5MS0wLjUzNiwxLjMwMy0wLjUzNmMwLjUyNiwwLDAuOTcsMC4xNzgsMS4zMjYsMC41MzZDNTcuNiwzNC40MTMsNTcuNzg3LDM0Ljg1NSw1Ny43ODcsMzUuMzY4eiBNNTEuODk3LDU0LjcxMSAgIEg0My40Yy0wLjcxMiwwLTEuMzE4LDAuMjU2LTEuODE1LDAuNzY5Yy0wLjUxMiwwLjQ5Ny0wLjc2OSwxLjA5NC0wLjc2OSwxLjc5MmMwLDAuNzE0LDAuMjQ5LDEuMzE5LDAuNzQ2LDEuODE1bDAuMDIzLDAuMDI0ICAgYzAuNDk3LDAuNDk2LDEuMTAzLDAuNzQ0LDEuODE1LDAuNzQ0aDguNDk3YzAuNzE1LDAsMS4zMTgtMC4yNDgsMS44MTUtMC43NDRjMC40OTctMC41MTMsMC43NDUtMS4xMjYsMC43NDUtMS44NCAgIGMwLTAuNjk4LTAuMjQ4LTEuMjk1LTAuNzQ1LTEuNzkydi0wLjAyM0M1My4yMDEsNTQuOTU5LDUyLjU5Niw1NC43MTEsNTEuODk3LDU0LjcxMXogTTQyLjcyNiw2Mi40MzhoLTAuMDIzICAgYy0wLjQ5NywwLjQ5Ny0wLjc0NSwxLjEwMy0wLjc0NSwxLjgxNnMwLjI1NywxLjMxOCwwLjc2OSwxLjgxNWMwLjQ5NywwLjQ5NywxLjEwMiwwLjc0NSwxLjgxNiwwLjc0NWg2LjEyMiAgIGMwLjY5NywwLDEuMjk1LTAuMjQ4LDEuNzkyLTAuNzQ1aDAuMDIyYzAuNDk3LTAuNDk3LDAuNzQ2LTEuMTAyLDAuNzQ2LTEuODE1cy0wLjI0OS0xLjMxOS0wLjc0Ni0xLjgxNiAgIGMtMC41MTItMC41MTItMS4xMTctMC43NjgtMS44MTQtMC43NjhoLTYuMTIyQzQzLjgyOCw2MS42NzEsNDMuMjIzLDYxLjkyNyw0Mi43MjYsNjIuNDM4eiIvPjwvZz48L3N2Zz4=);
}
<div class="icon">
<span></span>
</div>
<div class="icon green">
<span></span>
</div>
<div class="icon red">
<span></span>
</div>

这是一次相当不错的兔子洞之旅,但它就在这里!

var tolerance = 1;
var invertRange = [0, 1];
var invertStep = 0.1;
var sepiaRange = [0, 1];
var sepiaStep = 0.1;
var saturateRange = [5, 100];
var saturateStep = 5;
var hueRotateRange = [0, 360];
var hueRotateStep = 5;
var possibleColors;
var color = document.getElementById('color');
var pixel = document.getElementById('pixel');
var filtersBox = document.getElementById('filters');
var button = document.getElementById('button');
button.addEventListener('click', function() {
getNewColor(color.value);
})


// matrices taken from https://www.w3.org/TR/filter-effects/#feColorMatrixElement
function sepiaMatrix(s) {
return [
(0.393 + 0.607 * (1 - s)), (0.769 - 0.769 * (1 - s)), (0.189 - 0.189 * (1 - s)),
(0.349 - 0.349 * (1 - s)), (0.686 + 0.314 * (1 - s)), (0.168 - 0.168 * (1 - s)),
(0.272 - 0.272 * (1 - s)), (0.534 - 0.534 * (1 - s)), (0.131 + 0.869 * (1 - s)),
]
}


function saturateMatrix(s) {
return [
0.213+0.787*s, 0.715-0.715*s, 0.072-0.072*s,
0.213-0.213*s, 0.715+0.285*s, 0.072-0.072*s,
0.213-0.213*s, 0.715-0.715*s, 0.072+0.928*s,
]
}


function hueRotateMatrix(d) {
var cos = Math.cos(d * Math.PI / 180);
var sin = Math.sin(d * Math.PI / 180);
var a00 = 0.213 + cos*0.787 - sin*0.213;
var a01 = 0.715 - cos*0.715 - sin*0.715;
var a02 = 0.072 - cos*0.072 + sin*0.928;


var a10 = 0.213 - cos*0.213 + sin*0.143;
var a11 = 0.715 + cos*0.285 + sin*0.140;
var a12 = 0.072 - cos*0.072 - sin*0.283;


var a20 = 0.213 - cos*0.213 - sin*0.787;
var a21 = 0.715 - cos*0.715 + sin*0.715;
var a22 = 0.072 + cos*0.928 + sin*0.072;


return [
a00, a01, a02,
a10, a11, a12,
a20, a21, a22,
]
}


function clamp(value) {
return value > 255 ? 255 : value < 0 ? 0 : value;
}


function filter(m, c) {
return [
clamp(m[0]*c[0] + m[1]*c[1] + m[2]*c[2]),
clamp(m[3]*c[0] + m[4]*c[1] + m[5]*c[2]),
clamp(m[6]*c[0] + m[7]*c[1] + m[8]*c[2]),
]
}


function invertBlack(i) {
return [
i * 255,
i * 255,
i * 255,
]
}


function generateColors() {
let possibleColors = [];


let invert = invertRange[0];
for (invert; invert <= invertRange[1]; invert+=invertStep) {
let sepia = sepiaRange[0];
for (sepia; sepia <= sepiaRange[1]; sepia+=sepiaStep) {
let saturate = saturateRange[0];
for (saturate; saturate <= saturateRange[1]; saturate+=saturateStep) {
let hueRotate = hueRotateRange[0];
for (hueRotate; hueRotate <= hueRotateRange[1]; hueRotate+=hueRotateStep) {
let invertColor = invertBlack(invert);
let sepiaColor = filter(sepiaMatrix(sepia), invertColor);
let saturateColor = filter(saturateMatrix(saturate), sepiaColor);
let hueRotateColor = filter(hueRotateMatrix(hueRotate), saturateColor);


let colorObject = {
filters: { invert, sepia, saturate, hueRotate },
color: hueRotateColor
}


possibleColors.push(colorObject);
}
}
}
}


return possibleColors;
}


function getFilters(targetColor, localTolerance) {
possibleColors = possibleColors || generateColors();


for (var i = 0; i < possibleColors.length; i++) {
var color = possibleColors[i].color;
if (
Math.abs(color[0] - targetColor[0]) < localTolerance &&
Math.abs(color[1] - targetColor[1]) < localTolerance &&
Math.abs(color[2] - targetColor[2]) < localTolerance
) {
return filters = possibleColors[i].filters;
break;
}
}


localTolerance += tolerance;
return getFilters(targetColor, localTolerance)
}


function getNewColor(color) {
var targetColor = color.split(',');
targetColor = [
parseInt(targetColor[0]), // [R]
parseInt(targetColor[1]), // [G]
parseInt(targetColor[2]), // [B]
]
var filters = getFilters(targetColor, tolerance);
var filtersCSS = 'filter: ' +
'invert('+Math.floor(filters.invert*100)+'%) '+
'sepia('+Math.floor(filters.sepia*100)+'%) ' +
'saturate('+Math.floor(filters.saturate*100)+'%) ' +
'hue-rotate('+Math.floor(filters.hueRotate)+'deg);';
pixel.style = filtersCSS;
filtersBox.innerText = filtersCSS
}


getNewColor(color.value);
#pixel {
width: 50px;
height: 50px;
background: rgb(0,0,0);
}
<input type="text" id="color" placeholder="R,G,B" value="250,150,50" />
<button id="button">get filters</button>
<div id="pixel"></div>
<div id="filters"></div>

编辑:此解决方案不适用于生产,仅说明了一种可用于实现OP要求的方法。事实上,它在色谱的某些区域很弱。由于在@MultiplyByZer0的答案中详细描述的原因,可以通过在步骤迭代中更多的粒度或者通过实现更多的滤波器函数来实现更好的结果。

编辑2: OP正在寻找非强力解决方案。在这种情况下,它非常简单,只需解这个方程:

CSS Filter Matrix Equations

在哪里

a = hue-rotation
b = saturation
c = sepia
d = invert

@Dave是第一个将一个答案发布到此(带有工作代码)的人,他的回答对我来说是无耻的复制粘贴灵感的宝贵来源。这篇文章一开始是试图解释和完善@Dave的 答案,但它后来演变成了自己的答案。

我的方法明显更快。根据随机生成的RGB颜色上的JSPerf基准测试,@Dave的算法在600毫秒中运行,而我的算法在30毫秒中运行。这肯定很重要,例如在加载时间中,速度是至关重要的。

此外,对于某些颜色,我的算法执行得更好:

  • 对于rgb(0,255,0),@Dave的生成rgb(29,218,34),而我的生成rgb(1,255,0)
  • 对于rgb(0,0,255),@Dave的生成rgb(37,39,255),而我的生成rgb(5,6,255)
  • 对于rgb(19,11,118),@Dave的生成rgb(36,27,102),而我的生成rgb(20,11,112)

演示

"use strict";


class Color {
constructor(r, g, b) { this.set(r, g, b); }
toString() { return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; }


set(r, g, b) {
this.r = this.clamp(r);
this.g = this.clamp(g);
this.b = this.clamp(b);
}


hueRotate(angle = 0) {
angle = angle / 180 * Math.PI;
let sin = Math.sin(angle);
let cos = Math.cos(angle);


this.multiply([
0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
]);
}


grayscale(value = 1) {
this.multiply([
0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value)
]);
}


sepia(value = 1) {
this.multiply([
0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value),
0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value),
0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value)
]);
}


saturate(value = 1) {
this.multiply([
0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value
]);
}


multiply(matrix) {
let newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
let newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
let newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
this.r = newR; this.g = newG; this.b = newB;
}


brightness(value = 1) { this.linear(value); }
contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }


linear(slope = 1, intercept = 0) {
this.r = this.clamp(this.r * slope + intercept * 255);
this.g = this.clamp(this.g * slope + intercept * 255);
this.b = this.clamp(this.b * slope + intercept * 255);
}


invert(value = 1) {
this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
}


hsl() { // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
let r = this.r / 255;
let g = this.g / 255;
let b = this.b / 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;


if(max === min) {
h = s = 0;
} else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
} h /= 6;
}


return {
h: h * 100,
s: s * 100,
l: l * 100
};
}


clamp(value) {
if(value > 255) { value = 255; }
else if(value < 0) { value = 0; }
return value;
}
}


class Solver {
constructor(target) {
this.target = target;
this.targetHSL = target.hsl();
this.reusedColor = new Color(0, 0, 0); // Object pool
}


solve() {
let result = this.solveNarrow(this.solveWide());
return {
values: result.values,
loss: result.loss,
filter: this.css(result.values)
};
}


solveWide() {
const A = 5;
const c = 15;
const a = [60, 180, 18000, 600, 1.2, 1.2];


let best = { loss: Infinity };
for(let i = 0; best.loss > 25 && i < 3; i++) {
let initial = [50, 20, 3750, 50, 100, 100];
let result = this.spsa(A, a, c, initial, 1000);
if(result.loss < best.loss) { best = result; }
} return best;
}


solveNarrow(wide) {
const A = wide.loss;
const c = 2;
const A1 = A + 1;
const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
return this.spsa(A, a, c, wide.values, 500);
}


spsa(A, a, c, values, iters) {
const alpha = 1;
const gamma = 0.16666666666666666;


let best = null;
let bestLoss = Infinity;
let deltas = new Array(6);
let highArgs = new Array(6);
let lowArgs = new Array(6);


for(let k = 0; k < iters; k++) {
let ck = c / Math.pow(k + 1, gamma);
for(let i = 0; i < 6; i++) {
deltas[i] = Math.random() > 0.5 ? 1 : -1;
highArgs[i] = values[i] + ck * deltas[i];
lowArgs[i]  = values[i] - ck * deltas[i];
}


let lossDiff = this.loss(highArgs) - this.loss(lowArgs);
for(let i = 0; i < 6; i++) {
let g = lossDiff / (2 * ck) * deltas[i];
let ak = a[i] / Math.pow(A + k + 1, alpha);
values[i] = fix(values[i] - ak * g, i);
}


let loss = this.loss(values);
if(loss < bestLoss) { best = values.slice(0); bestLoss = loss; }
} return { values: best, loss: bestLoss };


function fix(value, idx) {
let max = 100;
if(idx === 2 /* saturate */) { max = 7500; }
else if(idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; }


if(idx === 3 /* hue-rotate */) {
if(value > max) { value = value % max; }
else if(value < 0) { value = max + value % max; }
} else if(value < 0) { value = 0; }
else if(value > max) { value = max; }
return value;
}
}


loss(filters) { // Argument is array of percentages.
let color = this.reusedColor;
color.set(0, 0, 0);


color.invert(filters[0] / 100);
color.sepia(filters[1] / 100);
color.saturate(filters[2] / 100);
color.hueRotate(filters[3] * 3.6);
color.brightness(filters[4] / 100);
color.contrast(filters[5] / 100);


let colorHSL = color.hsl();
return Math.abs(color.r - this.target.r)
+ Math.abs(color.g - this.target.g)
+ Math.abs(color.b - this.target.b)
+ Math.abs(colorHSL.h - this.targetHSL.h)
+ Math.abs(colorHSL.s - this.targetHSL.s)
+ Math.abs(colorHSL.l - this.targetHSL.l);
}


css(filters) {
function fmt(idx, multiplier = 1) { return Math.round(filters[idx] * multiplier); }
return `filter: invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%);`;
}
}


$("button.execute").click(() => {
let rgb = $("input.target").val().split(",");
if (rgb.length !== 3) { alert("Invalid format!"); return; }


let color = new Color(rgb[0], rgb[1], rgb[2]);
let solver = new Solver(color);
let result = solver.solve();


let lossMsg;
if (result.loss < 1) {
lossMsg = "This is a perfect result.";
} else if (result.loss < 5) {
lossMsg = "The is close enough.";
} else if(result.loss < 15) {
lossMsg = "The color is somewhat off. Consider running it again.";
} else {
lossMsg = "The color is extremely off. Run it again!";
}


$(".realPixel").css("background-color", color.toString());
$(".filterPixel").attr("style", result.filter);
$(".filterDetail").text(result.filter);
$(".lossDetail").html(`Loss: ${result.loss.toFixed(1)}. <b>${lossMsg}</b>`);
});
.pixel {
display: inline-block;
background-color: #000;
width: 50px;
height: 50px;
}


.filterDetail {
font-family: "Consolas", "Menlo", "Ubuntu Mono", monospace;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>


<input class="target" type="text" placeholder="r, g, b" value="250, 150, 50" />
<button class="execute">Compute Filters</button>


<p>Real pixel, color applied through CSS <code>background-color</code>:</p>
<div class="pixel realPixel"></div>


<p>Filtered pixel, color applied through CSS <code>filter</code>:</p>
<div class="pixel filterPixel"></div>


<p class="filterDetail"></p>
<p class="lossDetail"></p>


用法

let color = new Color(0, 255, 0);
let solver = new Solver(color);
let result = solver.solve();
let filterCSS = result.filter;

解释

我们将从一些JavaScript开始。

"use strict";


class Color {
constructor(r, g, b) {
this.r = this.clamp(r);
this.g = this.clamp(g);
this.b = this.clamp(b);
} toString() { return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; }


hsl() { // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
let r = this.r / 255;
let g = this.g / 255;
let b = this.b / 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;


if(max === min) {
h = s = 0;
} else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
} h /= 6;
}


return {
h: h * 100,
s: s * 100,
l: l * 100
};
}


clamp(value) {
if(value > 255) { value = 255; }
else if(value < 0) { value = 0; }
return value;
}
}


class Solver {
constructor(target) {
this.target = target;
this.targetHSL = target.hsl();
}


css(filters) {
function fmt(idx, multiplier = 1) { return Math.round(filters[idx] * multiplier); }
return `filter: invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%);`;
}
}

解释:

  • Color类表示RGB颜色。
    • toString()函数返回CSSrgb(...)颜色字符串中的颜色。
    • hsl()函数返回转换为HSL的颜色。
    • 它的clamp()函数确保给定的颜色值在界限(0-255)内。
  • Solver类将尝试求解目标颜色。
    • css()函数返回CSS筛选器字符串中的给定筛选器。

实施grayscale()sepia()saturate()

CSS/SVG过滤器的核心是过滤基元,它表示对图像的低级修改。

滤波器grayscale()sepia()saturate()由滤波器原始<feColorMatrix>实现,该滤波器原始UNK2 ABC UNK3 7在由滤波器指定的矩阵(通常动态生成)和从颜色创建的矩阵之间执行矩阵乘法。图示:

Matrix multiplication

我们可以在这里进行一些优化:

  • 颜色矩阵的最后一个元素是并且将总是1。没有必要计算或存储它。
  • 也没有必要计算或存储alpha/透明度值(A),因为我们处理的是RGB,而不是RGBA.
  • 因此,我们可以将滤镜矩阵从5x5调整为3x5,将颜色矩阵从1x5调整为1x3。这节省了一些工作。
  • 所有<feColorMatrix>筛选器将列4和5保留为零。因此,我们可以进一步将滤波器矩阵减少到3×3
  • 由于乘法相对简单,因此不需要为此拖入复杂的数学库。我们可以自己实现矩阵乘法算法。

实施:

function multiply(matrix) {
let newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
let newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
let newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
this.r = newR; this.g = newG; this.b = newB;
}

(我们使用临时变量来保存每行乘法的结果,因为我们不希望this.r等发生变化。影响后续计算。)

现在我们已经实现了<feColorMatrix>,我们可以实现grayscale()sepia()saturate(),这只需使用给定的滤波器矩阵调用它:

function grayscale(value = 1) {
this.multiply([
0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value)
]);
}


function sepia(value = 1) {
this.multiply([
0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value),
0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value),
0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value)
]);
}


function saturate(value = 1) {
this.multiply([
0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value
]);
}

实施hue-rotate()

hue-rotate()滤波器由<feColorMatrix type="hueRotate" />实现。

滤波器矩阵的计算如下所示:

例如,元素A00的计算方法如下:

注意事项:

  • 旋转角度以度为单位,在传递到Math.sin()Math.cos()之前,必须将其转换为弧度。
  • Math.sin(angle)Math.cos(angle)应计算一次,然后缓存。

实施:

function hueRotate(angle = 0) {
angle = angle / 180 * Math.PI;
let sin = Math.sin(angle);
let cos = Math.cos(angle);


this.multiply([
0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
]);
}

实施brightness()contrast()

brightness()contrast()滤波器由具有<feFuncX type="linear" /><feComponentTransfer>实现。

每个<feFuncX type="linear" />元素接受倾斜截击属性。然后通过一个简单的公式计算每个新的颜色值:

value = slope * value + intercept

这很容易实现:

function linear(slope = 1, intercept = 0) {
this.r = this.clamp(this.r * slope + intercept * 255);
this.g = this.clamp(this.g * slope + intercept * 255);
this.b = this.clamp(this.b * slope + intercept * 255);
}

一旦实现了这一点,brightness()contrast()也可以实现:

function brightness(value = 1) { this.linear(value); }
function contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }

实施invert()

invert()滤波器由具有<feFuncX type="table" /><feComponentTransfer>实现。

规范规定:

在下文中,C是初始分量,C是重新映射的分量;都在闭区间[0,1]内。

对于";在“表”中,该函数由属性表值中给出的值之间的线性插值定义。该表具有名词+1值(即,V0到V名词),其指定名词的均匀大小的内插区域的开始值和结束值。插值使用以下公式:

对于C的值,求K,使得:

K/N≤C<;(K+1)/n

C的结果由下式给出:

C=VK+(C-K/N)*N*(VK+1-VK

对此公式的解释:

  • invert()筛选器定义此表:[value,1-value]。这是表值
  • 该公式定义_0的_ABC,因此名词+1是表的长度。由于表的长度为2,因此名词=1。
  • 公式定义KKK+1是表的索引。由于表有2个元素,K=0。

因此,我们可以将公式简化为:

C=V0+C*(V1-V0

内嵌表的值,我们只剩下:

C=值+C*(1-值-值)

还有一个简化:

C=值+C*(1-2*值)

规范将CC定义为范围0-1(与0-255相对)内的RGB值。因此,我们必须在计算之前按比例缩小数值,并在计算之后按比例缩小数值。

因此,我们得出了我们的实现:

function invert(value = 1) {
this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
}

插曲:@Dave的蛮力算法

@Dave的代码生成176,660筛选器组合,包括:

  • 11invert()过滤器(0%,10%,20%,..,100%)
  • 11sepia()过滤器(0%,10%,20%,..,100%)
  • 20saturate()过滤器(5%,10%,15%,..,100%)
  • 73hue-rotate()滤波器(0度,5度,10度,..,360度)

它按以下顺序计算过滤器:

filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);

然后遍历所有计算的颜色。一旦发现生成的颜色在容差范围内(所有RGB值都在目标颜色的5个单位范围内),它就会停止。

然而,这是缓慢和低效的。因此,我提出我自己的答案。

实施SPSA

首先,我们必须定义一个损失函数,它返回由滤镜组合产生的颜色与目标颜色之间的差异。如果滤波器是完美的,则损失函数应返回0。

我们将测量色差作为两个度量的总和:

  • RGB差异,因为目标是产生最接近的RGB值。
  • HSL差,因为许多HSL值对应于滤波器(例如,色调粗略地与hue-rotate()相关,饱和度与saturate()相关,等等)。

损失函数将采用一个参数,即过滤器百分比的数组。

我们将使用以下筛选顺序:

filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg) brightness(e%) contrast(f%);

实施:

function loss(filters) {
let color = new Color(0, 0, 0);
color.invert(filters[0] / 100);
color.sepia(filters[1] / 100);
color.saturate(filters[2] / 100);
color.hueRotate(filters[3] * 3.6);
color.brightness(filters[4] / 100);
color.contrast(filters[5] / 100);


let colorHSL = color.hsl();
return Math.abs(color.r - this.target.r)
+ Math.abs(color.g - this.target.g)
+ Math.abs(color.b - this.target.b)
+ Math.abs(colorHSL.h - this.targetHSL.h)
+ Math.abs(colorHSL.s - this.targetHSL.s)
+ Math.abs(colorHSL.l - this.targetHSL.l);
}

我们将尝试最小化损失函数,使得:

loss([a, b, c, d, e, f]) = 0

斯普萨算法(网址更多信息执行文件参考代码)非常擅长于此。它被设计用于优化具有局部最小值、噪声/非线性/多变量损失函数等的复杂系统。它已被用于调整国际象棋引擎。。与许多其他算法不同的是,描述它的论文实际上是可以理解的(尽管付出了很大的努力)。

实施:

function spsa(A, a, c, values, iters) {
const alpha = 1;
const gamma = 0.16666666666666666;


let best = null;
let bestLoss = Infinity;
let deltas = new Array(6);
let highArgs = new Array(6);
let lowArgs = new Array(6);


for(let k = 0; k < iters; k++) {
let ck = c / Math.pow(k + 1, gamma);
for(let i = 0; i < 6; i++) {
deltas[i] = Math.random() > 0.5 ? 1 : -1;
highArgs[i] = values[i] + ck * deltas[i];
lowArgs[i]  = values[i] - ck * deltas[i];
}


let lossDiff = this.loss(highArgs) - this.loss(lowArgs);
for(let i = 0; i < 6; i++) {
let g = lossDiff / (2 * ck) * deltas[i];
let ak = a[i] / Math.pow(A + k + 1, alpha);
values[i] = fix(values[i] - ak * g, i);
}


let loss = this.loss(values);
if(loss < bestLoss) { best = values.slice(0); bestLoss = loss; }
} return { values: best, loss: bestLoss };


function fix(value, idx) {
let max = 100;
if(idx === 2 /* saturate */) { max = 7500; }
else if(idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; }


if(idx === 3 /* hue-rotate */) {
if(value > max) { value = value % max; }
else if(value < 0) { value = max + value % max; }
} else if(value < 0) { value = 0; }
else if(value > max) { value = max; }
return value;
}
}

我对SPSA做了一些修改/优化:

  • 使用产生的最佳结果,而不是最后一个。
  • 重用所有数组(deltashighArgslowArgs),而不是在每次迭代中重新创建它们。
  • 对ABC_0的_使用值的数组,而不是单个值。这是因为所有的滤波器都是不同的,因此它们应该以不同的速度移动/收敛。
  • 在每次迭代之后运行fix函数。它将所有值钳位到0%和100%之间,但saturate(其中最大值为7500%)、brightnesscontrast(其中最大值为200%)以及hueRotate(其中值回绕而不是被钳位)除外。

我在两个阶段的过程中使用SPSA:

  1. “宽”舞台,试图&“探索&”搜索空间。如果结果不令人满意,它将对SPSA进行有限的重试。
  2. “窄”阶段,从宽阶段中获得最佳结果,并尝试&“细化&”它。它使用AA的动态值。

实施:

function solve() {
let result = this.solveNarrow(this.solveWide());
return {
values: result.values,
loss: result.loss,
filter: this.css(result.values)
};
}


function solveWide() {
const A = 5;
const c = 15;
const a = [60, 180, 18000, 600, 1.2, 1.2];


let best = { loss: Infinity };
for(let i = 0; best.loss > 25 && i < 3; i++) {
let initial = [50, 20, 3750, 50, 100, 100];
let result = this.spsa(A, a, c, initial, 1000);
if(result.loss < best.loss) { best = result; }
} return best;
}


function solveNarrow(wide) {
const A = wide.loss;
const c = 2;
const A1 = A + 1;
const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
return this.spsa(A, a, c, wide.values, 500);
}

调谐SPSA

警告:不要弄乱SPSA代码,尤其是它的常量,除非您确定您知道自己在做什么。

重要的常数是AAC、初始值、重试阈值、fix()中的max的值以及每个阶段的迭代次数。所有这些值都经过仔细调整,以产生良好的结果,随机调整它们几乎肯定会降低算法的有用性。

如果你坚持要改变它,你必须事先测量。优化";。

首先,应用这个补丁

然后运行Node.JS中的代码。经过一段时间后,结果应该如下所示:

Average loss: 3.4768521401985275
Average time: 11.4915ms

现在把常量调到你心满意足的程度。

一些提示:

  • 平均损失应该在4左右。如果大于4,则产生的结果相差太远,您应该调整准确性。如果小于4,就是在浪费时间,应该减少迭代次数。
  • 如果增加/减少迭代次数,请适当调整A
  • 如果增加/减少A,请适当调整A
  • 如果要查看每次迭代的结果,请使用--debug标志。

热释光;博士

只需使用CSS中引用的SVG过滤器,即可使所有非常变得简单。你只需要一个FecolorMatrix来重新着色。这张是黄色的。FecolorMatrix中的第五列保存单位比例上的RGB目标值。(对于黄色-它是1,1,0)

.icon {
filter: url(#recolorme);
}
<svg height="0px" width="0px">
<defs>
#ffff00
<filter id="recolorme" color-interpolation-filters="sRGB">
<feColorMatrix type="matrix" values="0 0 0 0 1
0 0 0 0 1
0 0 0 0 0
0 0 0 1 0"/>
</filter>
</defs>
</svg>




<img class="icon" src="https://www.nouveauelevator.com/image/black-icon/android.png">

我注意到通过SVG过滤器处理的例子是不完整的,我写了我的(它工作得很好):(见Michael Mullany的回答) 因此,这里是获得任何你想要的颜色的方法:

PickColor.onchange=()=>{
RGBval.textContent = PickColor.value;


let
HexT = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(PickColor.value),
r = parseInt(HexT[1], 16),
g = parseInt(HexT[2], 16),
b = parseInt(HexT[3], 16);


FilterVal.textContent =  SetFilter( r, g, b);
}
function SetFilter( r, g, b )
{
const Matrix  = document.querySelector('#FilterSVG feColorMatrix');
r = r/255;
g = g/255;
b = b/255;


Matrix.setAttribute("values",  "0 0 0 0 "+r+" 0 0 0 0 "+g+ " 0 0 0 0 "+b+" 0 0 0 1 0");


return "\n 0 0 0 0 "+r+"\n 0 0 0 0 "+g+ "\n 0 0 0 0 "+b+"\n 0 0 0 1 0"
}
#RGBval    { text-transform: uppercase }
#PickColor { height: 50px; margin: 0 20px }
th         { background-color: lightblue; padding: 5px 20px }
pre        { margin: 0 15px }
#ImgTest   { filter: url(#FilterSVG) }
<svg height="0px" width="0px">
<defs>
<filter id="FilterSVG" color-interpolation-filters="sRGB">
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
</filter>
</defs>
</svg>


<table>
<caption>SVG method</caption>
<tr> <th>Image</th> <th>Color</th> </tr>
<tr>
<td><img src="https://www.nouveauelevator.com/image/black-icon/android.png" id="ImgTest" /></td>
<td><input type="color" value="#000000"  id="PickColor" ></td>
</tr>
<tr> <td>.</td> <td>.</td> </tr>
<tr> <th>Filter value </th> <th>#RBG target</th> </tr>
<tr>
<td><pre id="FilterVal">
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 1 0</pre></td>
<td id="RGBval">#000000</td>
</tr>
</table>

下面是第二种解决方案,仅在CODE=>URL.CreateObjectURL中使用SVG过滤器

const
SVG_Filter = {
init(ImgID)
{
this.Img = document.getElementById(ImgID);
let
NS = 'http://www.w3.org/2000/svg';


this.SVG    = document.createElementNS(NS,'svg'),
this.filter = document.createElementNS(NS,'filter'),
this.matrix = document.createElementNS(NS,'feColorMatrix');


this.filter.setAttribute( 'id', 'FilterSVG');
this.filter.setAttribute( 'color-interpolation-filters', 'sRGB');


this.matrix.setAttribute( 'type', 'matrix');
this.matrix.setAttribute('values', '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0');


this.filter.appendChild(this.matrix);
this.SVG.appendChild(this.filter);


this.xXMLs = new XMLSerializer();
},
SetColor( r, g, b )
{
r = r/255;
g = g/255;
b = b/255;


this.matrix.setAttribute('values', '0 0 0 0 '+r+' 0 0 0 0 '+g+ ' 0 0 0 0 '+b+' 0 0 0 1 0');


let
xBlob = new Blob( [ this.xXMLs.serializeToString(this.SVG) ], { type: 'image/svg+xml' });
xURL  = URL.createObjectURL(xBlob);


this.Img.style.filter = 'url(' + xURL + '#FilterSVG)';


return '\n 0 0 0 0 '+r+'\n 0 0 0 0 '+g+ '\n 0 0 0 0 '+b+'\n 0 0 0 1 0';
}
}


SVG_Filter.init('ImgTest');


PickColor.onchange=()=>{
RGBval.textContent = PickColor.value;


let
HexT = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(PickColor.value),
r = parseInt(HexT[1], 16),
g = parseInt(HexT[2], 16),
b = parseInt(HexT[3], 16);


FilterVal.textContent = SVG_Filter.SetColor( r, g, b );
}
#RGBval    { text-transform: uppercase }
#PickColor { height: 50px; margin: 0 20px }
th         { background-color: lightblue; padding: 5px 20px }
pre        { margin: 0 15px }
#PickColor { width:90px; height:28px; }
<table>
<caption>SVG method</caption>
<tr> <th>Image</th> <th>Color</th> </tr>
<tr>
<td><img src="https://www.nouveauelevator.com/image/black-icon/android.png" id="ImgTest" /></td>
<td><input type="color" value="#E2218A" id="PickColor" ></td>
</tr>
<tr> <td>.</td> <td>.</td> </tr>
<tr> <th>Filter value </th> <th>#RBG target</th> </tr>
<tr>
<td><pre id="FilterVal">
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 1 0</pre></td>
<td id="RGBval">#000000</td>
</tr>
</table>

就用

fill: #000000
CSS中

fill属性用于填充SVG形状的颜色。 fill属性可以接受任何CSS颜色值。

我从使用SVG过滤器的这个答案开始,并进行了以下修改:

基于数据URL的SVG筛选器

如果不想在标记中的某个位置定义SVG滤波器,则可以使用数据URL(用所需的颜色替换RGA):

filter: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg">\
<filter id="recolor" color-interpolation-filters="sRGB">\
<feColorMatrix type="matrix" values="\
0 0 0 0 R\
0 0 0 0 G\
0 0 0 0 B\
0 0 0 A 0\
"/>\
</filter>\
</svg>\
#recolor');

灰度回退

如果上面的版本不起作用,您还可以添加灰度回退。

saturatebrightness功能将任何颜色变为黑色(如果颜色已经是黑色,则不必包括黑色),然后invert使用所需的亮度使其变亮(L),并且您还可以选择指定不透明度(A)。

filter: saturate(0%) brightness(0%) invert(L) opacity(A);

SCSS混合

如果要动态指定颜色,可以使用以下SCSS混合:

@mixin recolor($color: #000, $opacity: 1) {
$r: red($color) / 255;
$g: green($color) / 255;
$b: blue($color) / 255;
$a: $opacity;


// grayscale fallback if SVG from data url is not supported
$lightness: lightness($color);
filter: saturate(0%) brightness(0%) invert($lightness) opacity($opacity);


// color filter
$svg-filter-id: "recolor";
filter: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg">\
<filter id="#{$svg-filter-id}" color-interpolation-filters="sRGB">\
<feColorMatrix type="matrix" values="\
0 0 0 0 #{$r}\
0 0 0 0 #{$g}\
0 0 0 0 #{$b}\
0 0 0 #{$a} 0\
"/>\
</filter>\
</svg>\
##{$svg-filter-id}');
}

用法示例:

.icon-green {
@include recolor(#00fa86, 0.8);
}

优点:

  • 无JavaScript
  • 没有额外的HTML元素
  • 如果支持CSS筛选器,但SVG筛选器不起作用,则会出现灰度回退
  • 如果使用mixin,用法非常简单(参见上面的示例)。
  • 颜色更具可读性,并且比Sepia技巧更容易修改(纯CSS中的RGBA组件,您甚至可以在SCSS中使用十六进制颜色)。
  • 避免hue-rotate的怪异行为

注意事项:

  • 并非所有浏览器支持 SVG都从数据URL(尤其是ID哈希)进行过滤,但它适用于当前的Firefox和Chromium浏览器(可能还有其他)。
  • 如果要动态指定颜色,则必须使用SCSS混合。
  • 纯CSS版本有点难看,如果你想要许多不同的颜色,你必须多次包含SVG.

基于以前惊人的答案,我试图让代码更容易理解。

我已经使它更具功能性,在我感到足够自信的地方添加了TypeScript类型,并在我理解正在发生的事情时重命名了一些变量。

import ColorParser from 'color';


function parseColorToRgb(input: string) {
const colorInstance = new ColorParser(input);


return new RgbColor(
colorInstance.red(),
colorInstance.green(),
colorInstance.blue(),
);
}


function clampRgbPart(value: number): number {
if (value > 255) {
return 255;
}


if (value < 0) {
return 0;
}


return value;
}


class RgbColor {
constructor(public red: number, public green: number, public blue: number) {}


toString() {
return `rgb(${Math.round(this.red)}, ${Math.round(
this.green,
)}, ${Math.round(this.blue)})`;
}


set(r: number, g: number, b: number) {
this.red = clampRgbPart(r);
this.green = clampRgbPart(g);
this.blue = clampRgbPart(b);
}


hueRotate(angle = 0) {
angle = (angle / 180) * Math.PI;
const sin = Math.sin(angle);
const cos = Math.cos(angle);


this.multiply([
0.213 + cos * 0.787 - sin * 0.213,
0.715 - cos * 0.715 - sin * 0.715,
0.072 - cos * 0.072 + sin * 0.928,
0.213 - cos * 0.213 + sin * 0.143,
0.715 + cos * 0.285 + sin * 0.14,
0.072 - cos * 0.072 - sin * 0.283,
0.213 - cos * 0.213 - sin * 0.787,
0.715 - cos * 0.715 + sin * 0.715,
0.072 + cos * 0.928 + sin * 0.072,
]);
}


grayscale(value = 1) {
this.multiply([
0.2126 + 0.7874 * (1 - value),
0.7152 - 0.7152 * (1 - value),
0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value),
0.7152 + 0.2848 * (1 - value),
0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value),
0.7152 - 0.7152 * (1 - value),
0.0722 + 0.9278 * (1 - value),
]);
}


sepia(value = 1) {
this.multiply([
0.393 + 0.607 * (1 - value),
0.769 - 0.769 * (1 - value),
0.189 - 0.189 * (1 - value),
0.349 - 0.349 * (1 - value),
0.686 + 0.314 * (1 - value),
0.168 - 0.168 * (1 - value),
0.272 - 0.272 * (1 - value),
0.534 - 0.534 * (1 - value),
0.131 + 0.869 * (1 - value),
]);
}


saturate(value = 1) {
this.multiply([
0.213 + 0.787 * value,
0.715 - 0.715 * value,
0.072 - 0.072 * value,
0.213 - 0.213 * value,
0.715 + 0.285 * value,
0.072 - 0.072 * value,
0.213 - 0.213 * value,
0.715 - 0.715 * value,
0.072 + 0.928 * value,
]);
}


multiply(matrix: number[]) {
const newR = clampRgbPart(
this.red * matrix[0] + this.green * matrix[1] + this.blue * matrix[2],
);
const newG = clampRgbPart(
this.red * matrix[3] + this.green * matrix[4] + this.blue * matrix[5],
);
const newB = clampRgbPart(
this.red * matrix[6] + this.green * matrix[7] + this.blue * matrix[8],
);
this.red = newR;
this.green = newG;
this.blue = newB;
}


brightness(value = 1) {
this.linear(value);
}


contrast(value = 1) {
this.linear(value, -(0.5 * value) + 0.5);
}


linear(slope = 1, intercept = 0) {
this.red = clampRgbPart(this.red * slope + intercept * 255);
this.green = clampRgbPart(this.green * slope + intercept * 255);
this.blue = clampRgbPart(this.blue * slope + intercept * 255);
}


invert(value = 1) {
this.red = clampRgbPart((value + (this.red / 255) * (1 - 2 * value)) * 255);
this.green = clampRgbPart(
(value + (this.green / 255) * (1 - 2 * value)) * 255,
);
this.blue = clampRgbPart(
(value + (this.blue / 255) * (1 - 2 * value)) * 255,
);
}


applyFilters(filters: Filters) {
this.set(0, 0, 0);
this.invert(filters[0] / 100);
this.sepia(filters[1] / 100);
this.saturate(filters[2] / 100);
this.hueRotate(filters[3] * 3.6);
this.brightness(filters[4] / 100);
this.contrast(filters[5] / 100);
}


hsl(): HSLData {
// Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
const r = this.red / 255;
const g = this.green / 255;
const b = this.blue / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h: number,
s: number,
l = (max + min) / 2;


if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;


case g:
h = (b - r) / d + 2;
break;


case b:
h = (r - g) / d + 4;
break;
}
h! /= 6;
}


return {
h: h! * 100,
s: s * 100,
l: l * 100,
};
}
}


interface HSLData {
h: number;
s: number;
l: number;
}


interface ColorFilterSolveResult {
loss: number;
filters: Filters;
}


const reusedColor = new RgbColor(0, 0, 0);


function formatFilterValue(value: number, multiplier = 1) {
return Math.round(value * multiplier);
}


type Filters = [
invert: number,
sepia: number,
saturate: number,
hueRotate: number,
brightness: number,
contrast: number,
];


function convertFiltersListToCSSFilter(filters: Filters) {
function fmt(idx: number, multiplier = 1) {
return Math.round(filters[idx] * multiplier);
}
const [invert, sepia, saturate, hueRotate, brightness, contrast] = filters;
return `filter: invert(${formatFilterValue(
invert,
)}%) sepia(${formatFilterValue(sepia)}%) saturate(${formatFilterValue(
saturate,
)}%) hue-rotate(${formatFilterValue(
hueRotate,
3.6,
)}deg) brightness(${formatFilterValue(
brightness,
)}%) contrast(${formatFilterValue(contrast)}%);`;
}


function calculateLossForFilters(
filters: Filters,
targetColor: RgbColor,
targetHSL: HSLData,
) {
reusedColor.applyFilters(filters);
const actualHSL = reusedColor.hsl();


return (
Math.abs(reusedColor.red - targetColor.red) +
Math.abs(reusedColor.green - targetColor.green) +
Math.abs(reusedColor.blue - targetColor.blue) +
Math.abs(actualHSL.h - targetHSL.h) +
Math.abs(actualHSL.s - targetHSL.s) +
Math.abs(actualHSL.l - targetHSL.l)
);
}






export function solveColor(input: string) {
const targetColor = parseColorToRgb(input);
const targetHSL = targetColor.hsl();


function improveInitialSolveResult(initialResult: ColorFilterSolveResult) {
const A = initialResult.loss;
const c = 2;
const A1 = A + 1;
const a: Filters = [
0.25 * A1,
0.25 * A1,
A1,
0.25 * A1,
0.2 * A1,
0.2 * A1,
];
return findColorFilters(A, a, c, initialResult.filters, 500);
}


function findColorFilters(
initialLoss: number,
filters: Filters,
c: number,
values: Filters,
iterationsCount: number,
): ColorFilterSolveResult {
const alpha = 1;
const gamma = 0.16666666666666666;


let best = null;
let bestLoss = Infinity;
const deltas = new Array(6);
const highArgs = new Array(6) as Filters;
const lowArgs = new Array(6) as Filters;


for (
let iterationIndex = 0;
iterationIndex < iterationsCount;
iterationIndex++
) {
const ck = c / Math.pow(iterationIndex + 1, gamma);
for (let i = 0; i < 6; i++) {
deltas[i] = Math.random() > 0.5 ? 1 : -1;
highArgs[i] = values[i] + ck * deltas[i];
lowArgs[i] = values[i] - ck * deltas[i];
}


const lossDiff =
calculateLossForFilters(highArgs, targetColor, targetHSL) -
calculateLossForFilters(lowArgs, targetColor, targetHSL);


for (let i = 0; i < 6; i++) {
const g = (lossDiff / (2 * ck)) * deltas[i];
const ak =
filters[i] / Math.pow(initialLoss + iterationIndex + 1, alpha);
values[i] = fix(values[i] - ak * g, i);
}


const loss = calculateLossForFilters(values, targetColor, targetHSL);
if (loss < bestLoss) {
best = values.slice(0) as Filters;
bestLoss = loss;
}
}
return { filters: best!, loss: bestLoss };


function fix(value: number, idx: number) {
let max = 100;
if (idx === 2 /* saturate */) {
max = 7500;
} else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) {
max = 200;
}


if (idx === 3 /* hue-rotate */) {
if (value > max) {
value %= max;
} else if (value < 0) {
value = max + (value % max);
}
} else if (value < 0) {
value = 0;
} else if (value > max) {
value = max;
}
return value;
}
}


function solveInitial(): ColorFilterSolveResult {
const A = 5;
const c = 15;
const a: Filters = [60, 180, 18000, 600, 1.2, 1.2];


let best: ColorFilterSolveResult = {
loss: Infinity,
filters: [0, 0, 0, 0, 0, 0],
};
for (let i = 0; best.loss > 25 && i < 3; i++) {
const initial: Filters = [50, 20, 3750, 50, 100, 100];
const result = findColorFilters(A, a, c, initial, 1000);
if (result.loss < best.loss) {
best = result;
}
}
return best;
}


const result = improveInitialSolveResult(solveInitial());


return convertFiltersListToCSSFilter(result.filters)
}

我还使用了npmcolor包,所以主函数将接受几乎任何有效的颜色输入字符串(十六进制,RGB等)。


我想补充几点:

  • 你可能想添加一些可靠的缓存层。
  • 如果您需要滤镜的颜色数量有限,那么为它们“预先计算”滤镜并对这些颜色进行一些硬编码映射可能是个好主意,这样最终用户就永远不必为它们运行计算,因为它们非常繁重

这是我的缓存层。


const colorFiltersCache = new Map<string, string>();


export function cachedSolveColor(input: string) {
const existingResult = colorFiltersCache.get(input);


if (existingResult) {
return existingResult;
}


const newResult = solveColor(input);


colorFiltersCache.set(input, newResult);


return newResult;
}

为了扩展大卫·多斯塔尔斯SCSS混合,我删除了opacity参数并更新了语法以匹配新的Sass划分语法

删除不透明度参数并直接从颜色值获取不透明度允许我获取任何给定的十六进制/RGBA颜色(例如,从Sass变量)并相应地应用滤镜。

@use "sass:math";


@mixin recolor($color: #000) {
$r: math.div(red($color), 255);
$g: math.div(green($color), 255);
$b: math.div(blue($color), 255);
$a: alpha($color);
 

// grayscale fallback if SVG from data url is not supported
$lightness: lightness($color);
filter: saturate(0%) brightness(0%) invert($lightness) opacity($a);


// color filter
$svg-filter-id: "recolor";
filter: url('data:image/svg+xml;utf8,\
<svg xmlns="http://www.w3.org/2000/svg">\
<filter id="#{$svg-filter-id}" color-interpolation-filters="sRGB">\
<feColorMatrix type="matrix" values="\
0 0 0 0 #{$r}\
0 0 0 0 #{$g}\
0 0 0 0 #{$b}\
0 0 0 #{$a} 0\
"/>\
</filter>\
</svg>\
##{$svg-filter-id}');
}
// applied with
@include recolor($arbitrary-color);

"use strict";


class Color {
constructor(r, g, b) { this.set(r, g, b); }
toString() { return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; }


set(r, g, b) {
this.r = this.clamp(r);
this.g = this.clamp(g);
this.b = this.clamp(b);
}


hueRotate(angle = 0) {
angle = angle / 180 * Math.PI;
let sin = Math.sin(angle);
let cos = Math.cos(angle);


this.multiply([
0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
]);
}


grayscale(value = 1) {
this.multiply([
0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value)
]);
}


sepia(value = 1) {
this.multiply([
0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value),
0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value),
0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value)
]);
}


saturate(value = 1) {
this.multiply([
0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value
]);
}


multiply(matrix) {
let newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
let newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
let newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
this.r = newR; this.g = newG; this.b = newB;
}


brightness(value = 1) { this.linear(value); }
contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }


linear(slope = 1, intercept = 0) {
this.r = this.clamp(this.r * slope + intercept * 255);
this.g = this.clamp(this.g * slope + intercept * 255);
this.b = this.clamp(this.b * slope + intercept * 255);
}


invert(value = 1) {
this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
}


hsl() { // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
let r = this.r / 255;
let g = this.g / 255;
let b = this.b / 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;


if(max === min) {
h = s = 0;
} else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
} h /= 6;
}


return {
h: h * 100,
s: s * 100,
l: l * 100
};
}


clamp(value) {
if(value > 255) { value = 255; }
else if(value < 0) { value = 0; }
return value;
}
}


class Solver {
constructor(target) {
this.target = target;
this.targetHSL = target.hsl();
this.reusedColor = new Color(0, 0, 0); // Object pool
}


solve() {
let result = this.solveNarrow(this.solveWide());
return {
values: result.values,
loss: result.loss,
filter: this.css(result.values)
};
}


solveWide() {
const A = 5;
const c = 15;
const a = [60, 180, 18000, 600, 1.2, 1.2];


let best = { loss: Infinity };
for(let i = 0; best.loss > 25 && i < 3; i++) {
let initial = [50, 20, 3750, 50, 100, 100];
let result = this.spsa(A, a, c, initial, 1000);
if(result.loss < best.loss) { best = result; }
} return best;
}


solveNarrow(wide) {
const A = wide.loss;
const c = 2;
const A1 = A + 1;
const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
return this.spsa(A, a, c, wide.values, 500);
}


spsa(A, a, c, values, iters) {
const alpha = 1;
const gamma = 0.16666666666666666;


let best = null;
let bestLoss = Infinity;
let deltas = new Array(6);
let highArgs = new Array(6);
let lowArgs = new Array(6);


for(let k = 0; k < iters; k++) {
let ck = c / Math.pow(k + 1, gamma);
for(let i = 0; i < 6; i++) {
deltas[i] = Math.random() > 0.5 ? 1 : -1;
highArgs[i] = values[i] + ck * deltas[i];
lowArgs[i]  = values[i] - ck * deltas[i];
}


let lossDiff = this.loss(highArgs) - this.loss(lowArgs);
for(let i = 0; i < 6; i++) {
let g = lossDiff / (2 * ck) * deltas[i];
let ak = a[i] / Math.pow(A + k + 1, alpha);
values[i] = fix(values[i] - ak * g, i);
}


let loss = this.loss(values);
if(loss < bestLoss) { best = values.slice(0); bestLoss = loss; }
} return { values: best, loss: bestLoss };


function fix(value, idx) {
let max = 100;
if(idx === 2 /* saturate */) { max = 7500; }
else if(idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; }


if(idx === 3 /* hue-rotate */) {
if(value > max) { value = value % max; }
else if(value < 0) { value = max + value % max; }
} else if(value < 0) { value = 0; }
else if(value > max) { value = max; }
return value;
}
}


loss(filters) { // Argument is array of percentages.
let color = this.reusedColor;
color.set(0, 0, 0);


color.invert(filters[0] / 100);
color.sepia(filters[1] / 100);
color.saturate(filters[2] / 100);
color.hueRotate(filters[3] * 3.6);
color.brightness(filters[4] / 100);
color.contrast(filters[5] / 100);


let colorHSL = color.hsl();
return Math.abs(color.r - this.target.r)
+ Math.abs(color.g - this.target.g)
+ Math.abs(color.b - this.target.b)
+ Math.abs(colorHSL.h - this.targetHSL.h)
+ Math.abs(colorHSL.s - this.targetHSL.s)
+ Math.abs(colorHSL.l - this.targetHSL.l);
}


css(filters) {
function fmt(idx, multiplier = 1) { return Math.round(filters[idx] * multiplier); }
return `filter: invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%);`;
}
}


$("button.execute").click(() => {
let rgb = $("input.target").val().split(",");
if (rgb.length !== 3) { alert("Invalid format!"); return; }


let color = new Color(rgb[0], rgb[1], rgb[2]);
let solver = new Solver(color);
let result = solver.solve();


let lossMsg;
if (result.loss < 1) {
lossMsg = "This is a perfect result.";
} else if (result.loss < 5) {
lossMsg = "The is close enough.";
} else if(result.loss < 15) {
lossMsg = "The color is somewhat off. Consider running it again.";
} else {
lossMsg = "The color is extremely off. Run it again!";
}


$(".realPixel").css("background-color", color.toString());
$(".filterPixel").attr("style", result.filter);
$(".filterDetail").text(result.filter);
$(".lossDetail").html(`Loss: ${result.loss.toFixed(1)}. <b>${lossMsg}</b>`);
});
.pixel {
display: inline-block;
background-color: #000;
width: 50px;
height: 50px;
}


.filterDetail {
font-family: "Consolas", "Menlo", "Ubuntu Mono", monospace;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>


<input class="target" type="text" placeholder="r, g, b" value="250, 150, 50" />
<button class="execute">Compute Filters</button>


<p>Real pixel, color applied through CSS <code>background-color</code>:</p>
<div class="pixel realPixel"></div>


<p>Filtered pixel, color applied through CSS <code>filter</code>:</p>
<div class="pixel filterPixel"></div>


<p class="filterDetail"></p>
<p class="lossDetail"></p>

@David Dostal的回答和@Silvan的回答是很好的解决方案,不像其他一些答案那样存在丢失问题。然而,我想用LESS代替Sass来使用它,所以我转换了mixin.如果有人感兴趣,这里是较少的版本:

.recolor(@color: #333) {
@r: red(@color) / 255;
@g: green(@color) / 255;
@b: blue(@color) / 255;
@a: alpha(@color);
  

// grayscale fallback if SVG from data url is not supported
@lightness: lightness(@color);
filter: saturate(0%) brightness(0%) invert(@lightness) opacity(@a);
  

// color filter
@svg-filter-id: "recolor";
filter: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><filter id="@{svg-filter-id}" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="0 0 0 0 @{r} 0 0 0 0 @{g} 0 0 0 0 @{b} 0 0 0 @{a} 0"/></filter></svg> #@{svg-filter-id}');
}

用法示例:

.icon-green {
.recolor(rgba(0, 250, 134, 0.8));
}