Comment transformer le noir en une couleur donnée en utilisant uniquement des filtres CSS


116

Ma question est la suivante: étant donné une couleur RVB cible, quelle est la formule pour recolorer le noir ( #000) dans cette couleur en utilisant uniquement des filtres CSS ?

Pour qu'une réponse soit acceptée, il faudrait fournir une fonction (dans n'importe quelle langue) qui accepterait la couleur cible comme argument et renverrait la filterchaîne CSS correspondante .

Le contexte pour cela est la nécessité de recolorer un SVG dans un fichier background-image. Dans ce cas, il s'agit de prendre en charge certaines fonctionnalités mathématiques TeX dans KaTeX: 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 sans filtre CSS.
  • À partir d'une couleur autre que le noir.
  • Se soucier de ce qui arrive aux couleurs autres que le noir.

Résultats à ce jour

Vous pouvez toujours obtenir une réponse acceptée en soumettant une solution sans force brute!

Ressources

  • Comment hue-rotateet sepiasont calculés: https://stackoverflow.com/a/29521147/181228 Exemple d'implémentation 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 ce qui clampprécède rend la hue-rotatefonction non linéaire.

    Implémentations de navigateur: Chromium , 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 fonctionne presque (à partir d'une question similaire ):
    https://stackoverflow.com/a/29958459/181228

    Une explication détaillée des raisons pour lesquelles la formule ci-dessus est fausse (CSS hue-rotaten'est pas une vraie rotation de teinte mais une approximation linéaire):
    https://stackoverflow.com/a/19325417/2441511


Vous voulez donc LERP # 000000 vers #RRGGBB? (Juste clarification)
Zze

1
Ouais doux - juste clarifier que vous ne vouliez pas incorporer une transition dans la solution.
Zze

1
Peut-être qu'un mode de fusion fonctionnerait pour vous? Vous pouvez facilement convertir le noir en n'importe quelle couleur ... Mais je n'ai pas une image globale de ce que vous voulez réaliser
vals

1
@glebm vous devez donc trouver une formule (en utilisant n'importe quelle méthode) pour transformer le noir en n'importe quelle couleur et l'appliquer en utilisant le CSS?
ProllyGeek

2
@ProllyGeek Oui. Une autre contrainte que je devrais mentionner est que la formule résultante ne peut pas être une recherche par force brute d'une table 5GiB (elle devrait être utilisable par exemple à partir de javascript sur une page Web).
glebm

Réponses:


149

@Dave a été le premier à poster une réponse à cela (avec un code de travail), et sa réponse a été une source inestimable d' inspiration pour copier et coller sans vergogne . Cet article a commencé comme une tentative d'expliquer et d'affiner la réponse de @ Dave, mais il a depuis évolué pour devenir une réponse à part entière.

Ma méthode est nettement plus rapide. Selon un benchmark jsPerf sur les couleurs RVB générées aléatoirement, l'algorithme de @ Dave s'exécute en 600 ms , tandis que le mien s'exécute en 30 ms . Cela peut certainement avoir une importance, par exemple en temps de chargement, où la vitesse est essentielle.

De plus, pour certaines couleurs, mon algorithme fonctionne mieux:

  • Car rgb(0,255,0), @ Dave's produit rgb(29,218,34)et produitrgb(1,255,0)
  • Car rgb(0,0,255), @ Dave's produit rgb(37,39,255)et le mien produitrgb(5,6,255)
  • Car rgb(19,11,118), @ Dave's produit rgb(36,27,102)et le mien produitrgb(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 Colorclasse représente une couleur RVB.
    • Sa toString()fonction renvoie la couleur dans une rgb(...)chaîne de couleurs CSS .
    • Sa hsl()fonction renvoie la couleur, convertie en TSL .
    • Sa clamp()fonction garantit qu'une valeur de couleur donnée est dans les limites (0-255).
  • La Solverclasse tentera de trouver une couleur cible.
    • Sa css()fonction renvoie un filtre donné dans une chaîne de filtre CSS.

La mise en œuvre grayscale(), sepia()etsaturate()

Le cœur des filtres CSS / SVG sont les 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 la primative de filtre <feColorMatrix>, qui effectue une multiplication de matrice entre une matrice spécifiée par le filtre (souvent générée dynamiquement) et une matrice créée à partir de la couleur. Diagramme:

Multiplication matricielle

Il y a quelques optimisations que nous pouvons faire ici:

  • Le dernier élément de la matrice de couleurs est et sera toujours 1. Il ne sert à rien de le calculer ou de le stocker.
  • Il est inutile de calculer ou de stocker la valeur alpha / transparence ( A) non plus, puisque nous avons affaire à RVB, pas à RVBA.
  • Par conséquent, nous pouvons couper les matrices de filtre de 5x5 à 3x5 et la matrice de couleur de 1x5 à 1x3 . Cela économise un peu de travail.
  • Tous les <feColorMatrix>filtres laissent les colonnes 4 et 5 sous forme de zéros. Par conséquent, nous pouvons réduire davantage la matrice de filtre à 3x3 .
  • Comme la multiplication est relativement simple, il n'est pas nécessaire de faire glisser des bibliothèques mathématiques complexes pour cela. Nous pouvons implémenter nous-mêmes l'algorithme de multiplication matricielle.

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 des modifications this.r, etc. affectent les calculs ultérieurs.)

Maintenant que nous avons mis en place <feColorMatrix>, nous pouvons mettre en œuvre grayscale(), sepia()et saturate()qui tout simplement l' invoquons avec une matrice de filtre donné:

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

Exécution hue-rotate()

Le hue-rotate()filtre est implémenté par <feColorMatrix type="hueRotate" />.

La matrice de filtre est calculée comme indiqué ci-dessous:

Par exemple, l'élément a 00 serait calculé comme ceci:

Quelques notes:

  • L'angle de rotation est exprimé en degrés. Il doit être converti en radians avant d'être passé à Math.sin()ou Math.cos().
  • Math.sin(angle)et Math.cos(angle)doit être calculé 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
    ]);
}

