Jak przekształcić czerń w dowolny kolor, używając tylko filtrów CSS


116

Moje pytanie brzmi: biorąc pod uwagę docelowy kolor RGB, jaka jest formuła ponownego pokolorowania czerni ( #000) na ten kolor przy użyciu tylko filtrów CSS ?

Aby odpowiedź została zaakceptowana, musiałaby dostarczyć funkcję (w dowolnym języku), która zaakceptowałaby kolor docelowy jako argument i zwróciłaby odpowiedni filterciąg CSS .

Kontekstem tego jest potrzeba ponownego kolorowania SVG w pliku background-image. W tym przypadku ma obsługiwać pewne funkcje matematyczne TeX w KaTeX: https://github.com/Khan/KaTeX/issues/587 .

Przykład

Jeśli docelowy kolor to #ffff00(żółty), jednym poprawnym rozwiązaniem jest:

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

( wersja demonstracyjna )

Nie-cele

  • Animacja.
  • Rozwiązania bez filtrów CSS.
  • Począwszy od koloru innego niż czarny.
  • Dbanie o to, co stanie się z kolorami innymi niż czarny.

Dotychczasowe wyniki

Nadal możesz uzyskać zaakceptowaną odpowiedź, przesyłając rozwiązanie inne niż brutalne!

Zasoby

  • Jak hue-rotatei sepiasą obliczane: https://stackoverflow.com/a/29521147/181228 Przykładowa implementacja Rubiego:

    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

    Zauważ, że clamppowyższe sprawia, że hue-rotatefunkcja jest nieliniowa.

    Implementacje przeglądarek: Chromium , Firefox .

  • Demo: przejście do koloru innego niż w skali szarości z koloru w skali szarości: https://stackoverflow.com/a/25524145/181228

  • Formuła, która prawie działa (z podobnego pytania ):
    https://stackoverflow.com/a/29958459/181228

    Szczegółowe wyjaśnienie, dlaczego powyższa formuła jest błędna (CSS hue-rotatenie jest prawdziwą rotacją odcieni, ale liniowym przybliżeniem):
    https://stackoverflow.com/a/19325417/2441511


Więc chcesz LERP # 000000 do #RRGGBB? (Tylko wyjaśnienie)
Zze

1
Tak, słodkie - po prostu wyjaśniam, że nie chcesz uwzględniać przejścia do rozwiązania.
Zze

1
Może odpowiedni tryb mieszania będzie dla Ciebie odpowiedni? Możesz łatwo przekonwertować czarny na dowolny kolor ... Ale nie mam globalnego obrazu tego, co chcesz osiągnąć
vals

1
@glebm, więc musisz znaleźć formułę (dowolną metodą), aby zamienić czarny na dowolny kolor i zastosować go za pomocą CSS?
ProllyGeek

2
@ProllyGeek Yes. Kolejnym ograniczeniem, o którym powinienem wspomnieć, jest to, że wynikowa formuła nie może być brutalnym przeszukiwaniem tabeli 5GiB (powinna być używana np. Z javascript na stronie internetowej).
glebm

Odpowiedzi:


150

@Dave był pierwszym, który opublikował odpowiedź na to pytanie (z działającym kodem), a jego odpowiedź była nieocenionym źródłem bezwstydnego kopiowania i wklejania dla mnie inspiracji. Ten post rozpoczął się jako próba wyjaśnienia i udoskonalenia odpowiedzi @ Dave, ale od tego czasu przekształcił się w własną odpowiedź.

Moja metoda jest znacznie szybsza. Zgodnie z benchmarkiem jsPerf dotyczącym losowo generowanych kolorów RGB, algorytm @ Dave działa w 600 ms , podczas gdy mój działa w 30 ms . Może to z pewnością mieć znaczenie, na przykład w czasie ładowania, gdzie szybkość ma kluczowe znaczenie.

Ponadto w przypadku niektórych kolorów mój algorytm działa lepiej:

  • Bo rgb(0,255,0)@ Dave's produkuje rgb(29,218,34)i produkujergb(1,255,0)
  • Bo rgb(0,0,255)@ Dave's produkuje, rgb(37,39,255)a mój produkujergb(5,6,255)
  • Bo rgb(19,11,118)@ Dave's produkuje, rgb(36,27,102)a mój produkujergb(20,11,112)

Próbny

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


Stosowanie

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

Wyjaśnienie

Zaczniemy od napisania 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)}%);`;
    }
}

Wyjaśnienie:

  • ColorKlasa reprezentuje barwy RGB.
    • Jego toString()funkcja zwraca kolor w rgb(...)łańcuchu kolorów CSS .
    • Jego hsl()funkcja zwraca kolor, przekonwertowany na HSL .
    • Jego clamp()funkcja zapewnia, że ​​dana wartość koloru mieści się w granicach (0-255).
  • SolverKlasa będzie próbował rozwiązać za pomocą koloru docelowego.
    • Jego css()funkcja zwraca podany filtr w ciągu filtru CSS.

Wykonawczych grayscale(), sepia()orazsaturate()

Sercem filtrów CSS / SVG są prymitywy filtrów , które reprezentują niskopoziomowe modyfikacje obrazu.

Filtry grayscale(), sepia()i saturate()są implementowane przez element pierwotny filtru <feColorMatrix>, który wykonuje mnożenie macierzy między macierzą określoną przez filtr (często generowaną dynamicznie) a macierzą utworzoną z koloru. Diagram:

Mnożenie macierzy

Istnieje kilka optymalizacji, które możemy tutaj wprowadzić:

  • Ostatnim elementem matrycy kolorów jest i zawsze będzie 1. Nie ma sensu go obliczać ani przechowywać.
  • Nie ma też sensu obliczać ani przechowywać wartości alfa / przezroczystości ( A), ponieważ mamy do czynienia z RGB, a nie RGBA.
  • Dlatego możemy przycinać matryce filtrów od 5x5 do 3x5, a matrycę kolorów od 1x5 do 1x3 . Oszczędza to trochę pracy.
  • Wszystkie <feColorMatrix>filtry pozostawiają kolumny 4 i 5 jako zera. Dlatego możemy dodatkowo zmniejszyć matrycę filtra do 3x3 .
  • Ponieważ mnożenie jest stosunkowo proste, nie ma potrzeby przeciągania do tego skomplikowanych bibliotek matematycznych . Możemy sami zaimplementować algorytm mnożenia macierzy.

Realizacja:

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

(Używamy zmiennych tymczasowych do przechowywania wyników mnożenia każdego wiersza, ponieważ nie chcemy, aby zmiany this.ritp. Miały wpływ na kolejne obliczenia).

Teraz, wdrożyliśmy <feColorMatrix>możemy wdrożyć grayscale(), sepia()i saturate(), co po prostu wywołać ją z danej matrycy filtra:

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

Realizowanie hue-rotate()

hue-rotate()Filtr jest realizowany przez <feColorMatrix type="hueRotate" />.

Macierz filtrów jest obliczana w sposób pokazany poniżej:

Na przykład element a 00 zostałby obliczony w następujący sposób:

Kilka uwag:

  • Kąt obrotu podawany jest w stopniach. Musi zostać przekonwertowany na radiany przed przekazaniem do Math.sin()lub Math.cos().
  • Math.sin(angle)i Math.cos(angle)powinno być obliczone raz, a następnie zapisane w pamięci podręcznej.

Realizacja:

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

Wdrażanie brightness()icontrast()

brightness()I contrast()filtry są realizowane przez <feComponentTransfer>z <feFuncX type="linear" />.

Każdy <feFuncX type="linear" />element przyjmuje atrybut nachylenia i przecięcia . Następnie oblicza każdą nową wartość koloru za pomocą prostej formuły:

value = slope * value + intercept

Jest to łatwe do wdrożenia:

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

Po wdrożeniu brightness()i contrast()można to również zaimplementować:

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

Realizowanie invert()

invert()Filtr jest realizowany przez <feComponentTransfer>z <feFuncX type="table" />.

Specyfikacja stwierdza:

W dalszej części C to komponent początkowy, a C ' to komponent przemapowany; obie w przedziale zamkniętym [0,1].

W przypadku „tabeli” funkcja jest definiowana przez interpolację liniową między wartościami podanymi w atrybucie tableValues . Tabela zawiera n + 1 wartości (tj. V 0 do v n ) określających wartości początkowe i końcowe dla n obszarów interpolacji o równych rozmiarach. W interpolacji stosuje się następujący wzór:

Dla wartości C znajdź k takie, że:

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

Wynik C ' daje:

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

Wyjaśnienie tego wzoru:

  • invert()Filtr wyznacza poniższej tabeli: [wartość, 1 - wartość]. To jest tableValues lub v .
  • Formuła definiuje n , tak że n + 1 jest długością tabeli. Ponieważ długość stołu wynosi 2, n = 1.
  • Formuła definiuje k , gdzie k i k + 1 są indeksami tabeli. Ponieważ tabela ma 2 elementy, k = 0.

W ten sposób możemy uprościć wzór do:

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

Podkreślając wartości tabeli, pozostaje:

C '= wartość + C * (1 - wartość - wartość)

Jeszcze jedno uproszczenie:

C '= wartość + C * (1 - 2 * wartość)

Specyfikacja definiuje C i C ' jako wartości RGB, w granicach 0-1 (w przeciwieństwie do 0-255). W rezultacie musimy zmniejszyć wartości przed obliczeniem, a następnie przeskalować je ponownie.

W ten sposób dochodzimy do naszej realizacji:

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

Interludium: algorytm brutalnej siły @ Dave'a

Kod @ Dave'a generuje 176,660 kombinacji filtrów, w tym:

  • 11 invert()filtrów (0%, 10%, 20%, ..., 100%)
  • 11 sepia()filtrów (0%, 10%, 20%, ..., 100%)
  • 20 saturate()filtrów (5%, 10%, 15%, ..., 100%)
  • 73 hue-rotate()filtry (0 stopni, 5 stopni, 10 stopni, ..., 360 stopni)

Oblicza filtry w następującej kolejności:

filter: invert(a%) sepia(b%) saturate(c%) hue-rotatedeg);

Następnie wykonuje iterację przez wszystkie obliczone kolory. Zatrzymuje się, gdy znajdzie wygenerowany kolor z tolerancją (wszystkie wartości RGB mieszczą się w granicach 5 jednostek od koloru docelowego).

Jest to jednak powolne i nieefektywne. Dlatego przedstawiam własną odpowiedź.

Wdrażanie SPSA

Najpierw musimy zdefiniować funkcję strat , która zwraca różnicę między kolorem wytworzonym przez kombinację filtrów a kolorem docelowym. Jeśli filtry są doskonałe, funkcja utraty powinna zwrócić 0.

Różnicę kolorów zmierzymy jako sumę dwóch wskaźników:

  • Różnica RGB, ponieważ celem jest uzyskanie najbliższej wartości RGB.
  • Różnica HSL, ponieważ wiele wartości HSL odpowiada filtrom (np. Barwa jest z grubsza skorelowana hue-rotate(), nasycenie jest skorelowane z saturate()itp.) To prowadzi algorytm.

Funkcja straty przyjmie jeden argument - tablicę wartości procentowych filtrów.

Użyjemy następującej kolejności filtrów:

filter: invert(a%) sepia(b%) saturate(c%) hue-rotatedeg) brightness(e%) contrast(f%);

Realizacja:

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

Postaramy się zminimalizować funkcję straty, tak aby:

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

SpsA algorytm ( strona internetowa , więcej informacji , papier , papier realizacja , kod referencyjny ) jest bardzo dobry w tym. Został zaprojektowany w celu optymalizacji złożonych systemów z lokalnymi minimami, zaszumionymi / nieliniowymi / wielowymiarowymi funkcjami strat itp. Został użyty do strojenia silników szachowych . I w przeciwieństwie do wielu innych algorytmów, opisujące go artykuły są w rzeczywistości zrozumiałe (aczkolwiek z dużym wysiłkiem).

Realizacja:

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

Dokonałem pewnych modyfikacji / optymalizacji w SPSA:

  • Korzystanie z najlepszego uzyskanego wyniku zamiast ostatniego.
  • Ponowne użycie wszystkich tablic ( deltas, highArgs, lowArgs), zamiast odtworzyć je z każdej iteracji.
  • Użycie tablicy wartości dla a zamiast pojedynczej wartości. Dzieje się tak, ponieważ wszystkie filtry są różne i dlatego powinny poruszać się / zbierać z różnymi prędkościami.
  • Uruchamianie fixfunkcji po każdej iteracji. Obcina wszystkie wartości od 0% do 100%, z wyjątkiem saturate(gdzie maksimum to 7500%) brightnessi contrast(gdzie maksimum to 200%) i hueRotate(gdzie wartości są zawijane zamiast zaciśniętych).

Używam SPSA w dwuetapowym procesie:

  1. „Szeroka” scena, która próbuje „eksplorować” przestrzeń poszukiwań. Jeśli wyniki nie będą zadowalające, podejmie ograniczone próby SPSA.
  2. Scena „wąska”, która najlepiej sprawdza się na scenie szerokiej i próbuje ją „uszlachetnić”. Używa wartości dynamicznych dla A i a .

Realizacja:

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

Ostrzeżenie: nie mieszaj kodu SPSA, zwłaszcza jego stałych, chyba że jesteś pewien, że wiesz, co robisz.

Ważnymi stałymi są A , a , c , wartości początkowe, progi ponownych prób, wartości maxin fix()oraz liczba iteracji każdego etapu. Wszystkie te wartości zostały starannie dostrojone, aby uzyskać dobre wyniki, a przypadkowe wkręcanie się z nimi prawie na pewno zmniejszy użyteczność algorytmu.

Jeśli nalegasz na jego zmianę, musisz dokonać pomiaru przed „optymalizacją”.

Najpierw zastosuj tę poprawkę .

Następnie uruchom kod w Node.js. Po pewnym czasie wynik powinien wyglądać mniej więcej tak:

Average loss: 3.4768521401985275
Average time: 11.4915ms

Teraz dostrój stałe do zadowolenia twojego serca.

Kilka porad:

  • Średnia strata powinna wynosić około 4. Jeśli jest większa niż 4, wyniki są zbyt odległe i należy dostroić je pod kątem dokładności. Jeśli jest mniej niż 4, marnuje się czas i powinieneś zmniejszyć liczbę iteracji.
  • Jeśli zwiększysz / zmniejszysz liczbę iteracji, dostosuj odpowiednio A.
  • Jeśli zwiększyć / zmniejszyć A wyregulować odpowiednio.
  • Użyj --debugflagi, jeśli chcesz zobaczyć wynik każdej iteracji.

TL; DR


3
Bardzo fajne podsumowanie procesu tworzenia! Czytasz moje myśli ?!
Dave

1
@Dave Właściwie pracowałem nad tym niezależnie, ale pokonałeś mnie.
MultiplyByZer0

4

3
To całkowicie szalona metoda. Możesz ustawić kolor bezpośrednio za pomocą filtra SVG (piąta kolumna w feColorMatrix) i możesz odwołać się do tego filtra z CSS - dlaczego nie miałbyś użyć tej metody?
Michael Mullany

2
@MichaelMullany Cóż, to dla mnie żenujące, biorąc pod uwagę, jak długo nad tym pracowałem. Nie pomyślałem o twojej metodzie, ale teraz rozumiem - aby zmienić kolor elementu na dowolny kolor, po prostu dynamicznie generujesz plik SVG <filter>zawierający a <feColorMatrix>z odpowiednimi wartościami (wszystkie zera z wyjątkiem ostatniej kolumny, która zawiera docelowe RGB wartości, 0 i 1), wstaw SVG do DOM i odwołaj się do filtra z CSS. Napisz swoje rozwiązanie jako odpowiedź (z demo), a ja zagłosuję za.
MultiplyByZer0

55

To była niezła wycieczka w dół króliczej nory, ale oto jest!

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>

EDYCJA: To rozwiązanie nie jest przeznaczone do użytku produkcyjnego i ilustruje jedynie podejście, które można zastosować, aby osiągnąć to, o co prosi OP. W rzeczywistości jest słaby w niektórych obszarach spektrum kolorów. Lepsze wyniki można osiągnąć dzięki większej szczegółowości w iteracjach krokowych lub implementacji większej liczby funkcji filtrujących z powodów opisanych szczegółowo w odpowiedzi @ MultiplyByZer0 .

EDIT2: OP szuka rozwiązania innego niż brutalna siła. W takim przypadku jest to całkiem proste, po prostu rozwiąż to równanie:

Równania macierzy filtru CSS

gdzie

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

Jeśli wstawię 255,0,255, mój cyfrowy kolorowy miernik zgłasza wynik jako #d619d9zamiast #ff00ff.
Siguza

@Siguza To zdecydowanie nie jest idealne, kolory krawędzi można dostosować, dostosowując granice w pętlach.
Dave

3
To równanie nie jest wcale „całkiem proste”
MultiplyByZer0

Myślę, że powyższego równania również brakuje clamp?
glebm

1
Zacisk nie ma tam miejsca. A z tego, co pamiętam z matematyki na uczelni, równania te są obliczane za pomocą obliczeń numerycznych, czyli „brutalnej siły”, więc powodzenia!
Dave

28

Uwaga: OP poprosił mnie o cofnięcie usunięcia , ale nagroda zostanie przyznana za odpowiedź Dave'a.


Wiem, że nie o to pytano w treści pytania, a już na pewno nie o to, na co wszyscy czekaliśmy, ale jest jeden filtr CSS, który robi dokładnie to: drop-shadow()

Ostrzeżenia:

  • Cień jest rysowany za istniejącą zawartością. Oznacza to, że musimy zrobić pewne sztuczki związane z pozycjonowaniem.
  • Wszystkie piksele będą traktowane tak samo, ale OP powiedział [nie powinniśmy] „Dbać o to, co stanie się z kolorami innymi niż czarny”.
  • Wsparcie przeglądarki. (Nie jestem tego pewien, testowałem tylko pod najnowszymi FF i chrome).

/* the container used to hide the original bg */

.icon {
  width: 60px;
  height: 60px;
  overflow: hidden;
}


/* the content */

.icon.green>span {
  -webkit-filter: drop-shadow(60px 0px green);
  filter: drop-shadow(60px 0px green);
}

.icon.red>span {
  -webkit-filter: drop-shadow(60px 0px red);
  filter: drop-shadow(60px 0px red);
}

.icon>span {
  -webkit-filter: drop-shadow(60px 0px black);
  filter: drop-shadow(60px 0px black);
  background-position: -100% 0;
  margin-left: -60px;
  display: block;
  width: 61px; /* +1px for chrome bug...*/
  height: 60px;
  background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgOTAgOTAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDkwIDkwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48Zz48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTYxLjUxMSwyNi4xNWMtMC43MTQtMS43MzgtMS43MjMtMy4yOTgtMy4wMjYtNC42NzkgICBjLTEuMzAzLTEuMzY2LTIuODA5LTIuNDUyLTQuNTE1LTMuMjU5Yy0xLjc1NC0wLjgyMi0zLjYwMS0xLjI4OC01LjU0LTEuMzk2Yy0wLjI4LTAuMDMxLTAuNTUyLTAuMDQ3LTAuODE0LTAuMDQ3ICAgYy0wLjAxOCwwLTAuMDMxLDAtMC4wNDcsMGMtMC4zMjcsMC4wMTYtMC41NzQsMC4wMjMtMC43NDUsMC4wMjNjLTEuOTcxLDAuMTA4LTMuODQxLDAuNTc0LTUuNjA5LDEuMzk3ICAgYy0xLjcwOCwwLjgwNy0zLjIxMiwxLjg5My00LjUxNywzLjI1OWMtMS4zMTgsMS4zODEtMi4zMjcsMi45NDgtMy4wMjYsNC43MDJ2LTAuMDIzYy0wLjc0NCwxLjgxNS0xLjExOCwzLjcxNi0xLjExOCw1LjcwMiAgIGMtMC4wMTUsMi4wNjQsMC41MzcsNC4xODIsMS42NTQsNi4zNTVjMC41NzQsMS4xMzMsMS4yOTUsMi4yNSwyLjE2NCwzLjM1MmMwLjQ4MiwwLjYwNSwxLjAwMiwxLjIxLDEuNTYsMS44MTYgICBjMC4wMzEsMC4wMTYsMC4wNTUsMC4wMzksMC4wNzEsMC4wN2MwLjUyNywwLjQ5NiwwLjg5MiwwLjk3OCwxLjA5MywxLjQ0M2MwLjEwOCwwLjIzMywwLjE3OSwwLjUyLDAuMjEsMC44NjIgICBjMC4wNDYsMC4zNzEsMC4wNjksMC44MjIsMC4wNjksMS4zNXYxLjA0OGMwLDAuNjIsMC4xMTcsMS4yMTgsMC4zNDksMS43OTJjMC4yMzQsMC41NDMsMC41NiwxLjAyNCwwLjk3OCwxLjQ0M2gwLjAyNSAgIGMwLjQxOCwwLjQxOSwwLjg5MiwwLjc0NSwxLjQyLDAuOTc3aDAuMDIzYzAuNTU4LDAuMjQ5LDEuMTQ4LDAuMzczLDEuNzY5LDAuMzczaDcuMjg3YzAuNjIsMCwxLjIwOS0wLjEyNCwxLjc2OS0wLjM3MyAgIGMwLjU0My0wLjIzMSwxLjAyMy0wLjU1OCwxLjQ0My0wLjk3N2MwLjQxOC0wLjQxOSwwLjc0My0wLjksMC45NzgtMS40NDNjMC4yNDgtMC41NzQsMC4zNzEtMS4xNzIsMC4zNzEtMS43OTJ2LTEuMDQ4ICAgYzAtMC41MjcsMC4wMjMtMC45NzksMC4wNzEtMS4zNWMwLjAyOS0wLjM0MiwwLjA5Mi0wLjYzNywwLjE4Ni0wLjg4NWMwLjEwOC0wLjIzMywwLjI2NC0wLjQ3MywwLjQ2Ni0wLjcyMnYtMC4wMjMgICBjMC4xODctMC4yMzMsMC40MDMtMC40NjYsMC42NTEtMC42OTljMC4wMTYtMC4wMTYsMC4wMzEtMC4wMywwLjA0Ny0wLjA0NmMwLjU3NC0wLjYwNSwxLjEwMy0xLjIxLDEuNTgzLTEuODE2ICAgYzAuODY4LTEuMTAyLDEuNTkxLTIuMjE5LDIuMTY1LTMuMzUyYzEuMTE3LTIuMTczLDEuNjY3LTQuMjkxLDEuNjUyLTYuMzU1QzYyLjYwNSwyOS44NTksNjIuMjQsMjcuOTY2LDYxLjUxMSwyNi4xNXogICAgTTgxLjc4NSw0My4xNDJjMCw2Ljg3NS0xLjc1MywxMy4wMi01LjI2MSwxOC40MzZjLTEuMzgxLDIuMTQxLTMuMDMyLDQuMTY3LTQuOTU4LDYuMDc1Yy02Ljc1LDYuNzk3LTE0LjkxMywxMC4xOTUtMjQuNDg2LDEwLjE5NSAgIGMtNi40NTcsMC0xMi4yOTItMS41NDQtMTcuNTA1LTQuNjMyYy0wLjI0OSwwLjI5NS0wLjU2LDAuNTI3LTAuOTMyLDAuNjk4bC0xNi4xMzEsNy42NThjLTAuNTEyLDAuMjMzLTEuMDQ3LDAuMzAzLTEuNjA2LDAuMjEgICBjLTAuNTU5LTAuMDk0LTEuMDQtMC4zNDItMS40NDMtMC43NDVjLTAuNDA0LTAuNDAzLTAuNjUyLTAuODg2LTAuNzQ2LTEuNDQzYy0wLjA5My0wLjU2LTAuMDIzLTEuMDk0LDAuMjEtMS42MDVsNy42NTgtMTYuMjcxICAgYzAuMTQtMC4zMTEsMC4zMzQtMC41NzQsMC41ODMtMC43OTJjLTMuMTk3LTUuMjYxLTQuNzk2LTExLjE4OC00Ljc5Ni0xNy43ODRjMC05LjYyMSwzLjM3Ni0xNy44MDcsMTAuMTI1LTI0LjU1OCAgIGMwLjUyOC0wLjUyNywxLjA3MS0xLjA0LDEuNjMtMS41MzZjMi4yMDQtMS45NTYsNC41MzktMy41Nyw3LjAwNi00Ljg0MkMzNS45NDUsOS42OTIsNDEuMjYsOC40MzYsNDcuMDgsOC40MzYgICBjOS41NzMsMCwxNy43MzYsMy4zODIsMjQuNDg2LDEwLjE0OGM2LjQyNiw2LjM3OCw5LjgyNCwxNC4wMjksMTAuMTk1LDIyLjk1MkM4MS43NzgsNDIuMDYzLDgxLjc4NSw0Mi41OTksODEuNzg1LDQzLjE0MnogICAgTTUxLjM4NiwyNS4yNjZjLTAuNzE0LTAuMzI2LTEuNDU5LTAuNTEzLTIuMjM1LTAuNTU5Yy0wLjQ4LTAuMDMxLTAuODc2LTAuMjI1LTEuMTg4LTAuNTgzYy0wLjMxMS0wLjM0LTAuNDU3LTAuNzUyLTAuNDQxLTEuMjMzICAgYzAuMDMxLTAuNDY2LDAuMjI1LTAuODU0LDAuNTgyLTEuMTY1YzAuMzU3LTAuMzEsMC43NjktMC40NTcsMS4yMzQtMC40NDFjMS4yMjYsMC4wNzcsMi4zOTcsMC4zOCwzLjUxNSwwLjkwNyAgIGMxLjA2OSwwLjQ5NywyLjAxOCwxLjE3OSwyLjg0LDIuMDQ5YzAuODA3LDAuODY5LDEuNDM1LDEuODU0LDEuODg0LDIuOTU2YzAuNDY2LDEuMTMzLDAuNjk5LDIuMzIsMC42OTksMy41NjIgICBjMCwwLjQ2NS0wLjE3MSwwLjg2OS0wLjUxMiwxLjIxYy0wLjMyNSwwLjMyNi0wLjcyMiwwLjQ4OS0xLjE4OCwwLjQ4OWMtMC40OCwwLTAuODg0LTAuMTYzLTEuMjEtMC40ODkgICBjLTAuMzQyLTAuMzQxLTAuNTEzLTAuNzQ2LTAuNTEzLTEuMjFjMC0wLjc5Mi0wLjE0Ni0xLjU1Mi0wLjQ0MS0yLjI4MWMtMC4yNzktMC42OTktMC42ODMtMS4zMjctMS4yMTEtMS44ODYgICBTNTIuMDY3LDI1LjU5MSw1MS4zODYsMjUuMjY2eiBNNTcuNzg3LDM1LjM2OGMwLDAuNTEyLTAuMTg4LDAuOTU0LTAuNTYsMS4zMjZjLTAuMzU2LDAuMzU3LTAuOCwwLjUzNi0xLjMyNiwwLjUzNiAgIGMtMC41MTIsMC0wLjk0Ni0wLjE3OS0xLjMwMy0wLjUzNmMtMC4zNzQtMC4zNzItMC41Ni0wLjgxNC0wLjU2LTEuMzI2YzAtMC41MTMsMC4xODYtMC45NTYsMC41Ni0xLjMyNyAgIGMwLjM1Ni0wLjM1NywwLjc5MS0wLjUzNiwxLjMwMy0wLjUzNmMwLjUyNiwwLDAuOTcsMC4xNzgsMS4zMjYsMC41MzZDNTcuNiwzNC40MTMsNTcuNzg3LDM0Ljg1NSw1Ny43ODcsMzUuMzY4eiBNNTEuODk3LDU0LjcxMSAgIEg0My40Yy0wLjcxMiwwLTEuMzE4LDAuMjU2LTEuODE1LDAuNzY5Yy0wLjUxMiwwLjQ5Ny0wLjc2OSwxLjA5NC0wLjc2OSwxLjc5MmMwLDAuNzE0LDAuMjQ5LDEuMzE5LDAuNzQ2LDEuODE1bDAuMDIzLDAuMDI0ICAgYzAuNDk3LDAuNDk2LDEuMTAzLDAuNzQ0LDEuODE1LDAuNzQ0aDguNDk3YzAuNzE1LDAsMS4zMTgtMC4yNDgsMS44MTUtMC43NDRjMC40OTctMC41MTMsMC43NDUtMS4xMjYsMC43NDUtMS44NCAgIGMwLTAuNjk4LTAuMjQ4LTEuMjk1LTAuNzQ1LTEuNzkydi0wLjAyM0M1My4yMDEsNTQuOTU5LDUyLjU5Niw1NC43MTEsNTEuODk3LDU0LjcxMXogTTQyLjcyNiw2Mi40MzhoLTAuMDIzICAgYy0wLjQ5NywwLjQ5Ny0wLjc0NSwxLjEwMy0wLjc0NSwxLjgxNnMwLjI1NywxLjMxOCwwLjc2OSwxLjgxNWMwLjQ5NywwLjQ5NywxLjEwMiwwLjc0NSwxLjgxNiwwLjc0NWg2LjEyMiAgIGMwLjY5NywwLDEuMjk1LTAuMjQ4LDEuNzkyLTAuNzQ1aDAuMDIyYzAuNDk3LTAuNDk3LDAuNzQ2LTEuMTAyLDAuNzQ2LTEuODE1cy0wLjI0OS0xLjMxOS0wLjc0Ni0xLjgxNiAgIGMtMC41MTItMC41MTItMS4xMTctMC43NjgtMS44MTQtMC43NjhoLTYuMTIyQzQzLjgyOCw2MS42NzEsNDMuMjIzLDYxLjkyNyw0Mi43MjYsNjIuNDM4eiIvPjwvZz48L3N2Zz4=);
}
<div class="icon">
  <span></span>
</div>
<div class="icon green">
  <span></span>
</div>
<div class="icon red">
  <span></span>
</div>


1
Super sprytny, niesamowity! To działa dla mnie, doceniam to
jaminroe

Uważam, że jest to lepsze rozwiązanie, ponieważ za każdym razem jest w 100% dokładne z kolorem.
user835542

Kod w stanie obecnym pokazuje pustą stronę (W10 FF 69b). Nie ma jednak nic złego w ikonie (zaznaczone osobne pliki SVG).
Rene van der Lende

Dodanie background-color: black;do .icon>spanpowoduje, że działa to dla FF 69b. Jednak nie wyświetla ikony.
Rene van der Lende

@RenevanderLende Właśnie wypróbowany na FF70 nadal tam działa. Jeśli to nie zadziała, to musi być coś na twoim końcu.
Kaiido

15

Możesz to wszystko bardzo uprościć, używając po prostu filtra SVG, do którego odwołuje się CSS. Potrzebujesz tylko jednego feColorMatrix, aby wykonać ponowne kolorowanie. Ten zmienia kolor na żółty. Piąta kolumna w feColorMatrix zawiera docelowe wartości RGB w skali jednostkowej. (dla żółtego - 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">


Ciekawe rozwiązanie, ale wydaje się, że nie pozwala na kontrolowanie docelowego koloru przez CSS.
glebm

Musisz zdefiniować nowy filtr dla każdego koloru, który chcesz zastosować. Ale jest w pełni dokładny. hue-rotate to przybliżenie, które przycina określone kolory - co oznacza, że ​​nie można ich dokładnie uzyskać, używając go - jak potwierdzają powyższe odpowiedzi. To, czego naprawdę potrzebujemy, to skrótowy filtr CSS recolor ().
Michael Mullany

Odpowiedź MultiplyByZer0 oblicza serię filtrów, które osiągają bardzo dużą dokładność, bez modyfikowania HTML. Prawda hue-rotatew przeglądarkach byłaby fajna.
glebm

2
wygląda na to, że daje to dokładne kolory RGB tylko dla czarnych obrazów źródłowych, gdy dodasz „color-interpolation-filters” = „sRGB” do feColorMatrix.
John Smith

Edge 12-18 zostały pominięte, ponieważ nie obsługują urlfunkcji caniuse.com/#search=svg%20filter
Volker E.

2

Zauważyłem, że przykład obróbki przez filtr SVG był niekompletny, napisałem swój (który działa idealnie): (patrz odpowiedź Michaela Mullany'ego), więc oto sposób na uzyskanie dowolnego koloru:

Oto drugie rozwiązanie, używając filtru SVG tylko w code => URL.createObjectURL


1

po prostu użyj

fill: #000000

fillNieruchomość w CSS jest do wypełnienia w kolorze kształcie SVG. fillNieruchomość może przyjąć dowolną wartość koloru CSS.


3
Może to działać z CSS wewnętrznym obrazem SVG, ale nie działa jako CSS zastosowany zewnętrznie do imgelementu przez przeglądarkę.
David Moles

1

Zacząłem od tej odpowiedzi używając filtru svg i wprowadziłem następujące modyfikacje:

Filtr SVG z adresu URL danych

Jeśli nie chcesz definiować filtra SVG gdzieś w swoim znaczniku, możesz zamiast tego użyć adresu URL danych (zamień R , G , B i A na żądany kolor):

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

Powrót do skali szarości

Jeśli powyższa wersja nie działa, możesz również dodać rezerwę w skali szarości.

Funkcje saturatei brightnesszmieniają dowolny kolor na czarny (nie trzeba tego uwzględniać, jeśli kolor jest już czarny), inverta następnie rozjaśnia go żądaną jasnością ( L ) i opcjonalnie można również określić krycie ( A ).

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

Mieszanie SCSS

Jeśli chcesz dynamicznie określić kolor, możesz użyć następującej mieszanki SCSS:

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

Przykładowe użycie:

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

Zalety:

  • Bez Javascript .
  • Brak dodatkowych elementów HTML .
  • Jeśli filtry CSS są obsługiwane, ale filtr SVG nie działa, istnieje opcja zastępcza w skali szarości .
  • Jeśli używasz miksera, użycie jest dość proste (patrz przykład powyżej).
  • Kolor jest bardziej czytelny i łatwiejszy do modyfikacji niż sztuczka sepii (komponenty RGBA w czystym CSS, a kolory HEX można nawet używać w SCSS).
  • Pozwala uniknąć dziwnego zachowaniahue-rotate .

Ostrzeżenia:

  • Nie wszystkie przeglądarki obsługują filtry SVG z adresu URL danych (zwłaszcza z hash identyfikatora), ale działa to w obecnych przeglądarkach Firefox i Chromium (i być może w innych).
  • Jeśli chcesz określić kolor dynamicznie, musisz użyć mieszanki SCSS.
  • Czysta wersja CSS jest trochę brzydka, jeśli chcesz mieć wiele różnych kolorów, musisz kilka razy dołączyć SVG.

1
Och, to jest idealne, właśnie tego szukałem, czyli użyć wszystkiego w SASS, niesamowite, wielkie dzięki!
ghiscoding
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.