source

CSS 필터만 사용하여 검은색을 임의의 색상으로 변환하는 방법

ittop 2023. 8. 15. 11:46
반응형

CSS 필터만 사용하여 검은색을 임의의 색상으로 변환하는 방법

색상이 검은색을 요? (RGB: 대상기공무검까엇식니입은제는억하을은색면상색주어지이질문은?▁my▁to▁(까무)#000) CSS 필터만 사용하여 그 색으로?

답변이 받아들여지려면 대상 색상을 인수로 받아들이고 해당 CSS를 반환하는 함수(모든 언어로)를 제공해야 합니다.filterstring

은 에대컨텍내부는를 안에 있는 가 있다는 입니다.background-image이 경우 KaTeX: https://github.com/Khan/KaTeX/issues/587 의 특정 TeX 수학 기능을 지원하기 위한 것입니다.

이 상대색 다과같 경은우 #ffff00한은 다음과 같습니다

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

(계속)

비골

  • 애니매이션.
  • 비 CSS 필터 솔루션.
  • 검은색이 아닌 다른 색상부터 시작합니다.
  • 검은색이 아닌 다른 색상에 대해 신경 쓰는 것.

지금까지의 결과

  • 고정 필터 목록의 매개 변수에 대한 브루트 포스 검색: https://stackoverflow.com/a/43959856/181228
    색상 중 ,248개의 색상과 함께: 단점이적 16,777,216개의 색상).hueRotateStep=1).

  • SPSA를 사용한 더 빠른 검색 솔루션: https://stackoverflow.com/a/43960991/181228 바운티 수상

  • A drop-shadow해결책: https://stackoverflow.com/a/43959853/181228
    단점: Edge에서는 작동하지 않습니다.non-based 합니다.filterCSS 변경 및 사소한 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는 위는을 ▁the다를 만듭니다.hue-rotate비선형 함수

    브라우저 구현:크롬, 파이어폭스

  • 데모: 그레이스케일 색상에서 그레이스케일이 아닌 색상으로 전환하기: https://stackoverflow.com/a/25524145/181228

  • 거의 효과가 있는 공식(유사한 질문에서):
    https://.com/a/29958459/181228https ://stackoverflow.com/a/29958459/181228

    (CSS 위공잘이대설명자세한한(CSS))hue-rotate실제 색상 회전이 아니라 선형 근사치임):
    https://.com/a/19325417/2441511https ://stackoverflow.com/a/19325417/2441511

@데이브는 (작업 코드로) 이에 대한 답변을 가장 먼저 올렸고, 그의 답변은 저에게 뻔뻔한 복사와 붙여넣기 영감의 귀중한 원천이었습니다.이 게시물은 @Dave의 답변을 설명하고 개선하기 위한 시도로 시작되었지만, 그 이후로 독자적인 답변으로 발전했습니다.

제 방법이 훨씬 더 빠릅니다.무작위로 생성된 RGB 색상에 대한 jsPerf 벤치마크에 따르면, @Dave의 알고리즘은 600ms로 실행되는 반면, 내 알고리즘은 30ms로 실행됩니다.이는 예를 들어 속도가 중요한 로드 시간에서 분명히 중요할 수 있습니다.

또한 일부 색상의 경우 알고리즘 성능이 더 우수합니다.

  • 위해서rgb(0,255,0) products @이브제품데품.rgb(29,218,34)는 그고나생들물산의리들▁produces물▁and를 생산합니다.rgb(1,255,0)
  • 위해서rgb(0,0,255) products @이브제품데품.rgb(37,39,255)는 그고나생들물산의리들▁produces물▁and를 생산합니다.rgb(5,6,255)
  • 위해서rgb(19,11,118) products @이브제품데품.rgb(36,27,102)는 그고나생들물산의리들▁produces물▁and를 생산합니다.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.filter;

설명.

자바스크립트로 시작하겠습니다.

