florr.io | Text & Color Changer

Redecorate florr.io with your style.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         florr.io | Text & Color Changer
// @namespace    Hai
// @version      2.1.3
// @description  Redecorate florr.io with your style.
// @author       Furaken
// @match        https://florr.io/*
// @license      MIT
// @grant        GM_addStyle
// ==/UserScript==

function addAlpha(color, opacity) {
    opacity = Math.round(Math.min(Math.max(opacity ?? 1, 0), 1) * 255);
    return color + opacity.toString(16).toUpperCase();
}

function componentToHex(c) {
    let hex = c.toString(16);
    return hex.length == 1 ? "0" + hex : hex;
}

function rgbToHex(r, g, b) {
    return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}

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

function createEle(type, parent, style, innerHTML, id, className) {
    let ele = document.createElement(type)
    if (type) ele.type = type
    if (style) ele.style = style
    if (innerHTML) ele.innerHTML = innerHTML
    if (id) ele.id = id
    if (className) ele.className = className
    parent.appendChild(ele)
    return ele
}

let ls = localStorage.customizer || JSON.stringify({
    color: {
        from: [],
        to: [],
    },
    text: []
})

function errorMessage(error) {
    console.log(error)
    alert(`${error}\n\n${JSON.stringify(ls, null, 4)}`)
}

function syntaxHighlight(json) { // https://stackoverflow.com/questions/4810841/pretty-print-json-using-javascript
    if (typeof json != 'string') {
        json = JSON.stringify(json, undefined, 2);
    }
    json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
        var cls = 'number';
        if (/^"/.test(match)) {
            if (/:$/.test(match)) {
                cls = 'key';
            } else {
                cls = 'string';
            }
        } else if (/true|false/.test(match)) {
            cls = 'boolean';
        } else if (/null/.test(match)) {
            cls = 'null';
        }
        return '<span class="' + cls + '">' + match + '</span>';
    });
}

try {ls = JSON.parse(ls)}
catch (error) {errorMessage(error)}

let closeButtonSvg = "data:image/svg+xml;base64,PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KDTwhLS0gVXBsb2FkZWQgdG86IFNWRyBSZXBvLCB3d3cuc3ZncmVwby5jb20sIFRyYW5zZm9ybWVkIGJ5OiBTVkcgUmVwbyBNaXhlciBUb29scyAtLT4KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0iIzAwMDAwMCI+Cg08ZyBpZD0iU1ZHUmVwb19iZ0NhcnJpZXIiIHN0cm9rZS13aWR0aD0iMCIvPgoNPGcgaWQ9IlNWR1JlcG9fdHJhY2VyQ2FycmllciIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cg08ZyBpZD0iU1ZHUmVwb19pY29uQ2FycmllciI+Cg08cGF0aCBmaWxsPSIjZmZmZmZmY2MiIGQ9Ik0xOTUuMiAxOTUuMmE2NCA2NCAwIDAgMSA5MC40OTYgMEw1MTIgNDIxLjUwNCA3MzguMzA0IDE5NS4yYTY0IDY0IDAgMCAxIDkwLjQ5NiA5MC40OTZMNjAyLjQ5NiA1MTIgODI4LjggNzM4LjMwNGE2NCA2NCAwIDAgMS05MC40OTYgOTAuNDk2TDUxMiA2MDIuNDk2IDI4NS42OTYgODI4LjhhNjQgNjQgMCAwIDEtOTAuNDk2LTkwLjQ5Nkw0MjEuNTA0IDUxMiAxOTUuMiAyODUuNjk2YTY0IDY0IDAgMCAxIDAtOTAuNDk2eiIvPgoNPC9nPgoNPC9zdmc+"
let ctx = document.getElementById("canvas").getContext("2d")
let message =
    `This script is made by Furaken (discord username: <w style="color: #15b1d6">samerkizi</w>)
Toggle this menu with keybind: <w style="color: #f5945d">Shift \`</w>
Join my discord server: <w style="color: #5567f1; cursor:pointer;" onclick='window.open("https://discord.gg/tmWUfg4FR9")'>https://discord.gg/tmWUfg4FR9</w>`

