Layers.
Been working on this when I could the last few days. Not quite infinity. Middle mouse button drag, ctrl mouse wheel zoom. Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
This commit is contained in:
parent
4e27770284
commit
44cf9c0e70
26 changed files with 1549 additions and 1346 deletions
|
@ -104,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%);
|
||||
}
|
||||
|
@ -215,11 +211,6 @@ div.prompt-wrapper > textarea {
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0;
|
||||
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
|
@ -228,7 +219,6 @@ div.prompt-wrapper > textarea:focus {
|
|||
}
|
||||
|
||||
/* Tool buttons */
|
||||
|
||||
.button-array {
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
|
|
|
@ -4,3 +4,43 @@
|
|||
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;
|
||||
|
|
128
index.html
128
index.html
|
@ -191,123 +191,27 @@
|
|||
<div class="toolbar-section"></div>
|
||||
</div>
|
||||
|
||||
<!-- Layer Allocation View -->
|
||||
<div
|
||||
id="layer-preview"
|
||||
class="floating-window toolbar"
|
||||
style="left: 10px; bottom: 10px">
|
||||
<div class="draggable floating-window-title">Layer Debug View</div>
|
||||
<div class="menu-container" style="min-width: 200px">
|
||||
<canvas class="preview-canvas"></canvas>
|
||||
</div>
|
||||
</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/layers.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/commands.js" type="text/javascript"></script>
|
||||
<script src="js/ui/history.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>
|
||||
|
@ -315,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>
|
||||
|
|
345
js/index.js
345
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
|
||||
|
@ -89,44 +79,6 @@ 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");
|
||||
|
||||
// Layering
|
||||
const imageCollection = layers.registerCollection("image", {
|
||||
name: "Image Layers",
|
||||
scope: {
|
||||
always: {
|
||||
key: "default",
|
||||
options: {
|
||||
name: "Default Image Layer",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
layers.registerCollection("mask", {name: "Mask Layers", requiresActive: true});
|
||||
|
||||
//
|
||||
function startup() {
|
||||
testHostConfiguration();
|
||||
|
@ -163,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;
|
||||
}
|
||||
|
||||
|
@ -197,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:"
|
||||
|
@ -205,11 +158,7 @@ function testHostConfiguration() {
|
|||
}
|
||||
|
||||
function testHostConnection() {
|
||||
function CheckInProgressError(message = "") {
|
||||
this.name = "CheckInProgressError";
|
||||
this.message = message;
|
||||
}
|
||||
CheckInProgressError.prototype = Object.create(Error.prototype);
|
||||
class CheckInProgressError extends Error {}
|
||||
|
||||
const connectionIndicator = document.getElementById(
|
||||
"connection-status-indicator"
|
||||
|
@ -343,131 +292,9 @@ 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) + "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;
|
||||
};
|
||||
// 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,
|
||||
|
@ -548,10 +375,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);
|
||||
}
|
||||
|
@ -581,122 +404,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() {
|
||||
|
@ -815,10 +553,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1097,6 +836,18 @@ function loadSettings() {
|
|||
// document.getElementById("overMaskPx").value = Number(_overmask_px);
|
||||
}
|
||||
|
||||
document.getElementById("mainHSplit").addEventListener("wheel", (evn) => {
|
||||
imageCollection.element.addEventListener(
|
||||
"wheel",
|
||||
(evn) => {
|
||||
evn.preventDefault();
|
||||
});
|
||||
},
|
||||
{passive: false}
|
||||
);
|
||||
|
||||
imageCollection.element.addEventListener(
|
||||
"contextmenu",
|
||||
(evn) => {
|
||||
evn.preventDefault();
|
||||
},
|
||||
{passive: false}
|
||||
);
|
||||
|
|
|
@ -106,7 +106,7 @@ const infinity = {
|
|||
},
|
||||
};
|
||||
|
||||
infinity._init();
|
||||
//infinity._init();
|
||||
Array.from(document.getElementsByClassName("display-canvas")).forEach(
|
||||
(canvas) => infinity.registerViewport(canvas)
|
||||
);
|
||||
|
|
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 = snap(evn.x);
|
||||
snapYInfo.textContent = 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();
|
||||
};
|
182
js/initalize/layers.populate.js
Normal file
182
js/initalize/layers.populate.js
Normal file
|
@ -0,0 +1,182 @@
|
|||
// Layering
|
||||
const imageCollection = layers.registerCollection(
|
||||
"image",
|
||||
{w: 2560, h: 1472},
|
||||
{
|
||||
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) => {
|
||||
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;
|
||||
},
|
||||
{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)`;
|
||||
},
|
||||
};
|
||||
|
||||
let rotation = 0;
|
||||
let lastTime = performance.now();
|
||||
|
||||
const onframe = () => {
|
||||
const nowTime = performance.now();
|
||||
const dt = nowTime - lastTime;
|
||||
rotation += (10 * dt) / 1000.0;
|
||||
|
||||
lastTime = nowTime;
|
||||
|
||||
viewport.transform(imageCollection.element);
|
||||
|
||||
requestAnimationFrame(onframe);
|
||||
};
|
||||
|
||||
onframe();
|
||||
|
||||
viewport.cx = viewport.w / 2;
|
||||
viewport.cy = viewport.h / 2;
|
||||
|
||||
let worldInit = null;
|
||||
|
||||
imageCollection.element.style.transformOrigin = `${viewport.cx}px ${viewport.cy}px`;
|
||||
viewport.transform(imageCollection.element);
|
||||
|
||||
mouse.listen.window.onwheel.on((evn) => {
|
||||
if (evn.evn.ctrlKey) {
|
||||
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;
|
||||
});
|
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();
|
172
js/layers.js
172
js/layers.js
|
@ -1,172 +0,0 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
// Errors
|
||||
class LayerNestedScopesError extends Error {
|
||||
// For when a scope is created in another scope
|
||||
}
|
||||
class LayerNoScopeError extends Error {
|
||||
// For when an action that requires a scope is attempted
|
||||
// in a collection with no scope.
|
||||
}
|
||||
|
||||
const layers = {
|
||||
collections: makeWriteOnce({}, "layers.collections"),
|
||||
|
||||
// Registers a new collection
|
||||
registerCollection: (key, options = {}) => {
|
||||
defaultOpt(options, {
|
||||
// If collection is visible on the Layer View Toolbar
|
||||
visible: true,
|
||||
// Display name for the collection
|
||||
name: key,
|
||||
/**
|
||||
* If layer creates a layer scope
|
||||
*
|
||||
* A layer scope is a context where one, and only one layer inside it or its
|
||||
* subscopes can be active at a time. Nested scopes are not supported.
|
||||
* It receives an object of type:
|
||||
*
|
||||
* {
|
||||
* // If there must be a selected layer, pass information to create the first
|
||||
* always: {
|
||||
* key,
|
||||
* options
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
scope: null,
|
||||
// Parent collection
|
||||
parent: null,
|
||||
});
|
||||
|
||||
// Finds the closest parent with a defined scope
|
||||
const findScope = (collection = options.parent) => {
|
||||
if (!collection) return null;
|
||||
|
||||
if (collection.scope) return collection;
|
||||
return findScope(collection._parent);
|
||||
};
|
||||
|
||||
// Path used for logging purposes
|
||||
const _logpath = options.parent
|
||||
? options.parent + "." + key
|
||||
: "layers.collections." + key;
|
||||
|
||||
// If we have a scope already, we can't add a new scope
|
||||
if (options.scope && findScope())
|
||||
throw new LayerNestedScopesError(`Layer scopes must not be nested`);
|
||||
|
||||
const collection = makeWriteOnce(
|
||||
{
|
||||
_parent: options.parent,
|
||||
_logpath,
|
||||
_layers: [],
|
||||
layers: {},
|
||||
|
||||
name: options.name,
|
||||
|
||||
scope: options.scope,
|
||||
// Registers a new layer
|
||||
registerLayer: (key, options = {}) => {
|
||||
defaultOpt(options, {
|
||||
// Display name for the layer
|
||||
name: key,
|
||||
});
|
||||
|
||||
// Path used for logging purposes
|
||||
const _layerlogpath = _logpath + ".layers." + key;
|
||||
const layer = makeWriteOnce(
|
||||
{
|
||||
_logpath: _layerlogpath,
|
||||
id: guid(),
|
||||
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;
|
||||
},
|
||||
}
|
||||
),
|
||||
|
||||
// This is where black magic will take place in the future
|
||||
// A proxy for the canvas object
|
||||
canvas: new Proxy(document.createElement("canvas"), {}),
|
||||
|
||||
// Activates this layer in the scope
|
||||
activate: () => {
|
||||
const scope = findScope(collection);
|
||||
if (scope) {
|
||||
scope.active = layer;
|
||||
console.debug(
|
||||
`[layers] Layer ${layer._logpath} now active in scope ${scope._logpath}`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// Deactivates this layer in the scope
|
||||
deactivate: () => {
|
||||
const scope = findScope(collection);
|
||||
if (scope && scope.active === layer) scope.active = null;
|
||||
console.debug();
|
||||
},
|
||||
},
|
||||
_layerlogpath
|
||||
);
|
||||
|
||||
// Add to indexers
|
||||
collection._layers.push(layer);
|
||||
collection.layers[key] = layer;
|
||||
|
||||
console.info(
|
||||
`[layers] Layer '${layer.name}' at ${layer._logpath} registered`
|
||||
);
|
||||
return layer;
|
||||
},
|
||||
|
||||
// Deletes a layer
|
||||
deleteLayer: (layer) => {
|
||||
collection._layers.splice(
|
||||
collection._layers.findIndex(
|
||||
(l) => l.id === layer || l.id === layer.id
|
||||
),
|
||||
1
|
||||
);
|
||||
if (typeof layer === "object") {
|
||||
delete collection.layers[layer.id];
|
||||
} else if (typeof layer === "string") {
|
||||
delete collection.layers[layer];
|
||||
}
|
||||
|
||||
console.info(`[layers] Layer '${layer}' deleted`);
|
||||
},
|
||||
},
|
||||
_logpath
|
||||
);
|
||||
|
||||
if (parent) parent[key] = collection;
|
||||
else layers.collections[key] = collection;
|
||||
|
||||
console.info(
|
||||
`[layers] Collection '${options.name}' at ${_logpath} registered`
|
||||
);
|
||||
|
||||
// If always, we must create a layer to select
|
||||
if (options.scope && options.scope.always)
|
||||
collection
|
||||
.registerLayer(options.scope.always.key, options.scope.always.options)
|
||||
.activate();
|
||||
|
||||
return collection;
|
||||
},
|
||||
};
|
|
@ -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,7 +131,9 @@ const mouse = {
|
|||
const _double_click_timeout = {};
|
||||
const _drag_start_timeout = {};
|
||||
|
||||
window.onmousedown = (evn) => {
|
||||
window.addEventListener(
|
||||
"mousedown",
|
||||
(evn) => {
|
||||
const time = performance.now();
|
||||
|
||||
if (_double_click_timeout[evn.button]) {
|
||||
|
@ -178,9 +183,11 @@ window.onmousedown = (evn) => {
|
|||
|
||||
mouse.buttons[evn.button] = time;
|
||||
|
||||
mouse._contexts.forEach(({target, name, buttons}) => {
|
||||
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);
|
||||
|
@ -196,18 +203,25 @@ window.onmousedown = (evn) => {
|
|||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
},
|
||||
{
|
||||
passive: false,
|
||||
}
|
||||
);
|
||||
|
||||
window.onmouseup = (evn) => {
|
||||
window.addEventListener(
|
||||
"mouseup",
|
||||
(evn) => {
|
||||
const time = performance.now();
|
||||
|
||||
mouse._contexts.forEach(({target, name, buttons}) => {
|
||||
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,
|
||||
|
@ -267,9 +281,13 @@ window.onmouseup = (evn) => {
|
|||
delete _drag_start_timeout[evn.button];
|
||||
}
|
||||
mouse.buttons[evn.button] = null;
|
||||
};
|
||||
},
|
||||
{passive: false}
|
||||
);
|
||||
|
||||
window.onmousemove = (evn) => {
|
||||
window.addEventListener(
|
||||
"mousemove",
|
||||
(evn) => {
|
||||
mouse._contexts.forEach((context) => {
|
||||
const target = context.target;
|
||||
const name = context.name;
|
||||
|
@ -353,7 +371,9 @@ window.onmousemove = (evn) => {
|
|||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
},
|
||||
{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,7 +177,6 @@ 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,
|
||||
|
@ -190,33 +189,9 @@ const _reticle_draw = (evn, snapToGrid = true) => {
|
|||
ovCtx.lineWidth = 1;
|
||||
ovCtx.strokeStyle = "#FFF";
|
||||
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); //origin is middle of the frame
|
||||
}
|
||||
// TEMP
|
||||
ovCtx.fillStyle = "#0FF";
|
||||
ovCtx.beginPath();
|
||||
ovCtx.arc(evn.x, evn.y, 5, 0, Math.PI * 2);
|
||||
ovCtx.fill();
|
||||
};
|
||||
|
||||
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();
|
|
@ -150,7 +150,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,256 @@
|
|||
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, {
|
||||
bb,
|
||||
after: imgLayer,
|
||||
});
|
||||
|
||||
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, 0, 0);
|
||||
});
|
||||
};
|
||||
|
||||
const stopMarchingAnts = march(bb);
|
||||
|
||||
// First Dream Run
|
||||
let stopProgress = _monitorProgress(bb);
|
||||
images.push(...(await _dream(endpoint, requestCopy)));
|
||||
stopProgress();
|
||||
|
||||
// Cleans up
|
||||
const clean = () => {
|
||||
stopMarchingAnts();
|
||||
imageCollection.inputElement.removeChild(imageSelectMenu);
|
||||
imageCollection.deleteLayer(layer);
|
||||
blockNewImages = false;
|
||||
};
|
||||
|
||||
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", () => {
|
||||
at--;
|
||||
if (at < 0) at = images.length - 1;
|
||||
|
||||
imageindextxt.textContent = `${at + 1}/${images.length}`;
|
||||
redraw();
|
||||
});
|
||||
imageSelectMenu.appendChild(backbtn);
|
||||
imageSelectMenu.appendChild(imageindextxt);
|
||||
|
||||
const nextbtn = document.createElement("button");
|
||||
nextbtn.textContent = ">";
|
||||
nextbtn.title = "Next Image";
|
||||
nextbtn.addEventListener("click", () => {
|
||||
at++;
|
||||
if (at >= images.length) at = 0;
|
||||
|
||||
imageindextxt.textContent = `${at + 1}/${images.length}`;
|
||||
redraw();
|
||||
});
|
||||
imageSelectMenu.appendChild(nextbtn);
|
||||
|
||||
const morebtn = document.createElement("button");
|
||||
morebtn.textContent = "+";
|
||||
morebtn.title = "Generate More";
|
||||
morebtn.addEventListener("click", async () => {
|
||||
let stopProgress = _monitorProgress(bb);
|
||||
images.push(...(await _dream(endpoint, requestCopy)));
|
||||
stopProgress();
|
||||
|
||||
imageindextxt.textContent = `${at + 1}/${images.length}`;
|
||||
});
|
||||
imageSelectMenu.appendChild(morebtn);
|
||||
|
||||
const acceptbtn = document.createElement("button");
|
||||
acceptbtn.textContent = "Y";
|
||||
acceptbtn.title = "Apply Current";
|
||||
acceptbtn.addEventListener("click", async () => {
|
||||
commands.runCommand("drawImage", "Image Dream", {
|
||||
x: bb.x,
|
||||
y: bb.y,
|
||||
image: layer.canvas,
|
||||
});
|
||||
clean();
|
||||
});
|
||||
imageSelectMenu.appendChild(acceptbtn);
|
||||
|
||||
const discardbtn = document.createElement("button");
|
||||
discardbtn.textContent = "N";
|
||||
discardbtn.title = "Cancel";
|
||||
discardbtn.addEventListener("click", async () => {
|
||||
clean();
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
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 +270,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 +280,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 +349,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 +396,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,
|
||||
|
@ -233,7 +481,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 +496,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 +521,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 +580,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,8 +611,8 @@ 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,
|
||||
|
@ -394,13 +643,13 @@ const img2imgTool = () =>
|
|||
bb.w,
|
||||
state.keepBorderSize
|
||||
);
|
||||
console.debug("hey");
|
||||
}
|
||||
|
||||
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,10 +23,6 @@ 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";
|
||||
|
||||
|
@ -39,14 +35,9 @@ const _mask_brush_draw_callback = (evn, state) => {
|
|||
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";
|
||||
|
||||
|
@ -59,7 +50,6 @@ const _mask_brush_erase_callback = (evn, state) => {
|
|||
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,8 +105,8 @@ const maskBrushTool = () =>
|
|||
state.preview = false;
|
||||
|
||||
state.movecb = (evn) => {
|
||||
if (evn.target.id === "overlayCanvas") {
|
||||
// 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";
|
||||
|
@ -129,11 +119,10 @@ const maskBrushTool = () =>
|
|||
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) => {
|
||||
|
@ -185,10 +185,10 @@ const selectTransformTool = () =>
|
|||
|
||||
// Mouse move handelr. 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) {
|
||||
|
@ -281,7 +281,7 @@ const selectTransformTool = () =>
|
|||
|
||||
// Change cursor
|
||||
if (cursorInHandle || state.selected.contains(evn.x, evn.y))
|
||||
ovCanvas.style.cursor = "pointer";
|
||||
imageCollection.inputElement.style.cursor = "pointer";
|
||||
}
|
||||
|
||||
// Draw current cursor location
|
||||
|
@ -294,12 +294,10 @@ const selectTransformTool = () =>
|
|||
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(
|
||||
|
@ -322,12 +320,10 @@ const selectTransformTool = () =>
|
|||
|
||||
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) {
|
||||
|
@ -355,12 +351,10 @@ const selectTransformTool = () =>
|
|||
// 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) {
|
||||
|
@ -426,41 +420,31 @@ const selectTransformTool = () =>
|
|||
state.dragging = null;
|
||||
}
|
||||
redraw();
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for right clicks. Basically resets everything
|
||||
state.cancelcb = (evn) => {
|
||||
if (evn.target.id === "overlayCanvas") {
|
||||
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
|
||||
);
|
||||
commands.runCommand("eraseImage", "Erase Area", state.selected);
|
||||
state.selected = null;
|
||||
redraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Register Ctrl-C/V Shortcut
|
||||
|
||||
// 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");
|
||||
|
||||
|
@ -501,7 +485,6 @@ const selectTransformTool = () =>
|
|||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handles pasting
|
||||
|
|
|
@ -5,12 +5,12 @@ const stampTool = () =>
|
|||
(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.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) {
|
||||
|
@ -32,9 +32,9 @@ const stampTool = () =>
|
|||
},
|
||||
(state, opt) => {
|
||||
// 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;
|
||||
|
@ -153,7 +153,6 @@ const stampTool = () =>
|
|||
};
|
||||
|
||||
state.movecb = (evn) => {
|
||||
if (evn.target && evn.target.id === "overlayCanvas") {
|
||||
let x = evn.x;
|
||||
let y = evn.y;
|
||||
if (state.snapToGrid) {
|
||||
|
@ -163,6 +162,8 @@ const stampTool = () =>
|
|||
|
||||
state.lastMouseMove = evn;
|
||||
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
|
||||
// Draw selected image
|
||||
if (state.selected) {
|
||||
ovCtx.drawImage(state.selected.image, x, y);
|
||||
|
@ -178,11 +179,9 @@ const stampTool = () =>
|
|||
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) {
|
||||
|
@ -208,10 +207,8 @@ const stampTool = () =>
|
|||
state.back = null;
|
||||
backfn({message: "Returning from stamp", pasted: true});
|
||||
}
|
||||
}
|
||||
};
|
||||
state.cancelcb = (evn) => {
|
||||
if (evn.target.id === "overlayCanvas") {
|
||||
state.selectResource(null);
|
||||
|
||||
if (state.back) {
|
||||
|
@ -220,7 +217,6 @@ const stampTool = () =>
|
|||
state.back = null;
|
||||
backfn({message: "Returning from stamp", pasted: false});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
52
js/util.d.ts
vendored
52
js/util.d.ts
vendored
|
@ -1,52 +0,0 @@
|
|||
/**
|
||||
* Generates a random string in the following format:
|
||||
*
|
||||
* xxxx-xxxx-xxxx-...-xxxx
|
||||
*
|
||||
* @param size number of character quartets to generate
|
||||
* @return Generated ID
|
||||
*/
|
||||
declare function guid(size: number): string;
|
||||
|
||||
/**
|
||||
* Sets default values for options parameters
|
||||
*
|
||||
* @param options An object received as a parameter
|
||||
* @param defaults An object with default values for each expected key
|
||||
* @return The original options parameter
|
||||
*/
|
||||
declare function defaultOpt(
|
||||
options: {[key: string]: any},
|
||||
defaults: {[key: string]: any}
|
||||
): {[key: string]: any};
|
||||
|
||||
/**
|
||||
* Sets default values for options parameters
|
||||
*
|
||||
* @param options An object received as a parameter
|
||||
* @param defaults An object with default values for each expected key
|
||||
* @return The original options parameter
|
||||
*/
|
||||
declare function makeReadOnly(
|
||||
options: {[key: string]: any},
|
||||
defaults: {[key: string]: any}
|
||||
): {[key: string]: any};
|
||||
|
||||
/**
|
||||
* Makes an object read-only, throwing an exception when attempting to set
|
||||
*
|
||||
* @param obj Object to be proxied
|
||||
* @param name Name of the object, for logging purposes
|
||||
* @return The proxied object
|
||||
*/
|
||||
declare function makeReadOnly(obj: object, name?: string): object;
|
||||
|
||||
/**
|
||||
* Makes an object have each key be writeable only once, throwing an exception when
|
||||
* attempting to set an existing parameter
|
||||
*
|
||||
* @param obj Object to be proxied
|
||||
* @param name Name of the object, for logging purposes
|
||||
* @return The proxied object
|
||||
*/
|
||||
declare function makeWriteOnce(obj: object, name?: string): object;
|
Loading…
Reference in a new issue