diff --git a/index.html b/index.html index 47b3642..a16b2d5 100644 --- a/index.html +++ b/index.html @@ -325,6 +325,8 @@ + + diff --git a/js/index.js b/js/index.js index f39db49..5341df9 100644 --- a/js/index.js +++ b/js/index.js @@ -890,30 +890,22 @@ async function upscaleAndDownload() { function loadSettings() { // set default values if not set - var _prompt = - localStorage.getItem("prompt") == null - ? "ocean floor scientific expedition, underwater wildlife" - : localStorage.getItem("prompt"); - var _negprompt = - localStorage.getItem("neg_prompt") == null - ? "people, person, humans, human, divers, diver, glitch, error, text, watermark, bad quality, blurry" - : localStorage.getItem("neg_prompt"); var _mask_blur = localStorage.getItem("mask_blur") == null ? 0 : localStorage.getItem("mask_blur"); var _seed = localStorage.getItem("seed") == null ? -1 : localStorage.getItem("seed"); - var _enable_hr = Boolean( - localStorage.getItem("enable_hr") == (null || "false") + + let _enable_hr = + localStorage.getItem("enable_hr") === null ? false - : localStorage.getItem("enable_hr") - ); - var _sync_cursor_size = Boolean( - localStorage.getItem("sync_cursor_size") == (null || "false") - ? false - : localStorage.getItem("sync_cursor_size") - ); + : localStorage.getItem("enable_hr") === "true"; + + let _sync_cursor_size = + localStorage.getItem("sync_cursor_size") === null + ? true + : localStorage.getItem("sync_cursor_size") === "true"; // set the values into the UI document.getElementById("maskBlur").value = Number(_mask_blur); diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index 4094e85..ae5b974 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -126,7 +126,10 @@ const viewport = { return (window.innerHeight * 1) / this.zoom; }, viewToCanvas(x, y) { - return {x, y}; + return { + x: this.cx + this.w * (x / window.innerWidth - 0.5), + y: this.cy + this.h * (y / window.innerHeight - 0.5), + }; }, canvasToView(x, y) { return { @@ -185,7 +188,7 @@ mouse.listen.window.onwheel.on((evn) => { }); mouse.listen.window.btn.middle.onpaintstart.on((evn) => { - worldInit = {x: viewport.cx, y: viewport.cy}; + if (evn.evn.ctrlKey) worldInit = {x: viewport.cx, y: viewport.cy}; }); mouse.listen.window.btn.middle.onpaint.on((evn) => { diff --git a/js/lib/input.d.js b/js/lib/input.d.js index 4c5e420..07ae2b3 100644 --- a/js/lib/input.d.js +++ b/js/lib/input.d.js @@ -86,8 +86,8 @@ * * @typedef MouseCoordContext * @property {{[key: string]: MouseCoordContextDragInfo}} dragging Information about mouse button drags - * @property {{x: number, y: number}} prev Previous mouse position - * @property {{x: number, y: number}} pos Current mouse position + * @property {Point} prev Previous mouse position + * @property {Point} pos Current mouse position */ /* Here are keyboard-related types */ diff --git a/js/lib/ui.js b/js/lib/ui.js index a835567..28c1b1d 100644 --- a/js/lib/ui.js +++ b/js/lib/ui.js @@ -235,7 +235,7 @@ function createAutoComplete(name, wrapper, options = {}) { onchange: new Observer(), get value() { - const v = this._selectedOptions.map((opt) => opt.value); + const v = Array.from(this._selectedOptions).map((opt) => opt.value); return options.multiple ? v : v[0]; }, set value(values) { diff --git a/js/lib/util.js b/js/lib/util.js index d2af3f3..83a014e 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -1,17 +1,58 @@ /** * Some type definitions before the actual code */ + /** - * Represents a simple bounding box + * Simple Point Coordinate * - * @typedef BoundingBox - * @type {Object} - * @property {number} x - Leftmost coordinate of the box - * @property {number} y - Topmost coordinate of the box - * @property {number} w - The bounding box Width - * @property {number} h - The bounding box Height + * @typedef Point + * @property {number} x - x coordinate + * @property {number} y - y coordinate */ +/** + * Represents a simple bouding box + */ +class BoundingBox { + x = 0; + y = 0; + w = 0; + h = 0; + + constructor({x, y, w, h} = {x: 0, y: 0, w: 0, h: 0}) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + } + + contains(x, y) { + return ( + this.x < x && this.y < y && x < this.x + this.w && y < this.y + this.h + ); + } + + /** + * Gets bounding box from two points + * + * @param {Point} start Coordinate + * @param {Point} end + */ + static fromStartEnd(start, end) { + const minx = Math.min(start.x, end.x); + const miny = Math.min(start.y, end.y); + const maxx = Math.max(start.x, end.x); + const maxy = Math.max(start.y, end.y); + + return new BoundingBox({ + x: minx, + y: miny, + w: maxx - minx, + h: maxy - miny, + }); + } +} + /** * A simple implementation of the Observer programming pattern * @template [T=any] Message type @@ -19,28 +60,34 @@ class Observer { /** * List of handlers - * @type {Set<(msg: T) => void | Promise>} + * @type {Array<{handler: (msg: T) => void | Promise, priority: number}>} */ - _handlers = new Set(); + _handlers = []; /** * Adds a observer to the events * - * @param {(msg: T) => void | Promise} callback The function to run when receiving a message - * @returns {(msg:T) => void | Promise} The callback we received + * @param {(msg: T, state?: any) => void | Promise} callback The function to run when receiving a message + * @param {number} priority The priority level of the observer + * @param {boolean} wait If the handler must be waited for before continuing + * @returns {(msg:T, state?: any) => void | Promise} The callback we received */ - on(callback) { - this._handlers.add(callback); + on(callback, priority = 0, wait = false) { + this._handlers.push({handler: callback, priority, wait}); + this._handlers.sort((a, b) => b.priority - a.priority); return callback; } /** * Removes a observer * - * @param {(msg: T) => void | Promise} callback The function used to register the callback + * @param {(msg: T, state?: any) => void | Promise} callback The function used to register the callback * @returns {boolean} Whether the handler existed */ clear(callback) { - return this._handlers.delete(callback); + const index = this._handlers.findIndex((v) => v.handler === callback); + if (index === -1) return false; + this._handlers.splice(index, 1); + return true; } /** * Sends a message to all observers @@ -48,16 +95,23 @@ class Observer { * @param {T} msg The message to send to the observers */ async emit(msg) { - return Promise.all( - Array.from(this._handlers).map(async (handler) => { + const state = {}; + const promises = []; + for (const {handler, wait} of this._handlers) { + const run = async () => { try { - await handler(msg); + await handler(msg, state); } catch (e) { console.warn("Observer failed to run handler"); console.warn(e); } - }) - ); + }; + + if (wait) await run(); + else promises.push(run()); + } + + return Promise.all(promises); } } @@ -211,12 +265,12 @@ function getBoundingBox(cx, cy, w, h, gridSnap = null, offset = 0) { box.x = Math.round(offs.x + cx); box.y = Math.round(offs.y + cy); - return { + return new BoundingBox({ x: Math.floor(box.x - w / 2), y: Math.floor(box.y - h / 2), w: Math.round(w), h: Math.round(h), - }; + }); } class NoContentError extends Error {} @@ -236,7 +290,7 @@ function cropCanvas(sourceCanvas, options = {}) { const h = sourceCanvas.height; var imageData = sourceCanvas.getContext("2d").getImageData(0, 0, w, h); /** @type {BoundingBox} */ - const bb = {x: 0, y: 0, w: 0, h: 0}; + const bb = new BoundingBox(); let minx = w; let maxx = -1; @@ -307,7 +361,37 @@ function downloadCanvas(options = {}) { ? cropCanvas(options.canvas).canvas : options.canvas; if (croppedCanvas != null) { - link.href = croppedCanvas.toDataURL("image/png"); - link.click(); + croppedCanvas.toBlob((blob) => { + link.href = URL.createObjectURL(blob); + link.click(); + }); } } + +/** + * Makes an element in a location + * @param {string} type Element Tag + * @param {number} x X coordinate of the element + * @param {number} y Y coordinate of the element + * @param {{x: number y: offset}} offset Offset to apply to the element + * @returns + */ +const makeElement = ( + type, + x, + y, + offset = { + x: -imageCollection.inputOffset.x, + y: -imageCollection.inputOffset.y, + } +) => { + const el = document.createElement(type); + el.style.position = "absolute"; + el.style.left = `${x + offset.x}px`; + el.style.top = `${y + offset.y}px`; + + // We can use the input element to add interactible html elements in the world + imageCollection.inputElement.appendChild(el); + + return el; +}; diff --git a/js/prompt.js b/js/prompt.js index 5b9c55b..53ebf3e 100644 --- a/js/prompt.js +++ b/js/prompt.js @@ -76,9 +76,12 @@ async function getStyles() { }; // Load from local storage if set - const promptDefaultValue = localStorage.getItem("prompt") || defaultPrompt; + const storedPrompt = localStorage.getItem("prompt"); + const storedNeg = localStorage.getItem("neg_prompt"); + const promptDefaultValue = + storedPrompt === null ? defaultPrompt : storedPrompt; const negativePromptDefaultValue = - localStorage.getItem("neg_prompt") || defaultNegativePrompt; + storedNeg === null ? defaultNegativePrompt : storedNeg; promptEl.value = promptEl.title = promptDefaultValue; negativePromptEl.value = negativePromptEl.title = negativePromptDefaultValue; diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 3e6baae..590c7ef 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -101,8 +101,12 @@ const _dream = async (endpoint, request) => { } finally { generating = false; } - - return data.images; + var responseSubdata = JSON.parse(data.info); + var returnData = { + images: data.images, + seeds: responseSubdata.all_seeds, + }; + return returnData; }; /** @@ -111,15 +115,19 @@ const _dream = async (endpoint, request) => { * @param {"txt2img" | "img2img"} endpoint Endpoint to send the request to * @param {StableDiffusionRequest} request Stable diffusion request * @param {BoundingBox} bb Generated image placement location - * @param {number} [drawEvery=0.2 / request.n_iter] Percentage delta to draw progress at (by default 20% of each iteration) + * @param {object} options Options + * @param {number} [options.drawEvery=0.2 / request.n_iter] Percentage delta to draw progress at (by default 20% of each iteration) + * @param {HTMLCanvasElement} [options.keepMask=null] Whether to force keep image under fully opaque mask + * @param {number} [options.keepMaskBlur=0] Blur when applying full resolution back to the image * @returns {Promise} */ -const _generate = async ( - endpoint, - request, - bb, - drawEvery = 0.2 / request.n_iter -) => { +const _generate = async (endpoint, request, bb, options = {}) => { + defaultOpt(options, { + drawEvery: 0.2 / request.n_iter, + keepMask: null, + keepMaskBlur: 0, + }); + events.tool.dream.emit({event: "generate", request}); const requestCopy = JSON.parse(JSON.stringify(request)); @@ -129,19 +137,6 @@ const _generate = async ( if (generationAreas.has(areaid)) return; generationAreas.add(areaid); - // Makes an element in a location - const makeElement = (type, x, y) => { - const el = document.createElement(type); - el.style.position = "absolute"; - el.style.left = `${x - imageCollection.inputOffset.x}px`; - el.style.top = `${y - imageCollection.inputOffset.y}px`; - - // We can use the input element to add interactible html elements in the world - imageCollection.inputElement.appendChild(el); - - return el; - }; - // Await for queue let cancelled = false; const waitQueue = async () => { @@ -199,10 +194,95 @@ const _generate = async ( return; } + // Save masked content + let keepMaskCanvas = null; + let keepMaskCtx = null; + + if (options.keepMask) { + const visibleCanvas = uil.getVisible({ + x: bb.x - options.keepMaskBlur, + y: bb.y - options.keepMaskBlur, + w: bb.w + 2 * options.keepMaskBlur, + h: bb.h + 2 * options.keepMaskBlur, + }); + const visibleCtx = visibleCanvas.getContext("2d"); + + const ctx = options.keepMask.getContext("2d", {willReadFrequently: true}); + + // Save current image + keepMaskCanvas = document.createElement("canvas"); + keepMaskCanvas.width = options.keepMask.width; + keepMaskCanvas.height = options.keepMask.height; + + keepMaskCtx = keepMaskCanvas.getContext("2d", {willReadFrequently: true}); + + if ( + visibleCanvas.width !== keepMaskCanvas.width + 2 * options.keepMaskBlur || + visibleCanvas.height !== keepMaskCanvas.height + 2 * options.keepMaskBlur + ) { + throw new Error( + "[dream] Provided mask is not the same size as the bounding box" + ); + } + + // Cut out changing elements + const blurMaskCanvas = document.createElement("canvas"); + // A bit bigger to handle literal corner cases + blurMaskCanvas.width = bb.w + options.keepMaskBlur * 2; + blurMaskCanvas.height = bb.h + options.keepMaskBlur * 2; + const blurMaskCtx = blurMaskCanvas.getContext("2d"); + + const blurMaskData = blurMaskCtx.getImageData( + options.keepMaskBlur, + options.keepMaskBlur, + keepMaskCanvas.width, + keepMaskCanvas.height + ); + + const image = blurMaskData.data; + + const maskData = ctx.getImageData( + 0, + 0, + options.keepMask.width, + options.keepMask.height + ); + + const mask = maskData.data; + + for (let i = 0; i < mask.length; i += 4) { + if (mask[i] !== 0 || mask[i + 1] !== 0 || mask[i + 2] !== 0) { + // If pixel is fully black + // Set pixel as fully black here as well + image[i] = 0; + image[i + 1] = 0; + image[i + 2] = 0; + image[i + 3] = 255; + } + } + + blurMaskCtx.putImageData( + blurMaskData, + options.keepMaskBlur, + options.keepMaskBlur + ); + + visibleCtx.filter = `blur(${options.keepMaskBlur}px)`; + visibleCtx.globalCompositeOperation = "destination-out"; + visibleCtx.drawImage(blurMaskCanvas, 0, 0); + + keepMaskCtx.drawImage( + visibleCanvas, + -options.keepMaskBlur, + -options.keepMaskBlur + ); + } + // Images to select through let at = 0; /** @type {Array} */ const images = [null]; + const seeds = [-1]; /** @type {HTMLDivElement} */ let imageSelectMenu = null; // Layer for the images @@ -215,16 +295,32 @@ const _generate = async ( layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height); if (!url) return; - const image = new Image(); - image.src = "data:image/png;base64," + url; - image.addEventListener("load", () => { + const img = new Image(); + img.src = "data:image/png;base64," + url; + img.addEventListener("load", () => { + const canvas = document.createElement("canvas"); + canvas.width = bb.w; + canvas.height = bb.h; + + // Creates new canvas for blurred mask + const blurMaskCanvas = document.createElement("canvas"); + blurMaskCanvas.width = bb.w + options.keepMaskBlur * 2; + blurMaskCanvas.height = bb.h + options.keepMaskBlur * 2; + + const ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, bb.w, bb.h); + + if (keepMaskCanvas) { + ctx.drawImage(keepMaskCanvas, 0, 0); + } + layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height); layer.ctx.drawImage( - image, + canvas, 0, 0, - image.width, - image.height, + canvas.width, + canvas.height, bb.x, bb.y, bb.w, @@ -241,7 +337,8 @@ const _generate = async ( fetch(`${host}${url}interrupt`, {method: "POST"}); interruptButton.disabled = true; }); - const stopMarchingAnts = march(bb); + const marchingOptions = {}; + const stopMarchingAnts = march(bb, marchingOptions); // First Dream Run console.info(`[dream] Generating images for prompt '${request.prompt}'`); @@ -251,12 +348,12 @@ const _generate = async ( try { let stopDrawingStatus = false; let lastProgress = 0; - let nextCP = drawEvery; + let nextCP = options.drawEvery; stopProgress = _monitorProgress(bb, (data) => { if (stopDrawingStatus) return; if (lastProgress < nextCP && data.progress >= nextCP) { - nextCP += drawEvery; + nextCP += options.drawEvery; fetch(`${host}${url}progress?skip_current_image=false`).then( async (response) => { if (stopDrawingStatus) return; @@ -269,7 +366,9 @@ const _generate = async ( }); imageCollection.inputElement.appendChild(interruptButton); - images.push(...(await _dream(endpoint, requestCopy))); + var dreamData = await _dream(endpoint, requestCopy); + images.push(...dreamData.images); + seeds.push(...dreamData.seeds); stopDrawingStatus = true; at = 1; } catch (e) { @@ -289,6 +388,8 @@ const _generate = async ( if (at < 0) at = images.length - 1; imageindextxt.textContent = `${at}/${images.length - 1}`; + var seed = seeds[at]; + seedbtn.title = "Use seed " + seed; redraw(); }; @@ -297,6 +398,8 @@ const _generate = async ( if (at >= images.length) at = 0; imageindextxt.textContent = `${at}/${images.length - 1}`; + var seed = seeds[at]; + seedbtn.title = "Use seed " + seed; redraw(); }; @@ -307,12 +410,22 @@ const _generate = async ( // load the image data after defining the closure img.src = "data:image/png;base64," + images[at]; img.addEventListener("load", () => { + const canvas = document.createElement("canvas"); + canvas.width = bb.w; + canvas.height = bb.h; + const ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, bb.w, bb.h); + + if (keepMaskCanvas) { + ctx.drawImage(keepMaskCanvas, 0, 0); + } + commands.runCommand("drawImage", "Image Dream", { x: bb.x, y: bb.y, w: bb.w, h: bb.h, - image: img, + image: canvas, }); clean(true); }); @@ -324,7 +437,14 @@ const _generate = async ( stopProgress = _monitorProgress(bb); interruptButton.disabled = false; imageCollection.inputElement.appendChild(interruptButton); - images.push(...(await _dream(endpoint, requestCopy))); + if (requestCopy.seed != -1) { + requestCopy.seed = + parseInt(requestCopy.seed) + + requestCopy.batch_size * requestCopy.n_iter; + } + dreamData = await _dream(endpoint, requestCopy); + images.push(...dreamData.images); + seeds.push(...dreamData.seeds); imageindextxt.textContent = `${at}/${images.length - 1}`; } catch (e) { alert( @@ -403,6 +523,76 @@ const _generate = async ( keyboard.listen.onkeyclick.on(onarrow); + // For handling mouse events for navigation + const onmovehandler = mouse.listen.world.onmousemove.on( + (evn, state) => { + const contains = bb.contains(evn.x, evn.y); + + if (!contains && !state.dream_processed) + imageCollection.inputElement.style.cursor = "auto"; + if (!contains || state.dream_processed) marchingOptions.style = "#FFF"; + + if (!state.dream_processed && contains) { + marchingOptions.style = "#F55"; + + imageCollection.inputElement.style.cursor = "pointer"; + + state.dream_processed = true; + } + }, + 0, + true + ); + + const onclickhandler = mouse.listen.world.btn.left.onclick.on( + (evn, state) => { + if (!state.dream_processed && bb.contains(evn.x, evn.y)) { + applyImg(); + imageCollection.inputElement.style.cursor = "auto"; + state.dream_processed = true; + } + }, + 1, + true + ); + const oncancelhandler = mouse.listen.world.btn.right.onclick.on( + (evn, state) => { + if (!state.dream_processed && bb.contains(evn.x, evn.y)) { + discardImg(); + imageCollection.inputElement.style.cursor = "auto"; + state.dream_processed = true; + } + }, + 1, + true + ); + const onmorehandler = mouse.listen.world.btn.middle.onclick.on( + (evn, state) => { + if ( + !state.dream_processed && + bb.contains(evn.x, evn.y) && + !evn.evn.ctrlKey + ) { + makeMore(); + state.dream_processed = true; + } + }, + 1, + true + ); + const onwheelhandler = mouse.listen.world.onwheel.on( + (evn, state) => { + if (evn.evn.ctrlKey) return; + if (!state.dream_processed && bb.contains(evn.x, evn.y)) { + if (evn.delta < 0) nextImg(); + else prevImg(); + state.dream_processed = true; + } + }, + 1, + true + ); + // Cleans up const clean = (removeBrushMask = false) => { if (removeBrushMask) { @@ -414,6 +604,13 @@ const _generate = async ( keyboard.listen.onkeyclick.clear(onarrow); // Remove area from no-generate list generationAreas.delete(areaid); + + // Stop handling inputs + mouse.listen.world.onmousemove.clear(onmovehandler); + mouse.listen.world.btn.left.onclick.clear(onclickhandler); + mouse.listen.world.btn.right.onclick.clear(oncancelhandler); + mouse.listen.world.btn.middle.onclick.clear(onmorehandler); + mouse.listen.world.onwheel.clear(onwheelhandler); }; redraw(); @@ -468,7 +665,10 @@ const _generate = async ( // 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"); + const response = prompt( + "Enter new resource name", + "Dream Resource " + seeds[at] + ); if (response) { tools.stamp.state.addResource(response, img); redraw(); // Redraw to avoid strange cursor behavior @@ -485,6 +685,14 @@ const _generate = async ( }); imageSelectMenu.appendChild(savebtn); + const seedbtn = document.createElement("button"); + seedbtn.textContent = "U"; + seedbtn.title = "Use seed " + `${seeds[at]}`; + seedbtn.addEventListener("click", () => { + sendSeed(seeds[at]); + }); + imageSelectMenu.appendChild(seedbtn); + nextQueue(initialQ); }; @@ -494,19 +702,14 @@ const _generate = async ( * @param {*} evn * @param {*} state */ -const dream_generate_callback = async (evn, state) => { - const bb = getBoundingBox( - evn.x, - evn.y, - state.cursorSize, - state.cursorSize, - state.snapToGrid && basePixelCount - ); - +const dream_generate_callback = async (bb, resolution, state) => { // Build request to the API const request = {}; Object.assign(request, stableDiffusionData); + request.width = resolution.w; + request.height = resolution.h; + // Load prompt (maybe we should add some events so we don't have to do this) request.prompt = document.getElementById("prompt").value; request.negative_prompt = document.getElementById("negPrompt").value; @@ -522,16 +725,21 @@ const dream_generate_callback = async (evn, state) => { // Use img2img if not // Temporary canvas for init image and mask generation - const auxCanvas = document.createElement("canvas"); - auxCanvas.width = request.width; - auxCanvas.height = request.height; - const auxCtx = auxCanvas.getContext("2d"); + const bbCanvas = document.createElement("canvas"); + bbCanvas.width = bb.w; + bbCanvas.height = bb.h; + const bbCtx = bbCanvas.getContext("2d"); - auxCtx.fillStyle = "#000F"; + const reqCanvas = document.createElement("canvas"); + reqCanvas.width = request.width; + reqCanvas.height = request.height; + const reqCtx = reqCanvas.getContext("2d"); + + bbCtx.fillStyle = "#000F"; // Get init image - auxCtx.fillRect(0, 0, request.width, request.height); - auxCtx.drawImage( + reqCtx.fillRect(0, 0, request.width, request.height); + reqCtx.drawImage( visibleCanvas, 0, 0, @@ -542,16 +750,16 @@ const dream_generate_callback = async (evn, state) => { request.width, request.height ); - request.init_images = [auxCanvas.toDataURL()]; + request.init_images = [reqCanvas.toDataURL()]; // Get mask image - auxCtx.fillStyle = "#000F"; - auxCtx.fillRect(0, 0, request.width, request.height); + bbCtx.fillStyle = "#000F"; + bbCtx.fillRect(0, 0, bb.w, bb.h); if (state.invertMask) { // overmasking by definition is entirely pointless with an inverted mask outpaint // since it should explicitly avoid brushed masks too, we just won't even bother - auxCtx.globalCompositeOperation = "destination-in"; - auxCtx.drawImage( + bbCtx.globalCompositeOperation = "destination-in"; + bbCtx.drawImage( maskPaintCanvas, bb.x, bb.y, @@ -559,47 +767,27 @@ const dream_generate_callback = async (evn, state) => { bb.h, 0, 0, - request.width, - request.height + bb.w, + bb.h ); - auxCtx.globalCompositeOperation = "destination-in"; - auxCtx.drawImage( - visibleCanvas, - 0, - 0, - bb.w, - bb.h, - 0, - 0, - request.width, - request.height - ); + bbCtx.globalCompositeOperation = "destination-in"; + bbCtx.drawImage(visibleCanvas, 0, 0); } else { - auxCtx.globalCompositeOperation = "destination-in"; - auxCtx.drawImage( - visibleCanvas, - 0, - 0, - bb.w, - bb.h, - 0, - 0, - request.width, - request.height - ); + bbCtx.globalCompositeOperation = "destination-in"; + bbCtx.drawImage(visibleCanvas, 0, 0); // here's where to overmask to avoid including the brushed mask // 99% of my issues were from failing to set source-over for the overmask blotches if (state.overMaskPx > 0) { // transparent to white first - auxCtx.globalCompositeOperation = "destination-atop"; - auxCtx.fillStyle = "#FFFF"; - auxCtx.fillRect(0, 0, request.width, request.height); - applyOvermask(auxCanvas, auxCtx, state.overMaskPx); + bbCtx.globalCompositeOperation = "destination-atop"; + bbCtx.fillStyle = "#FFFF"; + bbCtx.fillRect(0, 0, bb.w, bb.h); + applyOvermask(bbCanvas, bbCtx, state.overMaskPx); } - auxCtx.globalCompositeOperation = "destination-out"; // ??? - auxCtx.drawImage( + bbCtx.globalCompositeOperation = "destination-out"; // ??? + bbCtx.drawImage( maskPaintCanvas, bb.x, bb.y, @@ -607,26 +795,37 @@ const dream_generate_callback = async (evn, state) => { bb.h, 0, 0, - request.width, - request.height + bb.w, + bb.h ); } - auxCtx.globalCompositeOperation = "destination-atop"; - auxCtx.fillStyle = "#FFFF"; - auxCtx.fillRect(0, 0, request.width, request.height); - request.mask = auxCanvas.toDataURL(); + + bbCtx.globalCompositeOperation = "destination-atop"; + bbCtx.fillStyle = "#FFFF"; + bbCtx.fillRect(0, 0, bb.w, bb.h); + + reqCtx.clearRect(0, 0, reqCanvas.width, reqCanvas.height); + reqCtx.drawImage( + bbCanvas, + 0, + 0, + bb.w, + bb.h, + 0, + 0, + request.width, + request.height + ); + request.mask = reqCanvas.toDataURL(); + // Dream - _generate("img2img", request, bb); + _generate("img2img", request, bb, { + keepMask: state.keepMasked ? bbCanvas : null, + keepMaskBlur: state.keepMaskedBlur, + }); } }; -const dream_erase_callback = (evn, state) => { - const bb = getBoundingBox( - evn.x, - evn.y, - state.cursorSize, - state.cursorSize, - state.snapToGrid && basePixelCount - ); +const dream_erase_callback = (bb) => { commands.runCommand("eraseImage", "Erase Area", bb); }; @@ -672,15 +871,7 @@ function applyOvermask(canvas, ctx, px) { /** * Image to Image */ -const dream_img2img_callback = (evn, state) => { - const bb = getBoundingBox( - evn.x, - evn.y, - state.cursorSize, - state.cursorSize, - state.snapToGrid && basePixelCount - ); - +const dream_img2img_callback = (bb, resolution, state) => { // Get visible pixels const visibleCanvas = uil.getVisible(bb); @@ -691,6 +882,9 @@ const dream_img2img_callback = (evn, state) => { const request = {}; Object.assign(request, stableDiffusionData); + request.width = resolution.w; + request.height = resolution.h; + request.denoising_strength = state.denoisingStrength; request.inpainting_fill = 1; // For img2img use original @@ -701,33 +895,23 @@ const dream_img2img_callback = (evn, state) => { // Use img2img // Temporary canvas for init image and mask generation - const auxCanvas = document.createElement("canvas"); - auxCanvas.width = request.width; - auxCanvas.height = request.height; - const auxCtx = auxCanvas.getContext("2d"); + const bbCanvas = document.createElement("canvas"); + bbCanvas.width = bb.w; + bbCanvas.height = bb.h; + const bbCtx = bbCanvas.getContext("2d"); - auxCtx.fillStyle = "#000F"; + bbCtx.fillStyle = "#000F"; // Get init image - auxCtx.fillRect(0, 0, request.width, request.height); - auxCtx.drawImage( - visibleCanvas, - 0, - 0, - bb.w, - bb.h, - 0, - 0, - request.width, - request.height - ); - request.init_images = [auxCanvas.toDataURL()]; + bbCtx.fillRect(0, 0, bb.w, bb.h); + bbCtx.drawImage(visibleCanvas, 0, 0); + request.init_images = [bbCanvas.toDataURL()]; // Get mask image - auxCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F"; - auxCtx.fillRect(0, 0, request.width, request.height); - auxCtx.globalCompositeOperation = "destination-out"; - auxCtx.drawImage( + bbCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F"; + bbCtx.fillRect(0, 0, bb.w, bb.h); + bbCtx.globalCompositeOperation = "destination-out"; + bbCtx.drawImage( maskPaintCanvas, bb.x, bb.y, @@ -739,30 +923,30 @@ const dream_img2img_callback = (evn, state) => { request.height ); - auxCtx.globalCompositeOperation = "destination-atop"; - auxCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF"; - auxCtx.fillRect(0, 0, request.width, request.height); + bbCtx.globalCompositeOperation = "destination-atop"; + bbCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF"; + bbCtx.fillRect(0, 0, request.width, request.height); // Border Mask if (state.keepBorderSize > 0) { - auxCtx.globalCompositeOperation = "source-over"; - auxCtx.fillStyle = "#000F"; + bbCtx.globalCompositeOperation = "source-over"; + bbCtx.fillStyle = "#000F"; if (state.gradient) { - const lg = auxCtx.createLinearGradient(0, 0, state.keepBorderSize, 0); + const lg = bbCtx.createLinearGradient(0, 0, state.keepBorderSize, 0); lg.addColorStop(0, "#000F"); lg.addColorStop(1, "#0000"); - auxCtx.fillStyle = lg; + bbCtx.fillStyle = lg; } - auxCtx.fillRect(0, 0, state.keepBorderSize, request.height); + bbCtx.fillRect(0, 0, state.keepBorderSize, request.height); if (state.gradient) { - const tg = auxCtx.createLinearGradient(0, 0, 0, state.keepBorderSize); + const tg = bbCtx.createLinearGradient(0, 0, 0, state.keepBorderSize); tg.addColorStop(0, "#000F"); tg.addColorStop(1, "#0000"); - auxCtx.fillStyle = tg; + bbCtx.fillStyle = tg; } - auxCtx.fillRect(0, 0, request.width, state.keepBorderSize); + bbCtx.fillRect(0, 0, request.width, state.keepBorderSize); if (state.gradient) { - const rg = auxCtx.createLinearGradient( + const rg = bbCtx.createLinearGradient( request.width, 0, request.width - state.keepBorderSize, @@ -770,16 +954,16 @@ const dream_img2img_callback = (evn, state) => { ); rg.addColorStop(0, "#000F"); rg.addColorStop(1, "#0000"); - auxCtx.fillStyle = rg; + bbCtx.fillStyle = rg; } - auxCtx.fillRect( + bbCtx.fillRect( request.width - state.keepBorderSize, 0, state.keepBorderSize, request.height ); if (state.gradient) { - const bg = auxCtx.createLinearGradient( + const bg = bbCtx.createLinearGradient( 0, request.height, 0, @@ -787,9 +971,9 @@ const dream_img2img_callback = (evn, state) => { ); bg.addColorStop(0, "#000F"); bg.addColorStop(1, "#0000"); - auxCtx.fillStyle = bg; + bbCtx.fillStyle = bg; } - auxCtx.fillRect( + bbCtx.fillRect( 0, request.height - state.keepBorderSize, request.width, @@ -797,102 +981,19 @@ const dream_img2img_callback = (evn, state) => { ); } - request.mask = auxCanvas.toDataURL(); + request.mask = bbCanvas.toDataURL(); request.inpaint_full_res = state.fullResolution; // Dream - _generate("img2img", request, bb); + _generate("img2img", request, bb, { + keepMask: state.keepMasked ? bbCanvas : null, + keepMaskBlur: state.keepMaskedBlur, + }); }; /** * Dream and img2img tools */ -const _reticle_draw = (evn, state, tool, style = {}) => { - defaultOpt(style, { - sizeTextStyle: "#FFF5", - toolTextStyle: "#FFF5", - reticleWidth: 1, - reticleStyle: "#FFF", - }); - - const bb = getBoundingBox( - evn.x, - evn.y, - state.cursorSize, - state.cursorSize, - state.snapToGrid && basePixelCount - ); - const bbvp = { - ...viewport.canvasToView(bb.x, bb.y), - w: viewport.zoom * bb.w, - h: viewport.zoom * bb.h, - }; - - uiCtx.save(); - - // draw targeting square reticle thingy cursor - uiCtx.lineWidth = style.reticleWidth; - uiCtx.strokeStyle = style.reticleStyle; - uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h); //origin is middle of the frame - - uiCtx.font = `bold 20px Open Sans`; - - // Draw Tool Name - { - const xshrink = Math.min(1, (bbvp.w - 20) / uiCtx.measureText(tool).width); - - uiCtx.font = `bold ${20 * xshrink}px Open Sans`; - - uiCtx.textAlign = "left"; - uiCtx.fillStyle = style.toolTextStyle; - uiCtx.fillText( - tool, - bbvp.x + 10, - bbvp.y + 10 + 20 * xshrink, - state.cursorSize - ); - } - - // Draw width and height - { - uiCtx.textAlign = "center"; - uiCtx.fillStyle = style.sizeTextStyle; - uiCtx.translate(bbvp.x + bbvp.w / 2, bbvp.y + bbvp.h / 2); - const xshrink = Math.min( - 1, - (bbvp.w - 30) / uiCtx.measureText(`${state.cursorSize}px`).width - ); - const yshrink = Math.min( - 1, - (bbvp.h - 30) / uiCtx.measureText(`${state.cursorSize}px`).width - ); - uiCtx.font = `bold ${20 * xshrink}px Open Sans`; - uiCtx.fillText( - `${state.cursorSize}px`, - 0, - bbvp.h / 2 - 10 * xshrink, - state.cursorSize - ); - uiCtx.rotate(-Math.PI / 2); - uiCtx.font = `bold ${20 * yshrink}px Open Sans`; - uiCtx.fillText( - `${state.cursorSize}px`, - 0, - bbvp.h / 2 - 10 * yshrink, - state.cursorSize - ); - - uiCtx.restore(); - } - - return () => { - uiCtx.save(); - - uiCtx.clearRect(bbvp.x - 10, bbvp.y - 10, bbvp.w + 20, bbvp.h + 20); - - uiCtx.restore(); - }; -}; /** * Generic wheel handler @@ -930,9 +1031,24 @@ const dreamTool = () => // Start Listeners mouse.listen.world.onmousemove.on(state.mousemovecb); mouse.listen.world.onwheel.on(state.wheelcb); + mouse.listen.world.btn.left.onclick.on(state.dreamcb); mouse.listen.world.btn.right.onclick.on(state.erasecb); + // Select Region listeners + mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb); + mouse.listen.world.btn.left.ondrag.on(state.dragcb); + mouse.listen.world.btn.left.ondragend.on(state.dragendcb); + + mouse.listen.world.onmousemove.on(state.smousemovecb, 2, true); + mouse.listen.world.onwheel.on(state.swheelcb, 2, true); + mouse.listen.world.btn.left.onclick.on(state.sdreamcb, 2, true); + mouse.listen.world.btn.right.onclick.on(state.serasecb, 2, true); + mouse.listen.world.btn.middle.onclick.on(state.smiddlecb, 2, true); + + // Clear Selection + state.selection.deselect(); + // Display Mask setMask(state.invertMask ? "hold" : "clear"); @@ -945,9 +1061,24 @@ const dreamTool = () => // Clear Listeners mouse.listen.world.onmousemove.clear(state.mousemovecb); mouse.listen.world.onwheel.clear(state.wheelcb); + mouse.listen.world.btn.left.onclick.clear(state.dreamcb); mouse.listen.world.btn.right.onclick.clear(state.erasecb); + // Clear Select Region listeners + mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb); + mouse.listen.world.btn.left.ondrag.clear(state.dragcb); + mouse.listen.world.btn.left.ondragend.clear(state.dragendcb); + + mouse.listen.world.onmousemove.clear(state.smousemovecb); + mouse.listen.world.onwheel.clear(state.swheelcb); + mouse.listen.world.btn.left.onclick.clear(state.sdreamcb); + mouse.listen.world.btn.right.onclick.clear(state.serasecb); + mouse.listen.world.btn.middle.onclick.clear(state.smiddlecb); + + // Clear Selection + state.selection.deselect(); + // Hide Mask setMask("none"); @@ -962,8 +1093,12 @@ const dreamTool = () => state.cursorSize = 512; state.snapToGrid = true; state.invertMask = false; + state.keepMasked = true; + state.keepMaskedBlur = 8; state.overMaskPx = 0; + state.erasePrevCursor = () => + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.erasePrevReticle = () => uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); @@ -971,31 +1106,171 @@ const dreamTool = () => ...mouse.coords.world.pos, }; + /** + * Selection handlers + */ + const selection = _tool._draggable_selection(state); + state.dragstartcb = (evn) => selection.dragstartcb(evn); + state.dragcb = (evn) => selection.dragcb(evn); + state.dragendcb = (evn) => selection.dragendcb(evn); + state.smousemovecb = (evn, estate) => { + selection.smousemovecb(evn); + if (selection.inside) { + imageCollection.inputElement.style.cursor = "pointer"; + + estate.dream_processed = true; + } else { + imageCollection.inputElement.style.cursor = "auto"; + } + }; + state.swheelcb = (evn, estate) => { + if (selection.inside) { + state.wheelcb(evn, {}); + estate.dream_processed = true; + } + }; + + state.sdreamcb = (evn, estate) => { + if (selection.exists && !selection.inside) { + selection.deselect(); + state.redraw(); + estate.selection_processed = true; + } + if (selection.inside) { + state.dreamcb(evn, {}); + estate.dream_processed = true; + } + }; + + state.serasecb = (evn, estate) => { + if (selection.inside) { + selection.deselect(); + state.redraw(); + estate.dream_processed = true; + } + }; + state.smiddlecb = (evn, estate) => { + if (selection.inside) { + estate.dream_processed = true; + } + }; + + state.selection = selection; + + /** + * Dream Handlers + */ state.mousemovecb = (evn) => { state.lastMouseMove = evn; + + state.erasePrevCursor(); state.erasePrevReticle(); + + let x = evn.x; + let y = evn.y; + if (state.snapToGrid) { + x += snap(evn.x, 0, 64); + y += snap(evn.y, 0, 64); + } + + state.erasePrevReticle = _tool._cursor_draw(x, y); + + if (state.selection.exists) { + const bb = state.selection.bb; + + const style = + state.cursorSize > stableDiffusionData.width + ? "#FBB5" + : state.cursorSize < stableDiffusionData.width + ? "#BFB5" + : "#FFF5"; + + state.erasePrevReticle = _tool._reticle_draw( + bb, + "Dream", + { + w: Math.round( + bb.w * (stableDiffusionData.width / state.cursorSize) + ), + h: Math.round( + bb.h * (stableDiffusionData.height / state.cursorSize) + ), + }, + { + reticleStyle: state.selection.inside ? "#F55" : "#FFF", + sizeTextStyle: style, + } + ); + return; + } + const style = state.cursorSize > stableDiffusionData.width ? "#FBB5" : state.cursorSize < stableDiffusionData.width ? "#BFB5" : "#FFF5"; - state.erasePrevReticle = _reticle_draw(evn, state, "Dream", { - sizeTextStyle: style, - }); + state.erasePrevReticle = _tool._reticle_draw( + getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ), + "Dream", + { + w: stableDiffusionData.width, + h: stableDiffusionData.height, + }, + { + sizeTextStyle: style, + } + ); }; state.redraw = () => { state.mousemovecb(state.lastMouseMove); }; - state.wheelcb = (evn) => { + state.wheelcb = (evn, estate) => { + if (estate.dream_processed) return; _dream_onwheel(evn, state); }; - state.dreamcb = (evn) => { - dream_generate_callback(evn, state); + state.dreamcb = (evn, estate) => { + if (estate.dream_processed || estate.selection_processed) return; + const bb = + state.selection.bb || + getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ); + const resolution = state.selection.bb || { + w: stableDiffusionData.width, + h: stableDiffusionData.height, + }; + dream_generate_callback(bb, resolution, state); + state.selection.deselect(); + }; + state.erasecb = (evn, estate) => { + if (state.selection.exists) { + state.selection.deselect(); + state.redraw(); + return; + } + if (estate.dream_processed) return; + const bb = getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ); + dream_erase_callback(bb, state); }; - state.erasecb = (evn) => dream_erase_callback(evn, state); }, populateContextMenu: (menu, state) => { if (!state.ctxmenu) { @@ -1040,6 +1315,35 @@ const dreamTool = () => } ).label; + // Keep Masked Content Checkbox + state.ctxmenu.keepMaskedLabel = _toolbar_input.checkbox( + state, + "keepMasked", + "Keep Masked", + () => { + if (state.keepMasked) { + state.ctxmenu.keepMaskedBlurSlider.classList.remove( + "invisible" + ); + } else { + state.ctxmenu.keepMaskedBlurSlider.classList.add("invisible"); + } + } + ).label; + + // Keep Masked Content Blur Slider + state.ctxmenu.keepMaskedBlurSlider = _toolbar_input.slider( + state, + "keepMaskedBlur", + "Keep Masked Blur", + { + min: 0, + max: 64, + step: 4, + textStep: 1, + } + ).slider; + // Overmasking Slider state.ctxmenu.overMaskPxLabel = _toolbar_input.slider( state, @@ -1059,6 +1363,8 @@ const dreamTool = () => menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.invertMaskLabel); menu.appendChild(document.createElement("br")); + menu.appendChild(state.ctxmenu.keepMaskedLabel); + menu.appendChild(state.ctxmenu.keepMaskedBlurSlider); menu.appendChild(state.ctxmenu.overMaskPxLabel); }, shortcut: "D", @@ -1079,9 +1385,24 @@ const img2imgTool = () => // Start Listeners mouse.listen.world.onmousemove.on(state.mousemovecb); mouse.listen.world.onwheel.on(state.wheelcb); + mouse.listen.world.btn.left.onclick.on(state.dreamcb); mouse.listen.world.btn.right.onclick.on(state.erasecb); + // Select Region listeners + mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb); + mouse.listen.world.btn.left.ondrag.on(state.dragcb); + mouse.listen.world.btn.left.ondragend.on(state.dragendcb); + + mouse.listen.world.onmousemove.on(state.smousemovecb, 2, true); + mouse.listen.world.onwheel.on(state.swheelcb, 2, true); + mouse.listen.world.btn.left.onclick.on(state.sdreamcb, 2, true); + mouse.listen.world.btn.right.onclick.on(state.serasecb, 2, true); + mouse.listen.world.btn.middle.onclick.on(state.smiddlecb, 2, true); + + // Clear Selection + state.selection.deselect(); + // Display Mask setMask(state.invertMask ? "hold" : "clear"); @@ -1094,9 +1415,24 @@ const img2imgTool = () => // Clear Listeners mouse.listen.world.onmousemove.clear(state.mousemovecb); mouse.listen.world.onwheel.clear(state.wheelcb); + mouse.listen.world.btn.left.onclick.clear(state.dreamcb); mouse.listen.world.btn.right.onclick.clear(state.erasecb); + // Clear Select Region listeners + mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb); + mouse.listen.world.btn.left.ondrag.clear(state.dragcb); + mouse.listen.world.btn.left.ondragend.clear(state.dragendcb); + + mouse.listen.world.onmousemove.clear(state.smousemovecb); + mouse.listen.world.onwheel.clear(state.swheelcb); + mouse.listen.world.btn.left.onclick.clear(state.sdreamcb); + mouse.listen.world.btn.right.onclick.clear(state.serasecb); + mouse.listen.world.btn.middle.onclick.clear(state.smiddlecb); + + // Clear Selection + state.selection.deselect(); + // Hide mask setMask("none"); uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); @@ -1110,6 +1446,8 @@ const img2imgTool = () => state.cursorSize = 512; state.snapToGrid = true; state.invertMask = true; + state.keepMasked = true; + state.keepMaskedBlur = 8; state.fullResolution = false; state.denoisingStrength = 0.7; @@ -1117,39 +1455,154 @@ const img2imgTool = () => state.keepBorderSize = 64; state.gradient = true; + state.erasePrevCursor = () => + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.erasePrevReticle = () => uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.lastMouseMove = { ...mouse.coords.world.pos, }; + + /** + * Selection handlers + */ + const selection = _tool._draggable_selection(state); + state.dragstartcb = (evn) => selection.dragstartcb(evn); + state.dragcb = (evn) => selection.dragcb(evn); + state.dragendcb = (evn) => selection.dragendcb(evn); + state.smousemovecb = (evn, estate) => { + selection.smousemovecb(evn); + if (selection.inside) { + imageCollection.inputElement.style.cursor = "pointer"; + + estate.dream_processed = true; + } else { + imageCollection.inputElement.style.cursor = "auto"; + } + }; + state.swheelcb = (evn, estate) => { + if (selection.inside) { + state.wheelcb(evn, {}); + estate.dream_processed = true; + } + }; + + state.sdreamcb = (evn, estate) => { + if (selection.exists && !selection.inside) { + selection.deselect(); + state.redraw(); + estate.selection_processed = true; + } + if (selection.inside) { + state.dreamcb(evn, {}); + estate.dream_processed = true; + } + }; + + state.serasecb = (evn, estate) => { + if (selection.inside) { + state.erasecb(evn, {}); + estate.dream_processed = true; + } + }; + + state.smiddlecb = (evn, estate) => { + if (selection.inside) { + estate.dream_processed = true; + } + }; + + state.selection = selection; + + /** + * Dream handlers + */ state.mousemovecb = (evn) => { state.lastMouseMove = evn; + + state.erasePrevCursor(); state.erasePrevReticle(); - const style = - state.cursorSize > stableDiffusionData.width - ? "#FBB5" - : state.cursorSize < stableDiffusionData.width - ? "#BFB5" - : "#FFF5"; - state.erasePrevReticle = _reticle_draw(evn, state, "Img2Img", { - sizeTextStyle: style, - }); + let x = evn.x; + let y = evn.y; + if (state.snapToGrid) { + x += snap(evn.x, 0, 64); + y += snap(evn.y, 0, 64); + } - const bb = getBoundingBox( - evn.x, - evn.y, - state.cursorSize, - state.cursorSize, - state.snapToGrid && basePixelCount - ); + state.erasePrevReticle = _tool._cursor_draw(x, y); // Resolution - const request = { - width: stableDiffusionData.width, - height: stableDiffusionData.height, - }; + let bb = null; + let request = null; + + if (state.selection.exists) { + bb = state.selection.bb; + + request = {width: bb.w, height: bb.h}; + + const style = + state.cursorSize > stableDiffusionData.width + ? "#FBB5" + : state.cursorSize < stableDiffusionData.width + ? "#BFB5" + : "#FFF5"; + state.erasePrevReticle = _tool._reticle_draw( + bb, + "Img2Img", + { + w: Math.round( + bb.w * (stableDiffusionData.width / state.cursorSize) + ), + h: Math.round( + bb.h * (stableDiffusionData.height / state.cursorSize) + ), + }, + { + reticleStyle: state.selection.inside ? "#F55" : "#FFF", + sizeTextStyle: style, + } + ); + } else { + bb = getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ); + + request = { + width: stableDiffusionData.width, + height: stableDiffusionData.height, + }; + + const style = + state.cursorSize > stableDiffusionData.width + ? "#FBB5" + : state.cursorSize < stableDiffusionData.width + ? "#BFB5" + : "#FFF5"; + state.erasePrevReticle = _tool._reticle_draw( + bb, + "Img2Img", + {w: request.width, h: request.height}, + { + sizeTextStyle: style, + } + ); + } + + if ( + state.selection.exists && + (state.selection.selected.now.x === + state.selection.selected.start.x || + state.selection.selected.now.y === + state.selection.selected.start.y) + ) { + return; + } const bbvp = { ...viewport.canvasToView(bb.x, bb.y), @@ -1158,15 +1611,15 @@ const img2imgTool = () => }; // For displaying border mask - const auxCanvas = document.createElement("canvas"); - auxCanvas.width = request.width; - auxCanvas.height = request.height; - const auxCtx = auxCanvas.getContext("2d"); + const bbCanvas = document.createElement("canvas"); + bbCanvas.width = request.width; + bbCanvas.height = request.height; + const bbCtx = bbCanvas.getContext("2d"); if (state.keepBorderSize > 0) { - auxCtx.fillStyle = "#6A6AFF30"; + bbCtx.fillStyle = "#6A6AFF30"; if (state.gradient) { - const lg = auxCtx.createLinearGradient( + const lg = bbCtx.createLinearGradient( 0, 0, state.keepBorderSize, @@ -1174,11 +1627,11 @@ const img2imgTool = () => ); lg.addColorStop(0, "#6A6AFF30"); lg.addColorStop(1, "#0000"); - auxCtx.fillStyle = lg; + bbCtx.fillStyle = lg; } - auxCtx.fillRect(0, 0, state.keepBorderSize, request.height); + bbCtx.fillRect(0, 0, state.keepBorderSize, request.height); if (state.gradient) { - const tg = auxCtx.createLinearGradient( + const tg = bbCtx.createLinearGradient( 0, 0, 0, @@ -1186,11 +1639,11 @@ const img2imgTool = () => ); tg.addColorStop(0, "#6A6AFF30"); tg.addColorStop(1, "#6A6AFF00"); - auxCtx.fillStyle = tg; + bbCtx.fillStyle = tg; } - auxCtx.fillRect(0, 0, request.width, state.keepBorderSize); + bbCtx.fillRect(0, 0, request.width, state.keepBorderSize); if (state.gradient) { - const rg = auxCtx.createLinearGradient( + const rg = bbCtx.createLinearGradient( request.width, 0, request.width - state.keepBorderSize, @@ -1198,16 +1651,16 @@ const img2imgTool = () => ); rg.addColorStop(0, "#6A6AFF30"); rg.addColorStop(1, "#6A6AFF00"); - auxCtx.fillStyle = rg; + bbCtx.fillStyle = rg; } - auxCtx.fillRect( + bbCtx.fillRect( request.width - state.keepBorderSize, 0, state.keepBorderSize, request.height ); if (state.gradient) { - const bg = auxCtx.createLinearGradient( + const bg = bbCtx.createLinearGradient( 0, request.height, 0, @@ -1215,16 +1668,16 @@ const img2imgTool = () => ); bg.addColorStop(0, "#6A6AFF30"); bg.addColorStop(1, "#6A6AFF00"); - auxCtx.fillStyle = bg; + bbCtx.fillStyle = bg; } - auxCtx.fillRect( + bbCtx.fillRect( 0, request.height - state.keepBorderSize, request.width, state.keepBorderSize ); uiCtx.drawImage( - auxCanvas, + bbCanvas, 0, 0, request.width, @@ -1241,13 +1694,45 @@ const img2imgTool = () => state.mousemovecb(state.lastMouseMove); }; - state.wheelcb = (evn) => { + state.wheelcb = (evn, estate) => { + if (estate.dream_processed) return; _dream_onwheel(evn, state); }; - state.dreamcb = (evn) => { - dream_img2img_callback(evn, state); + state.dreamcb = (evn, estate) => { + if (estate.dream_processed || estate.selection_processed) return; + const bb = + state.selection.bb || + getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ); + const resolution = state.selection.bb || { + w: stableDiffusionData.width, + h: stableDiffusionData.height, + }; + dream_img2img_callback(bb, resolution, state); + state.selection.deselect(); + state.redraw(); + }; + state.erasecb = (evn, estate) => { + if (estate.dream_processed) return; + if (state.selection.exists) { + state.selection.deselect(); + state.redraw(); + return; + } + const bb = getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ); + dream_erase_callback(bb, state); }; - state.erasecb = (evn) => dream_erase_callback(evn, state); }, populateContextMenu: (menu, state) => { if (!state.ctxmenu) { @@ -1292,6 +1777,47 @@ const img2imgTool = () => } ).label; + // Keep Masked Content Checkbox + state.ctxmenu.keepMaskedLabel = _toolbar_input.checkbox( + state, + "keepMasked", + "Keep Masked", + () => { + if (state.keepMasked) { + state.ctxmenu.keepMaskedBlurSlider.classList.remove( + "invisible" + ); + state.ctxmenu.keepMaskedBlurSliderLinebreak.classList.add( + "invisible" + ); + } else { + state.ctxmenu.keepMaskedBlurSlider.classList.add("invisible"); + state.ctxmenu.keepMaskedBlurSliderLinebreak.classList.remove( + "invisible" + ); + } + } + ).label; + + // Keep Masked Content Blur Slider + state.ctxmenu.keepMaskedBlurSlider = _toolbar_input.slider( + state, + "keepMaskedBlur", + "Keep Masked Blur", + { + min: 0, + max: 64, + step: 4, + textStep: 1, + } + ).slider; + + state.ctxmenu.keepMaskedBlurSliderLinebreak = + document.createElement("br"); + state.ctxmenu.keepMaskedBlurSliderLinebreak.classList.add( + "invisible" + ); + // Inpaint Full Resolution Checkbox state.ctxmenu.fullResolutionLabel = _toolbar_input.checkbox( state, @@ -1338,6 +1864,9 @@ const img2imgTool = () => menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.invertMaskLabel); menu.appendChild(document.createElement("br")); + menu.appendChild(state.ctxmenu.keepMaskedLabel); + menu.appendChild(state.ctxmenu.keepMaskedBlurSlider); + menu.appendChild(state.ctxmenu.keepMaskedBlurSliderLinebreak); menu.appendChild(state.ctxmenu.fullResolutionLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.denoisingStrengthSlider); @@ -1352,3 +1881,7 @@ window.onbeforeunload = async () => { // Stop current generation on page close if (generating) await fetch(`${host}${url}interrupt`, {method: "POST"}); }; + +const sendSeed = (seed) => { + stableDiffusionData.seed = document.getElementById("seed").value = seed; +}; diff --git a/js/ui/tool/generic.js b/js/ui/tool/generic.js new file mode 100644 index 0000000..6de0388 --- /dev/null +++ b/js/ui/tool/generic.js @@ -0,0 +1,276 @@ +/** + * File to add generic rendering functions and shared utilities + */ + +const _tool = { + /** + * Draws a reticle used for image generation + * + * @param {BoundingBox} bb The bounding box of the reticle (world space) + * @param {string} tool Name of the tool to diplay + * @param {{w: number, h: number}} resolution Resolution of generation to display + * @param {object} style Styles to use for rendering the reticle + * @param {string} [style.sizeTextStyle = "#FFF5"] Style of the text for diplaying the bounding box size. + * @param {string} [style.genSizeTextStyle = "#FFF5"] Style of the text for diplaying generation size + * @param {string} [style.toolTextStyle = "#FFF5"] Style of the text for the tool name + * @param {number} [style.reticleWidth = 1] Width of the line of the reticle + * @param {string} [style.reticleStyle = "#FFF"] Style of the line of the reticle + * + * @returns A function that erases this reticle drawing + */ + _reticle_draw(bb, tool, resolution, style = {}) { + defaultOpt(style, { + sizeTextStyle: "#FFF5", + genSizeTextStyle: "#FFF5", + toolTextStyle: "#FFF5", + reticleWidth: 1, + reticleStyle: "#FFF", + }); + + const bbvp = { + ...viewport.canvasToView(bb.x, bb.y), + w: viewport.zoom * bb.w, + h: viewport.zoom * bb.h, + }; + + uiCtx.save(); + + // draw targeting square reticle thingy cursor + uiCtx.lineWidth = style.reticleWidth; + uiCtx.strokeStyle = style.reticleStyle; + uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h); //origin is middle of the frame + + uiCtx.font = `bold 20px Open Sans`; + + // Draw Tool Name + if (bb.h > 40) { + const xshrink = Math.min( + 1, + (bbvp.w - 20) / uiCtx.measureText(tool).width + ); + + uiCtx.font = `bold ${20 * xshrink}px Open Sans`; + + uiCtx.textAlign = "left"; + uiCtx.fillStyle = style.toolTextStyle; + uiCtx.fillText(tool, bbvp.x + 10, bbvp.y + 10 + 20 * xshrink, bb.w); + } + + // Draw width and height + { + // Render Cursor Width + uiCtx.textAlign = "center"; + uiCtx.fillStyle = style.sizeTextStyle; + uiCtx.translate(bbvp.x + bbvp.w / 2, bbvp.y + bbvp.h / 2); + const xshrink = Math.min( + 1, + (bbvp.w - 30) / uiCtx.measureText(`${bb.w}px`).width + ); + const yshrink = Math.min( + 1, + (bbvp.h - 30) / uiCtx.measureText(`${bb.h}px`).width + ); + uiCtx.font = `bold ${20 * xshrink}px Open Sans`; + uiCtx.fillText(`${bb.w}px`, 0, bbvp.h / 2 - 10 * xshrink, bb.w); + + // Render Generation Width + uiCtx.fillStyle = style.genSizeTextStyle; + uiCtx.font = `bold ${10 * xshrink}px Open Sans`; + if (bb.w !== resolution.w) + uiCtx.fillText(`${resolution.w}px`, 0, bbvp.h / 2 - 30 * xshrink, bb.h); + + // Render Cursor Height + uiCtx.rotate(-Math.PI / 2); + uiCtx.fillStyle = style.sizeTextStyle; + uiCtx.font = `bold ${20 * yshrink}px Open Sans`; + uiCtx.fillText(`${bb.h}px`, 0, bbvp.w / 2 - 10 * yshrink, bb.h); + + // Render Generation Height + uiCtx.fillStyle = style.genSizeTextStyle; + uiCtx.font = `bold ${10 * yshrink}px Open Sans`; + if (bb.h !== resolution.h) + uiCtx.fillText(`${resolution.h}px`, 0, bbvp.w / 2 - 30 * xshrink, bb.h); + + uiCtx.restore(); + } + + return () => { + uiCtx.save(); + + uiCtx.clearRect(bbvp.x - 64, bbvp.y - 64, bbvp.w + 128, bbvp.h + 128); + + uiCtx.restore(); + }; + }, + + /** + * Draws a generic crosshair cursor at the specified location + * + * @param {number} x X world coordinate of the cursor + * @param {number} y Y world coordinate of the cursor + * @param {object} style Style of the lines of the cursor + * @param {string} [style.width = 3] Line width of the lines of the cursor + * @param {string} [style.style = "#FFF5"] Stroke style of the lines of the cursor + * + * @returns A function that erases this cursor drawing + */ + _cursor_draw(x, y, style = {}) { + defaultOpt(style, { + width: 3, + style: "#FFF5", + }); + const vpc = viewport.canvasToView(x, y); + + // Draw current cursor location + uiCtx.lineWidth = style.width; + uiCtx.strokeStyle = style.style; + + uiCtx.beginPath(); + uiCtx.moveTo(vpc.x, vpc.y + 10); + uiCtx.lineTo(vpc.x, vpc.y - 10); + uiCtx.moveTo(vpc.x + 10, vpc.y); + uiCtx.lineTo(vpc.x - 10, vpc.y); + uiCtx.stroke(); + return () => { + uiCtx.clearRect(vpc.x - 15, vpc.y - 15, vpc.x + 30, vpc.y + 30); + }; + }, + + /** + * Creates generic handlers for dealing with draggable selection areas + * + * @param {object} state State of the tool + * @param {boolean} state.snapToGrid Whether the cursor should snap to the grid + * @param {() => void} [state.redraw] Function to redraw the cursor + * @returns + */ + _draggable_selection(state) { + const selection = { + _inside: false, + _dirty_bb: true, + _cached_bb: null, + _selected: null, + + /** + * If the cursor is cursor is currently inside the selection + */ + get inside() { + return this._inside; + }, + + /** + * Get intermediate selection object + */ + get selected() { + return this._selected; + }, + + /** + * If the selection exists + */ + get exists() { + return !!this._selected; + }, + + /** + * Gets the selection bounding box + */ + get bb() { + if (this._dirty_bb && this._selected) { + this._cached_bb = BoundingBox.fromStartEnd( + this._selected.start, + this._selected.now + ); + this._dirty_bb = false; + } + return this._selected && this._cached_bb; + }, + + /** + * When the cursor enters the selection + */ + onenter: new Observer(), + + /** + * When the cursor leaves the selection + */ + onleave: new Observer(), + + // Utility methods + deselect() { + if (this.inside) { + this._inside = false; + this.onleave.emit({evn: null}); + } + this._selected = null; + }, + + // Dragging handlers + /** + * Drag start event handler + * + * @param {Point} evn Drag start event + */ + dragstartcb(evn) { + const x = state.snapToGrid ? evn.ix + snap(evn.ix, 0, 64) : evn.ix; + const y = state.snapToGrid ? evn.iy + snap(evn.iy, 0, 64) : evn.iy; + this._selected = {start: {x, y}, now: {x, y}}; + this._dirty_bb = true; + }, + /** + * Drag event handler + * + * @param {Point} evn Drag event + */ + dragcb(evn) { + const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; + const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; + + if (x !== this._selected.now.x || y !== this._selected.now.y) { + this._selected.now = {x, y}; + this._dirty_bb = true; + } + }, + /** + * Drag end event handler + * + * @param {Point} evn Drag end event + */ + dragendcb(evn) { + const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; + const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; + + this._selected.now = {x, y}; + this._dirty_bb = true; + + if ( + this._selected.start.x === this._selected.now.x || + this._selected.start.y === this._selected.now.y + ) { + this.deselect(); + } + }, + + /** + * Mouse move event handler + * + * @param {Point} evn Mouse move event + */ + smousemovecb(evn) { + if (!this._selected || !this.bb.contains(evn.x, evn.y)) { + if (this.inside) { + this._inside = false; + this.onleave.emit({evn}); + } + } else { + if (!this.inside) { + this._inside = true; + this.onenter.emit({evn}); + } + } + }, + }; + + return selection; + }, +}; diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 97dd684..7def02e 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -131,20 +131,10 @@ const selectTransformTool = () => ); }, handles() { - const _createHandle = (x, y, originOffset = null, size = 10) => { + const _createHandle = (x, y, originOffset = null) => { return { - x: x - size / 2, - y: y - size / 2, - w: size, - h: size, - contains(x, y) { - return ( - this.x <= x && - x <= this.x + this.w && - this.y <= y && - y <= this.y + this.h - ); - }, + x, + y, scaleTo: (tx, ty, keepAspectRatio = true) => { const origin = { x: this.original.x + this.original.w / 2, @@ -182,11 +172,13 @@ const selectTransformTool = () => }, }; }; + + const size = viewport.zoom * 10; return [ - _createHandle(this.x, this.y), - _createHandle(this.x + this.w, this.y), - _createHandle(this.x, this.y + this.h), - _createHandle(this.x + this.w, this.y + this.h), + _createHandle(this.x, this.y, size), + _createHandle(this.x + this.w, this.y, size), + _createHandle(this.x, this.y + this.h, size), + _createHandle(this.x + this.w, this.y + this.h, size), ]; }, }; @@ -295,10 +287,20 @@ const selectTransformTool = () => state.selected.handles().forEach((handle) => { const bbvph = { ...viewport.canvasToView(handle.x, handle.y), - w: viewport.zoom * handle.w, - h: viewport.zoom * handle.h, + w: 10, + h: 10, }; - if (handle.contains(evn.x, evn.y)) { + + bbvph.x -= 5; + bbvph.y -= 5; + + const inhandle = + evn.evn.clientX > bbvph.x && + evn.evn.clientX < bbvph.x + bbvph.w && + evn.evn.clientY > bbvph.y && + evn.evn.clientY < bbvph.y + bbvph.h; + + if (inhandle) { cursorInHandle = true; uiCtx.strokeRect( bbvph.x - 1, @@ -333,10 +335,11 @@ 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.original || + (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; @@ -381,9 +384,24 @@ const selectTransformTool = () => if (state.selected) { const handles = state.selected.handles(); - const activeHandle = handles.find((v) => - v.contains(evn.ix, evn.iy) - ); + const activeHandle = handles.find((v) => { + const vpc = viewport.canvasToView(v.x, v.y); + const tlc = viewport.viewToCanvas(vpc.x - 5, vpc.y - 5); + const brc = viewport.viewToCanvas(vpc.x + 5, vpc.y + 5); + const bb = { + x: tlc.x, + y: tlc.y, + w: brc.x - tlc.x, + h: brc.y - tlc.y, + }; + + return ( + evn.ix > bb.x && + evn.ix < bb.x + bb.w && + evn.iy > bb.y && + evn.iy < bb.y + bb.h + ); + }); if (activeHandle) { state.scaling = activeHandle; return;