Bagaimana mengubah hitam menjadi warna tertentu hanya dengan menggunakan filter CSS

115

Pertanyaan saya adalah: diberi warna RGB target, apa rumus untuk mewarnai ulang hitam ( #000) menjadi warna itu hanya dengan menggunakan filter CSS ?

Agar jawaban dapat diterima, perlu menyediakan fungsi (dalam bahasa apa pun) yang akan menerima warna target sebagai argumen dan mengembalikan filterstring CSS yang sesuai .

Konteksnya adalah kebutuhan untuk mewarnai ulang SVG di dalam a background-image. Dalam hal ini, ini untuk mendukung fitur matematika TeX tertentu di KaTeX: https://github.com/Khan/KaTeX/issues/587 .

Contoh

Jika warna target adalah #ffff00(kuning), solusi yang tepat adalah:

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

( demo )

Bukan tujuan

  • Animasi.
  • Solusi non-CSS-filter.
  • Mulai dari warna selain hitam.
  • Peduli tentang apa yang terjadi pada warna selain hitam.

Hasil sejauh ini

Anda masih bisa mendapatkan jawaban Diterima dengan mengirimkan solusi non brute force!

Sumber daya

  • Bagaimana hue-rotatedan sepiadihitung: https://stackoverflow.com/a/29521147/181228 Contoh implementasi Ruby:

    LUM_R = 0.2126; LUM_G = 0.7152; LUM_B = 0.0722
    HUE_R = 0.1430; HUE_G = 0.1400; HUE_B = 0.2830
    
    def clamp(num)
      [0, [255, num].min].max.round
    end
    
    def hue_rotate(r, g, b, angle)
      angle = (angle % 360 + 360) % 360
      cos = Math.cos(angle * Math::PI / 180)
      sin = Math.sin(angle * Math::PI / 180)
      [clamp(
         r * ( LUM_R  +  (1 - LUM_R) * cos  -  LUM_R * sin       ) +
         g * ( LUM_G  -  LUM_G * cos        -  LUM_G * sin       ) +
         b * ( LUM_B  -  LUM_B * cos        +  (1 - LUM_B) * sin )),
       clamp(
         r * ( LUM_R  -  LUM_R * cos        +  HUE_R * sin       ) +
         g * ( LUM_G  +  (1 - LUM_G) * cos  +  HUE_G * sin       ) +
         b * ( LUM_B  -  LUM_B * cos        -  HUE_B * sin       )),
       clamp(
         r * ( LUM_R  -  LUM_R * cos        -  (1 - LUM_R) * sin ) +
         g * ( LUM_G  -  LUM_G * cos        +  LUM_G * sin       ) +
         b * ( LUM_B  +  (1 - LUM_B) * cos  +  LUM_B * sin       ))]
    end
    
    def sepia(r, g, b)
      [r * 0.393 + g * 0.769 + b * 0.189,
       r * 0.349 + g * 0.686 + b * 0.168,
       r * 0.272 + g * 0.534 + b * 0.131]
    end
    

    Perhatikan bahwa di clampatas membuat hue-rotatefungsi menjadi non-linier.

    Penerapan browser: Chromium , Firefox .

  • Demo: Mendapatkan warna non-skala abu-abu dari warna abu-abu: https://stackoverflow.com/a/25524145/181228

  • Formula yang hampir berfungsi (dari pertanyaan serupa ):
    https://stackoverflow.com/a/29958459/181228

    Penjelasan mendetail mengapa rumus di atas salah (CSS hue-rotatebukanlah rotasi hue yang sebenarnya, melainkan perkiraan linier):
    https://stackoverflow.com/a/19325417/2441511

glebm
sumber
Jadi Anda ingin LERP # 000000 ke #RRGGBB? (Hanya mengklarifikasi)
Zze
1
Ya manis - hanya mengklarifikasi bahwa Anda tidak ingin memasukkan transisi ke dalam solusi.
Zze
1
Mungkin mode campuran akan berhasil untuk Anda? Anda dapat dengan mudah mengubah hitam menjadi warna apa pun ... Tapi saya tidak mendapatkan gambaran global tentang apa yang ingin Anda capai
vals
1
@glebm jadi Anda perlu mencari rumus (menggunakan metode apa pun) untuk mengubah hitam menjadi warna apa pun dan menerapkannya menggunakan css?
ProllyGeek
2
@Bintangbete Satu kendala lain yang harus saya sebutkan adalah bahwa rumus yang dihasilkan tidak dapat berupa pencarian brute force dari tabel 5 GiB (harus dapat digunakan dari mis. Javascript pada halaman web).
glebm

Jawaban:

149

@Dave adalah orang pertama yang memposting jawaban untuk ini (dengan kode yang berfungsi), dan jawabannya telah menjadi sumber inspirasi tempel dan salinan tak tahu malu bagi saya. Postingan ini dimulai sebagai upaya untuk menjelaskan dan menyempurnakan jawaban @ Dave, tetapi sejak itu berkembang menjadi jawabannya sendiri.

Metode saya jauh lebih cepat. Menurut tolok ukur jsPerf pada warna RGB yang dihasilkan secara acak, algoritme @ Dave berjalan dalam 600 ms , sedangkan milikku berjalan dalam 30 ms . Ini pasti bisa menjadi masalah, misalnya dalam waktu muat, di mana kecepatan sangat penting.

Selain itu, untuk beberapa warna, algoritme saya bekerja lebih baik:

  • Untuk rgb(0,255,0), produksi rgb(29,218,34)dan produksi @ Davergb(1,255,0)
  • Untuk rgb(0,0,255), produksi @ Dave rgb(37,39,255)dan produksi sayargb(5,6,255)
  • Untuk rgb(19,11,118), produksi @ Dave rgb(36,27,102)dan produksi sayargb(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>


Pemakaian

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

Penjelasan

Kami akan mulai dengan menulis beberapa 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)}%);`;
    }
}

Penjelasan:

  • The Colorkelas merupakan warna RGB.
    • Fungsinya toString()mengembalikan warna dalam rgb(...)string warna CSS .
    • Fungsinya hsl()mengembalikan warna, diubah menjadi HSL .
    • Fungsinya clamp()memastikan bahwa nilai warna tertentu berada dalam batas (0-255).
  • The Solverkelas akan mencoba untuk memecahkan warna sasaran.
    • Fungsinya css()mengembalikan filter yang diberikan dalam string filter CSS.

Menerapkan grayscale(), sepia()dansaturate()

Inti dari filter CSS / SVG adalah filter primitif , yang mewakili modifikasi tingkat rendah pada gambar.

Filter grayscale(),, sepia()dan saturate()diimplementasikan oleh filter primatif <feColorMatrix>, yang melakukan perkalian matriks antara matriks yang ditentukan oleh filter (sering dibuat secara dinamis), dan matriks yang dibuat dari warna. Diagram:

Perkalian matriks

Ada beberapa pengoptimalan yang bisa kami lakukan di sini:

  • Elemen terakhir dari matriks warna adalah dan akan selalu 1. Tidak ada gunanya menghitung atau menyimpannya.
  • Tidak ada gunanya menghitung atau menyimpan nilai alpha / transparansi ( A), karena kita berurusan dengan RGB, bukan RGBA.
  • Oleh karena itu, kita dapat memangkas matriks filter dari 5x5 menjadi 3x5, dan matriks warna dari 1x5 menjadi 1x3 . Ini menghemat sedikit pekerjaan.
  • Semua <feColorMatrix>filter meninggalkan kolom 4 dan 5 sebagai nol. Oleh karena itu, kami selanjutnya dapat mengurangi matriks filter menjadi 3x3 .
  • Karena perkaliannya relatif sederhana, tidak perlu menyeret perpustakaan matematika yang rumit untuk ini. Algoritma perkalian matriks dapat kita implementasikan sendiri.

Penerapan:

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

(Kami menggunakan variabel sementara untuk menampung hasil perkalian setiap baris, karena kami tidak ingin perubahan this.r, dll. Mempengaruhi perhitungan selanjutnya.)

Sekarang kita telah menerapkan <feColorMatrix>, kita dapat menerapkan grayscale(), sepia(), dan saturate(), yang hanya meminta dengan matriks filter yang diberikan:

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

Menerapkan hue-rotate()

The hue-rotate()filter dilaksanakan oleh <feColorMatrix type="hueRotate" />.

Matriks filter dihitung seperti yang ditunjukkan di bawah ini:

Misalnya, elemen yang 00 akan dihitung seperti:

Beberapa catatan:

  • Sudut rotasi diberikan dalam derajat. Ini harus dikonversi ke radian sebelum diteruskan ke Math.sin()atau Math.cos().
  • Math.sin(angle)dan Math.cos(angle)harus dihitung sekali dan kemudian disimpan dalam cache.

Penerapan:

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

Menerapkan brightness()dancontrast()

The brightness()dan contrast()filter dilaksanakan oleh <feComponentTransfer>dengan <feFuncX type="linear" />.

Setiap <feFuncX type="linear" />elemen menerima atribut kemiringan dan intersep . Kemudian menghitung setiap nilai warna baru melalui rumus sederhana:

value = slope * value + intercept

Ini mudah diterapkan:

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

Setelah ini diterapkan, brightness()dan contrast()dapat diterapkan juga:

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

Menerapkan invert()

The invert()filter dilaksanakan oleh <feComponentTransfer>dengan <feFuncX type="table" />.

Spesifikasi menyatakan:

Berikut ini, C adalah komponen awal dan C ' adalah komponen yang dipetakan ulang; keduanya dalam interval tertutup [0,1].

Untuk "tabel", fungsinya ditentukan dengan interpolasi linier antara nilai yang diberikan dalam atribut tableValues . Tabel tersebut memiliki nilai n + 1 (yaitu, v 0 hingga v n ) yang menentukan nilai awal dan akhir untuk n wilayah interpolasi berukuran sama. Interpolasi menggunakan rumus berikut:

Untuk nilai C temukan k seperti itu:

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

Hasil C ' diberikan oleh:

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

Penjelasan rumus ini:

  • The invert()Filter mendefinisikan tabel ini: [value, 1 - value]. Ini adalah tableValues atau v .
  • Rumus tersebut mendefinisikan n , sehingga n + 1 adalah panjang tabel. Karena panjang meja adalah 2, n = 1.
  • Rumusnya mendefinisikan k , dengan k dan k + 1 menjadi indeks tabel. Karena tabel memiliki 2 elemen, k = 0.

Dengan demikian, kita dapat menyederhanakan rumusnya menjadi:

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

Menyebariskan nilai tabel, kita mendapatkan:

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

Satu lagi penyederhanaan:

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

Spesifikasi mendefinisikan C dan C ' menjadi nilai RGB, dalam batas 0-1 (sebagai lawan 0-255). Akibatnya, kita harus menurunkan nilai sebelum komputasi, dan menskalakannya kembali setelahnya.

Jadi kami sampai pada implementasi kami:

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

Selingan: Algoritma kekerasan @ Dave

Kode @ Dave menghasilkan 176.660 kombinasi filter, termasuk:

  • 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 (0deg, 5deg, 10deg, ..., 360deg)

Ini menghitung filter dalam urutan berikut:

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

Kemudian iterasi melalui semua warna yang dihitung. Ini berhenti setelah menemukan warna yang dihasilkan dalam toleransi (semua nilai RGB berada dalam 5 unit dari warna target).

Namun, ini lambat dan tidak efisien. Demikian jawaban saya sendiri.

Menerapkan SPSA

Pertama, kita harus mendefinisikan fungsi kerugian , yang mengembalikan perbedaan antara warna yang dihasilkan oleh kombinasi filter, dan warna target. Jika filternya sempurna, fungsi kerugian harus mengembalikan 0.

Kami akan mengukur perbedaan warna sebagai jumlah dari dua metrik:

  • Perbedaan RGB, karena tujuannya adalah menghasilkan nilai RGB yang paling mendekati.
  • Perbedaan HSL, karena banyak nilai HSL sesuai dengan filter (mis. Hue secara kasar berkorelasi dengan hue-rotate(), saturasi berkorelasi dengan saturate(), dll.) Ini memandu algoritme.

Fungsi kerugian akan mengambil satu argumen - larik persentase filter.

Kami akan menggunakan urutan filter berikut:

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

Penerapan:

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

Kami akan mencoba meminimalkan fungsi kerugian, sehingga:

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

The SPSA algoritma ( situs , info lebih lanjut , kertas , kertas implementasi , kode referensi ) sangat pandai dalam hal ini. Ini dirancang untuk mengoptimalkan sistem yang kompleks dengan fungsi minima lokal, noise / nonlinear / multivariate loss, dll. Telah digunakan untuk menyetel mesin catur . Dan tidak seperti banyak algoritme lainnya, makalah yang mendeskripsikannya sebenarnya dapat dipahami (meskipun dengan usaha keras).

Penerapan:

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

Saya membuat beberapa modifikasi / pengoptimalan pada SPSA:

  • Menggunakan hasil terbaik yang dihasilkan, bukan yang terakhir.
  • Menggunakan kembali semua array ( deltas, highArgs, lowArgs), bukannya menciptakan mereka dengan setiap iterasi.
  • Menggunakan larik nilai untuk a , bukan satu nilai. Ini karena semua filter berbeda, dan karenanya harus bergerak / menyatu pada kecepatan yang berbeda.
  • Menjalankan fixfungsi setelah setiap iterasi. Ini menjepit semua nilai antara 0% dan 100%, kecuali saturate(di mana maksimum adalah 7500%), brightnessdan contrast(di mana maksimum adalah 200%), dan hueRotate(di mana nilai-nilai dibungkus bukan dijepit).

Saya menggunakan SPSA dalam proses dua tahap:

  1. Panggung "lebar", yang mencoba "menjelajahi" ruang pencarian. Ini akan membatasi pengulangan SPSA jika hasilnya tidak memuaskan.
  2. Tahap "sempit", yang mengambil hasil terbaik dari panggung lebar dan mencoba "menyempurnakannya". Ini menggunakan nilai dinamis untuk A dan a .

Penerapan:

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

Menyetel SPSA

Peringatan: Jangan main-main dengan kode SPSA, terutama dengan konstanta, kecuali Anda yakin tahu apa yang Anda lakukan.

Konstanta yang penting adalah A , a , c , nilai awal, retry threshold, nilai maxin fix(), dan jumlah iterasi setiap tahapan. Semua nilai ini disetel dengan cermat untuk menghasilkan hasil yang baik, dan mengacaukannya secara acak hampir pasti akan mengurangi kegunaan algoritme.

Jika Anda bersikeras untuk mengubahnya, Anda harus mengukurnya sebelum Anda "mengoptimalkan".

Pertama, terapkan tambalan ini .

Kemudian jalankan kode di Node.js. Setelah beberapa lama, hasilnya akan seperti ini:

Average loss: 3.4768521401985275
Average time: 11.4915ms

Sekarang setel konstanta sesuka hati Anda.

Beberapa tips:

  • Kerugian rata-rata harus sekitar 4. Jika lebih besar dari 4, itu menghasilkan hasil yang terlalu jauh, dan Anda harus menyesuaikan keakuratannya. Jika kurang dari 4, ini membuang-buang waktu, dan Anda harus mengurangi jumlah iterasi.
  • Jika Anda menambah / mengurangi jumlah iterasi, sesuaikan A dengan tepat.
  • Jika Anda menaikkan / menurunkan A , sesuaikan a dengan benar.
  • Gunakan --debugbendera jika Anda ingin melihat hasil dari setiap iterasi.

TL; DR

MultiplyByZer0
sumber
3
Ringkasan yang sangat bagus dari proses pengembangan! Apakah Anda membaca pikiran saya ?!
Dave
1
@Dave Sebenarnya, saya mengerjakan ini secara mandiri, tetapi Anda mengalahkan saya untuk itu.
MultiplyByZer0
4
Jawaban Hebat! Implementasi dalam codepen ini
KyleMit
3
Ini adalah metode yang benar-benar gila. Anda dapat mengatur warna secara langsung menggunakan filter SVG (kolom kelima di feColorMatrix) dan Anda dapat mereferensikan filter itu dari CSS - mengapa Anda tidak menggunakan metode itu?
Michael Mullany
2
@MichaelMullany Yah, itu memalukan bagi saya, mengingat berapa lama saya mengerjakan ini. Saya tidak memikirkan metode Anda, tetapi sekarang saya mengerti - untuk mewarnai ulang elemen menjadi warna sembarang, Anda hanya secara dinamis menghasilkan SVG dengan yang <filter>berisi a <feColorMatrix>dengan nilai yang tepat (semua nol kecuali kolom terakhir, yang berisi RGB target values, 0, dan 1), masukkan SVG ke DOM, dan referensikan filter dari CSS. Silakan tulis solusi Anda sebagai jawaban (dengan demo), dan saya akan memberi suara positif.
MultiplyByZer0
55

Ini adalah perjalanan yang cukup menyusuri lubang kelinci tapi ini dia!

var tolerance = 1;
var invertRange = [0, 1];
var invertStep = 0.1;
var sepiaRange = [0, 1];
var sepiaStep = 0.1;
var saturateRange = [5, 100];
var saturateStep = 5;
var hueRotateRange = [0, 360];
var hueRotateStep = 5;
var possibleColors;
var color = document.getElementById('color');
var pixel = document.getElementById('pixel');
var filtersBox = document.getElementById('filters');
var button = document.getElementById('button');
button.addEventListener('click', function() { 			      
	getNewColor(color.value);
})

// matrices taken from https://www.w3.org/TR/filter-effects/#feColorMatrixElement
function sepiaMatrix(s) {
	return [
		(0.393 + 0.607 * (1 - s)), (0.769 - 0.769 * (1 - s)), (0.189 - 0.189 * (1 - s)),
		(0.349 - 0.349 * (1 - s)), (0.686 + 0.314 * (1 - s)), (0.168 - 0.168 * (1 - s)),
		(0.272 - 0.272 * (1 - s)), (0.534 - 0.534 * (1 - s)), (0.131 + 0.869 * (1 - s)),
	]
}

function saturateMatrix(s) {
	return [
		0.213+0.787*s, 0.715-0.715*s, 0.072-0.072*s,
		0.213-0.213*s, 0.715+0.285*s, 0.072-0.072*s,
		0.213-0.213*s, 0.715-0.715*s, 0.072+0.928*s,
	]
}

function hueRotateMatrix(d) {
	var cos = Math.cos(d * Math.PI / 180);
	var sin = Math.sin(d * Math.PI / 180);
	var a00 = 0.213 + cos*0.787 - sin*0.213;
	var a01 = 0.715 - cos*0.715 - sin*0.715;
	var a02 = 0.072 - cos*0.072 + sin*0.928;

	var a10 = 0.213 - cos*0.213 + sin*0.143;
	var a11 = 0.715 + cos*0.285 + sin*0.140;
	var a12 = 0.072 - cos*0.072 - sin*0.283;

	var a20 = 0.213 - cos*0.213 - sin*0.787;
	var a21 = 0.715 - cos*0.715 + sin*0.715;
	var a22 = 0.072 + cos*0.928 + sin*0.072;

	return [
		a00, a01, a02,
		a10, a11, a12,
		a20, a21, a22,
	]
}

function clamp(value) {
	return value > 255 ? 255 : value < 0 ? 0 : value;
}

function filter(m, c) {
	return [
		clamp(m[0]*c[0] + m[1]*c[1] + m[2]*c[2]),
		clamp(m[3]*c[0] + m[4]*c[1] + m[5]*c[2]),
		clamp(m[6]*c[0] + m[7]*c[1] + m[8]*c[2]),
	]
}

function invertBlack(i) {
	return [
		i * 255,
		i * 255,
		i * 255,
	]
}

function generateColors() {
	let possibleColors = [];

	let invert = invertRange[0];
	for (invert; invert <= invertRange[1]; invert+=invertStep) {
		let sepia = sepiaRange[0];
		for (sepia; sepia <= sepiaRange[1]; sepia+=sepiaStep) {
			let saturate = saturateRange[0];
			for (saturate; saturate <= saturateRange[1]; saturate+=saturateStep) {
				let hueRotate = hueRotateRange[0];
				for (hueRotate; hueRotate <= hueRotateRange[1]; hueRotate+=hueRotateStep) {
					let invertColor = invertBlack(invert);
					let sepiaColor = filter(sepiaMatrix(sepia), invertColor);
					let saturateColor = filter(saturateMatrix(saturate), sepiaColor);
					let hueRotateColor = filter(hueRotateMatrix(hueRotate), saturateColor);

					let colorObject = {
						filters: { invert, sepia, saturate, hueRotate },
						color: hueRotateColor
					}

					possibleColors.push(colorObject);
				}
			}
		}
	}

	return possibleColors;
}

function getFilters(targetColor, localTolerance) {
	possibleColors = possibleColors || generateColors();

	for (var i = 0; i < possibleColors.length; i++) {
		var color = possibleColors[i].color;
		if (
			Math.abs(color[0] - targetColor[0]) < localTolerance &&
			Math.abs(color[1] - targetColor[1]) < localTolerance &&
			Math.abs(color[2] - targetColor[2]) < localTolerance
		) {
			return filters = possibleColors[i].filters;
			break;
		}
	}

	localTolerance += tolerance;
	return getFilters(targetColor, localTolerance)
}

function getNewColor(color) {
	var targetColor = color.split(',');
	targetColor = [
	    parseInt(targetColor[0]), // [R]
	    parseInt(targetColor[1]), // [G]
	    parseInt(targetColor[2]), // [B]
    ]
    var filters = getFilters(targetColor, tolerance);
    var filtersCSS = 'filter: ' +
	    'invert('+Math.floor(filters.invert*100)+'%) '+
	    'sepia('+Math.floor(filters.sepia*100)+'%) ' +
	    'saturate('+Math.floor(filters.saturate*100)+'%) ' +
	    'hue-rotate('+Math.floor(filters.hueRotate)+'deg);';
    pixel.style = filtersCSS;
    filtersBox.innerText = filtersCSS
}

getNewColor(color.value);
#pixel {
  width: 50px;
  height: 50px;
  background: rgb(0,0,0);
}
<input type="text" id="color" placeholder="R,G,B" value="250,150,50" />
<button id="button">get filters</button>
<div id="pixel"></div>
<div id="filters"></div>

EDIT: Solusi ini tidak dimaksudkan untuk penggunaan produksi dan hanya menggambarkan pendekatan yang dapat diambil untuk mencapai apa yang diminta OP. Sebenarnya, itu lemah di beberapa area spektrum warna. Hasil yang lebih baik dapat dicapai dengan lebih banyak perincian dalam iterasi langkah atau dengan menerapkan lebih banyak fungsi filter untuk alasan yang dijelaskan secara mendetail dalam jawaban @ MultiplyByZer0 .

EDIT2: OP sedang mencari solusi non brute force. Dalam hal ini cukup sederhana, selesaikan saja persamaan ini:

Persamaan Matriks Filter CSS

dimana

a = hue-rotation
b = saturation
c = sepia
d = invert
Dave
sumber
Jika saya memasukkannya 255,0,255, pengukur warna digital saya melaporkan hasilnya sebagai #d619d9gantinya #ff00ff.
Siguza
@Siguza Jelas tidak sempurna, warna casing tepi dapat diubah dengan menyesuaikan batas di loop.
Dave
3
Persamaan itu sama sekali tidak "cukup sederhana"
MultiplyByZer0
Saya kira persamaan di atas juga hilang clamp?
glebm
1
Penjepit tidak memiliki tempat di sana. Dan dari apa yang saya ingat dari matematika kuliah saya, persamaan ini dihitung dengan perhitungan numerik alias "brute force" jadi semoga berhasil!
Dave
28

Catatan: OP meminta saya untuk membatalkan penghapusan , tetapi hadiah akan diberikan ke jawaban Dave.


Saya tahu ini bukan yang ditanyakan di badan pertanyaan, dan tentu saja bukan yang kita semua tunggu, tetapi ada satu filter CSS yang melakukan persis seperti ini: drop-shadow()

Peringatan:

  • Bayangan digambar di belakang konten yang ada. Artinya kita harus membuat beberapa trik penentuan posisi mutlak.
  • Semua piksel akan diperlakukan sama, tetapi OP mengatakan [kita tidak boleh] "Peduli tentang apa yang terjadi pada warna selain hitam."
  • Dukungan browser. (Saya tidak yakin tentang itu, hanya diuji di bawah FF dan chrome terbaru).

/* 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>

Kaiido
sumber
1
Sangat pintar, luar biasa! Ini bekerja untuk saya, hargai saja
jaminroe
Saya yakin ini adalah solusi yang lebih baik karena 100% akurat dengan warna setiap saat.
pengguna835542
Kode apa adanya menunjukkan halaman kosong (W10 FF 69b). Tidak ada yang salah dengan ikonnya, meskipun (memeriksa SVG terpisah).
Rene van der Lende
Menambahkan background-color: black;untuk .icon>spanmembuat ini bekerja untuk FF 69b. Namun, tidak menunjukkan ikon.
Rene van der Lende
@RenevanderLende Baru saja mencoba FF70 masih berfungsi di sana. Jika itu tidak berhasil untuk Anda, itu pasti sesuatu di pihak Anda.
Kaiido
15

Anda dapat membuat ini semua sangat sederhana hanya dengan menggunakan filter SVG yang direferensikan dari CSS. Anda hanya memerlukan satu feColorMatrix untuk melakukan pewarnaan ulang. Yang ini berubah menjadi kuning. Kolom kelima di feColorMatrix menyimpan nilai target RGB pada skala unit. (untuk kuning - itu 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">

Michael Mullany
sumber
Solusi yang menarik tetapi tampaknya tidak memungkinkan untuk mengontrol warna target melalui CSS.
glebm
Anda harus menentukan filter baru untuk setiap warna yang ingin Anda terapkan. Tapi itu sepenuhnya akurat. hue-rotate adalah perkiraan yang memotong warna tertentu - artinya Anda tidak dapat mencapai warna tertentu secara akurat menggunakannya - seperti yang dibuktikan oleh jawaban di atas. Yang benar-benar kita butuhkan adalah singkatan dari filter CSS recolor ().
Michael Mullany
Jawaban MultiplyByZer0 menghitung serangkaian filter yang dicapai dengan akurasi sangat tinggi, tanpa memodifikasi HTML. Benar hue-rotatedi browser akan menyenangkan ya.
glebm
2
tampaknya ini hanya menghasilkan warna RGB yang akurat untuk gambar sumber hitam ketika Anda menambahkan "color-interpolation-filter" = "sRGB" ke feColorMatrix.
John Smith
Tepi 12-18 ditinggalkan karena tidak mendukung urlfungsi caniuse.com/#search=svg%20filter
Volker E.
2

Saya perhatikan bahwa contoh perawatan melalui filter SVG tidak lengkap, saya menulis milik saya (yang berfungsi dengan sempurna): (lihat jawaban Michael Mullany) jadi inilah cara untuk mendapatkan warna apa pun yang Anda inginkan:

Berikut adalah solusi kedua, dengan menggunakan SVG Filter hanya di code => URL.createObjectURL

Pak Jojo
sumber
1

gunakan saja

fill: #000000

The fillproperti di CSS adalah untuk mengisi warna bentuk SVG. The fillproperti dapat menerima nilai warna CSS.

Situs web Odessa
sumber
3
Ini mungkin berfungsi dengan CSS internal ke gambar SVG, tetapi tidak berfungsi karena CSS diterapkan secara eksternal ke imgelemen oleh browser.
David Moles
1

Saya mulai dengan jawaban ini menggunakan filter svg dan membuat modifikasi berikut:

Filter SVG dari url data

Jika Anda tidak ingin menentukan filter SVG di suatu tempat di markup Anda, Anda dapat menggunakan url data (ganti R , G , B dan A dengan warna yang diinginkan):

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

Pengganti grayscale

Jika versi di atas tidak berfungsi, Anda juga dapat menambahkan fallback grayscale.

Fungsi saturatedan brightnessmengubah warna apa pun menjadi hitam (Anda tidak perlu memasukkannya jika warnanya sudah hitam), invertlalu mencerahkannya dengan kecerahan yang diinginkan ( L ) dan secara opsional Anda juga dapat menentukan opasitas ( A ).

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

Campuran SCSS

Jika Anda ingin menentukan warna secara dinamis, Anda dapat menggunakan mixin SCSS berikut:

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

Contoh penggunaan:

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

Keuntungan:

  • Tanpa Javascript .
  • Tidak ada elemen HTML tambahan .
  • Jika filter CSS didukung, tetapi filter SVG tidak berfungsi, ada penggantian grayscale .
  • Jika Anda menggunakan mixin, penggunaannya cukup mudah (lihat contoh di atas).
  • Warnanya lebih mudah dibaca dan dimodifikasi daripada trik sepia (komponen RGBA dalam CSS murni dan Anda bahkan dapat menggunakan warna HEX di SCSS).
  • Menghindari perilaku anehhue-rotate .

Peringatan:

  • Tidak semua browser mendukung filter SVG dari url data (terutama hash id), tetapi berfungsi di browser Firefox dan Chromium saat ini (dan mungkin lainnya).
  • Jika Anda ingin menentukan warna secara dinamis, Anda harus menggunakan mixin SCSS.
  • Versi CSS murni agak jelek, jika Anda menginginkan banyak warna berbeda, Anda harus menyertakan SVG beberapa kali.
David Dostal
sumber
1
oh itu sempurna, inilah yang saya cari yang menggunakan semua yang ada di SASS, terima kasih banyak!
ghiscoding