Come trasformare il nero in un dato colore usando solo filtri CSS


116

La mia domanda è: dato un colore RGB target, qual è la formula per ricolorare black ( #000) in quel colore usando solo filtri CSS ?

Affinché una risposta venga accettata, è necessario fornire una funzione (in qualsiasi lingua) che accetti il ​​colore di destinazione come argomento e restituisca la filterstringa CSS corrispondente .

Il contesto per questo è la necessità di ricolorare un SVG all'interno di un file background-image. In questo caso, supporta alcune funzionalità matematiche di TeX in KaTeX: https://github.com/Khan/KaTeX/issues/587 .

Esempio

Se il colore target è #ffff00(giallo), una soluzione corretta è:

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

( demo )

Non-obiettivi

  • Animazione.
  • Soluzioni senza filtri CSS.
  • A partire da un colore diverso dal nero.
  • Preoccuparsi di ciò che accade ai colori diversi dal nero.

Risultati finora

Puoi ancora ottenere una risposta accettata inviando una soluzione non a forza bruta!

risorse

  • Come hue-rotatee sepiavengono calcolati: https://stackoverflow.com/a/29521147/181228 Esempio di implementazione di 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

    Nota che quanto clampsopra rende il filehue-rotate funzione non lineare.

    Implementazioni del browser: Chromium , Firefox .

  • Demo: ottenere un colore non in scala di grigi da un colore in scala di grigi: https://stackoverflow.com/a/25524145/181228

  • Una formula che quasi funziona (da una domanda simile ):
    https://stackoverflow.com/a/29958459/181228

    Una spiegazione dettagliata del motivo per cui la formula sopra è sbagliata (CSS hue-rotatenon è una vera rotazione della tonalità ma un'approssimazione lineare):
    https://stackoverflow.com/a/19325417/2441511


Quindi vuoi LERP # 000000 a #RRGGBB? (Solo per chiarire)
Zze

1
Sì, dolcezza, sto solo chiarendo che non volevi incorporare una transizione nella soluzione.
Zze

1
Potrebbe essere una modalità di fusione che funzionerebbe per te? Puoi convertire facilmente il nero in qualsiasi colore ... Ma non ho il quadro globale di ciò che vuoi ottenere
vals

1
@glebm quindi devi trovare una formula (usando qualsiasi metodo) per trasformare il nero in qualsiasi colore e applicarla usando css?
ProllyGeek

2
@ProllyGeek Sì. Un altro vincolo che dovrei menzionare è che la formula risultante non può essere una ricerca di forza bruta di una tabella 5GiB (dovrebbe essere utilizzabile ad esempio da javascript su una pagina web).
glebm

Risposte:


149

@Dave è stato il primo a pubblicare una risposta a questa domanda (con codice funzionante) e la sua risposta è stata una fonte inestimabile di copia e incolla spudorata per me ispirazione per . Questo post è iniziato come un tentativo di spiegare e perfezionare la risposta di @ Dave, ma da allora si è evoluto in una risposta a sé stante.

Il mio metodo è notevolmente più veloce. Secondo un benchmark jsPerf sui colori RGB generati casualmente, l'algoritmo di @ Dave funziona in 600 ms , mentre il mio funziona in 30 ms . Questo può sicuramente avere importanza, ad esempio nel tempo di caricamento, dove la velocità è fondamentale.

Inoltre, per alcuni colori, il mio algoritmo funziona meglio:

  • Perché rgb(0,255,0), @ Dave's produce rgb(29,218,34)e producergb(1,255,0)
  • Perché rgb(0,0,255), @ Dave's produce rgb(37,39,255)e il mio producergb(5,6,255)
  • Perché rgb(19,11,118), @ Dave's produce rgb(36,27,102)e il mio producergb(20,11,112)

dimostrazione

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


uso

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

Spiegazione

Inizieremo scrivendo un po 'di 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)}%);`;
    }
}

Spiegazione:

  • La Colorclasse rappresenta un colore RGB.
    • La sua toString()funzione restituisce il colore in una rgb(...)stringa di colori CSS .
    • La sua hsl()funzione restituisce il colore, convertito in HSL .
    • La sua clamp()funzione garantisce che un dato valore di colore rientri nei limiti (0-255).
  • La Solverclasse tenterà di risolvere un colore target.
    • La sua css()funzione restituisce un dato filtro in una stringa di filtro CSS.

Implementazione grayscale(), sepia()esaturate()

Il cuore dei filtri CSS / SVG sono le primitive dei filtri , che rappresentano modifiche di basso livello a un'immagine.

I filtri grayscale(), sepia()e saturate()sono implementati dalla primativa del filtro <feColorMatrix>, che esegue la moltiplicazione di matrici tra una matrice specificata dal filtro (spesso generata dinamicamente) e una matrice creata dal colore. Diagramma:

Moltiplicazione di matrici

Ci sono alcune ottimizzazioni che possiamo apportare qui:

  • L'ultimo elemento della matrice dei colori è e sarà sempre 1. Non ha senso calcolarlo o memorizzarlo.
  • Non ha senso calcolare o memorizzare Aneanche il valore alfa / trasparenza ( ), poiché si tratta di RGB, non di RGBA.
  • Pertanto, possiamo tagliare le matrici dei filtri da 5x5 a 3x5 e la matrice dei colori da 1x5 a 1x3 . Ciò consente di risparmiare un po 'di lavoro.
  • Tutti i <feColorMatrix>filtri lasciano le colonne 4 e 5 come zero.Pertanto, possiamo ridurre ulteriormente la matrice del filtro a 3x3 .
  • Poiché la moltiplicazione è relativamente semplice, non è necessario trascinare in complesse librerie matematiche per questo. Possiamo implementare noi stessi l'algoritmo di moltiplicazione di matrici.

Implementazione:

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

(Usiamo variabili temporanee per contenere i risultati di ogni moltiplicazione di riga, perché non vogliamo modifiche a this.r , ecc. Influenzino i calcoli successivi.)

Ora che abbiamo implementato <feColorMatrix>, possiamo implementare grayscale(), sepia()e saturate()che semplicemente invoke con una data matrice del filtro:

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

Implementazione hue-rotate()

Il hue-rotate()filtro è implementato da <feColorMatrix type="hueRotate" />.

La matrice del filtro viene calcolata come mostrato di seguito:

Ad esempio, l'elemento a 00 verrebbe calcolato in questo modo:

Alcune note:

  • L'angolo di rotazione è espresso in gradi. Deve essere convertito in radianti prima di essere passato a Math.sin()o Math.cos().
  • Math.sin(angle)e Math.cos(angle)dovrebbe essere calcolato una volta e quindi memorizzato nella cache.

Implementazione:

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

Implementazione brightness()econtrast()

I filtri brightness()e contrast()sono implementati da <feComponentTransfer>con <feFuncX type="linear" />.

Ogni <feFuncX type="linear" />elemento accetta un attributo di pendenza e intercetta . Quindi calcola ogni nuovo valore di colore attraverso una semplice formula:

value = slope * value + intercept

Questo è facile da implementare:

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

Una volta implementato, brightness()e contrast()può essere implementato anche:

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

Implementazione invert()

Il invert()filtro è implementato da <feComponentTransfer>con <feFuncX type="table" />.

La specifica afferma:

Di seguito, C è il componente iniziale e C ' è il componente rimappato; entrambi nell'intervallo chiuso [0,1].

Per "table", la funzione è definita dall'interpolazione lineare tra i valori forniti nell'attributo tableValues . La tabella ha n + 1 valori (cioè, v 0 av n ) che specificano i valori iniziale e finale per n regioni di interpolazione di dimensioni uguali. Le interpolazioni utilizzano la seguente formula:

Per un valore C trova k tale che:

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

Il risultato C ' è dato da:

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

Una spiegazione di questa formula:

  • Il invert()filtro definisce questa tabella: [valore, 1 - valore]. Questo è tableValues o v .
  • La formula definisce n , in modo tale che n + 1 sia la lunghezza della tabella. Poiché la lunghezza della tabella è 2, n = 1.
  • La formula definisce k , dove k e k + 1 sono gli indici della tabella. Poiché la tabella ha 2 elementi, k = 0.

Quindi, possiamo semplificare la formula per:

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

Inlining i valori della tabella, ci rimane:

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

Un'altra semplificazione:

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

La specifica definisce C e C ' come valori RGB, entro i limiti 0-1 (invece di 0-255). Di conseguenza, dobbiamo ridimensionare i valori prima del calcolo e ridimensionarli nuovamente dopo.

Arriviamo così alla nostra implementazione:

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

Intermezzo: l'algoritmo di forza bruta di @ Dave

Il codice di @ Dave genera 176.660 combinazioni di filtri, tra cui:

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

Calcola i filtri nel seguente ordine:

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

Quindi itera attraverso tutti i colori calcolati. Si ferma una volta che ha trovato un colore generato entro la tolleranza (tutti i valori RGB sono entro 5 unità dal colore target).

Tuttavia, questo è lento e inefficiente. Quindi, presento la mia risposta.

Implementazione di SPSA

Innanzitutto, dobbiamo definire una funzione di perdita , che restituisca la differenza tra il colore prodotto da una combinazione di filtri e il colore target. Se i filtri sono perfetti, la funzione di perdita dovrebbe restituire 0.

Misureremo la differenza di colore come la somma di due metriche:

  • Differenza RGB, perché l'obiettivo è produrre il valore RGB più vicino.
  • Differenza HSL, perché molti valori HSL corrispondono ai filtri (es. La tonalità è approssimativamente correlata con hue-rotate(), la saturazione è correlata saturate(), ecc.) Questo guida l'algoritmo.

La funzione di perdita prenderà un argomento: un array di percentuali di filtro.

Useremo il seguente ordine di filtro:

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

Implementazione:

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

Cercheremo di ridurre al minimo la funzione di perdita, in modo tale che:

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

L' algoritmo SPSA ( sito web , maggiori informazioni , documento , documento di implementazione , codice di riferimento ) è molto bravo in questo. È stato progettato per ottimizzare sistemi complessi con minimi locali, funzioni di perdita rumorosa / non lineare / multivariata, ecc. È stato utilizzato per mettere a punto i motori scacchistici . E a differenza di molti altri algoritmi, i documenti che lo descrivono sono effettivamente comprensibili (anche se con grande sforzo).

Implementazione:

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

Ho apportato alcune modifiche / ottimizzazioni a SPSA:

  • Utilizzando il miglior risultato prodotto, invece dell'ultimo.
  • Riutilizzando tutte le matrici ( deltas, highArgs, lowArgs), invece di ricreare con ogni iterazione.
  • Utilizzo di una matrice di valori per a , invece di un singolo valore. Questo perché tutti i filtri sono diversi e quindi dovrebbero muoversi / convergere a velocità diverse.
  • Esecuzione di una fixfunzione dopo ogni iterazione. Blocca tutti i valori tra lo 0% e il 100%, tranne saturate(dove il massimo è 7500%) brightnesse contrast(dove il massimo è 200%) e hueRotate(dove i valori sono avvolti invece che bloccati).

Uso SPSA in un processo a due fasi:

  1. Il palco "ampio", che cerca di "esplorare" lo spazio di ricerca. Se i risultati non sono soddisfacenti, effettuerà un numero limitato di tentativi di SPSA.
  2. Il palco "stretto", che prende il miglior risultato dal palco largo e cerca di "rifinirlo". Utilizza valori dinamici per A e a .

Implementazione:

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

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

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

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

Tuning SPSA

Attenzione: non scherzare con il codice SPSA, specialmente con le sue costanti, a meno che tu non sia sicuro di sapere cosa stai facendo.

Le costanti importanti sono A , a , c , i valori iniziali, le soglie di tentativi, i valori di maxin fix()e il numero di iterazioni di ciascuna fase. Tutti questi valori sono stati attentamente regolati per produrre buoni risultati e avvitarli in modo casuale ridurrà quasi definitivamente l'utilità dell'algoritmo.

Se insisti a modificarlo, devi misurare prima di "ottimizzare".

Per prima cosa, applica questa patch .

Quindi esegui il codice in Node.js. Dopo un po 'di tempo, il risultato dovrebbe essere qualcosa del genere:

Average loss: 3.4768521401985275
Average time: 11.4915ms

Ora sintonizza le costanti al contenuto del tuo cuore.

Alcuni suggerimenti:

  • La perdita media dovrebbe essere di circa 4. Se è maggiore di 4, sta producendo risultati troppo distanti e dovresti regolare per la precisione. Se è inferiore a 4, significa perdere tempo e dovresti ridurre il numero di iterazioni.
  • Se aumenti / diminuisci il numero di iterazioni, regola A in modo appropriato.
  • Se si aumenta / diminuisce A , regolare un modo appropriato.
  • Usa il --debugflag se vuoi vedere il risultato di ogni iterazione.

TL; DR


