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


114

내 질문은 : 대상 RGB 색상이 주어지면 CSS 필터#000 만 사용하여 검정색 ( )을 해당 색상으로 다시 칠하는 공식은 무엇 입니까?

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

이에 대한 컨텍스트는 background-image. 이 경우 KaTeX에서 특정 TeX 수학 기능을 지원하기위한 것입니다 : https://github.com/Khan/KaTeX/issues/587 .

대상 색상이 #ffff00(노란색) 인 경우 올바른 해결책은 다음과 같습니다.

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

( 데모 )

비 목표

  • 생기.
  • 비 CSS 필터 솔루션.
  • 검은 색 이외의 색상에서 시작합니다.
  • 검은 색 이외의 색상에 대한 관심.

지금까지의 결과

무차별 대입 솔루션을 제출 하여 수락 된 답변을 얻을 수 있습니다 !

자원

  • 계산 방법 hue-rotatesepia계산 : https://stackoverflow.com/a/29521147/181228 Ruby 구현 예 :

    LUM_R = 0.2126; LUM_G = 0.7152; LUM_B = 0.0722
    HUE_R = 0.1430; HUE_G = 0.1400; HUE_B = 0.2830
    
    def clamp(num)
      [0, [255, num].min].max.round
    end
    
    def hue_rotate(r, g, b, angle)
      angle = (angle % 360 + 360) % 360
      cos = Math.cos(angle * Math::PI / 180)
      sin = Math.sin(angle * Math::PI / 180)
      [clamp(
         r * ( LUM_R  +  (1 - LUM_R) * cos  -  LUM_R * sin       ) +
         g * ( LUM_G  -  LUM_G * cos        -  LUM_G * sin       ) +
         b * ( LUM_B  -  LUM_B * cos        +  (1 - LUM_B) * sin )),
       clamp(
         r * ( LUM_R  -  LUM_R * cos        +  HUE_R * sin       ) +
         g * ( LUM_G  +  (1 - LUM_G) * cos  +  HUE_G * sin       ) +
         b * ( LUM_B  -  LUM_B * cos        -  HUE_B * sin       )),
       clamp(
         r * ( LUM_R  -  LUM_R * cos        -  (1 - LUM_R) * sin ) +
         g * ( LUM_G  -  LUM_G * cos        +  LUM_G * sin       ) +
         b * ( LUM_B  +  (1 - LUM_B) * cos  +  LUM_B * sin       ))]
    end
    
    def sepia(r, g, b)
      [r * 0.393 + g * 0.769 + b * 0.189,
       r * 0.349 + g * 0.686 + b * 0.168,
       r * 0.272 + g * 0.534 + b * 0.131]
    end

    clamp위 의 내용은hue-rotate 함수를 비선형으로 만듭니다.

    브라우저 구현 : Chromium , Firefox .

  • 데모 : 회색조 색상에서 비 회색조 색상 얻기 : https://stackoverflow.com/a/25524145/181228

  • 거의 작동 하는 공식 ( 비슷한 질문에서 ) :
    https://stackoverflow.com/a/29958459/181228

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


그래서 LERP # 000000에서 #RRGGBB로하고 싶습니까? (그냥 명확히)
Zze

1
예, 달콤합니다. 솔루션에 전환을 통합하고 싶지 않다는 점을 명확히합니다.
Zze

1
블렌드 모드가 당신을 위해 일할 수 있습니까? 당신은 쉽게 컬러에 블랙 변환 할 수 있습니다 ...하지만 당신이 무엇을 달성하고자하는의 글로벌 사진을 얻을하지 않습니다
발스

1
@glebm 그래서 당신은 어떤 방법을 사용하여 검은 색을 어떤 색상으로 바꾸고 CSS를 사용하여 적용 할 수있는 공식을 찾아야합니다.
ProllyGeek

2
@ProllyGeek 예. 내가 언급해야 할 또 다른 제약은 결과 공식이 5GiB 테이블의 무차별 조회가 될 수 없다는 것입니다 (예 : 웹 페이지의 자바 스크립트에서 사용할 수 있어야 함).
glebm

답변:


