Last active
November 5, 2020 08:28
-
-
Save Prottoy2938/7ff636330f2eec9bfeee56a49d6471e4 to your computer and use it in GitHub Desktop.
RGBA To CSS Filter Converter. This code is a slightly modified version of this stackoverflow answer: https://stackoverflow.com/a/43960991/604861
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; | |
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.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) { | |
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 | |
)}%);`; | |
} | |
} | |
export { Color, Solver }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage: