您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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); }); })();