Yalnızca CSS filtreleri kullanarak siyahı herhangi bir renge dönüştürme


116

Sorum şu: bir hedef RGB rengi verildiğinde, #000sadece CSS filtrelerini kullanarak siyahı ( ) o renge yeniden renklendirmenin formülü nedir?

Bir cevabın kabul edilmesi için, hedef rengi bir argüman olarak kabul edecek ve karşılık gelen CSS filterdizesini döndürecek bir işlev (herhangi bir dilde) sağlaması gerekir .

Bunun bağlamı, bir SVG'yi bir background-image. Bu durumda, KaTeX'teki belirli TeX matematik özelliklerini desteklemektir: https://github.com/Khan/KaTeX/issues/587 .

Misal

Hedef renk #ffff00(sarı) ise, doğru çözümlerden biri:

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

( demo )

Sigara hedefler

  • Animasyon.
  • CSS olmayan filtre çözümleri.
  • Siyah dışında bir renkten başlayarak.
  • Siyah dışındaki renklere ne olduğunu önemsemek.

Şimdiye kadarki sonuçlar

Kaba kuvvet dışı bir çözüm sunarak da Kabul Edilmiş bir yanıt alabilirsiniz !

kaynaklar

  • Nasıl hue-rotateve sepiahesaplanır: https://stackoverflow.com/a/29521147/181228 Örnek Ruby uygulaması:

    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

    Not bu clampyukarıda yapar hue-rotatefonksiyonu doğrusal olmayan.

    Tarayıcı uygulamaları: Chromium , Firefox .

  • Demo: Gri tonlamalı bir renkten gri tonlamalı olmayan bir renge ulaşmak: https://stackoverflow.com/a/25524145/181228

  • Neredeyse işe yarayan bir formül ( benzer bir sorudan ):
    https://stackoverflow.com/a/29958459/181228

    Yukarıdaki formülün neden yanlış olduğuna dair ayrıntılı bir açıklama (CSS hue-rotategerçek bir ton dönüşü değil, doğrusal bir yaklaşımdır):
    https://stackoverflow.com/a/19325417/2441511


Öyleyse LERP # 000000'dan #RRGGBB'ye? (Sadece açıklığa kavuşturuyor)
Zze

1
Evet tatlı - sadece çözüme bir geçişi dahil etmek istemediğinizi açıklığa kavuşturmak.
Zze

1
Bir karışım modu sizin için işe yarayabilir mi? Siyahı herhangi bir renge kolayca dönüştürebilirsiniz ... Ama elde etmek istediğiniz şeyin genel resmini alamıyorum
vals

1
@glebm yani siyahı herhangi bir renge dönüştürmek için bir formül (herhangi bir yöntem kullanarak) bulmanız ve css kullanarak uygulamanız gerekiyor mu?
ProllyGeek

2
@ProllyGeek Evet. Bahsetmem gereken diğer bir kısıtlama, elde edilen formülün bir 5GiB tablosunun kaba kuvvet araması olamayacağıdır (örneğin bir web sayfasındaki javascript'ten kullanılabilir olmalıdır).
glebm

Yanıtlar:


150

@Dave, buna (çalışma koduyla) bir yanıt gönderen ilk kişiydi ve cevabı, bana utanmaz bir kopya ve yapıştırma ilhamı için paha biçilmez bir kaynak oldu . Bu gönderi @ Dave'in cevabını açıklama ve iyileştirme girişimi olarak başladı, ancak o zamandan beri kendi başına bir cevaba dönüştü.

Yöntemim önemli ölçüde daha hızlı. Rastgele oluşturulmuş RGB renkleri üzerine bir jsPerf karşılaştırmasına göre , @ Dave'in algoritması 600 ms'de çalışırken benimki 30 ms'de çalışıyor . Bu, örneğin hızın kritik olduğu yükleme süresinde kesinlikle önemli olabilir.

Ayrıca, bazı renkler için algoritmam daha iyi performans gösteriyor:

  • İçin rgb(0,255,0)Dave üretir @, rgb(29,218,34)ve üretirrgb(1,255,0)
  • İçin rgb(0,0,255)Dave üretir @, rgb(37,39,255)ve mayın üretiyorrgb(5,6,255)
  • İçin rgb(19,11,118)Dave üretir @, rgb(36,27,102)ve mayın üretiyorrgb(20,11,112)

gösteri

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


kullanım

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

açıklama

Biraz Javascript yazarak başlayacağız.

"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)}%);`;
    }
}

Açıklama:

  • ColorSınıfı, bir RGB renk temsil eder.
    • İşlevi toString(), rengi bir CSS rgb(...)renk dizesinde döndürür.
    • İşlevi hsl(), HSL'ye dönüştürülen rengi döndürür .
    • İşlevi clamp(), belirli bir renk değerinin sınırlar içinde (0-255) olmasını sağlar.
  • SolverSınıf bir hedef renk için çözmeye çalışacaktır.
    • İşlevi css(), bir CSS filtre dizesinde belirli bir filtreyi döndürür.

Uygulama grayscale(), sepia()vesaturate()

CSS / SVG filtrelerinin kalbi filtre temel öğeleridir bir görüntüde düşük düzeyli değişiklikleri temsil eden .

Filtreler grayscale(), sepia()ve saturate()filtre tarafından belirtilen (genellikle dinamik olarak oluşturulan) bir matris ile renkten oluşturulan bir matris arasında matris çarpımını<feColorMatrix> gerçekleştiren filtre ilkeli tarafından uygulanır . Diyagram:

Matris çarpımı

Burada yapabileceğimiz bazı optimizasyonlar var:

  • Renk matrisinin son öğesi, ve her zaman öyle olacaktır 1. Hesaplamanın veya saklamanın bir anlamı yok.
  • ARGBA ile değil, RGB ile uğraştığımız için alfa / şeffaflık değerini ( ) hesaplamanın veya saklamanın da bir anlamı yoktur .
  • Bu nedenle, filtre matrislerini 5x5'ten 3x5'e ve renk matrisini 1x5'ten 1x3'e kırpabiliriz. . Bu biraz iş tasarrufu sağlar.
  • Tüm <feColorMatrix>filtreler, 4. ve 5. sütunları sıfır olarak bırakır. Bu nedenle, filtre matrisini 3x3'e daha da düşürebiliriz .
  • Çarpma işlemi nispeten basit olduğundan, bunun için karmaşık matematik kitaplıklarında sürüklemeye gerek yoktur . Matris çarpım algoritmasını kendimiz uygulayabiliriz.

Uygulama:

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

(Her satır çarpımının sonuçlarını tutmak için geçici değişkenler kullanıyoruz, çünkü this.rsonraki hesaplamaları etkileyen değişikliklerin vs. olmasını istemiyoruz .)

Artık <feColorMatrix>uyguladığımıza grayscale()göre sepia(), ve saturate()bunu belirli bir filtre matrisiyle basitçe çağırabiliriz:

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

Uygulama hue-rotate()

hue-rotate()Filtre tarafından uygulanmaktadır <feColorMatrix type="hueRotate" />.

Filtre matrisi aşağıda gösterildiği gibi hesaplanır:

Örneğin, eleman bir 00 şöyle hesaplanır:

Bazı notlar:

  • Dönme açısı derece cinsinden verilmiştir. Math.sin()Veya 'a geçmeden önce radyana dönüştürülmelidir Math.cos().
  • Math.sin(angle)ve bir Math.cos(angle)kez hesaplanmalı ve sonra önbelleğe alınmalıdır.

Uygulama:

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

Uygulama brightness()vecontrast()

brightness()Ve contrast()filtreler tarafından uygulanır <feComponentTransfer>ile <feFuncX type="linear" />.

Her <feFuncX type="linear" />öğe bir eğim ve kesişim niteliği kabul eder . Daha sonra her yeni renk değerini basit bir formülle hesaplar:

value = slope * value + intercept

Uygulanması kolaydır:

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

Bu kez uygulanan, brightness()ve contrast()sıra uygulanabilir:

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

Uygulama invert()

invert()Filtre tarafından uygulanan <feComponentTransfer>ile <feFuncX type="table" />.

Spesifikasyon şunları belirtir:

Aşağıda, C başlangıç ​​bileşenidir ve C ' yeniden eşlenen bileşendir; her ikisi de kapalı aralıkta [0,1].

"Tablo", bu fonksiyon özelliği verilen değerler arasında lineer interpolasyon ile tanımlanır tableValues . Tabloda, n tane eşit boyutlu enterpolasyon bölgesi için başlangıç ​​ve bitiş değerlerini belirten n + 1 değerleri (yani, v 0 ila v n ) vardır . Enterpolasyonlar aşağıdaki formülü kullanır:

C değeri için k bul şu şekilde :

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

C ' sonucunu şu şekilde verir:

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

Bu formülün bir açıklaması:

  • invert()[- değer, 1]: Filtre bu tabloyu tanımlar. Bu, tableValues veya v .
  • Formül, n + 1 tablonun uzunluğu olacak şekilde n'yi tanımlar . Tablonun uzunluğu 2 olduğu için n = 1.
  • Formül k'yi tanımlar , k ve k + 1 tablonun indeksleridir. Tablonun 2 öğesi olduğundan, k = 0.

Böylece formülü şu şekilde basitleştirebiliriz:

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

Tablonun değerlerini satır içine alırsak:

C '= değer + C * (1 - değer - değer)

Bir basitleştirme daha:

C '= değer + C * (1-2 * değer)

Spesifikasyon, C ve C ' yi 0-1 sınırları içinde (0-255'in aksine) RGB değerleri olarak tanımlar . Sonuç olarak, hesaplamadan önce değerleri küçültmeli ve sonra tekrar ölçeklendirmeliyiz.

Böylece uygulamamıza ulaşıyoruz:

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

Ara: @ Dave'in kaba kuvvet algoritması

@ Dave'in kodu , aşağıdakiler dahil 176.660 filtre kombinasyonu oluşturur :

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

Filtreleri aşağıdaki sırayla hesaplar:

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

Daha sonra tüm hesaplanan renkleri yineler. Tolerans dahilinde oluşturulmuş bir renk bulduğunda durur (tüm RGB değerleri, hedef rengin 5 birim içindedir).

Ancak bu yavaş ve verimsizdir. Böylece kendi cevabımı sunuyorum.

SPSA'nın uygulanması

İlk olarak, bir filtre kombinasyonu tarafından üretilen renk ile hedef renk arasındaki farkı döndüren bir kayıp fonksiyonu tanımlamalıyız . Filtreler mükemmelse, kayıp işlevi 0 döndürmelidir.

Renk farkını iki ölçümün toplamı olarak ölçeceğiz:

  • RGB farkı, çünkü amaç en yakın RGB değerini üretmektir.
  • HSL farkı, çünkü birçok HSL değeri filtrelere karşılık gelir (örn. Ton kabaca hue-rotate(), doygunluk ile ilişkilidir saturate(), vb.) Bu, algoritmayı yönlendirir.

Kayıp işlevi bir bağımsız değişken alır - bir dizi filtre yüzdesi.

Aşağıdaki filtre sırasını kullanacağız:

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

Uygulama:

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

Kayıp işlevini şu şekilde en aza indirmeye çalışacağız:

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

SPSA algoritması ( web sitesi , daha fazla bilgi , kağıt , uygulama kağıt , referans kodu ) bu işte çok iyidir. Karmaşık sistemleri yerel minimum, gürültülü / doğrusal olmayan / çok değişkenli kayıp fonksiyonları vb. İle optimize etmek için tasarlanmıştır . Satranç motorlarını ayarlamak için kullanılmıştır . Ve diğer birçok algoritmanın aksine, onu açıklayan makaleler aslında anlaşılırdır (büyük bir çabayla da olsa).

Uygulama:

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

SPSA'da bazı değişiklikler / optimizasyonlar yaptım:

  • Sonuncusu yerine üretilen en iyi sonucu kullanmak.
  • Tüm diziler yeniden kullanma ( deltas, highArgs, lowArgs) yerine her yineleme ile yeniden yaratma.
  • Tek bir değer yerine a için bir değer dizisi kullanma . Bunun nedeni, tüm filtrelerin farklı olmasıdır ve bu nedenle farklı hızlarda hareket etmeleri / birleşmeleri gerekir.
  • fixHer yinelemeden sonra bir işlevi çalıştırma . saturate(Maksimumun% 7500 olduğu) brightnessve contrast(maksimumun% 200 olduğu) ve hueRotate(değerlerin kelepçelenmek yerine etrafına sarıldığı ) hariç tüm değerleri% 0 ile% 100 arasında sıkıştırır.

