web-dev-qa-db-fra.com

Comment transformer le noir en n'importe quelle couleur en utilisant uniquement des filtres CSS

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 .

Exemple

Si la couleur cible est #ffff00 (jaune), une solution correcte est:

filter: invert(100%) sepia() saturate(10000%) hue-rotate(0deg)

( démo )

Non-buts

  • Animation.
  • Solutions non-CSS-filter.
  • Partir d'une couleur autre que le noir.
  • Se soucier de ce qui arrive aux couleurs autres que le noir.

Résultats jusqu'ici

Vous pouvez toujours obtenir une réponse acceptée en soumettant une solution non-brutale!

Ressources

  • 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.

    Implémentations de navigateur: Chrome , Firefox .

  • 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

85
glebm

@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:

  • Pour rgb(0,255,0), @ Dave produit rgb(29,218,34) et produit rgb(1,255,0)
  • Pour rgb(0,0,255), @ Dave produit rgb(37,39,255) et le mien produit rgb(5,6,255)
  • Pour rgb(19,11,118), @ Dave produit rgb(36,27,102) et le mien produit rgb(20,11,112)

Démo

"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>

Usage

let color = new Color(0, 255, 0);
let solver = new Solver(color);
let result = solver.solve();
let filterCSS = result.css;

Explication

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:

  • La classe Color représente une couleur RVB.
    • Sa fonction toString() renvoie la couleur dans une chaîne de couleurs CSS rgb(...).
    • Sa fonction hsl() renvoie la couleur, convertie en HSL .
    • Sa fonction clamp() garantit qu'une valeur de couleur donnée est comprise dans les limites (0-255).
  • La classe Solver tentera de résoudre une couleur cible.
    • Sa fonction css() renvoie un filtre donné dans une chaîne de filtrage CSS.

Implémentation de 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:

Matrix multiplication

Nous pouvons faire quelques optimisations ici:

  • Le dernier élément de la matrice de couleur est et sera toujours 1. Il ne sert à rien de le calculer ou de le stocker.
  • Il ne sert à rien de calculer ou de stocker la valeur alpha/transparence (A) non plus, puisqu'il s'agit de RVB et non de RGBA.
  • Par conséquent, nous pouvons couper les matrices de filtres de 5x5 à 3x5 et la matrice de couleurs de 1x5 à 1x. Cela économise un peu de travail.
  • Tous les filtres <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.
  • Comme la multiplication est relativement simple, il n’est pas nécessaire de faire glisser bibliothèques de mathématiques complexes pour cela. Nous pouvons implémenter nous-mêmes l'algorithme de multiplication de matrice.

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
    ]);
}

Implémentation de 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:

  • L'angle de rotation est donné en degrés. Il doit être converti en radians avant de passer à 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
    ]);
}

Implémentation de 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); }

Implémentation de 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:

  • Le filtre invert() définit cette table: [valeur, 1-valeur]. C'est tableValues ​​ ou v .
  • La formule définit n , telle que n + 1 est la longueur de la table. Puisque la longueur de la table est 2, n = 1.
  • La formule définit k , avec k et k 1 indices de la table. Puisque la table a 2 éléments, k = 0.

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);
}

Interlude: @ l'algorithme de force brute de Dave

Le code de @ Dave génère des combinaisons de filtres 176,66, notamment:

  • 11 invert() filtres (0%, 10%, 20%, ..., 100%)
  • 11 sepia() filtres (0%, 10%, 20%, ..., 100%)
  • 20 saturate() filtres (5%, 10%, 15%, ..., 100%)
  • 73 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.

Mise en œuvre de SPSA

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:

  • Différence RVB, car l’objectif est de produire la valeur RVB la plus proche.
  • Différence HSL, car de nombreuses valeurs HSL correspondent à des filtres (par exemple, la teinte est corrélée grossièrement avec 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:

  • Utiliser le meilleur résultat obtenu au lieu du dernier.
  • Réutiliser tous les tableaux (deltas, highArgs, lowArgs), au lieu de les recréer à chaque itération.
  • Utilisation d'un tableau de valeurs pour a , au lieu d'une valeur unique. En effet, tous les filtres sont différents et doivent donc se déplacer/converger à des vitesses différentes.
  • Exécution d’une fonction 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:

  1. La scène "large", qui tente "d'explorer" l'espace de recherche. Si le résultat n'est pas satisfaisant, le nombre d'essais SPSA sera limité.
  2. La scène "étroite", qui tire le meilleur résultat de la scène large et tente de "l'affiner". Il utilise des valeurs dynamiques pour [~ # ~] a [~ # ~] et a .

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);
}

Réglage SPSA

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:

  • La perte moyenne devrait être autour de 4. Si elle est supérieure à 4, les résultats obtenus sont trop éloignés et vous devez régler pour plus de précision. S'il est inférieur à 4, cela fait perdre du temps et vous devriez réduire le nombre d'itérations.
  • Si vous augmentez/diminuez le nombre d'itérations, ajustez [~ # ~] a [~ # ~] correctement.
  • Si vous augmentez/diminuez [~ # ~] a [~ # ~] , ajustez a correctement.
  • Utilisez l'indicateur --debug Si vous voulez voir le résultat de chaque itération.

TL; DR

71
MultiplyByZer0

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:

CSS Filter Matrix Equations

a = hue-rotation
b = saturation
c = sepia
d = invert
44
Dave

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:

  • L'ombre est dessinée derrière le contenu existant. Cela signifie que nous devons faire des astuces de positionnement absolues.
  • Tous les pixels seront traités de la même manière, mais OP a déclaré [nous ne devrions pas l'être] "Nous nous soucions de ce qu'il advient des couleurs autres que le noir."
  • Prise en charge du navigateur. (Je ne suis pas sûr, testé seulement sous FF et chrome).
/* 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>
25
Kaiido

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">
5
Michael Mullany

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>
1
Mister Jojo