GhostCanvas

An SubModule for the Definable ModMenu for Drawaria.Online.

// ==UserScript==
// @name         GhostCanvas
// @namespace    Definable
// @version      0.1.2
// @description  An SubModule for the Definable ModMenu for Drawaria.Online.
// @homepage     https://drawaria.online/profile/?uid=63196790-c7da-11ec-8266-c399f90709b7
// @author       ≺ᴄᴜʙᴇ³≻
// @match        https://drawaria.online/
// @match        https://drawaria.online/test
// @match        https://drawaria.online/room/*
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGRSURBVHhe7dkxbsJAEAXQP3ZJwyWQLOUENKGg5xYRR+AMHAHlFvQUpOEEkSxxCRpKPGmwlIyUxesMMWj+65g12vXXsGsAICIioqjEFqzJZKLfXx+Pxx/vsePW0NfbcauwhWgYgC0QEUWSPCPR41wdWu56w58CyXSQmehsU1eNXNYA5iIY2fG/UMUZwK7QcrVfVrUdb+WsF54dcL35gwgW3jcPACIYiWDRyOUw29SVHe/LLYBGLmsRjG3dmwjG1y5z4RYAgLkt3JHbXG4B3KPtf+M5l1sAzyq5Q+Z4ff9Mfm/39vH24rL28B3AAGwhmpufo65PVo+yB3Rdbyt8BzAAW4iGAdhCNAzAFqJJnpE5HuU5IFf4DmAAthDNzc9R12frR9kDuq63Fb4D3AK4/m7/LzzncgsAwM4W7shtLrcACi1XqjjZujdVnAotV7bel1sA+2VVF1pOVbH1bNGWKs6q2BZaTlN/jeVK7pDosasOLXe9bh1ARPSMkjskOuyqdtwa+no7boU/BRiALRARERFF8QWXXJ5ITbASawAAAABJRU5ErkJggg==
// @grant        none
// @license      GNU GPLv3
// ==/UserScript==

//#region TypeDefinitions
/**
 * @typedef {Object} PlayerInstance
 * @property {string} name - The name of the player.
 * @property {string} uid - The unique identifier for the player.
 * @property {string} wt - The weight of the player.
 * @property {string} roomID - The room ID the player is in.
 * @property {WebSocket} socket - The WebSocket connection for the player.
 * @property {Map<string, Function[]>} events - The events map for the player.
 * @property {boolean} isConnected - Whether the player is connected.
 * @property {(invitelink:string)=>void} connect - Connects the player to a room.
 * @property {()=>void} disconnect - Disconnects the player.
 * @property {()=>void} reconnect - Reconnects the player.
 * @property {(invitelink:string)=>void} enterRoom - Enters a room.
 * @property {()=>void} nextRoom - Moves the player to the next room.
 * @property {()=>void} leaveRoom - Leaves the current room.
 * @property {(payload:string)=>void} send - Sends a message through the WebSocket.
 * @property {(event:string,handler:Function)=>void} addEventListener - Adds an event listener.
 * @property {(event:string)=>boolean} hasEventListener - Checks if an event listener exists.
 * @property {()=>void} __invokeEvent - Invokes an event.
 */

