วิธีเปลี่ยนสีดำเป็นสีที่กำหนดโดยใช้ฟิลเตอร์ CSS เท่านั้น


117

คำถามของฉัน: กำหนดสี RGB เป้าหมายสิ่งที่เป็นสูตรการเปลี่ยนสีสีดำ ( #000) เป็นสีที่ใช้เพียงฟิลเตอร์ CSS ?

เพื่อให้ได้รับคำตอบจะต้องมีฟังก์ชัน (ในภาษาใดก็ได้) ที่จะยอมรับสีเป้าหมายเป็นอาร์กิวเมนต์และส่งคืนfilterสตริงCSS ที่เกี่ยวข้อง

บริบทนี้คือความจำเป็นในการเปลี่ยนสี SVG ภายในไฟล์background-image. ในกรณีนี้ก็คือการสนับสนุนคุณสมบัติคณิตศาสตร์เท็กซ์บางอย่างใน KaTeX: https://github.com/Khan/KaTeX/issues/587

ตัวอย่าง

หากสีเป้าหมายเป็น#ffff00(สีเหลือง) วิธีแก้ไขปัญหาที่ถูกต้องคือ:

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

( สาธิต )

ที่ไม่ใช่เป้าหมาย

  • นิเมชั่น
  • โซลูชันที่ไม่ใช่ตัวกรอง CSS
  • เริ่มจากสีอื่นที่ไม่ใช่สีดำ
  • การดูแลเกี่ยวกับสิ่งที่เกิดขึ้นกับสีอื่นที่ไม่ใช่สีดำ

ผลลัพธ์จนถึงตอนนี้

  • การค้นหาพารามิเตอร์ของรายการตัวกรองแบบคงที่: https://stackoverflow.com/a/43959856/181228 จุด
    ด้อย: ไม่มีประสิทธิภาพสร้างสีที่เป็นไปได้เพียง 16,777,216 สี (676,248 กับhueRotateStep=1)

  • โซลูชันการค้นหาที่เร็วขึ้นโดยใช้SPSA : https://stackoverflow.com/a/43960991/181228 ได้รับรางวัล

  • drop-shadowวิธีการแก้ปัญหา: https://stackoverflow.com/a/43959853/181228
    จุดด้อย: ไม่ทำงานบนขอบ ต้องมีfilterการเปลี่ยนแปลงที่ไม่ใช่CSS และการเปลี่ยนแปลง HTML เล็กน้อย

คุณยังคงได้รับคำตอบที่ได้รับการยอมรับโดยการส่งโซลูชันที่ไม่ดุร้าย!

ทรัพยากร

  • วิธีhue-rotateและsepiaมีการคำนวณ: https://stackoverflow.com/a/29521147/181228 การดำเนินงานตัวอย่างทับทิม:

    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ฟังก์ชันไม่เป็นเชิงเส้น

    การใช้งานเบราเซอร์: โครเมี่ยม , 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
อาจเป็นโหมดผสมผสานที่เหมาะกับคุณ? คุณสามารถแปลงสีดำกับสีใด ๆ ... แต่ฉันไม่ได้ภาพระดับโลกของสิ่งที่คุณต้องการเพื่อให้บรรลุ
Vals

1
@glebm ดังนั้นคุณต้องหาสูตร (โดยใช้วิธีใดก็ได้) เพื่อเปลี่ยนสีดำเป็นสีใด ๆ และใช้มันโดยใช้ css?
ProllyGeek

2
@ProllyGeek ค่ะ ข้อ จำกัด อีกประการหนึ่งที่ฉันควรพูดถึงคือสูตรผลลัพธ์ไม่สามารถค้นหาแรงเดรัจฉานของตาราง 5GiB ได้ (ควรใช้จากจาวาสคริปต์บนหน้าเว็บ)
glebm

คำตอบ:


151

@Dave เป็นคนแรกที่โพสต์คำตอบสำหรับสิ่งนี้ (พร้อมรหัสการทำงาน) และคำตอบของเขาเป็นแหล่งข้อมูลอันล้ำค่าของสำเนาไร้ยางอายและวางแรงบันดาลใจให้ฉัน โพสต์นี้เริ่มต้นจากความพยายามที่จะอธิบายและปรับแต่งคำตอบของ @ Dave แต่ตั้งแต่นั้นมาก็ได้พัฒนาเป็นคำตอบของตัวเอง

วิธีการของฉันเร็วกว่ามาก ตามมาตรฐาน jsPerfบนที่สร้างแบบสุ่มสี RGB, @ อัลกอริทึมของเดฟทำงานใน600 มิลลิวินาทีในขณะที่ทำงานในเหมือง30 มิลลิวินาที สิ่งนี้มีความสำคัญอย่างแน่นอนเช่นในเวลาโหลดซึ่งความเร็วเป็นสิ่งสำคัญ

นอกจากนี้สำหรับบางสีอัลกอริทึมของฉันยังทำงานได้ดีกว่า:

  • สำหรับ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()มีการดำเนินการโดย primative กรอง<feColorMatrix>ซึ่งดำเนินการคูณเมทริกซ์ระหว่างเมทริกซ์ที่ระบุโดยตัวกรอง (มักจะสร้างแบบไดนามิก) และเมทริกซ์ที่สร้างขึ้นจากสี แผนภาพ:

การคูณเมทริกซ์

มีการเพิ่มประสิทธิภาพบางอย่างที่เราสามารถทำได้ที่นี่:

  • 1องค์ประกอบสุดท้ายของเมทริกซ์สีเป็นและจะเป็น ไม่มีจุดคำนวณหรือเก็บไว้
  • ไม่มีจุดใดในการคำนวณหรือจัดเก็บค่าอัลฟา / ความโปร่งใส ( A) เนื่องจากเรากำลังจัดการกับ RGB ไม่ใช่ RGBA
  • ดังนั้นเราจึงสามารถตัดการฝึกอบรมกรองจาก 5x5 ไป 3x5 และเมทริกซ์สีจาก 1x5 ไป 1x3 ซึ่งจะช่วยประหยัดงานได้เล็กน้อย
  • <feColorMatrix>ตัวกรองทั้งหมดปล่อยให้คอลัมน์ 4 และ 5 เป็นศูนย์ ดังนั้นเราสามารถลดเมทริกซ์ตัวกรองให้เหลือ 3x3ได้อีก
  • เนื่องจากการคูณนั้นค่อนข้างง่ายจึงไม่จำเป็นต้องลากไลบรารีคณิตศาสตร์ที่ซับซ้อนสำหรับสิ่งนี้ เราสามารถใช้อัลกอริทึมการคูณเมทริกซ์ได้เอง

การดำเนินงาน:

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

(เราใช้ตัวแปรชั่วคราวเพื่อเก็บผลลัพธ์ของการคูณแต่ละแถวเนื่องจากเราไม่ต้องการให้มีการเปลี่ยนแปลงthis.rฯลฯ ซึ่งส่งผลต่อการคำนวณในภายหลัง)

ตอนนี้ที่เราได้ดำเนินการ<feColorMatrix>เราสามารถดำเนินการgrayscale(), sepia()และsaturate()ซึ่งก็วิงวอนกับเมทริกซ์กรองได้รับ:

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

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

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

การดำเนินการ hue-rotate()

กรองจะดำเนินการโดยhue-rotate()<feColorMatrix type="hueRotate" />

เมทริกซ์ตัวกรองคำนวณตามที่แสดงด้านล่าง:

ตัวอย่างเช่นองค์ประกอบa 00จะได้รับการคำนวณดังนี้:

หมายเหตุบางประการ:

  • มุมของการหมุนกำหนดเป็นองศา มันจะต้องถูกแปลงเป็นเรเดียนก่อนที่จะส่งผ่านไปยังหรือMath.sin()Math.cos()
  • Math.sin(angle)และMath.cos(angle)ควรคำนวณครั้งเดียวแล้วจึงแคช

การดำเนินงาน:

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

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

การดำเนินการbrightness()และcontrast()

brightness()และcontrast()ฟิลเตอร์จะดำเนินการโดยมี<feComponentTransfer><feFuncX type="linear" />

แต่ละ<feFuncX type="linear" />องค์ประกอบยอมรับแอตทริบิวต์ความชันและการสกัดกั้น จากนั้นจะคำนวณค่าสีใหม่แต่ละค่าผ่านสูตรง่ายๆ:

value = slope * value + intercept

ใช้งานง่าย:

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

เมื่อดำเนินการแล้วbrightness()และcontrast()สามารถนำไปใช้งานได้เช่นกัน:

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

การดำเนินการ invert()

invert()กรองจะดำเนินการโดยมี<feComponentTransfer><feFuncX type="table" />

ข้อมูลจำเพาะระบุ:

ต่อไปนี้Cคือส่วนประกอบเริ่มต้นและC 'คือส่วนประกอบที่ถูกรีแมป ทั้งในช่วงปิด [0,1]

สำหรับ "ตาราง" ฟังก์ชั่นจะถูกกำหนดโดยสอดแทรกเชิงเส้นระหว่างค่าที่กำหนดในแอตทริบิวต์tableValues ตารางมีn + 1 ค่า (เช่นวี0ไปทาง v n ) ระบุเริ่มต้นและสิ้นสุดค่าสำหรับnภูมิภาคแก้ไขขนาดเท่า ๆ กัน การแก้ไขใช้สูตรต่อไปนี้:

สำหรับค่าC ให้ค้นหาkเช่นนั้น:

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

ผลลัพธ์C 'ได้รับจาก:

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

คำอธิบายของสูตรนี้:

  • invert()กรองกำหนดตารางนี้: [มูลค่า 1 - ค่า] นี่คือtableValuesหรือวี
  • สูตรกำหนดnดังนั้นn + 1 คือความยาวของตาราง เนื่องจากความยาวของตารางคือ 2, n = 1
  • สูตรกำหนดkโดยkและk + 1 เป็นดัชนีของตาราง เนื่องจากตารางมี 2 องค์ประกอบk = 0

ดังนั้นเราสามารถลดความซับซ้อนของสูตรเป็น:

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

การแทรกค่าของตารางเราจะเหลือ:

C '= ค่า + C * (1 - ค่า - มูลค่า)

อีกหนึ่งความเรียบง่าย:

C '= ค่า + C * (ค่า 1 - 2 *)

ข้อกำหนดกำหนด CและC 'เป็นค่า RGB ภายในขอบเขต 0-1 (ตรงข้ามกับ 0-255) ด้วยเหตุนี้เราจึงต้องลดขนาดของค่าก่อนการคำนวณและปรับขนาดค่าเหล่านี้สำรอง

ดังนั้นเราจึงมาถึงการใช้งานของเรา:

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

Interlude: อัลกอริธึมกำลังดุร้ายของ @ 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อัลกอริทึม ( เว็บไซต์ , ข้อมูลเพิ่มเติม , กระดาษ , กระดาษการดำเนินงาน , รหัสอ้างอิง ) เป็นสิ่งที่ดีมากในตอนนี้ ได้รับการออกแบบมาเพื่อเพิ่มประสิทธิภาพระบบที่ซับซ้อนด้วย minima ในพื้นที่ฟังก์ชันการสูญเสียที่มีเสียงดัง / ไม่เชิงเส้น / หลายตัวแปร ฯลฯมันถูกใช้เพื่อปรับแต่งเครื่องมือหมากรุกจะได้รับการใช้ในการปรับแต่งเครื่องยนต์หมากรุกและแตกต่างจากอัลกอริทึมอื่น ๆ อีกมากมายเอกสารที่อธิบายมันสามารถเข้าใจได้จริง (แม้ว่าจะใช้ความพยายามอย่างมากก็ตาม)

การดำเนินงาน:

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

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

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

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

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

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

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

ฉันได้ทำการแก้ไข / ปรับแต่ง SPSA:

  • ใช้ผลลัพธ์ที่ดีที่สุดที่สร้างขึ้นแทนผลสุดท้าย
  • การนำอาร์เรย์ทั้งหมด ( deltas, highArgs, lowArgs) แทนการสร้างพวกเขาด้วยซ้ำ
  • การใช้อาร์เรย์ของค่าสำหรับaแทนค่าเดียว เนื่องจากตัวกรองทั้งหมดแตกต่างกันดังนั้นจึงควรเคลื่อนที่ / บรรจบกันด้วยความเร็วที่ต่างกัน
  • เรียกใช้fixฟังก์ชันหลังจากการทำซ้ำแต่ละครั้ง จะยึดค่าทั้งหมดให้อยู่ระหว่าง 0% ถึง 100% ยกเว้นsaturate(โดยที่ค่าสูงสุดคือ 7500%) brightnessและcontrast(โดยที่ค่าสูงสุดคือ 200%) และhueRotate(โดยที่ค่าจะถูกพันรอบแทนการยึด)

ฉันใช้ SPSA ในกระบวนการสองขั้นตอน:

  1. พื้นที่งาน "กว้าง" ที่พยายาม "สำรวจ" พื้นที่ค้นหา จะทำการทดลอง SPSA แบบ จำกัด หากผลลัพธ์ไม่เป็นที่น่าพอใจ
  2. สเตจ "แคบ" ซึ่งให้ผลลัพธ์ที่ดีที่สุดจากสเตจกว้างและพยายาม "ปรับแต่ง" จะใช้ค่าแบบไดนามิกสำหรับและ

การดำเนินงาน:

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ให้ปรับa ให้เหมาะสม
  • ใช้--debugแฟล็กหากคุณต้องการดูผลลัพธ์ของการวนซ้ำแต่ละครั้ง

TL; DR


3
สรุปขั้นตอนการพัฒนาได้ดีมาก! คุณอ่านความคิดของฉันหรือไม่!
Dave

1
@ เดฟที่จริงฉันทำงานนี้ด้วยตัวเอง แต่คุณเอาชนะฉันไปได้
MultiplyByZer0


3
นี่เป็นวิธีการที่บ้าอย่างสมบูรณ์ คุณสามารถกำหนดสีได้โดยตรงโดยใช้ตัวกรอง SVG (คอลัมน์ที่ห้าใน feColorMatrix) และคุณสามารถอ้างอิงตัวกรองนั้นจาก CSS ได้ - ทำไมคุณถึงไม่ใช้วิธีนั้น
Michael Mullany

2
@MichaelMullany นั่นเป็นเรื่องน่าอายสำหรับฉันเมื่อพิจารณาว่าฉันทำงานนี้มานานแค่ไหน ฉันไม่ได้คิดถึงวิธีการของคุณ แต่ตอนนี้ฉันเข้าใจแล้ว - ในการเปลี่ยนสีองค์ประกอบเป็นสีใดก็ได้คุณเพียงแค่สร้าง SVG แบบไดนามิกโดยมี a <filter>ที่<feColorMatrix>มีค่าที่เหมาะสม (ศูนย์ทั้งหมดยกเว้นคอลัมน์สุดท้ายซึ่งมี RGB เป้าหมาย ค่า 0 และ 1) แทรก SVG ลงใน DOM และอ้างอิงตัวกรองจาก CSS โปรดเขียนคำตอบของคุณเป็นคำตอบ (พร้อมตัวอย่าง) แล้วฉันจะโหวตให้
MultiplyByZer0

55

นี่เป็นการเดินทางลงโพรงกระต่าย แต่นี่มัน!

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

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

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

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

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

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

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

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

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

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

function generateColors() {
	let possibleColors = [];

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

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

					possibleColors.push(colorObject);
				}
			}
		}
	}

	return possibleColors;
}

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

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

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

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

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

