web-dev-qa-db-ja.com

CSSフィルターのみを使用して黒を任意の色に変換する方法

私の質問は、ターゲットRGB色が与えられた場合、 CSSフィルター のみを使用して黒(#000)をその色に色変更する式は何ですか?

回答を受け入れるには、ターゲットの色を引数として受け入れ、対応するCSS filter文字列を返す関数を(任意の言語で)提供する必要があります。

このコンテキストは、background-image内のSVGの色を変更する必要があることです。この場合、KaTeXで特定のTeX数学機能をサポートすることです: https://github.com/Khan/KaTeX/issues/587

ターゲットの色が#ffff00(黄色)の場合、1つの正しい解決策は次のとおりです。

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

デモ

非目標

  • アニメーション。
  • 非CSSフィルターソリューション。
  • 黒以外の色から始めます。
  • 黒以外の色に何が起こるかを気にする。

これまでの結果

それでも、ブルートフォースではないソリューションを送信することで、Accepted回答を得ることができます!

資源

  • hue-rotatesepiaの計算方法: https://stackoverflow.com/a/29521147/181228 例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
    

    上記のclampは、hue-rotate関数を非線形にすることに注意してください。

    ブラウザの実装: ChromiumFirefox

  • デモ:グレースケールカラーから非グレースケールカラーへのアクセス: https://stackoverflow.com/a/25524145/181228

  • almostが機能する式( 類似した質問 から):
    https://stackoverflow.com/a/29958459/181228

    上記の式が間違っている理由の詳細な説明(CSS hue-rotateは真の色相回転ではなく、線形近似です):
    https://stackoverflow.com/a/19325417/2441511

85
glebm

@Daveが最初に投稿したのは 回答 (作業コード付き)であり、彼の回答は非常に貴重な情報源です。 恥知らずなコピーと貼り付け 私にインスピレーション。この投稿は、@ Daveの回答を説明および改良する試みとして始まりましたが、それ以来、独自の回答に進化しました。

私の方法は非常に高速です。ランダムに生成されたRGBカラーの jsPerfベンチマーク によれば、@ Daveのアルゴリズムは600ミリ秒で実行されますが、私の場合は30 ms。これは、速度が重要なロード時間など、間違いなく重要です。

さらに、一部の色については、私のアルゴリズムのパフォーマンスが向上しています。

  • rgb(0,255,0)の場合、@ Daveはrgb(29,218,34)を生成し、rgb(1,255,0)を生成します
  • rgb(0,0,255)の場合、@ Daveはrgb(37,39,255)を生成し、私のものはrgb(5,6,255)を生成します
  • rgb(19,11,118)の場合、@ Daveはrgb(36,27,102)を生成し、私のものはrgb(20,11,112)を生成します

デモ

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

使用法

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

説明

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

説明:

  • Colorクラスは、RGBカラーを表します。
    • toString()関数は、CSSのrgb(...)色文字列で色を返します。
    • hsl()関数は、 HSL に変換された色を返します。
    • clamp()関数は、指定された色の値が範囲内(0〜255)であることを保証します。
  • Solverクラスは、ターゲットカラーを解決しようとします。
    • css()関数は、CSSフィルター文字列で指定されたフィルターを返します。

grayscale()sepia()、およびsaturate()の実装

CSS/SVGフィルターの中心は フィルタープリミティブ です。これは、画像に対する低レベルの変更を表します。

フィルター grayscale()sepia() 、および saturate() は、フィルタープリミティブによって実装されます <feColorMatrix> 、これは マトリックス乗算 フィルターで指定されたマトリックス(多くの場合、動的に生成されます)と、色から作成されたマトリックスの間。図:

Matrix multiplication

ここでいくつかの最適化を行うことができます。

  • カラーマトリックスの最後の要素は、常に1です。計算したり保存したりする意味はありません。
  • RGBAではなくRGBを扱っているため、アルファ/透明度の値(A)を計算または保存する意味もありません。
  • したがって、フィルターマトリックスを5x5から3x5に、カラーマトリックスを1x5から1x3にトリミングできます。これにより、作業が少し節約されます。
  • すべての<feColorMatrix>フィルターは、列4および5をゼロのままにします。 したがって、フィルター行列をさらに3x3に減らすことができます。
  • 乗算は比較的単純なので、このために 複雑な数学ライブラリ をドラッグする必要はありません。行列乗算アルゴリズムを自分で実装できます。

実装:

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

(一時変数を使用して、各行の乗算の結果を保持します。これは、this.rなどの変更が後続の計算に影響しないようにするためです。)

<feColorMatrix>を実装したので、特定のフィルターマトリックスで単純に呼び出すgrayscale()sepia()、およびsaturate()を実装できます。

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

hue-rotate()の実装

hue-rotate() フィルターは <feColorMatrix type="hueRotate" /> によって実装されます。

フィルター行列は次のように計算されます。

たとえば、要素a00は次のように計算されます。

いくつかのメモ:

  • 回転角度は度で指定されます。 Math.sin()またはMath.cos()に渡す前にラジアンに変換する必要があります。
  • Math.sin(angle)Math.cos(angle)は一度計算してからキャッシュする必要があります。

実装:

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

brightness()およびcontrast()の実装

brightness() および contrast() フィルターは <feComponentTransfer><feFuncX type="linear" /> で実装されます。

<feFuncX type="linear" />要素は、slopeおよびintercept属性を受け入れます。次に、単純な式を使用して新しい各色の値を計算します。

value = slope * value + intercept

これは簡単に実装できます。

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

これが実装されると、brightness()contrast()も実装できます。

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

invert()の実装

invert() フィルターは <feComponentTransfer><feFuncX type="table" /> で実装されます。

仕様の状態:

以下では、Cは初期コンポーネントであり、C 'は再マップされたコンポーネントです。両方とも閉区間[0,1]にあります。

「テーブル」の場合、関数は属性tableValuesで指定された値間の線形補間によって定義されます。テーブルには、n+ 1個の値(つまり、v vへnn均等サイズの補間領域の開始値と終了値を指定します。補間は次の式を使用します。

Cの場合、次のようなkを見つけます。

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

結果C 'は次のように与えられます:

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

この式の説明:

  • invert()フィルターは、このテーブルを定義します:[値、1-値]。これは、tableValuesまたはvです。
  • 式はnを定義し、n+ 1はテーブルの長さです。テーブルの長さは2であるため、n= 1.
  • 式はkを定義し、kおよびk+ 1はテーブル。テーブルには2つの要素があるため、k= 0です。

したがって、式を次のように単純化できます。

C '= v + C *(v1 -v

テーブルの値をインライン化すると、次のようになります。

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

もう1つの簡略化:

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

仕様では、CC 'がRGB値であり、境界0-1(0- 255)。その結果、計算の前に値を縮小し、後で拡大する必要があります。

したがって、実装に到達します。

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

間奏:@Daveのブルートフォースアルゴリズム

@Daveのコードは、以下を含む176,660フィルターの組み合わせを生成します。

  • 11個のinvert()フィルター(0%、10%、20%、...、100%)
  • 11個のsepia()フィルター(0%、10%、20%、...、100%)
  • 20個のsaturate()フィルター(5%、10%、15%、...、100%)
  • 73 hue-rotate()フィルター(0度、5度、10度、...、360度)

次の順序でフィルターを計算します。

filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);

次に、計算されたすべての色を反復処理します。許容範囲内で生成された色が見つかると停止します(すべてのRGB値はターゲット色から5単位以内です)。

ただし、これは遅く、非効率的です。したがって、私は自分の答えを提示します。

SPSAの実装

まず、 損失関数 を定義する必要があります。これは、フィルターの組み合わせによって生成される色とターゲットの色の差を返します。フィルターが完全な場合、損失関数は0を返します。

色差を2つのメトリックの合計として測定します。

  • 目標は最も近いRGB値を生成することであるため、RGBの違い。
  • HSLの違い。これは、多くのHSL値がフィルターに対応しているためです(たとえば、色相はhue-rotate()とほぼ相関しており、彩度はsaturate()と相関しています)。これがアルゴリズムを導きます。

損失関数は、1つの引数(フィルターの割合の配列)を取ります。

次のフィルター次数を使用します。

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

実装:

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

次のような損失関数を最小化しようとします。

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

SPSA アルゴリズム( website詳細paperimplementation paper参照コード )は非常に優れています。局所的な最小値、ノイズ/非線形/多変量損失関数などを使用して複雑なシステムを最適化するように設計されました。 チェスエンジンの調整に使用されています 。そして、他の多くのアルゴリズムとは異なり、それを説明する論文は実際に理解できます(多大な努力を払っていますが)。

実装:

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にいくつかの変更/最適化を行いました。

  • 最後ではなく、生成された最良の結果を使用します。
  • 反復ごとに再作成する代わりに、すべての配列(deltashighArgslowArgs)を再利用します。
  • 単一の値の代わりに、aの値の配列を使用します。これは、すべてのフィルターが異なるため、異なる速度で移動/収束する必要があるためです。
  • 各反復後にfix関数を実行します。 saturate(最大値は7500%)、brightnessおよびcontrast(最大値は200%)、およびhueRotate(値はクランプではなくラップされます)。

SPSAは2段階のプロセスで使用します。

  1. サーチスペースを「探索」しようとする「ワイド」ステージ。結果に満足できない場合、SPSAの再試行は制限されます。
  2. 「ナロー」ステージ。ワイドステージから最高の結果を取得し、それを「洗練」しようとします。 Aおよびaに動的な値を使用します。

実装:

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のチューニング

警告:SPSAコード、特に定数は、sure自分が何をしているかわからない限り混乱させないでください。

重要な定数は、Aac、初期値値、再試行しきい値、fix()maxの値、および各ステージの反復回数。これらの値はすべて、良好な結果が得られるように慎重に調整されており、ランダムにねじ込むと、アルゴリズムの有用性がほぼ確実に低下します。

変更を主張する場合は、「最適化」する前に測定する必要があります。

まず、 このパッチ を適用します。

次に、Node.jsでコードを実行します。しばらくすると、結果は次のようになります。

Average loss: 3.4768521401985275
Average time: 11.4915ms

ここで、定数を心ゆくまで調整してください。

いくつかのヒント:

  • 平均損失は約4である必要があります。4を超える場合、結果があまりにも離れすぎているため、精度を調整する必要があります。 4未満の場合、時間を浪費しているため、反復回数を減らす必要があります。
  • 反復回数を増減する場合は、Aを適切に調整します。
  • Aを増減する場合は、aを適切に調整します。
  • 各反復の結果を表示する場合は、--debugフラグを使用します。

TL; DR

71
MultiplyByZer0

これはウサギの穴を下るかなりの旅行でしたが、ここにあります!

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:このソリューションは、実稼働での使用を意図したものではなく、OPが求めているものを達成するために実行できるアプローチを示しています。そのままで、色スペクトルの一部の領域では弱いです。より良い結果を得るには、ステップの繰り返しをより細かくするか、または @ MultiplyByZer0's answer で詳細に説明されている理由により、より多くのフィルター関数を実装します。

EDIT2:OPは非ブルートフォースソリューションを探しています。その場合、それは非常に簡単で、この方程式を解くだけです:

CSS Filter Matrix Equations

どこ

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

注:OPは削除を取り消すように私に頼んだ 、しかし賞金はデイブの答えに行くものとする。


私はそれが質問の本文で尋ねられたものではなく、確かに私たち全員が待っていたものではないことを知っていますが、まさにこれを行うCSSフィルターがあります: drop-shadow()

注意事項:

  • 影は既存のコンテンツの背後に描画されます。これは、絶対位置決めのトリックを作成する必要があることを意味します。
  • すべてのピクセルは同じように扱われますが、OPは[そうすべきではありません]「黒以外の色に何が起こるかを気にかけます。」
  • ブラウザのサポート。 (それについてはよくわかりませんが、最新のFFと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>
25
Kaiido

CSSから参照されているSVGフィルターを使用するだけで、これをすべてveryにすることができます。色を変更するには、feColorMatrixが1つだけ必要です。これは黄色に変色します。 feColorMatrixの5番目の列には、単位スケールのRGBターゲット値が保持されます。 (黄色の場合-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">
5
Michael Mullany

SVGフィルターによる処理の例が不完全であることに気付いたので、私は(完全に機能する)私が書いた:(マイケルミュラニーの回答をご覧ください)

PickColor.onchange=()=>{
    RGBval.textContent = PickColor.value;

    let 
    HexT = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(PickColor.value),
    r = parseInt(HexT[1], 16),
    g = parseInt(HexT[2], 16),
    b = parseInt(HexT[3], 16);

    FilterVal.textContent =  SetFilter( r, g, b);
}
function SetFilter( r, g, b )
{
    const Matrix  = document.querySelector('#FilterSVG feColorMatrix');
    r = r/255;
    g = g/255;
    b = b/255;

    Matrix.setAttribute("values",  "0 0 0 0 "+r+" 0 0 0 0 "+g+ " 0 0 0 0 "+b+" 0 0 0 1 0");

    return "\n 0 0 0 0 "+r+"\n 0 0 0 0 "+g+ "\n 0 0 0 0 "+b+"\n 0 0 0 1 0"
}
#RGBval    { text-transform: uppercase }
#PickColor { height: 50px; margin: 0 20px }
th         { background-color: lightblue; padding: 5px 20px }
pre        { margin: 0 15px }
#ImgTest   { filter: url(#FilterSVG) }
<svg height="0px" width="0px">
    <defs>
      <filter id="FilterSVG" color-interpolation-filters="sRGB">
        <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
      </filter>
    </defs>
  </svg>

  <table>
    <caption>SVG method</caption>
    <tr> <th>Image</th> <th>Color</th> </tr>
    <tr>
      <td><img src="https://www.nouveauelevator.com/image/black-icon/Android.png" id="ImgTest" /></td> 
      <td><input type="color" value="#000000"  id="PickColor" ></td>
    </tr>
    <tr> <td>.</td> <td>.</td> </tr>
    <tr> <th>Filter value </th> <th>#RBG target</th> </tr>
    <tr>
      <td><pre id="FilterVal">
    0 0 0 0 0
    0 0 0 0 0
    0 0 0 0 0
    0 0 0 1 0</pre></td>
        <td id="RGBval">#000000</td>
    </tr>
  </table>

2番目のソリューションは、コードでのみSVGフィルターを使用することです=> RL.createObjectURL

const
  SVG_Filter = {
    init(ImgID) 
    {
      this.Img = document.getElementById(ImgID);
      let
        NS = 'http://www.w3.org/2000/svg';

      this.SVG    = document.createElementNS(NS,'svg'),
      this.filter = document.createElementNS(NS,'filter'),
      this.matrix = document.createElementNS(NS,'feColorMatrix');

      this.filter.setAttribute( 'id', 'FilterSVG');
      this.filter.setAttribute( 'color-interpolation-filters', 'sRGB');

      this.matrix.setAttribute( 'type', 'matrix');
      this.matrix.setAttribute('values', '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0');

      this.filter.appendChild(this.matrix);
      this.SVG.appendChild(this.filter);

      this.xXMLs = new XMLSerializer();
    },
    SetColor( r, g, b )
    {
      r = r/255;
      g = g/255;
      b = b/255;

      this.matrix.setAttribute('values', '0 0 0 0 '+r+' 0 0 0 0 '+g+ ' 0 0 0 0 '+b+' 0 0 0 1 0');

      let
        xBlob = new Blob( [ this.xXMLs.serializeToString(this.SVG) ], { type: 'image/svg+xml' });
        xURL  = URL.createObjectURL(xBlob);

      this.Img.style.filter = 'url(' + xURL + '#FilterSVG)';

      return '\n 0 0 0 0 '+r+'\n 0 0 0 0 '+g+ '\n 0 0 0 0 '+b+'\n 0 0 0 1 0';
    }
  }

SVG_Filter.init('ImgTest');

PickColor.onchange=()=>{
  RGBval.textContent = PickColor.value;

  let 
    HexT = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(PickColor.value),
    r = parseInt(HexT[1], 16),
    g = parseInt(HexT[2], 16),
    b = parseInt(HexT[3], 16);

  FilterVal.textContent = SVG_Filter.SetColor( r, g, b );
}
#RGBval    { text-transform: uppercase }
#PickColor { height: 50px; margin: 0 20px }
th         { background-color: lightblue; padding: 5px 20px }
pre        { margin: 0 15px }
#PickColor { width:90px; height:28px; }
<table>
  <caption>SVG method</caption>
  <tr> <th>Image</th> <th>Color</th> </tr>
  <tr>
    <td><img src="https://www.nouveauelevator.com/image/black-icon/Android.png" id="ImgTest" /></td> 
    <td><input type="color" value="#E2218A" id="PickColor" ></td>
  </tr>
  <tr> <td>.</td> <td>.</td> </tr>
  <tr> <th>Filter value </th> <th>#RBG target</th> </tr>
  <tr>
    <td><pre id="FilterVal">
  0 0 0 0 0
  0 0 0 0 0
  0 0 0 0 0
  0 0 0 1 0</pre></td>
      <td id="RGBval">#000000</td>
  </tr>
</table>
1
Mister Jojo