Mettre en œuvre brightness()etcontrast()

Les filtres brightness()et contrast()sont implémentés par <feComponentTransfer>avec <feFuncX type="linear" />.

Chaque <feFuncX type="linear" />élément accepte un attribut de pente et d' interception . Il calcule ensuite chaque nouvelle valeur de couleur via 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 cela est implémenté, brightness()et contrast()peut également être implémenté:

function brightness(value = 1) { this.linear(value); }
function contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }

Exécution invert()

Le invert()filtre est implémenté par <feComponentTransfer>avec <feFuncX type="table" />.

La spécification stipule:

Dans ce qui suit, 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 données dans l'attribut tableValues . Le tableau a n + 1 valeurs (c'est-à-dire, v 0 à v n ) spécifiant les valeurs de début et de fin pour n régions d'interpolation de taille égale. Les interpolations utilisent la formule suivante:

Pour une valeur C trouver k tel que:

k / n ≤ C <(k + 1) / n

Le résultat C ' est donné par:

C '= v k + (C - k / n) * n * (v k + 1 - v k )

Une explication de cette formule:

  • Le invert()filtre définit cette table: [valeur, 1 - valeur]. C'est tableValues ou v .
  • La formule définit n , tel 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 étant des index de la table. Puisque la table a 2 éléments, k = 0.

Ainsi, nous pouvons simplifier la formule pour:

C '= v 0 + C * (v 1 - v 0 )

En incorporant les valeurs de la table, nous nous retrouvons avec:

C '= valeur + C * (1 - valeur - valeur)

Encore une simplification:

C '= valeur + C * (1 - 2 * valeur)

La spécification définit C et C ' comme étant des 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 176 660 combinaisons de filtres, dont:

  • 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-rotatedeg);

Il parcourt ensuite toutes les couleurs calculées. Il s'arrête une fois qu'il a trouvé une couleur générée dans 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 doit renvoyer 0.