แก้ไข:โซลูชันนี้ไม่ได้มีไว้สำหรับใช้ในการผลิตและแสดงให้เห็นถึงแนวทางที่สามารถนำไปใช้เพื่อให้บรรลุสิ่งที่ OP ต้องการเท่านั้น ในบางพื้นที่ของสเปกตรัมสีจะอ่อนลง ผลลัพธ์ที่ดีขึ้นสามารถทำได้โดยละเอียดมากขึ้นในการทำซ้ำขั้นตอนหรือโดยการใช้ฟังก์ชั่นกรองมากขึ้นสำหรับเหตุผลที่อธิบายในรายละเอียดใน@ คำตอบของ

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 กล่าวว่า [เราไม่ควร] "สนใจว่าจะเกิดอะไรขึ้นกับสีอื่นที่ไม่ใช่สีดำ"
  • รองรับเบราว์เซอร์ (ฉันไม่แน่ใจเกี่ยวกับเรื่องนี้ทดสอบภายใต้ latests FF และ chrome เท่านั้น)

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

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


/* the content */

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

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

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


1
ฉลาดสุดยอด! สิ่งนี้ได้ผลสำหรับฉันขอขอบคุณ
jaminroe

ฉันเชื่อว่านี่เป็นทางออกที่ดีกว่าเนื่องจากมีความแม่นยำ 100% กับสีทุกครั้ง
user835542

Code as-is แสดงหน้าว่าง (W10 FF 69b) ไม่มีอะไรผิดปกติกับไอคอน (เลือก SVG แยกต่างหาก)
Rene van der Lende

การเพิ่มbackground-color: black;เพื่อ.icon>spanทำให้งานนี้สำหรับ FF 69b อย่างไรก็ตามไม่แสดงไอคอน
Rene van der Lende

@RenevanderLende เพิ่งลอง FF70 ยังใช้งานได้ที่นั่น ถ้ามันไม่ได้ผลสำหรับคุณมันจะต้องเป็นสิ่งที่คุณต้องทำ
Kaiido

15

คุณสามารถทำให้ทั้งหมดนี้ง่ายมากโดยใช้ตัวกรอง SVG ที่อ้างอิงจาก CSS คุณต้องการเพียง feColorMatrix เดียวในการเปลี่ยนสี อันนี้เปลี่ยนเป็นสีเหลือง คอลัมน์ที่ห้าใน feColorMatrix เก็บค่าเป้าหมาย RGB ในหน่วยสเกล (สำหรับสีเหลือง - คือ 1,1,0)

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


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


วิธีแก้ปัญหาที่น่าสนใจ แต่ดูเหมือนว่าจะไม่อนุญาตให้ควบคุมสีเป้าหมายผ่าน CSS
glebm

