From 42a1266da55e867ad945e1b36a198b747bece967 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sun, 27 Nov 2022 09:48:13 -0300 Subject: [PATCH 01/11] Add custom errors Signed-off-by: Victor Seiji Hariki --- index.html | 1 + js/error.js | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 js/error.js diff --git a/index.html b/index.html index dffa80b..aad308e 100644 --- a/index.html +++ b/index.html @@ -284,6 +284,7 @@ + diff --git a/js/error.js b/js/error.js new file mode 100644 index 0000000..3c1e61d --- /dev/null +++ b/js/error.js @@ -0,0 +1,7 @@ +/** + * This is a file to configure custom errors + */ + +/* Proxy Restriction Errors */ +class ProxyReadOnlySetError extends Error {} +class ProxyWriteOnceSetError extends Error {} From 6ec27df919a32359276b4b273334ea8defaf3460 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sun, 27 Nov 2022 09:49:02 -0300 Subject: [PATCH 02/11] add some guards to input handlers to easier debug Sometimes before this I used mouse.listen.window.left.onclick = () => {thing}. Now it throws an error telling me not to do this. Signed-off-by: Victor Seiji Hariki --- js/input.js | 57 +++++++++++++++++++++++++++++------------------------ js/util.js | 26 ++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 26 deletions(-) diff --git a/js/input.js b/js/input.js index 5abed16..0492f9c 100644 --- a/js/input.js +++ b/js/input.js @@ -10,29 +10,32 @@ const inputConfig = { * Mouse input processing */ // Base object generator functions -function _mouse_observers() { - return { - // Simple click handler - onclick: new Observer(), - // Double click handler (will still trigger simple click handler as well) - ondclick: new Observer(), - // Drag handler - ondragstart: new Observer(), - ondrag: new Observer(), - ondragend: new Observer(), - // Paint handler (like drag handler, but with no delay); will trigger during clicks too - onpaintstart: new Observer(), - onpaint: new Observer(), - onpaintend: new Observer(), - }; +function _mouse_observers(name = "generic_mouse_observer_array") { + return makeReadOnly( + { + // Simple click handler + onclick: new Observer(), + // Double click handler (will still trigger simple click handler as well) + ondclick: new Observer(), + // Drag handler + ondragstart: new Observer(), + ondrag: new Observer(), + ondragend: new Observer(), + // Paint handler (like drag handler, but with no delay); will trigger during clicks too + onpaintstart: new Observer(), + onpaint: new Observer(), + onpaintend: new Observer(), + }, + name + ); } const mouse = { - contexts: [], + _contexts: [], buttons: {}, - coords: {}, + coords: makeWriteOnce({}, "mouse.coords"), - listen: {}, + listen: makeWriteOnce({}, "mouse.listen"), // Register Context registerContext: (name, onmove, options = {}) => { @@ -76,7 +79,9 @@ const mouse = { Object.keys(options.buttons).forEach((index) => { const button = options.buttons[index]; mouse.coords[name].dragging[button] = null; - mouse.listen[name][button] = _mouse_observers(); + mouse.listen[name][button] = _mouse_observers( + `mouse.listen[${name}][${button}]` + ); }); // Add to context @@ -84,7 +89,7 @@ const mouse = { context.listen = mouse.listen[name]; // Add to list - mouse.contexts.push(context); + mouse._contexts.push(context); return context; }, @@ -98,7 +103,7 @@ window.onmousedown = (evn) => { if (_double_click_timeout[evn.button]) { // ondclick event - mouse.contexts.forEach(({target, name, buttons}) => { + mouse._contexts.forEach(({target, name, buttons}) => { if ((!target || target === evn.target) && buttons[evn.button]) mouse.listen[name][buttons[evn.button]].ondclick.emit({ target: evn.target, @@ -119,7 +124,7 @@ window.onmousedown = (evn) => { // Set drag start timeout _drag_start_timeout[evn.button] = setTimeout(() => { - mouse.contexts.forEach(({target, name, buttons}) => { + mouse._contexts.forEach(({target, name, buttons}) => { const key = buttons[evn.button]; if ( (!target || target === evn.target) && @@ -143,7 +148,7 @@ window.onmousedown = (evn) => { mouse.buttons[evn.button] = time; - mouse.contexts.forEach(({target, name, buttons}) => { + mouse._contexts.forEach(({target, name, buttons}) => { const key = buttons[evn.button]; if ((!target || target === evn.target) && key) { mouse.coords[name].dragging[key] = {}; @@ -166,7 +171,7 @@ window.onmousedown = (evn) => { window.onmouseup = (evn) => { const time = performance.now(); - mouse.contexts.forEach(({target, name, buttons}) => { + mouse._contexts.forEach(({target, name, buttons}) => { const key = buttons[evn.button]; if ( (!target || target === evn.target) && @@ -235,7 +240,7 @@ window.onmouseup = (evn) => { }; window.onmousemove = (evn) => { - mouse.contexts.forEach((context) => { + mouse._contexts.forEach((context) => { const target = context.target; const name = context.name; @@ -323,7 +328,7 @@ window.onmousemove = (evn) => { window.addEventListener( "wheel", (evn) => { - mouse.contexts.forEach(({name}) => { + mouse._contexts.forEach(({name}) => { mouse.listen[name].onwheel.emit({ target: evn.target, delta: evn.deltaY, diff --git a/js/util.js b/js/util.js index 0069c7a..3ba25da 100644 --- a/js/util.js +++ b/js/util.js @@ -52,6 +52,32 @@ function defaultOpt(options, defaults) { }); } +/** + * Make object read-only + */ +function makeReadOnly(obj, name = "read-only object") { + return new Proxy(obj, { + set: (obj, prop, value) => { + throw new ProxyReadOnlySetError( + `Tried setting the '${prop}' property on '${name}'` + ); + }, + }); +} + +// Makes an object so you can't rewrite already written values +function makeWriteOnce(obj, name = "write-once object") { + return new Proxy(obj, { + set: (obj, prop, value) => { + if (obj[prop] !== undefined) + throw new ProxyWriteOnceSetError( + `Tried setting the '${prop}' property on '${name}' after it was already set` + ); + obj[prop] = value; + }, + }); +} + /** * Bounding box Calculation */ From 8d0b44e36b6534215c630d85106de057a4abe662 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 28 Nov 2022 12:11:19 -0300 Subject: [PATCH 03/11] Some jsdoc for util Also allows for downloadCanvas to open image in new tab. This may be useful for debugging anything that has a canvas (such as overmasking). Signed-off-by: Victor Seiji Hariki --- js/jsconfig.json | 7 ++++++ js/util.js | 64 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 js/jsconfig.json diff --git a/js/jsconfig.json b/js/jsconfig.json new file mode 100644 index 0000000..802b39e --- /dev/null +++ b/js/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6" + }, + "include": ["**/*.js"] +} diff --git a/js/util.js b/js/util.js index 3ba25da..e2fc497 100644 --- a/js/util.js +++ b/js/util.js @@ -27,7 +27,10 @@ Observer.prototype = { }; /** - * Generates unique id + * Generates a simple UID in the format xxxx-xxxx-...-xxxx, with x being [0-9a-f] + * + * @param {number} size Number of quartets of characters to generate + * @returns {string} The new UID */ const guid = (size = 3) => { const s4 = () => { @@ -43,17 +46,25 @@ const guid = (size = 3) => { }; /** - * Default option set + * Assigns defaults to an option object passed to the function. + * + * @param {{[key: string]: any}} options Original options object + * @param {{[key: string]: any}} defaults Default values to assign */ - function defaultOpt(options, defaults) { Object.keys(defaults).forEach((key) => { if (options[key] === undefined) options[key] = defaults[key]; }); } +/** Custom error for attempt to set read-only objects */ +class ProxyReadOnlySetError extends Error {} /** - * Make object read-only + * Makes a given object read-only; throws a ProxyReadOnlySetError exception if modification is attempted + * + * @param {any} obj Object to be proxied + * @param {string} name Name for logging purposes + * @returns {any} Proxied object, intercepting write attempts */ function makeReadOnly(obj, name = "read-only object") { return new Proxy(obj, { @@ -65,7 +76,15 @@ function makeReadOnly(obj, name = "read-only object") { }); } -// Makes an object so you can't rewrite already written values +/** Custom error for attempt to set write-once objects a second time */ +class ProxyWriteOnceSetError extends Error {} +/** + * Makes a given object write-once; Attempts to overwrite an existing prop in the object will throw a ProxyWriteOnceSetError exception + * + * @param {any} obj Object to be proxied + * @param {string} name Name for logging purposes + * @returns {any} Proxied object, intercepting write attempts + */ function makeWriteOnce(obj, name = "write-once object") { return new Proxy(obj, { set: (obj, prop, value) => { @@ -79,7 +98,12 @@ function makeWriteOnce(obj, name = "write-once object") { } /** - * Bounding box Calculation + * Snaps a single value to an infinite grid + * + * @param {number} i Original value to be snapped + * @param {boolean} scaled If grid will change alignment for odd scaleFactor values (default: true) + * @param {number} gridSize Size of the grid + * @returns an offset, in which [i + offset = (a location snapped to the grid)] */ function snap(i, scaled = true, gridSize = 64) { // very cheap test proof of concept but it works surprisingly well @@ -100,6 +124,16 @@ function snap(i, scaled = true, gridSize = 64) { return -snapOffset; } +/** + * Gets a bounding box centered on a given set of coordinates. Supports grid snapping + * + * @param {number} cx x-coordinate of the center of the box + * @param {number} cy y-coordinate of the center of the box + * @param {number} w the width of the box + * @param {height} h the height of the box + * @param {number | null} gridSnap The size of the grid to snap to + * @returns {BoundingBox} A bounding box object centered at (cx, cy) + */ function getBoundingBox(cx, cy, w, h, gridSnap = null) { const offset = {x: 0, y: 0}; const box = {x: 0, y: 0}; @@ -122,6 +156,12 @@ function getBoundingBox(cx, cy, w, h, gridSnap = null) { /** * Triggers Canvas Download */ +/** + * Crops a given canvas to content, returning a new canvas object with the content in it. + * + * @param {HTMLCanvasElement} sourceCanvas Canvas to get a content crop from + * @returns {HTMLCanvasElement} A new canvas with the cropped part of the image + */ function cropCanvas(sourceCanvas) { var w = sourceCanvas.width; var h = sourceCanvas.height; @@ -169,6 +209,15 @@ function cropCanvas(sourceCanvas) { return cutCanvas; } +/** + * Downloads the content of a canvas to the disk, or opens it + * + * @param {{cropToContent: boolean, canvas: HTMLCanvasElement, filename: string}} options A options array with the following:\ + * cropToContent: If we wish to crop to content first (default: true) + * canvas: The source canvas (default: imgCanvas) + * filename: The filename to save as (default: '[ISO date] [Hours] [Minutes] [Seconds] openOutpaint image.png').\ + * If null, opens image in new tab. + */ function downloadCanvas(options = {}) { defaultOpt(options, { cropToContent: true, @@ -182,7 +231,8 @@ function downloadCanvas(options = {}) { }); var link = document.createElement("a"); - link.download = options.filename; + link.target = "_blank"; + if (options.filename) link.download = options.filename; var croppedCanvas = options.cropToContent ? cropCanvas(options.canvas) From 54c381de8eed48cdb3cfcaee939d4c2f8f43d875 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 28 Nov 2022 12:17:07 -0300 Subject: [PATCH 04/11] added jsdoc to commands Signed-off-by: Victor Seiji Hariki --- css/index.css | 8 +- index.html | 1 - js/commands.js | 244 +++++++++++++++++++++++++++-------------------- js/error.js | 7 -- js/ui/history.js | 4 +- js/util.js | 89 +++++++++++------ 6 files changed, 204 insertions(+), 149 deletions(-) delete mode 100644 js/error.js diff --git a/css/index.css b/css/index.css index 13bc49a..e57686e 100644 --- a/css/index.css +++ b/css/index.css @@ -241,7 +241,7 @@ div.prompt-wrapper > textarea:focus { border-bottom-right-radius: 5px; } -.button-array > .button.tool { +.button.tool { background-color: rgb(0, 0, 50); color: rgb(255, 255, 255); cursor: pointer; @@ -254,15 +254,15 @@ div.prompt-wrapper > textarea:focus { margin-bottom: 5px; } -.button-array > .button.tool:disabled { +.button.tool:disabled { background-color: #666 !important; cursor: default; } -.button-array > .button.tool:hover { +.button.tool:hover { background-color: rgb(30, 30, 80); } -.button-array > .button.tool:active, +.button.tool:active, .button.tool.active { background-color: rgb(60, 60, 130); } diff --git a/index.html b/index.html index aad308e..dffa80b 100644 --- a/index.html +++ b/index.html @@ -284,7 +284,6 @@ - diff --git a/js/commands.js b/js/commands.js index d63923d..7f16c06 100644 --- a/js/commands.js +++ b/js/commands.js @@ -4,122 +4,158 @@ const _commands_events = new Observer(); -const commands = { - current: -1, - history: [], - undo(n = 1) { - for (var i = 0; i < n && this.current > -1; i++) { - this.history[this.current--].undo(); - } - }, - redo(n = 1) { - for (var i = 0; i < n && this.current + 1 < this.history.length; i++) { - this.history[++this.current].redo(); - } - }, +/** Global Commands Object */ +const commands = makeReadOnly( + { + /** Current History Index Reader */ + get current() { + return this._current; + }, + /** Current History Index (private) */ + _current: -1, + /** Command History (private) */ + _history: [], + /** The types of commands we can run (private) */ + _types: {}, - /** - * These are basic commands that can be done/undone - * - * They must contain a 'run' method that performs the action the first time, - * a 'undo' method that undoes that action and a 'redo' method that does the - * action again, but without requiring parameters. 'redo' is by default the - * same as 'run'. - * - * The 'run' and 'redo' functions will receive a 'options' parameter which will be - * forwarded directly to the operation, and a 'state' parameter that - * can be used to store state for undoing things. - * - * The 'state' object will be passed to the 'undo' function as well. - */ - createCommand(name, run, undo, redo = run) { - const command = function runWrapper(title, options) { - // Create copy of options and state object - const copy = {}; - Object.assign(copy, options); - const state = {}; - - const entry = { - id: guid(), - title, - state, - }; - - // Attempt to run command - try { - run(title, copy, state); - } catch (e) { - console.warn(`Error while running command '${name}' with options:`); - console.warn(copy); - console.warn(e); - return; + /** + * Undoes the last commands in the history + * + * @param {number} n Number of actions to undo + */ + undo(n = 1) { + for (var i = 0; i < n && this.current > -1; i++) { + this._history[this._current--].undo(); } + }, + /** + * Redoes the next commands in the history + * + * @param {number} n Number of actions to redo + */ + redo(n = 1) { + for (var i = 0; i < n && this.current + 1 < this._history.length; i++) { + this._history[++this._current].redo(); + } + }, + + /** + * Creates a basic command, that can be done and undone + * + * They must contain a 'run' method that performs the action for the first time, + * a 'undo' method that undoes that action and a 'redo' method that does the + * action again, but without requiring parameters. 'redo' is by default the + * same as 'run'. + * + * The 'run' and 'redo' functions will receive a 'options' parameter which will be + * forwarded directly to the operation, and a 'state' parameter that + * can be used to store state for undoing things. + * + * The 'state' object will be passed to the 'undo' function as well. + * + * @param {string} name Command identifier (name) + * @param {(title: string, options: any, state: {[key: string]: any}) => void | Promise} run A method that performs the action for the first time + * @param {(title: string, state: {[key: string]: any}) => } undo A method that reverses what the run method did + * @param {(title: string, options: any, state: {[key: string]: any}) => void | Promise} redo A method that redoes the action after undone (default: run) + * @returns + */ + createCommand(name, run, undo, redo = run) { + const command = async function runWrapper(title, options) { + // Create copy of options and state object + const copy = {}; + Object.assign(copy, options); + const state = {}; + + const entry = { + id: guid(), + title, + state, + }; + + // Attempt to run command + try { + await run(title, copy, state); + } catch (e) { + console.warn(`Error while running command '${name}' with options:`); + console.warn(copy); + console.warn(e); + return; + } + + const undoWrapper = () => { + console.debug(`Undoing ${name}, currently ${this._current}`); + undo(title, state); + _commands_events.emit({ + id: entry.id, + name, + action: "undo", + state, + current: this._current, + }); + }; + const redoWrapper = () => { + console.debug(`Redoing ${name}, currently ${this._current}`); + redo(title, copy, state); + _commands_events.emit({ + id: entry.id, + name, + action: "redo", + state, + current: this._current, + }); + }; + + // Add to history + if (commands._history.length > commands._current + 1) { + commands._history.forEach((entry, index) => { + if (index >= commands._current + 1) + _commands_events.emit({ + id: entry.id, + name, + action: "deleted", + state, + current: this._current, + }); + }); + + commands._history.splice(commands._current + 1); + } + + commands._history.push(entry); + commands._current++; + + entry.undo = undoWrapper; + entry.redo = redoWrapper; - const undoWrapper = () => { - console.debug(`Undoing ${name}, currently ${commands.current}`); - undo(title, state); _commands_events.emit({ id: entry.id, name, - action: "undo", + action: "run", state, - current: commands.current, - }); - }; - const redoWrapper = () => { - console.debug(`Redoing ${name}, currently ${commands.current}`); - redo(title, copy, state); - _commands_events.emit({ - id: entry.id, - name, - action: "redo", - state, - current: commands.current, + current: commands._current, }); + + return entry; }; - // Add to history - if (commands.history.length > commands.current + 1) { - commands.history.forEach((entry, index) => { - if (index >= commands.current + 1) - _commands_events.emit({ - id: entry.id, - name, - action: "deleted", - state, - current: commands.current, - }); - }); + this._types[name] = command; - commands.history.splice(commands.current + 1); - } - - commands.history.push(entry); - commands.current++; - - entry.undo = undoWrapper; - entry.redo = redoWrapper; - - _commands_events.emit({ - id: entry.id, - name, - action: "run", - state, - current: commands.current, - }); - - return entry; - }; - - this.types[name] = command; - - return command; + return command; + }, + /** + * Runs a command + * + * @param {string} name The name of the command to run + * @param {string} title The display name of the command on the history panel view + * @param {any} options The options to be sent to the command to be run + */ + runCommand(name, title, options = null) { + this._types[name](title, options); + }, }, - runCommand(name, title, options) { - this.types[name](title, options); - }, - types: {}, -}; + "commands", + ["_current"] +); /** * Draw Image Command, used to draw a Image to a context diff --git a/js/error.js b/js/error.js deleted file mode 100644 index 3c1e61d..0000000 --- a/js/error.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * This is a file to configure custom errors - */ - -/* Proxy Restriction Errors */ -class ProxyReadOnlySetError extends Error {} -class ProxyWriteOnceSetError extends Error {} diff --git a/js/ui/history.js b/js/ui/history.js index ebc37f7..fbc08f9 100644 --- a/js/ui/history.js +++ b/js/ui/history.js @@ -28,7 +28,7 @@ if (message.action === "run") { Array.from(historyView.children).forEach((child) => { if ( - !commands.history.find((entry) => `hist-${entry.id}` === child.id) + !commands._history.find((entry) => `hist-${entry.id}` === child.id) ) { console.log("Removing " + child.id); historyView.removeChild(child); @@ -36,7 +36,7 @@ }); } - commands.history.forEach((entry, index) => { + commands._history.forEach((entry, index) => { if (!document.getElementById(`hist-${entry.id}`)) { console.log("Inserting " + entry.id); historyView.appendChild( diff --git a/js/util.js b/js/util.js index e2fc497..bf83c07 100644 --- a/js/util.js +++ b/js/util.js @@ -1,30 +1,50 @@ /** - * Implementation of a simple Oberver Pattern for custom event handling + * Observer class */ -function Observer() { - this.handlers = new Set(); -} +class Observer { + /** + * List of handlers + * @type {Set<(msg: any) => void | Promise>} + */ + _handlers = new Set(); -Observer.prototype = { - // Adds handler for this message + /** + * Adds a observer to the events + * + * @param {(msg: any) => void | Promise} callback The function to run when receiving a message + * @returns {(msg:any) => void | Promise} The callback we received + */ on(callback) { - this.handlers.add(callback); + this._handlers.add(callback); return callback; - }, + } + /** + * Removes a observer + * + * @param {(msg: any) => void | Promise} callback The function used to register the callback + * @returns {boolean} Whether the handler existed + */ clear(callback) { - return this.handlers.delete(callback); - }, - emit(msg) { - this.handlers.forEach(async (handler) => { - try { - await handler(msg); - } catch (e) { - console.warn("Observer failed to run handler"); - console.warn(e); - } - }); - }, -}; + return this._handlers.delete(callback); + } + /** + * Send a message to all observers + * + * @param {any} msg The message to send to the observers + */ + async emit(msg) { + Promise.all( + Array.from(this._handlers).map((handler) => async () => { + try { + await handler(msg); + } catch (e) { + console.warn("Observer failed to run handler"); + console.warn(e); + } + }) + ); + } +} /** * Generates a simple UID in the format xxxx-xxxx-...-xxxx, with x being [0-9a-f] @@ -62,16 +82,21 @@ class ProxyReadOnlySetError extends Error {} /** * Makes a given object read-only; throws a ProxyReadOnlySetError exception if modification is attempted * - * @param {any} obj Object to be proxied + * @template T Object Type + * + * @param {T} obj Object to be proxied * @param {string} name Name for logging purposes - * @returns {any} Proxied object, intercepting write attempts + * @param {string[]} exceptions Parameters excepted from this restriction + * @returns {T} Proxied object, intercepting write attempts */ -function makeReadOnly(obj, name = "read-only object") { +function makeReadOnly(obj, name = "read-only object", exceptions = []) { return new Proxy(obj, { set: (obj, prop, value) => { - throw new ProxyReadOnlySetError( - `Tried setting the '${prop}' property on '${name}'` - ); + if (!exceptions.some((v) => v === prop)) + throw new ProxyReadOnlySetError( + `Tried setting the '${prop}' property on '${name}'` + ); + obj[prop] = value; }, }); } @@ -81,14 +106,16 @@ class ProxyWriteOnceSetError extends Error {} /** * Makes a given object write-once; Attempts to overwrite an existing prop in the object will throw a ProxyWriteOnceSetError exception * - * @param {any} obj Object to be proxied + * @template T Object Type + * @param {T} obj Object to be proxied * @param {string} name Name for logging purposes - * @returns {any} Proxied object, intercepting write attempts + * @param {string[]} exceptions Parameters excepted from this restriction + * @returns {T} Proxied object, intercepting write attempts */ -function makeWriteOnce(obj, name = "write-once object") { +function makeWriteOnce(obj, name = "write-once object", exceptions = []) { return new Proxy(obj, { set: (obj, prop, value) => { - if (obj[prop] !== undefined) + if (obj[prop] !== undefined && !exceptions.some((v) => v === prop)) throw new ProxyWriteOnceSetError( `Tried setting the '${prop}' property on '${name}' after it was already set` ); From 4d874b5c9ab1e45beb269dfe161095f53ac0a713 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 28 Nov 2022 12:25:31 -0300 Subject: [PATCH 05/11] observer fix Signed-off-by: Victor Seiji Hariki --- js/util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/util.js b/js/util.js index bf83c07..a002865 100644 --- a/js/util.js +++ b/js/util.js @@ -33,8 +33,8 @@ class Observer { * @param {any} msg The message to send to the observers */ async emit(msg) { - Promise.all( - Array.from(this._handlers).map((handler) => async () => { + return Promise.all( + Array.from(this._handlers).map(async (handler) => { try { await handler(msg); } catch (e) { From 17b75cb64bd2c1e4cd1ed1a935289a28c67b2354 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 28 Nov 2022 12:32:31 -0300 Subject: [PATCH 06/11] util doc tab instead of space Signed-off-by: Victor Seiji Hariki --- js/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/util.js b/js/util.js index a002865..4d643fd 100644 --- a/js/util.js +++ b/js/util.js @@ -237,7 +237,7 @@ function cropCanvas(sourceCanvas) { } /** - * Downloads the content of a canvas to the disk, or opens it + * Downloads the content of a canvas to the disk, or opens it * * @param {{cropToContent: boolean, canvas: HTMLCanvasElement, filename: string}} options A options array with the following:\ * cropToContent: If we wish to crop to content first (default: true) From 593a37bdfcff8f9ba3bf1cc2892af4c64abd0719 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 28 Nov 2022 12:44:01 -0300 Subject: [PATCH 07/11] add error for non-existent commands Signed-off-by: Victor Seiji Hariki --- js/commands.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/js/commands.js b/js/commands.js index 7f16c06..87f6ea2 100644 --- a/js/commands.js +++ b/js/commands.js @@ -4,6 +4,9 @@ const _commands_events = new Observer(); +/** CommandNonExistentError */ +class CommandNonExistentError extends Error {} + /** Global Commands Object */ const commands = makeReadOnly( { @@ -150,6 +153,10 @@ const commands = makeReadOnly( * @param {any} options The options to be sent to the command to be run */ runCommand(name, title, options = null) { + if (!this._types[name]) + throw CommandNonExistentError( + `[commands] Command '${name}' does not exist` + ); this._types[name](title, options); }, }, From fdb93bad2669a9761aeeb56f1be6329d3de494d2 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 28 Nov 2022 12:46:37 -0300 Subject: [PATCH 08/11] remove history console prints Signed-off-by: Victor Seiji Hariki --- js/commands.js | 2 +- js/ui/history.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/js/commands.js b/js/commands.js index 87f6ea2..ed81c5b 100644 --- a/js/commands.js +++ b/js/commands.js @@ -154,7 +154,7 @@ const commands = makeReadOnly( */ runCommand(name, title, options = null) { if (!this._types[name]) - throw CommandNonExistentError( + throw new CommandNonExistentError( `[commands] Command '${name}' does not exist` ); this._types[name](title, options); diff --git a/js/ui/history.js b/js/ui/history.js index fbc08f9..4712086 100644 --- a/js/ui/history.js +++ b/js/ui/history.js @@ -30,7 +30,6 @@ if ( !commands._history.find((entry) => `hist-${entry.id}` === child.id) ) { - console.log("Removing " + child.id); historyView.removeChild(child); } }); @@ -38,7 +37,6 @@ commands._history.forEach((entry, index) => { if (!document.getElementById(`hist-${entry.id}`)) { - console.log("Inserting " + entry.id); historyView.appendChild( makeHistoryEntry(index, `hist-${entry.id}`, entry.title) ); From 83470ebba30a521b7ff47022262580ea9986b964 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 28 Nov 2022 19:48:42 -0300 Subject: [PATCH 09/11] input.js types and move type definitions Moved type definitions to .d.js files to avoid clutter and network overhead. Added typing to input.js, but still no event typing Signed-off-by: Victor Seiji Hariki --- js/commands.d.js | 38 +++++++++++++ js/commands.js | 79 +++++++++++++++++++++----- js/input.d.js | 120 ++++++++++++++++++++++++++++++++++++++++ js/input.js | 111 ++++++++++++++++++++++++++++++------- js/settingsbar.js | 10 ++-- js/ui/tool/dream.js | 16 +++--- js/ui/tool/maskbrush.js | 16 +++--- js/ui/tool/select.js | 16 +++--- js/ui/tool/stamp.js | 8 +-- js/util.js | 64 ++++++++++++--------- 10 files changed, 385 insertions(+), 93 deletions(-) create mode 100644 js/commands.d.js create mode 100644 js/input.d.js diff --git a/js/commands.d.js b/js/commands.d.js new file mode 100644 index 0000000..5ac16a8 --- /dev/null +++ b/js/commands.d.js @@ -0,0 +1,38 @@ +/** + * An object that represents an entry of the command in the history + * + * @typedef CommandEntry + * @property {string} id A unique ID generated for this entry + * @property {string} title The title passed to the command being run + * @property {() => void | Promise} undo A method to undo whatever the command did + * @property {() => void | Promise} redo A method to redo whatever undo did + * @property {{[key: string]: any}} state The state of the current command instance + */ + +/** + * A command, which is run, then returns a CommandEntry object that can be used to manually undo/redo it + * + * @callback Command + * @param {string} title The title passed to the command being run + * @param {*} options A options object for the command + * @returns {Promise} + */ + +/** + * A method for running a command (or redoing it) + * + * @callback CommandDoCallback + * @param {string} title The title passed to the command being run + * @param {*} options A options object for the command + * @param {{[key: string]: any}} state The state of the current command instance + * @returns {void | Promise} + */ + +/** + * A method for undoing a command + * + * @callback CommandUndoCallback + * @param {string} title The title passed to the command when it was run + * @param {{[key: string]: any}} state The state of the current command instance + * @returns {void | Promise} + */ diff --git a/js/commands.js b/js/commands.js index ed81c5b..68a7a84 100644 --- a/js/commands.js +++ b/js/commands.js @@ -7,6 +7,45 @@ const _commands_events = new Observer(); /** CommandNonExistentError */ class CommandNonExistentError extends Error {} +/** + * An object that represents an entry of the command in the history + * + * @typedef CommandEntry + * @property {string} id A unique ID generated for this entry + * @property {string} title The title passed to the command being run + * @property {() => void | Promise} undo A method to undo whatever the command did + * @property {() => void | Promise} redo A method to redo whatever undo did + * @property {{[key: string]: any}} state The state of the current command instance + */ + +/** + * A command, which is run, then returns a CommandEntry object that can be used to manually undo/redo it + * + * @callback Command + * @param {string} title The title passed to the command being run + * @param {*} options A options object for the command + * @returns {Promise} + */ + +/** + * A method for running a command (or redoing it) + * + * @callback CommandDoCallback + * @param {string} title The title passed to the command being run + * @param {*} options A options object for the command + * @param {{[key: string]: any}} state The state of the current command instance + * @returns {void | Promise} + */ + +/** + * A method for undoing a command + * + * @callback CommandUndoCallback + * @param {string} title The title passed to the command when it was run + * @param {{[key: string]: any}} state The state of the current command instance + * @returns {void | Promise} + */ + /** Global Commands Object */ const commands = makeReadOnly( { @@ -16,7 +55,11 @@ const commands = makeReadOnly( }, /** Current History Index (private) */ _current: -1, - /** Command History (private) */ + /** + * Command History (private) + * + * @type {CommandEntry[]} + */ _history: [], /** The types of commands we can run (private) */ _types: {}, @@ -24,21 +67,21 @@ const commands = makeReadOnly( /** * Undoes the last commands in the history * - * @param {number} n Number of actions to undo + * @param {number} [n] Number of actions to undo */ - undo(n = 1) { + async undo(n = 1) { for (var i = 0; i < n && this.current > -1; i++) { - this._history[this._current--].undo(); + await this._history[this._current--].undo(); } }, /** * Redoes the next commands in the history * - * @param {number} n Number of actions to redo + * @param {number} [n] Number of actions to redo */ - redo(n = 1) { + async redo(n = 1) { for (var i = 0; i < n && this.current + 1 < this._history.length; i++) { - this._history[++this._current].redo(); + await this._history[++this._current].redo(); } }, @@ -57,10 +100,10 @@ const commands = makeReadOnly( * The 'state' object will be passed to the 'undo' function as well. * * @param {string} name Command identifier (name) - * @param {(title: string, options: any, state: {[key: string]: any}) => void | Promise} run A method that performs the action for the first time - * @param {(title: string, state: {[key: string]: any}) => } undo A method that reverses what the run method did - * @param {(title: string, options: any, state: {[key: string]: any}) => void | Promise} redo A method that redoes the action after undone (default: run) - * @returns + * @param {CommandDoCallback} run A method that performs the action for the first time + * @param {CommandUndoCallback} undo A method that reverses what the run method did + * @param {CommandDoCallback} redo A method that redoes the action after undone (default: run) + * @returns {Command} */ createCommand(name, run, undo, redo = run) { const command = async function runWrapper(title, options) { @@ -69,6 +112,7 @@ const commands = makeReadOnly( Object.assign(copy, options); const state = {}; + /** @type {CommandEntry} */ const entry = { id: guid(), title, @@ -77,16 +121,21 @@ const commands = makeReadOnly( // Attempt to run command try { + console.debug(`[commands] Running '${title}'[${name}]`); await run(title, copy, state); } catch (e) { - console.warn(`Error while running command '${name}' with options:`); + console.warn( + `[commands] Error while running command '${name}' with options:` + ); console.warn(copy); console.warn(e); return; } const undoWrapper = () => { - console.debug(`Undoing ${name}, currently ${this._current}`); + console.debug( + `[commands] Undoing '${title}'[${name}], currently ${this._current}` + ); undo(title, state); _commands_events.emit({ id: entry.id, @@ -97,7 +146,9 @@ const commands = makeReadOnly( }); }; const redoWrapper = () => { - console.debug(`Redoing ${name}, currently ${this._current}`); + console.debug( + `[commands] Redoing '${title}'[${name}], currently ${this._current}` + ); redo(title, copy, state); _commands_events.emit({ id: entry.id, diff --git a/js/input.d.js b/js/input.d.js new file mode 100644 index 0000000..498bd8f --- /dev/null +++ b/js/input.d.js @@ -0,0 +1,120 @@ +/* Here are event types */ +/** + * A base event type for input handlers + * + * @typedef InputEvent + * @property {HTMLElement} target The target for the event + * @property {MouseEvent | KeyboardEvent} evn An input event + * @property {number} timestamp The time an event was emmited + */ + +/** + * A base event type for input + */ +// TODO: Implement event typing +/** + * An object for mouse event listeners + * + * @typedef OnClickEvent + */ + +/* Here are mouse context types */ +/** + * An object for mouse button event listeners. + * + * Drag events are use timing and radius to determine if they will be triggered + * Paint events are triggered on any mousedown, mousemove and mouseup circunstances + * + * @typedef MouseListenerBtnContext + * @property {Observer} onclick A click handler + * @property {Observer} ondclick A double click handler + * + * @property {Observer} ondragstart A drag start handler + * @property {Observer} ondrag A drag handler + * @property {Observer} ondragend A drag end handler + * + * @property {Observer} onpaintstart A paint start handler + * @property {Observer} onpaint A paint handler + * @property {Observer} onpaintend A paint end handler + */ + +/** + * An object for mouse event listeners + * + * @typedef MouseListenerContext + * @property {Observer} onmousemove A mouse move handler + * @property {Observer} onwheel A mouse wheel handler + * @property {MouseListenerBtnContext} btn Button handlers + */ + +/** + * This callback defines how event coordinateswill be transformed + * for this context. This function should set ctx.coords appropriately. + * + * + * @callback ContextMoveTransformer + * @param {MouseEvent} evn The mousemove event to be transformed + * @param {MouseContext} ctx The context object we are currently in + * @returns {void} + */ + +/** + * A context for handling mouse coordinates and events + * + * @typedef MouseContext + * @property {string} id A unique identifier + * @property {string} name The key name + * @property {ContextMoveTransformer} onmove The coordinate transform callback + * @property {?HTMLElement} target The target + */ + +/** + * An object for storing dragging information + * + * @typedef MouseCoordContextDragInfo + * @property {number} x X coordinate of drag start + * @property {number} y Y coordinate of drag start + * @property {HTMLElement} target Original element of drag + * @property {boolean} drag If we are in a drag + */ + +/** + * An object for storing mouse coordinates in a context + * + * @typedef MouseCoordContext + * @property {{[key: string]: MouseCoordContextDragInfo}} dragging Information about mouse button drags + * @property {{x: number, y: number}} prev Previous mouse position + * @property {{x: number, y: number}} pos Current mouse position + */ + +/* Here are keyboard-related types */ +/** + * Stores key states + * + * @typedef KeyboardKeyState + * @property {boolean} pressed If the key is currently pressed or not + * @property {boolean} held If the key is currently held or not + * @property {?number} _hold_to A timeout for detecting key holding status + */ + +/* Here are the shortcut types */ +/** + * Keyboard shortcut callback + * + * @callback KeyboardShortcutCallback + * @param {KeyboardEvent} evn The keyboard event that triggered this shorcut + * @returns {void} + */ + +/** + * Shortcut information + * + * @typedef KeyboardShortcut + * @property {string} id A unique identifier for this shortcut + * + * @property {boolean} ctrl Shortcut ctrl key state + * @property {boolean} alt Shortcut alt key state + * @property {boolean} shift Shortcut shift key state + * + * @property {KeyboardShortcutCallback} callback If the key is currently held or not + */ diff --git a/js/input.js b/js/input.js index 0492f9c..ed7ef4e 100644 --- a/js/input.js +++ b/js/input.js @@ -30,14 +30,42 @@ function _mouse_observers(name = "generic_mouse_observer_array") { ); } +/** Global Mouse Object */ const mouse = { + /** + * Array of context objects + * @type {MouseContext[]} + */ _contexts: [], + /** + * Timestamps of the button's last down event + * @type {Record<,number | null>} + */ buttons: {}, + /** + * Coordinate storage of mouse positions + * @type {{[ctxKey: string]: MouseCoordContext}} + */ coords: makeWriteOnce({}, "mouse.coords"), + /** + * Listener storage for event observers + * @type {{[ctxKey: string]: MouseListenerContext}} + */ listen: makeWriteOnce({}, "mouse.listen"), // Register Context + + /** + * Registers a new mouse context + * + * @param {string} name The key name of the context + * @param {ContextMoveTransformer} onmove The function to perform coordinate transform + * @param {object} options Extra options + * @param {HTMLElement} [options.target=null] Target filtering + * @param {Record} [options.buttons={0: "left", 1: "middle", 2: "right"}] Custom button mapping + * @returns {MouseContext} + */ registerContext: (name, onmove, options = {}) => { // Options defaultOpt(options, { @@ -46,6 +74,7 @@ const mouse = { }); // Context information + /** @type {MouseContext} */ const context = { id: guid(), name, @@ -79,8 +108,8 @@ const mouse = { Object.keys(options.buttons).forEach((index) => { const button = options.buttons[index]; mouse.coords[name].dragging[button] = null; - mouse.listen[name][button] = _mouse_observers( - `mouse.listen[${name}][${button}]` + mouse.listen[name].btn[button] = _mouse_observers( + `mouse.listen[${name}].btn[${button}]` ); }); @@ -105,7 +134,7 @@ window.onmousedown = (evn) => { // ondclick event mouse._contexts.forEach(({target, name, buttons}) => { if ((!target || target === evn.target) && buttons[evn.button]) - mouse.listen[name][buttons[evn.button]].ondclick.emit({ + mouse.listen[name].btn[buttons[evn.button]].ondclick.emit({ target: evn.target, buttonId: evn.button, x: mouse.coords[name].pos.x, @@ -131,7 +160,7 @@ window.onmousedown = (evn) => { !mouse.coords[name].dragging[key].drag && key ) { - mouse.listen[name][key].ondragstart.emit({ + mouse.listen[name].btn[key].ondragstart.emit({ target: evn.target, buttonId: evn.button, x: mouse.coords[name].pos.x, @@ -156,7 +185,7 @@ window.onmousedown = (evn) => { Object.assign(mouse.coords[name].dragging[key], mouse.coords[name].pos); // onpaintstart event - mouse.listen[name][key].onpaintstart.emit({ + mouse.listen[name].btn[key].onpaintstart.emit({ target: evn.target, buttonId: evn.button, x: mouse.coords[name].pos.x, @@ -192,7 +221,7 @@ window.onmouseup = (evn) => { time - mouse.buttons[evn.button] < inputConfig.clickTiming && dx * dx + dy * dy < inputConfig.clickRadius * inputConfig.clickRadius ) - mouse.listen[name][key].onclick.emit({ + mouse.listen[name].btn[key].onclick.emit({ target: evn.target, buttonId: evn.button, x: mouse.coords[name].pos.x, @@ -202,7 +231,7 @@ window.onmouseup = (evn) => { }); // onpaintend event - mouse.listen[name][key].onpaintend.emit({ + mouse.listen[name].btn[key].onpaintend.emit({ target: evn.target, initialTarget: mouse.coords[name].dragging[key].target, buttonId: evn.button, @@ -216,7 +245,7 @@ window.onmouseup = (evn) => { // ondragend event if (mouse.coords[name].dragging[key].drag) - mouse.listen[name][key].ondragend.emit({ + mouse.listen[name].btn[key].ondragend.emit({ target: evn.target, initialTarget: mouse.coords[name].dragging[key].target, buttonId: evn.button, @@ -270,7 +299,7 @@ window.onmousemove = (evn) => { dx * dx + dy * dy >= inputConfig.clickRadius * inputConfig.clickRadius ) { - mouse.listen[name][key].ondragstart.emit({ + mouse.listen[name].btn[key].ondragstart.emit({ target: evn.target, buttonId: evn.button, ix: mouse.coords[name].dragging[key].x, @@ -290,7 +319,7 @@ window.onmousemove = (evn) => { mouse.coords[name].dragging[key] && mouse.coords[name].dragging[key].drag ) - mouse.listen[name][key].ondrag.emit({ + mouse.listen[name].btn[key].ondrag.emit({ target: evn.target, initialTarget: mouse.coords[name].dragging[key].target, button: index, @@ -306,7 +335,7 @@ window.onmousemove = (evn) => { // onpaint event if (mouse.coords[name].dragging[key]) { - mouse.listen[name][key].onpaint.emit({ + mouse.listen[name].btn[key].onpaint.emit({ target: evn.target, initialTarget: mouse.coords[name].dragging[key].target, button: index, @@ -366,20 +395,51 @@ mouse.registerContext( /** * Keyboard input processing */ -// Base object generator functions - +/** Global Keyboard Object */ const keyboard = { + /** + * Stores the key states for all keys + * + * @type {Record} + */ keys: {}, + /** + * Checks if a key is pressed or not + * + * @param {string} code - The code of the key + * @returns {boolean} + */ isPressed(code) { - return this.keys[key].pressed; + return this.keys[code].pressed; }, + /** + * Checks if a key is held or not + * + * @param {string} code - The code of the key + * @returns {boolean} + */ isHeld(code) { - return !!this; + return this.keys[code].held; }, + /** + * Object storing shortcuts. Uses key as indexing for better performance. + * @type {Record} + */ shortcuts: {}, + /** + * Adds a shortcut listener + * + * @param {object} shortcut Shortcut information + * @param {boolean} [shortcut.ctrl=false] If control must be pressed + * @param {boolean} [shortcut.alt=false] If alt must be pressed + * @param {boolean} [shortcut.shift=false] If shift must be pressed + * @param {string} shortcut.key The key code (evn.code) for the key pressed + * @param {KeyboardShortcutCallback} callback Will be called on shortcut detection + * @returns + */ onShortcut(shortcut, callback) { /** * Adds a shortcut handler (shorcut must be in format: {ctrl?: bool, alt?: bool, shift?: bool, key: string (code)}) @@ -389,23 +449,32 @@ const keyboard = { this.shortcuts[shortcut.key] = []; this.shortcuts[shortcut.key].push({ - ctrl: shortcut.ctrl, - alt: shortcut.alt, - shift: shortcut.shift, + ctrl: !!shortcut.ctrl, + alt: !!shortcut.alt, + shift: !!shortcut.shift, id: guid(), callback, }); + + return callback; }, - deleteShortcut(id, key = null) { + /** + * Deletes a shortcut (disables callback) + * + * @param {string | KeyboardShortcutCallback} shortcut A shortcut ID or its callback + * @param {string} [key=null] If you know the key code, to avoid searching all shortcuts + * @returns + */ + deleteShortcut(shortcut, key = null) { if (key) { this.shortcuts[key] = this.shortcuts[key].filter( - (v) => v.id !== id && v.callback !== id + (v) => v.id !== shortcut && v.callback !== shortcut ); return; } this.shortcuts.keys().forEach((key) => { this.shortcuts[key] = this.shortcuts[key].filter( - (v) => v.id !== id && v.callback !== id + (v) => v.id !== shortcut && v.callback !== shortcut ); }); }, diff --git a/js/settingsbar.js b/js/settingsbar.js index a041c0a..f7848cd 100644 --- a/js/settingsbar.js +++ b/js/settingsbar.js @@ -6,7 +6,7 @@ function makeDraggable(element) { element.style.top = startbb.y + "px"; element.style.left = startbb.x + "px"; - mouse.listen.window.left.onpaintstart.on((evn) => { + mouse.listen.window.btn.left.onpaintstart.on((evn) => { if ( element.contains(evn.target) && evn.target.classList.contains("draggable") @@ -18,14 +18,14 @@ function makeDraggable(element) { } }); - mouse.listen.window.left.onpaint.on((evn) => { + mouse.listen.window.btn.left.onpaint.on((evn) => { if (dragging) { element.style.top = evn.y - offset.y + "px"; element.style.left = evn.x - offset.x + "px"; } }); - mouse.listen.window.left.onpaintend.on((evn) => { + mouse.listen.window.btn.left.onpaintend.on((evn) => { dragging = false; }); } @@ -143,13 +143,13 @@ function createSlider(name, wrapper, options = {}) { } }); - mouse.listen.window.left.onclick.on((evn) => { + mouse.listen.window.btn.left.onclick.on((evn) => { if (evn.target === overEl) { textEl.select(); } }); - mouse.listen.window.left.ondrag.on((evn) => { + mouse.listen.window.btn.left.ondrag.on((evn) => { if (evn.target === overEl) { setValue( Math.max( diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index a2999aa..3eb5a06 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -254,8 +254,8 @@ const dreamTool = () => // Start Listeners mouse.listen.canvas.onmousemove.on(state.mousemovecb); - mouse.listen.canvas.left.onclick.on(state.dreamcb); - mouse.listen.canvas.right.onclick.on(state.erasecb); + mouse.listen.canvas.btn.left.onclick.on(state.dreamcb); + mouse.listen.canvas.btn.right.onclick.on(state.erasecb); // Display Mask setMask(state.invertMask ? "hold" : "clear"); @@ -263,8 +263,8 @@ const dreamTool = () => (state, opt) => { // Clear Listeners mouse.listen.canvas.onmousemove.clear(state.mousemovecb); - mouse.listen.canvas.left.onclick.clear(state.dreamcb); - mouse.listen.canvas.right.onclick.clear(state.erasecb); + mouse.listen.canvas.btn.left.onclick.clear(state.dreamcb); + mouse.listen.canvas.btn.right.onclick.clear(state.erasecb); // Hide Mask setMask("none"); @@ -336,8 +336,8 @@ const img2imgTool = () => // Start Listeners mouse.listen.canvas.onmousemove.on(state.mousemovecb); - mouse.listen.canvas.left.onclick.on(state.dreamcb); - mouse.listen.canvas.right.onclick.on(state.erasecb); + mouse.listen.canvas.btn.left.onclick.on(state.dreamcb); + mouse.listen.canvas.btn.right.onclick.on(state.erasecb); // Display Mask setMask(state.invertMask ? "hold" : "clear"); @@ -345,8 +345,8 @@ const img2imgTool = () => (state, opt) => { // Clear Listeners mouse.listen.canvas.onmousemove.clear(state.mousemovecb); - mouse.listen.canvas.left.onclick.clear(state.dreamcb); - mouse.listen.canvas.right.onclick.clear(state.erasecb); + mouse.listen.canvas.btn.left.onclick.clear(state.dreamcb); + mouse.listen.canvas.btn.right.onclick.clear(state.erasecb); // Hide mask setMask("none"); diff --git a/js/ui/tool/maskbrush.js b/js/ui/tool/maskbrush.js index 275364c..bc4af36 100644 --- a/js/ui/tool/maskbrush.js +++ b/js/ui/tool/maskbrush.js @@ -74,10 +74,10 @@ const maskBrushTool = () => // Start Listeners mouse.listen.canvas.onmousemove.on(state.movecb); mouse.listen.canvas.onwheel.on(state.wheelcb); - mouse.listen.canvas.left.onpaintstart.on(state.drawcb); - mouse.listen.canvas.left.onpaint.on(state.drawcb); - mouse.listen.canvas.right.onpaintstart.on(state.erasecb); - mouse.listen.canvas.right.onpaint.on(state.erasecb); + mouse.listen.canvas.btn.left.onpaintstart.on(state.drawcb); + mouse.listen.canvas.btn.left.onpaint.on(state.drawcb); + mouse.listen.canvas.btn.right.onpaintstart.on(state.erasecb); + mouse.listen.canvas.btn.right.onpaint.on(state.erasecb); // Display Mask setMask("neutral"); @@ -86,10 +86,10 @@ const maskBrushTool = () => // Clear Listeners mouse.listen.canvas.onmousemove.clear(state.movecb); mouse.listen.canvas.onwheel.clear(state.wheelcb); - mouse.listen.canvas.left.onpaintstart.clear(state.drawcb); - mouse.listen.canvas.left.onpaint.clear(state.drawcb); - mouse.listen.canvas.right.onpaintstart.clear(state.erasecb); - mouse.listen.canvas.right.onpaint.clear(state.erasecb); + mouse.listen.canvas.btn.left.onpaintstart.clear(state.drawcb); + mouse.listen.canvas.btn.left.onpaint.clear(state.drawcb); + mouse.listen.canvas.btn.right.onpaintstart.clear(state.erasecb); + mouse.listen.canvas.btn.right.onpaint.clear(state.erasecb); // Hide Mask setMask("none"); diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index a8814af..3b19d40 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -9,12 +9,12 @@ const selectTransformTool = () => // Canvas left mouse handlers mouse.listen.canvas.onmousemove.on(state.movecb); - mouse.listen.canvas.left.onclick.on(state.clickcb); - mouse.listen.canvas.left.ondragstart.on(state.dragstartcb); - mouse.listen.canvas.left.ondragend.on(state.dragendcb); + mouse.listen.canvas.btn.left.onclick.on(state.clickcb); + mouse.listen.canvas.btn.left.ondragstart.on(state.dragstartcb); + mouse.listen.canvas.btn.left.ondragend.on(state.dragendcb); // Canvas right mouse handler - mouse.listen.canvas.right.onclick.on(state.cancelcb); + mouse.listen.canvas.btn.right.onclick.on(state.cancelcb); // Keyboard click handlers keyboard.listen.onkeyclick.on(state.keyclickcb); @@ -30,11 +30,11 @@ const selectTransformTool = () => (state, opt) => { // Clear all those listeners and shortcuts we set up mouse.listen.canvas.onmousemove.clear(state.movecb); - mouse.listen.canvas.left.onclick.clear(state.clickcb); - mouse.listen.canvas.left.ondragstart.clear(state.dragstartcb); - mouse.listen.canvas.left.ondragend.clear(state.dragendcb); + mouse.listen.canvas.btn.left.onclick.clear(state.clickcb); + mouse.listen.canvas.btn.left.ondragstart.clear(state.dragstartcb); + mouse.listen.canvas.btn.left.ondragend.clear(state.dragendcb); - mouse.listen.canvas.right.onclick.clear(state.cancelcb); + mouse.listen.canvas.btn.right.onclick.clear(state.cancelcb); keyboard.listen.onkeyclick.clear(state.keyclickcb); keyboard.listen.onkeydown.clear(state.keydowncb); diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 4bf2fa3..0bb111a 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -9,8 +9,8 @@ const stampTool = () => // Start Listeners mouse.listen.canvas.onmousemove.on(state.movecb); - mouse.listen.canvas.left.onclick.on(state.drawcb); - mouse.listen.canvas.right.onclick.on(state.cancelcb); + mouse.listen.canvas.btn.left.onclick.on(state.drawcb); + mouse.listen.canvas.btn.right.onclick.on(state.cancelcb); // For calls from other tools to paste image if (opt && opt.image) { @@ -33,8 +33,8 @@ const stampTool = () => (state, opt) => { // Clear Listeners mouse.listen.canvas.onmousemove.clear(state.movecb); - mouse.listen.canvas.left.onclick.clear(state.drawcb); - mouse.listen.canvas.right.onclick.clear(state.cancelcb); + mouse.listen.canvas.btn.left.onclick.clear(state.drawcb); + mouse.listen.canvas.btn.right.onclick.clear(state.cancelcb); // Deselect state.selected = null; diff --git a/js/util.js b/js/util.js index 4d643fd..03f6a6d 100644 --- a/js/util.js +++ b/js/util.js @@ -1,18 +1,33 @@ /** - * Observer class + * Some type definitions before the actual code + */ +/** + * Represents a simple bounding box + * + * @typedef BoundingBox + * @type {Object} + * @property {number} x - Leftmost coordinate of the box + * @property {number} y - Topmost coordinate of the box + * @property {number} w - The bounding box Width + * @property {number} h - The bounding box Height + */ + +/** + * A simple implementation of the Observer programming pattern + * @template [T=any] Message type */ class Observer { /** * List of handlers - * @type {Set<(msg: any) => void | Promise>} + * @type {Set<(msg: T) => void | Promise>} */ _handlers = new Set(); /** * Adds a observer to the events * - * @param {(msg: any) => void | Promise} callback The function to run when receiving a message - * @returns {(msg:any) => void | Promise} The callback we received + * @param {(msg: T) => void | Promise} callback The function to run when receiving a message + * @returns {(msg:T) => void | Promise} The callback we received */ on(callback) { this._handlers.add(callback); @@ -21,16 +36,16 @@ class Observer { /** * Removes a observer * - * @param {(msg: any) => void | Promise} callback The function used to register the callback + * @param {(msg: T) => void | Promise} callback The function used to register the callback * @returns {boolean} Whether the handler existed */ clear(callback) { return this._handlers.delete(callback); } /** - * Send a message to all observers + * Sends a message to all observers * - * @param {any} msg The message to send to the observers + * @param {T} msg The message to send to the observers */ async emit(msg) { return Promise.all( @@ -49,7 +64,7 @@ class Observer { /** * Generates a simple UID in the format xxxx-xxxx-...-xxxx, with x being [0-9a-f] * - * @param {number} size Number of quartets of characters to generate + * @param {number} [size] Number of quartets of characters to generate * @returns {string} The new UID */ const guid = (size = 3) => { @@ -68,8 +83,10 @@ const guid = (size = 3) => { /** * Assigns defaults to an option object passed to the function. * - * @param {{[key: string]: any}} options Original options object - * @param {{[key: string]: any}} defaults Default values to assign + * @template T Object Type + * + * @param {T} options Original options object + * @param {T} defaults Default values to assign */ function defaultOpt(options, defaults) { Object.keys(defaults).forEach((key) => { @@ -108,8 +125,8 @@ class ProxyWriteOnceSetError extends Error {} * * @template T Object Type * @param {T} obj Object to be proxied - * @param {string} name Name for logging purposes - * @param {string[]} exceptions Parameters excepted from this restriction + * @param {string} [name] Name for logging purposes + * @param {string[]} [exceptions] Parameters excepted from this restriction * @returns {T} Proxied object, intercepting write attempts */ function makeWriteOnce(obj, name = "write-once object", exceptions = []) { @@ -154,12 +171,12 @@ function snap(i, scaled = true, gridSize = 64) { /** * Gets a bounding box centered on a given set of coordinates. Supports grid snapping * - * @param {number} cx x-coordinate of the center of the box - * @param {number} cy y-coordinate of the center of the box - * @param {number} w the width of the box - * @param {height} h the height of the box - * @param {number | null} gridSnap The size of the grid to snap to - * @returns {BoundingBox} A bounding box object centered at (cx, cy) + * @param {number} cx - x-coordinate of the center of the box + * @param {number} cy - y-coordinate of the center of the box + * @param {number} w - the width of the box + * @param {height} h - the height of the box + * @param {number | null} gridSnap - The size of the grid to snap to + * @returns {BoundingBox} - A bounding box object centered at (cx, cy) */ function getBoundingBox(cx, cy, w, h, gridSnap = null) { const offset = {x: 0, y: 0}; @@ -180,9 +197,6 @@ function getBoundingBox(cx, cy, w, h, gridSnap = null) { }; } -/** - * Triggers Canvas Download - */ /** * Crops a given canvas to content, returning a new canvas object with the content in it. * @@ -239,10 +253,10 @@ function cropCanvas(sourceCanvas) { /** * Downloads the content of a canvas to the disk, or opens it * - * @param {{cropToContent: boolean, canvas: HTMLCanvasElement, filename: string}} options A options array with the following:\ - * cropToContent: If we wish to crop to content first (default: true) - * canvas: The source canvas (default: imgCanvas) - * filename: The filename to save as (default: '[ISO date] [Hours] [Minutes] [Seconds] openOutpaint image.png').\ + * @param {Object} options - Optional Information + * @param {boolean} [options.cropToContent] - If we wish to crop to content first (default: true) + * @param {HTMLCanvasElement} [options.canvas] - The source canvas (default: imgCanvas) + * @param {string} [options.filename] - The filename to save as (default: '[ISO date] [Hours] [Minutes] [Seconds] openOutpaint image.png').\ * If null, opens image in new tab. */ function downloadCanvas(options = {}) { From 3088a9dfed9e022aa77b308399fa456494f1569f Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 28 Nov 2022 19:54:26 -0300 Subject: [PATCH 10/11] forgot to remove types from commands.js Signed-off-by: Victor Seiji Hariki --- js/commands.js | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/js/commands.js b/js/commands.js index 68a7a84..20211b1 100644 --- a/js/commands.js +++ b/js/commands.js @@ -7,45 +7,6 @@ const _commands_events = new Observer(); /** CommandNonExistentError */ class CommandNonExistentError extends Error {} -/** - * An object that represents an entry of the command in the history - * - * @typedef CommandEntry - * @property {string} id A unique ID generated for this entry - * @property {string} title The title passed to the command being run - * @property {() => void | Promise} undo A method to undo whatever the command did - * @property {() => void | Promise} redo A method to redo whatever undo did - * @property {{[key: string]: any}} state The state of the current command instance - */ - -/** - * A command, which is run, then returns a CommandEntry object that can be used to manually undo/redo it - * - * @callback Command - * @param {string} title The title passed to the command being run - * @param {*} options A options object for the command - * @returns {Promise} - */ - -/** - * A method for running a command (or redoing it) - * - * @callback CommandDoCallback - * @param {string} title The title passed to the command being run - * @param {*} options A options object for the command - * @param {{[key: string]: any}} state The state of the current command instance - * @returns {void | Promise} - */ - -/** - * A method for undoing a command - * - * @callback CommandUndoCallback - * @param {string} title The title passed to the command when it was run - * @param {{[key: string]: any}} state The state of the current command instance - * @returns {void | Promise} - */ - /** Global Commands Object */ const commands = makeReadOnly( { From 7c7352a0ebd431b7a2b59024915bf86daf2984a9 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 28 Nov 2022 20:03:08 -0300 Subject: [PATCH 11/11] fixed issue when migrating mouse listeners Signed-off-by: Victor Seiji Hariki --- js/input.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/input.js b/js/input.js index ed7ef4e..8e1b15b 100644 --- a/js/input.js +++ b/js/input.js @@ -102,6 +102,7 @@ const mouse = { mouse.listen[name] = { onwheel: new Observer(), onmousemove: new Observer(), + btn: {}, }; // Button specific items