ewino / Colorful Google Docs FavIcon

// ==UserScript==
// @name         Colorful Google Docs FavIcon
// @namespace    https://openuserjs.org/users/ewino
// @version      0.95
// @description  Color the icon on a Google Docs (document/spreadsheet/presentation) tab according to the specific file (different colors for different files).
// @copyright    2023, ewino (https://openuserjs.org/users/ewino)
// @author       ewino
// @license      MIT
// @match        https://docs.google.com/document/d/*
// @match        https://docs.google.com/spreadsheets/d/*
// @match        https://docs.google.com/presentation/d/*
// @icon         https://www.google.com/s2/favicons?domain=google.com
// @grant        none
// ==/UserScript==

// ==OpenUserJS==
// @author ewino
// ==/OpenUserJS==

/* jshint esversion: 6 */

(function () {
    'use strict';
    const globals = {};

    const percentOfIconColored = 60;

    const persistColor = (color) => localStorage.setItem('color-' + location.pathname, color);
    const fetchPersistedColor = () => localStorage.getItem('color-' + location.pathname);

    const getMostCommonColorHex = (imageData) => {
        const colorCounts = new Map();
        let mostCommonColor = null;
        let mostCommonColorCount = 0;

        for (var i = 0; i < imageData.data.length; i += 4) {
            var pixelColor = rgbToHex([imageData.data[i], imageData.data[i + 1], imageData.data[i + 2]]);
            if (!colorCounts.has(pixelColor)) {
                colorCounts.set(pixelColor, 0);
            }

            const newCount = colorCounts.get(pixelColor) + 1;
            colorCounts.set(pixelColor, newCount);
            if (newCount > mostCommonColorCount) {
                mostCommonColorCount = newCount;
                mostCommonColor = pixelColor;
            }
        }

        return mostCommonColor;
    };

    const replaceColorHex = (imageData, originalColorHex, newColorHex) => {
        const originalColorRgb = hexToRgb(originalColorHex);
        const newColorRgb = hexToRgb(newColorHex);
        // Examine every pixel, change any old rgb to the new-rgb
        for (var i = 0; i < imageData.data.length; i += 4) {
            // is this pixel the old rgb?
            if (imageData.data[i] == originalColorRgb[0] && imageData.data[i + 1] == originalColorRgb[1] && imageData.data[i + 2] == originalColorRgb[2]
               ) {
                // change to your new rgb
                imageData.data[i] = newColorRgb[0];
                imageData.data[i + 1] = newColorRgb[1];
                imageData.data[i + 2] = newColorRgb[2];
            }
        }
    };

    const rgbToHex = (rgb) => '#' + ((rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).padStart(6, '0');

    const hexToRgb = (hex) => [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)];

    const hashStringToColor = (str) => {
        var hash = 0;
        if (str.length === 0) return hash;
        for (let i = 0; i < str.length; i++) {
            hash = str.charCodeAt(i) + ((hash << 5) - hash);
            hash = hash & hash;
        }
        hash = hash % Math.pow(256, 3);
        var rgb = [0, 0, 0];
        for (let i = 0; i < 3; i++) {
            var value = (hash >> (i * 8)) & 255;
            rgb[i] = value;
        }
                console.log('hash', hash, rgb);
        return rgbToHex(rgb);
    };

    const refreshCustomColor = (newCustomColor) => {
        globals.customColor = newCustomColor;
        globals.canvasCtx.drawImage(globals.origFavIconImg, 0, 0);
        const startX = 0;
        const endX = globals.origFavIconImg.width * percentOfIconColored / 100;
        const imageData = globals.canvasCtx.getImageData(startX, 0, endX, globals.origFavIconImg.height);
        replaceColorHex(imageData, globals.baseColor, newCustomColor);
        globals.canvasCtx.putImageData(imageData, startX, 0);
        lnk.href = globals.canvas.toDataURL();

        globals.cornerEl.style.backgroundColor = newCustomColor;
        console.log('tab color for favicon is', newCustomColor);
    };


    const init = (img) => {
        const canvas = document.createElement('canvas');
        canvas.height = img.height;
        canvas.width = img.width;
        var ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0);
        globals.origFavIconImg = img;
        globals.canvas = canvas;
        globals.canvasCtx = ctx;
        globals.baseColor = getMostCommonColorHex(ctx.getImageData(0, 0, img.width, img.height));
        globals.customColor = 'transparent';

        var colorPicker = document.createElement('input');
        colorPicker.type = 'color';
        colorPicker.style = 'display: none;';
        colorPicker.addEventListener('change', (e) => {
            persistColor(colorPicker.value)
            refreshCustomColor(colorPicker.value);
        });
        var corner = document.createElement('div');
        corner.setAttribute('style', 'width: 20px; height: 20px; position: absolute; top: -10px; right: -10px; transform: rotate(45deg); z-index: 100000; cursor: pointer;');
        corner.setAttribute('title', 'Change FavIcon Color');
        document.body.appendChild(corner);
        document.body.appendChild(colorPicker);
        corner.addEventListener('click', () => {
            colorPicker.focus();
            colorPicker.value = globals.customColor;
            colorPicker.click();
        });
        globals.cornerEl = corner;
        refreshCustomColor(fetchPersistedColor() || hashStringToColor(location.pathname));
    }

    const lnk = document.querySelector('link[rel*="icon"]');
    if (lnk) {
        const img = new Image();
        img.crossOrigin = "anonymous";
        img.src = lnk.href;
        img.onload = function () { init(img); }
    }
})();