คุณต้องกำหนดฟิลเตอร์ใหม่สำหรับแต่ละสีที่คุณต้องการใช้ แต่ก็ถูกต้องครบถ้วน hue-rot เป็นการประมาณที่หนีบสีบางสีซึ่งหมายความว่าคุณไม่สามารถใช้สีบางสีได้อย่างถูกต้องตามที่คำตอบข้างต้นยืนยัน สิ่งที่เราต้องการจริงๆคือชวเลขตัวกรองสีใหม่ () CSS
Michael Mullany

คำตอบของ MultiplyByZer0 จะคำนวณชุดของตัวกรองที่ได้รับความแม่นยำสูงมากโดยไม่ต้องแก้ไข HTML ความจริงhue-rotateในเบราว์เซอร์จะดีมาก
glebm

2
ดูเหมือนว่าสิ่งนี้จะสร้างสี RGB ที่แม่นยำสำหรับภาพต้นฉบับสีดำเท่านั้นเมื่อคุณเพิ่ม "color-interpolation-filters" = "sRGB" ให้กับ feColorMatrix
John Smith

Edge 12-18 ถูกปล่อยทิ้งไว้เนื่องจากไม่รองรับurlฟังก์ชันcaniuse.com/#search=svg%20filter
Volker E.

2

ฉันสังเกตเห็นว่าตัวอย่างของการรักษาผ่านตัวกรอง SVG ไม่สมบูรณ์ฉันเขียนของฉัน (ซึ่งทำงานได้ดี): (ดูคำตอบของ Michael Mullany) ดังนั้นนี่คือวิธีที่จะได้สีที่คุณต้องการ:

