A Pen by Barrett Sonntag on CodePen.
-
-
Save chrisidakwo/89e0265203bdfe779fd9c5c6fe3aece4 to your computer and use it in GitHub Desktop.
CSS filter generator to convert from black to target hex color
This file contains 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 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 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 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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment