So transformieren Sie Schwarz mit nur CSS-Filtern in eine bestimmte Farbe


114

Meine Frage lautet: Wie lautet die Formel, um bei einer Ziel-RGB-Farbe Schwarz ( #000) nur mit CSS-Filtern in diese Farbe einzufärben ?

Damit eine Antwort akzeptiert werden kann, muss eine Funktion (in einer beliebigen Sprache) bereitgestellt werden, die die Zielfarbe als Argument akzeptiert und die entsprechende CSS- filterZeichenfolge zurückgibt .

Der Kontext hierfür ist die Notwendigkeit, eine SVG in a neu einzufärben background-image. 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, ist eine richtige Lösung:

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

( Demo )

Nichtziele

  • 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 akzeptierte Antwort erhalten, indem Sie eine Non-Brute-Force-Lösung einreichen!

Ressourcen

  • Wie hue-rotateund sepiawerden berechnet: https://stackoverflow.com/a/29521147/181228 Beispiel für eine 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 die clampoben genannten macht diehue-rotate Funktion nichtlinear ist.

    Browser-Implementierungen: Chromium , Firefox .

  • Demo: Von einer Graustufenfarbe zu einer Nicht-Graustufenfarbe gelangen: https://stackoverflow.com/a/25524145/181228

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

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


Sie möchten also # 000000 an #RRGGBB LERPEN? (Nur zur Klarstellung)
Zze

1
Ja, süß - nur um zu verdeutlichen, dass Sie keinen Übergang in die Lösung integrieren wollten.
Zze

1
Könnte ein Mischmodus für Sie funktionieren? Sie können Schwarz leicht in jede Farbe umwandeln ... Aber ich verstehe nicht das globale Bild von dem, was Sie erreichen wollen
vals

1
@glebm Sie müssen also eine Formel finden (mit einer beliebigen Methode), um Schwarz in eine beliebige Farbe umzuwandeln und mit CSS anzuwenden?
ProllyGeek

2
@ProllyGeek Ja. Eine weitere Einschränkung, die ich erwähnen sollte, ist, dass die resultierende Formel keine Brute-Force-Suche einer 5GiB-Tabelle sein kann (sie sollte beispielsweise von Javascript auf einer Webseite aus verwendbar sein).
Glebm

Antworten:


148

@ Dave war der erste, der eine Antwort gepostet hat darauf (mit ) veröffentlichte, und seine Antwort war eine unschätzbare Quelle für schamloses Kopieren und Einfügen für mich Inspiration. 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. Laut einem jsPerf-Benchmark für zufällig generierte RGB-Farben läuft der @ Dave-Algorithmus in 600 ms , während meiner in 30 ms läuft . Dies kann definitiv von Bedeutung sein, beispielsweise in der Ladezeit, in der die Geschwindigkeit kritisch ist.

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

  • Denn rgb(0,255,0)@ Dave's produziert rgb(29,218,34)und produziertrgb(1,255,0)
  • Denn rgb(0,0,255)@ Dave's produziert rgb(37,39,255)und meins produziertrgb(5,6,255)
  • Denn rgb(19,11,118)@ Dave's produziert rgb(36,27,102)und meins produziertrgb(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 ColorKlasse repräsentiert eine RGB-Farbe.
    • Seine toString()Funktion gibt die Farbe in einer CSS- rgb(...)Farbzeichenfolge zurück.
    • Seine hsl()Funktion gibt die in HSL konvertierte Farbe zurück .
    • Seine clamp()Funktion stellt sicher, dass ein bestimmter Farbwert innerhalb der Grenzen (0-255) liegt.
  • Die SolverKlasse wird versuchen, nach einer Zielfarbe zu suchen.
    • Seine css()Funktion gibt einen bestimmten Filter in einer CSS-Filterzeichenfolge zurück.

Die Implementierung grayscale(), sepia()undsaturate()

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 die Filter primitiven implementiert <feColorMatrix>, das führt Matrizenmultiplikation zwischen einer Matrix , die durch die Filter festgelegt (oft dynamisch erzeugt) und eine Matrix von der Farbe erzeugt. Diagramm:

Matrix-Multiplikation

Hier können wir einige Optimierungen vornehmen:

  • Das letzte Element der Farbmatrix ist und bleibt 1. Es macht keinen Sinn, es zu berechnen oder zu speichern.
  • Es macht auch keinen Sinn, 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 zuschneiden . Das spart ein bisschen Arbeit.
  • Alle <feColorMatrix>Filter lassen die Spalten 4 und 5 als Nullen. Daher können wir die Filtermatrix weiter auf 3x3 reduzieren .
  • Da die Multiplikation relativ einfach ist, müssen hierfür keine komplexen mathematischen Bibliotheken gezogen werden . 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 this.rusw. an nachfolgenden Berechnungen wünschen .)

