A Pen by Barrett Sonntag on CodePen.
Last active
March 26, 2021 09:15
-
-
Save barretts/e90d7e5251f36b183c67e02ba54c9ae1 to your computer and use it in GitHub Desktop.
CSS filter generator to convert from black to target hex color
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<table> | |
<tr valign="top"> | |
<td width="50%"> | |
<fieldset> | |
<p> | |
<label>Target color</label> <input class="target" type="text" placeholder="target hex" value="#00a4d6"/> | |
</p> | |
<button class="execute">Compute Filters</button> | |
</fieldset> | |
<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> | |
</td> | |
<td> | |
<p>The goal was to be able to create custom style sheets and allow for the coloring of icons</p> | |
<p>For this code to work well the starting color needs to be black. If your icon set isn't black you can prepend "brightness(0) saturate(100%)" to your filter property which will first turn the icon set to black.</p> | |
<p> | |
For as long as I worked on creating this solution from multiple resources I found some had spent far | |
longer to create this already completed solution. Only slightly modified to focus on HEX colors. Credit | |
goes to MultiplyByZer0 for their post <a href="https://stackoverflow.com/a/43960991/604861" target="_blank">https://stackoverflow.com/a/43960991/604861</a> | |
</p> | |
</td> | |
</tr> | |
</table> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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; | |
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.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) { | |
const newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]); | |
const newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]); | |
const 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. | |
const r = this.r / 255; | |
const g = this.g / 255; | |
const b = this.b / 255; | |
const max = Math.max(r, g, b); | |
const min = Math.min(r, g, b); | |
let h, s, 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, | |
}; | |
} | |
clamp(value) { | |
if (value > 255) { | |
value = 255; | |
} else if (value < 0) { | |
value = 0; | |
} | |
return value; | |
} | |
} | |
class Solver { | |
constructor(target, baseColor) { | |
this.target = target; | |
this.targetHSL = target.hsl(); | |
this.reusedColor = new Color(0, 0, 0); | |
} | |
solve() { | |
const 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++) { | |
const initial = [50, 20, 3750, 50, 100, 100]; | |
const 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; | |
const deltas = new Array(6); | |
const highArgs = new Array(6); | |
const lowArgs = new Array(6); | |
for (let k = 0; k < iters; k++) { | |
const 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]; | |
} | |
const lossDiff = this.loss(highArgs) - this.loss(lowArgs); | |
for (let i = 0; i < 6; i++) { | |
const g = lossDiff / (2 * ck) * deltas[i]; | |
const ak = a[i] / Math.pow(A + k + 1, alpha); | |
values[i] = fix(values[i] - ak * g, i); | |
} | |
const 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 %= 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. | |
const 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); | |
const 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)}%);`; | |
} | |
} | |
function hexToRgb(hex) { | |
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") | |
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; | |
hex = hex.replace(shorthandRegex, (m, r, g, b) => { | |
return r + r + g + g + b + b; | |
}); | |
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); | |
return result | |
? [ | |
parseInt(result[1], 16), | |
parseInt(result[2], 16), | |
parseInt(result[3], 16), | |
] | |
: null; | |
} | |
$(document).ready(() => { | |
$('button.execute').click(() => { | |
const rgb = hexToRgb($('input.target').val()); | |
if (rgb.length !== 3) { | |
alert('Invalid format!'); | |
return; | |
} | |
const color = new Color(rgb[0], rgb[1], rgb[2]); | |
const solver = new Solver(color); | |
const 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>`); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.pixel { | |
display: inline-block; | |
background-color: #000; | |
width: 50px; | |
height: 50px; | |
} | |
.filterDetail { | |
font-family: "Consolas", "Menlo", "Ubuntu Mono", monospace; | |
} |
Good catch. I'm not sure I ever did a full any color to any color example now that I was looking around. But if you have to do a color version there is a note in the side text:
If your icon set isn't black you can prepend "brightness(0) saturate(100%)" to your filter property which will first turn the icon set to black.
Nice, at least a solution after many hours of search
Can you modify html put put initial colors in input ?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The links to the pens seem to be identical (they both link to the one that converts from black)