From 0a33d3e49176c2a6d544431c093b022a912e29ca Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sun, 27 Nov 2022 09:54:34 -0300 Subject: [PATCH 01/19] Some first bases for infinity screen Signed-off-by: Victor Seiji Hariki --- js/infinity.js | 112 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 js/infinity.js diff --git a/js/infinity.js b/js/infinity.js new file mode 100644 index 0000000..3c2f696 --- /dev/null +++ b/js/infinity.js @@ -0,0 +1,112 @@ +const infinity = { + _init() { + console.info("[infinity] Loading infinity lib"); + infinity._canvas_update_size(); + + // Add event handlers + window.onresize = infinity._canvas_update_size; + + // Add draw loop + infinity._draw(); + }, + _canvas_update_size() { + // TEMPORARY + // TODO: Remove this for dynamic canvas sizing + Array.from(document.getElementsByClassName("content-canvas")).forEach( + (canvas) => { + canvas.width = 2560; + canvas.height = 1440; + } + ); + + // Update canvas size + Array.from(document.getElementsByClassName("display-canvas")).forEach( + (canvas) => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + ); + }, + + // Content Canvas Information + + // Viewport information + viewports: [], + + registerViewport: (el, options = {}) => { + defaultOpt(options, { + x: 0, + y: 0, + zoom: 1, + input: true, + }); + + // Registers a canvas as a viewport + const viewport = { + id: guid(), + canvas: el, + ctx: el.getContext("2d"), + x: options.x, + y: options.y, + zoom: options.zoom, + }; + + viewport.getBoundingBox = () => { + const w = viewport.canvas.width * viewport.zoom; + const h = viewport.canvas.height * viewport.zoom; + + return { + x: viewport.x - w / 2, + y: viewport.y - h / 2, + w, + h, + }; + }; + + infinity.viewports.push(viewport); + + // Register mouse input + const offset = {x: 0, y: 0}; + const oviewport = {x: 0, y: 0}; + mouse.listen.world.middle.onpaintstart = (evn) => { + offset.x = evn.x; + offset.y = evn.y; + oviewport.x = viewport.x; + oviewport.y = viewport.y; + }; + mouse.listen.world.middle.onpaint = (evn) => { + viewport.x = oviewport.x - (evn.x - offset.x); + viewport.y = oviewport.y - (evn.y - offset.y); + }; + + return viewport; + }, + + // Draw loop + _draw: () => { + infinity.viewports.forEach((viewport) => { + try { + const bb = viewport.getBoundingBox(); + + viewport.ctx.drawImage( + bgCanvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + bb.w, + bb.h + ); + } catch (e) {} + }); + + requestAnimationFrame(infinity._draw); + }, +}; + +infinity._init(); +Array.from(document.getElementsByClassName("display-canvas")).forEach( + (canvas) => infinity.registerViewport(canvas) +); From c1b17c1b0e9a68bc218440d972f78d67954aab36 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sun, 27 Nov 2022 09:54:34 -0300 Subject: [PATCH 02/19] Some start for layering Signed-off-by: Victor Seiji Hariki --- css/index.css | 8 ++++++++ css/layers.css | 6 ++++++ index.html | 13 +++++++++++++ js/layers.js | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 css/layers.css create mode 100644 js/layers.js diff --git a/css/index.css b/css/index.css index e57686e..54843d5 100644 --- a/css/index.css +++ b/css/index.css @@ -2,8 +2,16 @@ font-size: 100%; font-family: Arial, Helvetica, sans-serif; } + +/* Body is stuck with no scroll */ body { + width: 100%; + height: 100%; + margin: 0px; + padding: 0px; + + overflow: clip; } .container { diff --git a/css/layers.css b/css/layers.css new file mode 100644 index 0000000..2041088 --- /dev/null +++ b/css/layers.css @@ -0,0 +1,6 @@ +/* Debug floating window */ +#layer-preview .preview-canvas { + background-color: white; + width: 100%; + height: 150px; +} diff --git a/index.html b/index.html index dffa80b..6b65649 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,8 @@ + + @@ -189,6 +191,17 @@
+ +
+
Layer Debug View
+ +
+
{ + const layer = { + id: guid(), + name: layer, + // This is where black magic starts + // A proxy for the canvas object + canvas: new Proxy(document.createElement("canvas"), {}), + }; + }, + + // Deletes a layer + deleteLayer: (layer) => { + if (typeof layer === "object") { + layers._layers = layers._layers.filter((l) => l.id === layer.id); + delete layers[layer.id]; + } else if (typeof layer === "string") { + layers._layers = layers._layers.filter((l) => l.id === layer); + delete layers[layer]; + } + }, +}; From 4e2777028481fced71bc87ca7fa5f9897bbeac66 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sun, 27 Nov 2022 13:06:13 -0300 Subject: [PATCH 03/19] some layer upgrades Signed-off-by: Victor Seiji Hariki --- index.html | 2 + js/index.js | 16 +++++ js/layers.js | 180 +++++++++++++++++++++++++++++++++++++++++++++------ js/util.d.ts | 52 +++++++++++++++ 4 files changed, 230 insertions(+), 20 deletions(-) create mode 100644 js/util.d.ts diff --git a/index.html b/index.html index 6b65649..ef07ffa 100644 --- a/index.html +++ b/index.html @@ -299,6 +299,8 @@ + + diff --git a/js/index.js b/js/index.js index 3ccfe30..bf78890 100644 --- a/js/index.js +++ b/js/index.js @@ -112,6 +112,22 @@ 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(); testHostConnection(); diff --git a/js/layers.js b/js/layers.js index e7bb02b..9dff0b0 100644 --- a/js/layers.js +++ b/js/layers.js @@ -4,29 +4,169 @@ * 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 = { - _layers: [], - layers: {}, + collections: makeWriteOnce({}, "layers.collections"), - // Registers a new layer - registerLayer: (name) => { - const layer = { - id: guid(), - name: layer, - // This is where black magic starts - // A proxy for the canvas object - canvas: new Proxy(document.createElement("canvas"), {}), + // 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); }; - }, - // Deletes a layer - deleteLayer: (layer) => { - if (typeof layer === "object") { - layers._layers = layers._layers.filter((l) => l.id === layer.id); - delete layers[layer.id]; - } else if (typeof layer === "string") { - layers._layers = layers._layers.filter((l) => l.id === layer); - delete layers[layer]; - } + // 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; }, }; diff --git a/js/util.d.ts b/js/util.d.ts new file mode 100644 index 0000000..e4cb07f --- /dev/null +++ b/js/util.d.ts @@ -0,0 +1,52 @@ +/** + * 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; From 44cf9c0e701567ec94033f6eda0bef5f694d7430 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 29 Nov 2022 17:55:25 -0300 Subject: [PATCH 04/19] 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 --- css/index.css | 20 +- css/layers.css | 40 +++ css/ui/toolbar.css | 4 + index.html | 128 ++------ js/index.js | 347 +++----------------- js/infinity.js | 2 +- js/initalize/debug.populate.js | 29 ++ js/initalize/layers.populate.js | 182 +++++++++++ js/initalize/toolbar.populate.js | 27 ++ js/layers.js | 172 ---------- js/{ => lib}/commands.d.js | 0 js/{ => lib}/commands.js | 0 js/{ => lib}/input.d.js | 5 +- js/{ => lib}/input.js | 395 +++++++++++----------- js/lib/layers.js | 264 +++++++++++++++ js/{ui => lib}/toolbar.js | 57 +--- js/{ => lib}/util.js | 0 js/settingsbar.js | 2 +- js/ui/explore.js | 3 + js/ui/{ => floating}/history.js | 0 js/ui/tool/dream.d.js | 23 ++ js/ui/tool/dream.js | 373 +++++++++++++++++---- js/ui/tool/maskbrush.js | 107 +++--- js/ui/tool/select.js | 543 +++++++++++++++---------------- js/ui/tool/stamp.js | 120 ++++--- js/util.d.ts | 52 --- 26 files changed, 1549 insertions(+), 1346 deletions(-) create mode 100644 js/initalize/debug.populate.js create mode 100644 js/initalize/layers.populate.js create mode 100644 js/initalize/toolbar.populate.js delete mode 100644 js/layers.js rename js/{ => lib}/commands.d.js (100%) rename js/{ => lib}/commands.js (100%) rename js/{ => lib}/input.d.js (93%) rename js/{ => lib}/input.js (64%) create mode 100644 js/lib/layers.js rename js/{ui => lib}/toolbar.js (84%) rename js/{ => lib}/util.js (100%) create mode 100644 js/ui/explore.js rename js/ui/{ => floating}/history.js (100%) create mode 100644 js/ui/tool/dream.d.js delete mode 100644 js/util.d.ts diff --git a/css/index.css b/css/index.css index 54843d5..fb1d977 100644 --- a/css/index.css +++ b/css/index.css @@ -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; diff --git a/css/layers.css b/css/layers.css index 2041088..8675b29 100644 --- a/css/layers.css +++ b/css/layers.css @@ -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; +} diff --git a/css/ui/toolbar.css b/css/ui/toolbar.css index bb69d01..39c9341 100644 --- a/css/ui/toolbar.css +++ b/css/ui/toolbar.css @@ -9,6 +9,10 @@ background-color: var(--c-primary); } +#ui-toolbar * { + user-select: none; +} + #ui-toolbar .handle { display: flex; align-items: center; diff --git a/index.html b/index.html index ef07ffa..2bc6877 100644 --- a/index.html +++ b/index.html @@ -191,123 +191,27 @@
- -
-
Layer Debug View
- -
- -
-
-
- - -

lol ur browser sucks

-
- - -

lol ur browser sucks

-
- - -

lol ur browser sucks

-
- - -

lol ur browser sucks

-
- - -

lol ur browser sucks

-
- - -

lol ur browser sucks

-
-
- -
-
-
-
-
- - -

lol ur browser sucks

-
-
- -

lol ur browser sucks

-
-
-
-
-
+
- - - + + + + - - + + + + + @@ -315,6 +219,12 @@ - + + + diff --git a/js/index.js b/js/index.js index bf78890..4727faf 100644 --- a/js/index.js +++ b/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 = - ' of '; - - 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 = ''; - 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} +); diff --git a/js/infinity.js b/js/infinity.js index 3c2f696..f1aef79 100644 --- a/js/infinity.js +++ b/js/infinity.js @@ -106,7 +106,7 @@ const infinity = { }, }; -infinity._init(); +//infinity._init(); Array.from(document.getElementsByClassName("display-canvas")).forEach( (canvas) => infinity.registerViewport(canvas) ); diff --git a/js/initalize/debug.populate.js b/js/initalize/debug.populate.js new file mode 100644 index 0000000..20f5452 --- /dev/null +++ b/js/initalize/debug.populate.js @@ -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(); +}; diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js new file mode 100644 index 0000000..d2280b7 --- /dev/null +++ b/js/initalize/layers.populate.js @@ -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; +}); diff --git a/js/initalize/toolbar.populate.js b/js/initalize/toolbar.populate.js new file mode 100644 index 0000000..c7052df --- /dev/null +++ b/js/initalize/toolbar.populate.js @@ -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(); diff --git a/js/layers.js b/js/layers.js deleted file mode 100644 index 9dff0b0..0000000 --- a/js/layers.js +++ /dev/null @@ -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; - }, -}; diff --git a/js/commands.d.js b/js/lib/commands.d.js similarity index 100% rename from js/commands.d.js rename to js/lib/commands.d.js diff --git a/js/commands.js b/js/lib/commands.js similarity index 100% rename from js/commands.js rename to js/lib/commands.js diff --git a/js/input.d.js b/js/lib/input.d.js similarity index 93% rename from js/input.d.js rename to js/lib/input.d.js index 498bd8f..4c5e420 100644 --- a/js/input.d.js +++ b/js/lib/input.d.js @@ -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} 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 */ /** diff --git a/js/input.js b/js/lib/input.js similarity index 64% rename from js/input.js rename to js/lib/input.js index 8e1b15b..5c9608b 100644 --- a/js/input.js +++ b/js/lib/input.js @@ -64,6 +64,7 @@ const mouse = { * @param {object} options Extra options * @param {HTMLElement} [options.target=null] Target filtering * @param {Record} [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 */ diff --git a/js/lib/layers.js b/js/lib/layers.js new file mode 100644 index 0000000..6e38c9b --- /dev/null +++ b/js/lib/layers.js @@ -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; + }, +}; diff --git a/js/ui/toolbar.js b/js/lib/toolbar.js similarity index 84% rename from js/ui/toolbar.js rename to js/lib/toolbar.js index 955f6df..54a17c5 100644 --- a/js/ui/toolbar.js +++ b/js/lib/toolbar.js @@ -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(); diff --git a/js/util.js b/js/lib/util.js similarity index 100% rename from js/util.js rename to js/lib/util.js diff --git a/js/settingsbar.js b/js/settingsbar.js index f7848cd..a88fb30 100644 --- a/js/settingsbar.js +++ b/js/settingsbar.js @@ -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, diff --git a/js/ui/explore.js b/js/ui/explore.js new file mode 100644 index 0000000..212a275 --- /dev/null +++ b/js/ui/explore.js @@ -0,0 +1,3 @@ +/** + * This is a simple implementation of layer interaction + */ diff --git a/js/ui/history.js b/js/ui/floating/history.js similarity index 100% rename from js/ui/history.js rename to js/ui/floating/history.js diff --git a/js/ui/tool/dream.d.js b/js/ui/tool/dream.d.js new file mode 100644 index 0000000..24da203 --- /dev/null +++ b/js/ui/tool/dream.d.js @@ -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 + */ diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 3eb5a06..d69c200 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -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} + */ +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} + */ +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); diff --git a/js/ui/tool/maskbrush.js b/js/ui/tool/maskbrush.js index bc4af36..38a926c 100644 --- a/js/ui/tool/maskbrush.js +++ b/js/ui/tool/maskbrush.js @@ -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) diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 3b19d40..a9f4b8f 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -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); }); - } + }); } }; diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 0bb111a..8f87276 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -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}); } }; diff --git a/js/util.d.ts b/js/util.d.ts deleted file mode 100644 index e4cb07f..0000000 --- a/js/util.d.ts +++ /dev/null @@ -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; From 77380a7b535e5d59c09723e33aa218a991338197 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 29 Nov 2022 18:23:35 -0300 Subject: [PATCH 05/19] remove debug dot in the cursor Signed-off-by: Victor Seiji Hariki --- js/lib/toolbar.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/js/lib/toolbar.js b/js/lib/toolbar.js index 54a17c5..d6ae9d9 100644 --- a/js/lib/toolbar.js +++ b/js/lib/toolbar.js @@ -189,9 +189,4 @@ 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(); }; From b2d51d40d45e881c4f1f82c1f8f90681fb463fbe Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 29 Nov 2022 19:47:19 -0300 Subject: [PATCH 06/19] pixel things seem to have been fixed At least temporarily, still want to check what causes smaller canvases to break pixel perfection Signed-off-by: Victor Seiji Hariki --- js/infinity.js | 112 -------------------------------- js/initalize/layers.populate.js | 3 +- js/ui/tool/dream.js | 20 +++--- 3 files changed, 14 insertions(+), 121 deletions(-) delete mode 100644 js/infinity.js diff --git a/js/infinity.js b/js/infinity.js deleted file mode 100644 index f1aef79..0000000 --- a/js/infinity.js +++ /dev/null @@ -1,112 +0,0 @@ -const infinity = { - _init() { - console.info("[infinity] Loading infinity lib"); - infinity._canvas_update_size(); - - // Add event handlers - window.onresize = infinity._canvas_update_size; - - // Add draw loop - infinity._draw(); - }, - _canvas_update_size() { - // TEMPORARY - // TODO: Remove this for dynamic canvas sizing - Array.from(document.getElementsByClassName("content-canvas")).forEach( - (canvas) => { - canvas.width = 2560; - canvas.height = 1440; - } - ); - - // Update canvas size - Array.from(document.getElementsByClassName("display-canvas")).forEach( - (canvas) => { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - } - ); - }, - - // Content Canvas Information - - // Viewport information - viewports: [], - - registerViewport: (el, options = {}) => { - defaultOpt(options, { - x: 0, - y: 0, - zoom: 1, - input: true, - }); - - // Registers a canvas as a viewport - const viewport = { - id: guid(), - canvas: el, - ctx: el.getContext("2d"), - x: options.x, - y: options.y, - zoom: options.zoom, - }; - - viewport.getBoundingBox = () => { - const w = viewport.canvas.width * viewport.zoom; - const h = viewport.canvas.height * viewport.zoom; - - return { - x: viewport.x - w / 2, - y: viewport.y - h / 2, - w, - h, - }; - }; - - infinity.viewports.push(viewport); - - // Register mouse input - const offset = {x: 0, y: 0}; - const oviewport = {x: 0, y: 0}; - mouse.listen.world.middle.onpaintstart = (evn) => { - offset.x = evn.x; - offset.y = evn.y; - oviewport.x = viewport.x; - oviewport.y = viewport.y; - }; - mouse.listen.world.middle.onpaint = (evn) => { - viewport.x = oviewport.x - (evn.x - offset.x); - viewport.y = oviewport.y - (evn.y - offset.y); - }; - - return viewport; - }, - - // Draw loop - _draw: () => { - infinity.viewports.forEach((viewport) => { - try { - const bb = viewport.getBoundingBox(); - - viewport.ctx.drawImage( - bgCanvas, - bb.x, - bb.y, - bb.w, - bb.h, - 0, - 0, - bb.w, - bb.h - ); - } catch (e) {} - }); - - requestAnimationFrame(infinity._draw); - }, -}; - -//infinity._init(); -Array.from(document.getElementsByClassName("display-canvas")).forEach( - (canvas) => infinity.registerViewport(canvas) -); diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index d2280b7..6f49de7 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -1,7 +1,7 @@ // Layering const imageCollection = layers.registerCollection( "image", - {w: 2560, h: 1472}, + {w: 1536, h: 1536}, { name: "Image Layers", } @@ -132,6 +132,7 @@ viewport.transform(imageCollection.element); mouse.listen.window.onwheel.on((evn) => { if (evn.evn.ctrlKey) { + evn.evn.preventDefault(); const pcx = viewport.cx; const pcy = viewport.cy; if (evn.delta < 0) { diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index d69c200..d05d04b 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -117,8 +117,7 @@ const _generate = async (endpoint, request, bb) => { // Layer for the images const layer = imageCollection.registerLayer(null, { - bb, - after: imgLayer, + after: maskPaintLayer, }); const redraw = () => { @@ -126,7 +125,7 @@ const _generate = async (endpoint, request, bb) => { 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); + if (images[at]) layer.ctx.drawImage(image, bb.x, bb.y); }); }; @@ -211,12 +210,17 @@ const _generate = async (endpoint, request, bb) => { 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, + const img = new Image(); + // load the image data after defining the closure + img.src = "data:image/png;base64," + images[at]; + img.addEventListener("load", () => { + commands.runCommand("drawImage", "Image Dream", { + x: bb.x, + y: bb.y, + image: img, + }); + clean(); }); - clean(); }); imageSelectMenu.appendChild(acceptbtn); From d32e48a018531360ea85100312609e90f20229f2 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 29 Nov 2022 20:25:44 -0300 Subject: [PATCH 07/19] made canvas smaller for some tests; putting back Signed-off-by: Victor Seiji Hariki --- js/initalize/layers.populate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index 6f49de7..4fca5c9 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -1,7 +1,7 @@ // Layering const imageCollection = layers.registerCollection( "image", - {w: 1536, h: 1536}, + {w: 2560, h: 1536}, { name: "Image Layers", } From 6b0a25c15e2d8fe6f464bd1c64a4c843c9af688f Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 29 Nov 2022 20:43:13 -0300 Subject: [PATCH 08/19] remove debug message and fix snap coords Signed-off-by: Victor Seiji Hariki --- js/initalize/debug.populate.js | 4 ++-- js/ui/tool/dream.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/js/initalize/debug.populate.js b/js/initalize/debug.populate.js index 20f5452..8e40860 100644 --- a/js/initalize/debug.populate.js +++ b/js/initalize/debug.populate.js @@ -15,8 +15,8 @@ mouse.listen.window.onmousemove.on((evn) => { mouse.listen.world.onmousemove.on((evn) => { canvasXInfo.textContent = evn.x; canvasYInfo.textContent = evn.y; - snapXInfo.textContent = snap(evn.x); - snapYInfo.textContent = snap(evn.y); + snapXInfo.textContent = evn.x + snap(evn.x); + snapYInfo.textContent = evn.y + snap(evn.y); }); /** diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index d05d04b..e8e355d 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -647,7 +647,6 @@ const img2imgTool = () => bb.w, state.keepBorderSize ); - console.debug("hey"); } const tmp = ovCtx.globalAlpha; From e5597889f24f009a136774f30b817475e50190a7 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 29 Nov 2022 21:16:59 -0300 Subject: [PATCH 09/19] Add shortcuts for image choosing/fix cursor Add some shortcuts to dream image selection and fix strange cursor behavior on resource add from dream. (l/r arrow keys for navigation, enter for image select; esc for reject) Signed-off-by: Victor Seiji Hariki --- js/ui/tool/dream.js | 108 +++++++++++++++++++++++++++++++------------- js/ui/tool/stamp.js | 7 ++- 2 files changed, 83 insertions(+), 32 deletions(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index e8e355d..ff31154 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -136,12 +136,81 @@ const _generate = async (endpoint, request, bb) => { images.push(...(await _dream(endpoint, requestCopy))); stopProgress(); + // Image navigation + const prevImg = () => { + at--; + if (at < 0) at = images.length - 1; + + imageindextxt.textContent = `${at + 1}/${images.length}`; + redraw(); + }; + + const nextImg = () => { + at++; + if (at >= images.length) at = 0; + + imageindextxt.textContent = `${at + 1}/${images.length}`; + redraw(); + }; + + const applyImg = async () => { + const img = new Image(); + // load the image data after defining the closure + img.src = "data:image/png;base64," + images[at]; + img.addEventListener("load", () => { + commands.runCommand("drawImage", "Image Dream", { + x: bb.x, + y: bb.y, + image: img, + }); + clean(); + }); + }; + + const discardImg = async () => { + clean(); + }; + + // Listen for keyboard arrows + const onarrow = (evn) => { + switch (evn.target.tagName.toLowerCase()) { + case "input": + case "textarea": + case "select": + case "button": + return; // If in an input field, do not process arrow input + default: + // Do nothing + break; + } + + switch (evn.code) { + case "ArrowRight": + nextImg(); + break; + case "ArrowLeft": + prevImg(); + break; + case "Enter": + applyImg(); + break; + case "Esc": + applyImg(); + break; + default: + break; + } + }; + + keyboard.listen.onkeyclick.on(onarrow); + // Cleans up const clean = () => { stopMarchingAnts(); imageCollection.inputElement.removeChild(imageSelectMenu); imageCollection.deleteLayer(layer); blockNewImages = false; + keyboard.listen.onkeyclick.clear(onarrow); }; const makeElement = (type, x, y) => { @@ -172,26 +241,14 @@ const _generate = async (endpoint, request, bb) => { 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(); - }); + backbtn.addEventListener("click", prevImg); 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(); - }); + nextbtn.addEventListener("click", nextImg); imageSelectMenu.appendChild(nextbtn); const morebtn = document.createElement("button"); @@ -209,27 +266,13 @@ const _generate = async (endpoint, request, bb) => { const acceptbtn = document.createElement("button"); acceptbtn.textContent = "Y"; acceptbtn.title = "Apply Current"; - acceptbtn.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", () => { - commands.runCommand("drawImage", "Image Dream", { - x: bb.x, - y: bb.y, - image: img, - }); - clean(); - }); - }); + acceptbtn.addEventListener("click", applyImg); imageSelectMenu.appendChild(acceptbtn); const discardbtn = document.createElement("button"); discardbtn.textContent = "N"; discardbtn.title = "Cancel"; - discardbtn.addEventListener("click", async () => { - clean(); - }); + discardbtn.addEventListener("click", discardImg); imageSelectMenu.appendChild(discardbtn); const resourcebtn = document.createElement("button"); @@ -241,7 +284,10 @@ const _generate = async (endpoint, request, bb) => { 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); + if (response) { + tools.stamp.state.addResource(response, img); + redraw(); // Redraw to avoid strange cursor behavior + } }); }); imageSelectMenu.appendChild(resourcebtn); diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 8f87276..f5057ab 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -3,6 +3,8 @@ const stampTool = () => "res/icons/file-up.svg", "Stamp Image", (state, opt) => { + state.loaded = true; + // Draw new cursor immediately ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); state.movecb({...mouse.coords.world.pos}); @@ -31,6 +33,8 @@ const stampTool = () => } }, (state, opt) => { + state.loaded = false; + // Clear Listeners mouse.listen.world.onmousemove.clear(state.movecb); mouse.listen.world.btn.left.onclick.clear(state.drawcb); @@ -44,6 +48,7 @@ const stampTool = () => }, { init: (state) => { + state.loaded = false; state.snapToGrid = true; state.resources = []; state.selected = null; @@ -75,7 +80,7 @@ const stampTool = () => } ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); - state.movecb(state.lastMouseMove); + if (state.loaded) state.movecb(state.lastMouseMove); }; // Synchronizes resources array with the DOM From a63228a55295bb2b12f22af898a1d23f12b42151 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Wed, 30 Nov 2022 18:44:51 -0300 Subject: [PATCH 10/19] no more losing the window Signed-off-by: Victor Seiji Hariki --- js/settingsbar.js | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/js/settingsbar.js b/js/settingsbar.js index a88fb30..f71bdb1 100644 --- a/js/settingsbar.js +++ b/js/settingsbar.js @@ -1,10 +1,21 @@ function makeDraggable(element) { - const startbb = element.getBoundingClientRect(); let dragging = false; let offset = {x: 0, y: 0}; - element.style.top = startbb.y + "px"; - element.style.left = startbb.x + "px"; + const margin = 10; + + const fixPos = () => { + const dbb = element.getBoundingClientRect(); + if (dbb.left < margin) element.style.left = margin + "px"; + else if (dbb.right > window.innerWidth - margin) + element.style.left = + dbb.left + (window.innerWidth - margin - dbb.right) + "px"; + + if (dbb.top < margin) element.style.top = margin + "px"; + else if (dbb.bottom > window.innerHeight - margin) + element.style.top = + dbb.top + (window.innerHeight - margin - dbb.bottom) + "px"; + }; mouse.listen.window.btn.left.onpaintstart.on((evn) => { if ( @@ -20,14 +31,22 @@ function makeDraggable(element) { mouse.listen.window.btn.left.onpaint.on((evn) => { if (dragging) { + element.style.right = null; + element.style.bottom = null; element.style.top = evn.y - offset.y + "px"; element.style.left = evn.x - offset.x + "px"; + + fixPos(); } }); mouse.listen.window.btn.left.onpaintend.on((evn) => { dragging = false; }); + + window.addEventListener("resize", () => { + fixPos(); + }); } document.querySelectorAll(".floating-window").forEach((w) => { From 6d44db5d66e2de0df283668b7e50645bbd9eb658 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Wed, 30 Nov 2022 18:46:03 -0300 Subject: [PATCH 11/19] fixes chrome coords and canvas now starts at center Signed-off-by: Victor Seiji Hariki --- js/initalize/layers.populate.js | 45 ++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index 4fca5c9..4c16bb5 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -49,10 +49,30 @@ layers.registerCollection("mask", {name: "Mask Layers", requiresActive: true}); mouse.registerContext( "world", (evn, ctx) => { + // Fix because in chrome layerX and layerY simply doesnt work + /** @type {HTMLDivElement} */ + const target = evn.target; + + // Get element bounding rect + const bb = target.getBoundingClientRect(); + + // Get element width/height (css, cause I don't trust client sizes in chrome anymore) + const w = imageCollection.size.w; + const h = imageCollection.size.h; + + // Get cursor position + const x = evn.clientX; + const y = evn.clientY; + + // Map to layer space + const layerX = ((x - bb.left) / bb.width) * w; + const layerY = ((y - bb.top) / bb.height) * h; + + // ctx.coords.prev.x = ctx.coords.pos.x; ctx.coords.prev.y = ctx.coords.pos.y; - ctx.coords.pos.x = evn.layerX; - ctx.coords.pos.y = evn.layerY; + ctx.coords.pos.x = layerX; + ctx.coords.pos.y = layerY; }, {target: imageCollection.inputElement} ); @@ -105,25 +125,8 @@ const viewport = { }, }; -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; +viewport.cx = imageCollection.size.w / 2; +viewport.cy = imageCollection.size.h / 2; let worldInit = null; From 6f99b153204acd438cf146e0dd73c04d3efca62e Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Wed, 30 Nov 2022 18:47:30 -0300 Subject: [PATCH 12/19] general improvements for connection checking Signed-off-by: Victor Seiji Hariki --- css/index.css | 4 ++++ js/index.js | 60 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/css/index.css b/css/index.css index fb1d977..4db5e50 100644 --- a/css/index.css +++ b/css/index.css @@ -197,6 +197,10 @@ body { background-color: #dddd49; } +.host-field-wrapper .connection-status.before { + background-color: #777; +} + input#host { box-sizing: border-box; } diff --git a/js/index.js b/js/index.js index c51c209..c0c06a7 100644 --- a/js/index.js +++ b/js/index.js @@ -82,18 +82,19 @@ var marchCoords = {}; // function startup() { testHostConfiguration(); - testHostConnection(); - loadSettings(); const hostEl = document.getElementById("host"); - hostEl.onchange = () => { - host = hostEl.value.endsWith("/") - ? hostEl.value.substring(0, hostEl.value.length - 1) - : hostEl.value; - hostEl.value = host; - localStorage.setItem("host", host); - }; + testHostConnection().then((checkConnection) => { + hostEl.onchange = () => { + host = hostEl.value.endsWith("/") + ? hostEl.value.substring(0, hostEl.value.length - 1) + : hostEl.value; + hostEl.value = host; + localStorage.setItem("host", host); + checkConnection(); + }; + }); const promptEl = document.getElementById("prompt"); promptEl.oninput = () => { @@ -157,7 +158,7 @@ function testHostConfiguration() { } } -function testHostConnection() { +async function testHostConnection() { class CheckInProgressError extends Error {} const connectionIndicator = document.getElementById( @@ -174,6 +175,7 @@ function testHostConnection() { connectionIndicator.classList.remove( "cors-issue", "offline", + "before", "server-error" ); connectionIndicator.title = "Connected"; @@ -181,7 +183,12 @@ function testHostConnection() { }, error: () => { connectionIndicator.classList.add("server-error"); - connectionIndicator.classList.remove("online", "offline", "cors-issue"); + connectionIndicator.classList.remove( + "online", + "offline", + "before", + "cors-issue" + ); connectionIndicator.title = "Server is online, but is returning an error response"; connectionStatus = false; @@ -191,6 +198,7 @@ function testHostConnection() { connectionIndicator.classList.remove( "online", "offline", + "before", "server-error" ); connectionIndicator.title = @@ -202,17 +210,31 @@ function testHostConnection() { connectionIndicator.classList.remove( "cors-issue", "online", + "before", "server-error" ); connectionIndicator.title = "Server seems to be offline. Please check the console for more information."; connectionStatus = false; }, + before: () => { + connectionIndicator.classList.add("before"); + connectionIndicator.classList.remove( + "cors-issue", + "online", + "offline", + "server-error" + ); + connectionIndicator.title = "Waiting for check to complete."; + connectionStatus = false; + }, }; statuses[status] && statuses[status](); }; + setConnectionStatus("before"); + let checkInProgress = false; const checkConnection = async (notify = false) => { @@ -224,7 +246,10 @@ function testHostConnection() { var url = document.getElementById("host").value + "/startup-events"; // Attempt normal request try { - const response = await fetch(url); + /** @type {Response} */ + const response = await fetch(url, { + signal: AbortSignal.timeout(5000), + }); if (response.status === 200) { setConnectionStatus("online"); @@ -243,6 +268,7 @@ function testHostConnection() { } } catch (e) { try { + if (e instanceof DOMException) throw "offline"; // Tests if problem is CORS await fetch(url, {mode: "no-cors"}); @@ -266,7 +292,7 @@ function testHostConnection() { return status; }; - checkConnection(true); + await checkConnection(true); // On click, attempt to refresh connectionIndicator.onclick = async () => { @@ -281,8 +307,8 @@ function testHostConnection() { // Checks every 5 seconds if offline, 30 seconds if online const checkAgain = () => { setTimeout( - () => { - checkConnection(); + async () => { + await checkConnection(); checkAgain(); }, connectionStatus ? 30000 : 5000 @@ -290,6 +316,10 @@ function testHostConnection() { }; checkAgain(); + + return () => { + checkConnection().catch(() => {}); + }; } function newImage(evt) { From ce78370eb33a7027e0dbedd039231ca95198d88b Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Wed, 30 Nov 2022 18:57:40 -0300 Subject: [PATCH 13/19] fix reject shortcut and add + shortcut to dream Signed-off-by: Victor Seiji Hariki --- js/ui/tool/dream.js | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index ff31154..25a63e6 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -167,6 +167,14 @@ const _generate = async (endpoint, request, bb) => { }); }; + const makeMore = async () => { + let stopProgress = _monitorProgress(bb); + images.push(...(await _dream(endpoint, requestCopy))); + stopProgress(); + + imageindextxt.textContent = `${at + 1}/${images.length}`; + }; + const discardImg = async () => { clean(); }; @@ -184,20 +192,24 @@ const _generate = async (endpoint, request, bb) => { break; } - switch (evn.code) { - case "ArrowRight": - nextImg(); - break; - case "ArrowLeft": - prevImg(); - break; - case "Enter": - applyImg(); - break; - case "Esc": - applyImg(); + switch (evn.key) { + case "+": + makeMore(); break; default: + switch (evn.code) { + case "ArrowRight": + nextImg(); + break; + case "ArrowLeft": + prevImg(); + break; + case "Escape": + applyImg(); + break; + default: + break; + } break; } }; @@ -254,13 +266,7 @@ const _generate = async (endpoint, request, bb) => { 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}`; - }); + morebtn.addEventListener("click", makeMore); imageSelectMenu.appendChild(morebtn); const acceptbtn = document.createElement("button"); From 7394ffff57099b081ca400862328f869d768641a Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Wed, 30 Nov 2022 19:18:32 -0300 Subject: [PATCH 14/19] Add back "Enter" shortcut Signed-off-by: Victor Seiji Hariki --- js/ui/tool/dream.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 25a63e6..a515775 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -204,9 +204,12 @@ const _generate = async (endpoint, request, bb) => { case "ArrowLeft": prevImg(); break; - case "Escape": + case "Enter": applyImg(); break; + case "Escape": + discardImg(); + break; default: break; } From b9976d9e8b266d29336133a25f187f71e99fee17 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Wed, 30 Nov 2022 19:28:05 -0300 Subject: [PATCH 15/19] avoid filling history with uneventful moves Signed-off-by: Victor Seiji Hariki --- js/ui/tool/select.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index a9f4b8f..92e667b 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -183,7 +183,7 @@ const selectTransformTool = () => }; }; - // Mouse move handelr. As always, also renders cursor + // Mouse move handler. As always, also renders cursor state.movecb = (evn) => { ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); imageCollection.inputElement.style.cursor = "auto"; @@ -298,6 +298,16 @@ const selectTransformTool = () => // Handles left mouse clicks state.clickcb = (evn) => { + if ( + state.original.x === state.selected.x && + state.original.y === state.selected.y && + state.original.w === state.selected.w && + state.original.h === state.selected.h + ) { + state.reset(); + return; + } + // If something is selected, commit changes to the canvas if (state.selected) { imgCtx.drawImage( From af0fb5f198e2b8b209e08f94bb3686ed6742102e Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Wed, 30 Nov 2022 22:08:14 -0300 Subject: [PATCH 16/19] fix #63 Signed-off-by: Victor Seiji Hariki --- js/index.js | 1 - js/ui/tool/dream.js | 3 --- 2 files changed, 4 deletions(-) diff --git a/js/index.js b/js/index.js index c0c06a7..bf890f5 100644 --- a/js/index.js +++ b/js/index.js @@ -75,7 +75,6 @@ var arbitraryImageBitmap; var arbitraryImageBase64; // seriously js cmon work with me here var placingArbitraryImage = false; // for when the user has loaded an existing image from their computer var marchOffset = 0; -var stopMarching = null; var inProgress = false; var marchCoords = {}; diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index a515775..912f0ee 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -481,9 +481,6 @@ const dream_img2img_callback = (evn, state) => { // Don't allow another image until is finished blockNewImages = true; - // Setup marching ants - stopMarching = march(bb); - // Setup some basic information for SD request.width = bb.w; request.height = bb.h; From 4dafe502485ea0d87a5b3e0c9946187933866842 Mon Sep 17 00:00:00 2001 From: tim h Date: Wed, 30 Nov 2022 20:20:57 -0600 Subject: [PATCH 17/19] tiny fix to reintroduce mask removal on image accept --- js/ui/tool/dream.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 912f0ee..d305f85 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -163,7 +163,7 @@ const _generate = async (endpoint, request, bb) => { y: bb.y, image: img, }); - clean(); + clean(true); }); }; @@ -220,7 +220,20 @@ const _generate = async (endpoint, request, bb) => { keyboard.listen.onkeyclick.on(onarrow); // Cleans up - const clean = () => { + const clean = (removeBrushMask = false) => { + if (removeBrushMask) { + // don't want to put this in history, so not applicable for runCommand() + // probably a better way to do this though + var clearArea = maskPaintLayer.ctx.createImageData(bb.w, bb.h); + var clearD = clearArea.data; + for (i = 0; i < clearD.length; i += 4) { + clearD[i] = 0; + clearD[i + 1] = 0; + clearD[i + 2] = 0; + clearD[i + 3] = 0; + } + maskPaintLayer.ctx.putImageData(clearArea, bb.x, bb.y); + } stopMarchingAnts(); imageCollection.inputElement.removeChild(imageSelectMenu); imageCollection.deleteLayer(layer); From 3194f65253734d672503ef13497143301c72a58d Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Wed, 30 Nov 2022 23:50:00 -0300 Subject: [PATCH 18/19] not sure why resize was not triggering redraw Signed-off-by: Victor Seiji Hariki --- js/initalize/layers.populate.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index 4c16bb5..ca06b86 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -130,7 +130,6 @@ viewport.cy = imageCollection.size.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) => { @@ -184,3 +183,7 @@ mouse.listen.window.btn.middle.onpaint.on((evn) => { mouse.listen.window.btn.middle.onpaintend.on((evn) => { worldInit = null; }); + +window.addEventListener("resize", () => { + viewport.transform(imageCollection.element); +}); From e0eed2831c1c9ba6019bc07862eaddcab938661e Mon Sep 17 00:00:00 2001 From: tim h Date: Wed, 30 Nov 2022 21:25:06 -0600 Subject: [PATCH 19/19] so much better --- js/ui/tool/dream.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index d305f85..d980cef 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -222,17 +222,7 @@ const _generate = async (endpoint, request, bb) => { // Cleans up const clean = (removeBrushMask = false) => { if (removeBrushMask) { - // don't want to put this in history, so not applicable for runCommand() - // probably a better way to do this though - var clearArea = maskPaintLayer.ctx.createImageData(bb.w, bb.h); - var clearD = clearArea.data; - for (i = 0; i < clearD.length; i += 4) { - clearD[i] = 0; - clearD[i + 1] = 0; - clearD[i + 2] = 0; - clearD[i + 3] = 0; - } - maskPaintLayer.ctx.putImageData(clearArea, bb.x, bb.y); + maskPaintCtx.clearRect(bb.x, bb.y, bb.w, bb.h); } stopMarchingAnts(); imageCollection.inputElement.removeChild(imageSelectMenu);