Ma question est la suivante: à partir d’une couleur RVB cible, quelle est la formule pour recolorer le noir (#000
) dans cette couleur en utilisant seulement filtres CSS ?
Pour qu'une réponse soit acceptée, il faudrait qu'elle fournisse une fonction (dans n'importe quelle langue) qui accepterait la couleur cible en tant qu'argument et renverrait la chaîne CSS filter
correspondante.
Le contexte pour cela est la nécessité de recolorer un SVG à l'intérieur d'un background-image
. Dans ce cas, KaTeX doit prendre en charge certaines fonctionnalités mathématiques TeX: https://github.com/Khan/KaTeX/issues/587 .
Si la couleur cible est #ffff00
(jaune), une solution correcte est:
filter: invert(100%) sepia() saturate(10000%) hue-rotate(0deg)
( démo )
Recherche par force brute des paramètres d'une liste de filtres fixe: https://stackoverflow.com/a/43959856/181228
Inconvénients: inefficace, ne génère que quelques-unes des 16 777 216 couleurs possibles (676 248 avec hueRotateStep=1
).
Une solution de recherche plus rapide utilisant SPSA : https://stackoverflow.com/a/43960991/181228 Prime accordée
UNE drop-shadow
solution: https://stackoverflow.com/a/43959853/181228
Inconvénients: Ne fonctionne pas sur Edge. Requiert des modifications CSS non -filter
et des modifications mineures du code HTML.
Vous pouvez toujours obtenir une réponse acceptée en soumettant une solution non-brutale!
Comment hue-rotate
et sepia
sont calculés: https://stackoverflow.com/a/29521147/181228 Exemple 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
Notez que le clamp
ci-dessus rend le hue-rotate
fonction non linéaire.
Démo: Obtenir une couleur sans niveaux de gris à partir d'une couleur en niveaux de gris: https://stackoverflow.com/a/25524145/181228
Une formule qui presque fonctionne (à partir d'un question similaire ):
https://stackoverflow.com/a/29958459/181228
Une explication détaillée de la raison pour laquelle la formule ci-dessus est fausse (CSS hue-rotate
n'est pas une vraie rotation de teinte mais une approximation linéaire):
https://stackoverflow.com/a/19325417/2441511
@Dave a été le premier à poster ne réponse à cela (avec un code de travail), et sa réponse a été une source inestimable de copier et coller sans vergogne inspiration pour moi. Ce billet a été conçu pour tenter d’expliciter et d’affiner la réponse de @ Dave, mais il a depuis évolué pour devenir une réponse distincte.
Ma méthode est nettement plus rapide. Selon un test de référence jsPerf sur les couleurs RVB générées de manière aléatoire, l'algorithme de @ Dave s'exécute en 600 ms, tandis que le mien s'exécute en ms. Cela peut certainement être important, par exemple en temps de chargement, où la rapidité est essentielle.
De plus, pour certaines couleurs, mon algorithme fonctionne mieux:
rgb(0,255,0)
, @ Dave produit rgb(29,218,34)
et produit rgb(1,255,0)
rgb(0,0,255)
, @ Dave produit rgb(37,39,255)
et le mien produit rgb(5,6,255)
rgb(19,11,118)
, @ Dave produit rgb(36,27,102)
et le mien produit 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;
Nous allons commencer par écrire du 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)}%);`;
}
}
Explication:
Color
représente une couleur RVB. toString()
renvoie la couleur dans une chaîne de couleurs CSS rgb(...)
.hsl()
renvoie la couleur, convertie en HSL .clamp()
garantit qu'une valeur de couleur donnée est comprise dans les limites (0-255).Solver
tentera de résoudre une couleur cible. css()
renvoie un filtre donné dans une chaîne de filtrage CSS.grayscale()
, sepia()
et saturate()
Le cœur des filtres CSS/SVG sont primitives de filtre , qui représentent des modifications de bas niveau d'une image.
Les filtres grayscale()
, sepia()
et saturate()
sont implémentés par le primitif de filtre <feColorMatrix>
, qui effectue multiplication de matrice entre une matrice spécifiée par le filtre (souvent générée de manière dynamique) et une matrice créée à partir de la couleur. Diagramme:
Nous pouvons faire quelques optimisations ici:
1
. Il ne sert à rien de le calculer ou de le stocker.A
) non plus, puisqu'il s'agit de RVB et non de RGBA.<feColorMatrix>
Laissent les colonnes 4 et 5 sous forme de zéros. Par conséquent, nous pouvons réduire davantage la matrice de filtrage à 3x.La mise en oeuvre:
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;
}
(Nous utilisons des variables temporaires pour conserver les résultats de chaque multiplication de lignes, car nous ne voulons pas que les modifications apportées à this.r
, Etc. affectent les calculs ultérieurs.)
Maintenant que nous avons implémenté <feColorMatrix>
, Nous pouvons implémenter grayscale()
, sepia()
et saturate()
, qui l'invoque simplement avec une matrice de filtrage donnée:
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()
Le filtre hue-rotate()
est implémenté par <feColorMatrix type="hueRotate" />
.
La matrice de filtre est calculée comme indiqué ci-dessous:
Par exemple, élément a00 serait calculé comme suit:
Quelques notes:
Math.sin()
ou Math.cos()
.Math.sin(angle)
et Math.cos(angle)
doivent être calculés une fois, puis mis en cache.La mise en oeuvre:
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()
et contrast()
Les filtres brightness()
et contrast()
sont mis en oeuvre par <feComponentTransfer>
avec - <feFuncX type="linear" />
.
Chaque élément <feFuncX type="linear" />
Accepte un attribut pente et intercept . Il calcule ensuite chaque nouvelle valeur de couleur à l'aide d'une formule simple:
value = slope * value + intercept
C'est facile à mettre en œuvre:
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);
}
Une fois que ceci est implémenté, brightness()
et contrast()
peuvent également être implémentés:
function brightness(value = 1) { this.linear(value); }
function contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
invert()
Le filtre invert()
est implémenté par <feComponentTransfer>
avec <feFuncX type="table" />
.
La spécification stipule:
Dans la suite, [~ # ~] c [~ # ~] == est le composant initial et C ' est le composant remappé. ; les deux dans l'intervalle fermé [0,1].
Pour "table", la fonction est définie par interpolation linéaire entre les valeurs indiquées dans l'attribut tableValues . La table a n + 1 valeurs (c.-à-d. V à vn) spécifiant les valeurs de début et de fin pour n des régions d'interpolation de taille égale. Les interpolations utilisent la formule suivante:
Pour une valeur [~ # ~] c [~ # ~] find k tel que:
k/n ≤ C <(k + 1)/n
Le résultat C ' est donné par:
C '= vk + (C - k/n) * n * (vk + 1 - vk)
Une explication de cette formule:
invert()
définit cette table: [valeur, 1-valeur]. C'est tableValues ou v .Ainsi, nous pouvons simplifier la formule pour:
C '= v + C * (v1 - v)
En alignant les valeurs de la table, il nous reste:
C '= valeur + C * (1 - valeur - valeur)
Encore une simplification:
C '= valeur + C * (1 - 2 * valeur)
La spécification définit [~ # ~] c [~ # ~] et C ' = valeurs RVB, dans les limites 0 -1 (par opposition à 0-255). En conséquence, nous devons réduire les valeurs avant le calcul et les redimensionner après.
Nous arrivons ainsi à notre implémentation:
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);
}
Le code de @ Dave génère des combinaisons de filtres 176,66, notamment:
invert()
filtres (0%, 10%, 20%, ..., 100%)sepia()
filtres (0%, 10%, 20%, ..., 100%)saturate()
filtres (5%, 10%, 15%, ..., 100%)hue-rotate()
filtres (0deg, 5deg, 10deg, ..., 360deg)Il calcule les filtres dans l'ordre suivant:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);
Il parcourt ensuite toutes les couleurs calculées. Il s'arrête dès qu'il a trouvé une couleur générée dans les limites de la tolérance (toutes les valeurs RVB sont à moins de 5 unités de la couleur cible).
Cependant, cela est lent et inefficace. Ainsi, je présente ma propre réponse.
Tout d'abord, nous devons définir une fonction de perte , qui renvoie la différence entre la couleur produite par une combinaison de filtres et la couleur cible. Si les filtres sont parfaits, la fonction de perte devrait renvoyer 0.
Nous allons mesurer la différence de couleur en tant que somme de deux mesures:
hue-rotate()
, la saturation est corrélée à saturate()
, etc.) Ceci guide l'algorithme.La fonction de perte prendra un argument - un tableau de pourcentages de filtrage.
Nous allons utiliser l'ordre de filtrage suivant:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg) brightness(e%) contrast(f%);
La mise en oeuvre:
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);
}
Nous allons essayer de minimiser la fonction de perte, telle que:
loss([a, b, c, d, e, f]) = 0
L'algorithme SPSA ( site web , plus d'informations , papier , - document de mise en œuvre , code de référence ) est très bon pour cela. Il a été conçu pour optimiser des systèmes complexes avec des minima locaux, des fonctions de perte bruyantes/non linéaires/multivariées, etc. Il a été utilisé pour ajuster les moteurs d’échecs . Et contrairement à beaucoup d’autres algorithmes, les articles qui le décrivent sont en réalité compréhensibles (bien qu’avec beaucoup d’efforts).
La mise en oeuvre:
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;
}
}
J'ai apporté quelques modifications/optimisations à SPSA:
deltas
, highArgs
, lowArgs
), au lieu de les recréer à chaque itération.fix
après chaque itération. Il fixe toutes les valeurs entre 0% et 100%, sauf saturate
(où le maximum est 7500%), brightness
et contrast
(où le maximum est 200%), et hueRotate
(où les valeurs sont encapsulées au lieu d'être bloquées).J'utilise SPSA dans un processus en deux étapes:
La mise en oeuvre:
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);
}
Avertissement: ne jouez pas avec le code SPSA, en particulier avec ses constantes, sauf si vous êtes sure vous savez ce que vous faites.
Les constantes importantes sont [~ # ~] a [~ # ~] , a , c , les valeurs initiales, les seuils de nouvelle tentative, les valeurs de max
dans fix()
et le nombre d'itérations de chaque étape. Toutes ces valeurs ont été soigneusement réglées pour produire de bons résultats et leur vissage aléatoire réduira presque certainement l'utilité de l'algorithme.
Si vous insistez pour le modifier, vous devez mesurer avant d’optimiser.
Tout d’abord, appliquez ce patch .
Puis exécutez le code dans Node.js. Après un certain temps, le résultat devrait ressembler à ceci:
Average loss: 3.4768521401985275
Average time: 11.4915ms
Maintenant, réglez les constantes au contenu de votre coeur.
Quelques conseils:
--debug
Si vous voulez voir le résultat de chaque itération.Ce fut tout un voyage dans le terrier du lapin mais le voilà!
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: Cette solution n'est pas destinée à une utilisation en production et illustre uniquement une approche qui peut être prise pour réaliser ce que l'OP demande. Tel quel, il est faible dans certaines zones du spectre de couleurs. Vous obtiendrez de meilleurs résultats en augmentant la granularité dans les itérations par étapes ou en implémentant davantage de fonctions de filtrage pour les raisons décrites en détail dans réponse de @ MultiplyByZer .
EDIT2: OP recherche une solution sans force brute. Dans ce cas, c'est assez simple, il suffit de résoudre cette équation:
où
a = hue-rotation
b = saturation
c = sepia
d = invert
Remarque: OP m'a demandé de ne pas effacer , mais la prime ira à la réponse de Dave.
Je sais que ce n'est pas ce qui a été demandé dans le corps de la question, et certainement pas ce que nous attendions tous, mais il existe un filtre CSS qui fait exactement cela: drop-shadow()
Mises en garde:
/* 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>
Vous pouvez rendre tout cela simple très simple en utilisant simplement un filtre SVG référencé à partir de CSS. Vous n'avez besoin que d'un seul feColorMatrix pour effectuer une nouvelle coloration. Celui-ci se décolore en jaune. La cinquième colonne de la feColorMatrix contient les valeurs cibles RVB sur l'échelle de l'unité. (pour le jaune - c'est 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">
J'ai remarqué que l'exemple du traitement via un filtre SVG était incomplet, j'ai écrit le mien (qui fonctionne parfaitement): (voir la réponse de Michael Mullany), voici donc le moyen d'obtenir la couleur de votre choix:
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>
Voici une deuxième solution, en utilisant SVG Filter uniquement dans le code => RL.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>