Colorify - Steam Profiles

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

// ==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();
})();