Як перетворити чорний в будь-який колір, використовуючи лише фільтри CSS


114

Моє запитання: з урахуванням цільового кольору RGB, яка формула для перефарбування чорного ( #000) у цей колір, використовуючи лише CSS-фільтри ?

Щоб відповідь була прийнята, потрібно було б надати функцію (будь-якою мовою), яка б приймала цільовий колір як аргумент і повертала відповідний CSS filter рядок .

Контекстом для цього є необхідність відновити SVG всередині a background-image. У цьому випадку це підтримка певних функцій математики TeX в KaTeX: https://github.com/Khan/KaTeX/isissue/587 .

Приклад

Якщо цільовий колір #ffff00(жовтий), одне правильне рішення:

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

( демонстрація )

Нецілі

  • Анімація.
  • Не рішення CSS-фільтрів.
  • Починаючи з іншого кольору, ніж чорного.
  • Турбота про те, що відбувається з іншими кольорами, крім чорного.

Результати поки що

Ви все ще можете отримати прийняту відповідь, подавши рішення, що не стосується грубої сили!

Ресурси

  • Як hue-rotateі sepiaобчислюються: 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 функцію нелінійною.

    Реалізації браузера: Chromium , Firefox .

  • Демонстрація: Отримання кольору без відтінків сірого від кольору сірого: https://stackoverflow.com/a/25524145/181228

  • Формула, яка майже працює (з подібного питання ):
    https://stackoverflow.com/a/29958459/181228

    Детальне пояснення того, чому формула вище неправильна (CSS hue-rotate- це не справжнє обертання відтінку, а лінійне наближення):
    https://stackoverflow.com/a/19325417/2441511


Отже, ви хочете LERP # 000000 до #RRGGBB? (Тільки
уточнюю

1
Так солодко - просто уточнюючи, що ви не хочете включати перехід у рішення.
Zze

1
Можливо, режим суміші буде працювати для вас? Ви можете легко перетворити чорний колір у будь-який колір ... Але я не розумію глобальної картини того, чого ви хочете досягти
вал

1
@glebm, тож вам потрібно знайти формулу (використовуючи будь-який метод), щоб перетворити чорний колір у будь-який колір і застосувати його за допомогою css?
ProllyGeek

2
@ProllyGeek Так. Ще одне обмеження, яке я мушу зазначити, - це те, що отримана формула не може бути грубою силою пошуку таблиці 5GiB (вона повинна бути використана, наприклад, на JavaScript на веб-сторінці).
Глебм

Відповіді:


148

@Dave першим опублікував відповідь на це (з робочим кодом), і його відповідь була неоціненним джерелом безсоромної копії та вставлення натхнення для мене. Цей пост почався як спроба пояснити та уточнити відповідь @ Дейва, але з тих пір перетворився на власну відповідь.

Мій метод значно швидший. Відповідно до еталону jsPerf на випадково генерованих кольорах RGB, алгоритм @ Дейва працює в 600 мс , тоді як мін працює в 30 мс . Це, безумовно, може мати значення, наприклад, у час завантаження, де швидкість є критичною.

Крім того, для деяких кольорів мій алгоритм працює краще:

  • Бо rgb(0,255,0)@ Dave's виробляє rgb(29,218,34)і виробляєrgb(1,255,0)
  • Бо rgb(0,0,255)@ Dave's виробляєrgb(37,39,255)і моя виробляєrgb(5,6,255)
  • Бо rgb(19,11,118)@ Dave's виробляє 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()функція повертає колір у rgb(...)рядку кольорів CSS .
    • Його hsl()функція повертає колір, перетворений у HSL .
    • Його clamp()функція забезпечує, що задане значення кольору знаходиться в межах (0-255).
  • SolverКлас буде намагатися вирішити для необхідного кольору.
    • Його css()функція повертає заданий фільтр у рядку фільтра CSS.

Реалізація grayscale(), sepia()іsaturate()

Серцем фільтрів CSS / SVG є фільтри примітивів , які представляють модифікації зображення низького рівня.

Фільтри grayscale(), sepia()і saturate()реалізуються за допомогою фільтра примітиву <feColorMatrix>, який виконує матричне множення між матрицею, заданою фільтром (часто динамічно генерується), і матрицею, створеною з кольору. Діаграма:

Матричне множення

Тут можна зробити кілька оптимізацій:

  • Останній елемент кольорової матриці є і завжди буде 1. Немає сенсу його обчислювати чи зберігати.
  • Немає сенсу обчислювати або зберігати значення альфа / прозорості ( A), оскільки ми маємо справу з RGB, а не RGBA.
  • Тому ми можемо обрізати матриці фільтра від 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" />.

Матриця фільтру обчислюється, як показано нижче:

Наприклад, елемент 00 буде розраховуватися наступним чином:

Деякі примітки:

  • Кут повороту задається в градусах. Він повинен бути перетворений в радіани, перш ніж перейти до 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" />елемент приймає атрибут нахилу та перехоплення . Потім він обчислює кожне нове значення кольору за допомогою простої формули:

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].

Для "таблиці" функція визначається лінійною інтерполяцією між значеннями, наведеними в таблиці атрибутівValues . У таблиці є n + 1 значення (тобто v 0 до v n ) із зазначенням початкового та кінцевого значень для n рівних розмірів інтерполяційних областей. Інтерполяції використовують наступну формулу:

Для значення C знайдіть k таке, що:

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

Результат C ' задається:

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

Пояснення цієї формули:

  • invert()Фільтра визначає цю таблицю: [значення, 1 - значення]. Це tableValues або v .
  • Формула визначає n , таким, що n + 1 - довжина таблиці. Оскільки довжина таблиці дорівнює 2, n = 1.
  • Формула визначає k , при цьому k і k + 1 є індексами таблиці. Оскільки таблиця має 2 елементи, k = 0.

Таким чином, ми можемо спростити формулу до:

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

Вкладаючи значення таблиці, нам залишається:

C '= значення + C * (1 - значення - значення)

Ще одне спрощення:

C '= значення + C * (значення 1 - 2 *)

Специфікація визначає значення C і C ' як значення 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);
}

Інтерлюдія: @ алгоритм грубої сили @ Дейва

@ Код Дейва генерує 176 660 комбінацій фільтрів, включаючи:

  • 11 invert()фільтрів (0%, 10%, 20%, ..., 100%)
  • 11 sepia()фільтрів (0%, 10%, 20%, ..., 100%)
  • 20 saturate()фільтрів (5%, 10%, 15%, ..., 100%)
  • 73 hue-rotate()фільтри (0deg, 5deg, 10deg, ..., 360deg)

Він обчислює фільтри в такому порядку:

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

Потім вона повторюється через усі обчислені кольори. Він зупиняється, коли він знайде згенерований колір у межах допуску (усі значення RGB знаходяться в межах 5 одиниць від цільового кольору).

Однак це повільно і неефективно. Таким чином, я представляю власну відповідь.

Впровадження SPSA

По-перше, ми повинні визначити функцію втрат , яка повертає різницю між кольором, отриманим комбінацією фільтрів, і цільовим кольором. Якщо фільтри ідеальні, функція втрати повинна повернути 0.

Ми будемо вимірювати різницю кольорів як суму двох показників:

  • Різниця в RGB, оскільки мета полягає в тому, щоб отримати найближче значення RGB.
  • Різниця HSL, оскільки багато значень HSL відповідають фільтрам (наприклад, відтінок приблизно корелює з hue-rotate()насиченням, корелює з saturate()тощо). Це керується алгоритмом.

Функція втрати візьме один аргумент - масив відсотків фільтра.

Ми будемо використовувати такий фільтр:

filter: invert(a%) sepia(b%) saturate(c%) hue-rotatedeg) 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 ( веб-сайт , додаткова інформація , папір , документ про реалізацію , довідковий код ) дуже хороший у цьому. Він був розроблений для оптимізації складних систем з локальними мінімумами, шумовими / нелінійними / багатоваріантними функціями втрат тощо. Він був використаний для налаштування шахових двигунів . І на відміну від багатьох інших алгоритмів, документи, що описують його, насправді зрозумілі (хоча і з великими зусиллями).

Впровадження:

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:

  • Використання найкращого результату, а не останнього.
  • Повторне використання всіх масивів ( deltas, highArgs, lowArgs), замість того , щоб відтворити їх з кожної ітерації.
  • Використовуючи масив значень для , замість одного значення. Це тому, що всі фільтри різні, і тому вони повинні рухатися / зближуватися з різною швидкістю.
  • Запуск fixфункції після кожної ітерації. Він затискає всі значення від 0% до 100%, за винятком saturate(де максимум становить 7500%) brightnessі contrast(де максимум - 200%) та hueRotate(де значення обмотуються замість затиснутих).

