Merge pull request #58 from zero01101/bleeding-edge
Mostly Documentation and Development Tooling
This commit is contained in:
commit
64ff4e8f3e
13 changed files with 629 additions and 219 deletions
|
@ -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);
|
||||
}
|
||||
|
|
38
js/commands.d.js
Normal file
38
js/commands.d.js
Normal 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>}
|
||||
*/
|
261
js/commands.js
261
js/commands.js
|
@ -4,122 +4,177 @@
|
|||
|
||||
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();
|
||||
}
|
||||
},
|
||||
/** CommandNonExistentError */
|
||||
class CommandNonExistentError extends Error {}
|
||||
|
||||
/**
|
||||
* 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 = {};
|
||||
/** Global Commands Object */
|
||||
const commands = makeReadOnly(
|
||||
{
|
||||
/** Current History Index Reader */
|
||||
get current() {
|
||||
return this._current;
|
||||
},
|
||||
/** Current History Index (private) */
|
||||
_current: -1,
|
||||
/**
|
||||
* Command History (private)
|
||||
*
|
||||
* @type {CommandEntry[]}
|
||||
*/
|
||||
_history: [],
|
||||
/** The types of commands we can run (private) */
|
||||
_types: {},
|
||||
|
||||
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
|
||||
*/
|
||||
async undo(n = 1) {
|
||||
for (var i = 0; i < n && this.current > -1; i++) {
|
||||
await this._history[this._current--].undo();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Redoes the next commands in the history
|
||||
*
|
||||
* @param {number} [n] Number of actions to redo
|
||||
*/
|
||||
async redo(n = 1) {
|
||||
for (var i = 0; i < n && this.current + 1 < this._history.length; i++) {
|
||||
await 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 {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) {
|
||||
// Create copy of options and state object
|
||||
const copy = {};
|
||||
Object.assign(copy, options);
|
||||
const state = {};
|
||||
|
||||
/** @type {CommandEntry} */
|
||||
const entry = {
|
||||
id: guid(),
|
||||
title,
|
||||
state,
|
||||
};
|
||||
|
||||
// Attempt to run command
|
||||
try {
|
||||
console.debug(`[commands] Running '${title}'[${name}]`);
|
||||
await run(title, copy, state);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`[commands] Error while running command '${name}' with options:`
|
||||
);
|
||||
console.warn(copy);
|
||||
console.warn(e);
|
||||
return;
|
||||
}
|
||||
|
||||
const undoWrapper = () => {
|
||||
console.debug(
|
||||
`[commands] Undoing '${title}'[${name}], currently ${this._current}`
|
||||
);
|
||||
undo(title, state);
|
||||
_commands_events.emit({
|
||||
id: entry.id,
|
||||
name,
|
||||
action: "undo",
|
||||
state,
|
||||
current: this._current,
|
||||
});
|
||||
};
|
||||
const redoWrapper = () => {
|
||||
console.debug(
|
||||
`[commands] Redoing '${title}'[${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) {
|
||||
if (!this._types[name])
|
||||
throw new CommandNonExistentError(
|
||||
`[commands] Command '${name}' does not exist`
|
||||
);
|
||||
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
|
||||
|
|
120
js/input.d.js
Normal file
120
js/input.d.js
Normal 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
|
||||
*/
|
165
js/input.js
165
js/input.js
|
@ -10,31 +10,62 @@ 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
|
||||
);
|
||||
}
|
||||
|
||||
/** Global Mouse Object */
|
||||
const mouse = {
|
||||
contexts: [],
|
||||
/**
|
||||
* Array of context objects
|
||||
* @type {MouseContext[]}
|
||||
*/
|
||||
_contexts: [],
|
||||
/**
|
||||
* Timestamps of the button's last down event
|
||||
* @type {Record<,number | null>}
|
||||
*/
|
||||
buttons: {},
|
||||
coords: {},
|
||||
/**
|
||||
* Coordinate storage of mouse positions
|
||||
* @type {{[ctxKey: string]: MouseCoordContext}}
|
||||
*/
|
||||
coords: makeWriteOnce({}, "mouse.coords"),
|
||||
|
||||
listen: {},
|
||||
/**
|
||||
* 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, {
|
||||
|
@ -43,6 +74,7 @@ const mouse = {
|
|||
});
|
||||
|
||||
// Context information
|
||||
/** @type {MouseContext} */
|
||||
const context = {
|
||||
id: guid(),
|
||||
name,
|
||||
|
@ -70,13 +102,16 @@ const mouse = {
|
|||
mouse.listen[name] = {
|
||||
onwheel: new Observer(),
|
||||
onmousemove: new Observer(),
|
||||
btn: {},
|
||||
};
|
||||
|
||||
// Button specific items
|
||||
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].btn[button] = _mouse_observers(
|
||||
`mouse.listen[${name}].btn[${button}]`
|
||||
);
|
||||
});
|
||||
|
||||
// Add to context
|
||||
|
@ -84,7 +119,7 @@ const mouse = {
|
|||
context.listen = mouse.listen[name];
|
||||
|
||||
// Add to list
|
||||
mouse.contexts.push(context);
|
||||
mouse._contexts.push(context);
|
||||
|
||||
return context;
|
||||
},
|
||||
|
@ -98,9 +133,9 @@ 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({
|
||||
mouse.listen[name].btn[buttons[evn.button]].ondclick.emit({
|
||||
target: evn.target,
|
||||
buttonId: evn.button,
|
||||
x: mouse.coords[name].pos.x,
|
||||
|
@ -119,14 +154,14 @@ 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) &&
|
||||
!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,
|
||||
|
@ -143,7 +178,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] = {};
|
||||
|
@ -151,7 +186,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,
|
||||
|
@ -166,7 +201,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) &&
|
||||
|
@ -187,7 +222,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,
|
||||
|
@ -197,7 +232,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,
|
||||
|
@ -211,7 +246,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,
|
||||
|
@ -235,7 +270,7 @@ window.onmouseup = (evn) => {
|
|||
};
|
||||
|
||||
window.onmousemove = (evn) => {
|
||||
mouse.contexts.forEach((context) => {
|
||||
mouse._contexts.forEach((context) => {
|
||||
const target = context.target;
|
||||
const name = context.name;
|
||||
|
||||
|
@ -265,7 +300,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,
|
||||
|
@ -285,7 +320,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,
|
||||
|
@ -301,7 +336,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,
|
||||
|
@ -323,7 +358,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,
|
||||
|
@ -361,20 +396,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)})
|
||||
|
@ -384,23 +450,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
|
||||
);
|
||||
});
|
||||
},
|
||||
|
|
7
js/jsconfig.json
Normal file
7
js/jsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6"
|
||||
},
|
||||
"include": ["**/*.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(
|
||||
|
|
|
@ -28,17 +28,15 @@
|
|||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
commands.history.forEach((entry, index) => {
|
||||
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)
|
||||
);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
177
js/util.js
177
js/util.js
|
@ -1,33 +1,71 @@
|
|||
/**
|
||||
* Implementation of a simple Oberver Pattern for custom event handling
|
||||
* Some type definitions before the actual code
|
||||
*/
|
||||
function Observer() {
|
||||
this.handlers = new Set();
|
||||
/**
|
||||
* 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: T) => void | Promise<void>>}
|
||||
*/
|
||||
_handlers = new Set();
|
||||
|
||||
/**
|
||||
* Adds a observer to the events
|
||||
*
|
||||
* @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);
|
||||
return callback;
|
||||
}
|
||||
/**
|
||||
* Removes a observer
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
/**
|
||||
* Sends a message to all observers
|
||||
*
|
||||
* @param {T} msg The message to send to the observers
|
||||
*/
|
||||
async emit(msg) {
|
||||
return Promise.all(
|
||||
Array.from(this._handlers).map(async (handler) => {
|
||||
try {
|
||||
await handler(msg);
|
||||
} catch (e) {
|
||||
console.warn("Observer failed to run handler");
|
||||
console.warn(e);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Observer.prototype = {
|
||||
// Adds handler for this message
|
||||
on(callback) {
|
||||
this.handlers.add(callback);
|
||||
return callback;
|
||||
},
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 +81,73 @@ const guid = (size = 3) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Default option set
|
||||
* Assigns defaults to an option object passed to the function.
|
||||
*
|
||||
* @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) => {
|
||||
if (options[key] === undefined) options[key] = defaults[key];
|
||||
});
|
||||
}
|
||||
|
||||
/** Custom error for attempt to set read-only objects */
|
||||
class ProxyReadOnlySetError extends Error {}
|
||||
/**
|
||||
* Bounding box Calculation
|
||||
* Makes a given object read-only; throws a ProxyReadOnlySetError exception if modification is attempted
|
||||
*
|
||||
* @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
|
||||
* @returns {T} Proxied object, intercepting write attempts
|
||||
*/
|
||||
function makeReadOnly(obj, name = "read-only object", exceptions = []) {
|
||||
return new Proxy(obj, {
|
||||
set: (obj, prop, value) => {
|
||||
if (!exceptions.some((v) => v === prop))
|
||||
throw new ProxyReadOnlySetError(
|
||||
`Tried setting the '${prop}' property on '${name}'`
|
||||
);
|
||||
obj[prop] = value;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 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
|
||||
*
|
||||
* @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
|
||||
* @returns {T} Proxied object, intercepting write attempts
|
||||
*/
|
||||
function makeWriteOnce(obj, name = "write-once object", exceptions = []) {
|
||||
return new Proxy(obj, {
|
||||
set: (obj, prop, value) => {
|
||||
if (obj[prop] !== undefined && !exceptions.some((v) => v === prop))
|
||||
throw new ProxyWriteOnceSetError(
|
||||
`Tried setting the '${prop}' property on '${name}' after it was already set`
|
||||
);
|
||||
obj[prop] = value;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -74,6 +168,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};
|
||||
|
@ -94,7 +198,10 @@ 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;
|
||||
|
@ -143,6 +250,15 @@ function cropCanvas(sourceCanvas) {
|
|||
return cutCanvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the content of a canvas to the disk, or opens it
|
||||
*
|
||||
* @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 = {}) {
|
||||
defaultOpt(options, {
|
||||
cropToContent: true,
|
||||
|
@ -156,7 +272,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)
|
||||
|
|
Loading…
Reference in a new issue