148

@Dave는 이에 대한 답변 (작업 코드 포함)을 최초로 게시 했으며 그의 답변은 뻔뻔한 복사 및 붙여 넣기 의 귀중한 소스였습니다. 영감 . 이 게시물은 @Dave의 답변을 설명하고 수정하려는 시도로 시작되었지만 이후 자체 답변으로 발전했습니다.

내 방법이 훨씬 빠릅니다. A에 따라 jsPerf 벤치 마크 데이브의 알고리즘 @ 무작위로 생성 된 RGB 색상에에서 실행 (600) MS 광산에서 실행되는 동안, 30 MS . 예를 들어 속도가 중요한로드 시간에서 이는 확실히 중요 할 수 있습니다.

또한 일부 색상의 경우 알고리즘이 더 잘 수행됩니다.

  • 의 경우 rgb(0,255,0)@ Dave 's 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)

데모

"use strict";

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

    set(r, g, b) {
        this.r = this.clamp(r);
        this.g = this.clamp(g);
        this.b = this.clamp(b);
    }

    hueRotate(angle = 0) {
        angle = angle / 180 * Math.PI;
        let sin = Math.sin(angle);
        let cos = Math.cos(angle);

        this.multiply([
            0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
            0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
            0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
        ]);
    }

    grayscale(value = 1) {
        this.multiply([
            0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value),
            0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value),
            0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value)
        ]);
    }

    sepia(value = 1) {
        this.multiply([
            0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value),
            0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value),
            0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value)
        ]);
    }

    saturate(value = 1) {
        this.multiply([
            0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value,
            0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value,
            0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value
        ]);
    }

    multiply(matrix) {
        let newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
        let newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
        let newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
        this.r = newR; this.g = newG; this.b = newB;
    }

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

    linear(slope = 1, intercept = 0) {
        this.r = this.clamp(this.r * slope + intercept * 255);
        this.g = this.clamp(this.g * slope + intercept * 255);
        this.b = this.clamp(this.b * slope + intercept * 255);
    }

    invert(value = 1) {
        this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
        this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
        this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
    }

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

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

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

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

class Solver {
    constructor(target) {
        this.target = target;
        this.targetHSL = target.hsl();
        this.reusedColor = new Color(0, 0, 0); // Object pool
    }

    solve() {
        let result = this.solveNarrow(this.solveWide());
        return {
            values: result.values,
            loss: result.loss,
            filter: this.css(result.values)
        };
    }