Nun , da wir implementiert haben <feColorMatrix>, können wir umsetzen grayscale(), sepia()und saturate(), die einfach rufen Sie es mit einem bestimmten Filtermatrix:

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

Implementierung hue-rotate()

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

Die Filtermatrix wird wie folgt berechnet:

Zum Beispiel würde das Element a 00 folgendermaßen berechnet:

Einige Notizen:

  • Der Drehwinkel wird in Grad angegeben. Es muss in Bogenmaß umgerechnet werden, bevor es an Math.sin()oder übergeben wird Math.cos().
  • Math.sin(angle)und Math.cos(angle)sollte 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
    ]);
}

Implementierung brightness()undcontrast()

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

Jedes <feFuncX type="linear" />Element akzeptiert ein Steigungs- und Intercept- Attribut. Anschließend wird jeder neue Farbwert anhand 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 brightness()und contrast()auch implementiert werden kann:

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

Implementierung invert()

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

Die Spezifikation besagt:

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

Für "Tabelle" wird die Funktion durch lineare Interpolation zwischen den im Attribut tableValues angegebenen Werten definiert . Die Tabelle enthält n + 1 Werte (dh v 0 bis v n ), die die Start- und Endwerte für n gleichmäßig große Interpolationsbereiche angeben. Interpolationen verwenden die folgende Formel:

Für einen Wert C finde 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: [Wert, 1 - Wert]. 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 beträgt, ist n = 1.
  • Die Formel definiert k , wobei k und k + 1 Indizes der Tabelle sind. Da die Tabelle 2 Elemente enthält, ist k = 0.

Somit können wir die Formel vereinfachen, um:

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

Wenn wir die Werte der Tabelle einfügen, bleibt uns Folgendes übrig:

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 0-1 (im Gegensatz zu 0-255). Daher müssen wir die Werte vor der Berechnung verkleinern und danach wieder hochskalieren.

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.660 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-rotatedeg);

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

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

Implementierung von SPSA

Zunächst müssen wir eine Verlustfunktion definieren , die die Differenz zwischen der durch eine 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-Unterschied, weil das Ziel darin besteht, den nächstgelegenen RGB-Wert zu erzeugen.
  • HSL-Unterschied, da viele HSL-Werte Filtern entsprechen (z. B. Farbton korreliert grob mit hue-rotate(), 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-rotatedeg) 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 , Papier , Implementierungspapier , Referenzcode ) ist hier sehr gut. Es wurde entwickelt, um komplexe Systeme mit lokalen Minima, verrauschten / nichtlinearen / multivariaten Verlustfunktionen usw. zu optimieren. Es wurde verwendet, um Schach-Engines abzustimmen . Und im Gegensatz zu vielen anderen Algorithmen sind die Beschreibungen tatsächlich verständlich (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 alle Arrays ( deltas, highArgs, lowArgs), anstatt sie mit 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 fixFunktion nach jeder Iteration. Es klemmt alle Werte auf zwischen 0% und 100%, außer saturate(wo das Maximum 7500% beträgt) brightnessund contrast(wo das Maximum 200% beträgt) und hueRotate(wo die Werte umwickelt anstatt geklemmt werden).

Ich benutze SPSA in einem zweistufigen Prozess:

  1. Die "breite" Bühne, die versucht, den Suchraum zu "erkunden". SPSA wird nur begrenzt wiederholt, wenn die Ergebnisse nicht zufriedenstellend sind.
  2. Die "schmale" Bühne, die das beste Ergebnis von der breiten Bühne nimmt und versucht, es 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);
}

SPSA einstellen

Warnung: Spielen Sie nicht mit dem SPSA-Code, insbesondere nicht mit seinen Konstanten, es sei denn, Sie sind sicher , dass Sie wissen, was Sie tun.

Die wichtigen Konstanten sind A , a , c , die Anfangswerte, die Wiederholungsschwellenwerte, die Werte von maxin fix()und die Anzahl der Iterationen jeder Stufe. Alle diese Werte wurden sorgfältig abgestimmt, um gute Ergebnisse zu erzielen, und ein zufälliges Durchschrauben verringert mit ziemlicher Sicherheit die Nützlichkeit des Algorithmus.

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

Wenden Sie 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

Stellen Sie nun die Konstanten nach Herzenslust ein.

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 die Genauigkeit einstellen. 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 erhöhen / verringern A , stellen ein entsprechend.
  • Verwenden Sie das --debugFlag, wenn Sie das Ergebnis jeder Iteration sehen möchten.

TL; DR


3
Sehr schöne Zusammenfassung des Entwicklungsprozesses! Liest du meine Gedanken?!
Dave

1
@ Dave Eigentlich habe ich selbständig daran gearbeitet, aber du hast mich geschlagen.
MultiplyByZer0


3
Dies ist eine völlig verrückte Methode. Sie können eine Farbe direkt mit einem SVG-Filter (fünfte Spalte in einer feColorMatrix) festlegen und auf diesen Filter aus CSS verweisen - warum sollten Sie diese Methode nicht verwenden?
Michael Mullany

2
@ MichaelMullany Nun, das ist mir peinlich, wenn man bedenkt, wie lange ich daran gearbeitet habe. Ich habe nicht an Ihre Methode gedacht, aber jetzt verstehe ich - um ein Element in eine beliebige Farbe umzufärben, generieren Sie einfach dynamisch eine SVG mit einem <filter>a <feColorMatrix>mit den richtigen Werten (alle Nullen außer der letzten Spalte, die das Ziel-RGB enthält Werte 0 und 1), fügen Sie das SVG in das DOM ein und verweisen Sie auf den Filter aus CSS. Bitte schreiben Sie Ihre Lösung als Antwort (mit einer Demo), und ich werde abstimmen.
MultiplyByZer0

54

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, was OP verlangt. Wie es ist, ist es in einigen Bereichen des Farbspektrums schwach. Bessere Ergebnisse können durch mehr Granularität in den Schrittiterationen oder durch Implementierung weiterer Filterfunktionen aus Gründen erzielt werden, die in der Antwort von @ MultiplyByZer0 ausführlich beschrieben werden .

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

CSS-Filtermatrixgleichungen

wo

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

Wenn ich einsetze, 255,0,255meldet mein digitales Farbmessgerät das Ergebnis als #d619d9und nicht #ff00ff.
Siguza

@Siguza Es ist definitiv nicht perfekt. Kantenfarben können durch Anpassen der Grenzen in den Schleifen angepasst werden.
Dave

3
Diese Gleichung ist alles andere als "ziemlich einfach"
MultiplyByZer0

Ich denke, die obige Gleichung fehlt auch clamp?
Glebm

1
Klammer hat dort keinen Platz. Und soweit ich mich aus meiner College-Mathematik erinnere, werden diese Gleichungen durch numerische Berechnungen berechnet, auch bekannt als "Brute Force". Also viel Glück!
Dave

28

Hinweis: OP hat mich gebeten, die Löschung wiederherzustellen , aber das Kopfgeld geht an Daves Antwort.


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

Vorsichtsmaßnahmen:

  • Der Schatten wird hinter den vorhandenen Inhalten gezeichnet. Dies bedeutet, dass wir einige absolute Positionierungstricks machen müssen.
  • Alle Pixel werden gleich behandelt, aber OP sagte [wir sollten nicht sein] "Kümmere dich darum, was mit anderen Farben als Schwarz passiert."
  • Browser-Unterstützung. (Ich bin mir nicht sicher, nur unter den neuesten FF und Chrom 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>


1
Super klug, super! Das funktioniert für mich, schätze es
jaminroe

Ich glaube, dies ist eine bessere Lösung, da die Farbe jedes Mal 100% genau ist.
user835542

Der Ist-Code zeigt eine leere Seite (W10 FF 69b). An dem Symbol ist jedoch nichts auszusetzen (separate SVG aktiviert).
Rene van der Lende

Durch Hinzufügen background-color: black;zu .icon>spanfunktioniert dies für FF 69b. Symbol wird jedoch nicht angezeigt.
Rene van der Lende

@RenevanderLende Gerade auf FF70 ausprobiert funktioniert dort noch. Wenn es bei Ihnen nicht funktioniert, muss es etwas für Sie sein.
Kaiido

15

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 Neufärbung durchzuführen. Dieser färbt sich 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">


Eine interessante Lösung, aber es scheint nicht möglich zu sein, die Zielfarbe über CSS zu steuern.
Glebm

Sie müssen für jede Farbe, die Sie anwenden möchten, einen neuen Filter definieren. Aber es ist völlig genau. Farbton-Drehen ist eine Annäherung, bei der bestimmte Farben abgeschnitten werden - was bedeutet, dass Sie bestimmte Farben mit dieser Farbe nicht genau erzielen können -, wie die obigen Antworten bestätigen. Was wir wirklich brauchen, ist eine Abkürzung für recolor () CSS-Filter.
Michael Mullany

Die Antwort von MultiplyByZer0 berechnet eine Reihe von Filtern, die mit sehr hoher Genauigkeit erzielt werden, ohne HTML zu ändern. Ein wahrer hue-rotateBrowser wäre schön ja.
Glebm

2
Es scheint, dass dies nur dann genaue RGB-Farben für schwarze Quellbilder erzeugt, wenn Sie der feColorMatrix "Farbinterpolationsfilter" = "sRGB" hinzufügen.
John Smith

Rand 12-18 werden weggelassen , da sie die urlFunktion caniuse.com/#search=svg%20filter
Volker E

2

Ich habe festgestellt, dass das Beispiel für die Behandlung über einen SVG-Filter unvollständig war. Ich habe mein Beispiel geschrieben (was perfekt funktioniert): (siehe Antwort von Michael Mullany) Hier ist also der Weg, um jede gewünschte Farbe zu erhalten:

Hier ist eine zweite Lösung, bei der SVG-Filter nur im Code verwendet wird => URL.createObjectURL verwendet wird


1

benutz einfach

fill: #000000

Die fillEigenschaft in CSS dient zum Ausfüllen der Farbe einer SVG-Form. Die fillEigenschaft kann jeden CSS-Farbwert akzeptieren.


3
Dies funktioniert möglicherweise mit CSS innerhalb eines SVG-Bildes, jedoch nicht als CSS, das imgvom Browser extern auf ein Element angewendet wird .
David Moles

0

Ich habe mit dieser Antwort mit einem SVG-Filter begonnen und die folgenden Änderungen vorgenommen:

SVG-Filter aus Daten-URL

Wenn Sie den SVG-Filter nicht irgendwo in Ihrem Markup definieren möchten , können Sie stattdessen eine Daten-URL verwenden (ersetzen Sie R , G , B und A durch die gewünschte Farbe):

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

Graustufen-Fallback

Wenn die obige Version nicht funktioniert, können Sie auch einen Graustufen-Fallback hinzufügen.

Die Funktionen saturateund brightnessverwandeln jede Farbe in Schwarz (Sie müssen dies nicht einschließen, wenn die Farbe bereits schwarz ist), inverthellen sie dann mit der gewünschten Helligkeit ( L ) auf und optional können Sie auch die Deckkraft ( A ) angeben .

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

SCSS-Mixin

Wenn Sie die Farbe dynamisch angeben möchten, können Sie das folgende SCSS-Mixin verwenden:

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

Anwendungsbeispiel:

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

Vorteile:

  • Kein Javascript .
  • Keine zusätzlichen HTML-Elemente .
  • Wenn CSS-Filter unterstützt werden, der SVG-Filter jedoch nicht funktioniert, tritt ein Graustufen-Fallback auf .
  • Wenn Sie das Mixin verwenden, ist die Verwendung ziemlich einfach (siehe Beispiel oben).
  • Die Farbe ist besser lesbar und einfacher zu ändern als der Sepia-Trick (RGBA-Komponenten in reinem CSS und Sie können sogar HEX-Farben in SCSS verwenden).
  • Vermeidet das seltsame Verhalten vonhue-rotate .

Vorsichtsmaßnahmen:

  • Nicht alle Browser unterstützen SVG-Filter von einer Daten-URL (insbesondere dem ID-Hash), aber es funktioniert in aktuellen Firefox- und Chromium-Browsern (und möglicherweise auch in anderen).
  • Wenn Sie die Farbe dynamisch angeben möchten, müssen Sie ein SCSS-Mixin verwenden.
  • Die reine CSS-Version ist etwas hässlich. Wenn Sie viele verschiedene Farben möchten, müssen Sie die SVG-Datei mehrmals einschließen.
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.