جافاسكريبت - كيفية تحويل اللون الأسود إلى أي لون محدد باستخدام مرشحات CSS فقط


سؤالي هو: بالنظر إلى لون RGB مستهدف، ما هي الصيغة لإعادة تلوين اللون الأسود (#000) إلى هذا اللون باستخدام فلاتر CSS فقط؟

لكي يتم قبول الإجابة، يجب أن توفر وظيفة (بأي لغة) تقبل اللون المستهدف كوسيطة وتعيد سلسلة filter الخاصة بـ CSS المقابلة.

السياق لذلك هو الحاجة إلى إعادة تلوين SVG داخل background-image. في هذه الحالة، يكون ذلك لدعم بعض ميزات الرياضيات في TeX في KaTeX: https://github.com/Khan/KaTeX/issues/587.

مثال

إذا كان اللون المستهدف هو #ffff00 (أصفر)، فإن أحد الحلول الصحيحة هو:

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

(عرض تجريبي)

الأهداف غير المراد تحقيقها

  • الرسوم المتحركة.
  • حلول غير قائمة على مرشحات 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
    السلبيات: لا يعمل على Edge. يتطلب تغييرات في CSS غير filter وتغييرات بسيطة في 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 غير خطية.

    تطبيقات المتصفح: كروميوم، فايرفوكس.

  • عرض توضيحي: الانتقال من لون رمادي إلى لون غير رمادي: https://stackoverflow.com/a/25524145/181228

  • صيغة تعمل تقريبًا (من سؤال مشابه):
    https://stackoverflow.com/a/29958459/181228

    شرح مفصل عن سبب خطأ الصيغة أعلاه (CSS hue-rotate ليس دوران تدرج اللون الحقيقي ولكنه تقريب خطي):
    https://stackoverflow.com/a/19325417/2441511

الإجابة رقم 1

@Dave كان أول من نشر إجابة على هذا (مع كود يعمل)، وكانت إجابته مصدرًا لا يقدر بثمن من النسخ واللصق بلا خجل الإلهام بالنسبة لي. بدأ هذا المنشور كمحاولة لشرح وتحسين إجابة @Dave، لكنه تطور منذ ذلك الحين ليصبح إجابة مستقلة بحد ذاته.

طَرِيقَتِي أسرع بشكل كبير. وفقًا لمقياس jsPerf على ألوان RGB المُولَّدة عشوائيًا، يعمل خوارزم @Dave في 600 مللي ثانية، بينما يعمل طريقتي في 30 مللي ثانية. هذا يمكن أن يكون مهمًا بالتأكيد، على سبيل المثال في وقت التحميل، حيث تكون السرعة حرجة.

علاوة على ذلك، بالنسبة لبعض الألوان، يقوم خوارزميتي بالأداء بشكل أفضل:

  • بالنسبة لـ rgb(0,255,0)، يُنتج @Dave's rgb(29,218,34) وأنا أنتج rgb(1,255,0)
  • بالنسبة لـ rgb(0,0,255)، يُنتج @Dave 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.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 rgb(...).
    • تُرجع دالتها hsl() اللون، محولاً إلى HSL.
    • وظيفته clamp() تضمن أن تكون قيمة اللون المعطاة ضمن الحدود (0-255).
  • ستحاول فئة Solver حل اللون المستهدف.

    • تُرجع دالتها css() فلترًا معينًا في سلسلة فلتر CSS.

تنفيذ grayscale() و sepia() و saturate()

جوهر مرشحات CSS/SVG هو البدائل المرشحة، التي تمثل تعديلات منخفضة المستوى على الصورة.

يتم تنفيذ المرشحات grayscale() وsepia() وsaturate() بواسطة العنصر الأولي للفلتر <feColorMatrix>، والذي يقوم بعملية ضرب مصفوفة بين مصفوفة محددة بواسطة الفلتر (غالبًا ما يتم توليدها ديناميكيًا)، ومصفوفة مُنشأة من اللون. الرسم البياني:

هناك بعض التحسينات التي يمكننا إجراؤها هنا:

  • العنصر الأخير من مصفوفة الألوان هو وسيظل دائمًا 1. لا فائدة من حسابه أو تخزينه.
  • لا فائدة من حساب أو تخزين قيمة ألفا/الشفافية (A) أيضًا، لأننا نتعامل مع RGB، وليس RGBA.
  • لذلك، يمكننا تقليص مصفوفات الفلتر من 5x5 إلى 3x5، ومصفوفة الألوان من 1x5 إلى 1x3. هذا يوفر بعض العمل.
  • جميع مرشحات <feColorMatrix> تترك العمودين 4 و5 كأصفار. لذلك، يمكننا تقليل مصفوفة المرشح أكثر إلى 3x3.
  • نظرًا لأن عملية الضرب بسيطة نسبيًا، لا توجد حاجة لاستدعاء مكتبات رياضية معقدة من أجل هذا. يمكننا تنفيذ خوارزمية ضرب المصفوفات بأنفسنا.

التنفيذ:

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

(نستخدم متغيرات مؤقتة للاحتفاظ بنتائج ضرب كل صف، لأننا لا نريد أن تؤثر التغييرات على this.r، وما إلى ذلك، في الحسابات اللاحقة.)

الآن بعد أن قمنا بتطبيق <feColorMatrix>، يمكننا تطبيق grayscale()، وsepia()، وsaturate()، والتي تستدعيه ببساطة مع مصفوفة الفلتر المعطاة:

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

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

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

تنفيذ hue-rotate()

يتم تنفيذ فلتر hue-rotate() بواسطة <feColorMatrix type="hueRotate" />.

يتم حساب مصفوفة الفلتر كما هو موضح أدناه:

على سبيل المثال، سيتم حساب العنصر a00 بهذه الطريقة:

ملاحظات :

  • زاوية الدوران تُعطى بالدرجات، ويجب تحويلها إلى راديان قبل تمريرها إلى 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" /> يقبل سمة slope وسمة intercept. ثم يحسب كل قيمة لون جديدة من خلال صيغة بسيطة:

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 قيم (أي، v0 إلى vn) تحدد القيم الابتدائية والنهائية لـ n مناطق استيفاء متساوية الحجم. وتستخدم الاستيفاءات الصيغة التالية:

بالنسبة للقيمة C، جد k بحيث:

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

النتيجة C' تُعطى بواسطة:

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

شرح هذه الصيغة:

  • يعرف الفلتر invert() هذا الجدول: [القيمة، 1 - القيمة]. هذه هي tableValues أو v.
  • تعرف الصيغة n، بحيث أن n + 1 هي طول الجدول. وبما أن طول الجدول هو 2، فإن n=1.
  • تُحدد الصيغة k، حيث أن k و k + 1 هما فهارس الجدول. بما أن الجدول يحتوي على عنصرين، فإن k=0.

وبالتالي، يمكننا تبسيط الصيغة إلى:

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

عند دمج قيم الجدول في النص، يبقى لدينا:

C'=القيمة + C * (1 - القيمة - القيمة)

تبسيط آخر:

C'=القيمة + C * (1 - 2 * القيمة)

يحدد المعيار C وC' كقيم RGB، ضمن النطاق 0-1 (على عكس 0-255). ونتيجة لذلك، يجب علينا تصغير القيم قبل الحساب، وإعادتها للتكبير بعد ذلك.

وهكذا نصل إلى تنفيذنا:

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

فاصل: خوارزمية القوة الغاشمة لديف

كود @Dave يولد 176,660 تركيبة من المرشحات، بما في ذلك:

  • 11 مرشحات invert() (0٪، 10٪، 20٪، ...، 100٪)
  • 11 sepia() فلاتر (0٪، 10٪، 20٪، ...، 100٪)
  • 20 فلاتر saturate() (5%، 10%، 15%، ...، 100%)
  • 73 مرشحات hue-rotate() (0°، 5°، 10°، ...، 360°)

يقوم بحساب الفلاتر بالترتيب التالي:

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

ثم يقوم بالتكرار عبر جميع الألوان المحسوبة. يتوقف بمجرد أن يجد لونًا مولدًا داخل التسامح (جميع قيم RGB ضمن 5 وحدات من اللون المستهدف).

ومع ذلك، هذا بطيء وغير فعال. لذلك، أقدم إجابتي الخاصة.

تنفيذ SPSA

أولاً، يجب علينا تعريف دالة الخسارة، التي تُرجع الفرق بين اللون الناتج عن مجموعة من الفلاتر واللون المستهدف. إذا كانت الفلاتر مثالية، يجب أن تعيد دالة الخسارة القيمة 0.

سنقوم بقياس فرق اللون كمجموع لمؤشرين:

  • فرق RGB، لأن الهدف هو إنتاج أقرب قيمة RGB.
  • فرق HSL، لأن العديد من قيم HSL تتوافق مع المرشحات (على سبيل المثال، اللون تقريبا يرتبط بـ hue-rotate()، والتشبع يرتبط بـ 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:

  • باستخدام أفضل نتيجة تم إنتاجها، بدلاً من الأخيرة.
  • إعادة استخدام جميع المصفوفات (deltas، highArgs، lowArgs)، بدلاً من إعادة إنشائها مع كل تكرار.
  • استخدام مصفوفة من القيم للـ a، بدلاً من قيمة واحدة. وذلك لأن جميع المرشحات مختلفة، وبالتالي يجب أن تتحرك/تتقارب بسرعات مختلفة.
  • تشغيل دالة fix بعد كل تكرار. فهي تقيد جميع القيم بين 0٪ و 100٪، باستثناء saturate (حيث الحد الأقصى هو 7500٪)، وbrightness وcontrast (حيث الحد الأقصى هو 200٪)، وhueRotate (حيث تُلتف القيم بدلاً من أن تُقيد).

أستخدم SPSA في عملية من مرحلتين:

  1. المرحلة "الواسعة"، التي تحاول "استكشاف" فضاء البحث. ستقوم بمحاولات محدودة لـ SPSA إذا لم تكن النتائج مرضية.
  2. المرحلة "الضيقة"، التي تأخذ أفضل نتيجة من المرحلة الواسعة وتحاول "تنقيحها". تستخدم قيمًا ديناميكية لــ A و a.

التنفيذ:

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

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

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

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

ضبط SPSA

تحذير: لا تعبث بشفرة SPSA، خاصة بالثوابت الخاصة بها، إلا إذا كنت متأكدًا من أنك تعرف ما تفعله.

الثوابت المهمة هي A، a، c، القيم الأولية، حدود المحاولة مرة أخرى، قيم max في fix()، وعدد التكرارات في كل مرحلة. جميع هذه القيم تم ضبطها بعناية لإنتاج نتائج جيدة، والتلاعب العشوائي بها سيقلل على الأرجح من فائدة الخوارزمية.

إذا أصررت على تغييره، يجب أن تقيس قبل أن "تحسّن".

أولاً، قم بتطبيق هذا التصحيح.

ثم شغّل الكود في Node.js. بعد فترة من الوقت، يجب أن تكون النتيجة شيئًا مثل هذا:

Average loss: 3.4768521401985275
Average time: 11.4915ms

الآن اضبط الثوابت بما يرضي قلبك.

بعض النصائح:

  • يجب أن يكون متوسط الخسارة حوالي 4. إذا كان أكبر من 4، فإنه ينتج نتائج بعيدة جدًا عن المطلوب، ويجب أن تقوم بضبط الدقة. إذا كان أقل من 4، فإنه يهدر الوقت، ويجب عليك تقليل عدد التكرارات.
  • إذا زدت/قللت عدد التكرارات، قم بضبط A بشكل مناسب.
  • إذا قمت بزيادة/تقليل A، قم بضبط a بشكل مناسب.
  • استخدم العلم --debug إذا كنت تريد رؤية نتيجة كل تكرار.

ملخص

الإجابة رقم 2

كانت هذه رحلة ممتعة في عمق الموضوع، ولكنها هنا!

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.

تحرير2: السائل يبحث عن حل غير قائم على القوة الغاشمة. في هذه الحالة الأمر بسيط جدًا، فقط حل هذه المعادلة:

أين

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

الإجابة رقم 3

ملاحظة : طلب مني صاحب السؤال استعادة الحذف، لكن الجائزة ستكون لإجابة ديف.


أعلم أن هذا ليس ما طُلب في جسم السؤال، وبالتأكيد ليس ما كنا جميعاً ننتظره، لكن هناك فلتر CSS واحد يفعل هذا تماماً: drop-shadow()

تنبيهات :

  • يتم رسم الظل خلف المحتوى الموجود. هذا يعني أنه علينا القيام ببعض الحيل باستخدام التمركز المطلق.
  • سيتم التعامل مع جميع البكسلات بنفس الطريقة، لكن OP قال [يجب ألا نقوم بـ] "الاهتمام بما يحدث للألوان غير الأسود."
  • دعم المتصفح. (لست متأكداً من ذلك، اختبرت فقط على أحدث إصدارات فايرفوكس وكروم).

/* 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>

الإجابة رقم 4

بدأت بهذا الجواب باستخدام مرشح SVG وقمت بالتعديلات التالية:

فلتر SVG من رابط بيانات

إذا كنت لا ترغب في تحديد مرشح SVG في مكان ما في التعليمات البرمجية الخاصة بك، يمكنك استخدام رابط بيانات بدلاً من ذلك (استبدل 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);
}

المزايا:

  • لا يوجد جافا سكريبت.
  • لا عناصر HTML إضافية.
  • إذا كانت فلاتر CSS مدعومة، ولكن فلتر SVG لا يعمل، يوجد حل بديل للتدرج الرمادي.
  • إذا استخدمت الميكسين، يكون الاستخدام بسيطًا جدًا (انظر المثال أعلاه).
  • اللون أكثر قابلية للقراءة وأسهل في التعديل من خدعة القسم (مكونات RGBA في CSS النقي ويمكنك حتى استخدام ألوان HEX في SCSS).
  • يتجنب السلوك الغريب لـ hue-rotate.

تنبيهات :

  • ليس كل المتصفحات تدعم فلاتر SVG من عنوان بيانات URL (خصوصًا هاش المعرف)، لكنها تعمل في متصفحات فايرفوكس وكروميوم الحالية (وربما في الأخرى أيضًا).
  • إذا كنت تريد تحديد اللون بشكل ديناميكي، عليك استخدام مزيج SCSS.
  • نسخة CSS النقية قبيحة قليلاً، إذا كنت تريد العديد من الألوان المختلفة، عليك إدراج SVG عدة مرات.

الإجابة رقم 5

يمكنك جعل كل هذا بسيطًا جدًا بمجرد استخدام مرشح 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">