/**
 * @typedef {Object} DrawariaOnlineMessageTypes
 * @property {(message: string) => string} chatmsg - Sends a chat message.
 * @property {() => string} passturn - Passes the turn.
 * @property {(playerid: number|string) => string} pgdrawvote - Votes for a player to draw.
 * @property {() => string} pgswtichroom - Switches the room.
 * @property {() => string} playerafk - Marks the player as AFK.
 * @property {() => string} playerrated - Rates the player.
 * @property {(gestureid: number|string) => string} sendgesture - Sends a gesture.
 * @property {() => string} sendvote - Sends a vote.
 * @property {(playerid: number|string) => string} sendvotekick - Votes to kick a player.
 * @property {(wordid: number|string) => string} wordselected - Selects a word.
 * @property {Object} clientcmd - Client commands.
 * @property {(itemid: number|string, isactive: boolean) => string} clientcmd.activateitem - Activates an item.
 * @property {(itemid: number|string) => string} clientcmd.buyitem - Buys an item.
 * @property {(itemid: number|string, target: "zindex"|"shared", value: any) => string} clientcmd.canvasobj_changeattr - Changes an attribute of a canvas object.
 * @property {() => string} clientcmd.canvasobj_getobjects - Gets canvas objects.
 * @property {(itemid: number|string) => string} clientcmd.canvasobj_remove - Removes a canvas object.
 * @property {(itemid: number|string, positionX: number|string, positionY: number|string, speed: number|string) => string} clientcmd.canvasobj_setposition - Sets the position of a canvas object.
 * @property {(itemid: number|string, rotation: number|string) => string} clientcmd.canvasobj_setrotation - Sets the rotation of a canvas object.
 * @property {(value: any) => string} clientcmd.customvoting_setvote - Sets a custom vote.
 * @property {(value: any) => string} clientcmd.getfpid - Gets the FPID.
 * @property {() => string} clientcmd.getinventory - Gets the inventory.
 * @property {() => string} clientcmd.getspawnsstate - Gets the spawn state.
 * @property {(positionX: number|string, positionY: number|string) => string} clientcmd.moveavatar - Moves the avatar.
 * @property {() => string} clientcmd.setavatarprop - Sets the avatar properties.
 * @property {(flagid: number|string, isactive: boolean) => string} clientcmd.setstatusflag - Sets a status flag.
 * @property {(playerid: number|string, tokenid: number|string) => string} clientcmd.settoken - Sets a token.
 * @property {(playerid: number|string, value: any) => string} clientcmd.snapchatmessage - Sends a Snapchat message.
 * @property {() => string} clientcmd.spawnavatar - Spawns an avatar.
 * @property {() => string} clientcmd.startrollbackvoting - Starts rollback voting.
 * @property {() => string} clientcmd.trackforwardvoting - Tracks forward voting.
 * @property {(trackid: number|string) => string} clientcmd.votetrack - Votes for a track.
 * @property {(roomID: string, name?: string, uid?: string, wt?: string) => string} startplay - Starts the play.
 * @property {Object} clientnotify - Client notifications.
 * @property {(playerid: number|string) => string} clientnotify.requestcanvas - Requests a canvas.
 * @property {(playerid: number|string, base64: string) => string} clientnotify.respondcanvas - Responds with a canvas.
 * @property {(playerid: number|string, imageid: number|string) => string} clientnotify.galleryupload - Uploads to the gallery.
 * @property {(playerid: number|string, type: any) => string} clientnotify.warning - Sends a warning.
 * @property {(playerid: number|string, targetname: string, mute?: boolean) => string} clientnotify.mute - Mutes a player.
 * @property {(playerid: number|string, targetname: string, hide?: boolean) => string} clientnotify.hide - Hides a player.
 * @property {(playerid: number|string, reason: string, targetname: string) => string} clientnotify.report - Reports a player.
 * @property {Object} drawcmd - Drawing commands.
 * @property {(x1: number|string, y1: number|string, x2: number|string, y2: number|string, color: number|string, size?: number|string, ispixel?: boolean, playerid?: number|string) => string} drawcmd.line - Draws a line.
 * @property {(x1: number|string, y1: number|string, x2: number|string, y2: number|string, color: number|string, size: number|string, ispixel?: boolean, playerid?: number|string) => string} drawcmd.erase - Erases a part of the drawing.
 * @property {(x: number|string, y: number|string, color: number|string, tolerance: number|string, r: number|string, g: number|string, b: number|string, a: number|string) => string} drawcmd.flood - Flood fills an area.
 * @property {(playerid: number|string) => string} drawcmd.undo - Undoes the last action.
 * @property {() => string} drawcmd.clear - Clears the drawing.
 */

/**
 * @typedef {Object} PlayerClass
 * @property {PlayerInstance[]} instances
 * @property {PlayerInstance} noConflict
 * @property {(inviteLink:string)=>URL} getSocketServerURL
 * @property {(inviteLink:string)=>string} getRoomID
 * @property {DrawariaOnlineMessageTypes} parseMessage
 */

/**
 * @typedef {Object} UI
 * @property {(selectors: string, parentElement?: ParentNode) => Element|null} querySelect - Returns the first element that is a descendant of node that matches selectors.
 * @property {(selectors: string, parentElement?: ParentNode) => NodeListOf<Element>} querySelectAll - Returns all element descendants of node that match selectors.
 * @property {(tagName: string, properties?: object) => Element} createElement - Creates an element and assigns properties to it.
 * @property {(element: Element, attributes: object) => void} setAttributes - Assigns attributes to an element.
 * @property {(element: Element, styles: object) => void} setStyles - Assigns styles to an element.
 * @property {(name?: string) => HTMLDivElement} createContainer - Creates a container element.
 * @property {() => HTMLDivElement} createRow - Creates a row element.
 * @property {(name: string) => HTMLElement} createIcon - Creates an icon element.
 * @property {(type: string, properties?: object) => HTMLInputElement} createInput - Creates an input element.
 * @property {(input: HTMLInputElement, properties?: object) => HTMLLabelElement} createLabelFor - Creates a label for an input element.
 * @property {(className?: string) => HTMLElement & { show: Function, hide: Function }} createSpinner - Creates a spinner element.
 * @property {(input: HTMLInputElement, addon: HTMLLabelElement|HTMLInputElement|HTMLButtonElement|HTMLElement) => HTMLDivElement} createGroup - Creates an input group element.
 * @property {(inputs: Array<HTMLLabelElement|HTMLInputElement|HTMLButtonElement|HTMLElement>) => HTMLDivElement} createInputGroup - Creates an input group element.
 */

/**
 * @typedef {Object} Other
 * @property {(message: string, styles?: string, application?: string) => void} log - Logs a message with styles.
 * @property {(size?: number) => string} uid - Generates a random UID.
 * @property {(byteArray: number[]) => string} toHexString - Converts a byte array to a hex string.
 * @property {(key: string, value: string) => void} setCookie - Sets a cookie.
 * @property {() => Array<*>&{addEventListener:(event:"delete"|"set",handler:(property:string,value:*)=>void)=>}} makeObservableArray - Creates an observable array.
 * @property {(message: string) => (Array<any> | object)} tryParseJSON - Tries to parse a JSON string.
 */

/**
 * @class
 * @typedef {Object} DefinableCore
 * @property {PlayerClass} Player
 * @property {UI} UI
 * @property {Other} helper
 */

/**
 * @typedef {Object} Definable
 * @property {PlayerClass} Player
 * @property {UI} UI
 * @property {()=>HTMLElement} createRow
 * @property {(submodule:Core)=>void} registerModule
 */

/**
 * @typedef {Object} Position
 * @prop {number} x
 * @prop {number} y
 */

/**
 * @typedef {Object} Color
 * @prop {number} r
 * @prop {number} g
 * @prop {number} b
 * @prop {number} a
 */

/**
 * @typedef {Object} Volume
 * @prop {number} width
 * @prop {number} height
 */

/**
 * @typedef {Position & Color} Pixel
 */

/**
 * @typedef {Pixel & Volume} Area
 */

/**
 * @typedef {Object} PromiseResponse
 * @prop {*} data
 * @prop {Error|string|undefined} error
 */
//#endregion TypeDefinitions