นี่คือวิธีที่สองโดยใช้ SVG Filter เฉพาะใน code => URL.createObjectURL


1

เพียงแค่ใช้

fill: #000000

fillคุณสมบัติใน CSS เป็นสำหรับการกรอกในสีของรูปร่าง SVG fillคุณสมบัติสามารถยอมรับค่าสี CSS ใด ๆ


3
สิ่งนี้อาจใช้ได้กับ CSS ภายในกับรูปภาพ SVG แต่ไม่ได้ผลเนื่องจาก CSS ที่ใช้ภายนอกกับimgองค์ประกอบโดยเบราว์เซอร์
David Moles

1

ฉันเริ่มต้นด้วยคำตอบนี้โดยใช้ตัวกรอง svg และทำการแก้ไขต่อไปนี้:

SVG กรองจาก URL ข้อมูล

หากคุณไม่ต้องการกำหนดตัวกรอง SVG ที่ใดที่หนึ่งในมาร์กอัปของคุณคุณสามารถใช้URL ข้อมูลแทน (แทนที่R , G , BและAด้วยสีที่ต้องการ):

filter: url('data:image/svg+xml;utf8,\
  <svg xmlns="http://www.w3.org/2000/svg">\
    <filter id="recolor" color-interpolation-filters="sRGB">\
      <feColorMatrix type="matrix" values="\
        0 0 0 0 R\
        0 0 0 0 G\
        0 0 0 0 B\
        0 0 0 A 0\
      "/>\
    </filter>\
  </svg>\
  #recolor');

