Colorify - Steam Profiles

Tries to put similar colors to other parts of steam according to the theme current profile have

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Colorify - Steam Profiles
// @namespace    tech.kobb.steam.colorify
// @version      0.1
// @description  Tries to put similar colors to other parts of steam according to the theme current profile have
// @author       kb
// @match        *://*.steamcommunity.com/id/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=steamcommunity.com
// @grant        none
// @license      GNU GPLv3
// @run-at       document-idle
// ==/UserScript==

(async function() {
    'use strict';

    // url check
    if(document.querySelector('.error_ctn')) return; // id exists
    let p = location.pathname.split('/').splice(1); if(p[p.length-1] === "") p.pop();
    if(p[0].toLowerCase() !== 'id' || p.length !== 2) return // valid path
    p=undefined;

    //other checks
    if(getComputedStyle(document.body).getPropertyValue('--btn-outline') === '') return; // no theme applied

    // ------------------------------------------------

    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;
            const sin = Math.sin(angle);
            const cos = Math.cos(angle);

            this.multiply([
                0.213 + cos * 0.787 - sin * 0.213,
                0.715 - cos * 0.715 - sin * 0.715,
                0.072 - cos * 0.072 + sin * 0.928,
                0.213 - cos * 0.213 + sin * 0.143,
                0.715 + cos * 0.285 + sin * 0.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) {
            const newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
            const newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
            const 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.
            const r = this.r / 255;
            const g = this.g / 255;
            const b = this.b / 255;
            const max = Math.max(r, g, b);
            const min = Math.min(r, g, b);
            let h, s, l = (max + min) / 2;

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

                    case g:
                        h = (b - r) / d + 2;
                        break;

                    case b:
                        h = (r - g) / d + 4;
                        break;
                }
                h /= 6;
            }

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

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

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

        solve() {
            const 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++) {
                const initial = [50, 20, 3750, 50, 100, 100];
                const 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;
            const deltas = new Array(6);
            const highArgs = new Array(6);
            const lowArgs = new Array(6);

            for (let k = 0; k < iters; k++) {
                const 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];
                }

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

                const 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 %= 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.
            const 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);

            const 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 `invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%);`;
        }
    }

    function hexToRgb(hex) {
        // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
        const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
        hex = hex.replace(shorthandRegex, (m, r, g, b) => {
            return r + r + g + g + b + b;
        });

        const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
        return result
            ? [
            parseInt(result[1], 16),
            parseInt(result[2], 16),
            parseInt(result[3], 16),
        ]
        : null;
    }

    /**
          /|\       Code made by Barrett Sonntag
         / | \  https://codepen.io/sosuke/pen/Pjoqqp
           |         Credits to this person.
    */


    const getMainColor = () => {
        const raw = getComputedStyle(document.body).getPropertyValue('--btn-outline');
        if(raw.includes("#")) return hexToRgb(raw);
        else if(raw.includes('rgb')) return raw.split('(')[1].split(')')[0].split(', ').map(c => Number(c))
        return null;
    }

    const getFilterColor = () => {
        const col = new Color(...getMainColor())
        const solver = new Solver(col);
        const result = solver.solve();

        return result.filter;
    }

    const filterColor = getFilterColor()







    let finished = false;
    let styles = {}

    const applyStyles = (selector, s) => {
        if(finished) return;

        if(!styles[selector]) styles[selector] = []
        styles[selector].push(...s)
    }

    const finishStyle = () => {
        if(finished) return;
        const styleElem = document.createElement('style')

        let rawStyle = [
            "/*",
            ...[
                "Style autogenerated by userscript",
                "Making steam [a little bit] better (lol)",
                "https://kobb.tech",
                "",
                "~2023"
            ].map(e => `\t${e}`),
            "*/"
        ].join('\n')
        for(let [sel,sty] of Object.entries(styles)){
            rawStyle+=`${sel} {`
            for(let [k,v] of sty) rawStyle+=`${k}: ${v}; `;
            rawStyle+=`}`
        }

        styleElem.innerHTML = rawStyle;
        document.head.appendChild(styleElem);
    }

    applyStyles('div#global_header', [
        ["background", "transparent"],
        ["color", "blue"]
    ])

    applyStyles('div#global_header > div.content', [
        ["background", "transparent"]
    ])

    applyStyles('div#global_header .menuitem.supernav_active, .persona.online, .whiteLink:hover', [
        ['color', 'var(--btn-outline) !important'],
    ])

    applyStyles('div#global_header .menuitem.supernav_active::after, .playerAvatar.online, .modal_top_bar', [
        ['background', 'var(--btn-outline) !important']
    ])

    applyStyles('span[class*="_check"], .pagebtn', [
        ['filter', filterColor]
    ])


    finishStyle();
})();