Javascript 如何仅使用CSS过滤器将黑色转换为任何给定颜色
我的问题是:给定一个目标RGB颜色,仅使用该颜色将黑色(Javascript 如何仅使用CSS过滤器将黑色转换为任何给定颜色,javascript,css,math,algebra,css-filters,Javascript,Css,Math,Algebra,Css Filters,我的问题是:给定一个目标RGB颜色,仅使用该颜色将黑色(#000)重着色为该颜色的公式是什么 要接受答案,它需要提供一个函数(任何语言),该函数将接受目标颜色作为参数,并返回相应的CSSfilter字符串 其背景是需要在背景图像中重新存储SVG。在这种情况下,它将支持KaTeX中的某些TeX数学功能: 例子 如果目标颜色为#ffff00(黄色),则正确的解决方案是: filter: invert(100%) sepia() saturate(10000%) hue-rotate(0deg) (
#000
)重着色为该颜色的公式是什么
要接受答案,它需要提供一个函数(任何语言),该函数将接受目标颜色作为参数,并返回相应的CSSfilter
字符串
其背景是需要在背景图像
中重新存储SVG。在这种情况下,它将支持KaTeX中的某些TeX数学功能:
例子
如果目标颜色为#ffff00
(黄色),则正确的解决方案是:
filter: invert(100%) sepia() saturate(10000%) hue-rotate(0deg)
()
非目标
- 动画
- 非CSS过滤器解决方案
- 从黑色以外的颜色开始
- 关心黑色以外的颜色会发生什么
- 强制搜索固定筛选器列表的参数:
缺点:效率低下,仅生成16777216种可能的颜色中的一些(676248,带有
)hueRotateStep=1
- 更快的搜索解决方案使用: 获得赏金
- A
解决方案:投阴影
缺点:在边缘不起作用。需要非过滤器CSS更改和较小的HTML更改
- 如何计算
和色调旋转
: 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
使夹具
功能非线性 浏览器实现:色调旋转
- 演示:从灰度颜色获取非灰度颜色:
- 几乎有效的公式(来自A):
以上公式错误原因的详细解释(CSS
不是真正的色调旋转,而是线性近似):hue rotate
我知道这不是问题主体中提出的问题,当然也不是我们都在等待的问题,但有一个CSS过滤器正是这样做的: 注意事项:
- 阴影绘制在现有内容的后面。这意味着我们必须做一些绝对定位技巧
- 所有像素都将被同等对待,但OP说[我们不应该]“关心黑色以外的颜色会发生什么。”
- 浏览器支持。(我不确定,只在最新的FF和chrome下测试)
/*用于隐藏原始bg的容器*/
.图标{
宽度:60px;
高度:60px;
溢出:隐藏;
}
/*内容*/
.icon.green>span{
-webkit过滤器:投阴影(60px 0px绿色);
滤镜:投影(60px 0px绿色);
}
.icon.red>span{
-webkit过滤器:投阴影(60px 0px红色);
滤镜:投影(60px 0px红色);
}
.icon>span{
-webkit过滤器:投阴影(60px 0px黑色);
滤镜:投影(60px 0px黑色);
背景位置:-100%0;
左边距:-60px;
显示:块;
宽度:61px;/*+1px用于chrome bug*/
高度:60px;
背景图片:url数据:数据:图像/svg+xml;Bas64,基础64,基础64,基础6,基础4,基础4,基础4,基础4,基础4,基础4,基础4,基础4,基础4,基础4,基础4,基础3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,30IZXZLBM9KZCIGZD0ITTYXLJUXMSWYNI4XNWMTMC43MTQTMS43MZGTMS43MJMTMY4YOTGMY4WMJYTNC42NZKGICbjLtzYLtZY2LtU5Y0Lc1Nc0WlJymi0CxYWlJI4Oc0LtZGTMS43MJMY4Y4YMY4Y4Y4Y0WlZGY4LtKg2LtKg2LtKg2LtKg2LtUg2LtUg2Lt0Lt0Lt0LtUg0LtUg0LtUg0LtUg0LtUg0LtUg0LtUtUtUtUtUtUtUtUtUtUtUtUtUtUtUtUtUtUtU未经批准的ZMJ3Icagy0xLJCWOCWWLJGWWnY0ZLJIXmiWxlJG5MY00LJUXNYWZLJI1OmTMS4ZMTGSM4ZMjCs45NdGtmy4WmJYSNC43MdJ2LtauMDizy0WlJGxWn0xLjexOcWnZLjCxWnXn0xLjexOcWlJWmIcWmS4WmS4WmJmS41MzCsNzCsNzNzNqS4xNzMmWmWmS4WmZmWmWmJ4ZmWmWmWmWmJ4YWmWmWmWmJ4WmWmWmWmWmWmWmWmWmWmWmWmWmWmWmWmWmWmWmWmWmWMC4WntusMC4WmzKsmc4Wnzesmc4Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn2Wn4Wn2Wn2Wn2Wn4Wn4Wn4Wn2Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4Wn4WnJQ5LdeumTQ4LdaumZczlDeuzy5LdaumZczadCumJg3Yzaunjismcwxljc2o0Wljm3my0WljizmSwxly0Wlju1oCwxly0Wl3n2MWlJQxOc0WlqxOwljc0WlJKsmc45NZgtmS40NjmJmmc4NjmDdG4nzzzmzzmzzzzmzzm4znznzzzzzzmzzzzzzmznznzmzzmzzzzznzzzzmzzzmzmzzzzmzznzzzmzmzzmzzzzzmzmzmzzzzmzzzmznznzzmzmznzmzmzzzWWLJI2NC0WLJQ3MYWWLJQ2NI0WLJCYMNYTMC4WMGICBJMC4XODCTMC4YMZMSC40MDMC40NJYSMC42OTLJMC4WMTYTMC4WMTYSMC4WMZETMC4WMWMWWYWYJ0NMMWYJ3NC0WLJYWWNSWXLJEWMY0xLJIXLDEUZULTE2CAGZUYZU2YZUZUZUZUZUZUZUZUZUZUZUZUZUZUZUZUZUZYKYYK4ZUZUZYKYKYKYKZUZUZUZYKYYKYKYKZYKZUZYKZYKZYKZYKZYKZYKZYKZYKZYKZYKZYKZYKZYKYKYKYKYXLJC4NSW0MY4XDJJMCW2LJG3NS0XLJC1YWXMY4WMI01JI2MSWXOC40MZZZLKXLDYLDYUMDYLDYYYY0Y0Y0Y0Y0Y0Y0Y0Y0Y0Y0XMC4XOC4XOTUTMJKY0DY0DY2DEWY0L5NSAGGINGNI40NTCSMCM0xMY0Y0Y0Y0Y0Y0LJ0ZLKY0LJ0ZLJ0ZLJ0LKY0LKY0LY0LY0DY0DY0LY0DY0DY0LY0DY0LY0DY0LY0LJ0LY0LJ0LZZZY0LK0LZZY0LY0DY0LY0LY0LY0LY0LY0LLTEUMDQTMC4ZNDITMS40NDMTMC43NDVJLTAUNDAULTAUNZUYY0WLJA5MY0WLJU2LTUYYY0WLJA5MY0WLJU2LTAUMDIZMT42MDVSNY42NTGTMYUMJCxICAGYZAUMTQTMC41NZQSMC41ODMC43OTJJJJLK3LTZUYK2LLTEZLJ4OC00L5NI0NY43ODRJ0LJJJJYYWZY3NI0NYK4MDJ044LJJJJ0YYZZZYYZZZZLZLZYM4MJJ0YYZZZZYYMJ0MZZZZZZZYYMJ0MJ0LZLZZYYYYZZZZLZLZLZLZLZLZZZ
fill: #000000
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)
}
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;
}
@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);