Merge pull request #60 from zero01101/bleeding-edge

Automatic Layers and Not-Quite Infinity
This commit is contained in:
Victor Seiji Hariki 2022-12-01 00:33:32 -03:00 committed by GitHub
commit 3281277e4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1716 additions and 1117 deletions

View file

@ -69,7 +69,7 @@ you'll obviously need A1111's webUI installed before you can use this, thus you'
5. configure your local webhost in your homelab to serve the newly cloned repo like the technological bastion you are, or simply run the included `openOutpaint.bat` on windows or `openOutpaint.sh` on mac/linux.
6. open your locally-hosted web server at http://127.0.0.1:3456 (or wherever, i'm not your boss)
7. update the host field if necessary to point at your stable diffusion API address, change my stupid prompts with whatever you want, click somewhere in the canvas using the dream [`d`] tool, and wait (**OR** you can load as many existing images from your computer as you'd like using the "stamp image" tool [`U`]). if you've requested a batch of generated images and one of them sparks you as something you might want to use later, you can click the "res" button to add the image to the stampable dream resources as well. _(NOTE: you can select or deselect imported images/added resources freely simply by clicking on them)_
7. update the host field if necessary to point at your stable diffusion API address, change my stupid prompts with whatever you want, click somewhere in the canvas using the dream [`d`] tool, and wait (**OR** you can load as many existing images from your computer as you'd like using the "stamp image" tool [`U`]). If you've requested a batch of generated images and one of them sparks you as something you might want to use later, you can click the "res" button to add the image to the stampable dream resources as well. _(NOTE: you can select or deselect imported images/added resources freely simply by clicking on them)_
8. once an image appears\*, click the `<` and `>` buttons at the bottom-left corner of the image to cycle through the others in the batch if you requested multiple (it defaults to 2 batch size, 2 batch count) - click `y` to choose one you like, or `n` to cancel that image generation batch outright and possibly try again
9. now that you've got a starter, click somewhere near it to outpaint - try and include as much of the "context" as possible in the reticle for the best result convergence, or you can right-click to remove some of it if you want to completely retry a chunk but leave the rest alone
10. enable the mask mode to prepare previously rendered imagery for touchups/inpainting, then paint over the objectionable region; once your masked region is drawn, disable mask mode and change your prompt if necessary, then click over the canvas containing the mask you just painted to request the refined image(s)

View file

@ -2,8 +2,16 @@
font-size: 100%;
font-family: Arial, Helvetica, sans-serif;
}
/* Body is stuck with no scroll */
body {
width: 100%;
height: 100%;
margin: 0px;
padding: 0px;
overflow: clip;
}
.container {
@ -96,31 +104,27 @@ body {
grid-row-gap: 0px;
}
.maskCanvasMonitor .overMaskCanvasMonitor .initImgCanvasMonitor {
position: absolute;
}
/* Mask colors for mask inversion */
/* Filters are some magic acquired at https://codepen.io/sosuke/pen/Pjoqqp */
.maskPaintCanvas {
.mask-canvas {
opacity: 0%;
}
.maskPaintCanvas.display {
.mask-canvas.display {
opacity: 40%;
filter: invert(100%);
}
.maskPaintCanvas.display.opaque {
.mask-canvas.display.opaque {
opacity: 100%;
}
.maskPaintCanvas.display.clear {
.mask-canvas.display.clear {
filter: invert(71%) sepia(46%) saturate(6615%) hue-rotate(321deg)
brightness(106%) contrast(100%);
}
.maskPaintCanvas.display.hold {
.mask-canvas.display.hold {
filter: invert(41%) sepia(16%) saturate(5181%) hue-rotate(218deg)
brightness(103%) contrast(108%);
}
@ -193,6 +197,10 @@ body {
background-color: #dddd49;
}
.host-field-wrapper .connection-status.before {
background-color: #777;
}
input#host {
box-sizing: border-box;
}
@ -207,11 +215,6 @@ div.prompt-wrapper > textarea {
margin: 0;
padding: 0;
top: 0px;
bottom: 0px;
left: 0px;
right: 0;
resize: vertical;
}
@ -220,7 +223,6 @@ div.prompt-wrapper > textarea:focus {
}
/* Tool buttons */
.button-array {
display: flex;
justify-content: stretch;

46
css/layers.css Normal file
View file

@ -0,0 +1,46 @@
/* Debug floating window */
#layer-preview .preview-canvas {
background-color: white;
width: 100%;
height: 150px;
}
#layer-manager .menu-container {
height: 200px;
}
.layer-render-target {
position: fixed;
background-color: #466;
margin: 0;
padding: 0;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.layer-render-target .collection {
position: absolute;
}
.layer-render-target .collection > .collection-input-overlay {
position: absolute;
top: 0;
left: 0;
z-index: 10;
}
.layer-render-target canvas {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}

View file

@ -9,6 +9,10 @@
background-color: var(--c-primary);
}
#ui-toolbar * {
user-select: none;
}
#ui-toolbar .handle {
display: flex;
align-items: center;

View file

@ -7,6 +7,8 @@
<link href="css/colors.css" rel="stylesheet" />
<link href="css/index.css" rel="stylesheet" />
<link href="css/layers.css" rel="stylesheet" />
<link href="css/ui/generic.css" rel="stylesheet" />
<link href="css/ui/history.css" rel="stylesheet" />
@ -190,109 +192,26 @@
</div>
<!-- Canvases -->
<div
id="mainHSplit"
class="mainHSplit"
onmouseover="document.activeElement.blur()">
<div id="uiWrapper" class="uiWrapper">
<div
id="canvasHolder"
class="canvasHolder"
oncontextmenu="return false;">
<canvas
id="backgroundCanvas"
class="mainCanvases backgroundCanvas"
width="2560"
height="1440"
style="z-index: 0">
<!-- gray grid bg canvas -->
<p>lol ur browser sucks</p>
</canvas>
<canvas
id="canvas"
class="mainCanvases canvas"
width="2560"
height="1440"
style="z-index: 1">
<!-- normal canvas on which images are drawn -->
<p>lol ur browser sucks</p>
</canvas>
<canvas
id="tempCanvas"
class="mainCanvases tempCanvas"
width="2560"
height="1440"
style="z-index: 2">
<!-- temporary canvas on which images being selected/rejected or imported arbitrary images are superimposed -->
<p>lol ur browser sucks</p>
</canvas>
<canvas
id="targetCanvas"
class="mainCanvases targetCanvas"
width="2560"
height="1440"
style="z-index: 3">
<!-- canvas on which "targeting" squares are drawn -->
<p>lol ur browser sucks</p>
</canvas>
<canvas
id="maskPaintCanvas"
class="mainCanvases maskPaintCanvas"
width="2560"
height="1440"
style="z-index: 4">
<!-- canvas on which masking brush is "painted" -->
<p>lol ur browser sucks</p>
</canvas>
<canvas
id="overlayCanvas"
class="mainCanvases overlayCanvas"
width="2560"
height="1440"
style="z-index: 5">
<!-- canvas on which "cursor" reticle or arc is drawn -->
<p>lol ur browser sucks</p>
</canvas>
<div id="tempDiv" style="position: relative; z-index: 6">
<!-- where popup buttons go -->
</div>
</div>
</div>
<div id="masks" class="masks">
<div>
<!-- <canvas id="maskCanvasMonitor" class="maskCanvasMonitor" width="512" height="512">
<p>lol ur browser sucks</p>
</canvas><br /> -->
<canvas
id="overMaskCanvasMonitor"
class="overMaskCanvasMonitor"
width="512"
height="512">
<p>lol ur browser sucks</p>
</canvas>
<br />
<canvas
id="initImgCanvasMonitor"
class="initImgCanvasMonitor"
width="512"
height="512">
<p>lol ur browser sucks</p>
</canvas>
<br />
</div>
</div>
</div>
<div id="layer-render" class="layer-render-target"></div>
<!-- Base Libs -->
<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/ui/history.js" type="text/javascript"></script>
<script src="js/lib/util.js" type="text/javascript"></script>
<script src="js/lib/input.js" type="text/javascript"></script>
<script src="js/lib/layers.js" type="text/javascript"></script>
<script src="js/lib/commands.js" type="text/javascript"></script>
<script src="js/settingsbar.js" type="text/javascript"></script>
<script src="js/lib/toolbar.js" type="text/javascript"></script>
<script
src="js/initalize/layers.populate.js"
type="text/javascript"></script>
<!-- Content -->
<script src="js/index.js" type="text/javascript"></script>
<script src="js/shortcuts.js" type="text/javascript"></script>
<script src="js/ui/floating/history.js" type="text/javascript"></script>
<!-- Load Tools -->
<script src="js/ui/tool/dream.js" type="text/javascript"></script>
@ -300,6 +219,12 @@
<script src="js/ui/tool/select.js" type="text/javascript"></script>
<script src="js/ui/tool/stamp.js" type="text/javascript"></script>
<script src="js/ui/toolbar.js" type="text/javascript"></script>
<!-- Initialize -->
<script
src="js/initalize/toolbar.populate.js"
type="text/javascript"></script>
<script
src="js/initalize/debug.populate.js"
type="text/javascript"></script>
</body>
</html>

View file

@ -48,7 +48,6 @@ var stableDiffusionData = {
};
// stuff things use
var blockNewImages = false;
var returnedImages;
var imageIndex = 0;
var tmpImgXYWH = {};
@ -57,15 +56,6 @@ var url = "/sdapi/v1/";
var endpoint = "txt2img";
var frameX = 512;
var frameY = 512;
var prevMouseX = 0;
var prevMouseY = 0;
var mouseX = 0;
var mouseY = 0;
var canvasX = 0;
var canvasY = 0;
var heldButton = 0;
var snapX = 0;
var snapY = 0;
var drawThis = {};
const basePixelCount = 64; //64 px - ALWAYS 64 PX
var scaleFactor = 8; //x64 px
@ -85,47 +75,25 @@ var arbitraryImageBitmap;
var arbitraryImageBase64; // seriously js cmon work with me here
var placingArbitraryImage = false; // for when the user has loaded an existing image from their computer
var marchOffset = 0;
var stopMarching = null;
var inProgress = false;
var marchCoords = {};
// info div, sometimes hidden
let mouseXInfo = document.getElementById("mouseX");
let mouseYInfo = document.getElementById("mouseY");
let canvasXInfo = document.getElementById("canvasX");
let canvasYInfo = document.getElementById("canvasY");
let snapXInfo = document.getElementById("snapX");
let snapYInfo = document.getElementById("snapY");
let heldButtonInfo = document.getElementById("heldButton");
// canvases and related
const ovCanvas = document.getElementById("overlayCanvas"); // where mouse cursor renders
const ovCtx = ovCanvas.getContext("2d");
const tgtCanvas = document.getElementById("targetCanvas"); // where "box" gets drawn before dream happens
const tgtCtx = tgtCanvas.getContext("2d");
const maskPaintCanvas = document.getElementById("maskPaintCanvas"); // where masking brush gets painted
const maskPaintCtx = maskPaintCanvas.getContext("2d");
const tempCanvas = document.getElementById("tempCanvas"); // where select/rejects get superimposed temporarily
const tempCtx = tempCanvas.getContext("2d");
const imgCanvas = document.getElementById("canvas"); // where dreams go
const imgCtx = imgCanvas.getContext("2d");
const bgCanvas = document.getElementById("backgroundCanvas"); // gray bg grid
const bgCtx = bgCanvas.getContext("2d");
//
function startup() {
testHostConfiguration();
testHostConnection();
loadSettings();
const hostEl = document.getElementById("host");
hostEl.onchange = () => {
host = hostEl.value.endsWith("/")
? hostEl.value.substring(0, hostEl.value.length - 1)
: hostEl.value;
hostEl.value = host;
localStorage.setItem("host", host);
};
testHostConnection().then((checkConnection) => {
hostEl.onchange = () => {
host = hostEl.value.endsWith("/")
? hostEl.value.substring(0, hostEl.value.length - 1)
: hostEl.value;
hostEl.value = host;
localStorage.setItem("host", host);
checkConnection();
};
});
const promptEl = document.getElementById("prompt");
promptEl.oninput = () => {
@ -147,9 +115,6 @@ function startup() {
changeSeed();
changeOverMaskPx();
changeHiResFix();
document.getElementById("overlayCanvas").onmousemove = mouseMove;
document.getElementById("overlayCanvas").onmousedown = mouseDown;
document.getElementById("overlayCanvas").onmouseup = mouseUp;
document.getElementById("scaleFactor").value = scaleFactor;
}
@ -181,6 +146,10 @@ function testHostConfiguration() {
"Host seems to be invalid! Please fix your host here:",
current
);
else
host = current.endsWith("/")
? current.substring(0, current.length - 1)
: current;
} else {
requestHost(
"This seems to be the first time you are using openOutpaint! Please set your host here:"
@ -188,12 +157,8 @@ function testHostConfiguration() {
}
}
function testHostConnection() {
function CheckInProgressError(message = "") {
this.name = "CheckInProgressError";
this.message = message;
}
CheckInProgressError.prototype = Object.create(Error.prototype);
async function testHostConnection() {
class CheckInProgressError extends Error {}
const connectionIndicator = document.getElementById(
"connection-status-indicator"
@ -209,6 +174,7 @@ function testHostConnection() {
connectionIndicator.classList.remove(
"cors-issue",
"offline",
"before",
"server-error"
);
connectionIndicator.title = "Connected";
@ -216,7 +182,12 @@ function testHostConnection() {
},
error: () => {
connectionIndicator.classList.add("server-error");
connectionIndicator.classList.remove("online", "offline", "cors-issue");
connectionIndicator.classList.remove(
"online",
"offline",
"before",
"cors-issue"
);
connectionIndicator.title =
"Server is online, but is returning an error response";
connectionStatus = false;
@ -226,6 +197,7 @@ function testHostConnection() {
connectionIndicator.classList.remove(
"online",
"offline",
"before",
"server-error"
);
connectionIndicator.title =
@ -237,17 +209,31 @@ function testHostConnection() {
connectionIndicator.classList.remove(
"cors-issue",
"online",
"before",
"server-error"
);
connectionIndicator.title =
"Server seems to be offline. Please check the console for more information.";
connectionStatus = false;
},
before: () => {
connectionIndicator.classList.add("before");
connectionIndicator.classList.remove(
"cors-issue",
"online",
"offline",
"server-error"
);
connectionIndicator.title = "Waiting for check to complete.";
connectionStatus = false;
},
};
statuses[status] && statuses[status]();
};
setConnectionStatus("before");
let checkInProgress = false;
const checkConnection = async (notify = false) => {
@ -259,7 +245,10 @@ function testHostConnection() {
var url = document.getElementById("host").value + "/startup-events";
// Attempt normal request
try {
const response = await fetch(url);
/** @type {Response} */
const response = await fetch(url, {
signal: AbortSignal.timeout(5000),
});
if (response.status === 200) {
setConnectionStatus("online");
@ -278,6 +267,7 @@ function testHostConnection() {
}
} catch (e) {
try {
if (e instanceof DOMException) throw "offline";
// Tests if problem is CORS
await fetch(url, {mode: "no-cors"});
@ -301,7 +291,7 @@ function testHostConnection() {
return status;
};
checkConnection(true);
await checkConnection(true);
// On click, attempt to refresh
connectionIndicator.onclick = async () => {
@ -316,8 +306,8 @@ function testHostConnection() {
// Checks every 5 seconds if offline, 30 seconds if online
const checkAgain = () => {
setTimeout(
() => {
checkConnection();
async () => {
await checkConnection();
checkAgain();
},
connectionStatus ? 30000 : 5000
@ -325,133 +315,15 @@ function testHostConnection() {
};
checkAgain();
}
function dream(
x,
y,
prompt,
extra = {
method: endpoint,
stopMarching: () => {},
bb: {x, y, w: prompt.width, h: prompt.height},
}
) {
tmpImgXYWH.x = x;
tmpImgXYWH.y = y;
tmpImgXYWH.w = prompt.width;
tmpImgXYWH.h = prompt.height;
console.log(
"dreaming to " +
host +
url +
(extra.method || endpoint) +
":\r\n" +
JSON.stringify(prompt)
);
console.info(`dreaming "${prompt.prompt}"`);
console.debug(prompt);
// Start checking for progress
const progressCheck = checkProgress(extra.bb);
postData(prompt, extra)
.then((data) => {
returnedImages = data.images;
totalImagesReturned = data.images.length;
blockNewImages = true;
//console.log(data); // JSON data parsed by `data.json()` call
imageAcceptReject(x, y, data, extra);
})
.finally(() => clearInterval(progressCheck));
}
async function postData(promptData, extra = null) {
this.host = document.getElementById("host").value;
// Default options are marked with *
const response = await fetch(
this.host + this.url + extra.method || endpoint,
{
method: "POST", // *GET, POST, PUT, DELETE, etc.
mode: "cors", // no-cors, *cors, same-origin
cache: "default", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
redirect: "follow", // manual, *follow, error
referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
body: JSON.stringify(promptData), // body data type must match "Content-Type" header
}
);
return response.json(); // parses JSON response into native JavaScript objects
}
function imageAcceptReject(x, y, data, extra = null) {
inProgress = false;
document.getElementById("progressDiv").remove();
const img = new Image();
img.onload = function () {
backupAndClearMask(x, y, img.width, img.height);
tempCtx.drawImage(img, x, y); //imgCtx for actual image, tmp for... holding?
var div = document.createElement("div");
div.id = "veryTempDiv";
div.style.position = "absolute";
div.style.left = parseInt(x) < 0 ? 0 + "px" : parseInt(x) + "px";
div.style.top = parseInt(y + data.parameters.height) + "px";
div.style.width = "200px";
div.style.height = "70px";
div.innerHTML =
'<button onclick="prevImg(this)">&lt;</button><button onclick="nextImg(this)">&gt;</button><span class="strokeText" id="currentImgIndex"></span><span class="strokeText"> of </span><span class="strokeText" id="totalImgIndex"></span><button onclick="accept(this)">Y</button><button onclick="reject(this)">N</button><button onclick="resource(this)">RES</button><span class="strokeText" id="estRemaining"></span>';
document.getElementById("tempDiv").appendChild(div);
document.getElementById("currentImgIndex").innerText = "1";
document.getElementById("totalImgIndex").innerText = totalImagesReturned;
return () => {
checkConnection().catch(() => {});
};
// set the image displayed as the first regardless of batch size/count
imageIndex = 0;
// load the image data after defining the closure
img.src = "data:image/png;base64," + returnedImages[imageIndex];
}
function accept(evt) {
// write image to imgcanvas
stopMarching && stopMarching();
stopMarching = null;
clearBackupMask();
placeImage();
removeChoiceButtons();
clearTargetMask();
blockNewImages = false;
}
function reject(evt) {
// remove image entirely
stopMarching && stopMarching();
stopMarching = null;
restoreBackupMask();
clearBackupMask();
clearTargetMask();
removeChoiceButtons();
blockNewImages = false;
}
function resource(evt) {
// send image to resources
const img = new Image();
// load the image data after defining the closure
img.src = "data:image/png;base64," + returnedImages[imageIndex];
tools.stamp.state.addResource(
prompt("Enter new resource name", "Dream Resource"),
img
);
}
function newImage(evt) {
clearPaintedMask();
clearBackupMask();
clearTargetMask();
commands.runCommand("eraseImage", "Clear Canvas", {
x: 0,
y: 0,
@ -532,10 +404,6 @@ function clearBackupMask() {
backupMaskY = null;
}
function clearTargetMask() {
tgtCtx.clearRect(0, 0, tgtCanvas.width, tgtCanvas.height);
}
function clearImgMask() {
imgCtx.clearRect(0, 0, imgCanvas.width, imgCanvas.height);
}
@ -565,122 +433,37 @@ function sleep(ms) {
}
function march(bb) {
const expanded = {...bb};
expanded.x--;
expanded.y--;
expanded.w += 2;
expanded.h += 2;
// Get temporary layer to draw marching ants
const layer = imageCollection.registerLayer(null, {
bb: expanded,
});
layer.canvas.style.imageRendering = "pixelated";
let offset = 0;
const interval = setInterval(() => {
drawMarchingAnts(bb, offset++);
offset %= 16;
drawMarchingAnts(layer.ctx, bb, offset++);
offset %= 12;
}, 20);
return () => clearInterval(interval);
return () => {
clearInterval(interval);
imageCollection.deleteLayer(layer);
};
}
function drawMarchingAnts(bb, offset) {
clearTargetMask();
tgtCtx.strokeStyle = "#FFFFFFFF"; //"#55000077";
tgtCtx.setLineDash([4, 2]);
tgtCtx.lineDashOffset = -offset;
tgtCtx.strokeRect(bb.x, bb.y, bb.w, bb.h);
}
function checkProgress(bb) {
document.getElementById("progressDiv") &&
document.getElementById("progressDiv").remove();
// Skip image to stop using a ton of networking resources
endpoint = "progress?skip_current_image=true";
var div = document.createElement("div");
div.id = "progressDiv";
div.style.position = "absolute";
div.style.width = "200px";
div.style.height = "70px";
div.style.left = parseInt(bb.x + bb.w - 100) + "px";
div.style.top = parseInt(bb.y + bb.h) + "px";
div.innerHTML = '<span class="strokeText" id="estRemaining"></span>';
document.getElementById("tempDiv").appendChild(div);
return setInterval(() => {
fetch(host + url + endpoint)
.then((response) => response.json())
.then((data) => {
var estimate =
Math.round(data.progress * 100) +
"% :: " +
Math.floor(data.eta_relative) +
" sec.";
document.getElementById("estRemaining").innerText = estimate;
});
}, 1000);
}
function mouseMove(evt) {
const rect = ovCanvas.getBoundingClientRect(); // not-quite pixel offset was driving me insane
const canvasOffsetX = rect.left;
const canvasOffsetY = rect.top;
heldButton = evt.buttons;
mouseXInfo.innerText = mouseX = evt.clientX;
mouseYInfo.innerText = mouseY = evt.clientY;
canvasXInfo.innerText = canvasX = parseInt(evt.clientX - rect.left);
canvasYInfo.innerText = canvasY = parseInt(evt.clientY - rect.top);
snapXInfo.innerText = canvasX + snap(canvasX);
snapYInfo.innerText = canvasY + snap(canvasY);
heldButtonInfo.innerText = heldButton;
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); // clear out the previous mouse cursor
if (placingArbitraryImage) {
// ugh refactor so this isn't duplicated between arbitrary image and dream reticle modes
snapOffsetX = 0;
snapOffsetY = 0;
if (snapToGrid) {
snapOffsetX = snap(canvasX, false);
snapOffsetY = snap(canvasY, false);
}
finalX = snapOffsetX + canvasX;
finalY = snapOffsetY + canvasY;
ovCtx.drawImage(arbitraryImage, finalX, finalY);
}
}
function mouseDown(evt) {
const rect = ovCanvas.getBoundingClientRect();
var oddOffset = 0;
if (scaleFactor % 2 != 0) {
oddOffset = basePixelCount / 2;
}
if (evt.button == 0) {
// left click
if (placingArbitraryImage) {
var nextBox = {};
nextBox.x = evt.offsetX;
nextBox.y = evt.offsetY;
nextBox.w = arbitraryImageData.width;
nextBox.h = arbitraryImageData.height;
dropTargets.push(nextBox);
}
}
}
function mouseUp(evt) {
if (evt.button == 0) {
// left click
if (placingArbitraryImage) {
// jeez i REALLY need to refactor tons of this to not be duplicated all over, that's definitely my next chore after figuring out that razza frazza overmask fade
var target = dropTargets[dropTargets.length - 1]; //get the last one... why am i storing all of them?
snapOffsetX = 0;
snapOffsetY = 0;
if (snapToGrid) {
snapOffsetX = snap(target.x, false);
snapOffsetY = snap(target.y, false);
}
finalX = snapOffsetX + target.x;
finalY = snapOffsetY + target.y;
drawThis.x = finalX;
drawThis.y = finalY;
drawThis.w = target.w;
drawThis.h = target.h;
drawIt = drawThis; // i still think this is really stupid and redundant and unnecessary and redundant
drop(drawIt);
}
}
function drawMarchingAnts(ctx, bb, offset) {
ctx.clearRect(0, 0, bb.w + 2, bb.h + 2);
ctx.strokeStyle = "#FFFFFFFF"; //"#55000077";
ctx.strokeWidth = "2px";
ctx.setLineDash([4, 2]);
ctx.lineDashOffset = -offset;
ctx.strokeRect(1, 1, bb.w, bb.h);
}
function changeSampler() {
@ -799,10 +582,11 @@ function drawBackground() {
// Checkerboard
let darkTileColor = "#333";
let lightTileColor = "#555";
for (var x = 0; x < bgCanvas.width; x += 64) {
for (var y = 0; y < bgCanvas.height; y += 64) {
bgCtx.fillStyle = (x + y) % 128 === 0 ? lightTileColor : darkTileColor;
bgCtx.fillRect(x, y, 64, 64);
for (var x = 0; x < bgLayer.canvas.width; x += 64) {
for (var y = 0; y < bgLayer.canvas.height; y += 64) {
bgLayer.ctx.fillStyle =
(x + y) % 128 === 0 ? lightTileColor : darkTileColor;
bgLayer.ctx.fillRect(x, y, 64, 64);
}
}
}
@ -1081,6 +865,18 @@ function loadSettings() {
// document.getElementById("overMaskPx").value = Number(_overmask_px);
}
document.getElementById("mainHSplit").addEventListener("wheel", (evn) => {
evn.preventDefault();
});
imageCollection.element.addEventListener(
"wheel",
(evn) => {
evn.preventDefault();
},
{passive: false}
);
imageCollection.element.addEventListener(
"contextmenu",
(evn) => {
evn.preventDefault();
},
{passive: false}
);

View file

@ -0,0 +1,29 @@
// info div, sometimes hidden
let mouseXInfo = document.getElementById("mouseX");
let mouseYInfo = document.getElementById("mouseY");
let canvasXInfo = document.getElementById("canvasX");
let canvasYInfo = document.getElementById("canvasY");
let snapXInfo = document.getElementById("snapX");
let snapYInfo = document.getElementById("snapY");
let heldButtonInfo = document.getElementById("heldButton");
mouse.listen.window.onmousemove.on((evn) => {
mouseXInfo.textContent = evn.x;
mouseYInfo.textContent = evn.y;
});
mouse.listen.world.onmousemove.on((evn) => {
canvasXInfo.textContent = evn.x;
canvasYInfo.textContent = evn.y;
snapXInfo.textContent = evn.x + snap(evn.x);
snapYInfo.textContent = evn.y + snap(evn.y);
});
/**
* Toggles the debug layer (Just run toggledebug() in the console)
*/
const toggledebug = () => {
const hidden = debugCanvas.style.display === "none";
if (hidden) debugLayer.unhide();
else debugLayer.hide();
};

View file

@ -0,0 +1,189 @@
// Layering
const imageCollection = layers.registerCollection(
"image",
{w: 2560, h: 1536},
{
name: "Image Layers",
}
);
const bgLayer = imageCollection.registerLayer("bg", {
name: "Background",
});
const imgLayer = imageCollection.registerLayer("image", {
name: "Image",
});
const maskPaintLayer = imageCollection.registerLayer("mask", {
name: "Mask Paint",
});
const ovLayer = imageCollection.registerLayer("overlay", {
name: "Overlay",
});
const debugLayer = imageCollection.registerLayer("debug", {
name: "Debug Layer",
});
const imgCanvas = imgLayer.canvas; // where dreams go
const imgCtx = imgLayer.ctx;
const maskPaintCanvas = maskPaintLayer.canvas; // where mouse cursor renders
const maskPaintCtx = maskPaintLayer.ctx;
maskPaintCanvas.classList.add("mask-canvas");
const ovCanvas = ovLayer.canvas; // where mouse cursor renders
const ovCtx = ovLayer.ctx;
const debugCanvas = debugLayer.canvas; // where mouse cursor renders
const debugCtx = debugLayer.ctx;
debugLayer.hide(); // Hidden by default
layers.registerCollection("mask", {name: "Mask Layers", requiresActive: true});
// Where CSS and javascript magic happens to make the canvas viewport work
/**
* Ended up using a CSS transforms approach due to more flexibility on transformations
* and capability to automagically translate input coordinates to layer space.
*/
mouse.registerContext(
"world",
(evn, ctx) => {
// Fix because in chrome layerX and layerY simply doesnt work
/** @type {HTMLDivElement} */
const target = evn.target;
// Get element bounding rect
const bb = target.getBoundingClientRect();
// Get element width/height (css, cause I don't trust client sizes in chrome anymore)
const w = imageCollection.size.w;
const h = imageCollection.size.h;
// Get cursor position
const x = evn.clientX;
const y = evn.clientY;
// Map to layer space
const layerX = ((x - bb.left) / bb.width) * w;
const layerY = ((y - bb.top) / bb.height) * h;
//
ctx.coords.prev.x = ctx.coords.pos.x;
ctx.coords.prev.y = ctx.coords.pos.y;
ctx.coords.pos.x = layerX;
ctx.coords.pos.y = layerY;
},
{target: imageCollection.inputElement}
);
/**
* The global viewport object (may be modularized in the future). All
* coordinates given are of the center of the viewport
*
* cx and cy are the viewport's world coordinates, scaled to zoom level.
* _x and _y are actual coordinates in the DOM space
*
* The transform() function does some transforms and writes them to the
* provided element.
*/
const viewport = {
get cx() {
return this._x * this.zoom;
},
set cx(v) {
return (this._x = v / this.zoom);
},
_x: 0,
get cy() {
return this._y * this.zoom;
},
set cy(v) {
return (this._y = v / this.zoom);
},
_y: 0,
zoom: 1,
rotation: 0,
get w() {
return (window.innerWidth * 1) / this.zoom;
},
get h() {
return (window.innerHeight * 1) / this.zoom;
},
/**
* Apply transformation
*
* @param {HTMLElement} el Element to apply CSS transform to
*/
transform(el) {
el.style.transformOrigin = `${this.cx}px ${this.cy}px`;
el.style.transform = `scale(${this.zoom}) translate(${-(
this._x -
this.w / 2
)}px, ${-(this._y - this.h / 2)}px)`;
},
};
viewport.cx = imageCollection.size.w / 2;
viewport.cy = imageCollection.size.h / 2;
let worldInit = null;
viewport.transform(imageCollection.element);
mouse.listen.window.onwheel.on((evn) => {
if (evn.evn.ctrlKey) {
evn.evn.preventDefault();
const pcx = viewport.cx;
const pcy = viewport.cy;
if (evn.delta < 0) {
viewport.zoom *= 1 + Math.abs(evn.delta * 0.0002);
} else {
viewport.zoom *= 1 - Math.abs(evn.delta * 0.0002);
}
viewport.cx = pcx;
viewport.cy = pcy;
viewport.transform(imageCollection.element);
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
debugCtx.fillStyle = "#F0F";
debugCtx.beginPath();
debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2);
debugCtx.fill();
}
});
mouse.listen.window.btn.middle.onpaintstart.on((evn) => {
worldInit = {x: viewport.cx, y: viewport.cy};
});
mouse.listen.window.btn.middle.onpaint.on((evn) => {
if (worldInit) {
viewport.cx = worldInit.x + (evn.ix - evn.x) / viewport.zoom;
viewport.cy = worldInit.y + (evn.iy - evn.y) / viewport.zoom;
// Limits
viewport.cx = Math.max(Math.min(viewport.cx, imageCollection.size.w), 0);
viewport.cy = Math.max(Math.min(viewport.cy, imageCollection.size.h), 0);
// Draw Viewport location
}
viewport.transform(imageCollection.element);
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
debugCtx.fillStyle = "#F0F";
debugCtx.beginPath();
debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2);
debugCtx.fill();
});
mouse.listen.window.btn.middle.onpaintend.on((evn) => {
worldInit = null;
});
window.addEventListener("resize", () => {
viewport.transform(imageCollection.element);
});

View file

@ -0,0 +1,27 @@
const tools = {};
/**
* Dream tool
*/
tools.dream = dreamTool();
tools.img2img = img2imgTool();
/**
* Mask Editing tools
*/
toolbar.addSeparator();
/**
* Mask Brush tool
*/
tools.maskbrush = maskBrushTool();
/**
* Image Editing tools
*/
toolbar.addSeparator();
tools.selecttransform = selectTransformTool();
tools.stamp = stampTool();
toolbar.tools[0].enable();

View file

@ -44,7 +44,7 @@
* @typedef MouseListenerContext
* @property {Observer} onmousemove A mouse move handler
* @property {Observer} onwheel A mouse wheel handler
* @property {MouseListenerBtnContext} btn Button handlers
* @property {Record<string, MouseListenerBtnContext>} btn Button handlers
*/
/**
@ -65,7 +65,10 @@
* @property {string} id A unique identifier
* @property {string} name The key name
* @property {ContextMoveTransformer} onmove The coordinate transform callback
* @property {(evn) => void} onany A function to be run on any event
* @property {?HTMLElement} target The target
* @property {MouseCoordContext} coords Coordinates object
* @property {MouseListenerContext} listen Listeners object
*/
/**

View file

@ -64,6 +64,7 @@ const mouse = {
* @param {object} options Extra options
* @param {HTMLElement} [options.target=null] Target filtering
* @param {Record<number, string>} [options.buttons={0: "left", 1: "middle", 2: "right"}] Custom button mapping
* @param {(evn) => void} [options.genericcb=null] Function that will be run for all events (useful for preventDefault)
* @returns {MouseContext}
*/
registerContext: (name, onmove, options = {}) => {
@ -71,6 +72,7 @@ const mouse = {
defaultOpt(options, {
target: null,
buttons: {0: "left", 1: "middle", 2: "right"},
genericcb: null,
});
// Context information
@ -79,6 +81,7 @@ const mouse = {
id: guid(),
name,
onmove,
onany: options.genericcb,
target: options.target,
buttons: options.buttons,
};
@ -128,101 +131,69 @@ const mouse = {
const _double_click_timeout = {};
const _drag_start_timeout = {};
window.onmousedown = (evn) => {
const time = performance.now();
window.addEventListener(
"mousedown",
(evn) => {
const time = performance.now();
if (_double_click_timeout[evn.button]) {
// ondclick event
mouse._contexts.forEach(({target, name, buttons}) => {
if ((!target || target === evn.target) && buttons[evn.button])
mouse.listen[name].btn[buttons[evn.button]].ondclick.emit({
target: evn.target,
buttonId: evn.button,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: time,
});
});
} else {
// Start timer
_double_click_timeout[evn.button] = setTimeout(
() => delete _double_click_timeout[evn.button],
inputConfig.dClickTiming
);
}
// Set drag start timeout
_drag_start_timeout[evn.button] = setTimeout(() => {
mouse._contexts.forEach(({target, name, buttons}) => {
const key = buttons[evn.button];
if (
(!target || target === evn.target) &&
!mouse.coords[name].dragging[key].drag &&
key
) {
mouse.listen[name].btn[key].ondragstart.emit({
target: evn.target,
buttonId: evn.button,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: time,
});
mouse.coords[name].dragging[key].drag = true;
}
});
delete _drag_start_timeout[evn.button];
}, inputConfig.clickTiming);
mouse.buttons[evn.button] = time;
mouse._contexts.forEach(({target, name, buttons}) => {
const key = buttons[evn.button];
if ((!target || target === evn.target) && key) {
mouse.coords[name].dragging[key] = {};
mouse.coords[name].dragging[key].target = evn.target;
Object.assign(mouse.coords[name].dragging[key], mouse.coords[name].pos);
// onpaintstart event
mouse.listen[name].btn[key].onpaintstart.emit({
target: evn.target,
buttonId: evn.button,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: performance.now(),
if (_double_click_timeout[evn.button]) {
// ondclick event
mouse._contexts.forEach(({target, name, buttons}) => {
if ((!target || target === evn.target) && buttons[evn.button])
mouse.listen[name].btn[buttons[evn.button]].ondclick.emit({
target: evn.target,
buttonId: evn.button,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: time,
});
});
} else {
// Start timer
_double_click_timeout[evn.button] = setTimeout(
() => delete _double_click_timeout[evn.button],
inputConfig.dClickTiming
);
}
});
};
window.onmouseup = (evn) => {
const time = performance.now();
// Set drag start timeout
_drag_start_timeout[evn.button] = setTimeout(() => {
mouse._contexts.forEach(({target, name, buttons}) => {
const key = buttons[evn.button];
if (
(!target || target === evn.target) &&
!mouse.coords[name].dragging[key].drag &&
key
) {
mouse.listen[name].btn[key].ondragstart.emit({
target: evn.target,
buttonId: evn.button,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: time,
});
mouse._contexts.forEach(({target, name, buttons}) => {
const key = buttons[evn.button];
if (
(!target || target === evn.target) &&
key &&
mouse.coords[name].dragging[key]
) {
const start = {
x: mouse.coords[name].dragging[key].x,
y: mouse.coords[name].dragging[key].y,
};
mouse.coords[name].dragging[key].drag = true;
}
});
delete _drag_start_timeout[evn.button];
}, inputConfig.clickTiming);
// onclick event
const dx = mouse.coords[name].pos.x - start.x;
const dy = mouse.coords[name].pos.y - start.y;
mouse.buttons[evn.button] = time;
if (
mouse.buttons[evn.button] &&
time - mouse.buttons[evn.button] < inputConfig.clickTiming &&
dx * dx + dy * dy < inputConfig.clickRadius * inputConfig.clickRadius
)
mouse.listen[name].btn[key].onclick.emit({
mouse._contexts.forEach(({target, name, buttons, onany}) => {
const key = buttons[evn.button];
if ((!target || target === evn.target) && key) {
onany && onany();
mouse.coords[name].dragging[key] = {};
mouse.coords[name].dragging[key].target = evn.target;
Object.assign(mouse.coords[name].dragging[key], mouse.coords[name].pos);
// onpaintstart event
mouse.listen[name].btn[key].onpaintstart.emit({
target: evn.target,
buttonId: evn.button,
x: mouse.coords[name].pos.x,
@ -230,23 +201,52 @@ window.onmouseup = (evn) => {
evn,
timestamp: performance.now(),
});
}
});
},
{
passive: false,
}
);
// onpaintend event
mouse.listen[name].btn[key].onpaintend.emit({
target: evn.target,
initialTarget: mouse.coords[name].dragging[key].target,
buttonId: evn.button,
ix: mouse.coords[name].dragging[key].x,
iy: mouse.coords[name].dragging[key].y,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: performance.now(),
});
window.addEventListener(
"mouseup",
(evn) => {
const time = performance.now();
// ondragend event
if (mouse.coords[name].dragging[key].drag)
mouse.listen[name].btn[key].ondragend.emit({
mouse._contexts.forEach(({target, name, buttons, onany}) => {
const key = buttons[evn.button];
if (
(!target || target === evn.target) &&
key &&
mouse.coords[name].dragging[key]
) {
onany && onany();
const start = {
x: mouse.coords[name].dragging[key].x,
y: mouse.coords[name].dragging[key].y,
};
// onclick event
const dx = mouse.coords[name].pos.x - start.x;
const dy = mouse.coords[name].pos.y - start.y;
if (
mouse.buttons[evn.button] &&
time - mouse.buttons[evn.button] < inputConfig.clickTiming &&
dx * dx + dy * dy < inputConfig.clickRadius * inputConfig.clickRadius
)
mouse.listen[name].btn[key].onclick.emit({
target: evn.target,
buttonId: evn.button,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: performance.now(),
});
// onpaintend event
mouse.listen[name].btn[key].onpaintend.emit({
target: evn.target,
initialTarget: mouse.coords[name].dragging[key].target,
buttonId: evn.button,
@ -258,102 +258,122 @@ window.onmouseup = (evn) => {
timestamp: performance.now(),
});
mouse.coords[name].dragging[key] = null;
// ondragend event
if (mouse.coords[name].dragging[key].drag)
mouse.listen[name].btn[key].ondragend.emit({
target: evn.target,
initialTarget: mouse.coords[name].dragging[key].target,
buttonId: evn.button,
ix: mouse.coords[name].dragging[key].x,
iy: mouse.coords[name].dragging[key].y,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: performance.now(),
});
mouse.coords[name].dragging[key] = null;
}
});
if (_drag_start_timeout[evn.button] !== undefined) {
clearTimeout(_drag_start_timeout[evn.button]);
delete _drag_start_timeout[evn.button];
}
});
mouse.buttons[evn.button] = null;
},
{passive: false}
);
if (_drag_start_timeout[evn.button] !== undefined) {
clearTimeout(_drag_start_timeout[evn.button]);
delete _drag_start_timeout[evn.button];
}
mouse.buttons[evn.button] = null;
};
window.addEventListener(
"mousemove",
(evn) => {
mouse._contexts.forEach((context) => {
const target = context.target;
const name = context.name;
window.onmousemove = (evn) => {
mouse._contexts.forEach((context) => {
const target = context.target;
const name = context.name;
if (!target || target === evn.target) {
context.onmove(evn, context);
if (!target || target === evn.target) {
context.onmove(evn, context);
mouse.listen[name].onmousemove.emit({
target: evn.target,
px: mouse.coords[name].prev.x,
py: mouse.coords[name].prev.y,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: performance.now(),
});
mouse.listen[name].onmousemove.emit({
target: evn.target,
px: mouse.coords[name].prev.x,
py: mouse.coords[name].prev.y,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: performance.now(),
});
Object.keys(context.buttons).forEach((index) => {
const key = context.buttons[index];
// ondragstart event (2)
if (mouse.coords[name].dragging[key]) {
const dx =
mouse.coords[name].pos.x - mouse.coords[name].dragging[key].x;
const dy =
mouse.coords[name].pos.y - mouse.coords[name].dragging[key].y;
if (
!mouse.coords[name].dragging[key].drag &&
dx * dx + dy * dy >=
inputConfig.clickRadius * inputConfig.clickRadius
) {
mouse.listen[name].btn[key].ondragstart.emit({
target: evn.target,
buttonId: evn.button,
ix: mouse.coords[name].dragging[key].x,
iy: mouse.coords[name].dragging[key].y,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: performance.now(),
});
Object.keys(context.buttons).forEach((index) => {
const key = context.buttons[index];
// ondragstart event (2)
if (mouse.coords[name].dragging[key]) {
const dx =
mouse.coords[name].pos.x - mouse.coords[name].dragging[key].x;
const dy =
mouse.coords[name].pos.y - mouse.coords[name].dragging[key].y;
mouse.coords[name].dragging[key].drag = true;
}
}
// ondrag event
if (
!mouse.coords[name].dragging[key].drag &&
dx * dx + dy * dy >=
inputConfig.clickRadius * inputConfig.clickRadius
) {
mouse.listen[name].btn[key].ondragstart.emit({
mouse.coords[name].dragging[key] &&
mouse.coords[name].dragging[key].drag
)
mouse.listen[name].btn[key].ondrag.emit({
target: evn.target,
buttonId: evn.button,
initialTarget: mouse.coords[name].dragging[key].target,
button: index,
ix: mouse.coords[name].dragging[key].x,
iy: mouse.coords[name].dragging[key].y,
px: mouse.coords[name].prev.x,
py: mouse.coords[name].prev.y,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: performance.now(),
});
mouse.coords[name].dragging[key].drag = true;
// onpaint event
if (mouse.coords[name].dragging[key]) {
mouse.listen[name].btn[key].onpaint.emit({
target: evn.target,
initialTarget: mouse.coords[name].dragging[key].target,
button: index,
ix: mouse.coords[name].dragging[key].x,
iy: mouse.coords[name].dragging[key].y,
px: mouse.coords[name].prev.x,
py: mouse.coords[name].prev.y,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: performance.now(),
});
}
}
// ondrag event
if (
mouse.coords[name].dragging[key] &&
mouse.coords[name].dragging[key].drag
)
mouse.listen[name].btn[key].ondrag.emit({
target: evn.target,
initialTarget: mouse.coords[name].dragging[key].target,
button: index,
ix: mouse.coords[name].dragging[key].x,
iy: mouse.coords[name].dragging[key].y,
px: mouse.coords[name].prev.x,
py: mouse.coords[name].prev.y,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: performance.now(),
});
// onpaint event
if (mouse.coords[name].dragging[key]) {
mouse.listen[name].btn[key].onpaint.emit({
target: evn.target,
initialTarget: mouse.coords[name].dragging[key].target,
button: index,
ix: mouse.coords[name].dragging[key].x,
iy: mouse.coords[name].dragging[key].y,
px: mouse.coords[name].prev.x,
py: mouse.coords[name].prev.y,
x: mouse.coords[name].pos.x,
y: mouse.coords[name].pos.y,
evn,
timestamp: performance.now(),
});
}
});
}
});
};
});
}
});
},
{passive: false}
);
window.addEventListener(
"wheel",
@ -382,17 +402,6 @@ mouse.registerContext("window", (evn, ctx) => {
ctx.coords.pos.x = evn.clientX;
ctx.coords.pos.y = evn.clientY;
});
mouse.registerContext(
"canvas",
(evn, ctx) => {
ctx.coords.prev.x = ctx.coords.pos.x;
ctx.coords.prev.y = ctx.coords.pos.y;
ctx.coords.pos.x = evn.layerX;
ctx.coords.pos.y = evn.layerY;
},
document.getElementById("overlayCanvas")
);
/**
* Keyboard input processing
*/

264
js/lib/layers.js Normal file
View file

@ -0,0 +1,264 @@
/**
* This is a manager for the many canvas and content layers that compose the application
*
* It manages canvases and their locations and sizes according to current viewport views
*/
const layers = {
_collections: [],
collections: makeWriteOnce({}, "layers.collections"),
listen: {
oncollectioncreate: new Observer(),
oncollectiondelete: new Observer(),
onlayercreate: new Observer(),
onlayerdelete: new Observer(),
},
// Registers a new collection
// Layer collections are a group of layers (canvases) that are rendered in tandem. (same width, height, position, transform, etc)
registerCollection: (key, size, options = {}) => {
defaultOpt(options, {
// Display name for the collection
name: key,
// Initial layer
initLayer: {
key: "default",
options: {},
},
// Target
targetElement: document.getElementById("layer-render"),
// Resolution of the image
resolution: size,
});
// Path used for logging purposes
const _logpath = "layers.collections." + key;
// Collection ID
const id = guid();
// Collection element
const element = document.createElement("div");
element.id = `collection-${id}`;
element.style.width = `${size.w}px`;
element.style.height = `${size.h}px`;
element.classList.add("collection");
// Input element (overlay element for input handling)
const inputel = document.createElement("div");
inputel.id = `collection-input-${id}`;
inputel.style.width = `${size.w}px`;
inputel.style.height = `${size.h}px`;
inputel.addEventListener("mouseover", (evn) => {
document.activeElement.blur();
});
inputel.classList.add("collection-input-overlay");
element.appendChild(inputel);
options.targetElement.appendChild(element);
const collection = makeWriteOnce(
{
id,
_logpath,
_layers: [],
layers: {},
name: options.name,
element,
inputElement: inputel,
size,
resolution: options.resolution,
active: null,
/**
* Registers a new layer
*
* @param {string | null} key Name and key to use to access layer. If null, it is a temporary layer.
* @param {object} options
* @param {string} options.name
* @param {?BoundingBox} options.bb
* @param {{w: number, h: number}} options.resolution
* @param {object} options.after
* @returns
*/
registerLayer: (key = null, options = {}) => {
// Make ID
const id = guid();
defaultOpt(options, {
// Display name for the layer
name: key || `Temporary ${id}`,
// Bounding box for layer
bb: {x: 0, y: 0, w: collection.size.w, h: collection.size.h},
// Bounding box for layer
resolution: null,
// If set, will insert the layer after the given one
after: null,
});
// Calculate resolution
if (!options.resolution)
options.resolution = {
w: (collection.resolution.w / collection.size.w) * options.bb.w,
h: (collection.resolution.h / collection.size.h) * options.bb.h,
};
// This layer's canvas
// This is where black magic will take place in the future
/**
* @todo Use the canvas black arts to auto-scale canvas
*/
const canvas = document.createElement("canvas");
canvas.id = `layer-${id}`;
canvas.style.left = `${options.bb.x}px`;
canvas.style.top = `${options.bb.y}px`;
canvas.style.width = `${options.bb.w}px`;
canvas.style.height = `${options.bb.h}px`;
canvas.width = options.resolution.w;
canvas.height = options.resolution.h;
if (!options.after) collection.element.appendChild(canvas);
else {
options.after.canvas.after(canvas);
}
const ctx = canvas.getContext("2d");
// Path used for logging purposes
const _layerlogpath = key
? _logpath + ".layers." + key
: _logpath + ".layers[" + id + "]";
const layer = makeWriteOnce(
{
_logpath: _layerlogpath,
_collection: collection,
id,
key,
name: options.name,
state: new Proxy(
{visible: true},
{
set(obj, opt, val) {
switch (opt) {
case "visible":
layer.canvas.style.display = val ? "block" : "none";
break;
}
obj[opt] = val;
},
}
),
/** Our canvas */
canvas,
ctx,
/**
* Moves this layer to another location
*
* @param {number} x X coordinate of the top left of the canvas
* @param {number} y X coordinate of the top left of the canvas
*/
moveTo(x, y) {
canvas.style.left = `${x}px`;
canvas.style.top = `${y}px`;
},
// Hides this layer (don't draw)
hide() {
this.canvas.style.display = "none";
},
// Hides this layer (don't draw)
unhide() {
this.canvas.style.display = "block";
},
// Activates this layer
activate() {
collection.active = this;
},
},
_layerlogpath,
["active"]
);
// Add to indexers
if (!options.after) collection._layers.push(layer);
else {
const index = collection._layers.findIndex(
(l) => l === options.after
);
collection._layers.splice(index, 0, layer);
}
if (key) collection.layers[key] = layer;
if (key === null)
console.debug(
`[layers] Anonymous layer '${layer.name}' registered`
);
else
console.info(
`[layers] Layer '${layer.name}' at ${layer._logpath} registered`
);
layers.listen.onlayercreate.emit({
layer,
});
return layer;
},
// Deletes a layer
deleteLayer: (layer) => {
const lobj = collection._layers.splice(
collection._layers.findIndex(
(l) => l.id === layer || l.id === layer.id
),
1
)[0];
if (!lobj) return;
layers.listen.onlayerdelete.emit({
layer: lobj,
});
if (lobj.key) delete collection.layers[lobj.key];
collection.element.removeChild(lobj.canvas);
if (lobj.key) console.info(`[layers] Layer '${lobj.key}' deleted`);
else console.debug(`[layers] Anonymous layer '${lobj.id}' deleted`);
},
},
_logpath,
["active"]
);
layers._collections.push(collection);
layers.collections[key] = collection;
console.info(
`[layers] Collection '${options.name}' at ${_logpath} registered`
);
// We must create a layer to select
collection
.registerLayer(options.initLayer.key, options.initLayer.options)
.activate();
return collection;
},
};

View file

@ -177,46 +177,16 @@ const _toolbar_input = {
* Dream and img2img tools
*/
const _reticle_draw = (evn, snapToGrid = true) => {
if (evn.target.id === "overlayCanvas") {
const bb = getBoundingBox(
evn.x,
evn.y,
basePixelCount * scaleFactor,
basePixelCount * scaleFactor,
snapToGrid && basePixelCount
);
const bb = getBoundingBox(
evn.x,
evn.y,
basePixelCount * scaleFactor,
basePixelCount * scaleFactor,
snapToGrid && basePixelCount
);
// draw targeting square reticle thingy cursor
ovCtx.lineWidth = 1;
ovCtx.strokeStyle = "#FFF";
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); //origin is middle of the frame
}
// draw targeting square reticle thingy cursor
ovCtx.lineWidth = 1;
ovCtx.strokeStyle = "#FFF";
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); //origin is middle of the frame
};
const tools = {};
/**
* Dream tool
*/
tools.dream = dreamTool();
tools.img2img = img2imgTool();
/**
* Mask Editing tools
*/
toolbar.addSeparator();
/**
* Mask Brush tool
*/
tools.maskbrush = maskBrushTool();
/**
* Image Editing tools
*/
toolbar.addSeparator();
tools.selecttransform = selectTransformTool();
tools.stamp = stampTool();
toolbar.tools[0].enable();

View file

@ -1,10 +1,21 @@
function makeDraggable(element) {
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";
const margin = 10;
const fixPos = () => {
const dbb = element.getBoundingClientRect();
if (dbb.left < margin) element.style.left = margin + "px";
else if (dbb.right > window.innerWidth - margin)
element.style.left =
dbb.left + (window.innerWidth - margin - dbb.right) + "px";
if (dbb.top < margin) element.style.top = margin + "px";
else if (dbb.bottom > window.innerHeight - margin)
element.style.top =
dbb.top + (window.innerHeight - margin - dbb.bottom) + "px";
};
mouse.listen.window.btn.left.onpaintstart.on((evn) => {
if (
@ -20,14 +31,22 @@ function makeDraggable(element) {
mouse.listen.window.btn.left.onpaint.on((evn) => {
if (dragging) {
element.style.right = null;
element.style.bottom = null;
element.style.top = evn.y - offset.y + "px";
element.style.left = evn.x - offset.x + "px";
fixPos();
}
});
mouse.listen.window.btn.left.onpaintend.on((evn) => {
dragging = false;
});
window.addEventListener("resize", () => {
fixPos();
});
}
document.querySelectorAll(".floating-window").forEach((w) => {
@ -150,7 +169,7 @@ function createSlider(name, wrapper, options = {}) {
});
mouse.listen.window.btn.left.ondrag.on((evn) => {
if (evn.target === overEl) {
if (evn.initialTarget === overEl) {
setValue(
Math.max(
options.min,

3
js/ui/explore.js Normal file
View file

@ -0,0 +1,3 @@
/**
* This is a simple implementation of layer interaction
*/

23
js/ui/tool/dream.d.js Normal file
View file

@ -0,0 +1,23 @@
/**
* Stable Diffusion Request
*
* @typedef StableDiffusionRequest
* @property {string} prompt Stable Diffusion prompt
* @property {string} negative_prompt Stable Diffusion negative prompt
*/
/**
* Stable Diffusion Response
*
* @typedef StableDiffusionResponse
* @property {string[]} images Response images
*/
/**
* Stable Diffusion Progress Response
*
* @typedef StableDiffusionProgressResponse
* @property {number} progress Progress (from 0 to 1)
* @property {number} eta_relative Estimated finish time
* @property {?string} current_image Progress image
*/

View file

@ -1,5 +1,318 @@
const dream_generate_callback = (evn, state) => {
if (evn.target.id === "overlayCanvas" && !blockNewImages) {
let blockNewImages = false;
/**
* Starts progress monitoring bar
*
* @param {BoundingBox} bb Bouding Box to draw progress to
* @returns {() => void}
*/
const _monitorProgress = (bb) => {
const minDelay = 1000;
const apiURL = `${host}${url}progress?skip_current_image=true`;
const expanded = {...bb};
expanded.x--;
expanded.y--;
expanded.w += 2;
expanded.h += 2;
// Get temporary layer to draw progress bar
const layer = imageCollection.registerLayer(null, {
bb: expanded,
});
layer.canvas.style.opacity = "70%";
let running = true;
const _checkProgress = async () => {
const init = performance.now();
try {
const response = await fetch(apiURL);
/** @type {StableDiffusionProgressResponse} */
const data = await response.json();
// Draw Progress Bar
layer.ctx.fillStyle = "#5F5";
layer.ctx.fillRect(1, 1, bb.w * data.progress, 10);
// Draw Progress Text
layer.ctx.clearRect(0, 11, expanded.w, 40);
layer.ctx.fillStyle = "#FFF";
layer.ctx.fillRect(0, 15, 60, 25);
layer.ctx.fillRect(bb.w - 58, 15, 60, 25);
layer.ctx.font = "20px Open Sans";
layer.ctx.fillStyle = "#000";
layer.ctx.textAlign = "right";
layer.ctx.fillText(`${Math.round(data.progress * 100)}%`, 55, 35);
// Draw ETA Text
layer.ctx.fillText(`${Math.round(data.eta_relative)}s`, bb.w - 5, 35);
} finally {
}
const timeSpent = performance.now() - init;
setTimeout(() => {
if (running) _checkProgress();
}, Math.max(0, minDelay - timeSpent));
};
_checkProgress();
return () => {
imageCollection.deleteLayer(layer);
running = false;
};
};
/**
* Starts a dream
*
* @param {"txt2img" | "img2img"} endpoint Endpoint to send the request to
* @param {StableDiffusionRequest} request Stable diffusion request
* @returns {Promise<string[]>}
*/
const _dream = async (endpoint, request) => {
const apiURL = `${host}${url}${endpoint}`;
/** @type {StableDiffusionResponse} */
let data = null;
try {
const response = await fetch(apiURL, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
data = await response.json();
} finally {
}
return data.images;
};
/**
* Generate and pick an image for placement
*
* @param {"txt2img" | "img2img"} endpoint Endpoint to send the request to
* @param {StableDiffusionRequest} request Stable diffusion request
* @param {BoundingBox} bb Generated image placement location
* @returns {Promise<HTMLImageElement | null>}
*/
const _generate = async (endpoint, request, bb) => {
const requestCopy = {...request};
// Images to select through
let at = 0;
/** @type {Image[]} */
const images = [];
/** @type {HTMLDivElement} */
let imageSelectMenu = null;
// Layer for the images
const layer = imageCollection.registerLayer(null, {
after: maskPaintLayer,
});
const redraw = () => {
const image = new Image();
image.src = "data:image/png;base64," + images[at];
image.addEventListener("load", () => {
layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
if (images[at]) layer.ctx.drawImage(image, bb.x, bb.y);
});
};
const stopMarchingAnts = march(bb);
// First Dream Run
let stopProgress = _monitorProgress(bb);
images.push(...(await _dream(endpoint, requestCopy)));
stopProgress();
// Image navigation
const prevImg = () => {
at--;
if (at < 0) at = images.length - 1;
imageindextxt.textContent = `${at + 1}/${images.length}`;
redraw();
};
const nextImg = () => {
at++;
if (at >= images.length) at = 0;
imageindextxt.textContent = `${at + 1}/${images.length}`;
redraw();
};
const applyImg = async () => {
const img = new Image();
// load the image data after defining the closure
img.src = "data:image/png;base64," + images[at];
img.addEventListener("load", () => {
commands.runCommand("drawImage", "Image Dream", {
x: bb.x,
y: bb.y,
image: img,
});
clean(true);
});
};
const makeMore = async () => {
let stopProgress = _monitorProgress(bb);
images.push(...(await _dream(endpoint, requestCopy)));
stopProgress();
imageindextxt.textContent = `${at + 1}/${images.length}`;
};
const discardImg = async () => {
clean();
};
// Listen for keyboard arrows
const onarrow = (evn) => {
switch (evn.target.tagName.toLowerCase()) {
case "input":
case "textarea":
case "select":
case "button":
return; // If in an input field, do not process arrow input
default:
// Do nothing
break;
}
switch (evn.key) {
case "+":
makeMore();
break;
default:
switch (evn.code) {
case "ArrowRight":
nextImg();
break;
case "ArrowLeft":
prevImg();
break;
case "Enter":
applyImg();
break;
case "Escape":
discardImg();
break;
default:
break;
}
break;
}
};
keyboard.listen.onkeyclick.on(onarrow);
// Cleans up
const clean = (removeBrushMask = false) => {
if (removeBrushMask) {
maskPaintCtx.clearRect(bb.x, bb.y, bb.w, bb.h);
}
stopMarchingAnts();
imageCollection.inputElement.removeChild(imageSelectMenu);
imageCollection.deleteLayer(layer);
blockNewImages = false;
keyboard.listen.onkeyclick.clear(onarrow);
};
const makeElement = (type, x, y) => {
const el = document.createElement(type);
el.style.position = "absolute";
el.style.left = `${x}px`;
el.style.top = `${y}px`;
// We can use the input element to add interactible html elements in the world
imageCollection.inputElement.appendChild(el);
return el;
};
redraw();
imageSelectMenu = makeElement("div", bb.x, bb.y + bb.h);
const imageindextxt = document.createElement("button");
imageindextxt.textContent = `${at + 1}/${images.length}`;
imageindextxt.addEventListener("click", () => {
at = 0;
imageindextxt.textContent = `${at + 1}/${images.length}`;
redraw();
});
const backbtn = document.createElement("button");
backbtn.textContent = "<";
backbtn.title = "Previous Image";
backbtn.addEventListener("click", prevImg);
imageSelectMenu.appendChild(backbtn);
imageSelectMenu.appendChild(imageindextxt);
const nextbtn = document.createElement("button");
nextbtn.textContent = ">";
nextbtn.title = "Next Image";
nextbtn.addEventListener("click", nextImg);
imageSelectMenu.appendChild(nextbtn);
const morebtn = document.createElement("button");
morebtn.textContent = "+";
morebtn.title = "Generate More";
morebtn.addEventListener("click", makeMore);
imageSelectMenu.appendChild(morebtn);
const acceptbtn = document.createElement("button");
acceptbtn.textContent = "Y";
acceptbtn.title = "Apply Current";
acceptbtn.addEventListener("click", applyImg);
imageSelectMenu.appendChild(acceptbtn);
const discardbtn = document.createElement("button");
discardbtn.textContent = "N";
discardbtn.title = "Cancel";
discardbtn.addEventListener("click", discardImg);
imageSelectMenu.appendChild(discardbtn);
const resourcebtn = document.createElement("button");
resourcebtn.textContent = "R";
resourcebtn.title = "Save to Resources";
resourcebtn.addEventListener("click", async () => {
const img = new Image();
// load the image data after defining the closure
img.src = "data:image/png;base64," + images[at];
img.addEventListener("load", () => {
const response = prompt("Enter new resource name", "Dream Resource");
if (response) {
tools.stamp.state.addResource(response, img);
redraw(); // Redraw to avoid strange cursor behavior
}
});
});
imageSelectMenu.appendChild(resourcebtn);
};
/**
* Callback for generating a image (dream tool)
*
* @param {*} evn
* @param {*} state
*/
const dream_generate_callback = async (evn, state) => {
if (!blockNewImages) {
const bb = getBoundingBox(
evn.x,
evn.y,
@ -19,9 +332,6 @@ const dream_generate_callback = (evn, state) => {
// Don't allow another image until is finished
blockNewImages = true;
// Setup marching ants
stopMarching = march(bb);
// Setup some basic information for SD
request.width = bb.w;
request.height = bb.h;
@ -32,7 +342,7 @@ const dream_generate_callback = (evn, state) => {
// Use txt2img if canvas is blank
if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) {
// Dream
dream(bb.x, bb.y, request, {method: "txt2img", stopMarching, bb});
_generate("txt2img", request, bb);
} else {
// Use img2img if not
@ -101,7 +411,7 @@ const dream_generate_callback = (evn, state) => {
auxCtx.fillRect(0, 0, bb.w, bb.h);
request.mask = auxCanvas.toDataURL();
// Dream
dream(bb.x, bb.y, request, {method: "img2img", stopMarching, bb});
_generate("img2img", request, bb);
}
}
};
@ -148,7 +458,7 @@ function applyOvermask(canvas, ctx, px) {
* Image to Image
*/
const dream_img2img_callback = (evn, state) => {
if (evn.target.id === "overlayCanvas" && !blockNewImages) {
if (!blockNewImages) {
const bb = getBoundingBox(
evn.x,
evn.y,
@ -174,9 +484,6 @@ const dream_img2img_callback = (evn, state) => {
// Don't allow another image until is finished
blockNewImages = true;
// Setup marching ants
stopMarching = march(bb);
// Setup some basic information for SD
request.width = bb.w;
request.height = bb.h;
@ -233,7 +540,7 @@ const dream_img2img_callback = (evn, state) => {
request.inpaint_full_res = state.fullResolution;
// Dream
dream(bb.x, bb.y, request, {method: "img2img", stopMarching, bb});
_generate("img2img", request, bb);
}
};
@ -248,23 +555,22 @@ const dreamTool = () =>
// Draw new cursor immediately
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.mousemovecb({
...mouse.coords.canvas.pos,
target: {id: "overlayCanvas"},
...mouse.coords.world.pos,
});
// Start Listeners
mouse.listen.canvas.onmousemove.on(state.mousemovecb);
mouse.listen.canvas.btn.left.onclick.on(state.dreamcb);
mouse.listen.canvas.btn.right.onclick.on(state.erasecb);
mouse.listen.world.onmousemove.on(state.mousemovecb);
mouse.listen.world.btn.left.onclick.on(state.dreamcb);
mouse.listen.world.btn.right.onclick.on(state.erasecb);
// Display Mask
setMask(state.invertMask ? "hold" : "clear");
},
(state, opt) => {
// Clear Listeners
mouse.listen.canvas.onmousemove.clear(state.mousemovecb);
mouse.listen.canvas.btn.left.onclick.clear(state.dreamcb);
mouse.listen.canvas.btn.right.onclick.clear(state.erasecb);
mouse.listen.world.onmousemove.clear(state.mousemovecb);
mouse.listen.world.btn.left.onclick.clear(state.dreamcb);
mouse.listen.world.btn.right.onclick.clear(state.erasecb);
// Hide Mask
setMask("none");
@ -274,7 +580,10 @@ const dreamTool = () =>
state.snapToGrid = true;
state.invertMask = false;
state.overMaskPx = 0;
state.mousemovecb = (evn) => _reticle_draw(evn, state.snapToGrid);
state.mousemovecb = (evn) => {
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
_reticle_draw(evn, state.snapToGrid);
};
state.dreamcb = (evn) => {
dream_generate_callback(evn, state);
};
@ -330,23 +639,22 @@ const img2imgTool = () =>
// Draw new cursor immediately
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.mousemovecb({
...mouse.coords.canvas.pos,
target: {id: "overlayCanvas"},
...mouse.coords.world.pos,
});
// Start Listeners
mouse.listen.canvas.onmousemove.on(state.mousemovecb);
mouse.listen.canvas.btn.left.onclick.on(state.dreamcb);
mouse.listen.canvas.btn.right.onclick.on(state.erasecb);
mouse.listen.world.onmousemove.on(state.mousemovecb);
mouse.listen.world.btn.left.onclick.on(state.dreamcb);
mouse.listen.world.btn.right.onclick.on(state.erasecb);
// Display Mask
setMask(state.invertMask ? "hold" : "clear");
},
(state, opt) => {
// Clear Listeners
mouse.listen.canvas.onmousemove.clear(state.mousemovecb);
mouse.listen.canvas.btn.left.onclick.clear(state.dreamcb);
mouse.listen.canvas.btn.right.onclick.clear(state.erasecb);
mouse.listen.world.onmousemove.clear(state.mousemovecb);
mouse.listen.world.btn.left.onclick.clear(state.dreamcb);
mouse.listen.world.btn.right.onclick.clear(state.erasecb);
// Hide mask
setMask("none");
@ -362,45 +670,44 @@ const img2imgTool = () =>
state.keepBorderSize = 64;
state.mousemovecb = (evn) => {
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
_reticle_draw(evn, state.snapToGrid);
if (evn.target.id === "overlayCanvas") {
const bb = getBoundingBox(
evn.x,
evn.y,
basePixelCount * scaleFactor,
basePixelCount * scaleFactor,
state.snapToGrid && basePixelCount
const bb = getBoundingBox(
evn.x,
evn.y,
basePixelCount * scaleFactor,
basePixelCount * scaleFactor,
state.snapToGrid && basePixelCount
);
// For displaying border mask
const auxCanvas = document.createElement("canvas");
auxCanvas.width = bb.w;
auxCanvas.height = bb.h;
const auxCtx = auxCanvas.getContext("2d");
if (state.keepBorderSize > 0) {
auxCtx.fillStyle = "#6A6AFF7F";
auxCtx.fillRect(0, 0, state.keepBorderSize, bb.h);
auxCtx.fillRect(0, 0, bb.w, state.keepBorderSize);
auxCtx.fillRect(
bb.w - state.keepBorderSize,
0,
state.keepBorderSize,
bb.h
);
auxCtx.fillRect(
0,
bb.h - state.keepBorderSize,
bb.w,
state.keepBorderSize
);
// For displaying border mask
const auxCanvas = document.createElement("canvas");
auxCanvas.width = bb.w;
auxCanvas.height = bb.h;
const auxCtx = auxCanvas.getContext("2d");
if (state.keepBorderSize > 0) {
auxCtx.fillStyle = "#6A6AFF7F";
auxCtx.fillRect(0, 0, state.keepBorderSize, bb.h);
auxCtx.fillRect(0, 0, bb.w, state.keepBorderSize);
auxCtx.fillRect(
bb.w - state.keepBorderSize,
0,
state.keepBorderSize,
bb.h
);
auxCtx.fillRect(
0,
bb.h - state.keepBorderSize,
bb.w,
state.keepBorderSize
);
}
const tmp = ovCtx.globalAlpha;
ovCtx.globalAlpha = 0.4;
ovCtx.drawImage(auxCanvas, bb.x, bb.y);
ovCtx.globalAlpha = tmp;
}
const tmp = ovCtx.globalAlpha;
ovCtx.globalAlpha = 0.4;
ovCtx.drawImage(auxCanvas, bb.x, bb.y);
ovCtx.globalAlpha = tmp;
};
state.dreamcb = (evn) => {
dream_img2img_callback(evn, state);

View file

@ -1,5 +1,5 @@
const setMask = (state) => {
const canvas = document.querySelector("#maskPaintCanvas");
const canvas = imageCollection.layers.mask.canvas;
switch (state) {
case "clear":
canvas.classList.remove("hold");
@ -23,43 +23,33 @@ const setMask = (state) => {
};
const _mask_brush_draw_callback = (evn, state) => {
if (
(evn.initialTarget && evn.initialTarget.id === "overlayCanvas") ||
(!evn.initialTarget && evn.target.id === "overlayCanvas")
) {
maskPaintCtx.globalCompositeOperation = "source-over";
maskPaintCtx.strokeStyle = "black";
maskPaintCtx.globalCompositeOperation = "source-over";
maskPaintCtx.strokeStyle = "black";
maskPaintCtx.lineWidth = state.brushSize;
maskPaintCtx.beginPath();
maskPaintCtx.moveTo(
evn.px === undefined ? evn.x : evn.px,
evn.py === undefined ? evn.y : evn.py
);
maskPaintCtx.lineTo(evn.x, evn.y);
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
maskPaintCtx.stroke();
}
maskPaintCtx.lineWidth = state.brushSize;
maskPaintCtx.beginPath();
maskPaintCtx.moveTo(
evn.px === undefined ? evn.x : evn.px,
evn.py === undefined ? evn.y : evn.py
);
maskPaintCtx.lineTo(evn.x, evn.y);
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
maskPaintCtx.stroke();
};
const _mask_brush_erase_callback = (evn, state) => {
if (
(evn.initialTarget && evn.initialTarget.id === "overlayCanvas") ||
(!evn.initialTarget && evn.target.id === "overlayCanvas")
) {
maskPaintCtx.globalCompositeOperation = "destination-out";
maskPaintCtx.strokeStyle = "black";
maskPaintCtx.globalCompositeOperation = "destination-out";
maskPaintCtx.strokeStyle = "black";
maskPaintCtx.lineWidth = state.brushSize;
maskPaintCtx.beginPath();
maskPaintCtx.moveTo(
evn.px === undefined ? evn.x : evn.px,
evn.py === undefined ? evn.y : evn.py
);
maskPaintCtx.lineTo(evn.x, evn.y);
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
maskPaintCtx.stroke();
}
maskPaintCtx.lineWidth = state.brushSize;
maskPaintCtx.beginPath();
maskPaintCtx.moveTo(
evn.px === undefined ? evn.x : evn.px,
evn.py === undefined ? evn.y : evn.py
);
maskPaintCtx.lineTo(evn.x, evn.y);
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
maskPaintCtx.stroke();
};
const maskBrushTool = () =>
@ -69,27 +59,27 @@ const maskBrushTool = () =>
(state, opt) => {
// Draw new cursor immediately
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb({...mouse.coords.canvas.pos, target: {id: "overlayCanvas"}});
state.movecb({...mouse.coords.world.pos});
// Start Listeners
mouse.listen.canvas.onmousemove.on(state.movecb);
mouse.listen.canvas.onwheel.on(state.wheelcb);
mouse.listen.canvas.btn.left.onpaintstart.on(state.drawcb);
mouse.listen.canvas.btn.left.onpaint.on(state.drawcb);
mouse.listen.canvas.btn.right.onpaintstart.on(state.erasecb);
mouse.listen.canvas.btn.right.onpaint.on(state.erasecb);
mouse.listen.world.onmousemove.on(state.movecb);
mouse.listen.world.onwheel.on(state.wheelcb);
mouse.listen.world.btn.left.onpaintstart.on(state.drawcb);
mouse.listen.world.btn.left.onpaint.on(state.drawcb);
mouse.listen.world.btn.right.onpaintstart.on(state.erasecb);
mouse.listen.world.btn.right.onpaint.on(state.erasecb);
// Display Mask
setMask("neutral");
},
(state, opt) => {
// Clear Listeners
mouse.listen.canvas.onmousemove.clear(state.movecb);
mouse.listen.canvas.onwheel.clear(state.wheelcb);
mouse.listen.canvas.btn.left.onpaintstart.clear(state.drawcb);
mouse.listen.canvas.btn.left.onpaint.clear(state.drawcb);
mouse.listen.canvas.btn.right.onpaintstart.clear(state.erasecb);
mouse.listen.canvas.btn.right.onpaint.clear(state.erasecb);
mouse.listen.world.onmousemove.clear(state.movecb);
mouse.listen.world.onwheel.clear(state.wheelcb);
mouse.listen.world.btn.left.onpaintstart.clear(state.drawcb);
mouse.listen.world.btn.left.onpaint.clear(state.drawcb);
mouse.listen.world.btn.right.onpaintstart.clear(state.erasecb);
mouse.listen.world.btn.right.onpaint.clear(state.erasecb);
// Hide Mask
setMask("none");
@ -115,25 +105,24 @@ const maskBrushTool = () =>
state.preview = false;
state.movecb = (evn) => {
if (evn.target.id === "overlayCanvas") {
// draw big translucent white blob cursor
ovCtx.beginPath();
ovCtx.arc(evn.x, evn.y, state.brushSize / 2, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
ovCtx.fillStyle = "#FFFFFF50";
// draw big translucent white blob cursor
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
ovCtx.beginPath();
ovCtx.arc(evn.x, evn.y, state.brushSize / 2, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
ovCtx.fillStyle = "#FFFFFF50";
ovCtx.fill();
ovCtx.fill();
if (state.preview) {
ovCtx.strokeStyle = "#000F";
ovCtx.setLineDash([4, 2]);
ovCtx.stroke();
ovCtx.setLineDash([]);
}
if (state.preview) {
ovCtx.strokeStyle = "#000F";
ovCtx.setLineDash([4, 2]);
ovCtx.stroke();
ovCtx.setLineDash([]);
}
};
state.wheelcb = (evn) => {
if (evn.target.id === "overlayCanvas") {
if (!evn.evn.ctrlKey) {
state.brushSize = state.setBrushSize(
state.brushSize -
Math.floor(state.config.brushScrollSpeed * evn.delta)

View file

@ -5,16 +5,16 @@ const selectTransformTool = () =>
(state, opt) => {
// Draw new cursor immediately
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb({...mouse.coords.canvas.pos, target: {id: "overlayCanvas"}});
state.movecb(mouse.coords.world.pos);
// Canvas left mouse handlers
mouse.listen.canvas.onmousemove.on(state.movecb);
mouse.listen.canvas.btn.left.onclick.on(state.clickcb);
mouse.listen.canvas.btn.left.ondragstart.on(state.dragstartcb);
mouse.listen.canvas.btn.left.ondragend.on(state.dragendcb);
mouse.listen.world.onmousemove.on(state.movecb);
mouse.listen.world.btn.left.onclick.on(state.clickcb);
mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb);
mouse.listen.world.btn.left.ondragend.on(state.dragendcb);
// Canvas right mouse handler
mouse.listen.canvas.btn.right.onclick.on(state.cancelcb);
mouse.listen.world.btn.right.onclick.on(state.cancelcb);
// Keyboard click handlers
keyboard.listen.onkeyclick.on(state.keyclickcb);
@ -29,12 +29,12 @@ const selectTransformTool = () =>
},
(state, opt) => {
// Clear all those listeners and shortcuts we set up
mouse.listen.canvas.onmousemove.clear(state.movecb);
mouse.listen.canvas.btn.left.onclick.clear(state.clickcb);
mouse.listen.canvas.btn.left.ondragstart.clear(state.dragstartcb);
mouse.listen.canvas.btn.left.ondragend.clear(state.dragendcb);
mouse.listen.world.onmousemove.clear(state.movecb);
mouse.listen.world.btn.left.onclick.clear(state.clickcb);
mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb);
mouse.listen.world.btn.left.ondragend.clear(state.dragendcb);
mouse.listen.canvas.btn.right.onclick.clear(state.cancelcb);
mouse.listen.world.btn.right.onclick.clear(state.cancelcb);
keyboard.listen.onkeyclick.clear(state.keyclickcb);
keyboard.listen.onkeydown.clear(state.keydowncb);
@ -46,7 +46,7 @@ const selectTransformTool = () =>
state.reset();
// Resets cursor
ovCanvas.style.cursor = "auto";
imageCollection.inputElement.style.cursor = "auto";
},
{
init: (state) => {
@ -183,276 +183,271 @@ const selectTransformTool = () =>
};
};
// Mouse move handelr. As always, also renders cursor
// Mouse move handler. As always, also renders cursor
state.movecb = (evn) => {
ovCanvas.style.cursor = "auto";
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
imageCollection.inputElement.style.cursor = "auto";
state.lastMouseTarget = evn.target;
state.lastMouseMove = evn;
if (evn.target.id === "overlayCanvas") {
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, true, 64);
y += snap(evn.y, true, 64);
}
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, true, 64);
y += snap(evn.y, true, 64);
}
// Update scale
if (state.scaling) {
state.scaling.scaleTo(x, y, state.keepAspectRatio);
}
// Update scale
if (state.scaling) {
state.scaling.scaleTo(x, y, state.keepAspectRatio);
}
// Update position
if (state.moving) {
state.selected.x = x - state.moving.offset.x;
state.selected.y = y - state.moving.offset.y;
state.selected.updateOriginal();
}
// Update position
if (state.moving) {
state.selected.x = x - state.moving.offset.x;
state.selected.y = y - state.moving.offset.y;
state.selected.updateOriginal();
}
// Draw dragging box
if (state.dragging) {
ovCtx.setLineDash([2, 2]);
ovCtx.lineWidth = 1;
ovCtx.strokeStyle = "#FFF";
const ix = state.dragging.ix;
const iy = state.dragging.iy;
const bb = selectionBB(ix, iy, x, y);
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h);
ovCtx.setLineDash([]);
}
if (state.selected) {
ovCtx.lineWidth = 1;
ovCtx.strokeStyle = "#FFF";
const bb = {
x: state.selected.x,
y: state.selected.y,
w: state.selected.w,
h: state.selected.h,
};
// Draw Image
ovCtx.drawImage(
state.selected.image,
0,
0,
state.selected.image.width,
state.selected.image.height,
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h
);
// Draw selection box
ovCtx.setLineDash([4, 2]);
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h);
ovCtx.setLineDash([]);
// Draw Scaling/Rotation Origin
ovCtx.beginPath();
ovCtx.arc(
state.selected.x + state.selected.w / 2,
state.selected.y + state.selected.h / 2,
5,
0,
2 * Math.PI
);
ovCtx.stroke();
// Draw Scaling Handles
let cursorInHandle = false;
state.selected.handles().forEach((handle) => {
if (handle.contains(evn.x, evn.y)) {
cursorInHandle = true;
ovCtx.strokeRect(
handle.x - 1,
handle.y - 1,
handle.w + 2,
handle.h + 2
);
} else {
ovCtx.strokeRect(handle.x, handle.y, handle.w, handle.h);
}
});
// Change cursor
if (cursorInHandle || state.selected.contains(evn.x, evn.y))
ovCanvas.style.cursor = "pointer";
}
// Draw current cursor location
ovCtx.lineWidth = 3;
// Draw dragging box
if (state.dragging) {
ovCtx.setLineDash([2, 2]);
ovCtx.lineWidth = 1;
ovCtx.strokeStyle = "#FFF";
ovCtx.beginPath();
ovCtx.moveTo(x, y + 10);
ovCtx.lineTo(x, y - 10);
ovCtx.moveTo(x + 10, y);
ovCtx.lineTo(x - 10, y);
ovCtx.stroke();
const ix = state.dragging.ix;
const iy = state.dragging.iy;
const bb = selectionBB(ix, iy, x, y);
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h);
ovCtx.setLineDash([]);
}
if (state.selected) {
ovCtx.lineWidth = 1;
ovCtx.strokeStyle = "#FFF";
const bb = {
x: state.selected.x,
y: state.selected.y,
w: state.selected.w,
h: state.selected.h,
};
// Draw Image
ovCtx.drawImage(
state.selected.image,
0,
0,
state.selected.image.width,
state.selected.image.height,
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h
);
// Draw selection box
ovCtx.setLineDash([4, 2]);
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h);
ovCtx.setLineDash([]);
// Draw Scaling/Rotation Origin
ovCtx.beginPath();
ovCtx.arc(
state.selected.x + state.selected.w / 2,
state.selected.y + state.selected.h / 2,
5,
0,
2 * Math.PI
);
ovCtx.stroke();
// Draw Scaling Handles
let cursorInHandle = false;
state.selected.handles().forEach((handle) => {
if (handle.contains(evn.x, evn.y)) {
cursorInHandle = true;
ovCtx.strokeRect(
handle.x - 1,
handle.y - 1,
handle.w + 2,
handle.h + 2
);
} else {
ovCtx.strokeRect(handle.x, handle.y, handle.w, handle.h);
}
});
// Change cursor
if (cursorInHandle || state.selected.contains(evn.x, evn.y))
imageCollection.inputElement.style.cursor = "pointer";
}
// Draw current cursor location
ovCtx.lineWidth = 3;
ovCtx.strokeStyle = "#FFF";
ovCtx.beginPath();
ovCtx.moveTo(x, y + 10);
ovCtx.lineTo(x, y - 10);
ovCtx.moveTo(x + 10, y);
ovCtx.lineTo(x - 10, y);
ovCtx.stroke();
};
// Handles left mouse clicks
state.clickcb = (evn) => {
if (evn.target.id === "overlayCanvas") {
// If something is selected, commit changes to the canvas
if (state.selected) {
imgCtx.drawImage(
state.selected.image,
state.original.x,
state.original.y
);
commands.runCommand(
"eraseImage",
"Image Transform Erase",
state.original
);
commands.runCommand(
"drawImage",
"Image Transform Draw",
state.selected
);
state.original = null;
state.selected = null;
if (
state.original.x === state.selected.x &&
state.original.y === state.selected.y &&
state.original.w === state.selected.w &&
state.original.h === state.selected.h
) {
state.reset();
return;
}
redraw();
}
// If something is selected, commit changes to the canvas
if (state.selected) {
imgCtx.drawImage(
state.selected.image,
state.original.x,
state.original.y
);
commands.runCommand(
"eraseImage",
"Image Transform Erase",
state.original
);
commands.runCommand(
"drawImage",
"Image Transform Draw",
state.selected
);
state.original = null;
state.selected = null;
redraw();
}
};
// Handles left mouse drag events
state.dragstartcb = (evn) => {
if (evn.target.id === "overlayCanvas") {
let ix = evn.ix;
let iy = evn.iy;
if (state.snapToGrid) {
ix += snap(evn.ix, true, 64);
iy += snap(evn.iy, true, 64);
}
// If is selected, check if drag is in handles/body and act accordingly
if (state.selected) {
const handles = state.selected.handles();
const activeHandle = handles.find((v) =>
v.contains(evn.ix, evn.iy)
);
if (activeHandle) {
state.scaling = activeHandle;
return;
} else if (state.selected.contains(ix, iy)) {
state.moving = {
offset: {x: ix - state.selected.x, y: iy - state.selected.y},
};
return;
}
}
// If it is not, just create new selection
state.reset();
state.dragging = {ix, iy};
let ix = evn.ix;
let iy = evn.iy;
if (state.snapToGrid) {
ix += snap(evn.ix, true, 64);
iy += snap(evn.iy, true, 64);
}
// If is selected, check if drag is in handles/body and act accordingly
if (state.selected) {
const handles = state.selected.handles();
const activeHandle = handles.find((v) =>
v.contains(evn.ix, evn.iy)
);
if (activeHandle) {
state.scaling = activeHandle;
return;
} else if (state.selected.contains(ix, iy)) {
state.moving = {
offset: {x: ix - state.selected.x, y: iy - state.selected.y},
};
return;
}
}
// If it is not, just create new selection
state.reset();
state.dragging = {ix, iy};
};
// Handles left mouse drag end events
state.dragendcb = (evn) => {
if (evn.target.id === "overlayCanvas") {
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, true, 64);
y += snap(evn.y, true, 64);
}
// If we are scaling, stop scaling and do some handler magic
if (state.scaling) {
state.selected.updateOriginal();
state.scaling = null;
// If we are moving the selection, just... stop
} else if (state.moving) {
state.moving = null;
/**
* If we are dragging, create a cutout selection area and save to an auxiliar image
* We will be rendering the image to the overlay, so it will not be noticeable
*/
} else if (state.dragging) {
state.original = selectionBB(
state.dragging.ix,
state.dragging.iy,
x,
y
);
state.selected = selectionBB(
state.dragging.ix,
state.dragging.iy,
x,
y
);
// Cut out selected portion of the image for manipulation
const cvs = document.createElement("canvas");
cvs.width = state.selected.w;
cvs.height = state.selected.h;
const ctx = cvs.getContext("2d");
ctx.drawImage(
imgCanvas,
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h,
0,
0,
state.selected.w,
state.selected.h
);
imgCtx.clearRect(
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h
);
state.selected.image = cvs;
state.original.image = cvs;
if (state.selected.w === 0 || state.selected.h === 0)
state.selected = null;
state.dragging = null;
}
redraw();
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, true, 64);
y += snap(evn.y, true, 64);
}
// If we are scaling, stop scaling and do some handler magic
if (state.scaling) {
state.selected.updateOriginal();
state.scaling = null;
// If we are moving the selection, just... stop
} else if (state.moving) {
state.moving = null;
/**
* If we are dragging, create a cutout selection area and save to an auxiliar image
* We will be rendering the image to the overlay, so it will not be noticeable
*/
} else if (state.dragging) {
state.original = selectionBB(
state.dragging.ix,
state.dragging.iy,
x,
y
);
state.selected = selectionBB(
state.dragging.ix,
state.dragging.iy,
x,
y
);
// Cut out selected portion of the image for manipulation
const cvs = document.createElement("canvas");
cvs.width = state.selected.w;
cvs.height = state.selected.h;
const ctx = cvs.getContext("2d");
ctx.drawImage(
imgCanvas,
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h,
0,
0,
state.selected.w,
state.selected.h
);
imgCtx.clearRect(
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h
);
state.selected.image = cvs;
state.original.image = cvs;
if (state.selected.w === 0 || state.selected.h === 0)
state.selected = null;
state.dragging = null;
}
redraw();
};
// Handler for right clicks. Basically resets everything
state.cancelcb = (evn) => {
if (evn.target.id === "overlayCanvas") {
state.reset();
}
state.reset();
};
// Keyboard callbacks (For now, they just handle the "delete" key)
state.keydowncb = (evn) => {};
state.keyclickcb = (evn) => {
if (state.lastMouseTarget.id === "overlayCanvas") {
switch (evn.code) {
case "Delete":
// Deletes selected area
state.selected &&
commands.runCommand(
"eraseImage",
"Erase Area",
state.selected
);
state.selected = null;
redraw();
}
switch (evn.code) {
case "Delete":
// Deletes selected area
state.selected &&
commands.runCommand("eraseImage", "Erase Area", state.selected);
state.selected = null;
redraw();
}
};
@ -460,47 +455,45 @@ const selectTransformTool = () =>
// Handles copying
state.ctrlccb = (evn, cut = false) => {
if (state.selected && state.lastMouseTarget.id === "overlayCanvas") {
// We create a new canvas to store the data
state.clipboard.copy = document.createElement("canvas");
// We create a new canvas to store the data
state.clipboard.copy = document.createElement("canvas");
state.clipboard.copy.width = state.selected.w;
state.clipboard.copy.height = state.selected.h;
state.clipboard.copy.width = state.selected.w;
state.clipboard.copy.height = state.selected.h;
const ctx = state.clipboard.copy.getContext("2d");
const ctx = state.clipboard.copy.getContext("2d");
ctx.clearRect(0, 0, state.selected.w, state.selected.h);
ctx.drawImage(
state.selected.image,
0,
0,
state.selected.image.width,
state.selected.image.height,
0,
0,
state.selected.w,
state.selected.h
);
ctx.clearRect(0, 0, state.selected.w, state.selected.h);
ctx.drawImage(
state.selected.image,
0,
0,
state.selected.image.width,
state.selected.image.height,
0,
0,
state.selected.w,
state.selected.h
);
// If cutting, we reverse the selection and erase the selection area
if (cut) {
const aux = state.original;
state.reset();
// If cutting, we reverse the selection and erase the selection area
if (cut) {
const aux = state.original;
state.reset();
commands.runCommand("eraseImage", "Cut Image", aux);
}
commands.runCommand("eraseImage", "Cut Image", aux);
}
// Because firefox needs manual activation of the feature
if (state.useClipboard) {
// Send to clipboard
state.clipboard.copy.toBlob((blob) => {
const item = new ClipboardItem({"image/png": blob});
navigator.clipboard.write([item]).catch((e) => {
console.warn("Error sending to clipboard");
console.warn(e);
});
// Because firefox needs manual activation of the feature
if (state.useClipboard) {
// Send to clipboard
state.clipboard.copy.toBlob((blob) => {
const item = new ClipboardItem({"image/png": blob});
navigator.clipboard.write([item]).catch((e) => {
console.warn("Error sending to clipboard");
console.warn(e);
});
}
});
}
};

View file

@ -3,14 +3,16 @@ const stampTool = () =>
"res/icons/file-up.svg",
"Stamp Image",
(state, opt) => {
state.loaded = true;
// Draw new cursor immediately
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb({...mouse.coords.canvas.pos, target: {id: "overlayCanvas"}});
state.movecb({...mouse.coords.world.pos});
// Start Listeners
mouse.listen.canvas.onmousemove.on(state.movecb);
mouse.listen.canvas.btn.left.onclick.on(state.drawcb);
mouse.listen.canvas.btn.right.onclick.on(state.cancelcb);
mouse.listen.world.onmousemove.on(state.movecb);
mouse.listen.world.btn.left.onclick.on(state.drawcb);
mouse.listen.world.btn.right.onclick.on(state.cancelcb);
// For calls from other tools to paste image
if (opt && opt.image) {
@ -31,10 +33,12 @@ const stampTool = () =>
}
},
(state, opt) => {
state.loaded = false;
// Clear Listeners
mouse.listen.canvas.onmousemove.clear(state.movecb);
mouse.listen.canvas.btn.left.onclick.clear(state.drawcb);
mouse.listen.canvas.btn.right.onclick.clear(state.cancelcb);
mouse.listen.world.onmousemove.clear(state.movecb);
mouse.listen.world.btn.left.onclick.clear(state.drawcb);
mouse.listen.world.btn.right.onclick.clear(state.cancelcb);
// Deselect
state.selected = null;
@ -44,6 +48,7 @@ const stampTool = () =>
},
{
init: (state) => {
state.loaded = false;
state.snapToGrid = true;
state.resources = [];
state.selected = null;
@ -75,7 +80,7 @@ const stampTool = () =>
}
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb(state.lastMouseMove);
if (state.loaded) state.movecb(state.lastMouseMove);
};
// Synchronizes resources array with the DOM
@ -153,73 +158,69 @@ const stampTool = () =>
};
state.movecb = (evn) => {
if (evn.target && evn.target.id === "overlayCanvas") {
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, true, 64);
y += snap(evn.y, true, 64);
}
state.lastMouseMove = evn;
// Draw selected image
if (state.selected) {
ovCtx.drawImage(state.selected.image, x, y);
}
// Draw current cursor location
ovCtx.lineWidth = 3;
ovCtx.strokeStyle = "#FFF";
ovCtx.beginPath();
ovCtx.moveTo(x, y + 10);
ovCtx.lineTo(x, y - 10);
ovCtx.moveTo(x + 10, y);
ovCtx.lineTo(x - 10, y);
ovCtx.stroke();
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, true, 64);
y += snap(evn.y, true, 64);
}
state.lastMouseMove = evn;
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
// Draw selected image
if (state.selected) {
ovCtx.drawImage(state.selected.image, x, y);
}
// Draw current cursor location
ovCtx.lineWidth = 3;
ovCtx.strokeStyle = "#FFF";
ovCtx.beginPath();
ovCtx.moveTo(x, y + 10);
ovCtx.lineTo(x, y - 10);
ovCtx.moveTo(x + 10, y);
ovCtx.lineTo(x - 10, y);
ovCtx.stroke();
};
state.drawcb = (evn) => {
if (evn.target.id === "overlayCanvas") {
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, true, 64);
y += snap(evn.y, true, 64);
}
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
x += snap(evn.x, true, 64);
y += snap(evn.y, true, 64);
}
const resource = state.selected;
const resource = state.selected;
if (resource) {
commands.runCommand("drawImage", "Image Stamp", {
image: resource.image,
x,
y,
});
if (resource) {
commands.runCommand("drawImage", "Image Stamp", {
image: resource.image,
x,
y,
});
if (resource.temporary) state.deleteResource(resource.id);
}
if (resource.temporary) state.deleteResource(resource.id);
}
if (state.back) {
toolbar.unlock();
const backfn = state.back;
state.back = null;
backfn({message: "Returning from stamp", pasted: true});
}
if (state.back) {
toolbar.unlock();
const backfn = state.back;
state.back = null;
backfn({message: "Returning from stamp", pasted: true});
}
};
state.cancelcb = (evn) => {
if (evn.target.id === "overlayCanvas") {
state.selectResource(null);
state.selectResource(null);
if (state.back) {
toolbar.unlock();
const backfn = state.back;
state.back = null;
backfn({message: "Returning from stamp", pasted: false});
}
if (state.back) {
toolbar.unlock();
const backfn = state.back;
state.back = null;
backfn({message: "Returning from stamp", pasted: false});
}
};