Nous mesurerons la différence de couleur comme la somme de deux métriques:

  • 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 en corrélation avec hue-rotate(), la saturation est corrélée avec saturate(), etc.) Ceci guide l'algorithme.

La fonction de perte prendra un argument - un tableau de pourcentages de filtre.

Nous utiliserons l'ordre de filtrage suivant:

filter: invert(a%) sepia(b%) saturate(c%) hue-rotatedeg) 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, de sorte que:

loss([a, b, c, d, e, f]) = 0

L' algorithme SPSA ( site Web , plus d'infos , papier , document de mise en œuvre , code de référence ) est très bon à cela. Il a été conçu pour optimiser des systèmes complexes avec des minima locaux, des fonctions de perte bruyante / non linéaire / multivariée, etc. Il a été utilisé pour régler les moteurs d'échecs . Et contrairement à de nombreux autres algorithmes, les articles qui le décrivent sont en fait 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éutilisant 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 seule valeur. En effet, tous les filtres sont différents et doivent donc se déplacer / converger à des vitesses différentes.
  • Exécution d'une fixfonction après chaque itération. Il fixe toutes les valeurs entre 0% et 100%, sauf saturate(où le maximum est 7500%), brightnesset contrast(où le maximum est 200%), et hueRotate(où les valeurs sont enroulées au lieu de serrées).

J'utilise SPSA dans un processus en deux étapes:

  1. La scène "large", qui tente "d'explorer" l'espace de recherche. Il fera des tentatives limitées de SPSA si les résultats ne sont pas satisfaisants.
  2. La scène "étroite", qui prend le meilleur résultat de la scène large et tente de la "raffiner". 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

Attention: ne jouez pas avec le code SPSA, en particulier avec ses constantes, sauf si vous êtes sûr de savoir ce que vous faites.

Les constantes importantes sont A , a , c , les valeurs initiales, les seuils de nouvelle tentative, les valeurs de maxin fix()et le nombre d'itérations de chaque étape. Toutes ces valeurs ont été soigneusement ajustées pour produire de bons résultats, et les visser aléatoirement avec elles réduira presque certainement l'utilité de l'algorithme.

Si vous insistez pour le modifier, vous devez mesurer avant de «l'optimiser».

Tout d'abord, appliquez ce patch .

Ensuite, 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

Réglez maintenant les constantes à votre guise.

Quelques conseils:

  • La perte moyenne doit être d'environ 4. Si elle est supérieure à 4, cela produit des résultats 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 devez réduire le nombre d'itérations.
  • Si vous augmentez / diminuez le nombre d'itérations, ajustez A de manière appropriée.
  • Si vous augmentez / diminuez A , ajustez a de manière appropriée.
  • Utilisez l' --debugindicateur si vous souhaitez voir le résultat de chaque itération.

TL; DR


3
Très beau résumé du processus de développement! Lisez-vous mes pensées?!
Dave

1
@Dave En fait, je travaillais là-dessus de manière indépendante, mais vous m'avez battu.
MultiplyByZer0

4
Très bonne réponse! Implémentation dans ce codepen
KyleMit

3
C'est une méthode complètement insensée. Vous pouvez définir une couleur directement à l'aide d'un filtre SVG (cinquième colonne dans une feColorMatrix) et vous pouvez référencer ce filtre à partir de CSS - pourquoi n'utiliseriez-vous pas cette méthode?
Michael Mullany

2
@MichaelMullany Eh bien, c'est embarrassant pour moi, compte tenu du temps que j'ai travaillé là-dessus. Je n'ai pas pensé à votre méthode, mais maintenant je comprends - pour recolorer un élément en n'importe quelle couleur arbitraire, vous générez simplement dynamiquement un SVG avec un <filter>contenant un <feColorMatrix>avec les valeurs appropriées (tous les zéros sauf la dernière colonne, qui contient le RVB cible valeurs, 0 et 1), insérez le SVG dans le DOM et référencez le filtre à partir de CSS. Veuillez rédiger votre solution en tant que réponse (avec une démo) et je voterai pour.
MultiplyByZer0