let container = createEle(
    "div",
    document.querySelector("body"),
    `
    margin: 0;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) scale(1);
    width: 80%;
    height: 80%;
    display: flex;
    z-index: 999;
    transition: all 0.4s ease-in-out;
    font-family: 'Ubuntu';
    color: white;
    text-shadow: rgb(0 0 0) 2px 0px 0px, rgb(0 0 0) 1.75517px 0.958851px 0px, rgb(0 0 0) 1.0806px 1.68294px 0px, rgb(0 0 0) 0.141474px 1.99499px 0px, rgb(0 0 0) -0.832294px 1.81859px 0px, rgb(0 0 0) -1.60229px 1.19694px 0px, rgb(0 0 0) -1.97998px 0.28224px 0px, rgb(0 0 0) -1.87291px -0.701566px 0px, rgb(0 0 0) -1.30729px -1.5136px 0px, rgb(0 0 0) -0.421592px -1.95506px 0px, rgb(0 0 0) 0.567324px -1.91785px 0px, rgb(0 0 0) 1.41734px -1.41108px 0px, rgb(0 0 0) 1.92034px -0.558831px 0px;
    `,
    `
    <div style="background: #333;border-radius: 10px;box-shadow: 5px 5px rgba(0, 0, 0, 0.3);padding: 15px;display: flex; width: 100%;">
        <div style="display: flex; flex-direction: column; width: 75%;">
            <div style="height: 100%; background: #00000050; border-radius: 10px; position: sticky; display: flex; flex-direction: column;">
                <div id="editLabel" style="text-align:center;background: #75ba75;padding: 10px;top: 0;left: 0;border-radius: 10px 10px 0 0;position: sticky;z-index: 1;">COLOR CHANGER</div>
                <div id="con_edit" style="white-space: break-spaces; background: #00000030; padding: 15px; border-radius:0 0 5px 5px;font-family:'Space Mono', monospace;overflow: hidden auto;word-wrap: break-word; padding: 15px; position: relative; height: 100%;">${message}<br><br><br>${syntaxHighlight(JSON.stringify(ls, null, 4))}</div>
                <div style="padding: 10px;position: sticky;z-index: 1;height: 40px;display: flex;font-size: 12px;text-align: center;line-height: 27px;">
                    <div id="con_edit_button_text" class="button" style="overflow:hidden;position: relative;height: 100%;width: fit-content;padding: 0 15px; border-radius: 3px;border: solid #ce6529 4px;box-sizing: border-box;background: #f5945d;">Text changer</div>
                    <div id="con_edit_button_appendTheme" class="button" style="overflow:hidden;position: relative;height: 100%;width: fit-content;padding: 0 15px; border-radius: 3px;border: solid #4b7a4b 4px;box-sizing: border-box;margin-left: 3px;background: #75ba75;">Append</div>
                    <div id="con_edit_button_copyJSON" class="button" style="overflow:hidden;position: relative;height: 100%;width: fit-content;padding: 0 15px;border-radius: 3px;border: solid #4b837e 4px;box-sizing: border-box;margin-left: 3px;background: #6dbfb8;">Copy JSON</div>
                    <div id="con_edit_button_viewJSON" class="button" style="overflow:hidden;position: relative;height: 100%;width: fit-content;padding: 0 15px;border-radius: 3px;border: solid #4b837e 4px;box-sizing: border-box;margin-left: 3px;background: #6dbfb8;">View JSON</div>
                    <div id="con_edit_button_deleteTheme" class="button" style="overflow:hidden;position: relative;height: 100%;width: fit-content;padding: 0 15px;border-radius: 3px;border: solid #974545 4px;box-sizing: border-box;margin-left: 3px;background: #BB5555;">Reset all</div>
                </div>
            </div>
        </div>
        <div style="display: flex; flex-direction: column; width: 25%; margin-left: 5px; font-size: 14px;">
            <div style="height: 100%; background: #00000050; border-radius: 10px; position: sticky; display: flex; flex-direction: column;">
                <div style="text-align:center;background: #6dbfb8;padding: 12px;top: 0;border-radius: 10px 10px 0 0;position: sticky;z-index: 1;">ELEMENTS</div>
                <div id="con_element" style="font-family:'Space Mono', monospace;padding: 15px; position: relative; height: 100%; overflow-y: auto"></div>
                <div style="padding: 10px;position: sticky;z-index: 1;height: 40px;display: flex;font-size: 12px;text-align: center;line-height: 27px;">
                    <div id="con_element_button_addColor" class="button" style="overflow:hidden;position: relative;height: 100%;width: 100%;border-radius: 3px;border: solid #4b837e 4px;box-sizing: border-box;background: #6dbfb8;margin-right: 2px;">New color</div>
                    <div id="con_element_button_findColor" class="button" style="overflow:hidden;position: relative;height: 100%;width: 100%;border-radius: 3px;border: solid #785978 4px;box-sizing: border-box;margin-left: 2px;background: #be95be;">Find color</div>
                </div>
            </div>
        </div>
    </div>
    <div id="closeButton" style="cursor: pointer; background-color: #BB5555; background-image: url(${closeButtonSvg});background-position: center;background-size: contain;background-repeat: no-repeat;border: 4px solid #974545; border-radius: 5px; height: 25px; width: 25px; margin-left: 5px;box-shadow: 4px 4px rgba(0, 0, 0, 0.3)"></div>
    `
)