Я використовую SPSA у двоступеневому процесі:

  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, особливо з його константами, якщо ви не впевнені, що знаєте, що робите.

Важливими константами є A , a , c , початкові значення, порогові значення, значення maxin fix()та кількість ітерацій кожного етапу. Усі ці значення були ретельно налаштовані, щоб отримати хороші результати, і випадкове вкручування їх майже напевно зменшить корисність алгоритму.

Якщо ви наполягаєте на її зміні, потрібно провести вимірювання, перш ніж "оптимізувати".

Спочатку застосуйте цей пластир .

Потім запустіть код у Node.js. Через досить тривалий час результат повинен бути приблизно таким:

Average loss: 3.4768521401985275
Average time: 11.4915ms

Тепер налаштуйте константи на зміст вашого серця.

Деякі поради:

  • Середня втрата повинна становити близько 4. Якщо вона більша за 4, це дає занадто далекі результати, і вам слід налаштуватися на точність. Якщо вона менше 4, це витрачає час, і вам слід зменшити кількість ітерацій.
  • Якщо ви збільшуєте / зменшуєте кількість ітерацій, відрегулюйте A відповідним чином.
  • Якщо збільшити / зменшити A , відрегулюйте відповідним чином .
  • Використовуйте --debugпрапор, якщо ви хочете побачити результат кожної ітерації.

TL; DR


3
Дуже приємний підсумок процесу розробки! Читаєте ви мої думки ?!
Дейв

1
@Dave Насправді я працював над цим самостійно, але ти мене до цього побив.
MultiplyByZer0

4
Чудова відповідь! Реалізація в цьому кодексі
KyleMit

3
Це абсолютно божевільний метод. Ви можете встановити колір безпосередньо за допомогою фільтра SVG (п'ятий стовпець у feColorMatrix), і ви можете посилатися на цей фільтр з CSS - чому б ви не використовували цей метод?
Майкл Муллані

2
@MichaelMullany Ну, це мені бентежить, враховуючи, як довго я працюю над цим. Я не думав про ваш метод, але тепер я розумію - щоб відновити елемент до будь-якого довільного кольору, ви просто динамічно генеруєте SVG з <filter>вмістом a <feColorMatrix>з відповідними значеннями (усі нулі, крім останнього стовпця, який містить цільовий RGB значення 0 і 1), вставте SVG у DOM та відправте фільтр із CSS. Будь ласка, напишіть своє рішення як відповідь (з демонстрацією), і я піднесу пропозицію.
MultiplyByZer0

55

Це була повністю поїздка вниз у кролячій норі, але ось вона!

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: Це рішення не призначене для використання у виробництві і лише ілюструє підхід, який можна застосувати для досягнення того, чого вимагає ОП. Оскільки він слабкий у деяких областях кольорового спектру. Кращі результати можуть бути досягнуті за рахунок більш деталізованості в ітераціях кроків або за допомогою реалізації більше функцій фільтра з причин, детально описаних у відповіді @ MultiplyByZer0 .

EDIT2: ОП шукає рішення, яке не стосується грубої сили. У цьому випадку це досить просто, просто вирішіть це рівняння:

Матричні рівняння фільтрів CSS

де

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

Якщо я вставлю 255,0,255, мій цифровий кольоромір відображає результат, #d619d9а не #ff00ff.
Сигуза

@Siguza Це, безумовно, не ідеально, кольори кромки корпусу можна налаштувати, регулюючи межі в петлях.
Дейв

3
Це рівняння не що інше, але "досить просте"
MultiplyByZer0

Я думаю, що рівняння вище також відсутнє clamp?
Глебм

1
Затиску там немає місця. І з того, що я пам'ятаю з моєї математики в коледжі, ці рівняння обчислюються числовими розрахунками, відомими також як "груба сила", так що удачі!
Дейв

28

Примітка: ОП попросив мене зняти , але щедрість відповість Дейву.


Я знаю, що це не те, що було задано в основному питання, і, звичайно, не те, чого ми всі чекали, але є один CSS фільтр, який робить саме це: drop-shadow()

Застереження:

  • Тінь намальована за існуючим змістом. Це означає, що ми повинні зробити кілька абсолютних хитрощів щодо позиціонування.
  • Усі пікселі будуть оброблятися однаково, але ОП сказав [ми не повинні бути] "Турбота про те, що відбувається з іншими кольорами, крім чорного".
  • Підтримка браузера. (Я не впевнений у цьому, перевірений лише під пізні FF та хром).

/* 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
Супер розумний, дивовижний! Це працює для мене, оцініть це
jaminroe

Я вважаю, що це краще рішення, оскільки він щоразу на 100% точний з кольором.
користувач835542

Показ коду відображає порожню сторінку (W10 FF 69b). З іконою нічого поганого (перевірено окремим SVG).
Рене ван дер Ленде

Додавання background-color: black;до .icon>spanробить цю роботу для FF 69b. Однак значок не відображається.
Рене ван дер Ленде

@RenevanderLende Щойно спробував на FF70 досі працює. Якщо це не працює для вас, це повинно бути щось у вашому кінці.
Каїдо

15

Ви можете зробити це все дуже просто, використовуючи фільтр SVG, на який посилається CSS. Для відновлення потрібно лише одна feColorMatrix. Цей перефарбовується в жовтий. П'ятий стовпець у feColorMatrix містить цільові значення 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">


Цікаве рішення, але, здається, воно не дозволяє контролювати цільовий колір за допомогою CSS.
glebm

Ви повинні визначити новий фільтр для кожного кольору, який потрібно застосувати. Але це повністю точно. hue-rotate - це наближення, яке вирізує певні кольори - це означає, що певні кольори не вдасться точно використовувати, як свідчать відповіді вище. Нам дійсно потрібно скорочення () CSS-фільтра.
Майкл Муллані

Відповідь MultiplyByZer0 обчислює серію фільтрів, які досягаються з дуже високою точністю, не змінюючи HTML. Вірно hue-rotateв браузерах було б добре.
glebm

2
здається, що це створює лише точні кольори RGB для зображень з чорним джерелом, коли ви додаєте "color-interpolation-filters" = "sRGB" до feColorMatrix.
Джон Сміт

Край 12-18 не залишається, оскільки вони не підтримують urlфункцію caniuse.com/#search=svg%20filter
Volker E.

2

Я помітив, що приклад лікування через SVG-фільтр був неповним, я написав моє (що прекрасно працює): (див. Відповідь Майкла Маллані), ось ось спосіб отримати будь-який колір, який ви хочете:

Ось друге рішення, використовуючи фільтр SVG лише у коді => URL.createObjectURL


1

просто використовувати

fill: #000000

fillВластивість в CSS для заповнення кольору форми SVG. fillВластивість може приймати будь-яке значення CSS кольору.


3
Це може працювати з CSS внутрішнім для SVG-зображення, але він не працює, оскільки CSS, застосований зовнішньо до imgелемента браузером.
Девід Молес

0

Я почав з цієї відповіді за допомогою фільтра svg і вніс такі зміни:

SVG-фільтр з URL-адреси даних

Якщо ви не хочете визначати фільтр SVG десь у своїй розмітці, замість цього можете використовувати URL-адресу даних (замініть R , G , B і A на потрібний колір):

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

Відстань у відтінках сірого

Якщо версія вище не працює, ви також можете додати резервну копію відтінків сірого.

Функції saturateта brightnessперетворюють будь-який колір у чорний (вам не потрібно включати, що якщо колір вже чорний), invertтоді він світліше бажаної легкості ( L ), а за бажанням ви також можете вказати непрозорість ( A ).

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

SCSS mixin

Якщо ви хочете динамічно вказати колір, ви можете використовувати наступний SCSS mixin:

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

Приклад використання:

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

Переваги:

  • Немає Javascript .
  • Без додаткових елементів HTML .
  • Якщо підтримуються фільтри CSS, але SVG-фільтр не працює, є запасний варіант сірого масштабу .
  • Якщо ви використовуєте mixin, використання є досить простим (див. Приклад вище).
  • Колір є легше читабельним та простішим у модифікації, ніж хитрість sepia (компоненти RGBA у чистому CSS, а у SCSS можна навіть використовувати HEX кольори).
  • Уникає дивної поведінкиhue-rotate .

Застереження:

  • Не всі браузери підтримують SVG-фільтри з URL-адреси даних (особливо хеш-код id), але він працює в поточних браузерах Firefox та Chromium (і, можливо, інших).
  • Якщо ви хочете динамічно вказати колір, вам доведеться використовувати SCSS mixin.
  • Чиста версія CSS трохи потворна, якщо вам потрібно багато різних кольорів, ви повинні включати SVG кілька разів.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.