2022-11-20 20:03:07 -06:00
|
|
|
const inputConfig = {
|
2022-11-20 21:02:25 -06:00
|
|
|
clickRadius: 10, // Radius to be considered a click (pixels). If farther, turns into a drag
|
|
|
|
clickTiming: 500, // Timing window to be considered a click (ms). If longer, turns into a drag
|
|
|
|
dClickTiming: 500, // Timing window to be considered a double click (ms).
|
2022-11-21 20:19:41 -06:00
|
|
|
|
|
|
|
keyboardHoldTiming: 100, // Timing window after which to consider holding a key (ms)
|
2022-11-20 20:03:07 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Mouse input processing
|
|
|
|
*/
|
|
|
|
// Base object generator functions
|
|
|
|
function _context_coords() {
|
2022-11-20 21:02:25 -06:00
|
|
|
return {
|
|
|
|
dragging: {
|
|
|
|
left: null,
|
|
|
|
middle: null,
|
|
|
|
right: null,
|
|
|
|
},
|
|
|
|
|
|
|
|
prev: {
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
},
|
|
|
|
|
|
|
|
pos: {
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
},
|
|
|
|
};
|
2022-11-20 20:03:07 -06:00
|
|
|
}
|
|
|
|
function _mouse_observers() {
|
2022-11-20 21:02:25 -06:00
|
|
|
return {
|
2022-11-22 16:24:55 -06:00
|
|
|
// Simple click handler
|
2022-11-20 21:02:25 -06:00
|
|
|
onclick: new Observer(),
|
2022-11-22 16:24:55 -06:00
|
|
|
// Double click handler (will still trigger simple click handler as well)
|
2022-11-20 21:02:25 -06:00
|
|
|
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(),
|
|
|
|
};
|
2022-11-20 20:03:07 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
function _context_observers() {
|
2022-11-20 21:02:25 -06:00
|
|
|
return {
|
2022-11-22 16:24:55 -06:00
|
|
|
onwheel: new Observer(),
|
2022-11-20 21:02:25 -06:00
|
|
|
onmousemove: new Observer(),
|
|
|
|
left: _mouse_observers(),
|
|
|
|
middle: _mouse_observers(),
|
|
|
|
right: _mouse_observers(),
|
|
|
|
};
|
2022-11-20 20:03:07 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
const mouse = {
|
2022-11-20 21:02:25 -06:00
|
|
|
buttons: {
|
|
|
|
right: null,
|
|
|
|
left: null,
|
|
|
|
middle: null,
|
|
|
|
},
|
|
|
|
|
|
|
|
// Mouse Actions in Window Coordinates
|
|
|
|
window: _context_coords(),
|
|
|
|
|
|
|
|
// Mouse Actions in Canvas Coordinates
|
|
|
|
canvas: _context_coords(),
|
|
|
|
|
|
|
|
// Mouse Actions in World Coordinates
|
|
|
|
world: _context_coords(),
|
|
|
|
|
|
|
|
listen: {
|
|
|
|
window: _context_observers(),
|
|
|
|
canvas: _context_observers(),
|
|
|
|
world: _context_observers(),
|
|
|
|
},
|
2022-11-20 20:03:07 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
function _mouse_state_snapshot() {
|
2022-11-20 21:02:25 -06:00
|
|
|
return {
|
|
|
|
buttons: window.structuredClone(mouse.buttons),
|
|
|
|
window: window.structuredClone(mouse.window),
|
|
|
|
canvas: window.structuredClone(mouse.canvas),
|
|
|
|
world: window.structuredClone(mouse.world),
|
|
|
|
};
|
2022-11-20 20:03:07 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
const _double_click_timeout = {};
|
|
|
|
const _drag_start_timeout = {};
|
|
|
|
|
|
|
|
window.onmousedown = (evn) => {
|
2022-11-20 21:02:25 -06:00
|
|
|
const time = new Date();
|
|
|
|
|
|
|
|
// Processes for a named button
|
|
|
|
const onhold = (key) => () => {
|
|
|
|
if (_double_click_timeout[key]) {
|
|
|
|
// ondclick event
|
|
|
|
["window", "canvas", "world"].forEach((ctx) =>
|
|
|
|
mouse.listen[ctx][key].ondclick.emit({
|
|
|
|
target: evn.target,
|
|
|
|
buttonId: evn.button,
|
|
|
|
x: mouse[ctx].pos.x,
|
|
|
|
y: mouse[ctx].pos.y,
|
|
|
|
timestamp: new Date(),
|
|
|
|
})
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// Start timer
|
|
|
|
_double_click_timeout[key] = setTimeout(
|
|
|
|
() => delete _double_click_timeout[key],
|
|
|
|
inputConfig.dClickTiming
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set drag start timeout
|
|
|
|
_drag_start_timeout[key] = setTimeout(() => {
|
|
|
|
["window", "canvas", "world"].forEach((ctx) => {
|
|
|
|
mouse.listen[ctx][key].ondragstart.emit({
|
|
|
|
target: evn.target,
|
|
|
|
buttonId: evn.button,
|
|
|
|
x: mouse[ctx].pos.x,
|
|
|
|
y: mouse[ctx].pos.y,
|
|
|
|
timestamp: new Date(),
|
|
|
|
});
|
|
|
|
if (mouse[ctx].dragging[key]) mouse[ctx].dragging[key].drag = true;
|
|
|
|
|
|
|
|
delete _drag_start_timeout[key];
|
|
|
|
});
|
|
|
|
}, inputConfig.clickTiming);
|
|
|
|
|
|
|
|
["window", "canvas", "world"].forEach((ctx) => {
|
|
|
|
mouse.buttons[key] = time;
|
2022-11-21 05:24:35 -06:00
|
|
|
mouse[ctx].dragging[key] = {target: evn.target};
|
2022-11-20 21:02:25 -06:00
|
|
|
Object.assign(mouse[ctx].dragging[key], mouse[ctx].pos);
|
|
|
|
|
|
|
|
// onpaintstart event
|
|
|
|
mouse.listen[ctx][key].onpaintstart.emit({
|
|
|
|
target: evn.target,
|
|
|
|
buttonId: evn.button,
|
|
|
|
x: mouse[ctx].pos.x,
|
|
|
|
y: mouse[ctx].pos.y,
|
|
|
|
timestamp: new Date(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// Runs the correct handler
|
|
|
|
const buttons = [onhold("left"), onhold("middle"), onhold("right")];
|
|
|
|
|
|
|
|
buttons[evn.button] && buttons[evn.button]();
|
2022-11-20 20:03:07 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
window.onmouseup = (evn) => {
|
2022-11-20 21:02:25 -06:00
|
|
|
const time = new Date();
|
|
|
|
|
|
|
|
// Processes for a named button
|
|
|
|
const onrelease = (key) => () => {
|
|
|
|
["window", "canvas", "world"].forEach((ctx) => {
|
|
|
|
const start = {
|
|
|
|
x: mouse[ctx].dragging[key].x,
|
|
|
|
y: mouse[ctx].dragging[key].y,
|
|
|
|
};
|
|
|
|
|
|
|
|
// onclick event
|
|
|
|
const dx = mouse[ctx].pos.x - start.x;
|
|
|
|
const dy = mouse[ctx].pos.y - start.y;
|
|
|
|
|
|
|
|
if (
|
|
|
|
time.getTime() - mouse.buttons[key].getTime() <
|
|
|
|
inputConfig.clickTiming &&
|
|
|
|
dx * dx + dy * dy < inputConfig.clickRadius * inputConfig.clickRadius
|
|
|
|
)
|
|
|
|
mouse.listen[ctx][key].onclick.emit({
|
|
|
|
target: evn.target,
|
|
|
|
buttonId: evn.button,
|
|
|
|
x: mouse[ctx].pos.x,
|
|
|
|
y: mouse[ctx].pos.y,
|
|
|
|
timestamp: new Date(),
|
|
|
|
});
|
|
|
|
|
|
|
|
// onpaintend event
|
|
|
|
mouse.listen[ctx][key].onpaintend.emit({
|
|
|
|
target: evn.target,
|
2022-11-21 05:24:35 -06:00
|
|
|
initialTarget: mouse[ctx].dragging[key].target,
|
2022-11-20 21:02:25 -06:00
|
|
|
buttonId: evn.button,
|
|
|
|
x: mouse[ctx].pos.x,
|
|
|
|
y: mouse[ctx].pos.y,
|
|
|
|
timestamp: new Date(),
|
|
|
|
});
|
|
|
|
|
|
|
|
// ondragend event
|
|
|
|
if (mouse[ctx].dragging[key].drag)
|
|
|
|
mouse.listen[ctx][key].ondragend.emit({
|
|
|
|
target: evn.target,
|
2022-11-21 05:24:35 -06:00
|
|
|
initialTarget: mouse[ctx].dragging[key].target,
|
2022-11-20 21:02:25 -06:00
|
|
|
buttonId: evn.button,
|
|
|
|
x: mouse[ctx].pos.x,
|
|
|
|
y: mouse[ctx].pos.y,
|
|
|
|
timestamp: new Date(),
|
|
|
|
});
|
|
|
|
|
|
|
|
mouse[ctx].dragging[key] = null;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (_drag_start_timeout[key] !== undefined) {
|
|
|
|
clearTimeout(_drag_start_timeout[key]);
|
|
|
|
delete _drag_start_timeout[key];
|
|
|
|
}
|
|
|
|
mouse.buttons[key] = null;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Runs the correct handler
|
|
|
|
const buttons = [onrelease("left"), onrelease("middle"), onrelease("right")];
|
|
|
|
|
|
|
|
buttons[evn.button] && buttons[evn.button]();
|
2022-11-20 20:03:07 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
window.onmousemove = (evn) => {
|
2022-11-20 21:02:25 -06:00
|
|
|
// Set Window Coordinates
|
|
|
|
Object.assign(mouse.window.prev, mouse.window.pos);
|
|
|
|
mouse.window.pos = {x: evn.clientX, y: evn.clientY};
|
|
|
|
|
|
|
|
// Set Canvas Coordinates (using overlay canvas as reference)
|
|
|
|
if (evn.target.id === "overlayCanvas") {
|
|
|
|
Object.assign(mouse.canvas.prev, mouse.canvas.pos);
|
|
|
|
mouse.canvas.pos = {x: evn.layerX, y: evn.layerY};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set World Coordinates (For now the same as canvas coords; Will be useful with infinite canvas)
|
|
|
|
if (evn.target.id === "overlayCanvas") {
|
|
|
|
Object.assign(mouse.world.prev, mouse.world.pos);
|
|
|
|
mouse.world.pos = {x: evn.layerX, y: evn.layerY};
|
|
|
|
}
|
|
|
|
|
|
|
|
["window", "canvas", "world"].forEach((ctx) => {
|
|
|
|
mouse.listen[ctx].onmousemove.emit({
|
|
|
|
target: evn.target,
|
|
|
|
px: mouse[ctx].prev.x,
|
|
|
|
py: mouse[ctx].prev.y,
|
|
|
|
x: mouse[ctx].pos.x,
|
|
|
|
y: mouse[ctx].pos.y,
|
|
|
|
timestamp: new Date(),
|
|
|
|
});
|
|
|
|
["left", "middle", "right"].forEach((key) => {
|
|
|
|
// ondrag event
|
|
|
|
if (mouse[ctx].dragging[key] && mouse[ctx].dragging[key].drag)
|
|
|
|
mouse.listen[ctx][key].ondrag.emit({
|
|
|
|
target: evn.target,
|
2022-11-21 05:24:35 -06:00
|
|
|
initialTarget: mouse[ctx].dragging[key].target,
|
2022-11-20 21:02:25 -06:00
|
|
|
px: mouse[ctx].prev.x,
|
|
|
|
py: mouse[ctx].prev.y,
|
|
|
|
x: mouse[ctx].pos.x,
|
|
|
|
y: mouse[ctx].pos.y,
|
|
|
|
timestamp: new Date(),
|
|
|
|
});
|
|
|
|
|
|
|
|
// onpaint event
|
|
|
|
if (mouse[ctx].dragging[key])
|
|
|
|
mouse.listen[ctx][key].onpaint.emit({
|
|
|
|
target: evn.target,
|
2022-11-21 05:24:35 -06:00
|
|
|
initialTarget: mouse[ctx].dragging[key].target,
|
2022-11-20 21:02:25 -06:00
|
|
|
px: mouse[ctx].prev.x,
|
|
|
|
py: mouse[ctx].prev.y,
|
|
|
|
x: mouse[ctx].pos.x,
|
|
|
|
y: mouse[ctx].pos.y,
|
|
|
|
timestamp: new Date(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2022-11-20 20:03:07 -06:00
|
|
|
};
|
|
|
|
|
2022-11-22 16:24:55 -06:00
|
|
|
window.addEventListener(
|
|
|
|
"wheel",
|
|
|
|
(evn) => {
|
|
|
|
evn.preventDefault();
|
|
|
|
["window", "canvas", "world"].forEach((ctx) => {
|
|
|
|
mouse.listen[ctx].onwheel.emit({
|
|
|
|
target: evn.target,
|
|
|
|
delta: evn.deltaY,
|
|
|
|
deltaX: evn.deltaX,
|
|
|
|
deltaY: evn.deltaY,
|
|
|
|
deltaZ: evn.deltaZ,
|
|
|
|
mode: evn.deltaMode,
|
|
|
|
x: mouse[ctx].pos.x,
|
|
|
|
y: mouse[ctx].pos.y,
|
|
|
|
timestamp: new Date(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
{passive: false}
|
2022-11-20 20:03:07 -06:00
|
|
|
);
|
2022-11-20 20:23:08 -06:00
|
|
|
/**
|
2022-11-21 20:19:41 -06:00
|
|
|
* Keyboard input processing
|
2022-11-20 20:23:08 -06:00
|
|
|
*/
|
2022-11-21 20:19:41 -06:00
|
|
|
// Base object generator functions
|
|
|
|
|
|
|
|
const keyboard = {
|
|
|
|
keys: {},
|
|
|
|
|
|
|
|
isPressed(code) {
|
|
|
|
return this.keys[key].pressed;
|
|
|
|
},
|
|
|
|
|
|
|
|
isHeld(code) {
|
|
|
|
return !!this;
|
|
|
|
},
|
|
|
|
|
|
|
|
shortcuts: {},
|
|
|
|
onShortcut(shortcut, callback) {
|
|
|
|
/**
|
|
|
|
* Adds a shortcut handler (shorcut must be in format: {ctrl?: bool, alt?: bool, shift?: bool, key: string (code)})
|
|
|
|
* key must be the "code" parameter from keydown event; A key is "KeyA" for example
|
|
|
|
*/
|
|
|
|
if (this.shortcuts[shortcut.key] === undefined)
|
|
|
|
this.shortcuts[shortcut.key] = [];
|
|
|
|
|
|
|
|
this.shortcuts[shortcut.key].push({
|
|
|
|
ctrl: shortcut.ctrl,
|
|
|
|
alt: shortcut.alt,
|
|
|
|
shift: shortcut.shift,
|
|
|
|
id: guid(),
|
|
|
|
callback,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
deleteShortcut(id) {
|
|
|
|
this.shortcuts.keys().forEach((key) => {
|
|
|
|
this.shortcuts[key] = this.shortcuts[key].filter((v) => v.id !== id);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
listen: {
|
|
|
|
onkeydown: new Observer(),
|
|
|
|
onkeyup: new Observer(),
|
|
|
|
onkeyholdstart: new Observer(),
|
|
|
|
onkeyholdend: new Observer(),
|
|
|
|
onkeyclick: new Observer(),
|
|
|
|
onshortcut: new Observer(),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
window.onkeydown = (evn) => {
|
|
|
|
keyboard.listen.onkeydown.emit({
|
|
|
|
code: evn.code,
|
|
|
|
key: evn.key,
|
|
|
|
evn,
|
|
|
|
});
|
|
|
|
|
|
|
|
keyboard.keys[evn.code] = {
|
|
|
|
pressed: true,
|
|
|
|
held: false,
|
|
|
|
_hold_to: setTimeout(() => {
|
|
|
|
keyboard.keys[evn.code].held = true;
|
|
|
|
delete keyboard.keys[evn.code]._hold_to;
|
|
|
|
keyboard.listen.onkeyholdstart.emit({
|
|
|
|
code: evn.code,
|
|
|
|
key: evn.key,
|
|
|
|
evn,
|
|
|
|
});
|
|
|
|
}, inputConfig.keyboardHoldTiming),
|
|
|
|
};
|
|
|
|
|
2022-11-22 19:24:04 -06:00
|
|
|
// Process shortcuts if input target is not a text field
|
|
|
|
if (evn.target instanceof HTMLInputElement && evn.type === "text") return;
|
|
|
|
|
2022-11-21 20:19:41 -06:00
|
|
|
const callbacks = keyboard.shortcuts[evn.code];
|
|
|
|
|
|
|
|
if (callbacks)
|
|
|
|
callbacks.forEach((callback) => {
|
|
|
|
if (
|
|
|
|
!!callback.ctrl === evn.ctrlKey &&
|
|
|
|
!!callback.alt === evn.altKey &&
|
|
|
|
!!callback.shift === evn.shiftKey
|
|
|
|
) {
|
|
|
|
keyboard.listen.onshortcut.emit({
|
|
|
|
code: evn.code,
|
|
|
|
key: evn.key,
|
|
|
|
id: callback.id,
|
|
|
|
evn,
|
|
|
|
});
|
|
|
|
callback.callback(evn);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
window.onkeyup = (evn) => {
|
|
|
|
keyboard.listen.onkeyup.emit({
|
|
|
|
code: evn.code,
|
|
|
|
key: evn.key,
|
|
|
|
evn,
|
|
|
|
});
|
|
|
|
if (keyboard.keys[evn.code] && keyboard.keys[evn.code].held) {
|
|
|
|
keyboard.listen.onkeyholdend.emit({
|
|
|
|
code: evn.code,
|
|
|
|
key: evn.key,
|
|
|
|
evn,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
keyboard.listen.onkeyclick.emit({
|
|
|
|
code: evn.code,
|
|
|
|
key: evn.key,
|
|
|
|
evn,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
keyboard.keys[evn.code] = {
|
|
|
|
pressed: false,
|
|
|
|
held: false,
|
|
|
|
};
|
|
|
|
};
|