wake-up-neo.net

So wandeln Sie Schwarz mit CSS-Filtern in eine beliebige Farbe um

Meine Frage ist: Wenn man eine RGB-Zielfarbe hat, wie lautet die Formel, um Schwarz (#000) Nur mit CSS-Filter in diese Farbe umzufärben?

Damit eine Antwort akzeptiert wird, muss eine Funktion (in einer beliebigen Sprache) bereitgestellt werden, die die Zielfarbe als Argument akzeptiert und den entsprechenden CSS-String filter zurückgibt.

Der Kontext dafür ist die Notwendigkeit, eine SVG in einem background-image Neu einzufärben. In diesem Fall werden bestimmte TeX-Mathematikfunktionen in KaTeX unterstützt: https://github.com/Khan/KaTeX/issues/587 .

Beispiel

Wenn die Zielfarbe #ffff00 (Gelb) ist, lautet eine richtige Lösung:

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

( Demo )

Nicht-Ziele

  • Animation.
  • Nicht CSS-Filterlösungen.
  • Ausgehend von einer anderen Farbe als Schwarz.
  • Kümmere dich darum, was mit anderen Farben als Schwarz passiert.

Ergebnisse bisher

Sie können immer noch eine Accepted Antwort erhalten, indem Sie eine nicht-brute-force-Lösung einreichen!

Ressourcen

  • Wie hue-rotate Und sepia berechnet werden: https://stackoverflow.com/a/29521147/181228 Beispiel Ruby Implementierung:

    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
    

    Beachten Sie, dass das clamp die Funktion hue-rotate Nichtlinear macht.

    Browser-Implementierungen: Chromium , Firefox .

  • Demo: Aufrufen einer Nicht-Graustufen-Farbe von einer Graustufen-Farbe: https://stackoverflow.com/a/25524145/181228

  • Eine Formel, die fast funktioniert (aus einer ähnlichen Frage ):
    https://stackoverflow.com/a/29958459/181228

    Eine detaillierte Erklärung, warum die obige Formel falsch ist (CSS hue-rotate Ist keine echte Farbtonrotation, sondern eine lineare Annäherung):
    https://stackoverflow.com/a/19325417/2441511

85
glebm

@Dave hat als erster eine Antwort darauf (mit Arbeitscode) gepostet, und seine Antwort war eine unschätzbare Quelle für schamloses Kopieren und Einfügen Inspiration für mich. Dieser Beitrag begann als Versuch, die Antwort von @ Dave zu erklären und zu verfeinern, hat sich aber inzwischen zu einer eigenen Antwort entwickelt.

Meine Methode ist deutlich schneller. Gemäß einem jsPerf-Benchmark für zufällig generierte RGB-Farben läuft der @ Dave-Algorithmus in 600 ms, während meiner in ms läuft. Dies kann definitiv von Bedeutung sein, zum Beispiel in der Ladezeit, in der die Geschwindigkeit entscheidend ist.

Außerdem ist mein Algorithmus für einige Farben besser:

  • Für rgb(0,255,0) erzeugt @ Dave's rgb(29,218,34) und rgb(1,255,0)
  • Für rgb(0,0,255) erzeugt @ Dave rgb(37,39,255) und meine rgb(5,6,255)
  • Für rgb(19,11,118) erzeugt @ Dave rgb(36,27,102) und meine rgb(20,11,112)

Demo

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

Verwendung

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

Erläuterung

Wir beginnen mit dem Schreiben von 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)}%);`;
    }
}
_

Erläuterung:

  • Die Klasse Color repräsentiert eine RGB-Farbe.
    • Die Funktion toString() gibt die Farbe in einer CSS-Zeichenfolge rgb(...) zurück.
    • Die Funktion hsl() gibt die Farbe zurück, die in HSL konvertiert wurde.
    • Die Funktion clamp() stellt sicher, dass ein bestimmter Farbwert innerhalb der Grenzen liegt (0-255).
  • Die Klasse Solver versucht, nach einer Zielfarbe zu suchen.
    • Die Funktion css() gibt einen bestimmten Filter in einer CSS-Filterzeichenfolge zurück.

Implementieren von grayscale(), sepia() und saturate()

Das Herzstück von CSS/SVG-Filtern sind Filterprimitive , die geringfügige Änderungen an einem Bild darstellen.

Die Filter grayscale() , sepia() und saturate() werden durch das Filterprimativ _<feColorMatrix>_ implementiert , das Matrixmultiplikation zwischen einer vom Filter angegebenen Matrix durchführt (häufig dynamisch generiert), und eine aus der Farbe erzeugte Matrix. Diagramm:

Matrix multiplication

Es gibt einige Optimierungen, die wir hier vornehmen können:

  • Das letzte Element der Farbmatrix ist und bleibt _1_. Es gibt keinen Grund, es zu berechnen oder zu speichern.
  • Es ist auch nicht sinnvoll, den Alpha/Transparenz-Wert (A) zu berechnen oder zu speichern, da es sich um RGB handelt, nicht um RGBA.
  • Daher können wir die Filtermatrizen von 5x5 auf 3x5 und die Farbmatrix von 1x5 auf 1x3 trimmen. Das spart ein bisschen Arbeit.
  • Alle _<feColorMatrix>_ Filter belassen die Spalten 4 und 5 als Nullen. Daher können wir die Filtermatrix weiter auf 3x3 reduzieren.
  • Da die Multiplikation relativ einfach ist, ist es nicht erforderlich, komplexe mathematische Bibliotheken dafür zu ziehen. Wir können den Matrixmultiplikationsalgorithmus selbst implementieren.

Implementierung:

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

(Wir verwenden temporäre Variablen, um die Ergebnisse jeder Zeilenmultiplikation zu speichern, da wir keine Änderungen an _this.r_ usw. wünschen, die sich auf nachfolgende Berechnungen auswirken.)

Nachdem wir _<feColorMatrix>_ implementiert haben, können wir grayscale(), sepia() und saturate() implementieren, die es einfach mit einer gegebenen Filtermatrix aufrufen:

_function grayscale(value = 1) {
    this.multiply([
        0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value),
        0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value),
        0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value)
    ]);
}

function sepia(value = 1) {
    this.multiply([
        0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value),
        0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value),
        0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value)
    ]);
}

function saturate(value = 1) {
    this.multiply([
        0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value,
        0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value,
        0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value
    ]);
}
_

hue-rotate() implementieren

Der hue-rotate() Filter wird durch _<feColorMatrix type="hueRotate" />_ implementiert.

Die Filtermatrix wird wie folgt berechnet:

Beispiel: element a00 würde folgendermaßen berechnet:

Einige Notizen:

  • Der Drehwinkel wird in Grad angegeben. Es muss in Bogenmaß konvertiert werden, bevor es an Math.sin() oder Math.cos() übergeben wird.
  • Math.sin(angle) und Math.cos(angle) sollten einmal berechnet und dann zwischengespeichert werden.

Implementierung:

_function hueRotate(angle = 0) {
    angle = angle / 180 * Math.PI;
    let sin = Math.sin(angle);
    let cos = Math.cos(angle);

    this.multiply([
        0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
        0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
        0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
    ]);
}
_

brightness() und contrast() implementieren

Die brightness() und contrast() Filter werden implementiert von _<feComponentTransfer>_ mit _<feFuncX type="linear" />_ .

Jedes _<feFuncX type="linear" />_ -Element akzeptiert die Attribute slope und intercept . Anschließend wird jeder neue Farbwert mit einer einfachen Formel berechnet:

_value = slope * value + intercept
_

Dies ist einfach zu implementieren:

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

Sobald dies implementiert ist, können auch brightness() und contrast() implementiert werden:

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

invert() implementieren

Der invert() Filter wird implementiert von _<feComponentTransfer>_ mit _<feFuncX type="table" />_ .

Die Spezifikation besagt:

Im Folgenden ist [~ # ~] c [~ # ~] die Anfangskomponente und C ' die neu zugeordnete Komponente ; beide im geschlossenen Intervall [0,1].

Bei "table" wird die Funktion durch lineare Interpolation zwischen den Werten definiert, die im Attribut tableValues ​​ angegeben sind. Die Tabelle hat n + 1 Werte (d. H. V zu vn) Angabe der Start- und Endwerte für n gleich große Interpolationsbereiche. Interpolationen verwenden die folgende Formel:

Für einen Wert [~ # ~] c [~ # ~] find k so, dass:

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

Das Ergebnis C ' ist gegeben durch:

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

Eine Erklärung dieser Formel:

  • Der invert() Filter definiert diese Tabelle: [value, 1 - value]. Dies ist tableValues ​​ oder v .
  • Die Formel definiert n , so dass n + 1 die Länge der Tabelle ist. Da die Länge der Tabelle 2 ist, ist n = 1.
  • Die Formel definiert k , wobei k und k + 1 Indizes von sind Die Tabelle. Da die Tabelle 2 Elemente enthält, ist k = 0.

So können wir die Formel vereinfachen, um:

C '= v + C * (v1 - v)

Inlining die Werte der Tabelle, bleiben wir mit:

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

Noch eine Vereinfachung:

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

Die Spezifikation definiert [~ # ~] c [~ # ~] und C ' als RGB-Werte innerhalb der Grenzen von 0 -1 (im Gegensatz zu 0-255). Aus diesem Grund müssen wir die Werte vor der Berechnung verkleinern und anschließend wieder vergrößern.

So kommen wir zu unserer Umsetzung:

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

Zwischenspiel: @ Daves Brute-Force-Algorithmus

@ Daves Code generiert 176,66 Filterkombinationen, einschließlich:

  • 11 invert() Filter (0%, 10%, 20%, ..., 100%)
  • 11 sepia() Filter (0%, 10%, 20%, ..., 100%)
  • 20 saturate() Filter (5%, 10%, 15%, ..., 100%)
  • 73 hue-rotate() Filter (0 Grad, 5 Grad, 10 Grad, ..., 360 Grad)

Es berechnet Filter in der folgenden Reihenfolge:

_filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);
_

Anschließend werden alle berechneten Farben durchlaufen. Es stoppt, sobald eine generierte Farbe innerhalb der Toleranz gefunden wurde (alle RGB-Werte liegen innerhalb von 5 Einheiten von der Zielfarbe).

Dies ist jedoch langsam und ineffizient. So präsentiere ich meine eigene Antwort.

Implementierung von SPSA

Zuerst müssen wir eine loss-Funktion definieren, die die Differenz zwischen der von einer Filterkombination erzeugten Farbe und der Zielfarbe zurückgibt. Wenn die Filter perfekt sind, sollte die Verlustfunktion 0 zurückgeben.

Wir werden den Farbunterschied als die Summe von zwei Metriken messen:

  • RGB-Differenz, da das Ziel darin besteht, den nächsten RGB-Wert zu erzielen.
  • HSL-Differenz, da viele HSL-Werte Filtern entsprechen (z. B. der Farbton korreliert grob mit hue-rotate(), die Sättigung korreliert mit saturate() usw.). Dies führt den Algorithmus.

Die Verlustfunktion akzeptiert ein Argument - ein Array von Filterprozentsätzen.

Wir werden die folgende Filterreihenfolge verwenden:

_filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg) brightness(e%) contrast(f%);
_

Implementierung:

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

Wir werden versuchen, die Verlustfunktion so zu minimieren, dass:

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

Der SPSA Algorithmus ( Website , Weitere Informationen , Das Papier , Implementierungspapier , Referenzcode ) ist in dieser Hinsicht sehr gut. Es wurde entwickelt, um komplexe Systeme mit lokalen Minima, verrauschten/nichtlinearen/multivariaten Verlustfunktionen usw. zu optimieren. Es wurde verwendet, um Schach-Engines zu optimieren . Und im Gegensatz zu vielen anderen Algorithmen sind die Arbeiten, die dies beschreiben, tatsächlich nachvollziehbar (wenn auch mit großem Aufwand).

Implementierung:

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

Ich habe einige Änderungen/Optimierungen an SPSA vorgenommen:

  • Verwenden Sie das beste Ergebnis anstelle des letzten.
  • Wiederverwenden aller Arrays (deltas, highArgs, lowArgs), anstatt sie bei jeder Iteration neu zu erstellen.
  • Verwenden eines Array von Werten für a anstelle eines einzelnen Werts. Dies liegt daran, dass alle Filter unterschiedlich sind und sich daher mit unterschiedlichen Geschwindigkeiten bewegen/konvergieren sollten.
  • Ausführen einer fix -Funktion nach jeder Iteration. Alle Werte werden auf 0% bis 100% begrenzt, mit Ausnahme von saturate (maximal 7500%), brightness und contrast (maximal 200%). und hueRotate (wobei die Werte umbrochen und nicht geklemmt werden).

Ich benutze SPSA in einem zweistufigen Prozess:

  1. Die "weite" Bühne, die versucht, den Suchraum "zu erkunden". SPSA wird nur in begrenztem Umfang wiederholt, wenn die Ergebnisse nicht zufriedenstellend sind.
  2. Die "schmale" Bühne, die das beste Ergebnis aus der breiten Bühne holt und versucht, sie zu "verfeinern". Es werden dynamische Werte für [~ # ~] a [~ # ~] und a verwendet.

Implementierung:

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

Tuning SPSA

Warnung: Verwirren Sie den SPSA-Code nicht, insbesondere nicht die Konstanten, es sei denn, Sie wissen sure , was Sie tun.

Die wichtigen Konstanten sind [~ # ~] a [~ # ~] , a , c , die Anfangswerte, die Wiederholungsschwellenwerte, die Werte von max in fix() und die Anzahl der Iterationen jeder Stufe. Alle diese Werte wurden sorgfältig abgestimmt, um gute Ergebnisse zu erzielen, und das zufällige Verschrauben mit ihnen verringert fast definitiv den Nutzen des Algorithmus.

Wenn Sie darauf bestehen, es zu ändern, müssen Sie messen, bevor Sie "optimieren".

Wende zuerst diesen Patch an.

Führen Sie dann den Code in Node.js aus. Nach einiger Zeit sollte das Ergebnis ungefähr so ​​aussehen:

_Average loss: 3.4768521401985275
Average time: 11.4915ms
_

Stimmen Sie nun die Konstanten nach Herzenslust ab.

Einige Hinweise:

  • Der durchschnittliche Verlust sollte bei 4 liegen. Wenn er größer als 4 ist, werden zu weit entfernte Ergebnisse erzielt, und Sie sollten auf Genauigkeit achten. Wenn es weniger als 4 ist, wird Zeit verschwendet, und Sie sollten die Anzahl der Iterationen reduzieren.
  • Wenn Sie die Anzahl der Iterationen erhöhen/verringern, passen Sie [~ # ~] a [~ # ~] entsprechend an.
  • Wenn Sie [~ # ~] a [~ # ~] erhöhen/verringern, passen Sie a entsprechend an.
  • Verwenden Sie das Flag _--debug_, wenn Sie das Ergebnis jeder Iteration anzeigen möchten.

TL; DR

71
MultiplyByZer0

Dies war eine ziemliche Reise durch das Kaninchenloch, aber hier ist es!

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>

BEARBEITEN: Diese Lösung ist nicht für den Einsatz in der Produktion vorgesehen und zeigt nur einen Ansatz, mit dem erreicht werden kann, wonach OP verlangt. Es ist in einigen Bereichen des Farbspektrums schwach. Bessere Ergebnisse können erzielt werden, indem die schrittweisen Iterationen granularer ausgeführt werden oder aus den in @ MultiplyByZer0's answer ausführlich beschriebenen Gründen mehr Filterfunktionen implementiert werden.

EDIT2: OP sucht nach einer Lösung ohne Brute Force. In diesem Fall ist es ziemlich einfach, lösen Sie einfach diese Gleichung:

CSS Filter Matrix Equations

woher

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

Hinweis: OP hat mich gebeten, das Löschen rückgängig zu machen , aber das Kopfgeld geht an Daves Antwort.


Ich weiß, es ist nicht das, worauf wir im Hauptteil der Frage gewartet haben, aber es gibt einen CSS-Filter, der genau dies tut: drop-shadow()

Vorsichtsmaßnahmen:

  • Der Schatten wird hinter den vorhandenen Inhalten gezeichnet. Das bedeutet, dass wir einige absolute Positionierungs-Tricks machen müssen.
  • Alle Pixel werden gleich behandelt, aber OP sagte [das sollten wir nicht] "Kümmert sich darum, was mit anderen Farben als Schwarz passiert."
  • Browser-Unterstützung. (Da bin ich mir nicht sicher, nur unter den neuesten Versionen von FF und Chrome getestet).
/* 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

Sie können dies alles sehr einfach machen, indem Sie einfach einen SVG-Filter verwenden, auf den von CSS verwiesen wird. Sie benötigen nur eine einzige feColorMatrix, um eine Umfärbung durchzuführen. Dieser färbt sich nach gelb. Die fünfte Spalte in der feColorMatrix enthält die RGB-Zielwerte auf der Einheitenskala. (für gelb - es ist 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

Mir ist aufgefallen, dass das Beispiel für die Behandlung über einen SVG-Filter unvollständig war. Ich habe meins geschrieben (was perfekt funktioniert): (siehe die Antwort von Michael Mullany).

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>

Hier ist eine zweite Lösung: Verwenden Sie den SVG-Filter nur in 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