ทางเลือกสีเทา

หากเวอร์ชันด้านบนใช้ไม่ได้คุณสามารถเพิ่มทางเลือกสีเทาได้ด้วย

saturateและbrightnessฟังก์ชั่นเปิดสีใดสีดำ (คุณไม่จำเป็นต้องรวมว่าถ้าสีเป็นสีดำอยู่แล้ว) invertแล้วสดใสกับความสว่างที่ต้องการ ( L ) และเลือกที่คุณยังสามารถระบุความทึบ ( )

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

SCSS mixin

หากคุณต้องการระบุสีแบบไดนามิกคุณสามารถใช้ SCSS mixin ต่อไปนี้:

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

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

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

ตัวอย่างการใช้งาน:

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

ข้อดี:

  • จาวาสคริปต์
  • ไม่มีองค์ประกอบ HTML เพิ่มเติม
  • หากฟิลเตอร์ CSS ได้รับการสนับสนุน แต่กรอง SVG ไม่ทำงานมีทางเลือกโทนสีเทา
  • หากคุณใช้มิกซ์อินการใช้งานจะค่อนข้างตรงไปตรงมา (ดูตัวอย่างด้านบน)
  • สีสามารถอ่านได้ง่ายกว่าและปรับเปลี่ยนได้ง่ายกว่าเคล็ดลับซีเปีย (ส่วนประกอบ RGBA ใน CSS บริสุทธิ์และคุณสามารถใช้สี HEX ใน SCSS ได้ด้วย)
  • หลีกเลี่ยงพฤติกรรมแปลก ๆ ของhue-rotate .

คำเตือน:

  • เบราว์เซอร์บางประเภทไม่รองรับตัวกรอง SVG จาก URL ข้อมูล (โดยเฉพาะแฮช id) แต่ใช้งานได้ในเบราว์เซอร์ Firefox และ Chromium ปัจจุบัน (และอาจจะอื่น ๆ )
  • หากคุณต้องการระบุสีแบบไดนามิกคุณต้องใช้ SCSS mixin
  • เวอร์ชัน Pure CSS ค่อนข้างน่าเกลียดหากคุณต้องการสีที่แตกต่างกันคุณต้องรวม SVG หลาย ๆ ครั้ง

1
โอ้นั่นสมบูรณ์แบบนี่คือสิ่งที่ฉันกำลังมองหาซึ่งจะใช้ทุกอย่างใน SASS สุดยอดมากขอบคุณมาก!
ghiscoding

1
@ghiscoding ฉันดีใจที่ได้ช่วย!
เดวิดโดสทัล
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.