document.getElementById("con_edit_button_text").onclick = function() {
    document.getElementById("editLabel").innerHTML = "TEXT CHANGER"
    document.getElementById("editLabel").style.background = "#f5945c"
    document.getElementById("con_edit").innerHTML =
`This text changer uses <w style="color: #f5945d">.replace(pattern, replacement)</w> function with pattern used as RegEx object (global flag).
How to RegEx: <w style="color: #5567f1; cursor:pointer;" onclick='window.open("https://www3.ntu.edu.sg/home/ehchua/programming/howto/Regexe.html")'>https://www3.ntu.edu.sg/home/ehchua/programming/howto/Regexe.html</w>


`
    ls.text.forEach((t, i) => {
        createEle("div", document.getElementById("con_edit"), null, `${i}: <w style="color: #e6db74">/${t.from}/g</w>`, null, "button").onclick = function() {
            let regex = prompt("from (RegEx)", t.from)
            if (!regex) return
            ls.text[i].from = regex
            localStorage.customizer = JSON.stringify(ls)
        }
        createEle("div", document.getElementById("con_edit"), "margin-left: 40px; margin-bottom: 10px;",`<w style="color: #e6db74">"</w>${t.to}<w style="color: #e6db74">"</w>`, null, "button").onclick = function() {
            let a = prompt("to (String)", t.to)
            if (!a) return
            ls.text[i].to = a
            localStorage.customizer = JSON.stringify(ls)
        }
    })

    let textButtons = createEle("div", document.getElementById("con_edit"), "margin-top: 20px; font-size: 12px; display: inline-flex")
    createEle("div", textButtons, "cursor: pointer; overflow:hidden;height: fit-content;width: fit-content;padding: 3px 7px; border-radius: 3px;border: solid #4b837e 4px;box-sizing: border-box;background: #6dbfb8;", "New Text").onclick = function() {
        let from = prompt("from (RegEx)")
        if (!from) return
        if (ls.text.map(x => x.from).includes(from)) return alert("Text already exists.")
        let to = prompt("to (String)")
        if (!to) to = ""
        ls.text.push({
            from: from,
            to: to
        })
        localStorage.customizer = JSON.stringify(ls)
    }
    createEle("div", textButtons, "margin-left: 5px; cursor: pointer; overflow:hidden;height: fit-content;width: fit-content;padding: 3px 7px; border-radius: 3px;border: solid #974545 4px;box-sizing: border-box;background: #bb5555;", "Remove Text").onclick = function() {
        if (ls.text.length <= 0) return alert("You cannot remove more text.")
        let indexNumber = prompt("Index:", ls.text.length - 1)
        if (isNaN(indexNumber) || indexNumber == "" || indexNumber >= ls.text.length || indexNumber < 0) return alert("This index does not exist")
        indexNumber = Number(indexNumber)
        if (!confirm(`Are you sure to delete this?\n\n${JSON.stringify(ls.text[indexNumber], null, 4)}`)) return
        ls.text.splice(indexNumber, 1)
        localStorage.customizer = JSON.stringify(ls)
    }
}
document.getElementById("con_edit_button_appendTheme").onclick = function() {
    let newObj = prompt("JSON")
    if (newObj == null) return
    try {
        newObj = JSON.parse(newObj)
        newObj.color.from.forEach((from, index) => {
            if (from == null) return
            if (JSON.stringify(ls.color.from).includes(JSON.stringify(from))) return
            if (typeof from.color != "string") return
            if (!(typeof from.alpha == "number" || from.alpha == "*")) return

            let to = newObj.color.to[index]
            if (!["solid", "linear", "radial", "animated"].includes(to.type)) return
            if (!(typeof to.alpha == "number" || to.alpha == "*")) return
            if (to.data == null) return

            if (from.alpha != "*") from.alpha = Math.min(Math.max(from.alpha ?? 1, 0), 1)
            if (to.alpha != "*") to.alpha = Math.min(Math.max(to.alpha ?? 1, 0), 1)

            if (/^#[A-Fa-f0-9]{6}$/g.test(from.color)) from.color = from.color.match(/^#[A-Fa-f0-9]{6}$/g)[0]
            else return

            ls.color.from.push(from)
            ls.color.to.push(to)
        })
        newObj.text.forEach((obj, index) => {
            if (obj.from == null || obj.from == "" || obj.to == null) return
            if (ls.text.map(x => x.from).includes(obj.from)) return
            if (typeof obj.from != "string" || typeof obj.to != "string" ) return

            ls.text.push(obj)
        })
    } catch (error) {
        errorMessage(error)
    }
    localStorage.customizer = JSON.stringify(ls)
}

document.getElementById("con_edit_button_copyJSON").onclick = function() { navigator.clipboard.writeText(JSON.stringify(ls, null, 4)) }
document.getElementById("con_edit_button_viewJSON").onclick = function() { document.getElementById("con_edit").innerHTML = `${message}<br><br><br>${syntaxHighlight(JSON.stringify(ls, null, 4))}` }
document.getElementById("con_edit_button_deleteTheme").onclick = function() {
    if (!confirm(`Are you sure to reset all?`)) return
    ls = {
        color: {
            from: [],
            to: [],
        },
        text: []
    }
    updateElements()
}

var animatedObj = []

function getPosition(x_, y_, r_, isRadiusExist) {
    let string
    if (!isRadiusExist) string = prompt("x, y", `${x_}, ${y_}`)
    else string = prompt("x, y, radius", `${x_}, ${y_}, ${r_}`)
    if (string == null) return
    string = string.split(",")
    if ((string.length < 2 && !isRadiusExist) || (string.length < 3 && isRadiusExist)) return alert("Invalid input.")
    let x = string[0].trim(), y = string[1].trim()
    if (isNaN(x) || x == "") x = null
    else x = Number(x)
    if (isNaN(y) || y == "") y = null
    else y = Number(y)
    if (isRadiusExist) {
        var r = string[2].trim()
        if (isNaN(r) || r == "") r = null
        else r = Number(r)
        if (r < 0) return alert("Radius cannot be less than 0")
    }
    return {x, y, r}
}

function updateElements() {
    localStorage.customizer = JSON.stringify(ls)
    getColorElementsFromLocalStorage()
}

function getColorElementsFromLocalStorage() {
    document.getElementById("con_element").innerHTML = `<div style="display: flex; flex-direction: column">` + ls.color.from.map((x, index) => `
    <div id="con_element_${x.color}-${x.alpha}" style="cursor: pointer; display: flex; flex-direction: row; margin-bottom: 3px">
        <w id="con_element_${x.color}-${x.alpha}_a" style="border-radius: 5px 0 0 5px; padding: 5px 10px; background:${addAlpha(x.color, x.alpha == "*" ? null : x.alpha)}; width: 50%;">${x.color} (${x.alpha == "*" ? x.alpha : x.alpha.toFixed(1)})</w>
        <w id="con_element_${x.color}-${x.alpha}_b" style="border-radius: 0 5px 5px 0; padding: 5px 10px; background:${addAlpha(ls.color.to[index].preview, ls.color.to[index].alpha == "*" ? null : ls.color.to[index].alpha)}; width: 50%;">${ls.color.to[index].type == "solid" ? ls.color.to[index].preview : ls.color.to[index].type} (${ls.color.to[index].alpha == "*" ? ls.color.to[index].alpha : ls.color.to[index].alpha.toFixed(1)})</w>
    </div>
    `).toString().replaceAll(",", "") + "</div>"
    ls.color.from.forEach((x, index) => {
        let listOfCategory = [`category_solid`, `category_linear`, `category_radial`, `category_animated`]

        if (ls.color.to[index].type == "solid") ls.color.to[index].preview = ls.color.to[index].data
        else if (ls.color.to[index].type == "linear" || ls.color.to[index].type == "radial") ls.color.to[index].preview = ls.color.to[index].data.colorStop[0].color
        else if (ls.color.to[index].type == "animated") ls.color.to[index].preview = ls.color.to[index].data.keyframes[0]
        localStorage.customizer = JSON.stringify(ls)
        document.getElementById(`con_element_${x.color}-${x.alpha}_b`).style.background = `${addAlpha(ls.color.to[index].preview, ls.color.to[index].alpha == "*" ? null : ls.color.to[index].alpha)}`
        document.getElementById(`con_element_${x.color}-${x.alpha}_b`).innerHTML = `${ls.color.to[index].type == "solid" ? ls.color.to[index].preview : ls.color.to[index].type} (${ls.color.to[index].alpha})`

        document.getElementById(`con_element_${x.color}-${x.alpha}`).onclick = function() {
            document.getElementById("editLabel").innerHTML = "COLOR CHANGER"
            document.getElementById("editLabel").style.background = "#75ba75"
            document.getElementById("con_edit").innerHTML = `from <w id="originalColor" style="cursor: pointer; border-radius: 5px; padding: 5px 10px; background:${addAlpha(x.color, x.alpha == "*" ? null : x.alpha)};">${x.color} (${x.alpha})</w> to <w id="convertColor" style="cursor: default; border-radius: 5px; padding: 5px 10px; background:${addAlpha(ls.color.to[index].preview, ls.color.to[index].alpha == "*" ? null : ls.color.to[index].alpha)};">${ls.color.to[index].type == "solid" ? ls.color.to[index].preview : ls.color.to[index].type} (${ls.color.to[index].alpha})</w>`

            let type = createEle("div", document.getElementById("con_edit"), "margin-top: 20px", "type ")

            document.getElementById("originalColor").onclick = function() {
                let color = prompt("Color (hex6):", ls.color.from[index].color)
                if (color == null) return
                if (/^#[A-Fa-f0-9]{6}$/g.test(color)) color = color.match(/^#[A-Fa-f0-9]{6}$/g)[0]
                else return alert("Invalid hex color.")
                let alpha = prompt("Alpha (0~1 or * for any) :", ls.color.from[index].alpha)
                if (alpha != "*") {
                    if (isNaN(alpha)) alpha = 1
                    else alpha = Number(alpha)
                    alpha = Math.min(Math.max(alpha ?? 1, 0), 1)
                }
                ls.color.from[index] = ({color: color, alpha: alpha})
                document.getElementById("originalColor").innerHTML = `${color} (${alpha})`
                document.getElementById("originalColor").style.background = `${addAlpha(color, alpha == "*" ? null : alpha)}`
                updateElements()
            }

            let dropdown = createEle("select", type,
                                     `
                                     border-radius: 5px;
                                     padding: 5px 10px;
                                     background: #6ebfb8;
                                     width: fit-content;
                                     cursor: pointer;
                                     font-family: 'Space Mono', monospace;
                                     font-size: 16px;
                                     color: white;
                                     text-shadow: rgb(0 0 0) 2px 0px 0px, rgb(0 0 0) 1.75517px 0.958851px 0px, rgb(0 0 0) 1.0806px 1.68294px 0px, rgb(0 0 0) 0.141474px 1.99499px 0px, rgb(0 0 0) -0.832294px 1.81859px 0px, rgb(0 0 0) -1.60229px 1.19694px 0px, rgb(0 0 0) -1.97998px 0.28224px 0px, rgb(0 0 0) -1.87291px -0.701566px 0px, rgb(0 0 0) -1.30729px -1.5136px 0px, rgb(0 0 0) -0.421592px -1.95506px 0px, rgb(0 0 0) 0.567324px -1.91785px 0px, rgb(0 0 0) 1.41734px -1.41108px 0px, rgb(0 0 0) 1.92034px -0.558831px 0px;
                                     `, ls.color.to[index].type
                                    )

            dropdown.onchange = function() {
                if (!confirm(`Reselecting type will erase current data, continue?\n\n${JSON.stringify(ls.color.to[index].data, null, 4)}`)) {
                    updateElements()
                    return
                }
                ls.color.to[index].type = this.options[this.selectedIndex].text
                if (ls.color.to[index].type == "solid") ls.color.to[index].data = x.color
                else if (ls.color.to[index].type == "linear" || ls.color.to[index].type == "radial") {
                    ls.color.to[index].data = {
                        pos: {
                            x0: null,
                            y0: null,
                            x1: null,
                            y1: null,
                            defaultEnd: {
                                x: 100,
                                y: 100
                            }
                        },
                        colorStop: [
                            {
                                offset: 0,
                                color: x.color,
                            },
                            {
                                offset: 1,
                                color: x.color,
                            }
                        ]
                    }
                } else if (ls.color.to[index].type == "animated") {
                    ls.color.to[index].data = {
                        duration: 5,
                        keyframes: [
                            x.color,
                            x.color
                        ]
                    }
                }
                if (ls.color.to[index].type == "radial") {
                    ls.color.to[index].data.pos = {
                        x0: -1,
                        y0: -1,
                        r0: 1,
                        x1: 1,
                        y1: 1,
                        r1: null,
                        defaultEnd: {
                            x: null,
                            y: null,
                            r: 3
                        }
                    }
                }
                updateElements()
            }

            let category = createEle("div", document.getElementById("con_edit"), "display: inline-flex; margin-top: 20px;")
            let category_content = createEle("div", document.getElementById("con_edit"))

            listOfCategory.forEach(name => {
                let ele = createEle("option", dropdown, null, name.split("_")[1])
                if (ls.color.to[index].type == name.split("_")[1]) ele.selected = "selected"
            })
            createEle("div", category_content, null, `alpha = ${ls.color.to[index].alpha}`, null, "button").onclick = function() {
                let alpha = prompt("Alpha (0~1 or * for any) :", ls.color.to[index].alpha)
                if (alpha != "*") {
                    if (isNaN(alpha)) alpha = 1
                    else alpha = Number(alpha)
                    alpha = Math.min(Math.max(alpha ?? 1, 0), 1)
                }
                ls.color.to[index].alpha = alpha
                updateElements()
            }
            if (ls.color.to[index].type == "solid") { /* --------- SOLID --------- */
                createEle("div", category_content, null, `color = ${ls.color.to[index].data}`, null, "button").onclick = function() {
                    let color = prompt("Color (hex6):", ls.color.to[index].data)
                    if (color == null) return
                    if (/^#[A-Fa-f0-9]{6}$/g.test(color)) color = color.match(/^#[A-Fa-f0-9]{6}$/g)[0]
                    else return alert("Invalid hex color.")
                    ls.color.to[index].data = color
                    updateElements()
                }
            } else if (ls.color.to[index].type == "linear" || ls.color.to[index].type == "radial") { /* --------- LINEAR/RADIAL GRADIENT --------- */
                createEle("div", category_content, null, `start position = [${ls.color.to[index].data.pos.x0}, ${ls.color.to[index].data.pos.y0}${ls.color.to[index].type == "radial" ? `, ${ls.color.to[index].data.pos.r0}` : ""}]`, null, "button").onclick = function() {
                    let result = getPosition(ls.color.to[index].data.pos.x0, ls.color.to[index].data.pos.y0, ls.color.to[index].type == "radial" ? ls.color.to[index].data.pos.r0 : null, ls.color.to[index].type == "radial")
                    ls.color.to[index].data.pos.x0 = result.x
                    ls.color.to[index].data.pos.y0 = result.y
                    if (ls.color.to[index].type == "radial") ls.color.to[index].data.pos.r0 = result.r
                    updateElements()
                }
                createEle("div", category_content, null, `end position = [${ls.color.to[index].data.pos.x1}, ${ls.color.to[index].data.pos.y1}${ls.color.to[index].type == "radial" ? `, ${ls.color.to[index].data.pos.r1}` : ""}]`, null, "button").onclick = function() {
                    let result = getPosition(ls.color.to[index].data.pos.x1, ls.color.to[index].data.pos.y1, ls.color.to[index].type == "radial" ? ls.color.to[index].data.pos.r1 : null, ls.color.to[index].type == "radial")
                    ls.color.to[index].data.pos.x1 = result.x
                    ls.color.to[index].data.pos.y1 = result.y
                    if (ls.color.to[index].type == "radial") ls.color.to[index].data.pos.r1 = result.r
                    updateElements()
                }
                createEle("div", category_content, null, `<br>default end position = [${ls.color.to[index].data.pos.defaultEnd.x}, ${ls.color.to[index].data.pos.defaultEnd.y}${ls.color.to[index].type == "radial" ? `, ${ls.color.to[index].data.pos.defaultEnd.r}` : ""}]<br><w style="font-size:12px; color: #e6db74;">This attribute will be used if canvas prototype's end position is undefined.<br>output = start(x/y${ls.color.to[index].type == "radial" ? "/r" : ""}) + def_end(x/y${ls.color.to[index].type == "radial" ? "/r" : ""})</w><br><br>`, null, "button").onclick = function() {
                    let result = getPosition(ls.color.to[index].data.pos.defaultEnd.x, ls.color.to[index].data.pos.defaultEnd.y, ls.color.to[index].type == "radial" ? ls.color.to[index].data.pos.defaultEnd.r : null, ls.color.to[index].type == "radial")
                    ls.color.to[index].data.pos.defaultEnd.x = result.x
                    ls.color.to[index].data.pos.defaultEnd.y = result.y
                    if (ls.color.to[index].type == "radial") ls.color.to[index].data.pos.defaultEnd.r = result.r
                    updateElements()
                }
                ls.color.to[index].data.colorStop.forEach((thisColorStop, indexColorStop) => {
                    createEle("div", category_content, null, `color stop ${indexColorStop}: [${thisColorStop.offset}, ${thisColorStop.color}]`, null, "button").onclick = function() {
                        let input = prompt("Float (0~1), Color (hex6)", `${thisColorStop.offset}, ${thisColorStop.color}`)
                        if (input == null) return
                        input = input.split(",")
                        if (input.length <= 1) return alert("Invalid input.")
                        let offset = input[0].trim(), color = input[1].trim()
                        if (isNaN(offset) || offset == "") return alert("Invalid offset.")
                        offset = Math.min(Math.max(offset ?? 1, 0), 1)
                        offset = Number(offset)
                        if (/^#[A-Fa-f0-9]{6}$/g.test(color)) color = color.match(/^#[A-Fa-f0-9]{6}$/g)[0]
                        else return alert("Invalid hex color.")
                        ls.color.to[index].data.colorStop[indexColorStop].offset = offset
                        ls.color.to[index].data.colorStop[indexColorStop].color = color
                        updateElements()
                    }
                })
                let stopButtons = createEle("div", category_content, "margin-top: 20px; font-size: 12px; display: inline-flex")
                createEle("div", stopButtons, "cursor: pointer; overflow:hidden;height: fit-content;width: fit-content;padding: 3px 7px; border-radius: 3px;border: solid #4b837e 4px;box-sizing: border-box;background: #6dbfb8;", "New Color stop").onclick = function() {
                    let indexNumber = prompt("Index:", ls.color.to[index].data.colorStop.length)
                    if (isNaN(indexNumber) || indexNumber == "" || indexNumber > ls.color.to[index].data.colorStop.length || indexNumber < 0) indexNumber = ls.color.to[index].data.colorStop.length
                    indexNumber = Number(indexNumber)
                    ls.color.to[index].data.colorStop.splice(indexNumber, 0, ls.color.to[index].data.colorStop[ls.color.to[index].data.colorStop.length - 1])
                    updateElements()
                }
                createEle("div", stopButtons, "margin-left: 5px; cursor: pointer; overflow:hidden;height: fit-content;width: fit-content;padding: 3px 7px; border-radius: 3px;border: solid #974545 4px;box-sizing: border-box;background: #bb5555;", "Remove Color stop").onclick = function() {
                    if (ls.color.to[index].data.colorStop.length <= 2) return alert("You cannot remove more color stop.")
                    let indexNumber = prompt("Index:", ls.color.to[index].data.colorStop.length - 1)
                    if (isNaN(indexNumber) || indexNumber == "" || indexNumber >= ls.color.to[index].data.colorStop.length || indexNumber < 0) return alert("This index does not exist")
                    indexNumber = Number(indexNumber)
                    if (!confirm(`Are you sure to delete this?\n\n${JSON.stringify(ls.color.to[index].data.colorStop[indexNumber], null, 4)}`)) return
                    ls.color.to[index].data.colorStop.splice(indexNumber, 1)
                    updateElements()
                }
            } else if (ls.color.to[index].type == "animated") { /* --------- ANIMATED --------- */
                createEle("div", category_content, null, `duration = ${ls.color.to[index].data.duration}`, null, "button").onclick = function() {
                    let duration = prompt("Interval (second) > 0", ls.color.to[index].data.duration)
                    if (duration == null) return
                    if (isNaN(duration) || duration == "") return alert("Invalid duration.")
                    duration = Number(duration)
                    if (duration <= 0) duration = 5
                    ls.color.to[index].data.duration = duration
                    updateElements()
                }
                ls.color.to[index].data.keyframes.forEach((thisKeyframe, indexKeyframe) => {
                    createEle("div", category_content, null, `keyframe ${indexKeyframe}: ${thisKeyframe}`, null, "button").onclick = function() {
                        let keyframe = prompt("Color (hex6)", `${thisKeyframe}`)
                        if (keyframe == null) return
                        if (/^#[A-Fa-f0-9]{6}$/g.test(keyframe)) keyframe = keyframe.match(/^#[A-Fa-f0-9]{6}$/g)[0]
                        else return alert("Invalid hex color.")
                        ls.color.to[index].data.keyframes[indexKeyframe] = keyframe
                        updateElements()
                    }
                })
                let keyframeButtons = createEle("div", category_content, "margin-top: 20px; font-size: 12px; display: inline-flex")
                createEle("div", keyframeButtons, "cursor: pointer; overflow:hidden;height: fit-content;width: fit-content;padding: 3px 7px; border-radius: 3px;border: solid #4b837e 4px;box-sizing: border-box;background: #6dbfb8;", "New Keyframe").onclick = function() {
                    let indexNumber = prompt("Index:", ls.color.to[index].data.keyframes.length)
                    if (isNaN(indexNumber) || indexNumber == "" || indexNumber > ls.color.to[index].data.keyframes.length || indexNumber < 0) indexNumber = ls.color.to[index].data.keyframes.length
                    indexNumber = Number(indexNumber)
                    ls.color.to[index].data.keyframes.splice(indexNumber, 0, ls.color.to[index].data.keyframes[ls.color.to[index].data.keyframes.length - 1])
                    updateElements()
                }
                createEle("div", keyframeButtons, "margin-left: 5px; cursor: pointer; overflow:hidden;height: fit-content;width: fit-content;padding: 3px 7px; border-radius: 3px;border: solid #974545 4px;box-sizing: border-box;background: #bb5555;", "Remove Keyframe").onclick = function() {
                    if (ls.color.to[index].data.keyframes.length <= 2) return alert("You cannot remove more color stop.")
                    let indexNumber = prompt("Index:", ls.color.to[index].data.keyframes.length - 1)
                    if (isNaN(indexNumber) || indexNumber == "" || indexNumber >= ls.color.to[index].data.keyframes.length || indexNumber < 0) return alert("This index does not exist")
                    indexNumber = Number(indexNumber)
                    if (!confirm(`Are you sure to delete this?\n\n${JSON.stringify(ls.color.to[index].data.keyframes[indexNumber], null, 4)}`)) return
                    ls.color.to[index].data.keyframes.splice(indexNumber, 1)
                    updateElements()
                }
            }

            createEle("div", document.getElementById("con_edit"), "cursor: pointer;right: 15px; top: 15px;overflow:hidden;position: absolute;height: fit-content;width: fit-content;padding: 5px 15px; border-radius: 3px;border: solid #974545 4px;box-sizing: border-box;background: #bb5555;", "Delete this color").onclick = function() {
                if (!confirm(`Are you sure to delete?\n\n${JSON.stringify(x, null, 4)}`)) return
                ls.color.from.splice(index, 1)
                ls.color.to.splice(index, 1)
                updateElements()
            }
        }
        if (ls.color.to[index].type == "animated") {
            animatedObj[index] = {
                isTriggered: false,
                color: ls.color.to[index].data.keyframes[0],
                keyFrames: ls.color.to[index].data.keyframes,
                totalFrames: 60 * ls.color.to[index].data.duration,
                currentFrames: 0
            }
        }
    })
}

getColorElementsFromLocalStorage()

document.getElementById("con_element_button_addColor").onclick = function() {
    let color = prompt("Color (hex6):", "#ffffff")
    if (color == null) return
    if (/^#[A-Fa-f0-9]{6}$/g.test(color)) color = color.match(/^#[A-Fa-f0-9]{6}$/g)[0]
    else return alert("Invalid hex color.")
    let alpha = prompt("Alpha (0~1 or * for any) :", "*")
    if (alpha != "*") {
        if (isNaN(alpha)) alpha = 1
        else alpha = Number(alpha)
        alpha = Math.min(Math.max(alpha ?? 1, 0), 1)
    }
    if (!JSON.stringify(ls.color.from).includes(JSON.stringify({color: color, alpha: alpha}))) {
        ls.color.from.push({color: color, alpha: alpha})
        ls.color.to.push({
            type: "solid",
            alpha: alpha,
            preview: color,
            data: color
        })
    }
    updateElements()
}

let findColorArr = [],
    isFindColor = false
document.getElementById("con_element_button_findColor").onclick = function() {
    if (!isFindColor) {
        findColorArr = []
        isFindColor = true
        document.getElementById("con_element_button_findColor").style.background = "#BB5555"
        document.getElementById("con_element_button_findColor").style.border = "solid #974545 4px"
        document.getElementById("con_element_button_findColor").innerHTML = "Stop"
    } else {
        isFindColor = false
        document.getElementById("con_element_button_findColor").style.background = "#be95be"
        document.getElementById("con_element_button_findColor").style.border = "solid #785978 4px"
        document.getElementById("con_element_button_findColor").innerHTML = "Find color"
        document.getElementById("con_edit").innerHTML = findColorArr.map(x => `<w onclick="navigator.clipboard.writeText('${x.color}')" style="cursor: pointer; display: inline-flex; padding: 5px 10px; background:${addAlpha(x.color, x.alpha)}">${x.color} (${x.alpha.toFixed(1)})</w>`).toString().replaceAll(",", "")
    }
}

function convertColor(this_, x0, y0, x1, y1, isStroke) {
    try {
        ls.color.from.forEach((obj, index) => {
            let outputColor, thisObj
            if (!isStroke) outputColor = this_.fillStyle
            else outputColor = this_.strokeStyle
            if (outputColor == obj.color) {
                if (obj.alpha == "*" || obj.alpha == this_.globalAlpha) {
                    thisObj = ls.color.to[index].data
                    if (ls.color.to[index].type == "solid") outputColor = thisObj
                    if (ls.color.to[index].type == "linear") {
                        x0 = x0 || thisObj.pos.x0 || 0
                        y0 = y0 || thisObj.pos.y0 || 0
                        x1 = x1 || thisObj.pos.x1 || x0 + thisObj.pos.defaultEnd.x
                        y1 = y1 || thisObj.pos.y1 || y0 + thisObj.pos.defaultEnd.y
                        outputColor = ctx.createLinearGradient(x0, y0, x1, y1)
                        thisObj.colorStop.forEach(x => { outputColor.addColorStop(x.offset, x.color) })
                    }
                    if (ls.color.to[index].type == "radial") {
                        let r0, r1
                        x0 = x0 || thisObj.pos.x0 || (x1 == null ? (x0) : (x1 - (x1 - x0) * 1))
                        y0 = y0 || thisObj.pos.y0 || (y1 == null ? (y0) : (y1 - (y1 - y0) * 1))
                        r0 = r0 || thisObj.pos.r0 || 0
                        x1 = x1 || thisObj.pos.x1 || x0 + thisObj.pos.defaultEnd.x
                        y1 = y1 || thisObj.pos.y1 || y0 + thisObj.pos.defaultEnd.y
                        r1 = r1 || thisObj.pos.r1 || r0 + thisObj.pos.defaultEnd.r
                        outputColor = ctx.createRadialGradient(x0, y0, r0, x1, y1, r1)
                        thisObj.colorStop.forEach(x => { outputColor.addColorStop(x.offset, x.color) })
                    }
                    if (ls.color.to[index].type == "animated") { // https://stackoverflow.com/questions/53380267/colors-that-change-overtime-in-a-canvas-js-trouble-with-setinterval
                        if (!animatedObj[index]) return
                        if (!animatedObj[index].isTriggered) {
                            animatedObj[index].isTriggered = true
                            localStorage.customizer = JSON.stringify(ls)
                            animatedObj[index].color = [hexToRgb(thisObj.keyframes[0]).r, hexToRgb(thisObj.keyframes[0]).g, hexToRgb(thisObj.keyframes[0]).b]
                            animatedObj[index].keyFrames = thisObj.keyframes.map(x => [hexToRgb(x).r, hexToRgb(x).g, hexToRgb(x).b])
                            animatedObj[index].totalFrames = 60 * thisObj.duration
                            animatedObj[index].currentFrame = 0
                            function update() {
                                animatedObj[index].currentFrame = (animatedObj[index].currentFrame + 1) % animatedObj[index].totalFrames
                                let keyFrameIndex = animatedObj[index].currentFrame / (animatedObj[index].totalFrames / animatedObj[index].keyFrames.length)
                                let prev = animatedObj[index].keyFrames[Math.floor(keyFrameIndex) % animatedObj[index].keyFrames.length]
                                let next = animatedObj[index].keyFrames[Math.ceil(keyFrameIndex) % animatedObj[index].keyFrames.length]
                                let inBetweenRatio = keyFrameIndex - Math.floor(keyFrameIndex)
                                animatedObj[index].color[0] = Math.floor((next[0] - prev[0]) * inBetweenRatio) + prev[0]
                                animatedObj[index].color[1] = Math.floor((next[1] - prev[1]) * inBetweenRatio) + prev[1]
                                animatedObj[index].color[2] = Math.floor((next[2] - prev[2]) * inBetweenRatio) + prev[2]
                                requestAnimationFrame(update)
                            }
                            update()
                        }
                        outputColor = rgbToHex(animatedObj[index].color[0], animatedObj[index].color[1], animatedObj[index].color[2])
                    }
                    if (!isStroke) this_.fillStyle = outputColor
                    else this_.strokeStyle = outputColor
                    if (ls.color.to[index].alpha != "*") this_.globalAlpha = ls.color.to[index].alpha
                }
            }
        })
    } catch (error) {errorMessage(error)}
}

function convertText(text) {
    ls.text.forEach(t => {
        let a = new RegExp(t.from, "g")
        if (a.test(text)) text = text.replace(a, t.to)
    })
    return text
}

function colorFinder(color, alpha) {
    if (isFindColor && !findColorArr.map(x => x.color).includes(color) && /^#[A-Fa-f0-9]{6}$/g.test(color) && !ls.color.from.map(x => x.color).includes(color)) findColorArr.push({color: color, alpha: alpha})
}
// Credit to lexiyvv and Tinhone
for (let ctx of [CanvasRenderingContext2D, OffscreenCanvasRenderingContext2D]) {
    if (ctx.prototype.RarityColorFillText == undefined) {
        ctx.prototype.RarityColorFillText = ctx.prototype.fillText;
        ctx.prototype.RarityColorStrokeText = ctx.prototype.strokeText;
        ctx.prototype.RarityColorFillRect = ctx.prototype.fillRect;
        ctx.prototype.RarityColorStroke = ctx.prototype.stroke;
        ctx.prototype.RarityColorFill = ctx.prototype.fill;
        ctx.prototype.RarityColorStrokeRect = ctx.prototype.strokeRect;
        ctx.prototype.RarityColorMeasureText = ctx.prototype.measureText;
    } else { break };

    ctx.prototype.fillRect = function(x, y, width, height) {
        colorFinder(this.fillStyle, this.globalAlpha)
        convertColor(this, x, y, x + width, y + height, false)
        return this.RarityColorFillRect(x, y, width, height);
    };

    ctx.prototype.fill = function(path, fillRule) {
        colorFinder(this.fillStyle, this.globalAlpha)
        convertColor(this, null, null, null, null, false)
        if (path != null) return this.RarityColorFill(path, fillRule);
        else return this.RarityColorFill(fillRule);
    }

    ctx.prototype.fillText = function(text, x, y) {
        colorFinder(this.fillStyle, this.globalAlpha)
        convertColor(this, x, y, null, null, false)
        text = convertText(text)
        return this.RarityColorFillText(text, x, y);
    };

    ctx.prototype.strokeText = function(text, x, y) {
        colorFinder(this.fillStyle, this.globalAlpha)
        convertColor(this, x, y, null, null, true)
        text = convertText(text)
        return this.RarityColorStrokeText(text, x, y);
    };

    ctx.prototype.stroke = function(path) {
        colorFinder(this.fillStyle, this.globalAlpha)
        convertColor(this, null, null, null, null, true)
        if (path != null) return this.RarityColorStroke(path);
        else return this.RarityColorStroke();
    };

    ctx.prototype.strokeRect = function(x, y, width, height) {
        colorFinder(this.fillStyle, this.globalAlpha)
        convertColor(this, x, y, x + width, y + height, true)
        return this.RarityColorStrokeRect(x, y, width, height);
    };

    ctx.prototype.measureText = function(text) {
        text = convertText(text)
        return this.RarityColorMeasureText(text);
    }
}

document.documentElement.addEventListener("keydown", function (e) {
    if (event.keyCode == "192" && event.shiftKey) {
        if (container.style.transform == "translate(-50%, -50%) scale(1)") {
            container.style.transform = "translate(-50%, -50%) scale(0)"
        } else {
            container.style.transform = "translate(-50%, -50%) scale(1)"
        }
    }
});

document.getElementById("closeButton").onclick = function() {
    container.style.transform = "translate(-50%, -50%) scale(0)"
}

GM_addStyle(`
.category {
    padding: 5px 20px;
    margin-right: 5px;
    background: rgba(0, 0, 0, 0.2);
    border-radius: 5px;
    cursor: pointer;
}

.button {
    cursor: pointer;
}

::-webkit-scrollbar {
    width: 5px;
}
::-webkit-scrollbar-track {
    background: #00000000;
}
::-webkit-scrollbar-thumb {
    background: #444;
    border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
    background: #444;
}

.string { color: #e6db74; }
.number { color: #ae81ff; }
.boolean { color: #ae81ff; }
.null { color: #ae81ff; }
.key { color: #66d9ef; }

`)