SPSA'yı iki aşamalı bir süreçte kullanıyorum:

  1. Arama alanını "keşfetmeye" çalışan "geniş" aşama. Sonuçlar tatmin edici değilse, SPSA için sınırlı yeniden deneme yapacaktır.
  2. Geniş sahneden en iyi sonucu alan ve onu "iyileştirmeye" çalışan "dar" sahne. A ve a için dinamik değerler kullanır .

Uygulama:

function solve() {
    let result = this.solveNarrow(this.solveWide());
    return {
        values: result.values,
        loss: result.loss,
        filter: this.css(result.values)
    };
}

function solveWide() {
    const A = 5;
    const c = 15;
    const a = [60, 180, 18000, 600, 1.2, 1.2];

    let best = { loss: Infinity };
    for(let i = 0; best.loss > 25 && i < 3; i++) {
        let initial = [50, 20, 3750, 50, 100, 100];
        let result = this.spsa(A, a, c, initial, 1000);
        if(result.loss < best.loss) { best = result; }
    } return best;
}

function solveNarrow(wide) {
    const A = wide.loss;
    const c = 2;
    const A1 = A + 1;
    const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
    return this.spsa(A, a, c, wide.values, 500);
}

SPSA'yı Ayarlama

Uyarı: Ne yaptığınızı bildiğinizden emin olmadığınız sürece SPSA koduyla, özellikle de sabitleriyle uğraşmayın .

Önemli sabitler bir , bir , C , başlangıç değerleri, yeniden deneme eşikleri, değerleri maxiçinde fix()ve her bir aşamanın yineleme sayısı. Tüm bu değerler, iyi sonuçlar elde etmek için dikkatlice ayarlandı ve bunlarla rastgele karıştırmak, algoritmanın kullanışlılığını neredeyse kesin olarak azaltacaktır.

Değiştirmekte ısrar ediyorsanız, "optimize etmeden" önce ölçüm yapmalısınız.

İlk önce bu yamayı uygulayın .

Ardından kodu Node.js'de çalıştırın. Oldukça uzun bir süre sonra, sonuç şu şekilde olmalıdır:

Average loss: 3.4768521401985275
Average time: 11.4915ms

Şimdi sabitleri kalbinizin içeriğine göre ayarlayın.

Bazı ipuçları:

  • Ortalama kayıp 4 civarında olmalıdır. 4'ten büyükse, çok uzak sonuçlar veriyor demektir ve doğruluk için ayarlamanız gerekir. 4'ten azsa, zaman kaybediyordur ve yineleme sayısını azaltmalısınız.
  • Yineleme sayısını artırır / azaltırsanız, A'yı ayarlayın uygun şekilde .
  • A'yı artırır / azaltırsanız , bir uygun.
  • --debugHer yinelemenin sonucunu görmek istiyorsanız bayrağı kullanın .

TL; DR


3
Geliştirme sürecinin çok güzel bir özeti! Düşüncelerimi mi okuyorsun ?!
Dave

1
@Dave Aslında bağımsız olarak bunun üzerinde çalışıyordum ama beni yendin.
MultiplyByZer0

4
Mükemmel cevap! Bu codepen'de uygulama
KyleMit

