Merge pull request #22 from seijihariki/edit_utils

mouse input handler - input.js - proper solid mask
This commit is contained in:
tim h 2022-11-21 17:31:24 -06:00 committed by GitHub
commit 2efb01059c
6 changed files with 546 additions and 165 deletions

View file

@ -12,23 +12,23 @@
} }
.maskPaintCanvas { .maskPaintCanvas {
border: 3px dotted #993355C0 border: 3px dotted #993355c0;
} }
.overlayCanvas { .overlayCanvas {
border: 1px solid #F00; border: 1px solid #f00;
} }
.tempCanvas { .tempCanvas {
border: 3px dotted #007AFFC0; border: 3px dotted #007affc0;
} }
.targetCanvas { .targetCanvas {
border: 2px dashed #0F0; border: 2px dashed #0f0;
} }
.canvas { .canvas {
border: 2px dotted #00F; border: 2px dotted #00f;
} }
.mainHSplit { .mainHSplit {
@ -45,25 +45,22 @@
grid-template-rows: 1fr; grid-template-rows: 1fr;
grid-column-gap: 5px; grid-column-gap: 5px;
grid-row-gap: 5px; grid-row-gap: 5px;
} }
#infoContainer { .uiContainer {
position: absolute; position: absolute;
width: 250px; width: 250px;
height: auto; height: auto;
z-index: 999; z-index: 999;
}
#draggable{
cursor:move
} }
#DraggableTitleBar { .uiTitleBar {
z-index: 999; z-index: 999;
cursor: move; cursor: move;
background-color: rgba(104, 104, 104, 0.75); background-color: rgba(104, 104, 104, 0.75);
user-select: none;
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
padding-top: 5px; padding-top: 5px;
@ -79,6 +76,10 @@
border-color: black; border-color: black;
} }
.draggable {
cursor: move;
}
.toolbar { .toolbar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -153,7 +154,6 @@ button.tool:hover {
cursor: auto; cursor: auto;
} }
.canvasHolder { .canvasHolder {
position: relative; position: relative;
width: 2560px; width: 2560px;
@ -180,6 +180,10 @@ button.tool:hover {
position: absolute; position: absolute;
} }
.maskPaintCanvas {
filter: opacity(40%);
}
.strokeText { .strokeText {
-webkit-text-stroke: 1px #888; -webkit-text-stroke: 1px #888;
font-size: 150%; font-size: 150%;

View file

@ -9,8 +9,9 @@
</head> </head>
<body> <body>
<div id="infoContainer"> <!-- Main Toolbar -->
<div id="DraggableTitleBar" class="draggable">openOutpaint 🐠</div> <div id="infoContainer" class="uiContainer">
<div id="infoTitleBar" class="draggable uiTitleBar">openOutpaint 🐠</div>
<div id="info" class="info" style="min-width:200px;"> <div id="info" class="info" style="min-width:200px;">
<label for="host">Host</label> <label for="host">Host</label>
@ -69,9 +70,6 @@
<input type="checkbox" id="cbxEnableErasing" onchange="changeEnableErasing()"><br /> <input type="checkbox" id="cbxEnableErasing" onchange="changeEnableErasing()"><br />
<label for="cbxPaint">Mask mode?</label> <label for="cbxPaint">Mask mode?</label>
<input type="checkbox" id="cbxPaint" onchange="changePaintMode()"><br /> <input type="checkbox" id="cbxPaint" onchange="changePaintMode()"><br />
<!-- Having to tick a box to erase is a bad user experience, same goes for image erasing( cold be remedied by a temp confirm div) -->
<!-- <label for="cbxErase"><s>Erase mask?</s></label>
<input type="checkbox" id="cbxErase" onchange="changeEraseMode()" disabled="disabled"><br /> -->
<label for="cbxHRFix">Auto txt2img HRfix?</label> <label for="cbxHRFix">Auto txt2img HRfix?</label>
<input type="checkbox" id="cbxHRFix" onchange="changeHiResFix()"><br /> <input type="checkbox" id="cbxHRFix" onchange="changeHiResFix()"><br />
@ -116,6 +114,17 @@
<br /> <br />
<hr> <hr>
</div> </div>
</div>
</div>
<!-- History Toolbar -->
<div id="historyContainer" class="uiContainer" style="right: 0;">
<div id="historyTitleBar" class="draggable uiTitleBar">History</div>
<div class="info" style="min-width:200px;">
<div id="history" class="history">
</div>
<div class="toolbar"> <div class="toolbar">
<button type="button" onclick="commands.undo()" class="tool">undo</button> <button type="button" onclick="commands.undo()" class="tool">undo</button>
<button type="button" onclick="commands.redo()" class="tool">redo</button> <button type="button" onclick="commands.redo()" class="tool">redo</button>
@ -176,6 +185,8 @@
</div> </div>
<script src="js/util.js" type="text/javascript"></script>
<script src="js/input.js" type="text/javascript"></script>
<script src="js/commands.js" type="text/javascript"></script> <script src="js/commands.js" type="text/javascript"></script>
<script src="js/index.js" type="text/javascript"></script> <script src="js/index.js" type="text/javascript"></script>
<script src="js/settingsbar.js" type="text/javascript"></script> <script src="js/settingsbar.js" type="text/javascript"></script>

View file

@ -444,42 +444,49 @@ function mouseMove(evt) {
basePixelCount * scaleFactor, basePixelCount * scaleFactor,
basePixelCount * scaleFactor basePixelCount * scaleFactor
); //origin is middle of the frame ); //origin is middle of the frame
} else { }
}
/**
* Mask implementation
*/
mouse.listen.canvas.onmousemove.on((evn) => {
if (paintMode && evn.target.id === "overlayCanvas") {
// draw big translucent red blob cursor // draw big translucent red blob cursor
ovCtx.beginPath(); 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.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.fillStyle = "#FF6A6A50";
ovCtx.fill(); ovCtx.fill();
// in case i'm trying to draw }
mouseX = parseInt(evt.clientX - canvasOffsetX); });
mouseY = parseInt(evt.clientY - canvasOffsetY);
if (clicked) { mouse.listen.canvas.left.onpaint.on((evn) => {
// i'm trying to draw, please draw :( if (paintMode && evn.initialTarget.id === "overlayCanvas") {
maskPaintCtx.globalCompositeOperation = "source-over"; maskPaintCtx.globalCompositeOperation = "source-over";
maskPaintCtx.strokeStyle = "#FF6A6A10"; maskPaintCtx.strokeStyle = "#FF6A6A";
maskPaintCtx.lineWidth = 8 * scaleFactor; maskPaintCtx.lineWidth = 8 * scaleFactor;
maskPaintCtx.beginPath(); maskPaintCtx.beginPath();
maskPaintCtx.moveTo(prevMouseX, prevMouseY); maskPaintCtx.moveTo(evn.px, evn.py);
maskPaintCtx.lineTo(mouseX, mouseY); maskPaintCtx.lineTo(evn.x, evn.y);
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round"; maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
maskPaintCtx.stroke(); 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) { mouse.listen.canvas.right.onpaint.on((evn) => {
if (paintMode && evn.initialTarget.id === "overlayCanvas") {
maskPaintCtx.globalCompositeOperation = "destination-out"; maskPaintCtx.globalCompositeOperation = "destination-out";
maskPaintCtx.beginPath();
maskPaintCtx.strokeStyle = "#FFFFFFFF"; maskPaintCtx.strokeStyle = "#FFFFFFFF";
maskPaintCtx.lineWidth = 8 * scaleFactor; maskPaintCtx.lineWidth = 8 * scaleFactor;
maskPaintCtx.moveTo(prevMouseX, prevMouseY); maskPaintCtx.beginPath();
maskPaintCtx.lineTo(mouseX, mouseY); maskPaintCtx.moveTo(evn.px, evn.py);
maskPaintCtx.lineTo(evn.x, evn.y);
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round"; maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
maskPaintCtx.stroke(); maskPaintCtx.stroke();
} }
prevMouseX = mouseX; });
prevMouseY = mouseY;
}
}
function mouseDown(evt) { function mouseDown(evt) {
const rect = ovCanvas.getBoundingClientRect(); const rect = ovCanvas.getBoundingClientRect();
@ -496,14 +503,7 @@ function mouseDown(evt) {
nextBox.w = arbitraryImageData.width; nextBox.w = arbitraryImageData.width;
nextBox.h = arbitraryImageData.height; nextBox.h = arbitraryImageData.height;
dropTargets.push(nextBox); dropTargets.push(nextBox);
} else if (paintMode) { } 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 {
//const rect = ovCanvas.getBoundingClientRect() //const rect = ovCanvas.getBoundingClientRect()
var nextBox = {}; var nextBox = {};
nextBox.x = nextBox.x =
@ -738,6 +738,7 @@ function changePaintMode() {
} }
function changeEnableErasing() { function changeEnableErasing() {
// yeah because this is for the image layer
enableErasing = document.getElementById("cbxEnableErasing").checked; enableErasing = document.getElementById("cbxEnableErasing").checked;
localStorage.setItem("enable_erase", enableErasing); localStorage.setItem("enable_erase", enableErasing);
} }

303
js/input.js Normal file
View file

@ -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
*/

View file

@ -1,11 +1,10 @@
dragElement(document.getElementById("infoContainer")); //dragElement(document.getElementById("infoContainer"));
//dragElement(document.getElementById("historyContainer"));
function dragElement(elmnt) { function dragElement(elmnt) {
var p1 = 0, var p3 = 0,
p2 = 0,
p3 = 0,
p4 = 0; p4 = 0;
var draggableElements = document.getElementsByClassName("draggable"); var draggableElements = elmnt.getElementsByClassName("draggable");
for (var i = 0; i < draggableElements.length; i++) { for (var i = 0; i < draggableElements.length; i++) {
draggableElements[i].onmousedown = dragMouseDown; draggableElements[i].onmousedown = dragMouseDown;
} }
@ -20,8 +19,8 @@ function dragElement(elmnt) {
function elementDrag(e) { function elementDrag(e) {
e.preventDefault(); e.preventDefault();
p1 = p3 - e.clientX; elmnt.style.bottom = null;
p2 = p4 - e.clientY; elmnt.style.right = null;
elmnt.style.top = elmnt.offsetTop - (p4 - e.clientY) + "px"; elmnt.style.top = elmnt.offsetTop - (p4 - e.clientY) + "px";
elmnt.style.left = elmnt.offsetLeft - (p3 - e.clientX) + "px"; elmnt.style.left = elmnt.offsetLeft - (p3 - e.clientX) + "px";
p3 = e.clientX; 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"); var coll = document.getElementsByClassName("collapsible");
for (var i = 0; i < coll.length; i++) { for (var i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function () { coll[i].addEventListener("click", function () {

27
js/util.js Normal file
View file

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