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 <victorseijih@gmail.com>
This commit is contained in:
Victor Seiji Hariki 2022-11-28 19:48:42 -03:00
parent fdb93bad26
commit 83470ebba3
10 changed files with 385 additions and 93 deletions

38
js/commands.d.js Normal file
View file

@ -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<void>} undo A method to undo whatever the command did
* @property {() => void | Promise<void>} 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<CommandEntry>}
*/
/**
* 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<void>}
*/
/**
* 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<void>}
*/

View file

@ -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<void>} undo A method to undo whatever the command did
* @property {() => void | Promise<void>} 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<CommandEntry>}
*/
/**
* 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<void>}
*/
/**
* 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<void>}
*/
/** 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<void>} 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<void>} 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,

120
js/input.d.js Normal file
View file

@ -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
*/

View file

@ -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<number, string>} [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<string, KeyboardKeyState>}
*/
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<string, KeyboardShortcut[]>}
*/
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
);
});
},

View file

@ -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(

View file

@ -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");

View file

@ -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");

View file

@ -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);

View file

@ -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;

View file

@ -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<void>>}
* @type {Set<(msg: T) => void | Promise<void>>}
*/
_handlers = new Set();
/**
* Adds a observer to the events
*
* @param {(msg: any) => void | Promise<void>} callback The function to run when receiving a message
* @returns {(msg:any) => void | Promise<void>} The callback we received
* @param {(msg: T) => void | Promise<void>} callback The function to run when receiving a message
* @returns {(msg:T) => void | Promise<void>} The callback we received
*/
on(callback) {
this._handlers.add(callback);
@ -21,16 +36,16 @@ class Observer {
/**
* Removes a observer
*
* @param {(msg: any) => void | Promise<void>} callback The function used to register the callback
* @param {(msg: T) => void | Promise<void>} 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 = {}) {