55

Ce fut tout un voyage dans le terrier du lapin mais le voici!

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 adoptée pour atteindre ce que OP demande. En l'état, il est faible dans certaines zones du spectre de couleurs. De meilleurs résultats peuvent être obtenus par plus de granularité dans les itérations d'étape ou en implémentant plus de fonctions de filtrage pour les raisons décrites en détail dans la réponse de @ MultiplyByZer0 .

EDIT2: OP recherche une solution sans force brute. Dans ce cas, c'est assez simple, résolvez simplement cette équation:

Equations de la matrice de filtre CSS

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

Si je mets en place 255,0,255, mon colorimètre numérique rapporte le résultat #d619d9plutôt que #ff00ff.
Siguza

@Siguza Ce n'est certainement pas parfait, les couleurs des bordures peuvent être modifiées en ajustant les limites dans les boucles.
Dave

3
Cette équation est tout sauf "assez simple"
MultiplyByZer0

Je pense que l'équation ci-dessus est également manquante clamp?
glebm

1
La pince n'a pas sa place là-dedans. Et d'après ce que je retiens de mes maths à l'université, ces équations sont calculées par des calculs numériques aka "force brute" alors bonne chance!
Dave

28

Remarque: OP m'a demandé d'annuler la suppression , 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 y a 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 dit [que nous ne devrions pas être] "soucieux de ce qui arrive aux couleurs autres que le noir."
  • Prise en charge du navigateur. (Je n'en suis pas sûr, testé uniquement sous les derniers 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>


1
Super intelligent, génial! Cela fonctionne pour moi, appréciez-le
jaminroe

Je pense que c'est une meilleure solution car elle est précise à 100% avec la couleur à chaque fois.
user835542

Le code tel quel affiche une page vierge (W10 FF 69b). Rien de mal avec l'icône, cependant (SVG séparé).
Rene van der Lende

L'ajout background-color: black;de .icon>spanrend ce travail pour FF 69b. Cependant, n'affiche pas l'icône.
Rene van der Lende

@RenevanderLende Juste essayé sur FF70 fonctionne toujours là-bas. Si cela ne fonctionne pas pour vous, cela doit être quelque chose de votre côté.
Kaiido

15

Vous pouvez rendre tout cela très simple en utilisant simplement un filtre SVG référencé à partir de CSS. Vous n'avez besoin que d'une seule feColorMatrix pour effectuer une recoloration. Celui-ci se recolore au jaune. La cinquième colonne de feColorMatrix contient les valeurs cibles RVB sur l'échelle des unités. (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">


Une solution intéressante mais il semble qu'elle ne permet pas de contrôler la couleur cible via CSS.
glebm

Vous devez définir un nouveau filtre pour chaque couleur que vous souhaitez appliquer. Mais c'est tout à fait exact. hue-rotate est une approximation qui coupe certaines couleurs - ce qui signifie que vous ne pouvez pas obtenir certaines couleurs avec précision en l'utilisant - comme l'attestent les réponses ci-dessus. Ce dont nous avons vraiment besoin, c'est d'un raccourci de filtre CSS recolor ().
Michael Mullany

La réponse de MultiplyByZer0 calcule une série de filtres qui atteignent avec une très grande précision, sans modifier le HTML. Un vrai hue-rotatedans les navigateurs serait bien ouais.
glebm

2
il semble que cela ne produise des couleurs RVB précises pour les images sources noires que lorsque vous ajoutez "color-interpolation-filters" = "sRGB" à la feColorMatrix.
John Smith

Edge 12-18 sont laissés de côté car ils ne prennent pas en charge la urlfonction caniuse.com/#search=svg%20filter
Volker E.

2

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) alors voici le moyen d'obtenir la couleur que vous voulez:

Voici une deuxième solution, en utilisant le filtre SVG uniquement dans code => URL.createObjectURL


1

juste utiliser

fill: #000000

