Cách chuyển màu đen thành bất kỳ màu nào chỉ sử dụng bộ lọc CSS


114

Câu hỏi của tôi là: với một màu RGB mục tiêu, công thức để chuyển màu đen ( #000) thành màu đó chỉ bằng cách sử dụng bộ lọc CSS là gì?

Để một câu trả lời được chấp nhận, nó cần cung cấp một hàm (bằng bất kỳ ngôn ngữ nào) chấp nhận màu đích làm đối số và trả về filterchuỗi CSS tương ứng .

Bối cảnh cho điều này là cần phải tô màu lại SVG bên trong a background-image. Trong trường hợp này, nó là để hỗ trợ một số tính năng toán học TeX trong KaTeX: https://github.com/Khan/KaTeX/issues/587 .

Thí dụ

Nếu màu đích là #ffff00(vàng), một giải pháp đúng là:

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

( bản demo )

Không mục tiêu

  • Hoạt hình.
  • Các giải pháp bộ lọc không CSS.
  • Bắt đầu từ một màu khác với màu đen.
  • Quan tâm đến những gì xảy ra với các màu khác với màu đen.

Kết quả cho đến nay

Bạn vẫn có thể nhận được câu trả lời Được chấp nhận bằng cách gửi một giải pháp không thô bạo!

Tài nguyên

  • Cách thức hue-rotatesepiađược tính toán: https://stackoverflow.com/a/29521147/181228 Ví dụ triển khai 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

    Lưu ý rằng điều clamptrên làm cho hue-rotatehàm phi tuyến tính.

    Triển khai trình duyệt: Chromium , Firefox .

  • Demo: Chuyển sang màu không phải thang độ xám từ màu thang độ xám: https://stackoverflow.com/a/25524145/181228

  • Một công thức gần như hoạt động (từ một câu hỏi tương tự ):
    https://stackoverflow.com/a/29958459/181228

    Giải thích chi tiết về lý do tại sao công thức ở trên sai (CSS hue-rotatekhông phải là xoay màu đúng mà là một phép gần đúng tuyến tính):
    https://stackoverflow.com/a/19325417/2441511


Vì vậy, bạn muốn LERP # 000000 thành #RRGGBB? (Chỉ làm rõ)
Zze

1
Vâng, tuyệt vời - chỉ cần làm rõ rằng bạn không muốn kết hợp quá trình chuyển đổi vào giải pháp.
Zze

1
Có thể là một chế độ hòa trộn sẽ làm việc cho bạn? Bạn có thể dễ dàng chuyển đổi màu đen sang bất kỳ màu nào ... Nhưng tôi không có được bức tranh toàn cầu về những gì bạn muốn đạt được
vals

1
@glebm vậy bạn cần tìm công thức (dùng cách nào) để biến màu đen thành màu nào và áp dụng bằng css?
ProllyGeek

2
@ProllyGeek Có. Một hạn chế khác mà tôi nên đề cập là công thức kết quả không thể là một tra cứu brute force của bảng 5GiB (nó phải có thể sử dụng được từ javascript trên một trang web).
glebm

Câu trả lời:


148

@Dave là người đầu tiên đăng câu trả lời cho vấn đề này (với mã làm việc), và câu trả lời của anh ấy là nguồn cảm hứng vô giá cho việc sao chép và dán vào tôi. Bài đăng này bắt đầu như một nỗ lực giải thích và tinh chỉnh câu trả lời của @ Dave, nhưng kể từ đó nó đã phát triển thành một câu trả lời của riêng mình.

Phương pháp của tôi nhanh hơn đáng kể. Theo điểm chuẩn jsPerf trên các màu RGB được tạo ngẫu nhiên, thuật toán của @ Dave chạy trong 600 mili giây , trong khi thuật toán của tôi chạy trong 30 mili giây . Điều này chắc chắn có thể quan trọng, chẳng hạn như thời gian tải, nơi tốc độ là rất quan trọng.

Hơn nữa, đối với một số màu, thuật toán của tôi hoạt động tốt hơn:

  • Đối với rgb(0,255,0), @ Dave's sản xuất rgb(29,218,34)và sản xuấtrgb(1,255,0)
  • Đối với rgb(0,0,255), @ Dave's sản xuất rgb(37,39,255)và của tôi sản xuấtrgb(5,6,255)
  • Đối với rgb(19,11,118), @ Dave's sản xuất rgb(36,27,102)và của tôi sản xuấtrgb(20,11,112)

Bản giới thiệu

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


Sử dụng

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

Giải trình

Chúng ta sẽ bắt đầu bằng cách viết một số 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)}%);`;
    }
}

Giải trình:

  • Các Colorlớp đại diện cho một màu RGB.
    • toString()Hàm của nó trả về màu trong một rgb(...)chuỗi màu CSS .
    • hsl()Hàm của nó trả về màu, được chuyển đổi thành HSL .
    • clamp()Chức năng của nó đảm bảo rằng một giá trị màu nhất định nằm trong giới hạn (0-255).
  • Các Solverlớp học sẽ cố gắng giải quyết cho một màu sắc mục tiêu.
    • css()Hàm của nó trả về một bộ lọc nhất định trong một chuỗi bộ lọc CSS.

Thực hiện grayscale(), sepia()saturate()

Trung tâm của bộ lọc CSS / SVG là bộ lọc nguyên thủy , đại diện cho các sửa đổi cấp thấp đối với hình ảnh.

Các bộ lọc grayscale(), sepia()saturate()được thực hiện bởi các primative lọc <feColorMatrix>, mà thực hiện phép nhân ma trận giữa một ma trận xác định bởi các bộ lọc (thường tạo động), và một ma trận được tạo ra từ màu sắc. Biểu đồ:

Phép nhân ma trận

Chúng tôi có thể thực hiện một số tối ưu hóa ở đây:

  • Phần tử cuối cùng của ma trận màu đang và sẽ luôn như vậy 1. Không có điểm nào để tính toán hoặc lưu trữ nó.
  • Không có điểm nào để tính toán hoặc lưu trữ giá trị alpha / trong suốt ( A), vì chúng tôi đang xử lý RGB, không phải RGBA.
  • Do đó, chúng ta có thể cắt ma trận bộ lọc từ 5x5 thành 3x5 và ma trận màu từ 1x5 thành 1x3 . Điều này giúp tiết kiệm một chút công việc.
  • Tất cả các <feColorMatrix>bộ lọc để lại cột 4 và 5 là số 0. Do đó, chúng ta có thể giảm thêm ma trận bộ lọc xuống 3x3 .
  • Vì phép nhân tương đối đơn giản, không cần phải kéo trong các thư viện toán học phức tạp cho việc này. Chúng ta có thể tự thực hiện thuật toán nhân ma trận.

Thực hiện:

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

(Chúng tôi sử dụng các biến tạm thời để giữ kết quả của mỗi phép nhân hàng, vì chúng tôi không muốn các thay đổi đối với this.r, v.v. ảnh hưởng đến các phép tính tiếp theo.)

Bây giờ chúng ta đã thực hiện <feColorMatrix>, chúng ta có thể thực hiện grayscale(), sepia()saturate(), mà chỉ đơn giản gọi nó với một ma trận lọc đưa ra:

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

Thực thi hue-rotate()

Bộ hue-rotate()lọc được thực hiện bởi <feColorMatrix type="hueRotate" />.

Ma trận bộ lọc được tính như hình dưới đây:

Ví dụ, phần tử a 00 sẽ được tính như vậy:

Một số lưu ý:

  • Góc quay được cho bằng độ. Nó phải được chuyển đổi sang radian trước khi chuyển đến Math.sin()hoặc Math.cos().
  • Math.sin(angle)Math.cos(angle)nên được tính toán một lần và sau đó lưu vào bộ nhớ cache.

Thực hiện:

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

Thực hiện brightness()contrast()

Bộ lọc brightness()contrast()được triển khai bằng <feComponentTransfer>với <feFuncX type="linear" />.

Mỗi <feFuncX type="linear" />phần tử chấp nhận một thuộc tính độ dốcđánh chặn . Sau đó, nó sẽ tính toán từng giá trị màu mới thông qua một công thức đơn giản:

value = slope * value + intercept

Điều này rất dễ thực hiện:

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

Khi điều này được triển khai brightness()contrast()cũng có thể được triển khai:

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

Thực thi invert()

Bộ invert()lọc được triển khai bằng <feComponentTransfer>với <feFuncX type="table" />.

Thông số kỹ thuật cho biết:

Trong phần sau, C là thành phần ban đầu và C ' là thành phần được ánh xạ lại; cả trong khoảng đóng [0,1].

Đối với "bảng", hàm được xác định bằng nội suy tuyến tính giữa các giá trị được cho trong bảng thuộc tính tableValues . Bảng có n + 1 giá trị (tức là v 0 đến v n ) xác định giá trị bắt đầu và giá trị kết thúc cho n vùng nội suy có kích thước đồng đều. Nội suy sử dụng công thức sau:

Với giá trị C tìm k sao cho:

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

Kết quả C ' được cho bởi:

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

Giải thích về công thức này:

  • Bộ invert()lọc xác định bảng này: [giá trị, 1 - giá trị]. Đây là tableValues hoặc v .
  • Công thức xác định n , sao cho n + 1 là độ dài của bảng. Vì chiều dài của bảng là 2 nên n = 1.
  • Công thức xác định k , với kk + 1 là các chỉ số của bảng. Vì bảng có 2 phần tử nên k = 0.

Do đó, chúng ta có thể đơn giản hóa công thức thành:

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

Nội tuyến các giá trị của bảng, chúng ta còn lại:

C '= giá trị + C * (1 - giá trị - giá trị)

Một đơn giản hóa nữa:

C '= giá trị + C * (1 - 2 * giá trị)

Thông số xác định CC ' là các giá trị RGB, nằm trong giới hạn 0-1 (trái ngược với 0-255). Do đó, chúng ta phải thu nhỏ các giá trị trước khi tính toán và sao lưu chúng sau đó.

Vì vậy, chúng tôi đi đến triển khai của mình:

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

Interlude: Thuật toán bạo lực của @ Dave

Mã của @ Dave tạo ra 176.660 kết hợp bộ lọc, bao gồm:

  • 11 invert()bộ lọc (0%, 10%, 20%, ..., 100%)
  • 11 sepia()bộ lọc (0%, 10%, 20%, ..., 100%)
  • 20 saturate()bộ lọc (5%, 10%, 15%, ..., 100%)
  • 73 hue-rotate()bộ lọc (0deg, 5deg, 10deg, ..., 360deg)

Nó tính toán các bộ lọc theo thứ tự sau:

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

Sau đó, nó lặp lại qua tất cả các màu được tính toán. Nó dừng lại khi nó đã tìm thấy một màu được tạo trong phạm vi dung sai (tất cả các giá trị RGB nằm trong vòng 5 đơn vị tính từ màu đích).

Tuy nhiên, việc này diễn ra chậm và không hiệu quả. Vì vậy, tôi trình bày câu trả lời của riêng tôi.

Triển khai SPSA

Đầu tiên, chúng ta phải xác định một hàm mất mát , hàm này trả về sự khác biệt giữa màu được tạo ra bởi tổ hợp bộ lọc và màu đích. Nếu bộ lọc hoàn hảo, hàm mất mát sẽ trả về 0.

Chúng tôi sẽ đo lường sự khác biệt màu sắc dưới dạng tổng của hai số liệu:

  • Sự khác biệt RGB, bởi vì mục tiêu là tạo ra giá trị RGB gần nhất.
  • Sự khác biệt HSL, vì nhiều giá trị HSL tương ứng với các bộ lọc (ví dụ: màu sắc tương quan với hue-rotate(), độ bão hòa tương quan với saturate(), v.v.) Điều này hướng dẫn thuật toán.

Hàm mất mát sẽ nhận một đối số - một mảng phần trăm bộ lọc.

Chúng tôi sẽ sử dụng thứ tự bộ lọc sau:

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

Thực hiện:

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

Chúng tôi sẽ cố gắng giảm thiểu hàm mất mát, như vậy:

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

Các SPSA thuật toán ( trang web , biết thêm , giấy , giấy thi , mã tham chiếu ) là rất tốt lúc này. Nó được thiết kế để tối ưu hóa các hệ thống phức tạp với các hàm cực tiểu cục bộ, nhiễu / phi tuyến / đa biến, v.v. Nó đã được sử dụng để điều chỉnh các động cơ cờ vua . Và không giống như nhiều thuật toán khác, các bài báo mô tả nó thực sự có thể hiểu được (mặc dù rất nỗ lực).

Thực hiện:

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

Tôi đã thực hiện một số sửa đổi / tối ưu hóa cho SPSA:

  • Sử dụng kết quả tốt nhất được tạo ra, thay vì kết quả cuối cùng.
  • Tái sử dụng tất cả các mảng ( deltas, highArgs, lowArgs), thay vì tái tạo chúng với mỗi lần lặp.
  • Sử dụng một mảng giá trị cho a , thay vì một giá trị duy nhất. Điều này là do tất cả các bộ lọc đều khác nhau, và do đó chúng sẽ di chuyển / hội tụ với tốc độ khác nhau.
  • Chạy một fixhàm sau mỗi lần lặp. Nó kẹp tất cả các giá trị trong khoảng từ 0% đến 100%, ngoại trừ saturate(trong đó giá trị tối đa là 7500%), brightnesscontrast(trong đó giá trị tối đa là 200%) và hueRotate(trong đó các giá trị được quấn quanh thay vì kẹp).

Tôi sử dụng SPSA trong một quy trình hai giai đoạn:

  1. Giai đoạn "rộng", cố gắng "khám phá" không gian tìm kiếm. Nó sẽ giới hạn số lần thử lại SPSA nếu kết quả không đạt yêu cầu.
  2. Giai đoạn "hẹp", lấy kết quả tốt nhất từ ​​giai đoạn rộng và cố gắng "tinh chỉnh" nó. Nó sử dụng các giá trị động cho Aa .

Thực hiện:

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

Điều chỉnh SPSA

Cảnh báo: Không làm rối mã SPSA, đặc biệt là với các hằng số của nó, trừ khi bạn chắc chắn rằng mình biết mình đang làm gì.

Các hằng số quan trọng là A , a , c , giá trị ban đầu, ngưỡng thử lại, giá trị maxtrong fix()và số lần lặp của mỗi giai đoạn. Tất cả các giá trị này đã được điều chỉnh cẩn thận để tạo ra kết quả tốt và việc thay đổi ngẫu nhiên chúng gần như chắc chắn sẽ làm giảm tính hữu dụng của thuật toán.

Nếu bạn khăng khăng muốn thay đổi nó, bạn phải đo lường trước khi bạn "tối ưu hóa".

Đầu tiên, hãy áp dụng bản vá này .

Sau đó chạy mã trong Node.js. Sau một thời gian, kết quả sẽ như thế này:

Average loss: 3.4768521401985275
Average time: 11.4915ms

Bây giờ hãy điều chỉnh các hằng số cho phù hợp với trái tim của bạn.

Một số lời khuyên:

  • Mức tổn thất trung bình nên vào khoảng 4. Nếu lớn hơn 4, nó đang tạo ra kết quả quá xa và bạn nên điều chỉnh để có độ chính xác. Nếu nhỏ hơn 4 thì rất lãng phí thời gian và bạn nên giảm số lần lặp lại.
  • Nếu bạn tăng / giảm số lần lặp, hãy điều chỉnh A hợp lý.
  • Nếu bạn tăng / giảm A , hãy điều chỉnh a hợp lý.
  • Sử dụng --debugcờ nếu bạn muốn xem kết quả của mỗi lần lặp.

TL; DR


3
Tóm tắt rất hay về quá trình phát triển! Bạn đang đọc những suy nghĩ của tôi ?!
Dave

1
@Dave Trên thực tế, tôi đã làm việc này một cách độc lập, nhưng bạn đã đánh bại tôi.
MultiplyByZer0

4
Câu trả lời chính xác! Thực hiện trong codepen này
KyleMit

3
Đây là một phương pháp hoàn toàn điên rồ. Bạn có thể đặt màu trực tiếp bằng cách sử dụng bộ lọc SVG (cột thứ năm trong feColorMatrix) và bạn có thể tham chiếu bộ lọc đó từ CSS - tại sao bạn không sử dụng phương pháp đó?
Michael Mullany

2
@MichaelMullany Chà, điều đó thật đáng xấu hổ đối với tôi, khi xem xét tôi đã làm việc này trong bao lâu. Tôi không nghĩ ra phương pháp của bạn, nhưng bây giờ tôi hiểu - để tô màu lại một phần tử thành bất kỳ màu nào tùy ý, bạn chỉ cần tạo động một SVG với một <filter>chứa a <feColorMatrix>với các giá trị thích hợp (tất cả các số 0 ngoại trừ cột cuối cùng chứa RGB mục tiêu giá trị, 0 và 1), chèn SVG vào DOM và tham chiếu bộ lọc từ CSS. Vui lòng viết giải pháp của bạn dưới dạng câu trả lời (kèm theo bản demo) và tôi sẽ ủng hộ.
MultiplyByZer0

55

Đây là một chuyến đi xuống lỗ thỏ nhưng đây rồi!

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>

CHỈNH SỬA: Giải pháp này không nhằm mục đích sử dụng trong sản xuất và chỉ minh họa một cách tiếp cận có thể được thực hiện để đạt được những gì OP đang yêu cầu. Như vậy, nó yếu ở một số vùng của quang phổ màu. Kết quả tốt hơn có thể đạt được bằng cách chi tiết hơn trong các bước lặp lại hoặc bằng cách triển khai nhiều chức năng lọc hơn vì những lý do được mô tả chi tiết trong câu trả lời của @ MultiplyByZer0 .

EDIT2: OP đang tìm kiếm một giải pháp không vũ phu. Trong trường hợp đó, nó khá đơn giản, chỉ cần giải phương trình này:

Phương trình ma trận bộ lọc CSS

Ở đâu

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

Nếu tôi đưa vào 255,0,255, máy đo màu kỹ thuật số của tôi sẽ báo cáo kết quả #d619d9thay vì #ff00ff.
Siguza

@Siguza Nó chắc chắn không hoàn hảo, màu sắc của vỏ máy có thể được điều chỉnh bằng cách điều chỉnh ranh giới trong các vòng lặp.
Dave

3
Phương trình đó là bất cứ điều gì nhưng "khá đơn giản"
MultiplyByZer0

Tôi nghĩ rằng phương trình trên cũng bị thiếu clamp?
glebm

1
Kẹp không có chỗ trong đó. Và từ những gì tôi nhớ từ môn toán thời đại học của mình, những phương trình này được tính bằng các phép tính số hay còn gọi là "brute force", chúc may mắn!
Dave

28

Lưu ý: OP yêu cầu tôi phục hồi , nhưng tiền thưởng sẽ thuộc về câu trả lời của Dave.


Tôi biết đó không phải là những gì được hỏi trong phần nội dung câu hỏi và chắc chắn không phải là điều mà tất cả chúng ta đang chờ đợi, nhưng có một bộ lọc CSS thực hiện chính xác điều này: drop-shadow()

Lưu ý:

  • Bóng được vẽ phía sau nội dung hiện có. Điều này có nghĩa là chúng ta phải thực hiện một số thủ thuật định vị tuyệt đối.
  • Tất cả các pixel sẽ được xử lý như nhau, nhưng OP nói [chúng ta không nên] "Quan tâm đến những gì xảy ra với các màu khác với màu đen."
  • Hỗ trợ trình duyệt. (Tôi không chắc về nó, chỉ được thử nghiệm dưới FF và 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
Siêu thông minh, tuyệt vời! Điều này phù hợp với tôi, đánh giá cao nó
jaminroe

Tôi tin rằng đây là một giải pháp tốt hơn vì nó luôn chính xác 100% với màu sắc.
user835542,

Mã nguyên trạng hiển thị một trang trống (W10 FF 69b). Tuy nhiên, không có gì sai với biểu tượng (đã chọn SVG riêng biệt).
Rene van der Lende

Thêm background-color: black;để .icon>spanlàm cho điều này hoạt động cho FF 69b. Tuy nhiên, không hiển thị biểu tượng.
Rene van der Lende

@RenevanderLende Vừa thử trên FF70 vẫn hoạt động ở đó. Nếu nó không hiệu quả với bạn, nó phải là thứ gì đó ở phía bạn.
Kaiido

15

Bạn có thể thực hiện tất cả điều này rất đơn giản bằng cách sử dụng bộ lọc SVG được tham chiếu từ CSS. Bạn chỉ cần một feColorMatrix duy nhất để tạo lại màu. Cái này đổi màu thành màu vàng. Cột thứ năm trong feColorMatrix chứa các giá trị đích RGB trên thang đơn vị. (đối với màu vàng - nó là 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">


Một giải pháp thú vị nhưng có vẻ như nó không cho phép kiểm soát màu đích thông qua CSS.
glebm

Bạn phải xác định một bộ lọc mới cho mỗi màu bạn muốn áp dụng. Nhưng nó hoàn toàn chính xác. xoay màu là một phép gần đúng cắt một số màu nhất định - nghĩa là bạn không thể đạt được chính xác các màu nhất định bằng cách sử dụng nó - như các câu trả lời ở trên chứng thực. Những gì chúng ta thực sự cần là một bộ lọc CSS recolor () viết tắt.
Michael Mullany

Câu trả lời của MultiplyByZer0 tính toán một loạt các bộ lọc đạt được với độ chính xác rất cao mà không cần sửa đổi HTML. Đúng hue-rotatetrong các trình duyệt sẽ rất tuyệt.
glebm

2
có vẻ như điều này chỉ tạo ra màu RGB chính xác cho hình ảnh nguồn màu đen khi bạn thêm "color-interpolation-filter" = "sRGB" vào feColorMatrix.
John Smith

Cạnh 12-18 bị loại bỏ vì chúng không hỗ trợ urlchức năng caniuse.com/#search=svg%20filter
Volker E.

2

Tôi nhận thấy rằng ví dụ về xử lý thông qua bộ lọc SVG chưa hoàn chỉnh, tôi đã viết của tôi (hoạt động hoàn hảo): (xem câu trả lời của Michael Mullany) vì vậy đây là cách để có được bất kỳ màu nào bạn muốn:

Đây là giải pháp thứ hai, bằng cách sử dụng Bộ lọc SVG chỉ trong mã => URL.createObjectURL


1

chỉ dùng

fill: #000000

Các fillbất động sản trong CSS là dành cho điền vào màu sắc của một hình dạng SVG. Các fillbất động sản có thể chấp nhận bất kỳ giá trị màu CSS.


3
Điều này có thể hoạt động với CSS bên trong ảnh SVG, nhưng nó không hoạt động như CSS được imgtrình duyệt áp dụng bên ngoài cho một phần tử.
David Moles

0

Tôi bắt đầu với câu trả lời này bằng cách sử dụng bộ lọc svg và thực hiện các sửa đổi sau:

Bộ lọc SVG từ url dữ liệu

Nếu bạn không muốn xác định bộ lọc SVG ở đâu đó trong đánh dấu của mình, bạn có thể sử dụng url dữ liệu thay thế (thay thế R , G , BA bằng màu mong muốn):

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

Dự phòng thang độ xám

Nếu phiên bản trên không hoạt động, bạn cũng có thể thêm dự phòng thang độ xám.

Các hàm saturatebrightnesschuyển bất kỳ màu nào thành màu đen (bạn không cần phải thêm màu đó nếu màu đó đã là màu đen), invertsau đó làm sáng nó với độ đậm nhạt mong muốn ( L ) và tùy chọn bạn cũng có thể chỉ định độ mờ ( A ).

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

SCSS mixin

Nếu bạn muốn chỉ định màu động, bạn có thể sử dụng kết hợp SCSS sau:

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

Ví dụ sử dụng:

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

Ưu điểm:

  • Không có Javascript .
  • Không có phần tử HTML bổ sung .
  • Nếu bộ lọc CSS được hỗ trợ, nhưng bộ lọc SVG không hoạt động, thì sẽ có dự phòng thang độ xám .
  • Nếu bạn sử dụng mixin, cách sử dụng khá đơn giản (xem ví dụ ở trên).
  • Màu sắc dễ đọc hơn và dễ sửa đổi hơn so với thủ thuật màu nâu đỏ (các thành phần RGBA trong CSS thuần túy và bạn thậm chí có thể sử dụng màu HEX trong SCSS).
  • Tránh các hành vi kỳ lạ củahue-rotate .

Lưu ý:

  • Không phải tất cả các trình duyệt đều hỗ trợ bộ lọc SVG từ url dữ liệu (đặc biệt là mã băm id), nhưng nó hoạt động trong các trình duyệt Firefox và Chromium hiện tại (và có thể các trình duyệt khác).
  • Nếu bạn muốn chỉ định màu động, bạn phải sử dụng một bộ trộn SCSS.
  • Phiên bản thuần CSS hơi xấu, nếu bạn muốn có nhiều màu khác nhau, bạn phải thêm SVG nhiều lần.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.