+
+
+
@@ -176,6 +185,8 @@
+
+
diff --git a/js/index.js b/js/index.js
index 0a73365..bf8ebc1 100644
--- a/js/index.js
+++ b/js/index.js
@@ -444,43 +444,50 @@ function mouseMove(evt) {
basePixelCount * scaleFactor,
basePixelCount * scaleFactor
); //origin is middle of the frame
- } else {
- // draw big translucent red blob cursor
- ovCtx.beginPath();
- ovCtx.arc(canvasX, canvasY, 4 * scaleFactor, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
- ovCtx.fillStyle = "#FF6A6A50";
- ovCtx.fill();
- // in case i'm trying to draw
- mouseX = parseInt(evt.clientX - canvasOffsetX);
- mouseY = parseInt(evt.clientY - canvasOffsetY);
- if (clicked) {
- // i'm trying to draw, please draw :(
- maskPaintCtx.globalCompositeOperation = "source-over";
- maskPaintCtx.strokeStyle = "#FF6A6A10";
- maskPaintCtx.lineWidth = 8 * scaleFactor;
- maskPaintCtx.beginPath();
- maskPaintCtx.moveTo(prevMouseX, prevMouseY);
- maskPaintCtx.lineTo(mouseX, mouseY);
- maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
- maskPaintCtx.stroke();
- }
- // Erase mask if right button is held
- // no reason to have to tick a checkbox for this, more intuitive for both erases (mask and actual images) to just work on right click and inform the user about it
- if (evt.buttons == 2) {
- maskPaintCtx.globalCompositeOperation = "destination-out";
- maskPaintCtx.beginPath();
- maskPaintCtx.strokeStyle = "#FFFFFFFF";
- maskPaintCtx.lineWidth = 8 * scaleFactor;
- maskPaintCtx.moveTo(prevMouseX, prevMouseY);
- maskPaintCtx.lineTo(mouseX, mouseY);
- maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
- maskPaintCtx.stroke();
- }
- prevMouseX = mouseX;
- prevMouseY = mouseY;
}
}
+/**
+ * Mask implementation
+ */
+mouse.listen.canvas.onmousemove.on((evn) => {
+ if (paintMode && evn.target.id === "overlayCanvas") {
+ // draw big translucent red blob cursor
+ ovCtx.beginPath();
+ ovCtx.arc(evn.x, evn.y, 4 * scaleFactor, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
+ ovCtx.fillStyle = "#FF6A6A50";
+ ovCtx.fill();
+ }
+});
+
+mouse.listen.canvas.left.onpaint.on((evn) => {
+ if (paintMode && evn.initialTarget.id === "overlayCanvas") {
+ maskPaintCtx.globalCompositeOperation = "source-over";
+ maskPaintCtx.strokeStyle = "#FF6A6A";
+
+ maskPaintCtx.lineWidth = 8 * scaleFactor;
+ maskPaintCtx.beginPath();
+ maskPaintCtx.moveTo(evn.px, evn.py);
+ maskPaintCtx.lineTo(evn.x, evn.y);
+ maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
+ maskPaintCtx.stroke();
+ }
+});
+
+mouse.listen.canvas.right.onpaint.on((evn) => {
+ if (paintMode && evn.initialTarget.id === "overlayCanvas") {
+ maskPaintCtx.globalCompositeOperation = "destination-out";
+ maskPaintCtx.strokeStyle = "#FFFFFFFF";
+
+ maskPaintCtx.lineWidth = 8 * scaleFactor;
+ maskPaintCtx.beginPath();
+ maskPaintCtx.moveTo(evn.px, evn.py);
+ maskPaintCtx.lineTo(evn.x, evn.y);
+ maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
+ maskPaintCtx.stroke();
+ }
+});
+
function mouseDown(evt) {
const rect = ovCanvas.getBoundingClientRect();
var oddOffset = 0;
@@ -496,14 +503,7 @@ function mouseDown(evt) {
nextBox.w = arbitraryImageData.width;
nextBox.h = arbitraryImageData.height;
dropTargets.push(nextBox);
- } else if (paintMode) {
- //const rect = ovCanvas.getBoundingClientRect() // not-quite pixel offset was driving me insane
- const canvasOffsetX = rect.left;
- const canvasOffsetY = rect.top;
- prevMouseX = mouseX = evt.clientX - canvasOffsetX;
- prevMouseY = mouseY = evt.clientY - canvasOffsetY;
- clicked = true;
- } else {
+ } else if (!paintMode) {
//const rect = ovCanvas.getBoundingClientRect()
var nextBox = {};
nextBox.x =
@@ -738,6 +738,7 @@ function changePaintMode() {
}
function changeEnableErasing() {
+ // yeah because this is for the image layer
enableErasing = document.getElementById("cbxEnableErasing").checked;
localStorage.setItem("enable_erase", enableErasing);
}
diff --git a/js/input.js b/js/input.js
new file mode 100644
index 0000000..432b93b
--- /dev/null
+++ b/js/input.js
@@ -0,0 +1,303 @@
+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 {
+ onmousemove: new Observer(),
+ 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({
+ 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;
+ mouse[ctx].dragging[key] = {target: evn.target};
+ 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]();
+};
+
+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({
+ 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,
+ initialTarget: mouse[ctx].dragging[key].target,
+ 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,
+ initialTarget: mouse[ctx].dragging[key].target,
+ 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) => {
+ 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,
+ initialTarget: mouse[ctx].dragging[key].target,
+ 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,
+ initialTarget: mouse[ctx].dragging[key].target,
+ px: mouse[ctx].prev.x,
+ py: mouse[ctx].prev.y,
+ 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')
+);
+*/
+
+/**
+ * Mouse input processing
+ */
diff --git a/js/settingsbar.js b/js/settingsbar.js
index 84120a3..b7d6e42 100644
--- a/js/settingsbar.js
+++ b/js/settingsbar.js
@@ -1,11 +1,10 @@
-dragElement(document.getElementById("infoContainer"));
+//dragElement(document.getElementById("infoContainer"));
+//dragElement(document.getElementById("historyContainer"));
function dragElement(elmnt) {
- var p1 = 0,
- p2 = 0,
- p3 = 0,
+ var p3 = 0,
p4 = 0;
- var draggableElements = document.getElementsByClassName("draggable");
+ var draggableElements = elmnt.getElementsByClassName("draggable");
for (var i = 0; i < draggableElements.length; i++) {
draggableElements[i].onmousedown = dragMouseDown;
}
@@ -20,8 +19,8 @@ function dragElement(elmnt) {
function elementDrag(e) {
e.preventDefault();
- p1 = p3 - e.clientX;
- p2 = p4 - e.clientY;
+ elmnt.style.bottom = null;
+ elmnt.style.right = null;
elmnt.style.top = elmnt.offsetTop - (p4 - e.clientY) + "px";
elmnt.style.left = elmnt.offsetLeft - (p3 - e.clientX) + "px";
p3 = e.clientX;
@@ -34,6 +33,42 @@ function dragElement(elmnt) {
}
}
+function makeDraggable(id) {
+ const element = document.getElementById(id);
+ const startbb = element.getBoundingClientRect();
+ let dragging = false;
+ let offset = {x: 0, y: 0};
+
+ element.style.top = startbb.y + "px";
+ element.style.left = startbb.x + "px";
+
+ mouse.listen.window.left.onpaintstart.on((evn) => {
+ if (
+ element.contains(evn.target) &&
+ evn.target.classList.contains("draggable")
+ ) {
+ const bb = element.getBoundingClientRect();
+ offset.x = evn.x - bb.x;
+ offset.y = evn.y - bb.y;
+ dragging = true;
+ }
+ });
+
+ mouse.listen.window.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) => {
+ dragging = false;
+ });
+}
+
+makeDraggable("infoContainer");
+makeDraggable("historyContainer");
+
var coll = document.getElementsByClassName("collapsible");
for (var i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function () {
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);
+ }
+ });
+ },
+};