Simply paste your color (as a hex code) into the text input, then click "compute filters". This will convert your hex color into a css color, and compare it with the "real color". Then, just copy and paste the CSS code as needed.
Use the CSS filter property to convert an image to black and white.
You can use filter: -webkit-filter: grayscale(1) invert(1); filter: grayscale(1) invert(1); Or just use invert(1) instead of grayscale(1) invert(1) if you have black and white image.
@Dave was the first to post an answer to this (with working code), and his answer has been an invaluable source of shameless copy and pasting inspiration to me. This post began as an attempt to explain and refine @Dave's
answer, but it has since evolved into an answer of its own.
My method is significantly faster. According to a jsPerf benchmark on randomly generated RGB colors, @Dave's algorithm runs in 600 ms, while mine runs in 30 ms. This can definitely matter, for instance in load time, where speed is critical.
Furthermore, for some colors, my algorithm performs better:
rgb(0,255,0)
, @Dave's produces rgb(29,218,34)
and mine produces rgb(1,255,0)
rgb(0,0,255)
, @Dave's produces rgb(37,39,255)
and mine produces rgb(5,6,255)
rgb(19,11,118)
, @Dave's produces rgb(36,27,102)
and mine produces 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.css;
We'll begin with some 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)}%);`;
}
}
Explanation:
Color
class represents a RGB color.
toString()
function returns the color in a CSS rgb(...)
color string.hsl()
function returns the color, converted to HSL.clamp()
function ensures that a given color value is within bounds (0-255).Solver
class will attempt to solve for a target color.
css()
function returns a given filter in a CSS filter string.grayscale()
, sepia()
, and saturate()
The heart of CSS/SVG filters are filter primitives, which represent low-level modifications to an image.
The filters grayscale()
, sepia()
, and saturate()
are implemented by the filter primative <feColorMatrix>
, which performs matrix multiplication between a matrix specified by the filter (often dynamically generated), and a matrix created from the color. Diagram:
There are some optimizations we can make here:
1
. There is no point of calculating or storing it.A
) either, since we are dealing with RGB, not RGBA.<feColorMatrix>
filters leave columns 4 and 5 as zeroes. Therefore, we can further reduce the filter matrix to 3x3.Implementation:
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;
}
(We use temporary variables to hold the results of each row multiplication, because we do not want changes to this.r
, etc. affecting subsequent calculations.)
Now that we have implemented <feColorMatrix>
, we can implement grayscale()
, sepia()
, and saturate()
, which simply invoke it with a given filter matrix:
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()
The hue-rotate()
filter is implemented by <feColorMatrix type="hueRotate" />
.
The filter matrix is calculated as shown below:
For instance, element a00 would be calculated like so:
Notes:
Math.sin()
or Math.cos()
.Math.sin(angle)
and Math.cos(angle)
should be computed once and then cached.Implementation:
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()
and contrast()
The brightness()
and contrast()
filters are implemented by <feComponentTransfer>
with <feFuncX type="linear" />
.
Each <feFuncX type="linear" />
element accepts a slope and intercept attribute. It then calculates each new color value through a simple formula:
value = slope * value + intercept
This is easy to implement:
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);
}
Once this is implemented, brightness()
and contrast()
can be implemented as well:
function brightness(value = 1) { this.linear(value); }
function contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
invert()
The invert()
filter is implemented by <feComponentTransfer>
with <feFuncX type="table" />
.
The spec states:
In the following, C is the initial component and C' is the remapped component; both in the closed interval [0,1].
For "table", the function is defined by linear interpolation between values given in the attribute tableValues. The table has n + 1 values (i.e., v0 to vn) specifying the start and end values for n evenly sized interpolation regions. Interpolations use the following formula:
For a value C find k such that:
k / n ≤ C < (k + 1) / n
The result C' is given by:
C' = vk + (C - k / n) * n * (vk+1 - vk)
An explanation of this formula:
invert()
filter defines this table: [value, 1 - value]. This is tableValues or v.Thus, we can simplify the formula to:
C' = v0 + C * (v1 - v0)
Inlining the table's values, we are left with:
C' = value + C * (1 - value - value)
One more simplification:
C' = value + C * (1 - 2 * value)
The spec defines C and C' to be RGB values, within the bounds 0-1 (as opposed to 0-255). As a result, we must scale down the values before computation, and scale them back up after.
Thus we arrive at our implementation:
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's code generates 176,660 filter combinations, including:
invert()
filters (0%, 10%, 20%, ..., 100%)sepia()
filters (0%, 10%, 20%, ..., 100%)saturate()
filters (5%, 10%, 15%, ..., 100%)hue-rotate()
filters (0deg, 5deg, 10deg, ..., 360deg)It calculates filters in the following order:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);
It then iterates through all computed colors. It stops once it has found a generated color within tolerance (all RGB values are within 5 units from the target color).
However, this is slow and inefficient. Thus, I present my own answer.
First, we must define a loss function, that returns the difference between the color produced by a filter combination, and the target color. If the filters are perfect, the loss function should return 0.
We will measure color difference as the sum of two metrics:
hue-rotate()
, saturation correlates with saturate()
, etc.) This guides the algorithm.The loss function will take one argument – an array of filter percentages.
We will use the following filter order:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg) brightness(e%) contrast(f%);
Implementation:
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);
}
We will try to minimize the loss function, such that:
loss([a, b, c, d, e, f]) = 0
The SPSA algorithm (website, more info, paper, implementation paper, reference code) is very good at this. It was designed to optimize complex systems with local minima, noisy/nonlinear/ multivariate loss functions, etc. It has been used to tune chess engines. And unlike many other algorithms, the papers describing it are actually comprehensible (albeit with great effort).
Implementation:
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;
}
}
I made some modifications/optimizations to SPSA:
deltas
, highArgs
, lowArgs
), instead of recreating them with each iteration.fix
function after each iteration. It clamps all values to between 0% and 100%, except saturate
(where the maximum is 7500%), brightness
and contrast
(where the maximum is 200%), and hueRotate
(where the values are wrapped around instead of clamped).I use SPSA in a two-stage process:
Implementation:
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);
}
Warning: Do not mess with the SPSA code, especially with its constants, unless you are sure you know what you are doing.
The important constants are A, a, c, the initial values, the retry thresholds, the values of max
in fix()
, and the number of iterations of each stage. All of these values were carefully tuned to produce good results, and randomly screwing with them will almost definitely reduce the usefulness of the algorithm.
If you insist on altering it, you must measure before you "optimize".
First, apply this patch.
Then run the code in Node.js. After quite some time, the result should be something like this:
Average loss: 3.4768521401985275
Average time: 11.4915ms
Now tune the constants to your heart's content.
Some tips:
--debug
flag if you want to see the result of each iteration.This was quite a trip down the rabbit hole but here it is!
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>
EDIT: This solution is not intended for production use and only illustrates an approach that can be taken to achieve what OP is asking for. As is, it is weak in some areas of the color spectrum. Better results can be achieved by more granularity in the step iterations or by implementing more filter functions for reasons described in detail in @MultiplyByZer0's answer.
EDIT2: OP is looking for a non brute force solution. In that case it's pretty simple, just solve this equation:
where
a = hue-rotation
b = saturation
c = sepia
d = invert
Note : OP asked me to undelete, but the bounty shall go to Dave's answer.
I know it's not what was asked in the body of the question, and certainly not what we were all waiting for, but there is one CSS filter which does exactly this :
drop-shadow()
Caveats :
/* 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>
You can make this all very simple by just using a SVG filter referenced from CSS. You only need a single feColorMatrix to do a recolor. This one recolors to yellow. The fifth column in the feColorMatrix holds the RGB target values on the unit scale. (for yellow - it's 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">
I started with this answer using a svg filter and made the following modifications:
If you don't want to define the SVG filter somewhere in your the markup, you can use a data url instead (replace R, G, B and A with the desired color):
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');
If the version above does not work, you could also add a grayscale fallback.
The saturate
and brightness
functions turn any color to black (you don't have to include that if the color is already black), invert
then brightens it with the desired lightness (L) and optionally you can also specify the opacity (A).
filter: saturate(0%) brightness(0%) invert(L) opacity(A);
If you want to specify the color dynamically, you could use the following 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);
}
hue-rotate
.I noticed that the example of the treatment via an SVG filter was incomplete, I wrote mine (which works perfectly): (see Michael Mullany answer) so here is the way to get any color you want :
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>
Here is a second solution, by using SVG Filter only in code => 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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With