3
Bu tamamen çılgınca bir yöntem. Doğrudan bir SVG filtresi (feColorMatrix'teki beşinci sütun) kullanarak bir renk ayarlayabilir ve bu filtreye CSS'den başvurabilirsiniz - neden bu yöntemi kullanmayasınız?
Michael Mullany

2
@MichaelMullany Bunun üzerinde ne kadar çalıştığımı düşünürsek, bu benim için utanç verici. Yönteminizi düşünmedim, ancak şimdi anlıyorum - bir öğeyi rastgele herhangi bir renge yeniden renklendirmek için, dinamik olarak , uygun değerlere sahip a <filter>içeren bir SVG oluşturursunuz <feColorMatrix>(hedef RGB'yi içeren son sütun hariç tüm sıfırlar değerler, 0 ve 1), SVG'yi DOM'a ekleyin ve CSS'den filtreye referans verin. Lütfen çözümünüzü bir cevap olarak yazın (bir demo ile), ben de oylayacağım.
MultiplyByZer0

55

Bu tavşan deliğinden aşağıya epey bir yolculuktu ama işte burada!

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>

DÜZENLEME: Bu çözüm, üretimde kullanım için tasarlanmamıştır ve yalnızca OP'nin istediği şeyi elde etmek için alınabilecek bir yaklaşımı göstermektedir. Olduğu gibi, renk spektrumunun bazı alanlarında zayıftır. Adım yinelemelerinde daha fazla ayrıntıyla veya @ MultiplyByZer0'ın cevabında ayrıntılı olarak açıklanan nedenlerle daha fazla filtre işlevi uygulayarak daha iyi sonuçlar elde edilebilir .

EDIT2: OP, kaba kuvvet dışı bir çözüm arıyor. Bu durumda oldukça basit, sadece şu denklemi çözün:

CSS Filtre Matrisi Denklemleri

nerede

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

Takarsam 255,0,255, dijital renk ölçüm cihazım sonucu #d619d9yerine olarak bildiriyor #ff00ff.
Siguza

@Siguza Kesinlikle mükemmel değil, kenar kasa renkleri ilmeklerdeki sınırlar ayarlanarak ince ayar yapılabilir.
Dave

3
Bu denklem "oldukça basit"
olmaktan

Sanırım yukarıdaki denklem de eksik clampmi?
glebm

1
Kelepçenin orada yeri yok. Ve kolej matematiğimden hatırladığım kadarıyla, bu denklemler sayısal hesaplamalarla hesaplanıyor, yani "kaba kuvvet", çok iyi şanslar!
Dave

28

Not: OP silmeyi geri almamı istedi , ancak ödül Dave'in cevabına gidecek.


Sorunun gövdesinde sorulanın ve kesinlikle beklediğimizin bu olmadığını biliyorum, ancak tam olarak bunu yapan bir CSS filtresi var: drop-shadow()

Uyarılar:

  • Gölge, mevcut içeriğin arkasına çekilir. Bu, bazı mutlak konumlandırma hileleri yapmamız gerektiği anlamına gelir.
  • Tüm pikseller aynı şekilde ele alınacak, ancak OP [olmamalıyız] "Siyah dışındaki renklere ne olacağıyla ilgileniyoruz" dedi.
  • Tarayıcı desteği. (Bundan emin değilim, yalnızca en son FF ve chrome sürümlerinde test edildi).

/* 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
Süper zeki, harika! Bu benim için işe yarıyor, takdir ediyorum
jaminroe

Her seferinde renkle% 100 doğru olduğu için bunun daha iyi bir çözüm olduğuna inanıyorum.
user835542

Kod olduğu gibi boş bir sayfa gösterir (W10 FF 69b). Yine de simgeyle ilgili bir sorun yok (ayrı SVG'yi kontrol etti).
Rene van der lende

Ekleme background-color: black;için .icon>spanyapar FF 69b'de bu işi. Ancak simgeyi göstermez.
Rene van der lende

@RenevanderLende FF70'i denedim, hala orada çalışıyor. Sizin için işe yaramazsa, sizin tarafınızdan bir şey olmalı.
Kaiido

15

CSS'den referans alınan bir SVG filtresi kullanarak bunu çok basit hale getirebilirsiniz . Yeniden renklendirmek için yalnızca tek bir feColorMatrix'e ihtiyacınız var. Bu sarıya yeniden renkleniyor. FeColorMatrix'teki beşinci sütun, birim ölçeğindeki RGB hedef değerlerini tutar. (sarı için - 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">


İlginç bir çözüm ama görünüşe göre hedef rengi CSS ile kontrol etmeye izin vermiyor.
glebm

Uygulamak istediğiniz her renk için yeni bir filtre tanımlamalısınız. Ama tamamen doğru. hue-rotate (hue-rotate), yukarıdaki yanıtların da gösterdiği gibi, belirli renkleri kesen bir yaklaşımdır - yani, belirli renkleri doğru bir şekilde kullanarak elde edemezsiniz. Gerçekten ihtiyacımız olan şey bir recolor () CSS filtresi kısaltmasıdır.
Michael Mullany

MultiplyByZer0'ın cevabı, HTML'yi değiştirmeden çok yüksek doğrulukla ulaşan bir dizi filtre hesaplar. hue-rotateTarayıcılarda gerçek güzel olurdu, evet.
glebm

2
feColorMatrix'e "renk-enterpolasyon-filtreleri" = "sRGB" eklediğinizde bu sadece siyah kaynak görüntüler için doğru RGB renkleri üretiyor gibi görünüyor.
John Smith


2

Bir SVG filtresiyle yapılan muamele örneğinin eksik olduğunu fark ettim, benimkini yazdım (mükemmel çalışıyor): (Michael Mullany cevabına bakın), işte istediğiniz rengi elde etmenin yolu:

İşte SVG Filtresini yalnızca code => URL.createObjectURL içinde kullanarak ikinci bir çözüm.


1

sadece kullan

fill: #000000

CSS'deki fillözellik, bir SVG şeklinin rengini doldurmak içindir. fillMülkiyet herhangi bir CSS renk değeri kabul edebilir.


3
Bu, bir SVG görüntüsünün içindeki CSS ile çalışabilir, ancak imgtarayıcı tarafından bir öğeye harici olarak uygulanan CSS olarak çalışmaz .
David Moles

1

Bu cevapla başladım bir svg filtresi kullanarak ve aşağıdaki değişiklikleri yaptım:

Veri url'sinden SVG filtresi

İşaretlemenizin herhangi bir yerinde SVG filtresini tanımlamak istemiyorsanız , bunun yerine bir veri url'si kullanabilirsiniz ( R , G , B ve A'yı istediğiniz renkle değiştirin):

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

Gri tonlamalı yedek

Yukarıdaki sürüm çalışmazsa, gri tonlamalı bir yedek de ekleyebilirsiniz.

saturateVe brightnessişlevleri siyaha herhangi bir renk, (eğer rengi önceden siyahsa o dahil etmek gerekmez) dönüş invertsonra istenen hafiflik (ile aydınlatır L ) ve isteğe bağlı olarak ayrıca saydamlığını (belirtebilirsiniz A ).

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

SCSS karışımı

Rengi dinamik olarak belirtmek istiyorsanız, aşağıdaki SCSS karışımını kullanabilirsiniz:

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

Örnek kullanım:

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

Avantajları:

  • Javascript yok .
  • Ek HTML öğesi yok .
  • CSS filtreleri destekleniyorsa, ancak SVG filtresi çalışmıyorsa, gri tonlamalı bir geri dönüş vardır .
  • Mixin'i kullanırsanız, kullanımı oldukça basittir (yukarıdaki örneğe bakın).
  • Renk, sepya numarasından daha okunabilir ve değiştirilmesi daha kolaydır (saf CSS'deki RGBA bileşenleri ve hatta SCSS'de HEX renkleri bile kullanabilirsiniz).
  • Tuhaf davranışlarındanhue-rotate kaçınır .

Uyarılar:

  • Tüm tarayıcılar bir veri url'sinden (özellikle kimlik karması) SVG filtrelerini desteklemez , ancak mevcut Firefox ve Chromium tarayıcılarda (ve belki diğerlerinde) çalışır.
  • Rengi dinamik olarak belirlemek istiyorsanız, bir SCSS karışımı kullanmanız gerekir.
  • Saf CSS sürümü biraz çirkin, birçok farklı renk istiyorsanız, SVG'yi birden çok kez eklemeniz gerekir.

1
oh bu mükemmel, tam olarak aradığım şey buydu ve SASS'deki her şeyi kullanmak için harika teşekkürler!
ghiscoding

1
@ghiscoding yardımcı olduğuna sevindim!
David Dostal
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.