diff --git a/css/index.css b/css/index.css index bd2f317..bd5b3d8 100644 --- a/css/index.css +++ b/css/index.css @@ -1,187 +1,191 @@ * { - font-size: 100%; - font-family: Arial, Helvetica, sans-serif; + font-size: 100%; + font-family: Arial, Helvetica, sans-serif; } .container { - position: relative; + position: relative; } .backgroundCanvas { - background-color: #ccc; + background-color: #ccc; } .maskPaintCanvas { - border: 3px dotted #993355C0 + border: 3px dotted #993355c0; } .overlayCanvas { - border: 1px solid #F00; + border: 1px solid #f00; } .tempCanvas { - border: 3px dotted #007AFFC0; + border: 3px dotted #007affc0; } .targetCanvas { - border: 2px dashed #0F0; + border: 2px dashed #0f0; } .canvas { - border: 2px dotted #00F; + border: 2px dotted #00f; } .mainHSplit { - display: grid; - grid-template-columns: 1fr; - grid-template-rows: repeat(2, 1fr); - grid-column-gap: 5px; - grid-row-gap: 5px; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: repeat(2, 1fr); + grid-column-gap: 5px; + grid-row-gap: 5px; } .uiWrapper { - display: grid; - grid-template-columns: 1fr 15fr; - grid-template-rows: 1fr; - grid-column-gap: 5px; - grid-row-gap: 5px; - + display: grid; + grid-template-columns: 1fr 15fr; + grid-template-rows: 1fr; + grid-column-gap: 5px; + grid-row-gap: 5px; } -#infoContainer { - position: absolute; - width: 250px; - height: auto; - z-index: 999; - -} -#draggable{ - cursor:move +.uiContainer { + position: absolute; + width: 250px; + height: auto; + z-index: 999; } -#DraggableTitleBar { - z-index: 999; - cursor: move; - background-color: rgba(104, 104, 104, 0.75); +.uiTitleBar { + z-index: 999; + cursor: move; + background-color: rgba(104, 104, 104, 0.75); - padding-left: 5px; - padding-right: 5px; - padding-top: 5px; - padding-bottom: 5px; - margin-bottom: auto; - font-size: 1.5em; - color: black; - text-align: center; - border-top-left-radius: 10px; - border-top-right-radius: 10px; - border: solid; - border-bottom: none; - border-color: black; + user-select: none; + + padding-left: 5px; + padding-right: 5px; + padding-top: 5px; + padding-bottom: 5px; + margin-bottom: auto; + font-size: 1.5em; + color: black; + text-align: center; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + border: solid; + border-bottom: none; + border-color: black; +} + +.draggable { + cursor: move; } .toolbar { - display: flex; - justify-content: space-between; + display: flex; + justify-content: space-between; } .toolbar > .tool { - flex: 1; + flex: 1; } .toolbar > .tool:not(:last-child) { - margin-right: 10px; + margin-right: 10px; } button.tool { - background-color: rgb(0, 0, 50); - color: rgb(255, 255, 255); - border-radius: 5px; - cursor: pointer; - border: none; - text-align: center; - outline: none; - font-size: 15px; - padding: 5px; - margin-top: 5px; - margin-bottom: 5px; + background-color: rgb(0, 0, 50); + color: rgb(255, 255, 255); + border-radius: 5px; + cursor: pointer; + border: none; + text-align: center; + outline: none; + font-size: 15px; + padding: 5px; + margin-top: 5px; + margin-bottom: 5px; } button.tool:hover { - background-color: #667; + background-color: #667; } .collapsible { - background-color: rgb(0, 0, 0); - color: rgb(255, 255, 255); - border-radius: 5px; - cursor: pointer; - width: 100%; - border: none; - text-align: center; - outline: none; - font-size: 15px; - padding: 5px; - margin-top: 5px; - margin-bottom: 5px; + background-color: rgb(0, 0, 0); + color: rgb(255, 255, 255); + border-radius: 5px; + cursor: pointer; + width: 100%; + border: none; + text-align: center; + outline: none; + font-size: 15px; + padding: 5px; + margin-top: 5px; + margin-bottom: 5px; } .collapsible:hover { - background-color: #777; + background-color: #777; } .content { - max-height: 0; - overflow: hidden; - transition: max-height 0.2s ease-out; + max-height: 0; + overflow: hidden; + transition: max-height 0.2s ease-out; } .info { - background-color: rgba(255, 255, 255, 0.5); - padding-left: 10px; - padding-right: 10px; - padding-top: 5px; - padding-bottom: 5px; + background-color: rgba(255, 255, 255, 0.5); + padding-left: 10px; + padding-right: 10px; + padding-top: 5px; + padding-bottom: 5px; - color: black; - border: solid; - border-top: none; - border-color: black; - font-size: medium; - text-align: left; - max-height: fit-content; - overflow: auto; - cursor: auto; + color: black; + border: solid; + border-top: none; + border-color: black; + font-size: medium; + text-align: left; + max-height: fit-content; + overflow: auto; + cursor: auto; } - .canvasHolder { - position: relative; - width: 2560px; - height: 1440px; + position: relative; + width: 2560px; + height: 1440px; } .mainCanvases { - position: absolute; - top: 0px; - left: 0px; - width: 2560px; - height: 1440px; + position: absolute; + top: 0px; + left: 0px; + width: 2560px; + height: 1440px; } .masks { - display: grid; - grid-template-columns: repeat(3, 1fr); - grid-template-rows: 1fr; - grid-column-gap: 0px; - grid-row-gap: 0px; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: 1fr; + grid-column-gap: 0px; + grid-row-gap: 0px; } .maskCanvasMonitor .overMaskCanvasMonitor .initImgCanvasMonitor { - position: absolute; + position: absolute; +} + +.maskPaintCanvas { + filter: opacity(40%); } .strokeText { - -webkit-text-stroke: 1px #888; - font-size: 150%; - color: #000; -} \ No newline at end of file + -webkit-text-stroke: 1px #888; + font-size: 150%; + color: #000; +} diff --git a/index.html b/index.html index a19a18f..42f70b7 100644 --- a/index.html +++ b/index.html @@ -9,8 +9,9 @@ -
-
openOutpaint 🐠
+ +
+
openOutpaint 🐠
@@ -69,9 +70,6 @@

- -
@@ -116,6 +114,17 @@

+
+ +
+ + +
+
History
+
+
+ +
@@ -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); + } + }); + }, +};