(function () {
  "use strict";

  /**
   * @typedef {Definable & {listOfPixels:object[],instructions:string[]&{addEventListener:(event:"delete"|"set",handler:(property:string,value:*)=>void)=>}}} GhostCanvas
   */

  /**
   * @param {Definable} definable
   * @param {DefinableCore} $core
   */
  function initialize(definable, $core) {
    /** @type {GhostCanvas} */
    const ghostcanvas = new $core("GhostCanvas", "ghost");
    definable.registerModule(ghostcanvas);
    ghostcanvas.listOfPixels = [];
    ghostcanvas.instructions = $core.helper.makeObservableArray();
    ghostcanvas.instructions.isRunning = false;
    const ui = $core.UI;
    const player = $core.Player.noConflict;

    const originalCanvas = ui.querySelect("#canvas");
    const canvas = ui.createElement("canvas", {
      className: "position-fixed",
      style: "pointer-events: none; opacity: 0.6; box-shadow: rebeccapurple 0px 0px 2px 2px inset;",
    });
    const context = getOptimizedRenderingContext(canvas);
    document.body.appendChild(canvas);

    /* Row 1 */ {
      const row = ghostcanvas.createRow();

      {
        const input = ui.createInput("checkbox");
        const label = ui.createLabelFor(input, { title: "Toggle Visibility" });
        label.appendChild(ui.createIcon("low-vision"));
        label.className = label.className.replace("secondary", "success");
        input.addEventListener("input", function () {
          canvas.classList[input.checked ? "remove" : "add"]("d-none");
        });
        row.appendChild(label);
      }

      {
        const input = ui.createInput("button");
        const label = ui.createLabelFor(input, { title: "Realign" });
        label.appendChild(ui.createIcon("arrows-alt"));
        input.addEventListener("click", function () {
          updatePositionAlt(originalCanvas, canvas);
        });
        row.appendChild(label);
      }

      {
        const input = ui.createInput("file", { accept: "image/*" });
        const label = ui.createLabelFor(input, { title: "Add Image" });
        label.appendChild(ui.createIcon("plus"));
        label.className = label.className.replace("secondary", "info");
        input.addEventListener("input", function () {
          loadFileAsImage(input.files[0]).then((response) => {
            if (response.data) {
              /** @type {HTMLImageElement} */
              const image = response.data;
              image.classList.add("transformable");
              document.body.appendChild(image);
              input.value = null;
            }
          });
        });
        row.appendChild(label);
      }

      {
        const input = ui.createInput("button");
        const label = ui.createLabelFor(input, { title: "Remove all images", style: "margin-left: auto;" });
        label.appendChild(ui.createIcon("trash"));
        label.className = label.className.replace("secondary", "warning");
        input.addEventListener("click", function () {
          globalThis["transformable"].target = null;
          const transformableImages = ui.querySelectAll(".transformable");
          Array.from(transformableImages).forEach((element) => element.remove());
        });
        row.appendChild(label);
      }
    }

    /* Row 2 */ {
      const row = ghostcanvas.createRow();

      // {
      //   const input = ui.createInput("button");
      //   const label = ui.createLabelFor(input, { title: "Save Image Position" });
      //   label.appendChild(ui.createIcon("print"));
      //   input.addEventListener("click", function () {
      //     ui.querySelectAll(".transformable").forEach((transformable) => {
      //       const placed = drawTransformedImage(context, transformable);
      //       if (!placed) console.debug("%o is out of bounds", transformable);
      //     });
      //   });
      //   row.appendChild(label);
      // }

      {
        const input = ui.createInput("button");
        const label = ui.createLabelFor(input, { title: "Save Pixels" });
        label.appendChild(ui.createIcon("print"));

        input.addEventListener("click", function () {
          ui.querySelectAll(".transformable").forEach((transformable) => {
            const placed = drawTransformedImage(context, transformable);
            if (!placed) console.debug("%o is out of bounds", transformable);
          });
          getDefaultPixels(context, ghostcanvas);
        });

        row.appendChild(label);
      }

      {
        const input = ui.createInput("text", { title: "Pixels Buffer", readOnly: true, value: 0, id: "pixelbufferDisplay" });

        input.classList.add("col", "form-control-sm");

        row.appendChild(input);
      }
    }

    /* Row 3 */ {
      const row = ghostcanvas.createRow();

      {
        const input = ui.createInput("number", { title: "Color Tolerance", id: "comparePixelsColorTolerance", value: 16, min: 4, max: 128 });
        input.classList.add("col", "form-control-sm");
        row.appendChild(input);
      }

      {
        const input = ui.createInput("button");
        const label = ui.createLabelFor(input, { title: "Group by Y" });
        const rotatedIcon = ui.createIcon("bars");
        rotatedIcon.style.rotate = "90deg";
        label.appendChild(rotatedIcon);
        input.addEventListener("click", async function () {
          if (ghostcanvas.listOfPixels.length < 1) {
            await getDefaultPixels(context, ghostcanvas);
          }
          const sortedPixels = await sortPixelsByPosition.callAsWorker(ghostcanvas.listOfPixels, true);
          ghostcanvas.listOfPixels = sortedPixels;
          const groupedPixels = await groupPixelsByColor.callAsWorker(sortedPixels, ui.querySelect("#comparePixelsColorTolerance").value, false, areSameColor.toString(), true);
          ghostcanvas.listOfPixels = groupedPixels;
          ui.querySelect("#pixelbufferDisplay").value = groupedPixels.length;
        });
        row.appendChild(label);
      }

      {
        const input = ui.createInput("button");
        const label = ui.createLabelFor(input, { title: "Group by X" });
        label.appendChild(ui.createIcon("bars"));
        input.addEventListener("click", async function () {
          if (ghostcanvas.listOfPixels.length < 1) {
            await getDefaultPixels(context, ghostcanvas);
          }
          const sortedPixels = await sortPixelsByPosition.callAsWorker(ghostcanvas.listOfPixels, false);
          ghostcanvas.listOfPixels = sortedPixels;
          const groupedPixels = await groupPixelsByColor.callAsWorker(sortedPixels, ui.querySelect("#comparePixelsColorTolerance").value, true, areSameColor.toString(), true);
          ghostcanvas.listOfPixels = groupedPixels;
          ui.querySelect("#pixelbufferDisplay").value = groupedPixels.length;
        });
        row.appendChild(label);
      }
    }

    /* Row 4 */ {
      const row = ghostcanvas.createRow();

      {
        const input = ui.createInput("button");
        const label = ui.createLabelFor(input, { title: "Sort by X (Left to Right)" });
        label.appendChild(ui.createIcon("arrow-right"));
        input.addEventListener("click", async function () {
          if (ghostcanvas.listOfPixels.length < 1) {
            await getDefaultPixels(context, ghostcanvas);
          }
          const sortedPixels = await sortPixelsByPosition.callAsWorker(ghostcanvas.listOfPixels, true);
          ghostcanvas.listOfPixels = sortedPixels;
          ui.querySelect("#pixelbufferDisplay").value = sortedPixels.length;
        });
        row.appendChild(label);
      }

      {
        const input = ui.createInput("button");
        const label = ui.createLabelFor(input, { title: "Sort by Y (Top to Bottom)" });
        label.appendChild(ui.createIcon("arrow-down"));
        input.addEventListener("click", async function () {
          if (ghostcanvas.listOfPixels.length < 1) {
            await getDefaultPixels(context, ghostcanvas);
          }
          const sortedPixels = await sortPixelsByPosition.callAsWorker(ghostcanvas.listOfPixels, false);
          ghostcanvas.listOfPixels = sortedPixels;
          ui.querySelect("#pixelbufferDisplay").value = sortedPixels.length;
        });
        row.appendChild(label);
      }

      {
        const input = ui.createInput("button");
        const label = ui.createLabelFor(input, { title: "Sort by Color" });
        label.appendChild(ui.createIcon("sort-alpha-down"));
        input.addEventListener("click", async function () {
          if (ghostcanvas.listOfPixels.length < 1) {
            await getDefaultPixels(context, ghostcanvas);
          }
          const sortedPixels = await sortPixelsByColor.callAsWorker(ghostcanvas.listOfPixels);
          ghostcanvas.listOfPixels = sortedPixels;
          ui.querySelect("#pixelbufferDisplay").value = sortedPixels.length;
        });
        row.appendChild(label);
      }
    }

    /* Row 5 */ {
      ghostcanvas.__contaier.appendChild(ui.querySelect("#chatbox_messages").previousElementSibling.cloneNode(false));
      const row = ghostcanvas.createRow();

      const parseInstructionsInput = ui.createInput("button");
      const parseInstructionsLabel = ui.createLabelFor(parseInstructionsInput, { title: "Generate Instructions", textContent: "Load" });
      row.appendChild(parseInstructionsLabel);
      parseInstructionsInput.addEventListener("click", function () {
        ghostcanvas.instructions.push(...ghostcanvas.listOfPixels.map((o) => ghostcanvas.Player.parseMessage.drawcmd.line(o.x, o.y, o.x + o.width, o.y + o.height, `rgb(${o.r},${o.g},${o.b})`, 2)));
        savedInstructionsCountInput.value = ghostcanvas.instructions.length;
      });

      const savedInstructionsCountInput = ui.createInput("text", { readOnly: true, title: "Total length of Instructions generated", value: 0, id: "savedInstructionsCountInput" });
      savedInstructionsCountInput.classList.add("col", "form-control-sm");
      row.appendChild(savedInstructionsCountInput);
      ghostcanvas.instructions.addEventListener("delete", (prop, val) => {
        savedInstructionsCountInput.value = ghostcanvas.instructions.length;
      });
    }

    /* Row 6 & 7 */ {
      const row6 = ghostcanvas.createRow();
      const row7 = ghostcanvas.createRow();

      const bulkSizeInput = ui.createInput("number", { title: "Bulk Execution Size", min: 1, value: 100 });
      bulkSizeInput.classList.add("col", "form-control-sm");
      row6.appendChild(bulkSizeInput);

      const intervalInput = ui.createInput("number", { title: "Wait interval between Executions (Milliseconds)", min: 1, value: 1000 });
      intervalInput.classList.add("col", "form-control-sm");
      row6.appendChild(intervalInput);

      {
        const input = ui.createInput("checkbox", { id: "gcIsRunning" });
        const label = ui.createLabelFor(input, { textContent: "Start" });
        label.classList.add("col");
        label.className = label.className.replace("secondary", "success");
        input.addEventListener("input", function () {
          ghostcanvas.instructions.isRunning = input.checked;
          execute(ghostcanvas.instructions, player.send, Number(intervalInput.value), Number(bulkSizeInput.value)).finally(() => {
            input.checked = false;
            label.classList.remove("active");
            ui.querySelect("#savedInstructionsCountInput").value = ghostcanvas.instructions.length;
          });
        });
        row7.appendChild(label);
      }

      {
        const input = ui.createInput("button");
        const label = ui.createLabelFor(input, { textContent: "Stop" });
        label.classList.add("col");
        label.className = label.className.replace("secondary", "warning");
        input.addEventListener("click", function () {
          ui.querySelect("#gcIsRunning").checked = false;
          ui.querySelect("#gcIsRunning").parentElement.classList.remove("active");
          ghostcanvas.instructions.isRunning = false;
        });
        row7.appendChild(label);
      }

      {
        const input = ui.createInput("button");
        const label = ui.createLabelFor(input, { textContent: "Step" });
        label.classList.add("col");
        input.addEventListener("click", function () {
          player.send(ghostcanvas.instructions.shift());
        });
        row7.appendChild(label);
      }

      {
        const input = ui.createInput("button");
        const label = ui.createLabelFor(input, { textContent: "Reset" });
        label.classList.add("col");
        label.className = label.className.replace("secondary", "danger");
        input.addEventListener("click", function () {
          ghostcanvas.instructions.length = 0;
          ui.querySelect("#gcIsRunning").checked = false;
          ui.querySelect("#gcIsRunning").parentElement.classList.remove("active");
          ui.querySelect("#savedInstructionsCountInput").value = 0;
        });
        row7.appendChild(label);
      }
    }

    updatePositionAlt(originalCanvas, canvas);
    canvas.classList.add("d-none");
    ui.querySelect("#definableStyles").textContent += "\n.transformable { position: fixed; top: 0px; left: 0px; }";
  }

  async function getDefaultPixels(context, ghostcanvas) {
    const [imageData, width] = getImageDataForProcessing(context);
    const allPixels = await convertImageDataToPixels.callAsWorker(imageData, width);
    const nonTransparentPixels = await filterForNonTransparentPixels.callAsWorker(allPixels, 128);

    ghostcanvas.listOfPixels = nonTransparentPixels;
    document.querySelector("#pixelbufferDisplay").value = nonTransparentPixels.length;
    return true;
  }

  /**
   * @param {HTMLCanvasElement} canvas
   * @returns {CanvasRenderingContext2D}
   */
  function getOptimizedRenderingContext(canvas) {
    if (canvas.optimizedRenderingContext) return canvas.optimizedRenderingContext;
    const context = canvas.getContext("2d", {
      alpha: true,
      willReadFrequently: true,
    });
    canvas.optimizedRenderingContext = context;
    return context;
  }

  /**
   * @param {File} file
   * @returns {Promise<PromiseResponse>}
   */
  function loadFileAsImage(file) {
    return new Promise((resolve, reject) => {
      if (!(FileReader && file)) {
        reject({ data: undefined, error: "Native FileReader not present." });
      } else {
        const reader = new FileReader();
        reader.onload = function () {
          const image = new Image();
          image.src = reader.result;
          image.onload = function () {
            resolve({ data: image, error: undefined });
          };
        };
        reader.readAsDataURL(file);
      }
    });
  }

  /**
   * @param {CanvasRenderingContext2D} context
   * @returns {[Uint8ClampedArray, number]}
   */
  function getImageDataForProcessing(context) {
    return [context.getImageData(0, 0, context.canvas.width, context.canvas.height).data, context.canvas.width];
  }

  /**
   * Get Pixels
   * @param {Uint8ClampedArray} imageData
   * @param {number} width
   * @returns {Array<Pixel>}
   */
  function convertImageDataToPixels(imageData, width) {
    const pixelCount = imageData.length / 4;
    const pixels = new Array(pixelCount);
    const widthFactor = 1 / width;

    for (let i = 0; i < pixelCount; i++) {
      const index = i * 4;
      const x = i % width;
      const y = (i * widthFactor) | 0;
      const [r, g, b, a] = imageData.slice(index, index + 4);
      pixels[i] = { x, y, r, g, b, a };
    }

    return pixels;
  }

  /**
   * Filter for NonTransparent Pixels
   * @param {Array<Pixel>} pixels
   * @param {number} [threshold=64]
   * @returns {Array<Pixel>}
   */
  function filterForNonTransparentPixels(pixels, threshold = 64) {
    return pixels.filter(({ a: alpha }) => alpha > threshold);
  }

  /**
   * Check if the two provided Pixels are close to similar in the rgb spectrum.
   * @param {Pixel} pixel1 - The first color object with r, g, b, and a properties.
   * @param {Pixel} pixel2 - The second color object with r, g, b, and a properties.
   * @param {number} tolerance - The maximum allowed difference for each color component.
   * @param {boolean} [ignoreAphaValue=true] - Should Alphavalues be ignored.
   * @returns {boolean} - True if the colors are considered the same, false otherwise.
   */
  function areSameColor(pixel1, pixel2, tolerance, ignoreAphaValue = true) {
    const dr = Math.abs(pixel1.r - pixel2.r);
    const dg = Math.abs(pixel1.g - pixel2.g);
    const db = Math.abs(pixel1.b - pixel2.b);
    const da_ok = !ignoreAphaValue ? Math.abs(pixel1.a - pixel2.a) <= tolerance : true;

    return dr <= tolerance && dg <= tolerance && db <= tolerance && da_ok;
  }

  /**
   * Combine Pixels to Areas by proximity and color.
   * @param {Array<Pixel>} data - Array of Pixel objects.
   * @param {number} [tolerance=32] - The maximum allowed difference for each color component.
   * @param {boolean} [groupByY=true] - If true, group by y then x; if false, group by y then x.
   * @param {string} comparisonFunctionAsString - The Function to be used for Color Comparison as a string.
   * @param {boolean} [ignoreAphaValue=true] - Should Alphavalues be ignored.
   * @returns {Array<Area>} - Array of Area objects.
   */
  function groupPixelsByColor(data, tolerance = 32, groupByY = true, comparisonFunctionAsString = undefined, ignoreAphaValue = true) {
    if (data.length === 0) return [];

    const comparisonFunction =
      comparisonFunctionAsString && typeof comparisonFunctionAsString === "string"
        ? new Function("return (" + comparisonFunctionAsString + ")(...arguments)")
        : areSameColor ?? new Function("return false;");

    const nameOfValueToIncrease = groupByY ? "width" : "height";
    const nameOfValueToBeSimilar = groupByY ? "y" : "x";
    const nameOfValueToNOTBeSimilar = !groupByY ? "y" : "x";

    data.sort();

    const lines = new Array(data.length);
    let newLineIndex = 0;
    let currentLine = {
      x: data[0].x,
      y: data[0].y,
      width: 0,
      height: 0,
      r: data[0].r,
      g: data[0].g,
      b: data[0].b,
      a: data[0].a,
    };

    for (let i = 1; i < data.length; i++) {
      const pixel = data[i];
      const lastPixel = data[i ? i - 1 : 0];
      if (
        pixel[nameOfValueToBeSimilar] === currentLine[nameOfValueToBeSimilar] &&
        comparisonFunction(currentLine, pixel, tolerance, ignoreAphaValue) &&
        Math.abs(lastPixel[nameOfValueToNOTBeSimilar] - pixel[nameOfValueToNOTBeSimilar]) < 2
      ) {
        currentLine[nameOfValueToIncrease]++;
      } else {
        lines[newLineIndex++] = currentLine;
        currentLine = {
          x: pixel.x,
          y: pixel.y,
          width: 1,
          height: 1,
          r: pixel.r,
          g: pixel.g,
          b: pixel.b,
          a: pixel.a,
        };
      }
    }

    // Push the last line
    lines[newLineIndex] = currentLine;

    return lines.slice(0, newLineIndex + 1);
  }

  /**
   * Sort Pixellike Object by their Position.
   * @param {Array<Pixel>} pixels - Array of Pixel objects.
   * @param {boolean} [sortByX=true] - If true, sort by x then y; if false, sort by y then x.
   * @returns {Array<Pixel>} - Sorted array of Pixel objects.
   */
  function sortPixelsByPosition(pixels, sortByX = true) {
    const sortFunction = sortByX
      ? function (a, b) {
          return a.x - b.x || a.y - b.y;
        }
      : function (a, b) {
          return a.y - b.y || a.x - b.x;
        };
    return pixels.sort(sortFunction);
  }

  /**
   * Sort Pixellike Object by their Color.
   * @param {Array<Pixel>} pixels - Array of Pixel objects.
   * @returns {Array<Pixel>}
   */
  function sortPixelsByColor(pixels) {
    return pixels.sort(function (a, b) {
      return a.r - b.r || a.g - b.g || a.b - b.b;
    });
  }

  /**
   * Get the Color of the Pixel formatted as rgb.
   * @param {Pixel|Area} pixel
   */
  function getPixelColorAsRGB(pixel) {
    return `rgb(${pixel.r},${pixel.g},${pixel.b})`;
  }
  /**
   * Check if two HTMLElements are overlapping over each other.
   * @param {HTMLElement} element1
   * @param {HTMLElement} element2
   */
  function areOverlapping(element1, element2) {
    const bbox1 = element1.getBoundingClientRect();
    const bbox2 = element2.getBoundingClientRect();
    return bbox1.bottom > bbox2.top || bbox1.right > bbox2.left || bbox1.top < bbox2.bottom || bbox1.left < bbox2.right;
  }

  /**
   * Get the position of the image relative to the canvas.
   * @param {HTMLCanvasElement} canvas
   * @param {HTMLImageElement} image
   * @returns
   */
  function getImagePositionRelativeToCanvas(canvas, image) {
    const rect = canvas.getBoundingClientRect();
    const style = window.getComputedStyle(image);
    const x = parseFloat(style.left);
    const y = parseFloat(style.top);
    return [x - rect.left, y - rect.top];
  }

  /**
   * Draw the image on the canvas with its transformations.
   * @param {CanvasRenderingContext2D} context
   * @param {HTMLImageElement} image
   */
  function drawTransformedImage(context, image) {
    if (!areOverlapping(context.canvas, image)) return false;

    // Get the computed style of the image
    const style = window.getComputedStyle(image);
    const transform = style.transform;
    const width = parseFloat(style.width);
    const height = parseFloat(style.height);

    // Save the current context state
    context.save();

    // Calculate the position of the image on the canvas
    const [x, y] = getImagePositionRelativeToCanvas(context.canvas, image);

    // Calculate the scaling factor
    const scaleX = context.canvas.width / context.canvas.clientWidth;
    const scaleY = context.canvas.height / context.canvas.clientHeight;

    // Apply the CSS transform to the canvas context
    context.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
    const matrix = new DOMMatrix(transform);

    // Translate to the center of the image
    context.translate((x + width / 2) * scaleX, (y + height / 2) * scaleY);
    // Apply the rotation
    context.transform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f);
    // Translate back
    context.translate((-width / 2) * scaleX, (-height / 2) * scaleY);

    // Draw the image on the canvas
    context.drawImage(image, 0, 0, width * scaleX, height * scaleY);

    // Restore the context to its original state
    context.restore();
    return true;
  }

  /**
   * @param {HTMLElement} parent
   * @param {HTMLElement} child
   */
  function updatePosition(parent, child) {
    const rect = parent.getBoundingClientRect();
    child.style.top = rect.top.toFixed(0) + "px";
    child.style.left = rect.left.toFixed(0) + "px";
    child.style.width = rect.width.toFixed(0) + "px";
    child.style.height = rect.height.toFixed(0) + "px";
    child.width = 1000;
    child.height = 1000;
  }

  function updatePositionAlt(parent, child) {
    const rect = parent.getBoundingClientRect();
    child.style.top = rect.top.toFixed(0) + "px";
    child.style.left = rect.left.toFixed(0) + "px";
    child.width = rect.width.toFixed(0);
    child.height = rect.height.toFixed(0);
  }

  /**
   * @param {string[]} instructions
   * @param {Function} callback
   * @param {number} [interval=1000]
   * @param {number} [bulkSize=100]
   */
  function execute(instructions, callback, interval = 1000, bulkSize = 100) {
    return new Promise((resolve, reject) => {
      if (!instructions || !callback) {
        reject();
        return;
      }
      if (!instructions.isRunning || !instructions.length) {
        resolve();
        return;
      }
      const intervalID = setInterval(() => {
        instructions.splice(0, bulkSize).forEach((s) => {
          callback(s);
        });
        if (!instructions.length || !instructions.isRunning) {
          clearInterval(intervalID);
          instructions.isRunning = false;
          resolve();
          return;
        }
      }, interval);
    });
  }

  window.addEventListener("definable:init", function (event) {
    /** @type {Definable} */
    const main = event.detail.main;
    /** @type {DefinableCore} */
    const core = event.detail.core;
    initialize(main, core);
  });
})();