Merge pull request #60 from zero01101/bleeding-edge
Automatic Layers and Not-Quite Infinity
This commit is contained in:
commit
3281277e4f
24 changed files with 1716 additions and 1117 deletions
|
@ -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)
|
||||
|
|
|
@ -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
46
css/layers.css
Normal 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;
|
||||
}
|
|
@ -9,6 +9,10 @@
|
|||
background-color: var(--c-primary);
|
||||
}
|
||||
|
||||
#ui-toolbar * {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#ui-toolbar .handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
119
index.html
119
index.html
|
@ -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>
|
||||
|
|
390
js/index.js
390
js/index.js
|
@ -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)"><</button><button onclick="nextImg(this)">></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}
|
||||
);
|
||||
|
|
29
js/initalize/debug.populate.js
Normal file
29
js/initalize/debug.populate.js
Normal 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();
|
||||
};
|
189
js/initalize/layers.populate.js
Normal file
189
js/initalize/layers.populate.js
Normal 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);
|
||||
});
|
27
js/initalize/toolbar.populate.js
Normal file
27
js/initalize/toolbar.populate.js
Normal 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();
|
|
@ -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
|
||||
*/
|
||||
|
||||
/**
|
|
@ -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
264
js/lib/layers.js
Normal 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;
|
||||
},
|
||||
};
|
|
@ -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();
|
|
@ -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
3
js/ui/explore.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
/**
|
||||
* This is a simple implementation of layer interaction
|
||||
*/
|
23
js/ui/tool/dream.d.js
Normal file
23
js/ui/tool/dream.d.js
Normal 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
|
||||
*/
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue