如何仅使用CSS滤镜将黑色转换为任何给定的颜色 [英] How to transform black into any given color using only CSS filters

查看:94
本文介绍了如何仅使用CSS滤镜将黑色转换为任何给定的颜色的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的问题是:给定目标RGB颜色,仅使用#000)重新着色为该颜色的公式是什么/CSS/filter"rel =" noreferrer> CSS过滤器?

My question is: given a target RGB color, what is the formula to recolor black (#000) into that color using only CSS filters?

要接受一个答案,它需要提供一个函数(使用任何语言),该函数接受目标颜色作为参数并返回相应的CSS filter字符串.

For an answer to be accepted, it would need to provide a function (in any language) that would accept the target color as an argument and return the corresponding CSS filter string.

此上下文是需要在background-image中重新着色SVG的颜色.在这种情况下,它将支持KaTeX中的某些TeX数学功能: https://github.com/Khan/KaTeX/issues/587 .

The context for this is the need to recolor an SVG inside a background-image. In this case, it is to support certain TeX math features in KaTeX: https://github.com/Khan/KaTeX/issues/587.

如果目标颜色为#ffff00(黄色),则一种正确的解决方案是:

If the target color is #ffff00 (yellow), one correct solution is:

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

(演示)

  • 动画.
  • 非CSS过滤器解决方案.
  • 从黑色以外的其他颜色开始.
  • 关心黑色以外的其他颜色会发生什么.
  • Brute-force search for parameters of a fixed filter list: https://stackoverflow.com/a/43959856/181228
    Cons: inefficient, only generates some of the 16,777,216 possible colors (676,248 with hueRotateStep=1).

使用 SPSA 的更快的搜索解决方案: https://stackoverflow.com/a/43960991/181228 获得赏金

A faster search solution using SPSA: https://stackoverflow.com/a/43960991/181228 Bounty awarded

一个drop-shadow解决方案: https://stackoverflow.com/a/43959853/181228
缺点:在Edge上不起作用.需要非filter CSS更改和较小的HTML更改.

A drop-shadow solution: https://stackoverflow.com/a/43959853/181228
Cons: Does not work on Edge. Requires non-filter CSS changes and minor HTML changes.

您仍然可以通过提交非蛮力解决方案来获得已接受的答案!

You can still get an Accepted answer by submitting a non brute-force solution!

  • How hue-rotate and sepia are calculated: https://stackoverflow.com/a/29521147/181228 Example Ruby implementation:

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函数是非线性的.

Note that the clamp above makes the hue-rotate function non-linear.

浏览器实现: Chromium Firefox .

演示:从灰度颜色变为非灰度颜色: https://stackoverflow.com/a/25524145/181228

Demo: Getting to a non-grayscale color from a grayscale color: https://stackoverflow.com/a/25524145/181228

几乎有效的公式(来自类似问题):
https://stackoverflow.com/a/29958459/181228

A formula that almost works (from a similar question):
https://stackoverflow.com/a/29958459/181228

为什么上面的公式是错误的(CSS hue-rotate不是真正的色相旋转而是线性近似)的详细说明:
https://stackoverflow.com/a/19325417/2441511

A detailed explanation of why the formula above is wrong (CSS hue-rotate is not a true hue rotation but a linear approximation):
https://stackoverflow.com/a/19325417/2441511

推荐答案

@Dave第一个发布答案对此(使用有效代码),他的回答对我而言是无耻复制和粘贴灵感的宝贵来源.这篇文章开始是为了解释和完善@Dave的 答案,但此后演变成它自己的答案.

@Dave was the first to post an answer to this (with working code), and his answer has been an invaluable source of shameless copy and pasting inspiration to me. This post began as an attempt to explain and refine @Dave's answer, but it has since evolved into an answer of its own.

我的方法明显更快.根据关于随机生成的RGB颜色的 jsPerf基准,@ Dave的算法在 600毫秒内运行,而我的运行时间为 30毫秒.这绝对很重要,例如在加载时间中,速度至关重要.

My method is significantly faster. According to a jsPerf benchmark on randomly generated RGB colors, @Dave's algorithm runs in 600 ms, while mine runs in 30 ms. This can definitely matter, for instance in load time, where speed is critical.

此外,对于某些颜色,我的算法性能更好:

Furthermore, for some colors, my algorithm performs better:

  • 对于rgb(0,255,0),@ Dave产生rgb(29,218,34)并产生rgb(1,255,0)
  • 对于rgb(0,0,255),@ Dave产生rgb(37,39,255),而我的产生rgb(5,6,255)
  • 对于rgb(19,11,118),@ Dave产生rgb(36,27,102),而我的产生rgb(20,11,112)
  • For rgb(0,255,0), @Dave's produces rgb(29,218,34) and produces rgb(1,255,0)
  • For rgb(0,0,255), @Dave's produces rgb(37,39,255) and mine produces rgb(5,6,255)
  • For rgb(19,11,118), @Dave's produces rgb(36,27,102) and mine produces 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.


Explanation

We'll begin by writing some Javascript.

"use strict";

class Color {
    constructor(r, g, b) {
        this.r = this.clamp(r);
        this.g = this.clamp(g);
        this.b = this.clamp(b);
    } toString() { return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; }

    hsl() { // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
        let r = this.r / 255;
        let g = this.g / 255;
        let b = this.b / 255;
        let max = Math.max(r, g, b);
        let min = Math.min(r, g, b);
        let h, s, l = (max + min) / 2;

        if(max === min) {
            h = s = 0;
        } else {
            let d = max - min;
            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
            switch(max) {
                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                case g: h = (b - r) / d + 2; break;
                case b: h = (r - g) / d + 4; break;
            } h /= 6;
        }

        return {
            h: h * 100,
            s: s * 100,
            l: l * 100
        };
    }

    clamp(value) {
        if(value > 255) { value = 255; }
        else if(value < 0) { value = 0; }
        return value;
    }
}

class Solver {
    constructor(target) {
        this.target = target;
        this.targetHSL = target.hsl();
    }

    css(filters) {
        function fmt(idx, multiplier = 1) { return Math.round(filters[idx] * multiplier); }
        return `filter: invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%);`;
    }
}

说明:

  • Color类代表RGB颜色.
    • toString()函数返回CSS rgb(...)颜色字符串中的颜色.
    • hsl()函数返回颜色,并转换为 HSL .
    • clamp()函数可确保给定的颜色值在范围内(0-255).
    • The Color class represents a RGB color.
      • Its toString() function returns the color in a CSS rgb(...) color string.
      • Its hsl() function returns the color, converted to HSL.
      • Its clamp() function ensures that a given color value is within bounds (0-255).
      • css()函数在CSS过滤器字符串中返回给定的过滤器.
      • Its css() function returns a given filter in a CSS filter string.

      CSS/SVG过滤器的核心是过滤器原语,它们表示图片的低级修改.

      The heart of CSS/SVG filters are filter primitives, which represent low-level modifications to an image.

      过滤器 grayscale() <feColorMatrix> ,它在指定的矩阵之间执行矩阵乘法由滤镜(通常是动态生成),以及根据颜色创建的矩阵.图:

      The filters grayscale(), sepia(), and saturate() are implemented by the filter primative <feColorMatrix>, which performs matrix multiplication between a matrix specified by the filter (often dynamically generated), and a matrix created from the color. Diagram:

      我们可以在此处进行一些优化:

      There are some optimizations we can make here:

      • 颜色矩阵的最后一个元素是并且将始终是1.没有必要计算或存储它.
      • 由于我们处理的是RGB而不是RGBA,因此也没有必要计算或存储alpha/transparency值(A).
      • 因此,我们可以将滤镜矩阵从5x5修剪到3x5,将颜色矩阵从1x5修剪到1x3 .这样可以节省一些工作.
      • 所有<feColorMatrix>筛选器将第4列和第5列保留为零. 因此,我们可以将滤镜矩阵进一步缩小为3x3 .
      • 由于乘法相对简单,因此无需拖入复杂的数学库为此.我们可以自己实现矩阵乘法算法.
      • The last element of the color matrix is and will always be 1. There is no point of calculating or storing it.
      • There is no point of calculating or storing the alpha/transparency value (A) either, since we are dealing with RGB, not RGBA.
      • Therefore, we can trim the filter matrices from 5x5 to 3x5, and the color matrix from 1x5 to 1x3. This saves a bit of work.
      • All <feColorMatrix> filters leave columns 4 and 5 as zeroes. Therefore, we can further reduce the filter matrix to 3x3.
      • Since the multiplication is relatively simple, there is no need to drag in complex math libraries for this. We can implement the matrix multiplication algorithm ourselves.

      实施:

      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等影响后续计算.)

      (We use temporary variables to hold the results of each row multiplication, because we do not want changes to this.r, etc. affecting subsequent calculations.)

      现在我们已经实现了<feColorMatrix>,我们可以实现grayscale()sepia()saturate(),它们只需使用给定的过滤器矩阵来调用它即可:

      Now that we have implemented <feColorMatrix>, we can implement grayscale(), sepia(), and saturate(), which simply invoke it with a given filter matrix:

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

      Implementing hue-rotate()

      The hue-rotate() filter is implemented by <feColorMatrix type="hueRotate" />.

      滤波器矩阵的计算如下:

      The filter matrix is calculated as shown below:

      例如,元素 a 00 的计算方式如下:

      For instance, element a00 would be calculated like so:

      一些注意事项:

      • 旋转角度以度为单位.在传递给Math.sin()Math.cos()之前,必须将其转换为弧度.
      • Math.sin(angle)Math.cos(angle)应该先计算一次,然后缓存.
      • The angle of rotation is given in degrees. It must be converted to radians before passed to Math.sin() or Math.cos().
      • Math.sin(angle) and Math.cos(angle) should be computed once and then cached.

      实施:

      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() .

      Implementing brightness() and contrast()

      The brightness() and contrast() filters are implemented by <feComponentTransfer> with <feFuncX type="linear" />.

      每个<feFuncX type="linear" />元素都接受 slope intercept 属性.然后,它通过一个简单的公式计算每个新的颜色值:

      Each <feFuncX type="linear" /> element accepts a slope and intercept attribute. It then calculates each new color value through a simple formula:

      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()也可以实现:

      Once this is implemented, brightness() and contrast() can be implemented as well:

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

      实施invert()

      invert() 过滤器由 .

      Implementing invert()

      The invert() filter is implemented by <feComponentTransfer> with <feFuncX type="table" />.

      规范指出:

      以下, C 是初始组件, C'是重新映射的组件;都在关闭时间间隔[0,1]中.

      In the following, C is the initial component and C' is the remapped component; both in the closed interval [0,1].

      对于表格",该函数是通过在属性 tableValues 中给定的值之间进行线性插值来定义的.该表具有 n + 1个值(即v 0 至v n ),这些值指定 n 均匀大小的插值区域.插值使用以下公式:

      For "table", the function is defined by linear interpolation between values given in the attribute tableValues. The table has n + 1 values (i.e., v0 to vn) specifying the start and end values for n evenly sized interpolation regions. Interpolations use the following formula:

      对于值 C ,找到 k 这样:

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

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

      结果 C'由下式给出:

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

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

      对此公式的解释:

      • invert()过滤器定义此表:[value,1-value].这是 tableValues v .
      • 该公式定义了 n ,因此 n +1是表的长度.由于表格的长度为2,所以 n = 1.
      • 该公式定义了 k ,其中 k k + 1是表的索引.由于该表包含2个元素,因此 k = 0.
      • The invert() filter defines this table: [value, 1 - value]. This is tableValues or v.
      • The formula defines n, such that n + 1 is the table's length. Since the table's length is 2, n = 1.
      • The formula defines k, with k and k + 1 being indexes of the table. Since the table has 2 elements, k = 0.

      因此,我们可以将公式简化为:

      Thus, we can simplify the formula to:

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

      C' = v0 + C * (v1 - v0)

      内联表的值,剩下的是:

      Inlining the table's values, we are left with:

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

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

      进一步简化:

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

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

      该规范将 C C'定义为RGB值,范围为0-1(相对于0-255).因此,我们必须在计算前按比例缩小这些值,然后在计算后按比例放大这些.

      The spec defines C and C' to be RGB values, within the bounds 0-1 (as opposed to 0-255). As a result, we must scale down the values before computation, and scale them back up after.

      因此,我们开始实施:

      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 个过滤器组合,包括:

      Interlude: @Dave's brute-force algorithm

      @Dave's code generates 176,660 filter combinations, including:

      • 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度)
      • 11 invert() filters (0%, 10%, 20%, ..., 100%)
      • 11 sepia() filters (0%, 10%, 20%, ..., 100%)
      • 20 saturate() filters (5%, 10%, 15%, ..., 100%)
      • 73 hue-rotate() filters (0deg, 5deg, 10deg, ..., 360deg)

      它按以下顺序计算过滤器:

      It calculates filters in the following order:

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

      然后迭代所有计算出的颜色.一旦发现生成的颜色在公差范围内(所有RGB值均距目标颜色5个单位以内),它将停止.

      It then iterates through all computed colors. It stops once it has found a generated color within tolerance (all RGB values are within 5 units from the target color).

      但是,这是缓慢且效率低下的.因此,我提出了自己的答案.

      However, this is slow and inefficient. Thus, I present my own answer.

      首先,我们必须定义一个损失函数,该函数返回由颜色产生的颜色之间的差异滤镜组合和目标颜色.如果滤波器是完美的,则损失函数应返回0.

      First, we must define a loss function, that returns the difference between the color produced by a filter combination, and the target color. If the filters are perfect, the loss function should return 0.

      我们将色差作为两个指标的总和:

      We will measure color difference as the sum of two metrics:

      • RGB差异,因为目标是产生最接近的RGB值.
      • HSL差异,因为许多HSL值对应于过滤器(例如,色调与hue-rotate()大致相关,饱和度与saturate()相关,等等).这指导了算法.
      • RGB difference, because the goal is to produce the closest RGB value.
      • HSL difference, because many HSL values correspond to filters (e.g. hue roughly correlates with hue-rotate(), saturation correlates with saturate(), etc.) This guides the algorithm.

      损失函数将采用一个参数–一组过滤百分比.

      The loss function will take one argument – an array of filter percentages.

      我们将使用以下过滤器顺序:

      We will use the following filter order:

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

      实施:

      function loss(filters) {
          let color = new Color(0, 0, 0);
          color.invert(filters[0] / 100);
          color.sepia(filters[1] / 100);
          color.saturate(filters[2] / 100);
          color.hueRotate(filters[3] * 3.6);
          color.brightness(filters[4] / 100);
          color.contrast(filters[5] / 100);
      
          let colorHSL = color.hsl();
          return Math.abs(color.r - this.target.r)
              + Math.abs(color.g - this.target.g)
              + Math.abs(color.b - this.target.b)
              + Math.abs(colorHSL.h - this.targetHSL.h)
              + Math.abs(colorHSL.s - this.targetHSL.s)
              + Math.abs(colorHSL.l - this.targetHSL.l);
      }
      

      我们将尝试最小化损失函数,例如:

      We will try to minimize the loss function, such that:

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

      SPSA 算法(更多信息纸张参考代码)非常擅长此操作.它旨在优化具有局部最小值,噪声/非线性/多元损失函数等的复杂系统.已用于调整国际象棋引擎.而且与许多其他算法不同,描述它的论文实际上是可以理解的(尽管付出了很大的努力).

      The SPSA algorithm (website, more info, paper, implementation paper, reference code) is very good at this. It was designed to optimize complex systems with local minima, noisy/nonlinear/ multivariate loss functions, etc. It has been used to tune chess engines. And unlike many other algorithms, the papers describing it are actually comprehensible (albeit with great effort).

      实施:

      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进行了一些修改/优化:

      I made some modifications/optimizations to SPSA:

      • 使用产生的最佳结果,而不是最后的结果.
      • 重新使用所有数组(deltashighArgslowArgs),而不是每次迭代都重新创建它们.
      • 使用 a 的值数组,而不是单个值.这是因为所有过滤器都不相同,因此它们应该以不同的速度移动/收敛.
      • 在每次迭代后运行fix函数.除了saturate(最大值为7500%),brightnesscontrast(最大值为200%)和hueRotate(其中值为包裹而不是夹紧).
      • Using the best result produced, instead of the last.
      • Reusing all arrays (deltas, highArgs, lowArgs), instead of recreating them with each iteration.
      • Using an array of values for a, instead of a single value. This is because all of the filters are different, and thus they should move/converge at different speeds.
      • Running a fix function after each iteration. It clamps all values to between 0% and 100%, except saturate (where the maximum is 7500%), brightness and contrast (where the maximum is 200%), and hueRotate (where the values are wrapped around instead of clamped).

      我在两个阶段使用SPSA:

      I use SPSA in a two-stage process:

      1. 宽"阶段,试图探索"搜索空间.如果结果不令人满意,它将重试SPSA.
      2. 窄"阶段,它从宽阶段获得最佳结果,并试图对其进行优化".它为 A a 使用动态值.
      1. The "wide" stage, that tries to "explore" the search space. It will make limited retries of SPSA if the results are not satisfactory.
      2. The "narrow" stage, that takes the best result from the wide stage and attempts to "refine" it. It uses dynamic values for A and 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代码,尤其是其常量,除非您确定您知道自己在做什么.

      Tuning SPSA

      Warning: Do not mess with the SPSA code, especially with its constants, unless you are sure you know what you are doing.

      重要常量为 A a c ,初始值,重试阈值和fix(),以及每个阶段的迭代次数.所有这些值都经过仔细调整以产生良好的结果,将它们随机拧紧几乎肯定会降低该算法的实用性.

      The important constants are A, a, c, the initial values, the retry thresholds, the values of max in fix(), and the number of iterations of each stage. All of these values were carefully tuned to produce good results, and randomly screwing with them will almost definitely reduce the usefulness of the algorithm.

      如果您坚持要更改它,则必须在优化"之前进行测量.

      If you insist on altering it, you must measure before you "optimize".

      首先,应用此补丁.

      然后在Node.js中运行代码.一段时间后,结果应该是这样的:

      Then run the code in Node.js. After quite some time, the result should be something like this:

      Average loss: 3.4768521401985275
      Average time: 11.4915ms
      

      现在将常量调整为您内心的满足感.

      Now tune the constants to your heart's content.

      一些提示:

      • 平均损失应在4左右.如果大于4,则其结果相差太远,您应该调整其准确性.如果小于4,则浪费时间,应减少迭代次数.
      • 如果增加/减少迭代次数,请适当调整 A .
      • 如果您增加/减少 A ,请适当调整 a .
      • 如果要查看每次迭代的结果,请使用--debug标志.
      • The average loss should be around 4. If it is greater than 4, it is producing results that are too far off, and you should tune for accuracy. If it is less than 4, it is wasting time, and you should reduce the number of iterations.
      • If you increase/decrease the number of iterations, adjust A appropriately.
      • If you increase/decrease A, adjust a appropriately.
      • Use the --debug flag if you want to see the result of each iteration.

      这篇关于如何仅使用CSS滤镜将黑色转换为任何给定的颜色的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