"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 의색을 CSS 합니다.rgb(...) 스트링 줄색
    • 그것의.hsl()함수는 HSL로 변환된 색상을 반환합니다.
    • 그것의.clamp()함수는 지정된 색상 값이 범위(0-255) 내에 있음을 보장합니다.
  • Solver클래스가 대상 색상에 대해 해결을 시도합니다.
    • 그것의.css()function은 CSS 필터 문자열에서 지정된 필터를 반환합니다.

»grayscale(),sepia(),그리고.saturate()

CSS/SVG 필터의 핵심은 이미지에 대한 낮은 수준의 수정을 나타내는 필터 프리미티브입니다.

필터, , 및 필터는 필터에 의해 지정된 행렬(종종 동적으로 생성됨)과 색상에서 생성된 행렬 사이에서 행렬 곱셈을 수행하는 필터 프리미티브에 의해 구현됩니다.다이어그램:

Matrix multiplication

여기서 몇 가지 최적화할 수 있습니다.

  • 색행의마요소다음같과으앞그것다입럴니도로으며는지렬막▁and▁will▁of▁always▁the▁be다니▁is입것▁matrix▁last▁color▁the▁element그색럴도로입니다.1그것을 계산하거나 저장하는 것은 의미가 없습니다.
  • 값을 .ARGBA가 아닌 RGB를 다루고 있기 때문에 둘 중 하나입니다.
  • 따라서 필터 행렬을 5x5에서 3x5로, 행렬을 1x5에서 1x3으로 자를있습니다.이것은 일을 조금 덜어줍니다.
  • 든모.<feColorMatrix>필터는 4열과 5열을 0으로 둡니다.따라서 필터 매트릭스를 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()

필터는 에 의해 구현됩니다.

필터 매트릭스는 다음과 같이 계산됩니다.

예를 들어 요소00 a는 다음과 같이 계산됩니다.

주의:

  • 회전 각도는 도 단위로 제공되며, 이는 다음으로 전달되기 전에 라디안으로 변환되어야 합니다.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()

및 필터는 를 사용하여 구현됩니다.

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

필터는 를 사용하여 구현됩니다.

사양은 다음과 같습니다.

다음에서 C는 초기 성분이고 C'는 재매핑된 성분입니다. 둘 다 닫힌 구간 [0,1]입니다.

"table"의 경우 함수는 tableValues 속성에 지정된 값 사이의 선형 보간으로 정의됩니다.표에는 n개의 균일한 크기의 보간 영역에 대한 시작 및 종료 값을 지정하는 n + 1 값(예0: v ~ vn)이 있습니다.보간은 다음 공식을 사용합니다.

C 값에 대해 다음과 같이 구합니다.

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

결과 C'는 다음과 같습니다.

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

이 공식에 대한 설명:

  • invert()필터는 [value, 1 - value] 테이블을 정의합니다.테이블또는 v입니다.
  • 공식은 n + 1이 테이블의 길이가 되도록 n을 정의합니다.표의 길이가 2이므로 n = 1입니다.
  • 공식은 k를 정의하며, k와 k + 1은 테이블의 인덱스입니다.표에는 두 개의 요소가 있으므로 k = 0입니다.

따라서 공식을 다음과 같이 단순화할 수 있습니다.

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

표의 값을 요약하면 다음과 같습니다.

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

한 가지 더 단순화:

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

규격에서는 C와 C'를 0-1 범위 내(0-255와 반대) RGB 값으로 정의합니다.결과적으로, 우리는 계산 전에 값을 축소하고 이후에 다시 확장해야 합니다.

이렇게 해서 우리는 구현에 도달한 것은 다음과 같습니다.

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()필터(0ppm, 5ppm, 10ppm, ..., 360ppm)

필터는 다음 순서로 계산됩니다.

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

그런 다음 모든 계산된 색상을 반복합니다.허용 오차 내에서 생성된 색상을 발견하면 중지됩니다(모든 RGB 값이 대상 색상에서 5단위 이내).

