Как преобразовать черный в любой заданный цвет, используя только фильтры CSS


117

Мой вопрос: учитывая целевой цвет RGB, по какой формуле перекрашивать черный ( #000) в этот цвет, используя только фильтры CSS ?

Чтобы ответ был принят, необходимо предоставить функцию (на любом языке), которая будет принимать целевой цвет в качестве аргумента и возвращать соответствующую filterстроку CSS .

Контекст для этого - необходимость перекрашивать SVG внутри файла background-image. В этом случае он предназначен для поддержки определенных математических функций TeX в KaTeX: https://github.com/Khan/KaTeX/issues/587 .

пример

Если целевой цвет #ffff00(желтый), одно правильное решение:

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

( демо )

Non-цели

  • Анимация.
  • Решения без CSS-фильтров.
  • Начиная с цвета, отличного от черного.
  • Забота о том, что происходит с другими цветами, кроме черного.

Результаты на данный момент

  • Поиск параметров фиксированного списка фильтров методом перебора: https://stackoverflow.com/a/43959856/181228
    Минусы: неэффективно, генерирует только некоторые из 16 777 216 возможных цветов (676 248 с hueRotateStep=1).

  • Более быстрое решение для поиска с использованием SPSA : https://stackoverflow.com/a/43960991/181228 Награда за вознаграждение

  • drop-shadowРешение: https://stackoverflow.com/a/43959853/181228
    Минусы: не работает на Крае. Требуются filterизменения, не связанные с CSS, и незначительные изменения HTML.

Вы по-прежнему можете получить ответ " Принято" , отправив решение без перебора!

Ресурсы

  • Как 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
Может быть, вам подойдет режим наложения? Вы можете легко преобразовать черный в любой цвет ... Но я не понимаю, чего вы хотите достичь
vals

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

2
@ProllyGeek Да. Еще одно ограничение, которое я должен упомянуть, заключается в том, что полученная формула не может быть прямым поиском таблицы 5 ГБ (ее можно использовать, например, из javascript на веб-странице).
glebm

Ответы:


153

@Dave был первым, кто опубликовал ответ на этот вопрос (с рабочим кодом), и его ответ стал для меня бесценным источником бессовестной копии и вдохновения. Этот пост начинался как попытка объяснить и уточнить ответ @Dave, но с тех пор превратился в собственный ответ.

Мой метод значительно быстрее. Согласно тесту jsPerf на случайно сгенерированных цветах RGB, алгоритм @Dave работает за 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" />.

Матрица фильтра рассчитывается, как показано ниже:

Например, элемент a 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].

Для «таблицы» функция определяется линейной интерполяцией между значениями, указанными в атрибуте tableValues . В таблице есть n + 1 значений (т. Е. От v 0 до v n ), определяющих начальное и конечное значения для n областей интерполяции одинакового размера. Для интерполяции используется следующая формула:

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

к / п ≤ С <(к + 1) / п

Результат 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);
}

Интерлюдия: алгоритм грубой силы @Dave

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

  • 11 invert() фильтров (0%, 10%, 20%, ..., 100%)
  • 11 sepia() фильтров (0%, 10%, 20%, ..., 100%)
  • 20 saturate() фильтров (5%, 10%, 15%, ..., 100%)
  • 73 hue-rotate() фильтра (0 градусов, 5 градусов, 10 градусов, ..., 360 градусов)

Он вычисляет фильтры в следующем порядке:

filter: invert(a%) sepia(b%) saturate(c%) hue-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), вместо того , чтобы воссоздать их с каждой итерации.
  • Использование массива значений для a вместо одного значения. Это потому, что все фильтры разные, и поэтому они должны двигаться / сходиться с разной скоростью.
  • Запуск fixфункции после каждой итерации. Он фиксирует все значения в saturateдиапазоне от 0% до 100%, кроме (где максимальное значение составляет 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
@ Дэйв На самом деле, я работал над этим независимо, но ты меня опередил.
MultiplyByZer0

4
Отличный ответ! Реализация в этом коде
KyleMit 03

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>

РЕДАКТИРОВАТЬ: это решение не предназначено для производственного использования и только иллюстрирует подход, который может быть использован для достижения того, о чем просит OP. Как есть, он слабый в некоторых областях цветового спектра. Лучшие результаты могут быть достигнуты за счет большей детализации в итерациях шага или путем реализации большего количества функций фильтрации по причинам, подробно описанным в ответе @ MultiplyByZer0 .

EDIT2: OP ищет решение без грубой силы. В этом случае это довольно просто, просто решите это уравнение:

Уравнения матрицы фильтров CSS

где

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

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

@Siguza Это определенно не идеально, цвета краев корпуса можно настроить, отрегулировав границы в циклах.
Дэйв

3
Это уравнение совсем не "довольно простое"
MultiplyByZer0

Я думаю, что приведенное выше уравнение также отсутствует clamp?
glebm

1
Зажиму там нет места. И из того, что я помню по математике в колледже, эти уравнения вычисляются путем численных расчетов, так называемых «грубой силы», так что удачи!
Дэйв

28

Примечание: OP попросил меня отменить удаление , но награда достанется ответу Дейва.


Я знаю, что это не то, что было задано в теле вопроса, и, конечно, не то, чего мы все ждали, но есть один фильтр CSS, который делает именно это: drop-shadow()

Предостережения:

  • Тень рисуется за существующим содержимым. Это означает, что мы должны использовать некоторые уловки абсолютного позиционирования.
  • Все пиксели будут обрабатываться одинаково, но OP сказал [мы не должны] «заботиться о том, что происходит с другими цветами, кроме черного».
  • Поддержка браузера. (Не уверен, тестировал только под последними FF и chrome).

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

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


/* the content */

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

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

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


1
Супер умно, круто! Это работает для меня, цените это
jaminroe

Я считаю, что это лучшее решение, поскольку оно всегда на 100% точно передает цвет.
user835542

Код как есть показывает пустую страницу (W10 FF 69b). Хотя с иконкой все в порядке (проверено отдельно SVG).
Рене ван дер Ленде

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

@RenevanderLende Только что попробовал, FF70 все еще там работает. Если это не работает для вас, это должно быть что-то на вашей стороне.
Kaiido

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 03

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

Ответ MultiplyByZer0 вычисляет серию фильтров, которые достигаются с очень высокой точностью без изменения HTML. Правда hue-rotateв браузерах было бы неплохо, да.
glebm 08

2
похоже, что это дает точные цвета RGB только для черных исходных изображений, когда вы добавляете "color-interpolation-filters" = "sRGB" в feColorMatrix.
Джон Смит

Edge 12-18 исключены, так как они не поддерживают urlфункцию caniuse.com/#search=svg%20filter
Volker E.

2

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

Вот второе решение, используя SVG-фильтр только в code => URL.createObjectURL


1

просто используйте

fill: #000000

fillСвойство в CSS для заполнения цвета формы SVG. fillСвойство может принимать любое значение CSS цвета.


3
Это может работать с внутренним CSS для изображения SVG, но не работает, поскольку CSS применяется imgбраузером к элементу извне .
Дэвид Молес

1

Я начал с этого ответа, используя фильтр 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

Если вы хотите указать цвет динамически, вы можете использовать следующий миксин SCSS:

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

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

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

Пример использования:

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

Преимущества:

  • Нет Javascript .
  • Никаких дополнительных HTML-элементов .
  • Если фильтры CSS поддерживаются, но фильтр SVG не работает, имеется откат оттенках серого .
  • Если вы используете миксин, использование довольно простое (см. Пример выше).
  • Цвет более читабельный и его легче изменить, чем трюк с сепией (компоненты RGBA в чистом CSS, и вы даже можете использовать цвета HEX в SCSS).
  • Избегает странного поведенияhue-rotate .

Предостережения:

  • Не все браузеры поддерживают фильтры SVG из URL-адреса данных (особенно хэша идентификатора), но он работает в текущих браузерах Firefox и Chromium. (и, возможно, в других).
  • Если вы хотите указать цвет динамически, вы должны использовать миксин SCSS.
  • Версия на чистом CSS немного уродлива, если вам нужно много разных цветов, вам придется включать SVG несколько раз.

1
о, это прекрасно, это именно то, что я искал, - использовать все в SASS, здорово, спасибо!
ghiscoding

1
@ghiscoding Рад, что помог!
Дэвид Досталь,
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.