GhostCanvas

An SubModule for the Definable ModMenu for Drawaria.Online.

  1. // ==UserScript==
  2. // @name GhostCanvas
  3. // @namespace Definable
  4. // @version 0.1.2
  5. // @description An SubModule for the Definable ModMenu for Drawaria.Online.
  6. // @homepage https://drawaria.online/profile/?uid=63196790-c7da-11ec-8266-c399f90709b7
  7. // @author ≺ᴄᴜʙᴇ³≻
  8. // @match https://drawaria.online/
  9. // @match https://drawaria.online/test
  10. // @match https://drawaria.online/room/*
  11. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGRSURBVHhe7dkxbsJAEAXQP3ZJwyWQLOUENKGg5xYRR+AMHAHlFvQUpOEEkSxxCRpKPGmwlIyUxesMMWj+65g12vXXsGsAICIioqjEFqzJZKLfXx+Pxx/vsePW0NfbcauwhWgYgC0QEUWSPCPR41wdWu56w58CyXSQmehsU1eNXNYA5iIY2fG/UMUZwK7QcrVfVrUdb+WsF54dcL35gwgW3jcPACIYiWDRyOUw29SVHe/LLYBGLmsRjG3dmwjG1y5z4RYAgLkt3JHbXG4B3KPtf+M5l1sAzyq5Q+Z4ff9Mfm/39vH24rL28B3AAGwhmpufo65PVo+yB3Rdbyt8BzAAW4iGAdhCNAzAFqJJnpE5HuU5IFf4DmAAthDNzc9R12frR9kDuq63Fb4D3AK4/m7/LzzncgsAwM4W7shtLrcACi1XqjjZujdVnAotV7bel1sA+2VVF1pOVbH1bNGWKs6q2BZaTlN/jeVK7pDosasOLXe9bh1ARPSMkjskOuyqdtwa+no7boU/BRiALRARERFF8QWXXJ5ITbASawAAAABJRU5ErkJggg==
  12. // @grant none
  13. // @license GNU GPLv3
  14. // ==/UserScript==
  15.  
  16. //#region TypeDefinitions
  17. /**
  18. * @typedef {Object} PlayerInstance
  19. * @property {string} name - The name of the player.
  20. * @property {string} uid - The unique identifier for the player.
  21. * @property {string} wt - The weight of the player.
  22. * @property {string} roomID - The room ID the player is in.
  23. * @property {WebSocket} socket - The WebSocket connection for the player.
  24. * @property {Map<string, Function[]>} events - The events map for the player.
  25. * @property {boolean} isConnected - Whether the player is connected.
  26. * @property {(invitelink:string)=>void} connect - Connects the player to a room.
  27. * @property {()=>void} disconnect - Disconnects the player.
  28. * @property {()=>void} reconnect - Reconnects the player.
  29. * @property {(invitelink:string)=>void} enterRoom - Enters a room.
  30. * @property {()=>void} nextRoom - Moves the player to the next room.
  31. * @property {()=>void} leaveRoom - Leaves the current room.
  32. * @property {(payload:string)=>void} send - Sends a message through the WebSocket.
  33. * @property {(event:string,handler:Function)=>void} addEventListener - Adds an event listener.
  34. * @property {(event:string)=>boolean} hasEventListener - Checks if an event listener exists.
  35. * @property {()=>void} __invokeEvent - Invokes an event.
  36. */
  37.  
  38. /**
  39. * @typedef {Object} DrawariaOnlineMessageTypes
  40. * @property {(message: string) => string} chatmsg - Sends a chat message.
  41. * @property {() => string} passturn - Passes the turn.
  42. * @property {(playerid: number|string) => string} pgdrawvote - Votes for a player to draw.
  43. * @property {() => string} pgswtichroom - Switches the room.
  44. * @property {() => string} playerafk - Marks the player as AFK.
  45. * @property {() => string} playerrated - Rates the player.
  46. * @property {(gestureid: number|string) => string} sendgesture - Sends a gesture.
  47. * @property {() => string} sendvote - Sends a vote.
  48. * @property {(playerid: number|string) => string} sendvotekick - Votes to kick a player.
  49. * @property {(wordid: number|string) => string} wordselected - Selects a word.
  50. * @property {Object} clientcmd - Client commands.
  51. * @property {(itemid: number|string, isactive: boolean) => string} clientcmd.activateitem - Activates an item.
  52. * @property {(itemid: number|string) => string} clientcmd.buyitem - Buys an item.
  53. * @property {(itemid: number|string, target: "zindex"|"shared", value: any) => string} clientcmd.canvasobj_changeattr - Changes an attribute of a canvas object.
  54. * @property {() => string} clientcmd.canvasobj_getobjects - Gets canvas objects.
  55. * @property {(itemid: number|string) => string} clientcmd.canvasobj_remove - Removes a canvas object.
  56. * @property {(itemid: number|string, positionX: number|string, positionY: number|string, speed: number|string) => string} clientcmd.canvasobj_setposition - Sets the position of a canvas object.
  57. * @property {(itemid: number|string, rotation: number|string) => string} clientcmd.canvasobj_setrotation - Sets the rotation of a canvas object.
  58. * @property {(value: any) => string} clientcmd.customvoting_setvote - Sets a custom vote.
  59. * @property {(value: any) => string} clientcmd.getfpid - Gets the FPID.
  60. * @property {() => string} clientcmd.getinventory - Gets the inventory.
  61. * @property {() => string} clientcmd.getspawnsstate - Gets the spawn state.
  62. * @property {(positionX: number|string, positionY: number|string) => string} clientcmd.moveavatar - Moves the avatar.
  63. * @property {() => string} clientcmd.setavatarprop - Sets the avatar properties.
  64. * @property {(flagid: number|string, isactive: boolean) => string} clientcmd.setstatusflag - Sets a status flag.
  65. * @property {(playerid: number|string, tokenid: number|string) => string} clientcmd.settoken - Sets a token.
  66. * @property {(playerid: number|string, value: any) => string} clientcmd.snapchatmessage - Sends a Snapchat message.
  67. * @property {() => string} clientcmd.spawnavatar - Spawns an avatar.
  68. * @property {() => string} clientcmd.startrollbackvoting - Starts rollback voting.
  69. * @property {() => string} clientcmd.trackforwardvoting - Tracks forward voting.
  70. * @property {(trackid: number|string) => string} clientcmd.votetrack - Votes for a track.
  71. * @property {(roomID: string, name?: string, uid?: string, wt?: string) => string} startplay - Starts the play.
  72. * @property {Object} clientnotify - Client notifications.
  73. * @property {(playerid: number|string) => string} clientnotify.requestcanvas - Requests a canvas.
  74. * @property {(playerid: number|string, base64: string) => string} clientnotify.respondcanvas - Responds with a canvas.
  75. * @property {(playerid: number|string, imageid: number|string) => string} clientnotify.galleryupload - Uploads to the gallery.
  76. * @property {(playerid: number|string, type: any) => string} clientnotify.warning - Sends a warning.
  77. * @property {(playerid: number|string, targetname: string, mute?: boolean) => string} clientnotify.mute - Mutes a player.
  78. * @property {(playerid: number|string, targetname: string, hide?: boolean) => string} clientnotify.hide - Hides a player.
  79. * @property {(playerid: number|string, reason: string, targetname: string) => string} clientnotify.report - Reports a player.
  80. * @property {Object} drawcmd - Drawing commands.
  81. * @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.
  82. * @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.
  83. * @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.
  84. * @property {(playerid: number|string) => string} drawcmd.undo - Undoes the last action.
  85. * @property {() => string} drawcmd.clear - Clears the drawing.
  86. */
  87.  
  88. /**
  89. * @typedef {Object} PlayerClass
  90. * @property {PlayerInstance[]} instances
  91. * @property {PlayerInstance} noConflict
  92. * @property {(inviteLink:string)=>URL} getSocketServerURL
  93. * @property {(inviteLink:string)=>string} getRoomID
  94. * @property {DrawariaOnlineMessageTypes} parseMessage
  95. */
  96.  
  97. /**
  98. * @typedef {Object} UI
  99. * @property {(selectors: string, parentElement?: ParentNode) => Element|null} querySelect - Returns the first element that is a descendant of node that matches selectors.
  100. * @property {(selectors: string, parentElement?: ParentNode) => NodeListOf<Element>} querySelectAll - Returns all element descendants of node that match selectors.
  101. * @property {(tagName: string, properties?: object) => Element} createElement - Creates an element and assigns properties to it.
  102. * @property {(element: Element, attributes: object) => void} setAttributes - Assigns attributes to an element.
  103. * @property {(element: Element, styles: object) => void} setStyles - Assigns styles to an element.
  104. * @property {(name?: string) => HTMLDivElement} createContainer - Creates a container element.
  105. * @property {() => HTMLDivElement} createRow - Creates a row element.
  106. * @property {(name: string) => HTMLElement} createIcon - Creates an icon element.
  107. * @property {(type: string, properties?: object) => HTMLInputElement} createInput - Creates an input element.
  108. * @property {(input: HTMLInputElement, properties?: object) => HTMLLabelElement} createLabelFor - Creates a label for an input element.
  109. * @property {(className?: string) => HTMLElement & { show: Function, hide: Function }} createSpinner - Creates a spinner element.
  110. * @property {(input: HTMLInputElement, addon: HTMLLabelElement|HTMLInputElement|HTMLButtonElement|HTMLElement) => HTMLDivElement} createGroup - Creates an input group element.
  111. * @property {(inputs: Array<HTMLLabelElement|HTMLInputElement|HTMLButtonElement|HTMLElement>) => HTMLDivElement} createInputGroup - Creates an input group element.
  112. */
  113.  
  114. /**
  115. * @typedef {Object} Other
  116. * @property {(message: string, styles?: string, application?: string) => void} log - Logs a message with styles.
  117. * @property {(size?: number) => string} uid - Generates a random UID.
  118. * @property {(byteArray: number[]) => string} toHexString - Converts a byte array to a hex string.
  119. * @property {(key: string, value: string) => void} setCookie - Sets a cookie.
  120. * @property {() => Array<*>&{addEventListener:(event:"delete"|"set",handler:(property:string,value:*)=>void)=>}} makeObservableArray - Creates an observable array.
  121. * @property {(message: string) => (Array<any> | object)} tryParseJSON - Tries to parse a JSON string.
  122. */
  123.  
  124. /**
  125. * @class
  126. * @typedef {Object} DefinableCore
  127. * @property {PlayerClass} Player
  128. * @property {UI} UI
  129. * @property {Other} helper
  130. */
  131.  
  132. /**
  133. * @typedef {Object} Definable
  134. * @property {PlayerClass} Player
  135. * @property {UI} UI
  136. * @property {()=>HTMLElement} createRow
  137. * @property {(submodule:Core)=>void} registerModule
  138. */
  139.  
  140. /**
  141. * @typedef {Object} Position
  142. * @prop {number} x
  143. * @prop {number} y
  144. */
  145.  
  146. /**
  147. * @typedef {Object} Color
  148. * @prop {number} r
  149. * @prop {number} g
  150. * @prop {number} b
  151. * @prop {number} a
  152. */
  153.  
  154. /**
  155. * @typedef {Object} Volume
  156. * @prop {number} width
  157. * @prop {number} height
  158. */
  159.  
  160. /**
  161. * @typedef {Position & Color} Pixel
  162. */
  163.  
  164. /**
  165. * @typedef {Pixel & Volume} Area
  166. */
  167.  
  168. /**
  169. * @typedef {Object} PromiseResponse
  170. * @prop {*} data
  171. * @prop {Error|string|undefined} error
  172. */
  173. //#endregion TypeDefinitions
  174.  
  175. (function () {
  176. "use strict";
  177.  
  178. /**
  179. * @typedef {Definable & {listOfPixels:object[],instructions:string[]&{addEventListener:(event:"delete"|"set",handler:(property:string,value:*)=>void)=>}}} GhostCanvas
  180. */
  181.  
  182. /**
  183. * @param {Definable} definable
  184. * @param {DefinableCore} $core
  185. */
  186. function initialize(definable, $core) {
  187. /** @type {GhostCanvas} */
  188. const ghostcanvas = new $core("GhostCanvas", "ghost");
  189. definable.registerModule(ghostcanvas);
  190. ghostcanvas.listOfPixels = [];
  191. ghostcanvas.instructions = $core.helper.makeObservableArray();
  192. ghostcanvas.instructions.isRunning = false;
  193. const ui = $core.UI;
  194. const player = $core.Player.noConflict;
  195.  
  196. const originalCanvas = ui.querySelect("#canvas");
  197. const canvas = ui.createElement("canvas", {
  198. className: "position-fixed",
  199. style: "pointer-events: none; opacity: 0.6; box-shadow: rebeccapurple 0px 0px 2px 2px inset;",
  200. });
  201. const context = getOptimizedRenderingContext(canvas);
  202. document.body.appendChild(canvas);
  203.  
  204. /* Row 1 */ {
  205. const row = ghostcanvas.createRow();
  206.  
  207. {
  208. const input = ui.createInput("checkbox");
  209. const label = ui.createLabelFor(input, { title: "Toggle Visibility" });
  210. label.appendChild(ui.createIcon("low-vision"));
  211. label.className = label.className.replace("secondary", "success");
  212. input.addEventListener("input", function () {
  213. canvas.classList[input.checked ? "remove" : "add"]("d-none");
  214. });
  215. row.appendChild(label);
  216. }
  217.  
  218. {
  219. const input = ui.createInput("button");
  220. const label = ui.createLabelFor(input, { title: "Realign" });
  221. label.appendChild(ui.createIcon("arrows-alt"));
  222. input.addEventListener("click", function () {
  223. updatePositionAlt(originalCanvas, canvas);
  224. });
  225. row.appendChild(label);
  226. }
  227.  
  228. {
  229. const input = ui.createInput("file", { accept: "image/*" });
  230. const label = ui.createLabelFor(input, { title: "Add Image" });
  231. label.appendChild(ui.createIcon("plus"));
  232. label.className = label.className.replace("secondary", "info");
  233. input.addEventListener("input", function () {
  234. loadFileAsImage(input.files[0]).then((response) => {
  235. if (response.data) {
  236. /** @type {HTMLImageElement} */
  237. const image = response.data;
  238. image.classList.add("transformable");
  239. document.body.appendChild(image);
  240. input.value = null;
  241. }
  242. });
  243. });
  244. row.appendChild(label);
  245. }
  246.  
  247. {
  248. const input = ui.createInput("button");
  249. const label = ui.createLabelFor(input, { title: "Remove all images", style: "margin-left: auto;" });
  250. label.appendChild(ui.createIcon("trash"));
  251. label.className = label.className.replace("secondary", "warning");
  252. input.addEventListener("click", function () {
  253. globalThis["transformable"].target = null;
  254. const transformableImages = ui.querySelectAll(".transformable");
  255. Array.from(transformableImages).forEach((element) => element.remove());
  256. });
  257. row.appendChild(label);
  258. }
  259. }
  260.  
  261. /* Row 2 */ {
  262. const row = ghostcanvas.createRow();
  263.  
  264. // {
  265. // const input = ui.createInput("button");
  266. // const label = ui.createLabelFor(input, { title: "Save Image Position" });
  267. // label.appendChild(ui.createIcon("print"));
  268. // input.addEventListener("click", function () {
  269. // ui.querySelectAll(".transformable").forEach((transformable) => {
  270. // const placed = drawTransformedImage(context, transformable);
  271. // if (!placed) console.debug("%o is out of bounds", transformable);
  272. // });
  273. // });
  274. // row.appendChild(label);
  275. // }
  276.  
  277. {
  278. const input = ui.createInput("button");
  279. const label = ui.createLabelFor(input, { title: "Save Pixels" });
  280. label.appendChild(ui.createIcon("print"));
  281.  
  282. input.addEventListener("click", function () {
  283. ui.querySelectAll(".transformable").forEach((transformable) => {
  284. const placed = drawTransformedImage(context, transformable);
  285. if (!placed) console.debug("%o is out of bounds", transformable);
  286. });
  287. getDefaultPixels(context, ghostcanvas);
  288. });
  289.  
  290. row.appendChild(label);
  291. }
  292.  
  293. {
  294. const input = ui.createInput("text", { title: "Pixels Buffer", readOnly: true, value: 0, id: "pixelbufferDisplay" });
  295.  
  296. input.classList.add("col", "form-control-sm");
  297.  
  298. row.appendChild(input);
  299. }
  300. }
  301.  
  302. /* Row 3 */ {
  303. const row = ghostcanvas.createRow();
  304.  
  305. {
  306. const input = ui.createInput("number", { title: "Color Tolerance", id: "comparePixelsColorTolerance", value: 16, min: 4, max: 128 });
  307. input.classList.add("col", "form-control-sm");
  308. row.appendChild(input);
  309. }
  310.  
  311. {
  312. const input = ui.createInput("button");
  313. const label = ui.createLabelFor(input, { title: "Group by Y" });
  314. const rotatedIcon = ui.createIcon("bars");
  315. rotatedIcon.style.rotate = "90deg";
  316. label.appendChild(rotatedIcon);
  317. input.addEventListener("click", async function () {
  318. if (ghostcanvas.listOfPixels.length < 1) {
  319. await getDefaultPixels(context, ghostcanvas);
  320. }
  321. const sortedPixels = await sortPixelsByPosition.callAsWorker(ghostcanvas.listOfPixels, true);
  322. ghostcanvas.listOfPixels = sortedPixels;
  323. const groupedPixels = await groupPixelsByColor.callAsWorker(sortedPixels, ui.querySelect("#comparePixelsColorTolerance").value, false, areSameColor.toString(), true);
  324. ghostcanvas.listOfPixels = groupedPixels;
  325. ui.querySelect("#pixelbufferDisplay").value = groupedPixels.length;
  326. });
  327. row.appendChild(label);
  328. }
  329.  
  330. {
  331. const input = ui.createInput("button");
  332. const label = ui.createLabelFor(input, { title: "Group by X" });
  333. label.appendChild(ui.createIcon("bars"));
  334. input.addEventListener("click", async function () {
  335. if (ghostcanvas.listOfPixels.length < 1) {
  336. await getDefaultPixels(context, ghostcanvas);
  337. }
  338. const sortedPixels = await sortPixelsByPosition.callAsWorker(ghostcanvas.listOfPixels, false);
  339. ghostcanvas.listOfPixels = sortedPixels;
  340. const groupedPixels = await groupPixelsByColor.callAsWorker(sortedPixels, ui.querySelect("#comparePixelsColorTolerance").value, true, areSameColor.toString(), true);
  341. ghostcanvas.listOfPixels = groupedPixels;
  342. ui.querySelect("#pixelbufferDisplay").value = groupedPixels.length;
  343. });
  344. row.appendChild(label);
  345. }
  346. }
  347.  
  348. /* Row 4 */ {
  349. const row = ghostcanvas.createRow();
  350.  
  351. {
  352. const input = ui.createInput("button");
  353. const label = ui.createLabelFor(input, { title: "Sort by X (Left to Right)" });
  354. label.appendChild(ui.createIcon("arrow-right"));
  355. input.addEventListener("click", async function () {
  356. if (ghostcanvas.listOfPixels.length < 1) {
  357. await getDefaultPixels(context, ghostcanvas);
  358. }
  359. const sortedPixels = await sortPixelsByPosition.callAsWorker(ghostcanvas.listOfPixels, true);
  360. ghostcanvas.listOfPixels = sortedPixels;
  361. ui.querySelect("#pixelbufferDisplay").value = sortedPixels.length;
  362. });
  363. row.appendChild(label);
  364. }
  365.  
  366. {
  367. const input = ui.createInput("button");
  368. const label = ui.createLabelFor(input, { title: "Sort by Y (Top to Bottom)" });
  369. label.appendChild(ui.createIcon("arrow-down"));
  370. input.addEventListener("click", async function () {
  371. if (ghostcanvas.listOfPixels.length < 1) {
  372. await getDefaultPixels(context, ghostcanvas);
  373. }
  374. const sortedPixels = await sortPixelsByPosition.callAsWorker(ghostcanvas.listOfPixels, false);
  375. ghostcanvas.listOfPixels = sortedPixels;
  376. ui.querySelect("#pixelbufferDisplay").value = sortedPixels.length;
  377. });
  378. row.appendChild(label);
  379. }
  380.  
  381. {
  382. const input = ui.createInput("button");
  383. const label = ui.createLabelFor(input, { title: "Sort by Color" });
  384. label.appendChild(ui.createIcon("sort-alpha-down"));
  385. input.addEventListener("click", async function () {
  386. if (ghostcanvas.listOfPixels.length < 1) {
  387. await getDefaultPixels(context, ghostcanvas);
  388. }
  389. const sortedPixels = await sortPixelsByColor.callAsWorker(ghostcanvas.listOfPixels);
  390. ghostcanvas.listOfPixels = sortedPixels;
  391. ui.querySelect("#pixelbufferDisplay").value = sortedPixels.length;
  392. });
  393. row.appendChild(label);
  394. }
  395. }
  396.  
  397. /* Row 5 */ {
  398. ghostcanvas.__contaier.appendChild(ui.querySelect("#chatbox_messages").previousElementSibling.cloneNode(false));
  399. const row = ghostcanvas.createRow();
  400.  
  401. const parseInstructionsInput = ui.createInput("button");
  402. const parseInstructionsLabel = ui.createLabelFor(parseInstructionsInput, { title: "Generate Instructions", textContent: "Load" });
  403. row.appendChild(parseInstructionsLabel);
  404. parseInstructionsInput.addEventListener("click", function () {
  405. 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)));
  406. savedInstructionsCountInput.value = ghostcanvas.instructions.length;
  407. });
  408.  
  409. const savedInstructionsCountInput = ui.createInput("text", { readOnly: true, title: "Total length of Instructions generated", value: 0, id: "savedInstructionsCountInput" });
  410. savedInstructionsCountInput.classList.add("col", "form-control-sm");
  411. row.appendChild(savedInstructionsCountInput);
  412. ghostcanvas.instructions.addEventListener("delete", (prop, val) => {
  413. savedInstructionsCountInput.value = ghostcanvas.instructions.length;
  414. });
  415. }
  416.  
  417. /* Row 6 & 7 */ {
  418. const row6 = ghostcanvas.createRow();
  419. const row7 = ghostcanvas.createRow();
  420.  
  421. const bulkSizeInput = ui.createInput("number", { title: "Bulk Execution Size", min: 1, value: 100 });
  422. bulkSizeInput.classList.add("col", "form-control-sm");
  423. row6.appendChild(bulkSizeInput);
  424.  
  425. const intervalInput = ui.createInput("number", { title: "Wait interval between Executions (Milliseconds)", min: 1, value: 1000 });
  426. intervalInput.classList.add("col", "form-control-sm");
  427. row6.appendChild(intervalInput);
  428.  
  429. {
  430. const input = ui.createInput("checkbox", { id: "gcIsRunning" });
  431. const label = ui.createLabelFor(input, { textContent: "Start" });
  432. label.classList.add("col");
  433. label.className = label.className.replace("secondary", "success");
  434. input.addEventListener("input", function () {
  435. ghostcanvas.instructions.isRunning = input.checked;
  436. execute(ghostcanvas.instructions, player.send, Number(intervalInput.value), Number(bulkSizeInput.value)).finally(() => {
  437. input.checked = false;
  438. label.classList.remove("active");
  439. ui.querySelect("#savedInstructionsCountInput").value = ghostcanvas.instructions.length;
  440. });
  441. });
  442. row7.appendChild(label);
  443. }
  444.  
  445. {
  446. const input = ui.createInput("button");
  447. const label = ui.createLabelFor(input, { textContent: "Stop" });
  448. label.classList.add("col");
  449. label.className = label.className.replace("secondary", "warning");
  450. input.addEventListener("click", function () {
  451. ui.querySelect("#gcIsRunning").checked = false;
  452. ui.querySelect("#gcIsRunning").parentElement.classList.remove("active");
  453. ghostcanvas.instructions.isRunning = false;
  454. });
  455. row7.appendChild(label);
  456. }
  457.  
  458. {
  459. const input = ui.createInput("button");
  460. const label = ui.createLabelFor(input, { textContent: "Step" });
  461. label.classList.add("col");
  462. input.addEventListener("click", function () {
  463. player.send(ghostcanvas.instructions.shift());
  464. });
  465. row7.appendChild(label);
  466. }
  467.  
  468. {
  469. const input = ui.createInput("button");
  470. const label = ui.createLabelFor(input, { textContent: "Reset" });
  471. label.classList.add("col");
  472. label.className = label.className.replace("secondary", "danger");
  473. input.addEventListener("click", function () {
  474. ghostcanvas.instructions.length = 0;
  475. ui.querySelect("#gcIsRunning").checked = false;
  476. ui.querySelect("#gcIsRunning").parentElement.classList.remove("active");
  477. ui.querySelect("#savedInstructionsCountInput").value = 0;
  478. });
  479. row7.appendChild(label);
  480. }
  481. }
  482.  
  483. updatePositionAlt(originalCanvas, canvas);
  484. canvas.classList.add("d-none");
  485. ui.querySelect("#definableStyles").textContent += "\n.transformable { position: fixed; top: 0px; left: 0px; }";
  486. }
  487.  
  488. async function getDefaultPixels(context, ghostcanvas) {
  489. const [imageData, width] = getImageDataForProcessing(context);
  490. const allPixels = await convertImageDataToPixels.callAsWorker(imageData, width);
  491. const nonTransparentPixels = await filterForNonTransparentPixels.callAsWorker(allPixels, 128);
  492.  
  493. ghostcanvas.listOfPixels = nonTransparentPixels;
  494. document.querySelector("#pixelbufferDisplay").value = nonTransparentPixels.length;
  495. return true;
  496. }
  497.  
  498. /**
  499. * @param {HTMLCanvasElement} canvas
  500. * @returns {CanvasRenderingContext2D}
  501. */
  502. function getOptimizedRenderingContext(canvas) {
  503. if (canvas.optimizedRenderingContext) return canvas.optimizedRenderingContext;
  504. const context = canvas.getContext("2d", {
  505. alpha: true,
  506. willReadFrequently: true,
  507. });
  508. canvas.optimizedRenderingContext = context;
  509. return context;
  510. }
  511.  
  512. /**
  513. * @param {File} file
  514. * @returns {Promise<PromiseResponse>}
  515. */
  516. function loadFileAsImage(file) {
  517. return new Promise((resolve, reject) => {
  518. if (!(FileReader && file)) {
  519. reject({ data: undefined, error: "Native FileReader not present." });
  520. } else {
  521. const reader = new FileReader();
  522. reader.onload = function () {
  523. const image = new Image();
  524. image.src = reader.result;
  525. image.onload = function () {
  526. resolve({ data: image, error: undefined });
  527. };
  528. };
  529. reader.readAsDataURL(file);
  530. }
  531. });
  532. }
  533.  
  534. /**
  535. * @param {CanvasRenderingContext2D} context
  536. * @returns {[Uint8ClampedArray, number]}
  537. */
  538. function getImageDataForProcessing(context) {
  539. return [context.getImageData(0, 0, context.canvas.width, context.canvas.height).data, context.canvas.width];
  540. }
  541.  
  542. /**
  543. * Get Pixels
  544. * @param {Uint8ClampedArray} imageData
  545. * @param {number} width
  546. * @returns {Array<Pixel>}
  547. */
  548. function convertImageDataToPixels(imageData, width) {
  549. const pixelCount = imageData.length / 4;
  550. const pixels = new Array(pixelCount);
  551. const widthFactor = 1 / width;
  552.  
  553. for (let i = 0; i < pixelCount; i++) {
  554. const index = i * 4;
  555. const x = i % width;
  556. const y = (i * widthFactor) | 0;
  557. const [r, g, b, a] = imageData.slice(index, index + 4);
  558. pixels[i] = { x, y, r, g, b, a };
  559. }
  560.  
  561. return pixels;
  562. }
  563.  
  564. /**
  565. * Filter for NonTransparent Pixels
  566. * @param {Array<Pixel>} pixels
  567. * @param {number} [threshold=64]
  568. * @returns {Array<Pixel>}
  569. */
  570. function filterForNonTransparentPixels(pixels, threshold = 64) {
  571. return pixels.filter(({ a: alpha }) => alpha > threshold);
  572. }
  573.  
  574. /**
  575. * Check if the two provided Pixels are close to similar in the rgb spectrum.
  576. * @param {Pixel} pixel1 - The first color object with r, g, b, and a properties.
  577. * @param {Pixel} pixel2 - The second color object with r, g, b, and a properties.
  578. * @param {number} tolerance - The maximum allowed difference for each color component.
  579. * @param {boolean} [ignoreAphaValue=true] - Should Alphavalues be ignored.
  580. * @returns {boolean} - True if the colors are considered the same, false otherwise.
  581. */
  582. function areSameColor(pixel1, pixel2, tolerance, ignoreAphaValue = true) {
  583. const dr = Math.abs(pixel1.r - pixel2.r);
  584. const dg = Math.abs(pixel1.g - pixel2.g);
  585. const db = Math.abs(pixel1.b - pixel2.b);
  586. const da_ok = !ignoreAphaValue ? Math.abs(pixel1.a - pixel2.a) <= tolerance : true;
  587.  
  588. return dr <= tolerance && dg <= tolerance && db <= tolerance && da_ok;
  589. }
  590.  
  591. /**
  592. * Combine Pixels to Areas by proximity and color.
  593. * @param {Array<Pixel>} data - Array of Pixel objects.
  594. * @param {number} [tolerance=32] - The maximum allowed difference for each color component.
  595. * @param {boolean} [groupByY=true] - If true, group by y then x; if false, group by y then x.
  596. * @param {string} comparisonFunctionAsString - The Function to be used for Color Comparison as a string.
  597. * @param {boolean} [ignoreAphaValue=true] - Should Alphavalues be ignored.
  598. * @returns {Array<Area>} - Array of Area objects.
  599. */
  600. function groupPixelsByColor(data, tolerance = 32, groupByY = true, comparisonFunctionAsString = undefined, ignoreAphaValue = true) {
  601. if (data.length === 0) return [];
  602.  
  603. const comparisonFunction =
  604. comparisonFunctionAsString && typeof comparisonFunctionAsString === "string"
  605. ? new Function("return (" + comparisonFunctionAsString + ")(...arguments)")
  606. : areSameColor ?? new Function("return false;");
  607.  
  608. const nameOfValueToIncrease = groupByY ? "width" : "height";
  609. const nameOfValueToBeSimilar = groupByY ? "y" : "x";
  610. const nameOfValueToNOTBeSimilar = !groupByY ? "y" : "x";
  611.  
  612. data.sort();
  613.  
  614. const lines = new Array(data.length);
  615. let newLineIndex = 0;
  616. let currentLine = {
  617. x: data[0].x,
  618. y: data[0].y,
  619. width: 0,
  620. height: 0,
  621. r: data[0].r,
  622. g: data[0].g,
  623. b: data[0].b,
  624. a: data[0].a,
  625. };
  626.  
  627. for (let i = 1; i < data.length; i++) {
  628. const pixel = data[i];
  629. const lastPixel = data[i ? i - 1 : 0];
  630. if (
  631. pixel[nameOfValueToBeSimilar] === currentLine[nameOfValueToBeSimilar] &&
  632. comparisonFunction(currentLine, pixel, tolerance, ignoreAphaValue) &&
  633. Math.abs(lastPixel[nameOfValueToNOTBeSimilar] - pixel[nameOfValueToNOTBeSimilar]) < 2
  634. ) {
  635. currentLine[nameOfValueToIncrease]++;
  636. } else {
  637. lines[newLineIndex++] = currentLine;
  638. currentLine = {
  639. x: pixel.x,
  640. y: pixel.y,
  641. width: 1,
  642. height: 1,
  643. r: pixel.r,
  644. g: pixel.g,
  645. b: pixel.b,
  646. a: pixel.a,
  647. };
  648. }
  649. }
  650.  
  651. // Push the last line
  652. lines[newLineIndex] = currentLine;
  653.  
  654. return lines.slice(0, newLineIndex + 1);
  655. }
  656.  
  657. /**
  658. * Sort Pixellike Object by their Position.
  659. * @param {Array<Pixel>} pixels - Array of Pixel objects.
  660. * @param {boolean} [sortByX=true] - If true, sort by x then y; if false, sort by y then x.
  661. * @returns {Array<Pixel>} - Sorted array of Pixel objects.
  662. */
  663. function sortPixelsByPosition(pixels, sortByX = true) {
  664. const sortFunction = sortByX
  665. ? function (a, b) {
  666. return a.x - b.x || a.y - b.y;
  667. }
  668. : function (a, b) {
  669. return a.y - b.y || a.x - b.x;
  670. };
  671. return pixels.sort(sortFunction);
  672. }
  673.  
  674. /**
  675. * Sort Pixellike Object by their Color.
  676. * @param {Array<Pixel>} pixels - Array of Pixel objects.
  677. * @returns {Array<Pixel>}
  678. */
  679. function sortPixelsByColor(pixels) {
  680. return pixels.sort(function (a, b) {
  681. return a.r - b.r || a.g - b.g || a.b - b.b;
  682. });
  683. }
  684.  
  685. /**
  686. * Get the Color of the Pixel formatted as rgb.
  687. * @param {Pixel|Area} pixel
  688. */
  689. function getPixelColorAsRGB(pixel) {
  690. return `rgb(${pixel.r},${pixel.g},${pixel.b})`;
  691. }
  692. /**
  693. * Check if two HTMLElements are overlapping over each other.
  694. * @param {HTMLElement} element1
  695. * @param {HTMLElement} element2
  696. */
  697. function areOverlapping(element1, element2) {
  698. const bbox1 = element1.getBoundingClientRect();
  699. const bbox2 = element2.getBoundingClientRect();
  700. return bbox1.bottom > bbox2.top || bbox1.right > bbox2.left || bbox1.top < bbox2.bottom || bbox1.left < bbox2.right;
  701. }
  702.  
  703. /**
  704. * Get the position of the image relative to the canvas.
  705. * @param {HTMLCanvasElement} canvas
  706. * @param {HTMLImageElement} image
  707. * @returns
  708. */
  709. function getImagePositionRelativeToCanvas(canvas, image) {
  710. const rect = canvas.getBoundingClientRect();
  711. const style = window.getComputedStyle(image);
  712. const x = parseFloat(style.left);
  713. const y = parseFloat(style.top);
  714. return [x - rect.left, y - rect.top];
  715. }
  716.  
  717. /**
  718. * Draw the image on the canvas with its transformations.
  719. * @param {CanvasRenderingContext2D} context
  720. * @param {HTMLImageElement} image
  721. */
  722. function drawTransformedImage(context, image) {
  723. if (!areOverlapping(context.canvas, image)) return false;
  724.  
  725. // Get the computed style of the image
  726. const style = window.getComputedStyle(image);
  727. const transform = style.transform;
  728. const width = parseFloat(style.width);
  729. const height = parseFloat(style.height);
  730.  
  731. // Save the current context state
  732. context.save();
  733.  
  734. // Calculate the position of the image on the canvas
  735. const [x, y] = getImagePositionRelativeToCanvas(context.canvas, image);
  736.  
  737. // Calculate the scaling factor
  738. const scaleX = context.canvas.width / context.canvas.clientWidth;
  739. const scaleY = context.canvas.height / context.canvas.clientHeight;
  740.  
  741. // Apply the CSS transform to the canvas context
  742. context.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
  743. const matrix = new DOMMatrix(transform);
  744.  
  745. // Translate to the center of the image
  746. context.translate((x + width / 2) * scaleX, (y + height / 2) * scaleY);
  747. // Apply the rotation
  748. context.transform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f);
  749. // Translate back
  750. context.translate((-width / 2) * scaleX, (-height / 2) * scaleY);
  751.  
  752. // Draw the image on the canvas
  753. context.drawImage(image, 0, 0, width * scaleX, height * scaleY);
  754.  
  755. // Restore the context to its original state
  756. context.restore();
  757. return true;
  758. }
  759.  
  760. /**
  761. * @param {HTMLElement} parent
  762. * @param {HTMLElement} child
  763. */
  764. function updatePosition(parent, child) {
  765. const rect = parent.getBoundingClientRect();
  766. child.style.top = rect.top.toFixed(0) + "px";
  767. child.style.left = rect.left.toFixed(0) + "px";
  768. child.style.width = rect.width.toFixed(0) + "px";
  769. child.style.height = rect.height.toFixed(0) + "px";
  770. child.width = 1000;
  771. child.height = 1000;
  772. }
  773.  
  774. function updatePositionAlt(parent, child) {
  775. const rect = parent.getBoundingClientRect();
  776. child.style.top = rect.top.toFixed(0) + "px";
  777. child.style.left = rect.left.toFixed(0) + "px";
  778. child.width = rect.width.toFixed(0);
  779. child.height = rect.height.toFixed(0);
  780. }
  781.  
  782. /**
  783. * @param {string[]} instructions
  784. * @param {Function} callback
  785. * @param {number} [interval=1000]
  786. * @param {number} [bulkSize=100]
  787. */
  788. function execute(instructions, callback, interval = 1000, bulkSize = 100) {
  789. return new Promise((resolve, reject) => {
  790. if (!instructions || !callback) {
  791. reject();
  792. return;
  793. }
  794. if (!instructions.isRunning || !instructions.length) {
  795. resolve();
  796. return;
  797. }
  798. const intervalID = setInterval(() => {
  799. instructions.splice(0, bulkSize).forEach((s) => {
  800. callback(s);
  801. });
  802. if (!instructions.length || !instructions.isRunning) {
  803. clearInterval(intervalID);
  804. instructions.isRunning = false;
  805. resolve();
  806. return;
  807. }
  808. }, interval);
  809. });
  810. }
  811.  
  812. window.addEventListener("definable:init", function (event) {
  813. /** @type {Definable} */
  814. const main = event.detail.main;
  815. /** @type {DefinableCore} */
  816. const core = event.detail.core;
  817. initialize(main, core);
  818. });
  819. })();