하지만, 이것은 느리고 비효율적입니다.그래서 저는 저만의 답을 제시합니다.

SPSA 구현

먼저 필터 조합에 의해 생성된 색상과 대상 색상 간의 차이를 반환하는 손실 함수를 정의해야 합니다.필터가 완벽하면 손실 함수는 0을 반환해야 합니다.

색상 차이는 두 메트릭의 합으로 측정합니다.

  • 가장 가까운 RGB 값을 생성하는 것이 목표이기 때문에 RGB 차이입니다.
  • 에 HSL 으로 많값 HSL 이차예이 HSL 은에로문으때기적대하략당에)hue-rotate()는 포도와상관있음가계관화ates▁with와 상관관계가 있습니다.saturate()등) 알고리즘을 안내합니다.

손실 함수에는 필터 백분율 배열이라는 하나의 인수가 사용됩니다.

다음 필터 순서를 사용합니다.

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

구현:

function loss(filters) {
    let color = new Color(0, 0, 0);
    color.invert(filters[0] / 100);
    color.sepia(filters[1] / 100);
    color.saturate(filters[2] / 100);
    color.hueRotate(filters[3] * 3.6);
    color.brightness(filters[4] / 100);
    color.contrast(filters[5] / 100);

    let colorHSL = color.hsl();
    return Math.abs(color.r - this.target.r)
        + Math.abs(color.g - this.target.g)
        + Math.abs(color.b - this.target.b)
        + Math.abs(colorHSL.h - this.targetHSL.h)
        + Math.abs(colorHSL.s - this.targetHSL.s)
        + Math.abs(colorHSL.l - this.targetHSL.l);
}

다음과 같이 손실 함수를 최소화하도록 노력할 것입니다.

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

SPSA 알고리즘(웹사이트, 더 많은 정보, 종이, 구현 문서, 참조 코드)은 이를 매우 잘 수행합니다.로컬 최소값, 노이즈/비선형/다변수 손실 함수 등으로 복잡한 시스템을 최적화하도록 설계되었습니다.그것은 체스 엔진을 조정하는 데 사용되어 왔습니다.그리고 다른 많은 알고리즘과 달리, 그것을 설명하는 논문은 실제로 이해할 수 있습니다(많은 노력을 기울였지만).

구현:

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를 몇 가지 수정/최적화했습니다.

  • 최종 결과 대신 최고의 결과를 사용합니다.
  • 배열 (모든레어재용사이(용▁re)deltas,highArgs,lowArgs각 반복을 사용하여 재생성하는 대신, ).
  • 단일 값 대신 값 배열을 사용합니다.이는 모든 필터가 서로 다르므로 필터가 서로 다른 속도로 이동/융합해야 하기 때문입니다.
  • fix각 반복 후의 함수입니다. 모값 든을 0%합 100%사를 제외한 을 0100.saturate인 경우), (7500%인 경우),brightness그리고.contrast은 200 및 (최대 200%),hueRotate(클램프가 아닌 랩핑된 값).

2단계 프로세스에서 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 코드, 특히 SPSA 코드의 상수를 사용하여 작업을 수행하지 마십시오.

중요한 상수는 A, a, c, 초기 값, 재시도 임계값, 다음 값입니다.maxfix()각 단계의 반복 횟수.이 모든 값은 좋은 결과를 내기 위해 신중하게 조정되었으며, 무작위로 나사를 조이면 알고리즘의 유용성이 거의 확실히 감소할 것입니다.

변경해야 할 경우 "최적화"하기 전에 측정해야 합니다.

먼저 이 패치를 적용합니다.

그런 다음 Node.js에서 코드를 실행합니다.시간이 꽤 지난 후에는 다음과 같은 결과가 나올 것입니다.

Average loss: 3.4768521401985275
Average time: 11.4915ms

이제 상수를 마음껏 조정합니다.

