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

我的问题是:给定一个目标RGB颜色,什么是只使用CSS滤镜将黑色( #000 )重新着色成该颜色的公式?

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

上下文是需要在background-image重新着色SVG。 在这种情况下,它将支持KaTeX中的某些TeXmath特性: https : //github.com/Khan/KaTeX/issues/587 。

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

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

( 演示 )

非目标

  • animation。
  • 非CSSfilter解决scheme。
  • 从黑色以外的颜色开始。
  • 关心黑色以外的颜色。

目前为止的结果

  • 蛮力search固定筛选器列表的参数: https : //stackoverflow.com/a/43959856/181228
    缺点:效率低下,只产生一些16,777,216种可能的颜色(676,248, hueRotateStep=1 )。

  • 更快的search解决scheme使用SPSA : https : //stackoverflow.com/a/43960991/181228 赏金颁发

  • drop-shadow解决scheme: https : //stackoverflow.com/a/43959853/181228
    缺点:不能在Edge上工作。 需要非filter CSS更改和小的HTML更改。

你仍然可以通过提交一个非暴力的解决scheme得到一个接受的答案!

资源

  • 如何计算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-rotatefunction非线性。

    浏览器实现: Chromium , Firefox 。

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

  • 几乎可以工作的公式:
    https://stackoverflow.com/a/29958459/181228

  • 关于为什么上面的公式是错误的详细解释(CSS hue-rotate不是一个真正的色调旋转,但一个线性近似):
    https://stackoverflow.com/a/19325417/2441511

这是一个相当顺利的兔子洞,但在这里!

 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> 

@Dave是第一个发表这个答案的人(与工作代码),他的回答是无耻复制和粘贴灵感的宝贵来源。 这篇文章是为了解释和提炼戴夫的答案而开始的,但是现在已经演变成了自己的答案。

我的方法明显更快。 根据随机生成的RGB颜色的jsPerf基准 ,@ Davealgorithm运行时间为600 ms ,而我的运行时间为30 ms 。 这可以绝对重要,例如在加载时间,速度至关重要。

此外,对于某些颜色,我的algorithmperformance更好:

  • 对于rgb(0,255,0) ,@Dave's生成rgb(29,218,34)并生成rgb(1,255,0)
  • 对于rgb(0,0,255) ,@ Dave's产生rgb(37,39,255)而我的产生rgb(5,6,255)
  • rgb(19,11,118) ,@ Dave's生产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> 

注意: OP要求我取消删除 ,但赏金应该去Dave的回答。


我知道这不是什么问题的主体,当然不是我们都在等待,但有一个CSSfilter正是这样做: drop-shadow()

注意事项:

  • 阴影被绘制在现有的内容之后。 这意味着我们必须做出一些绝对的定位技巧。
  • 所有像素将被视为相同,但OP说[我们不应该] “关心黑色以外的颜色会发生什么”。
  • 浏览器支持。 (I'm not sure about it, tested only under latests FF and 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,PHN=); } 
 <div class="icon"> <span></span> </div> <div class="icon green"> <span></span> </div> <div class="icon red"> <span></span> </div> 
Interesting Posts