    solveWide() {
        const A = 5;
        const c = 15;
        const a = [60, 180, 18000, 600, 1.2, 1.2];

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

    solveNarrow(wide) {
        const A = wide.loss;
        const c = 2;
        const A1 = A + 1;
        const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
        return this.spsa(A, a, c, wide.values, 500);
    }

    spsa(A, a, c, values, iters) {
        const alpha = 1;
        const gamma = 0.16666666666666666;

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

        for(let k = 0; k < iters; k++) {
            let ck = c / Math.pow(k + 1, gamma);
            for(let i = 0; i < 6; i++) {
                deltas[i] = Math.random() > 0.5 ? 1 : -1;
                highArgs[i] = values[i] + ck * deltas[i];
                lowArgs[i]  = values[i] - ck * deltas[i];
            }

            let lossDiff = this.loss(highArgs) - this.loss(lowArgs);
            for(let i = 0; i < 6; i++) {
                let g = lossDiff / (2 * ck) * deltas[i];
                let ak = a[i] / Math.pow(A + k + 1, alpha);
                values[i] = fix(values[i] - ak * g, i);
            }

            let loss = this.loss(values);
            if(loss < bestLoss) { best = values.slice(0); bestLoss = loss; }
        } return { values: best, loss: bestLoss };

        function fix(value, idx) {
            let max = 100;
            if(idx === 2 /* saturate */) { max = 7500; }
            else if(idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; }

            if(idx === 3 /* hue-rotate */) {
                if(value > max) { value = value % max; }
                else if(value < 0) { value = max + value % max; }
            } else if(value < 0) { value = 0; }
            else if(value > max) { value = max; }
            return value;
        }
    }

    loss(filters) { // Argument is array of percentages.
        let color = this.reusedColor;
        color.set(0, 0, 0);

        color.invert(filters[0] / 100);
        color.sepia(filters[1] / 100);
        color.saturate(filters[2] / 100);
        color.hueRotate(filters[3] * 3.6);
        color.brightness(filters[4] / 100);
        color.contrast(filters[5] / 100);

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

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

$("button.execute").click(() => {
    let rgb = $("input.target").val().split(",");
    if (rgb.length !== 3) { alert("Invalid format!"); return; }

    let color = new Color(rgb[0], rgb[1], rgb[2]);
    let solver = new Solver(color);
    let result = solver.solve();

    let lossMsg;
    if (result.loss < 1) {
        lossMsg = "This is a perfect result.";
    } else if (result.loss < 5) {
        lossMsg = "The is close enough.";
    } else if(result.loss < 15) {
        lossMsg = "The color is somewhat off. Consider running it again.";
    } else {
        lossMsg = "The color is extremely off. Run it again!";
    }

    $(".realPixel").css("background-color", color.toString());
    $(".filterPixel").attr("style", result.filter);
    $(".filterDetail").text(result.filter);
    $(".lossDetail").html(`Loss: ${result.loss.toFixed(1)}. <b>${lossMsg}</b>`);
});
.pixel {
    display: inline-block;
    background-color: #000;
    width: 50px;
    height: 50px;
}

.filterDetail {
    font-family: "Consolas", "Menlo", "Ubuntu Mono", monospace;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<input class="target" type="text" placeholder="r, g, b" value="250, 150, 50" />
<button class="execute">Compute Filters</button>

<p>Real pixel, color applied through CSS <code>background-color</code>:</p>
<div class="pixel realPixel"></div>

<p>Filtered pixel, color applied through CSS <code>filter</code>:</p>
<div class="pixel filterPixel"></div>

<p class="filterDetail"></p>
<p class="lossDetail"></p>


용법

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

설명

Javascript를 작성하는 것으로 시작하겠습니다.

"use strict";

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

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

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

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

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

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

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

설명:

  • Color클래스는 RGB 색상을 나타냅니다.
    • toString()함수는 CSS rgb(...)색상 문자열 의 색상을 반환 합니다.
    • hsl()함수는 HSL 로 변환 된 색상을 반환합니다 .
    • clamp()기능은 주어진 색상 값이 범위 (0-255) 내에 있는지 확인합니다.
  • Solver클래스는 대상 색상으로 문제 해결을 시도합니다.
    • css()함수는 CSS 필터 문자열에 지정된 필터를 반환합니다.

구현 grayscale(), sepia()saturate()

CSS / SVG 필터의 핵심은 필터 기본 요소입니다. 이미지에 대한 저수준 수정을 나타내는 입니다.

필터는 grayscale(), sepia()saturate()필터에 의해 구현되는 프리미티브 <feColorMatrix>행한다는 매트릭스 승산에 필터 (종종 동적으로 생성)에 의해 특정되는 행렬과 색상이 만든 매트릭스 사이. 도표:

행렬 곱셈

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

  • 색상 매트릭스의 마지막 요소는 다음과 같습니다. 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()

hue-rotate()필터에 의해 구현된다 <feColorMatrix type="hueRotate" />.

필터 매트릭스는 아래와 같이 계산됩니다.

예를 들어, 요소 00 과 같이 계산된다 :

몇 가지 참고 사항 :

  • 회전 각도는 각도로 표시됩니다. Math.sin()또는에 전달하기 전에 라디안으로 변환해야합니다 Math.cos().
  • Math.sin(angle)Math.cos(angle)캐시 된 후 한 번 계산해야합니다.

이행:

function hueRotate(angle = 0) {
    angle = angle / 180 * Math.PI;
    let sin = Math.sin(angle);
    let cos = Math.cos(angle);

    this.multiply([
        0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
        0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
        0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
    ]);
}

구현 brightness()contrast()

brightness()contrast()필터에 의해 구현 <feComponentTransfer>과 함께 <feFuncX type="linear" />.

<feFuncX type="linear" />요소는 경사가로 채기 속성을 허용 합니다. 그런 다음 간단한 공식을 통해 각각의 새로운 색상 값을 계산합니다.

value = slope * value + intercept

이것은 구현하기 쉽습니다.

function linear(slope = 1, intercept = 0) {
    this.r = this.clamp(this.r * slope + intercept * 255);
    this.g = this.clamp(this.g * slope + intercept * 255);
    this.b = this.clamp(this.b * slope + intercept * 255);
}

이것이 구현되면 다음 brightness()contrast()같이 구현할 수도 있습니다.

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

구현 invert()

invert()필터에 의해 구현된다 <feComponentTransfer><feFuncX type="table" />.

사양은 다음과 같습니다.

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

"table"의 경우 함수는 tableValues 속성에 지정된 값 간의 선형 보간에 의해 정의됩니다 . 테이블에는 n 개의 균등 한 크기의 보간 영역에 대한 시작 및 끝 값을 지정하는 n + 1 값 (즉, v 0 ~ v n )이 있습니다. 보간은 다음 공식을 사용합니다.

C에 대해 k를 찾으십시오. 과 같은 .

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

결과 C ' 는 다음과 같이 제공됩니다.

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

이 공식에 대한 설명 :

  • invert()[- 값이 값 1] : 필터는이 테이블을 정의하고있다. 이것은 tableValues 또는 v 입니다.
  • 공식은 n을 정의 하므로 n + 1이 테이블의 길이가됩니다. 테이블의 길이가 2이므로 n = 1입니다.
  • 공식은 k를 정의 하며 kk + 1은 테이블의 인덱스입니다. 테이블에 2 개의 요소가 있으므로 k = 0입니다.

따라서 공식을 단순화하여 다음을 수행 할 수 있습니다.

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

테이블의 값을 인라인하면 다음이 남습니다.

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

다음 순서로 필터를 계산합니다.

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

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

그러나 이것은 느리고 비효율적입니다. 따라서 나는 내 대답을 제시합니다.

SPSA 구현

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

두 가지 측정 항목의 합으로 색상 차이를 측정합니다.

  • 목표는 가장 가까운 RGB 값을 생성하는 것이므로 RGB 차이입니다.
  • 많은 HSL 값이 필터에 해당하기 때문에 HSL 차이 (예 : 색조는 대략적으로 상관 hue-rotate(), 채도는과 상관 됨 saturate()등)이 알고리즘을 안내합니다.

손실 함수는 하나의 인수 (필터 백분율 배열)를 사용합니다.

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

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

이행:

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

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

손실 함수를 최소화하려고 노력할 것입니다.

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

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

이행:

function spsa(A, a, c, values, iters) {
    const alpha = 1;
    const gamma = 0.16666666666666666;

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

    for(let k = 0; k < iters; k++) {
        let ck = c / Math.pow(k + 1, gamma);
        for(let i = 0; i < 6; i++) {
            deltas[i] = Math.random() > 0.5 ? 1 : -1;
            highArgs[i] = values[i] + ck * deltas[i];
            lowArgs[i]  = values[i] - ck * deltas[i];
        }

        let lossDiff = this.loss(highArgs) - this.loss(lowArgs);
        for(let i = 0; i < 6; i++) {
            let g = lossDiff / (2 * ck) * deltas[i];
            let ak = a[i] / Math.pow(A + k + 1, alpha);
            values[i] = fix(values[i] - ak * g, i);
        }

        let loss = this.loss(values);
        if(loss < bestLoss) { best = values.slice(0); bestLoss = loss; }
    } return { values: best, loss: bestLoss };

    function fix(value, idx) {
        let max = 100;
        if(idx === 2 /* saturate */) { max = 7500; }
        else if(idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; }

        if(idx === 3 /* hue-rotate */) {
            if(value > max) { value = value % max; }
            else if(value < 0) { value = max + value % max; }
        } else if(value < 0) { value = 0; }
        else if(value > max) { value = max; }
        return value;
    }
}

SPSA를 수정 / 최적화했습니다.

  • 마지막 대신 생성 된 최상의 결과를 사용합니다.
  • 모든 배열을 인용 ( deltas, highArgs, lowArgs), 대신 각각의 반복으로 그들을 재 작성.
  • 값의 어레이를 사용 대신 하나의 값. 이는 모든 필터가 다르기 때문에 서로 다른 속도로 이동 / 수렴해야하기 때문입니다.
  • fix각 반복 후 함수 실행 . saturate(최대 값이 7500 % 인 경우) brightnesscontrast(최대 값이 200 % 인 경우) 및 hueRotate(값이 고정되는 대신 래핑되는 경우 )를 제외하고 모든 값을 0 %에서 100 % 사이로 고정합니다.

2 단계 프로세스에서 SPSA를 사용합니다.

  1. 검색 공간을 "탐색"하려는 "넓은"단계. 결과가 만족스럽지 않으면 SPSA를 제한적으로 재 시도합니다.
  2. 넓은 스테이지에서 최상의 결과를 가져 와서 "조정"하려는 "좁은"스테이지입니다. Aa에 동적 값을 사용 합니다 .

이행:

function solve() {
    let result = this.solveNarrow(this.solveWide());
    return {
        values: result.values,
        loss: result.loss,
        filter: this.css(result.values)
    };
}

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

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

function solveNarrow(wide) {
    const A = wide.loss;
    const c = 2;
    const A1 = A + 1;
    const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
    return this.spsa(A, a, c, wide.values, 500);
}

SPSA 조정

경고 : 수행중인 작업을 확실히 알지 못하는 경우 SPSA 코드, 특히 해당 상수를 엉망으로 만들지 마십시오 .

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

변경을 고집하는 경우 "최적화"하기 전에 측정해야합니다.

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

그런 다음 Node.js에서 코드를 실행합니다. 꽤 오랜 시간이 지나면 결과는 다음과 같습니다.

Average loss: 3.4768521401985275
Average time: 11.4915ms

이제 상수를 마음의 내용으로 조정하십시오.

몇 가지 팁 :

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

TL; DR


3
개발 과정에 대한 아주 멋진 요약! 내 생각을 읽고 있습니까?!
Dave는

1
@Dave Actually, 나는 이것에 대해 독립적으로 작업하고 있었지만 당신은 나를 이겼습니다.
MultiplyByZer0

4
훌륭한 답변! 이 코드 펜에서 구현
KyleMit

3
이것은 완전히 미친 방법입니다. SVG 필터 (feColorMatrix의 다섯 번째 열)를 사용하여 직접 색상을 설정할 수 있으며 CSS에서 해당 필터를 참조 할 수 있습니다.이 방법을 사용하지 않는 이유는 무엇입니까?
Michael Mullany

2
@MichaelMullany 글쎄, 내가 이것에 대해 얼마나 오랫동안 일했는지 고려할 때 당황 스럽습니다. 나는 당신의 방법을 생각하지 않았지만 이제는 이해합니다. 요소를 임의의 색상으로 다시 칠 하려면 적절한 값 이 <filter>포함 된 a <feColorMatrix>를 포함하는 SVG를 동적으로 생성 합니다 (대상 RGB를 포함하는 마지막 열을 제외한 모든 0). 값, 0 및 1), SVG를 DOM에 삽입하고 CSS에서 필터를 참조합니다. 답변으로 솔루션을 작성해주세요 (데모 포함). 제가 찬성하겠습니다.
MultiplyByZer0

55

이것은 토끼 구멍 아래로 꽤 여행 이었지만 여기 있습니다!

var tolerance = 1;
var invertRange = [0, 1];
var invertStep = 0.1;
var sepiaRange = [0, 1];
var sepiaStep = 0.1;
var saturateRange = [5, 100];
var saturateStep = 5;
var hueRotateRange = [0, 360];
var hueRotateStep = 5;
var possibleColors;
var color = document.getElementById('color');
var pixel = document.getElementById('pixel');
var filtersBox = document.getElementById('filters');
var button = document.getElementById('button');
button.addEventListener('click', function() { 			      
	getNewColor(color.value);
})

// matrices taken from https://www.w3.org/TR/filter-effects/#feColorMatrixElement
function sepiaMatrix(s) {
	return [
		(0.393 + 0.607 * (1 - s)), (0.769 - 0.769 * (1 - s)), (0.189 - 0.189 * (1 - s)),
		(0.349 - 0.349 * (1 - s)), (0.686 + 0.314 * (1 - s)), (0.168 - 0.168 * (1 - s)),
		(0.272 - 0.272 * (1 - s)), (0.534 - 0.534 * (1 - s)), (0.131 + 0.869 * (1 - s)),
	]
}

function saturateMatrix(s) {
	return [
		0.213+0.787*s, 0.715-0.715*s, 0.072-0.072*s,
		0.213-0.213*s, 0.715+0.285*s, 0.072-0.072*s,
		0.213-0.213*s, 0.715-0.715*s, 0.072+0.928*s,
	]
}

function hueRotateMatrix(d) {
	var cos = Math.cos(d * Math.PI / 180);
	var sin = Math.sin(d * Math.PI / 180);
	var a00 = 0.213 + cos*0.787 - sin*0.213;
	var a01 = 0.715 - cos*0.715 - sin*0.715;
	var a02 = 0.072 - cos*0.072 + sin*0.928;

	var a10 = 0.213 - cos*0.213 + sin*0.143;
	var a11 = 0.715 + cos*0.285 + sin*0.140;
	var a12 = 0.072 - cos*0.072 - sin*0.283;

	var a20 = 0.213 - cos*0.213 - sin*0.787;
	var a21 = 0.715 - cos*0.715 + sin*0.715;
	var a22 = 0.072 + cos*0.928 + sin*0.072;

	return [
		a00, a01, a02,
		a10, a11, a12,
		a20, a21, a22,
	]
}

function clamp(value) {
	return value > 255 ? 255 : value < 0 ? 0 : value;
}

function filter(m, c) {
	return [
		clamp(m[0]*c[0] + m[1]*c[1] + m[2]*c[2]),
		clamp(m[3]*c[0] + m[4]*c[1] + m[5]*c[2]),
		clamp(m[6]*c[0] + m[7]*c[1] + m[8]*c[2]),
	]
}

function invertBlack(i) {
	return [
		i * 255,
		i * 255,
		i * 255,
	]
}

function generateColors() {
	let possibleColors = [];

	let invert = invertRange[0];
	for (invert; invert <= invertRange[1]; invert+=invertStep) {
		let sepia = sepiaRange[0];
		for (sepia; sepia <= sepiaRange[1]; sepia+=sepiaStep) {
			let saturate = saturateRange[0];
			for (saturate; saturate <= saturateRange[1]; saturate+=saturateStep) {
				let hueRotate = hueRotateRange[0];
				for (hueRotate; hueRotate <= hueRotateRange[1]; hueRotate+=hueRotateStep) {
					let invertColor = invertBlack(invert);
					let sepiaColor = filter(sepiaMatrix(sepia), invertColor);
					let saturateColor = filter(saturateMatrix(saturate), sepiaColor);
					let hueRotateColor = filter(hueRotateMatrix(hueRotate), saturateColor);

					let colorObject = {
						filters: { invert, sepia, saturate, hueRotate },
						color: hueRotateColor
					}

					possibleColors.push(colorObject);
				}
			}
		}
	}

	return possibleColors;
}

function getFilters(targetColor, localTolerance) {
	possibleColors = possibleColors || generateColors();

	for (var i = 0; i < possibleColors.length; i++) {
		var color = possibleColors[i].color;
		if (
			Math.abs(color[0] - targetColor[0]) < localTolerance &&
			Math.abs(color[1] - targetColor[1]) < localTolerance &&
			Math.abs(color[2] - targetColor[2]) < localTolerance
		) {
			return filters = possibleColors[i].filters;
			break;
		}
	}

	localTolerance += tolerance;
	return getFilters(targetColor, localTolerance)
}

function getNewColor(color) {
	var targetColor = color.split(',');
	targetColor = [
	    parseInt(targetColor[0]), // [R]
	    parseInt(targetColor[1]), // [G]
	    parseInt(targetColor[2]), // [B]
    ]
    var filters = getFilters(targetColor, tolerance);
    var filtersCSS = 'filter: ' +
	    'invert('+Math.floor(filters.invert*100)+'%) '+
	    'sepia('+Math.floor(filters.sepia*100)+'%) ' +
	    'saturate('+Math.floor(filters.saturate*100)+'%) ' +
	    'hue-rotate('+Math.floor(filters.hueRotate)+'deg);';
    pixel.style = filtersCSS;
    filtersBox.innerText = filtersCSS
}

getNewColor(color.value);
#pixel {
  width: 50px;
  height: 50px;
  background: rgb(0,0,0);
}
<input type="text" id="color" placeholder="R,G,B" value="250,150,50" />
<button id="button">get filters</button>
<div id="pixel"></div>
<div id="filters"></div>

편집 : 이 솔루션은 프로덕션 용이 아니며 OP가 요구하는 것을 달성하기 위해 취할 수있는 접근 방식을 보여줍니다. 그대로 색상 스펙트럼의 일부 영역에서 약합니다. 단계 반복에서 더 세분화하거나 @ MultiplyByZer0의 답변 에 자세히 설명 된 이유로 더 많은 필터 함수를 구현하면 더 나은 결과를 얻을 수 있습니다 .

EDIT2 : OP는 무차별 대입 솔루션을 찾고 있습니다. 이 경우 매우 간단합니다. 다음 방정식을 풀면됩니다.

CSS 필터 행렬 방정식

어디

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

내가 입력 255,0,255하면 내 디지털 컬러 미터는 결과가 #d619d9아닌 #ff00ff.
Siguza

@Siguza 확실히 완벽하지는 않습니다. 가장자리 케이스 색상은 루프의 경계를 조정하여 조정할 수 있습니다.
Dave

3
그 방정식은 아무것도하지만, "아주 간단합니다"입니다
MultiplyByZer0

위의 방정식도 빠진 것 같아요 clamp?
glebm

1
클램프는 거기에 없습니다. 그리고 제가 대학 수학에서 기억하는 것에서,이 방정식은 "무력한 힘"이라고 불리는 수치 계산으로 계산됩니다. 행운을 빕니다!
Dave는

28

참고 : OP는 삭제 취소를 요청 했지만 현상금은 Dave의 답변으로 이동합니다.


나는 그것이 질문의 본문에서 요청 된 것이 아니라 확실히 우리 모두가 기다리고 있던 것이 아니라는 것을 알고 있지만 정확히 이것을 수행하는 하나의 CSS 필터가 있습니다. drop-shadow()

주의 사항 :

  • 그림자는 기존 콘텐츠 뒤에 그려집니다. 이것은 우리가 절대적인 위치 결정 트릭을 만들어야 함을 의미합니다.
  • 모든 픽셀은 동일하게 취급되지만 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>


1
아주 영리하고 멋져요! 이것은 나를 위해 작동합니다. 감사합니다
jaminroe

매번 색상이 100 % 정확하기 때문에 이것이 더 나은 해결책이라고 생각합니다.
user835542

있는 그대로 코드는 빈 페이지를 표시합니다 (W10 FF 69b). 하지만 아이콘에는 아무런 문제가 없습니다 (별도의 SVG 확인).
Rene van der Lende

추가 background-color: black;.icon>span만든다 FF의 69B에 대해이 작업을. 그러나 아이콘은 표시되지 않습니다.
Rene van der Lende

@RenevanderLende 방금 FF70에서 시도했지만 여전히 작동합니다. 그것이 당신을 위해 작동하지 않는다면 그것은 당신의쪽에 있어야합니다.
Kaiido

15

CSS에서 참조 된 SVG 필터를 사용 하여이 모든 것을 매우 간단 하게 만들 수 있습니다 . 색상을 다시 칠하려면 feColorMatrix 하나만 있으면됩니다. 이것은 노란색으로 다시 칠해집니다. feColorMatrix의 다섯 번째 열에는 단위 배율의 RGB 대상 값이 있습니다. (노란색-1,1,0)

.icon {
  filter: url(#recolorme); 
}
<svg height="0px" width="0px">
<defs>
  #ffff00
  <filter id="recolorme" color-interpolation-filters="sRGB">
    <feColorMatrix type="matrix" values="0 0 0 0 1
                                         0 0 0 0 1
                                         0 0 0 0 0
                                         0 0 0 1 0"/>
  </filter>
</defs>
</svg>


<img class="icon" src="https://www.nouveauelevator.com/image/black-icon/android.png">


흥미로운 솔루션이지만 CSS를 통해 대상 색상을 제어 할 수없는 것 같습니다.
glebm

적용하려는 각 색상에 대해 새 필터를 정의해야합니다. 그러나 그것은 완전히 정확합니다. hue-rotate는 위의 답변에서 증명했듯이 특정 색상을 클립하는 근사치입니다. 즉, 특정 색상을 사용하여 정확하게 얻을 수 없습니다. 정말 필요한 것은 recolor () CSS 필터 속기입니다.
Michael Mullany

MultiplyByZer0의 답변은 HTML을 수정하지 않고 매우 높은 정확도로 달성하는 일련의 필터를 계산합니다. hue-rotate브라우저에서 사실이면 좋을 것입니다.
glebm

2
feColorMatrix에 "color-interpolation-filters"= "sRGB"를 추가 할 때 블랙 소스 이미지에 대해 정확한 RGB 색상 만 생성하는 것 같습니다.
존 스미스


2

SVG 필터를 통한 처리의 예가 불완전하다는 것을 알았습니다. 완벽하게 작동합니다. (Michael Mullany 답변 참조) 원하는 색상을 얻는 방법은 다음과 같습니다.

다음은 코드 => URL.createObjectURL 에서만 SVG 필터를 사용하는 두 번째 솔루션입니다.


1

그냥 사용

fill: #000000

fillCSS 의 속성은 SVG 모양의 색상을 채우기위한 것입니다. fill숙박 시설은 어떤 CSS 색상 값을 받아 들일 수 있습니다.


3
이것은 SVG 이미지 내부의 CSS에서 작동 할 수 있지만 img브라우저에 의해 요소에 외부 적으로 적용된 CSS로 작동하지 않습니다 .
David Moles

0

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

데이터 URL의 SVG 필터

마크 업 어딘가에 SVG 필터 를 정의하지 않으려면 대신 데이터 URL을 사용할 수 있습니다 ( R , G , BA 를 원하는 색상으로 대체 ).

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

그레이 스케일 대체

위 버전이 작동하지 않으면 그레이 스케일 폴백을 추가 할 수도 있습니다.

saturatebrightness기능 검정 색상을, (당신은 색상이 이미 검은 색이면 것을 포함 할 필요가 없습니다) 설정 invert후 원하는 밝기 (함께 밝게 L )와 선택적으로 당신은 또한 불투명도 (지정할 수 있습니다 ).

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

장점 :

  • 자바 스크립트가 없습니다 .
  • 추가 HTML 요소 없음 .
  • CSS 필터가 지원되지만 SVG 필터가 작동하지 않는 경우 그레이 스케일 대체가 있습니다.
  • 믹스 인을 사용하는 경우 사용법은 매우 간단합니다 (위의 예 참조).
  • 색상은 세피아 트릭보다 더 읽기 쉽고 수정하기 쉽습니다 (순수 CSS의 RGBA 구성 요소 및 SCSS에서 HEX 색상도 사용할 수 있음).
  • 이상한 동작을 피hue-rotate 합니다.

주의 사항 :

  • 모든 브라우저 가 데이터 URL (특히 ID 해시)의 SVG 필터를 지원 하는 것은 아니지만 현재 Firefox 및 Chromium 브라우저에서 작동합니다. (및 기타)에서 작동합니다.
  • 색상을 동적으로 지정하려면 SCSS 믹스 인을 사용해야합니다.
  • 순수한 CSS 버전은 약간 못 생겼습니다. 다양한 색상을 원한다면 SVG를 여러 번 포함해야합니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.