3
Riassunto molto bello del processo di sviluppo! Stai leggendo i miei pensieri ?!
Dave

1
@ Dave In realtà, ci stavo lavorando in modo indipendente, ma mi hai battuto.
MultiplyByZer0


3
Questo è un metodo completamente folle. Puoi impostare un colore direttamente usando un filtro SVG (quinta colonna in una feColorMatrix) e puoi fare riferimento a quel filtro da CSS - perché non dovresti usare quel metodo?
Michael Mullany il

2
@MichaelMullany Beh, è ​​imbarazzante per me, considerando quanto tempo ci ho lavorato. Non ho pensato al tuo metodo, ma ora capisco: per ricolorare un elemento in qualsiasi colore arbitrario, devi semplicemente generare dinamicamente un SVG con un <filter>contenente <feColorMatrix>i valori corretti (tutti gli zeri tranne l'ultima colonna, che contiene l'RGB di destinazione valori, 0 e 1), inserisci SVG nel DOM e fai riferimento al filtro da CSS. Per favore scrivi la tua soluzione come risposta (con una demo) e io voterò a favore.
MultiplyByZer0

55

Questo è stato un bel viaggio nella tana del coniglio, ma eccolo qui!

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: questa soluzione non è destinata all'uso in produzione e illustra solo un approccio che può essere adottato per ottenere ciò che OP richiede. Così com'è, è debole in alcune aree dello spettro dei colori. Risultati migliori possono essere ottenuti con una maggiore granularità nelle iterazioni dei passaggi o implementando più funzioni di filtro per i motivi descritti in dettaglio nella risposta di @ MultiplyByZer0 .

EDIT2: OP sta cercando una soluzione non di forza bruta. In tal caso è piuttosto semplice, risolvi questa equazione:

Equazioni della matrice del filtro CSS

dove

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

Se lo inserisco 255,0,255, il mio misuratore di colore digitale riporta il risultato come #d619d9invece di #ff00ff.
Siguza

@Siguza Non è sicuramente perfetto, i colori del case edge possono essere modificati regolando i bordi nei loop.
Dave

3
Quell'equazione è tutt'altro che "piuttosto semplice"
MultiplyByZer0

Penso che manchi anche l'equazione di cui sopra clamp?
glebm

1
Il morsetto non ha posto lì. E da quello che ricordo dai miei calcoli universitari, queste equazioni sono calcolate da calcoli numerici aka "forza bruta" quindi buona fortuna!
Dave

28

Nota: OP mi ha chiesto di annullare l'eliminazione , ma la taglia andrà alla risposta di Dave.


So che non è quello che è stato chiesto nel corpo della domanda, e certamente non quello che stavamo aspettando, ma c'è un filtro CSS che fa esattamente questo: drop-shadow()

Avvertenze:

  • L'ombra viene disegnata dietro il contenuto esistente. Ciò significa che dobbiamo fare alcuni trucchi di posizionamento assoluto.
  • Tutti i pixel saranno trattati allo stesso modo, ma OP ha detto [non dovremmo essere] "Preoccuparsi di ciò che accade ai colori diversi dal nero".
  • Supporto del browser. (Non ne sono sicuro, testato solo con gli ultimi FF e chrome).

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

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


/* the content */

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

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

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


1
Super intelligente, fantastico! Questo funziona per me, lo apprezzo
jaminroe

Credo che questa sia una soluzione migliore poiché è sempre accurata al 100% con il colore.
user835542

Il codice così com'è mostra una pagina vuota (W10 FF 69b). Niente di sbagliato con l'icona, però (controllato SVG separato).
Rene van der Lende

L'aggiunta background-color: black;di .icon>spanrende questo lavoro per FF 69b. Tuttavia, non mostra l'icona.
Rene van der Lende

@RenevanderLende Appena provato su FF70 funziona ancora lì. Se non funziona per te, deve essere qualcosa dalla tua parte.
Kaiido

15