몇 가지 팁:

  • 평균 손실은 약 4입니다.값이 4보다 크면 너무 멀리 떨어져 있는 결과가 생성되므로 정확도를 조정해야 합니다.4보다 작으면 시간이 낭비되므로 반복 횟수를 줄여야 합니다.
  • 반복 횟수를 늘리거나 줄이면 A를 적절히 조정합니다.
  • A를 늘리거나 줄이면 적절히 조정합니다.
  • 을 합니다.--debug각 반복의 결과를 보려면 플래그를 누르십시오.

TL;DR

이것은 토끼굴을 따라 꽤 먼 여행이었지만 여기 있습니다!

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 Filter Matrix Equations

어디에

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

참고: OP가 삭제를 해제하라고 했지만, 현상금은 데이브의 대답으로 돌아갈 입니다.


질문의 본문에서 질문받은 내용이 아니라는 것을 알고 있으며, 우리 모두가 기다리고 있었던 것은 확실하지 않습니다. 하지만 정확하게 이를 수행하는 CSS 필터가 있습니다.

주의사항:

  • 기존 내용 뒤에 그림자가 그려집니다.이것은 우리가 몇 가지 절대적인 위치 맞추기 묘기를 만들어야 한다는 것을 의미합니다.
  • 모든 픽셀은 동일하게 취급될 것이지만, OP는 [우리는 그렇게 되어서는 안 된다] "검은색이 아닌 다른 색에 무슨 일이 일어나는지 신경 쓴다"고 말했습니다.
  • 브라우저 지원. (최신 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>

저는 svg 필터를 사용하여 이 답변으로 시작하여 다음과 같이 수정했습니다.

데이터 URL의 SVG 필터

마크업에서 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 필터가 작동하지 않으면 그레이스케일 폴백이 발생합니다.
  • mixin을 사용하면 사용법이 매우 간단합니다(위의 예 참조).
  • 색상은 세피아 트릭(순수 CSS의 RGBA 구성 요소 및 SCSS의 HEX 색상을 사용할 수 있음)보다 읽기 쉽고 수정하기 쉽습니다.
  • 이상한 동작을 방지합니다.

주의사항:

  • 모든 브라우저가 데이터 URL(특히 ID 해시)의 SVG 필터를 지원하지는 않지만 현재 Firefox Cromium 브라우저(및 기타 브라우저)에서 작동합니다.
  • 색상을 동적으로 지정하려면 SCSS 혼합을 사용해야 합니다.
  • 순수한 CSS 버전은 조금 못생겼습니다. 만약 당신이 많은 다른 색상을 원한다면 SVG를 여러 번 포함해야 합니다.

CSS에서 참조된 SVG 필터를 사용하면 이 모든 것을 매우 간단하게 만들 수 있습니다.recolor를 수행하려면 단일 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">

David Dostals SCSS Mixin I에서 확장하기 위해 불투명 매개 변수를 제거하고 새로운 SAS 분할 구문과 일치하도록 구문을 업데이트했습니다.

불투명도 매개 변수를 제거하고 색상 값에서 직접 불투명도를 취하면 주어진 16진수/rgba 색상(예: SAS 변수)을 가져와서 그에 따라 필터를 적용할 수 있습니다.

@use "sass:math";

@mixin recolor($color: #000) {
  $r: math.div(red($color), 255);
  $g: math.div(green($color), 255);
  $b: math.div(blue($color), 255);
  $a: alpha($color);
 
  // grayscale fallback if SVG from data url is not supported
  $lightness: lightness($color);
  filter: saturate(0%) brightness(0%) invert($lightness) opacity($a);

  // 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}');
}
// applied with
@include recolor($arbitrary-color);

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

SVG 필터를 통한 치료 예제가 불완전하다는 것을 알게 되었습니다. 제 것을 썼습니다. (완벽하게 작동합니다.) (Michael Mullany 답변 참조) 그래서 원하는 색상을 얻을 수 있는 방법은 다음과 같습니다.

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

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

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

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

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

  <table>
    <caption>SVG method</caption>
    <tr> <th>Image</th> <th>Color</th> </tr>
    <tr>
      <td><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Doom_%E2%80%93_Game%E2%80%99s_logo.svg/120px-Doom_%E2%80%93_Game%E2%80%99s_logo.svg.png" id="ImgTest" /></td> 
      <td><input type="color" value="#000000"  id="PickColor" ></td>
    </tr>
    <tr> <td>.</td> <td>.</td> </tr>
    <tr> <th>Filter value </th> <th>#RBG target</th> </tr>
    <tr>
      <td><pre id="FilterVal">
    0 0 0 0 0
    0 0 0 0 0
    0 0 0 0 0
    0 0 0 1 0</pre></td>
        <td id="RGBval">#000000</td>
    </tr>
  </table>

코드 => URL.createObjectURL에서만 SVG Filter를 사용하는 두 번째 솔루션이 있습니다.

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

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

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

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

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

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

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

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

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

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

SVG_Filter.init('ImgTest');

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

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

  FilterVal.textContent = SVG_Filter.SetColor( r, g, b );
}
#RGBval    { text-transform: uppercase }
#PickColor { height: 50px; margin: 0 20px }
th         { background-color: lightblue; padding: 5px 20px }
pre        { margin: 0 15px }
#PickColor { width:90px; height:28px; }
<table>
  <caption>SVG method</caption>
  <tr> <th>Image</th> <th>Color</th> </tr>
  <tr>
    <td><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Doom_%E2%80%93_Game%E2%80%99s_logo.svg/120px-Doom_%E2%80%93_Game%E2%80%99s_logo.svg.png" id="ImgTest" /></td> 
    <td><input type="color" value="#E2218A" id="PickColor" ></td>
  </tr>
  <tr> <td>.</td> <td>.</td> </tr>
  <tr> <th>Filter value </th> <th>#RBG target</th> </tr>
  <tr>
    <td><pre id="FilterVal">
  0 0 0 0 0
  0 0 0 0 0
  0 0 0 0 0
  0 0 0 1 0</pre></td>
      <td id="RGBval">#000000</td>
  </tr>