La fillpropriété en CSS est de remplir la couleur d'une forme SVG. La fillpropriété peut accepter n'importe quelle valeur de couleur CSS.


3
Cela peut fonctionner avec du CSS interne à une image SVG, mais cela ne fonctionne pas en tant que CSS appliqué en externe à un imgélément par le navigateur.
David Moles le

1

J'ai commencé avec cette réponse en utilisant un filtre svg et j'ai apporté les modifications suivantes:

Filtre SVG à partir de l'URL des données

Si vous ne souhaitez pas définir le filtre SVG quelque part dans votre balisage, vous pouvez utiliser une URL de données à la place (remplacez R , V , B et A par la couleur souhaitée):

filter: url('data:image/svg+xml;utf8,\
  <svg xmlns="http://www.w3.org/2000/svg">\
    <filter id="recolor" color-interpolation-filters="sRGB">\
      <feColorMatrix type="matrix" values="\
        0 0 0 0 R\
        0 0 0 0 G\
        0 0 0 0 B\
        0 0 0 A 0\
      "/>\
    </filter>\
  </svg>\
  #recolor');

Repli en niveaux de gris

Si la version ci-dessus ne fonctionne pas, vous pouvez également ajouter une solution de secours en niveaux de gris.

Les fonctions saturateet brightnesstransforment n'importe quelle couleur en noir (vous n'avez pas à l'inclure si la couleur est déjà noire), invertpuis l'éclaircit avec la luminosité souhaitée ( L ) et vous pouvez également spécifier l'opacité ( A ).

filter: saturate(0%) brightness(0%) invert(L) opacity(A);

Mixin SCSS

Si vous souhaitez spécifier la couleur de manière dynamique, vous pouvez utiliser le mixin SCSS suivant:

@mixin recolor($color: #000, $opacity: 1) {
  $r: red($color) / 255;
  $g: green($color) / 255;
  $b: blue($color) / 255;
  $a: $opacity;

  // grayscale fallback if SVG from data url is not supported
  $lightness: lightness($color);
  filter: saturate(0%) brightness(0%) invert($lightness) opacity($opacity);

  // color filter
  $svg-filter-id: "recolor";
  filter: url('data:image/svg+xml;utf8,\
    <svg xmlns="http://www.w3.org/2000/svg">\
      <filter id="#{$svg-filter-id}" color-interpolation-filters="sRGB">\
        <feColorMatrix type="matrix" values="\
          0 0 0 0 #{$r}\
          0 0 0 0 #{$g}\
          0 0 0 0 #{$b}\
          0 0 0 #{$a} 0\
        "/>\
      </filter>\
    </svg>\
    ##{$svg-filter-id}');
}

Exemple d'utilisation:

.icon-green {
  @include recolor(#00fa86, 0.8);
}

Avantages:

  • Pas de Javascript .
  • Aucun élément HTML supplémentaire .
  • Si les filtres CSS sont pris en charge, mais que le filtre SVG ne fonctionne pas, il existe une solution de secours en niveaux de gris .
  • Si vous utilisez le mixin, l'utilisation est assez simple (voir l'exemple ci-dessus).
  • La couleur est plus lisible et plus facile à modifier que l'astuce sépia (composants RGBA en CSS pur et vous pouvez même utiliser des couleurs HEX en SCSS).
  • Évite le comportement étrange dehue-rotate .

Mises en garde:

  • Tous les navigateurs ne prennent pas en charge les filtres SVG à partir d'une URL de données (en particulier le hachage d'identifiant), mais cela fonctionne dans les navigateurs Firefox et Chromium actuels (et peut-être d'autres).
  • Si vous souhaitez spécifier la couleur de manière dynamique, vous devez utiliser un mixin SCSS.
  • La version CSS pure est un peu moche, si vous voulez beaucoup de couleurs différentes, vous devez inclure le SVG plusieurs fois.

1
oh c'est parfait, c'est exactement ce que je cherchais qui était de tout utiliser dans SASS génial merci beaucoup!
ghiscoding
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.