如何仅使用 CSS 过滤器将黑色转换为任何给定的颜色
Posted
技术标签:
【中文标题】如何仅使用 CSS 过滤器将黑色转换为任何给定的颜色【英文标题】:How to transform black into any given color using only CSS filters 【发布时间】:2017-08-15 10:51:17 【问题描述】:我的问题是:给定目标 RGB 颜色,仅使用 CSS filters 将黑色 (#000
) 重新着色为该颜色的公式是什么?
要让答案被接受,它需要提供一个函数(任何语言),接受目标颜色作为参数并返回相应的 CSS filter
字符串。
上下文是需要在background-image
中重新着色 SVG。在这种情况下,它是为了支持 KaTeX 中的某些 TeX 数学功能:https://github.com/Khan/KaTeX/issues/587。
示例
如果目标颜色是#ffff00
(黄色),一个正确的解决方案是:
filter: invert(100%) sepia() saturate(10000%) hue-rotate(0deg)
(demo)
非进球
动画。 非 CSS 过滤器解决方案。 从黑色以外的颜色开始。 关心黑色以外的颜色会发生什么。目前的结果
暴力搜索固定过滤器列表的参数:https://***.com/a/43959856/181228
缺点:效率低下,仅生成 16,777,216 种可能颜色中的一部分(hueRotateStep=1
为 676,248)。
使用SPSA 的更快搜索解决方案: https://***.com/a/43960991/181228 赏金奖励
drop-shadow
解决方案:
https://***.com/a/43959853/181228
缺点:不适用于 Edge。需要非filter
CSS 更改和较小的 html 更改。
您仍然可以通过提交非暴力解决方案获得接受答案!
资源
如何计算hue-rotate
和sepia
:
https://***.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
函数成为非线性函数。
浏览器实现:Chromium、Firefox。
演示:从灰度颜色到非灰度颜色: https://***.com/a/25524145/181228
几乎有效的公式(来自similar question):https://***.com/a/29958459/181228
上面的公式为什么错误的详细解释(CSS hue-rotate
不是真正的色调旋转而是线性近似):https://***.com/a/19325417/2441511
【问题讨论】:
所以你想 LERP #000000 到 #RRGGBB 吗? (只是澄清) 是的,甜蜜 - 只是澄清您不想将过渡合并到解决方案中。 混合模式可能适合您吗?您可以轻松地将黑色转换为任何颜色......但我没有得到您想要实现的全局图片 @glebm 所以你需要找到一个公式(使用任何方法)将黑色变成任何颜色并使用 css 应用它? @ProllyGeek 是的。我应该提到的另一个约束是,生成的公式不能是 5GiB 表的蛮力查找(它应该可以从例如网页上的 javascript 中使用)。 【参考方案1】:@Dave 是第一个在此发布an answer(带有工作代码)的人,他的回答对我来说是无耻复制和粘贴灵感的宝贵来源。这篇文章一开始是为了解释和完善@Dave's
答案,但它已经演变成自己的答案。
我的方法明显更快。根据jsPerf benchmark 随机生成的 RGB 颜色,@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://***.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.css;
说明
我们将从一些 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://***.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()
函数返回 CSS rgb(...)
颜色字符串中的颜色。
它的hsl()
函数返回颜色,转换为HSL。
它的clamp()
函数确保给定的颜色值在范围内 (0-255)。
Solver
类将尝试求解目标颜色。
它的 css()
函数在 CSS 过滤器字符串中返回给定的过滤器。
实现grayscale()
、sepia()
和saturate()
CSS/SVG 过滤器的核心是 filter primitives,它表示对图像的低级修改。
过滤器grayscale()
、sepia()
和saturate()
由过滤器原语<feColorMatrix>
实现,它在过滤器指定的矩阵(通常动态生成)和从颜色。图:
我们可以在这里进行一些优化:
颜色矩阵的最后一个元素是并且将永远是1
。没有必要计算或存储它。
也没有必要计算或存储 Alpha/透明度值 (A
),因为我们处理的是 RGB,而不是 RGBA。
因此,我们可以将过滤矩阵从 5x5 修剪到 3x5,将颜色矩阵从 1x5 修剪到 1x3。这样可以节省一些工作量。
所有 <feColorMatrix>
过滤器将第 4 列和第 5 列保留为零。 因此,我们可以进一步将过滤器矩阵缩小到 3x3。
由于乘法比较简单,所以没有必要为此拖入complex math libraries。我们可以自己实现矩阵乘法算法。
实施:
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()
过滤器由 <feComponentTransfer>
和 <feFuncX type="linear" />
实现。
每个<feFuncX type="linear" />
元素都接受一个slope 和intercept 属性。然后它通过一个简单的公式计算每个新的颜色值:
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()
过滤器由<feComponentTransfer>
和<feFuncX type="table" />
实现。
规范规定:
以下,C是初始组件,C'是重新映射的组件;都在闭区间 [0,1] 内。
对于“table”,该函数由属性tableValues中给出的值之间的线性插值定义。该表有 n + 1 个值(即 v0 到 vn)指定 n 的开始和结束值em> 大小均匀的插值区域。插值使用以下公式:
对于一个值 C 找到 k 使得:
k / n ≤ C
结果C'由下式给出:
C' = vk + (C - k / n) * n * (vk+1 - vk)
这个公式的解释:
invert()
过滤器定义了这个表:[value, 1 - value]。这是 tableValues 或 v。
公式定义n,使得n + 1 是表格的长度。由于表格的长度为 2,n = 1。
公式定义k,k和k+1是表的索引。由于表格有 2 个元素,k = 0。
因此,我们可以将公式简化为:
C' = v0 + C * (v1 - v0)
内联表格的值,我们剩下:
C' = 值 + C * (1 - 值 - 值)
进一步简化:
C' = 值 + C * (1 - 2 * 值)
规范将 C 和 C' 定义为 RGB 值,范围为 0-1(而不是 0-255)。因此,我们必须在计算前按比例缩小值,然后再按比例放大。
这样我们就实现了:
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%)
11 sepia()
过滤器(0%、10%、20%、...、100%)
20 saturate()
过滤器(5%、10%、15%、...、100%)
73 hue-rotate()
过滤器(0°、5°、10°、...、360°)
它按以下顺序计算过滤器:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);
然后它遍历所有计算的颜色。一旦发现生成的颜色在公差范围内(所有 RGB 值都在目标颜色的 5 个单位以内),它就会停止。
但是,这是缓慢且低效的。因此,我提出自己的答案。
实施 SPSA
首先,我们必须定义一个loss function,它返回过滤器组合产生的颜色与目标颜色之间的差异。如果过滤器是完美的,损失函数应该返回 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
SPSA 算法(website、more info、paper、implementation paper、reference code)非常擅长这一点。它旨在优化具有局部最小值、噪声/非线性/多元损失函数等的复杂系统。It has been used to tune chess engines。而且与许多其他算法不同,描述它的论文实际上是可以理解的(尽管付出了很大的努力)。
实施:
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 做了一些修改/优化:
使用产生的最佳结果,而不是最后一个。 重用所有数组(deltas
、highArgs
、lowArgs
),而不是在每次迭代时重新创建它们。
a 使用值数组,而不是单个值。这是因为所有过滤器都不同,因此它们应该以不同的速度移动/收敛。
在每次迭代后运行fix
函数。它将所有值限制在 0% 和 100% 之间,除了 saturate
(最大值为 7500%)、brightness
和 contrast
(最大值为 200%)和 hueRotate
(其中值是缠绕而不是夹住)。
我在两阶段过程中使用 SPSA:
-
“宽”阶段,试图“探索”搜索空间。如果结果不令人满意,它将有限地重试 SPSA。
“窄”阶段,从宽阶段中获取最佳结果并尝试“改进”它。它使用 A 和 a 的动态值。
实施:
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 代码,尤其是其常量,除非您确定您知道自己在做什么。
重要的常量有A、a、c,初始值,重试阈值,max
在@中的值987654440@,以及每个阶段的迭代次数。所有这些值都经过仔细调整以产生良好的结果,随意乱用它们几乎肯定会降低算法的实用性。
如果你坚持改变它,你必须在“优化”之前测量。
首先,申请this patch。
然后在 Node.js 中运行代码。一段时间后,结果应该是这样的:
Average loss: 3.4768521401985275
Average time: 11.4915ms
现在根据您的需要调整常量。
一些提示:
平均损失应该在 4 左右。如果大于 4,则产生的结果离得太远,您应该调整准确性。如果小于4,就是浪费时间,应该减少迭代次数。 如果增加/减少迭代次数,适当调整A。 如果增加/减少A,适当调整a。 如果您想查看每次迭代的结果,请使用--debug
标志。
TL;DR
【讨论】:
非常好的开发过程总结!你在读我的想法吗?! 很好的答案! Implementation in this codepen 这是一个完全疯狂的方法。您可以使用 SVG 过滤器(feColorMatrix 中的第五列)直接设置颜色,并且可以从 CSS 中引用该过滤器 - 为什么不使用该方法? @MichaelMullany 好吧,考虑到我在这方面工作了多长时间,这让我很尴尬。我没有想到你的方法,但现在我明白了——要将元素重新着色为任意颜色,你只需动态生成一个带有<filter>
的 SVG,其中包含具有正确值的 <feColorMatrix>
(除最后一列外全为零,其中包含目标 RGB 值 0 和 1),将 SVG 插入 DOM,并从 CSS 中引用过滤器。请写下您的解决方案作为答案(带有演示),我会投票。
很棒的答案。【参考方案2】:
这是一次相当不错的兔子洞之旅,但它就在这里!
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's answer中有详细描述。
EDIT2: OP 正在寻找非暴力解决方案。在那种情况下,它很简单,只需解这个方程:
在哪里
a = hue-rotation
b = saturation
c = sepia
d = invert
【讨论】:
如果我输入255,0,255
,我的数字色度计会报告结果为#d619d9
,而不是#ff00ff
。
@Siguza 这绝对不是完美的,边缘情况的颜色可以通过调整循环中的边界来调整。
这个等式绝不是“非常简单”
我认为上面的等式也缺少clamp
?
Clamp 没有位置。根据我大学数学的记忆,这些方程是通过数值计算计算出来的,也就是“蛮力”,祝你好运!【参考方案3】:
注意:OP asked me to undelete,但赏金应归于 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,PHN2ZyBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgOTAgOTAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDkwIDkwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48Zz48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTYxLjUxMSwyNi4xNWMtMC43MTQtMS43MzgtMS43MjMtMy4yOTgtMy4wMjYtNC42NzkgICBjLTEuMzAzLTEuMzY2LTIuODA5LTIuNDUyLTQuNTE1LTMuMjU5Yy0xLjc1NC0wLjgyMi0zLjYwMS0xLjI4OC01LjU0LTEuMzk2Yy0wLjI4LTAuMDMxLTAuNTUyLTAuMDQ3LTAuODE0LTAuMDQ3ICAgYy0wLjAxOCwwLTAuMDMxLDAtMC4wNDcsMGMtMC4zMjcsMC4wMTYtMC41NzQsMC4wMjMtMC43NDUsMC4wMjNjLTEuOTcxLDAuMTA4LTMuODQxLDAuNTc0LTUuNjA5LDEuMzk3ICAgYy0xLjcwOCwwLjgwNy0zLjIxMiwxLjg5My00LjUxNywzLjI1OWMtMS4zMTgsMS4zODEtMi4zMjcsMi45NDgtMy4wMjYsNC43MDJ2LTAuMDIzYy0wLjc0NCwxLjgxNS0xLjExOCwzLjcxNi0xLjExOCw1LjcwMiAgIGMtMC4wMTUsMi4wNjQsMC41MzcsNC4xODIsMS42NTQsNi4zNTVjMC41NzQsMS4xMzMsMS4yOTUsMi4yNSwyLjE2NCwzLjM1MmMwLjQ4MiwwLjYwNSwxLjAwMiwxLjIxLDEuNTYsMS44MTYgICBjMC4wMzEsMC4wMTYsMC4wNTUsMC4wMzksMC4wNzEsMC4wN2MwLjUyNywwLjQ5NiwwLjg5MiwwLjk3OCwxLjA5MywxLjQ0M2MwLjEwOCwwLjIzMywwLjE3OSwwLjUyLDAuMjEsMC44NjIgICBjMC4wNDYsMC4zNzEsMC4wNjksMC44MjIsMC4wNjksMS4zNXYxLjA0OGMwLDAuNjIsMC4xMTcsMS4yMTgsMC4zNDksMS43OTJjMC4yMzQsMC41NDMsMC41NiwxLjAyNCwwLjk3OCwxLjQ0M2gwLjAyNSAgIGMwLjQxOCwwLjQxOSwwLjg5MiwwLjc0NSwxLjQyLDAuOTc3aDAuMDIzYzAuNTU4LDAuMjQ5LDEuMTQ4LDAuMzczLDEuNzY5LDAuMzczaDcuMjg3YzAuNjIsMCwxLjIwOS0wLjEyNCwxLjc2OS0wLjM3MyAgIGMwLjU0My0wLjIzMSwxLjAyMy0wLjU1OCwxLjQ0My0wLjk3N2MwLjQxOC0wLjQxOSwwLjc0My0wLjksMC45NzgtMS40NDNjMC4yNDgtMC41NzQsMC4zNzEtMS4xNzIsMC4zNzEtMS43OTJ2LTEuMDQ4ICAgYzAtMC41MjcsMC4wMjMtMC45NzksMC4wNzEtMS4zNWMwLjAyOS0wLjM0MiwwLjA5Mi0wLjYzNywwLjE4Ni0wLjg4NWMwLjEwOC0wLjIzMywwLjI2NC0wLjQ3MywwLjQ2Ni0wLjcyMnYtMC4wMjMgICBjMC4xODctMC4yMzMsMC40MDMtMC40NjYsMC42NTEtMC42OTljMC4wMTYtMC4wMTYsMC4wMzEtMC4wMywwLjA0Ny0wLjA0NmMwLjU3NC0wLjYwNSwxLjEwMy0xLjIxLDEuNTgzLTEuODE2ICAgYzAuODY4LTEuMTAyLDEuNTkxLTIuMjE5LDIuMTY1LTMuMzUyYzEuMTE3LTIuMTczLDEuNjY3LTQuMjkxLDEuNjUyLTYuMzU1QzYyLjYwNSwyOS44NTksNjIuMjQsMjcuOTY2LDYxLjUxMSwyNi4xNXogICAgTTgxLjc4NSw0My4xNDJjMCw2Ljg3NS0xLjc1MywxMy4wMi01LjI2MSwxOC40MzZjLTEuMzgxLDIuMTQxLTMuMDMyLDQuMTY3LTQuOTU4LDYuMDc1Yy02Ljc1LDYuNzk3LTE0LjkxMywxMC4xOTUtMjQuNDg2LDEwLjE5NSAgIGMtNi40NTcsMC0xMi4yOTItMS41NDQtMTcuNTA1LTQuNjMyYy0wLjI0OSwwLjI5NS0wLjU2LDAuNTI3LTAuOTMyLDAuNjk4bC0xNi4xMzEsNy42NThjLTAuNTEyLDAuMjMzLTEuMDQ3LDAuMzAzLTEuNjA2LDAuMjEgICBjLTAuNTU5LTAuMDk0LTEuMDQtMC4zNDItMS40NDMtMC43NDVjLTAuNDA0LTAuNDAzLTAuNjUyLTAuODg2LTAuNzQ2LTEuNDQzYy0wLjA5My0wLjU2LTAuMDIzLTEuMDk0LDAuMjEtMS42MDVsNy42NTgtMTYuMjcxICAgYzAuMTQtMC4zMTEsMC4zMzQtMC41NzQsMC41ODMtMC43OTJjLTMuMTk3LTUuMjYxLTQuNzk2LTExLjE4OC00Ljc5Ni0xNy43ODRjMC05LjYyMSwzLjM3Ni0xNy44MDcsMTAuMTI1LTI0LjU1OCAgIGMwLjUyOC0wLjUyNywxLjA3MS0xLjA0LDEuNjMtMS41MzZjMi4yMDQtMS45NTYsNC41MzktMy41Nyw3LjAwNi00Ljg0MkMzNS45NDUsOS42OTIsNDEuMjYsOC40MzYsNDcuMDgsOC40MzYgICBjOS41NzMsMCwxNy43MzYsMy4zODIsMjQuNDg2LDEwLjE0OGM2LjQyNiw2LjM3OCw5LjgyNCwxNC4wMjksMTAuMTk1LDIyLjk1MkM4MS43NzgsNDIuMDYzLDgxLjc4NSw0Mi41OTksODEuNzg1LDQzLjE0MnogICAgTTUxLjM4NiwyNS4yNjZjLTAuNzE0LTAuMzI2LTEuNDU5LTAuNTEzLTIuMjM1LTAuNTU5Yy0wLjQ4LTAuMDMxLTAuODc2LTAuMjI1LTEuMTg4LTAuNTgzYy0wLjMxMS0wLjM0LTAuNDU3LTAuNzUyLTAuNDQxLTEuMjMzICAgYzAuMDMxLTAuNDY2LDAuMjI1LTAuODU0LDAuNTgyLTEuMTY1YzAuMzU3LTAuMzEsMC43NjktMC40NTcsMS4yMzQtMC40NDFjMS4yMjYsMC4wNzcsMi4zOTcsMC4zOCwzLjUxNSwwLjkwNyAgIGMxLjA2OSwwLjQ5NywyLjAxOCwxLjE3OSwyLjg0LDIuMDQ5YzAuODA3LDAuODY5LDEuNDM1LDEuODU0LDEuODg0LDIuOTU2YzAuNDY2LDEuMTMzLDAuNjk5LDIuMzIsMC42OTksMy41NjIgICBjMCwwLjQ2NS0wLjE3MSwwLjg2OS0wLjUxMiwxLjIxYy0wLjMyNSwwLjMyNi0wLjcyMiwwLjQ4OS0xLjE4OCwwLjQ4OWMtMC40OCwwLTAuODg0LTAuMTYzLTEuMjEtMC40ODkgICBjLTAuMzQyLTAuMzQxLTAuNTEzLTAuNzQ2LTAuNTEzLTEuMjFjMC0wLjc5Mi0wLjE0Ni0xLjU1Mi0wLjQ0MS0yLjI4MWMtMC4yNzktMC42OTktMC42ODMtMS4zMjctMS4yMTEtMS44ODYgICBTNTIuMDY3LDI1LjU5MSw1MS4zODYsMjUuMjY2eiBNNTcuNzg3LDM1LjM2OGMwLDAuNTEyLTAuMTg4LDAuOTU0LTAuNTYsMS4zMjZjLTAuMzU2LDAuMzU3LTAuOCwwLjUzNi0xLjMyNiwwLjUzNiAgIGMtMC41MTIsMC0wLjk0Ni0wLjE3OS0xLjMwMy0wLjUzNmMtMC4zNzQtMC4zNzItMC41Ni0wLjgxNC0wLjU2LTEuMzI2YzAtMC41MTMsMC4xODYtMC45NTYsMC41Ni0xLjMyNyAgIGMwLjM1Ni0wLjM1NywwLjc5MS0wLjUzNiwxLjMwMy0wLjUzNmMwLjUyNiwwLDAuOTcsMC4xNzgsMS4zMjYsMC41MzZDNTcuNiwzNC40MTMsNTcuNzg3LDM0Ljg1NSw1Ny43ODcsMzUuMzY4eiBNNTEuODk3LDU0LjcxMSAgIEg0My40Yy0wLjcxMiwwLTEuMzE4LDAuMjU2LTEuODE1LDAuNzY5Yy0wLjUxMiwwLjQ5Ny0wLjc2OSwxLjA5NC0wLjc2OSwxLjc5MmMwLDAuNzE0LDAuMjQ5LDEuMzE5LDAuNzQ2LDEuODE1bDAuMDIzLDAuMDI0ICAgYzAuNDk3LDAuNDk2LDEuMTAzLDAuNzQ0LDEuODE1LDAuNzQ0aDguNDk3YzAuNzE1LDAsMS4zMTgtMC4yNDgsMS44MTUtMC43NDRjMC40OTctMC41MTMsMC43NDUtMS4xMjYsMC43NDUtMS44NCAgIGMwLTAuNjk4LTAuMjQ4LTEuMjk1LTAuNzQ1LTEuNzkydi0wLjAyM0M1My4yMDEsNTQuOTU5LDUyLjU5Niw1NC43MTEsNTEuODk3LDU0LjcxMXogTTQyLjcyNiw2Mi40MzhoLTAuMDIzICAgYy0wLjQ5NywwLjQ5Ny0wLjc0NSwxLjEwMy0wLjc0NSwxLjgxNnMwLjI1NywxLjMxOCwwLjc2OSwxLjgxNWMwLjQ5NywwLjQ5NywxLjEwMiwwLjc0NSwxLjgxNiwwLjc0NWg2LjEyMiAgIGMwLjY5NywwLDEuMjk1LTAuMjQ4LDEuNzkyLTAuNzQ1aDAuMDIyYzAuNDk3LTAuNDk3LDAuNzQ2LTEuMTAyLDAuNzQ2LTEuODE1cy0wLjI0OS0xLjMxOS0wLjc0Ni0xLjgxNiAgIGMtMC41MTItMC41MTItMS4xMTctMC43NjgtMS44MTQtMC43NjhoLTYuMTIyQzQzLjgyOCw2MS42NzEsNDMuMjIzLDYxLjkyNyw0Mi43MjYsNjIuNDM4eiIvPjwvZz48L3N2Zz4=);
<div class="icon">
<span></span>
</div>
<div class="icon green">
<span></span>
</div>
<div class="icon red">
<span></span>
</div>
【讨论】:
超级聪明,太棒了!这对我有用,感谢它 我相信这是一个更好的解决方案,因为它每次都能 100% 准确地使用颜色。 代码原样显示空白页 (W10 FF 69b)。不过,图标没有问题(检查单独的 SVG)。 将background-color: black;
添加到.icon>span
使得这项工作适用于FF 69b。但是,不显示图标。
不能在 Safari 中使用! ?【参考方案4】:
只需使用从 CSS 中引用的 SVG 过滤器,您就可以让这一切变得非常简单。您只需要一个 feColorMatrix 即可重新着色。这个重新着色为黄色。 feColorMatrix 中的第五列保存单位比例的 RGB 目标值。 (黄色 - 它是 1,1,0)
.icon
filter: url(#recolorme);
<svg >
<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">
【讨论】:
一个有趣的解决方案,但似乎它不允许通过 CSS 控制目标颜色。 您必须为要应用的每种颜色定义一个新过滤器。但它是完全准确的。 hue-rotate 是一种裁剪某些颜色的近似值 - 这意味着您无法使用它准确地获得某些颜色 - 正如上面的答案所证明的那样。我们真正需要的是一个 recolor() CSS 过滤器简写。 MultiplyByZer0 的答案计算了一系列过滤器,这些过滤器以非常高的精度实现,无需修改 HTML。在浏览器中使用真正的hue-rotate
会很好。
当您将“color-interpolation-filters”="sRGB" 添加到 feColorMatrix 时,这似乎只会为黑色源图像生成准确的 RGB 颜色。
边缘 12-18 被排除在外,因为它们不支持 url
函数 caniuse.com/#search=svg%20filter【参考方案5】:
我从 this answer 开始使用 svg 过滤器并进行了以下修改:
来自数据 url 的 SVG 过滤器
如果您不想在标记中的某处定义 SVG 过滤器,则可以使用 data url 代替(替换 R、G、B 和 A 以及所需的颜色):
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');
灰度后备
如果上面的版本不起作用,您还可以添加灰度后备。
saturate
和 brightness
函数将任何颜色变为黑色(如果颜色已经是黑色,则不必包括该颜色),invert
然后以所需的亮度使其变亮(L) 并且您还可以选择指定不透明度 (A)。
filter: saturate(0%) brightness(0%) invert(L) opacity(A);
SCSS 混合
如果你想动态指定颜色,你可以使用下面的 SCSS mixin:
@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,其用法非常简单(参见上面的示例)。 与棕褐色技巧(纯 CSS 中的 RGBA 组件,您甚至可以在 SCSS 中使用 HEX 颜色)相比,颜色更具可读性且更易于修改。 避免使用weird behavior ofhue-rotate
。
注意事项:
并非所有浏览器都支持来自数据 url(尤其是 id 哈希)的 SVG 过滤器,但它适用于当前的 Firefox 和 Chromium 浏览器(可能还有其他浏览器)。 如果要动态指定颜色,则必须使用 SCSS mixin。 纯 CSS 版本有点丑,如果你想要很多不同的颜色,你必须多次包含 SVG。【讨论】:
哦,太完美了,这正是我正在寻找的,它可以使用 SASS 中的所有内容,非常感谢! @ghiscoding 很高兴它有帮助! 我尝试通过画布的上下文添加它:ctx.filter='url(url()
函数不支持插值。 RGB 值在编译时插入到 mixin 中,因此可以使用 SCSS 变量。而且由于 CSS 变量的值在运行时可用,因此您也无法在编译时从 SCSS 中获取它们的值。因此,您要么必须:从变量中获取颜色,然后在编译时或在运行时将其插入到 url 中,这是不可能的。【参考方案6】:
我注意到通过 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 >
<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>
这是第二种解决方案,仅在代码中使用 SVG 过滤器 => URL.createObjectURL
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>
【讨论】:
【参考方案7】:根据之前的惊人答案,我尝试让代码更易于理解。
我使它更实用,在我有足够信心的地方添加了 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://***.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)
我也在使用 npm
color
包,所以主函数几乎可以接受任何有效的颜色输入字符串(十六进制、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;
【讨论】:
【参考方案8】:为了扩展 David Dostals SCSS Mixin,我删除了 opacity 参数并更新了语法以匹配 new SASS division syntax。
删除 opacity 参数并直接从颜色值中获取 opacity 允许我获取任何给定的 hex/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);
【讨论】:
【参考方案9】:随便用
fill: #000000
CSS 中的fill
属性用于填充SVG 形状的颜色。
fill
属性可以接受任何 CSS 颜色值。
【讨论】:
这可能适用于 SVG 图像内部的 CSS,但它不适用于浏览器外部应用到img
元素的 CSS。以上是关于如何仅使用 CSS 过滤器将黑色转换为任何给定的颜色的主要内容,如果未能解决你的问题,请参考以下文章
如何仅使用在不使用动态SQL的情况下检查的复选框,将WHERE子句设置为在多个位列上进行过滤?