Puoi rendere tutto molto semplice usando semplicemente un filtro SVG a cui fa riferimento CSS. Hai solo bisogno di un singolo feColorMatrix per eseguire una ricolorazione. Questo ricolora in giallo. La quinta colonna in feColorMatrix contiene i valori target RGB sulla scala delle unità. (per il giallo - è 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">


Una soluzione interessante ma sembra che non consenta il controllo del colore target tramite CSS.
glebm

Devi definire un nuovo filtro per ogni colore che desideri applicare. Ma è completamente accurato. hue-rotate è un'approssimazione che ritaglia determinati colori, il che significa che non puoi ottenere determinati colori in modo accurato usandolo, come attestano le risposte sopra. Ciò di cui abbiamo veramente bisogno è una scorciatoia del filtro CSS recolor ().
Michael Mullany

La risposta di MultiplyByZer0 calcola una serie di filtri che raggiungono con altissima precisione, senza modificare l'HTML. Un vero hue-rotatenei browser sarebbe carino, sì.
glebm

2
sembra che questo produca colori RGB accurati per le immagini di origine nera solo quando aggiungi "color-interpolation-filters" = "sRGB" a feColorMatrix.
John Smith

Edge 12-18 sono esclusi in quanto non supportano la urlfunzione caniuse.com/#search=svg%20filter
Volker E.

2

Ho notato che l'esempio del trattamento tramite un filtro SVG era incompleto, ho scritto il mio (che funziona perfettamente): (vedi la risposta di Michael Mullany) quindi ecco il modo per ottenere il colore che desideri:

Ecco una seconda soluzione, utilizzando SVG Filter solo in code => URL.createObjectURL


1

basta usare

fill: #000000

La fillproprietà in CSS serve per riempire il colore di una forma SVG. La fillproprietà può accettare qualsiasi valore di colore CSS.


3
Potrebbe funzionare con CSS interno a un'immagine SVG, ma non funziona poiché CSS applicato esternamente a un imgelemento dal browser.
David Moles il

1

Ho iniziato con questa risposta utilizzando un filtro svg e apportato le seguenti modifiche:

Filtro SVG dall'URL dei dati

Se non desideri definire il filtro SVG da qualche parte nel markup, puoi utilizzare invece un URL di dati (sostituire R , G , B e A con il colore desiderato):

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

Fallback in scala di grigi

Se la versione precedente non funziona, puoi anche aggiungere un fallback in scala di grigi.

Le funzioni saturatee brightnesstrasformano qualsiasi colore in nero (non è necessario includerlo se il colore è già nero), invertquindi lo schiarisce con la luminosità desiderata ( L ) e facoltativamente puoi anche specificare l'opacità ( A ).

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

SCSS mixin

Se vuoi specificare il colore dinamicamente, puoi usare il seguente mixin SCSS:

@mixin recolor($color: #000, $opacity: 1) {
  $r: red($color) / 255;
  $g: green($color) / 255;
  $b: blue($color) / 255;
  $a: $opacity;

  // grayscale fallback if SVG from data url is not supported
  $lightness: lightness($color);
  filter: saturate(0%) brightness(0%) invert($lightness) opacity($opacity);

  // color filter
  $svg-filter-id: "recolor";
  filter: url('data:image/svg+xml;utf8,\
    <svg xmlns="http://www.w3.org/2000/svg">\
      <filter id="#{$svg-filter-id}" color-interpolation-filters="sRGB">\
        <feColorMatrix type="matrix" values="\
          0 0 0 0 #{$r}\
          0 0 0 0 #{$g}\
          0 0 0 0 #{$b}\
          0 0 0 #{$a} 0\
        "/>\
      </filter>\
    </svg>\
    ##{$svg-filter-id}');
}

Utilizzo di esempio:

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

vantaggi:

  • Nessun Javascript .
  • Nessun elemento HTML aggiuntivo .
  • Se i filtri CSS sono supportati, ma il filtro SVG non funziona, c'è un fallback in scala di grigi .
  • Se usi il mixin, l'utilizzo è abbastanza semplice (vedi esempio sopra).
  • Il colore è più leggibile e più facile da modificare rispetto al trucco seppia (componenti RGBA in puro CSS e puoi persino usare colori HEX in SCSS).
  • Evita lo strano comportamento dihue-rotate .

Avvertenze:

  • Non tutti i browser supportano i filtri SVG da un URL di dati (in particolare l'hash dell'id), ma funziona negli attuali browser Firefox e Chromium (e forse altri).
  • Se vuoi specificare il colore dinamicamente, devi usare un mixin SCSS.
  • La versione Pure CSS è un po 'brutta, se vuoi molti colori diversi devi includere SVG più volte.

1
oh questo è perfetto, questo è esattamente quello che stavo cercando, ovvero usare tutto in SASS fantastico, grazie mille!
ghiscoding
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.