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