</table>

이전의 놀라운 답변을 바탕으로, 저는 코드를 좀 더 이해하기 쉽게 만들려고 노력했습니다.

나는 그것을 더 기능적으로 만들었고, TypeScript 타이핑을 추가했고, 무슨 일이 일어나고 있는지 이해했을 때 몇 가지 변수의 이름을 바꿨습니다.

import ColorParser from 'color';

function parseColorToRgb(input: string) {
  const colorInstance = new ColorParser(input);

  return new RgbColor(
    colorInstance.red(),
    colorInstance.green(),
    colorInstance.blue(),
  );
}

function clampRgbPart(value: number): number {
  if (value > 255) {
    return 255;
  }

  if (value < 0) {
    return 0;
  }

  return value;
}

class RgbColor {
  constructor(public red: number, public green: number, public blue: number) {}

  toString() {
    return `rgb(${Math.round(this.red)}, ${Math.round(
      this.green,
    )}, ${Math.round(this.blue)})`;
  }

  set(r: number, g: number, b: number) {
    this.red = clampRgbPart(r);
    this.green = clampRgbPart(g);
    this.blue = clampRgbPart(b);
  }

  hueRotate(angle = 0) {
    angle = (angle / 180) * Math.PI;
    const sin = Math.sin(angle);
    const 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.14,
      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: number[]) {
    const newR = clampRgbPart(
      this.red * matrix[0] + this.green * matrix[1] + this.blue * matrix[2],
    );
    const newG = clampRgbPart(
      this.red * matrix[3] + this.green * matrix[4] + this.blue * matrix[5],
    );
    const newB = clampRgbPart(
      this.red * matrix[6] + this.green * matrix[7] + this.blue * matrix[8],
    );
    this.red = newR;
    this.green = newG;
    this.blue = newB;
  }

  brightness(value = 1) {
    this.linear(value);
  }

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

  linear(slope = 1, intercept = 0) {
    this.red = clampRgbPart(this.red * slope + intercept * 255);
    this.green = clampRgbPart(this.green * slope + intercept * 255);
    this.blue = clampRgbPart(this.blue * slope + intercept * 255);
  }

  invert(value = 1) {
    this.red = clampRgbPart((value + (this.red / 255) * (1 - 2 * value)) * 255);
    this.green = clampRgbPart(
      (value + (this.green / 255) * (1 - 2 * value)) * 255,
    );
    this.blue = clampRgbPart(
      (value + (this.blue / 255) * (1 - 2 * value)) * 255,
    );
  }

  applyFilters(filters: Filters) {
    this.set(0, 0, 0);
    this.invert(filters[0] / 100);
    this.sepia(filters[1] / 100);
    this.saturate(filters[2] / 100);
    this.hueRotate(filters[3] * 3.6);
    this.brightness(filters[4] / 100);
    this.contrast(filters[5] / 100);
  }

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

    if (max === min) {
      h = s = 0;
    } else {
      const 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,
    };
  }
}

interface HSLData {
  h: number;
  s: number;
  l: number;
}

interface ColorFilterSolveResult {
  loss: number;
  filters: Filters;
}

const reusedColor = new RgbColor(0, 0, 0);

function formatFilterValue(value: number, multiplier = 1) {
  return Math.round(value * multiplier);
}

type Filters = [
  invert: number,
  sepia: number,
  saturate: number,
  hueRotate: number,
  brightness: number,
  contrast: number,
];

function convertFiltersListToCSSFilter(filters: Filters) {
  function fmt(idx: number, multiplier = 1) {
    return Math.round(filters[idx] * multiplier);
  }
  const [invert, sepia, saturate, hueRotate, brightness, contrast] = filters;
  return `filter: invert(${formatFilterValue(
    invert,
  )}%) sepia(${formatFilterValue(sepia)}%) saturate(${formatFilterValue(
    saturate,
  )}%) hue-rotate(${formatFilterValue(
    hueRotate,
    3.6,
  )}deg) brightness(${formatFilterValue(
    brightness,
  )}%) contrast(${formatFilterValue(contrast)}%);`;
}

function calculateLossForFilters(
  filters: Filters,
  targetColor: RgbColor,
  targetHSL: HSLData,
) {
  reusedColor.applyFilters(filters);
  const actualHSL = reusedColor.hsl();

  return (
    Math.abs(reusedColor.red - targetColor.red) +
    Math.abs(reusedColor.green - targetColor.green) +
    Math.abs(reusedColor.blue - targetColor.blue) +
    Math.abs(actualHSL.h - targetHSL.h) +
    Math.abs(actualHSL.s - targetHSL.s) +
    Math.abs(actualHSL.l - targetHSL.l)
  );
}



export function solveColor(input: string) {
  const targetColor = parseColorToRgb(input);
  const targetHSL = targetColor.hsl();

  function improveInitialSolveResult(initialResult: ColorFilterSolveResult) {
    const A = initialResult.loss;
    const c = 2;
    const A1 = A + 1;
    const a: Filters = [
      0.25 * A1,
      0.25 * A1,
      A1,
      0.25 * A1,
      0.2 * A1,
      0.2 * A1,
    ];
    return findColorFilters(A, a, c, initialResult.filters, 500);
  }

  function findColorFilters(
    initialLoss: number,
    filters: Filters,
    c: number,
    values: Filters,
    iterationsCount: number,
  ): ColorFilterSolveResult {
    const alpha = 1;
    const gamma = 0.16666666666666666;

    let best = null;
    let bestLoss = Infinity;
    const deltas = new Array(6);
    const highArgs = new Array(6) as Filters;
    const lowArgs = new Array(6) as Filters;

    for (
      let iterationIndex = 0;
      iterationIndex < iterationsCount;
      iterationIndex++
    ) {
      const ck = c / Math.pow(iterationIndex + 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];
      }

      const lossDiff =
        calculateLossForFilters(highArgs, targetColor, targetHSL) -
        calculateLossForFilters(lowArgs, targetColor, targetHSL);

      for (let i = 0; i < 6; i++) {
        const g = (lossDiff / (2 * ck)) * deltas[i];
        const ak =
          filters[i] / Math.pow(initialLoss + iterationIndex + 1, alpha);
        values[i] = fix(values[i] - ak * g, i);
      }

      const loss = calculateLossForFilters(values, targetColor, targetHSL);
      if (loss < bestLoss) {
        best = values.slice(0) as Filters;
        bestLoss = loss;
      }
    }
    return { filters: best!, loss: bestLoss };

    function fix(value: number, idx: number) {
      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 %= max;
        } else if (value < 0) {
          value = max + (value % max);
        }
      } else if (value < 0) {
        value = 0;
      } else if (value > max) {
        value = max;
      }
      return value;
    }
  }

  function solveInitial(): ColorFilterSolveResult {
    const A = 5;
    const c = 15;
    const a: Filters = [60, 180, 18000, 600, 1.2, 1.2];

    let best: ColorFilterSolveResult = {
      loss: Infinity,
      filters: [0, 0, 0, 0, 0, 0],
    };
    for (let i = 0; best.loss > 25 && i < 3; i++) {
      const initial: Filters = [50, 20, 3750, 50, 100, 100];
      const result = findColorFilters(A, a, c, initial, 1000);
      if (result.loss < best.loss) {
        best = result;
      }
    }
    return best;
  }

  const result = improveInitialSolveResult(solveInitial());

  return convertFiltersListToCSSFilter(result.filters)
}

저도 사용하고 있습니다.npm color 문자열 등.


몇 가지 참고 사항을 추가하겠습니다.

  • 여기에 솔리드 캐싱 계층을 추가하고 싶을 것입니다.
  • 필터에 필요한 색상 수가 제한적인 경우 필터를 '사전에' 지정하고 해당 색상의 하드 코딩된 맵을 만들어 사용자가 해당 색상에 대해 계산을 실행할 필요가 없도록 하는 것이 좋습니다.

여기 내 캐싱 계층이 있습니다.


const colorFiltersCache = new Map<string, string>();

export function cachedSolveColor(input: string) {
  const existingResult = colorFiltersCache.get(input);

  if (existingResult) {
    return existingResult;
  }

  const newResult = solveColor(input);

  colorFiltersCache.set(input, newResult);

  return newResult;
}

@David Dosstal의 답변과 @silvan의 답변은 훌륭한 해결책이며 다른 답변처럼 손실 문제가 없습니다.하지만, 저는 이것을 SASS 대신 LESS와 함께 사용하고 싶어서 믹스인을 변환했습니다.관심 있는 사람이 있다면 다음은 LESS 버전입니다.

.recolor(@color: #333) {
    @r: red(@color) / 255;
    @g: green(@color) / 255;
    @b: blue(@color) / 255;
    @a: alpha(@color);
  
    // grayscale fallback if SVG from data url is not supported
    @lightness: lightness(@color);
    filter: saturate(0%) brightness(0%) invert(@lightness) opacity(@a);
  
    // 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 {
  .recolor(rgba(0, 250, 134, 0.8));
}

배경색 문제:svg에서 상속되는 배경색을 설정한 경우 필터를 적용하기 전에 svg의 배경을 투명으로 설정합니다(그렇지 않으면 반전이 배경을 수정합니다).

그냥 쓰기

fill: #000000

fillCSS의 속성은 SVG 모양의 색을 채우기 위한 것입니다.fill속성에서 모든 CSS 색상 값을 사용할 수 있습니다.

    -webkit-filter: invert(100%); /* Safari/Chrome */
    filter: invert(100%) brightness(0%);

이것이 가장 쉬운 방법입니다.

언급URL : https://stackoverflow.com/questions/42966641/how-to-transform-black-into-any-given-color-using-only-css-filters

반응형