diff --git a/index.html b/index.html index bd0b74d..ed2ec20 100644 --- a/index.html +++ b/index.html @@ -176,6 +176,8 @@ + + diff --git a/js/input.js b/js/input.js new file mode 100644 index 0000000..98b005e --- /dev/null +++ b/js/input.js @@ -0,0 +1,284 @@ +const inputConfig = { + 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). +}; + +/** + * Mouse input processing + */ +// Base object generator functions +function _context_coords() { + return { + dragging: { + left: null, + middle: null, + right: null, + }, + + prev: { + x: 0, + y: 0, + }, + + pos: { + x: 0, + y: 0, + }, + }; +} +function _mouse_observers() { + return { + // Simple click handlers + onclick: new Observer(), + // Double click handlers (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 _context_observers() { + return { + left: _mouse_observers(), + middle: _mouse_observers(), + right: _mouse_observers(), + }; +} + +const mouse = { + 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(), + }, +}; + +function _mouse_state_snapshot() { + return { + buttons: window.structuredClone(mouse.buttons), + window: window.structuredClone(mouse.window), + canvas: window.structuredClone(mouse.canvas), + world: window.structuredClone(mouse.world), + }; +} + +const _double_click_timeout = {}; +const _drag_start_timeout = {}; + +window.onmousedown = (evn) => { + 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({ + 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({ + 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; + mouse[ctx].dragging[key] = {}; + Object.assign(mouse[ctx].dragging[key], mouse[ctx].pos); + + // onpaintstart event + mouse.listen[ctx][key].onpaintstart.emit({ + 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](); +}; + +window.onmouseup = (evn) => { + 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({ + buttonId: evn.button, + x: mouse[ctx].pos.x, + y: mouse[ctx].pos.y, + timestamp: new Date(), + }); + + // onpaintend event + mouse.listen[ctx][key].onpaintend.emit({ + 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({ + 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](); +}; + +window.onmousemove = (evn) => { + // 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) => { + ['left', 'middle', 'right'].forEach((key) => { + // ondrag event + if (mouse[ctx].dragging[key] && mouse[ctx].dragging[key].drag) + mouse.listen[ctx][key].ondrag.emit({ + px: mouse[ctx].prev.x, + py: mouse[ctx].prev.x, + 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({ + px: mouse[ctx].prev.x, + py: mouse[ctx].prev.x, + x: mouse[ctx].pos.x, + y: mouse[ctx].pos.y, + timestamp: new Date(), + }); + }); + }); +}; +/** MOUSE DEBUG */ +/* +mouse.listen.window.right.onclick.on(() => + console.debug('mouse.listen.window.right.onclick') +); + +mouse.listen.window.right.ondclick.on(() => + console.debug('mouse.listen.window.right.ondclick') +); +mouse.listen.window.right.ondragstart.on(() => + console.debug('mouse.listen.window.right.ondragstart') +); +mouse.listen.window.right.ondrag.on(() => + console.debug('mouse.listen.window.right.ondrag') +); +mouse.listen.window.right.ondragend.on(() => + console.debug('mouse.listen.window.right.ondragend') +); + +mouse.listen.window.right.onpaintstart.on(() => + console.debug('mouse.listen.window.right.onpaintstart') +); +mouse.listen.window.right.onpaint.on(() => + console.debug('mouse.listen.window.right.onpaint') +); +mouse.listen.window.right.onpaintend.on(() => + console.debug('mouse.listen.window.right.onpaintend') +); +*/ diff --git a/js/util.js b/js/util.js new file mode 100644 index 0000000..c1c1eb0 --- /dev/null +++ b/js/util.js @@ -0,0 +1,27 @@ +/** + * Implementation of a simple Oberver Pattern for custom event handling + */ +function Observer() { + this.handlers = new Set(); +} + +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(handler); + } + }); + }, +};