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:
Victor Seiji Hariki 2022-11-29 17:55:25 -03:00
parent 4e27770284
commit 44cf9c0e70
26 changed files with 1549 additions and 1346 deletions

View file

@ -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;

View file

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

View file

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

View file

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

View file

@ -48,7 +48,6 @@ var stableDiffusionData = {
};
// stuff things use
var blockNewImages = false;
var returnedImages;
var imageIndex = 0;
var tmpImgXYWH = {};
@ -57,15 +56,6 @@ var url = "/sdapi/v1/";
var endpoint = "txt2img";
var frameX = 512;
var frameY = 512;
var prevMouseX = 0;
var prevMouseY = 0;
var mouseX = 0;
var mouseY = 0;
var canvasX = 0;
var canvasY = 0;
var heldButton = 0;
var snapX = 0;
var snapY = 0;
var drawThis = {};
const basePixelCount = 64; //64 px - ALWAYS 64 PX
var scaleFactor = 8; //x64 px
@ -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)">&lt;</button><button onclick="nextImg(this)">&gt;</button><span class="strokeText" id="currentImgIndex"></span><span class="strokeText"> of </span><span class="strokeText" id="totalImgIndex"></span><button onclick="accept(this)">Y</button><button onclick="reject(this)">N</button><button onclick="resource(this)">RES</button><span class="strokeText" id="estRemaining"></span>';
document.getElementById("tempDiv").appendChild(div);
document.getElementById("currentImgIndex").innerText = "1";
document.getElementById("totalImgIndex").innerText = totalImagesReturned;
};
// 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) => {
evn.preventDefault();
});
imageCollection.element.addEventListener(
"wheel",
(evn) => {
evn.preventDefault();
},
{passive: false}
);
imageCollection.element.addEventListener(
"contextmenu",
(evn) => {
evn.preventDefault();
},
{passive: false}
);

View file

@ -106,7 +106,7 @@ const infinity = {
},
};
infinity._init();
//infinity._init();
Array.from(document.getElementsByClassName("display-canvas")).forEach(
(canvas) => infinity.registerViewport(canvas)
);

View file

@ -0,0 +1,29 @@
// info div, sometimes hidden
let mouseXInfo = document.getElementById("mouseX");
let mouseYInfo = document.getElementById("mouseY");
let canvasXInfo = document.getElementById("canvasX");
let canvasYInfo = document.getElementById("canvasY");
let snapXInfo = document.getElementById("snapX");
let snapYInfo = document.getElementById("snapY");
let heldButtonInfo = document.getElementById("heldButton");
mouse.listen.window.onmousemove.on((evn) => {
mouseXInfo.textContent = evn.x;
mouseYInfo.textContent = evn.y;
});
mouse.listen.world.onmousemove.on((evn) => {
canvasXInfo.textContent = evn.x;
canvasYInfo.textContent = evn.y;
snapXInfo.textContent = 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();
};

View 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;
});

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -177,46 +177,21 @@ 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
// 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();

View file

@ -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
View file

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

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

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

View file

@ -1,5 +1,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,45 +611,45 @@ 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
);
// 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;
auxCtx.fillRect(
0,
bb.h - state.keepBorderSize,
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);

View file

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

View file

@ -5,16 +5,16 @@ const selectTransformTool = () =>
(state, opt) => {
// Draw new cursor immediately
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb({...mouse.coords.canvas.pos, target: {id: "overlayCanvas"}});
state.movecb(mouse.coords.world.pos);
// Canvas left mouse handlers
mouse.listen.canvas.onmousemove.on(state.movecb);
mouse.listen.canvas.btn.left.onclick.on(state.clickcb);
mouse.listen.canvas.btn.left.ondragstart.on(state.dragstartcb);
mouse.listen.canvas.btn.left.ondragend.on(state.dragendcb);
mouse.listen.world.onmousemove.on(state.movecb);
mouse.listen.world.btn.left.onclick.on(state.clickcb);
mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb);
mouse.listen.world.btn.left.ondragend.on(state.dragendcb);
// Canvas right mouse handler
mouse.listen.canvas.btn.right.onclick.on(state.cancelcb);
mouse.listen.world.btn.right.onclick.on(state.cancelcb);
// Keyboard click handlers
keyboard.listen.onkeyclick.on(state.keyclickcb);
@ -29,12 +29,12 @@ const selectTransformTool = () =>
},
(state, opt) => {
// Clear all those listeners and shortcuts we set up
mouse.listen.canvas.onmousemove.clear(state.movecb);
mouse.listen.canvas.btn.left.onclick.clear(state.clickcb);
mouse.listen.canvas.btn.left.ondragstart.clear(state.dragstartcb);
mouse.listen.canvas.btn.left.ondragend.clear(state.dragendcb);
mouse.listen.world.onmousemove.clear(state.movecb);
mouse.listen.world.btn.left.onclick.clear(state.clickcb);
mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb);
mouse.listen.world.btn.left.ondragend.clear(state.dragendcb);
mouse.listen.canvas.btn.right.onclick.clear(state.cancelcb);
mouse.listen.world.btn.right.onclick.clear(state.cancelcb);
keyboard.listen.onkeyclick.clear(state.keyclickcb);
keyboard.listen.onkeydown.clear(state.keydowncb);
@ -46,7 +46,7 @@ const selectTransformTool = () =>
state.reset();
// Resets cursor
ovCanvas.style.cursor = "auto";
imageCollection.inputElement.style.cursor = "auto";
},
{
init: (state) => {
@ -185,274 +185,259 @@ 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) {
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 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();
}
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 +445,45 @@ const selectTransformTool = () =>
// Handles copying
state.ctrlccb = (evn, cut = false) => {
if (state.selected && state.lastMouseTarget.id === "overlayCanvas") {
// We create a new canvas to store the data
state.clipboard.copy = document.createElement("canvas");
// We create a new canvas to store the data
state.clipboard.copy = document.createElement("canvas");
state.clipboard.copy.width = state.selected.w;
state.clipboard.copy.height = state.selected.h;
state.clipboard.copy.width = state.selected.w;
state.clipboard.copy.height = state.selected.h;
const ctx = state.clipboard.copy.getContext("2d");
const ctx = state.clipboard.copy.getContext("2d");
ctx.clearRect(0, 0, state.selected.w, state.selected.h);
ctx.drawImage(
state.selected.image,
0,
0,
state.selected.image.width,
state.selected.image.height,
0,
0,
state.selected.w,
state.selected.h
);
ctx.clearRect(0, 0, state.selected.w, state.selected.h);
ctx.drawImage(
state.selected.image,
0,
0,
state.selected.image.width,
state.selected.image.height,
0,
0,
state.selected.w,
state.selected.h
);
// If cutting, we reverse the selection and erase the selection area
if (cut) {
const aux = state.original;
state.reset();
// If cutting, we reverse the selection and erase the selection area
if (cut) {
const aux = state.original;
state.reset();
commands.runCommand("eraseImage", "Cut Image", aux);
}
commands.runCommand("eraseImage", "Cut Image", aux);
}
// Because firefox needs manual activation of the feature
if (state.useClipboard) {
// Send to clipboard
state.clipboard.copy.toBlob((blob) => {
const item = new ClipboardItem({"image/png": blob});
navigator.clipboard.write([item]).catch((e) => {
console.warn("Error sending to clipboard");
console.warn(e);
});
// Because firefox needs manual activation of the feature
if (state.useClipboard) {
// Send to clipboard
state.clipboard.copy.toBlob((blob) => {
const item = new ClipboardItem({"image/png": blob});
navigator.clipboard.write([item]).catch((e) => {
console.warn("Error sending to clipboard");
console.warn(e);
});
}
});
}
};

View file

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

52
js/util.d.ts vendored
View file

@ -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;