From f279c8457b3a4c853bdb2941386ed17d9cf01cd3 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 12 Dec 2022 00:50:25 -0300 Subject: [PATCH 1/6] dream queueing now supported - Image navigation shortcuts work for all simultaneously (probably not optimal) This is for #89. Some things should be ironed out, such as adding a cancel button for future jobs and an order indicator maybe? Signed-off-by: Victor Seiji Hariki --- js/ui/tool/dream.js | 511 ++++++++++++++++++++++++-------------------- js/ui/tool/stamp.js | 1 + 2 files changed, 277 insertions(+), 235 deletions(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index ec9719a..37d88a2 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -1,4 +1,6 @@ let blockNewImages = false; +let generationQueue = []; +let generationAreas = new Set(); let generating = false; /** @@ -118,7 +120,50 @@ const _generate = async ( bb, drawEvery = 0.2 / request.n_iter ) => { - const requestCopy = {...request}; + const requestCopy = JSON.parse(JSON.stringify(request)); + + // Block requests to identical areas + const areaid = `${bb.x}-${bb.y}-${bb.w}-${bb.h}`; + if (generationAreas.has(areaid)) return; + generationAreas.add(areaid); + + // Await for queue + const waitQueue = async () => { + const stopQueueMarchingAnts = march(bb, {style: "#AAF"}); + + let qPromise = null; + let qResolve = null; + await new Promise((finish) => { + // Will be this request's (kind of) semaphore + qPromise = new Promise((r) => (qResolve = r)); + generationQueue.push(qPromise); + + // Wait for last generation to end + if (generationQueue.length > 1) { + (async () => { + await generationQueue[generationQueue.length - 2]; + finish(); + })(); + } else { + // If this is the first, just continue + finish(); + } + }); + + stopQueueMarchingAnts(); + + return {promise: qPromise, resolve: qResolve}; + }; + + const nextQueue = (queueEntry) => { + const generationIndex = generationQueue.findIndex( + (v) => v === queueEntry.promise + ); + generationQueue.splice(generationIndex, 1); + queueEntry.resolve(); + }; + + const initialQ = await waitQueue(); // Images to select through let at = 0; @@ -253,6 +298,7 @@ const _generate = async ( }; const makeMore = async () => { + const moreQ = await waitQueue(); try { stopProgress = _monitorProgress(bb); interruptButton.disabled = false; @@ -269,6 +315,8 @@ const _generate = async ( stopProgress(); imageCollection.inputElement.removeChild(interruptButton); } + + nextQueue(moreQ); }; const discardImg = async () => { @@ -342,8 +390,9 @@ const _generate = async ( stopMarchingAnts(); imageCollection.inputElement.removeChild(imageSelectMenu); imageCollection.deleteLayer(layer); - blockNewImages = false; keyboard.listen.onkeyclick.clear(onarrow); + // Remove area from no-generate list + generationAreas.delete(areaid); }; redraw(); @@ -414,6 +463,8 @@ const _generate = async ( saveImg(); }); imageSelectMenu.appendChild(savebtn); + + nextQueue(initialQ); }; /** @@ -423,46 +474,75 @@ const _generate = async ( * @param {*} state */ const dream_generate_callback = async (evn, state) => { - if (!blockNewImages) { - const bb = getBoundingBox( - evn.x, - evn.y, - state.cursorSize, - state.cursorSize, - state.snapToGrid && basePixelCount + const bb = getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ); + + // Build request to the API + const request = {}; + Object.assign(request, stableDiffusionData); + + // 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; + + // Get visible pixels + const visibleCanvas = uil.getVisible(bb); + + // Use txt2img if canvas is blank + if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) { + // Dream + _generate("txt2img", request, bb); + } else { + // 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"); + + auxCtx.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()]; - // Build request to the API - const request = {}; - Object.assign(request, stableDiffusionData); + // Get mask image + auxCtx.fillStyle = "#000F"; + auxCtx.fillRect(0, 0, request.width, request.height); + 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( + maskPaintCanvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + request.width, + request.height + ); - // 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; - - // Don't allow another image until is finished - blockNewImages = true; - - // Get visible pixels - const visibleCanvas = uil.getVisible(bb); - - // Use txt2img if canvas is blank - if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) { - // Dream - _generate("txt2img", request, bb); - } else { - // 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"); - - auxCtx.fillStyle = "#000F"; - - // Get init image - auxCtx.fillRect(0, 0, request.width, request.height); + auxCtx.globalCompositeOperation = "destination-in"; auxCtx.drawImage( visibleCanvas, 0, @@ -474,82 +554,48 @@ const dream_generate_callback = async (evn, state) => { request.width, request.height ); - request.init_images = [auxCanvas.toDataURL()]; - - // Get mask image - auxCtx.fillStyle = "#000F"; - auxCtx.fillRect(0, 0, request.width, request.height); - 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( - maskPaintCanvas, - bb.x, - bb.y, - bb.w, - bb.h, - 0, - 0, - request.width, - request.height - ); - - auxCtx.globalCompositeOperation = "destination-in"; - auxCtx.drawImage( - visibleCanvas, - 0, - 0, - bb.w, - bb.h, - 0, - 0, - request.width, - request.height - ); - } else { - auxCtx.globalCompositeOperation = "destination-in"; - auxCtx.drawImage( - visibleCanvas, - 0, - 0, - bb.w, - bb.h, - 0, - 0, - request.width, - request.height - ); - // 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); - } - - auxCtx.globalCompositeOperation = "destination-out"; // ??? - auxCtx.drawImage( - maskPaintCanvas, - bb.x, - bb.y, - bb.w, - bb.h, - 0, - 0, - request.width, - request.height - ); + } else { + auxCtx.globalCompositeOperation = "destination-in"; + auxCtx.drawImage( + visibleCanvas, + 0, + 0, + bb.w, + bb.h, + 0, + 0, + request.width, + request.height + ); + // 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); } - auxCtx.globalCompositeOperation = "destination-atop"; - auxCtx.fillStyle = "#FFFF"; - auxCtx.fillRect(0, 0, request.width, request.height); - request.mask = auxCanvas.toDataURL(); - // Dream - _generate("img2img", request, bb); + + auxCtx.globalCompositeOperation = "destination-out"; // ??? + auxCtx.drawImage( + maskPaintCanvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + request.width, + request.height + ); } + auxCtx.globalCompositeOperation = "destination-atop"; + auxCtx.fillStyle = "#FFFF"; + auxCtx.fillRect(0, 0, request.width, request.height); + request.mask = auxCanvas.toDataURL(); + // Dream + _generate("img2img", request, bb); } }; const dream_erase_callback = (evn, state) => { @@ -606,140 +652,135 @@ function applyOvermask(canvas, ctx, px) { * Image to Image */ const dream_img2img_callback = (evn, state) => { - if (!blockNewImages) { - const bb = getBoundingBox( - evn.x, - evn.y, - state.cursorSize, - state.cursorSize, - state.snapToGrid && basePixelCount - ); + const bb = getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ); - // Get visible pixels - const visibleCanvas = uil.getVisible(bb); + // Get visible pixels + const visibleCanvas = uil.getVisible(bb); - // Do nothing if no image exists - if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) return; + // Do nothing if no image exists + if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) return; - // Build request to the API - const request = {}; - Object.assign(request, stableDiffusionData); + // Build request to the API + const request = {}; + Object.assign(request, stableDiffusionData); - request.denoising_strength = state.denoisingStrength; - request.inpainting_fill = 1; // For img2img use original + request.denoising_strength = state.denoisingStrength; + request.inpainting_fill = 1; // For img2img use original - // 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; + // 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; - // Don't allow another image until is finished - blockNewImages = true; + // Use img2img - // 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"); - // 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"); + auxCtx.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()]; + + // Get mask image + auxCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F"; + auxCtx.fillRect(0, 0, request.width, request.height); + auxCtx.globalCompositeOperation = "destination-out"; + auxCtx.drawImage( + maskPaintCanvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + request.width, + request.height + ); + + auxCtx.globalCompositeOperation = "destination-atop"; + auxCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF"; + auxCtx.fillRect(0, 0, request.width, request.height); + + // Border Mask + if (state.keepBorderSize > 0) { + auxCtx.globalCompositeOperation = "source-over"; auxCtx.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()]; - - // Get mask image - auxCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F"; - auxCtx.fillRect(0, 0, request.width, request.height); - auxCtx.globalCompositeOperation = "destination-out"; - auxCtx.drawImage( - maskPaintCanvas, - bb.x, - bb.y, - bb.w, - bb.h, - 0, - 0, - request.width, - request.height - ); - - auxCtx.globalCompositeOperation = "destination-atop"; - auxCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF"; - auxCtx.fillRect(0, 0, request.width, request.height); - - // Border Mask - if (state.keepBorderSize > 0) { - auxCtx.globalCompositeOperation = "source-over"; - auxCtx.fillStyle = "#000F"; - if (state.gradient) { - const lg = auxCtx.createLinearGradient(0, 0, state.keepBorderSize, 0); - lg.addColorStop(0, "#000F"); - lg.addColorStop(1, "#0000"); - auxCtx.fillStyle = lg; - } - auxCtx.fillRect(0, 0, state.keepBorderSize, request.height); - if (state.gradient) { - const tg = auxCtx.createLinearGradient(0, 0, 0, state.keepBorderSize); - tg.addColorStop(0, "#000F"); - tg.addColorStop(1, "#0000"); - auxCtx.fillStyle = tg; - } - auxCtx.fillRect(0, 0, request.width, state.keepBorderSize); - if (state.gradient) { - const rg = auxCtx.createLinearGradient( - request.width, - 0, - request.width - state.keepBorderSize, - 0 - ); - rg.addColorStop(0, "#000F"); - rg.addColorStop(1, "#0000"); - auxCtx.fillStyle = rg; - } - auxCtx.fillRect( - request.width - state.keepBorderSize, - 0, - state.keepBorderSize, - request.height - ); - if (state.gradient) { - const bg = auxCtx.createLinearGradient( - 0, - request.height, - 0, - request.height - state.keepBorderSize - ); - bg.addColorStop(0, "#000F"); - bg.addColorStop(1, "#0000"); - auxCtx.fillStyle = bg; - } - auxCtx.fillRect( - 0, - request.height - state.keepBorderSize, - request.width, - state.keepBorderSize - ); + if (state.gradient) { + const lg = auxCtx.createLinearGradient(0, 0, state.keepBorderSize, 0); + lg.addColorStop(0, "#000F"); + lg.addColorStop(1, "#0000"); + auxCtx.fillStyle = lg; } - - request.mask = auxCanvas.toDataURL(); - request.inpaint_full_res = state.fullResolution; - - // Dream - _generate("img2img", request, bb); + auxCtx.fillRect(0, 0, state.keepBorderSize, request.height); + if (state.gradient) { + const tg = auxCtx.createLinearGradient(0, 0, 0, state.keepBorderSize); + tg.addColorStop(0, "#000F"); + tg.addColorStop(1, "#0000"); + auxCtx.fillStyle = tg; + } + auxCtx.fillRect(0, 0, request.width, state.keepBorderSize); + if (state.gradient) { + const rg = auxCtx.createLinearGradient( + request.width, + 0, + request.width - state.keepBorderSize, + 0 + ); + rg.addColorStop(0, "#000F"); + rg.addColorStop(1, "#0000"); + auxCtx.fillStyle = rg; + } + auxCtx.fillRect( + request.width - state.keepBorderSize, + 0, + state.keepBorderSize, + request.height + ); + if (state.gradient) { + const bg = auxCtx.createLinearGradient( + 0, + request.height, + 0, + request.height - state.keepBorderSize + ); + bg.addColorStop(0, "#000F"); + bg.addColorStop(1, "#0000"); + auxCtx.fillStyle = bg; + } + auxCtx.fillRect( + 0, + request.height - state.keepBorderSize, + request.width, + state.keepBorderSize + ); } + + request.mask = auxCanvas.toDataURL(); + request.inpaint_full_res = state.fullResolution; + + // Dream + _generate("img2img", request, bb); }; /** diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 8be2e83..938ffa2 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -181,6 +181,7 @@ const stampTool = () => saveButton.addEventListener( "click", (evn) => { + evn.stopPropagation(); const canvas = document.createElement("canvas"); canvas.width = resource.image.width; canvas.height = resource.image.height; From b71731b7ca4a34622995392856e3c1aecc7547ea Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 12 Dec 2022 10:03:05 -0300 Subject: [PATCH 2/6] stamp now uses indexed DB for resources Signed-off-by: Victor Seiji Hariki --- js/ui/tool/stamp.js | 117 +++++++++++++++++++++++++++++++------------- 1 file changed, 84 insertions(+), 33 deletions(-) diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 938ffa2..6aac513 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -92,34 +92,48 @@ const stampTool = () => if (state.loaded) state.movecb(state.lastMouseMove); }; + // Open IndexedDB connection + const IDBOpenRequest = window.indexedDB.open("stamp", 1); + // Synchronizes resources array with the DOM and Local Storage const syncResources = () => { - // Saves to local storage + // Saves to IndexedDB + /** @type {IDBDatabase} */ + const db = state.stampDB; + const resources = db + .transaction("resources", "readwrite") + .objectStore("resources"); try { - localStorage.setItem( - "tools.stamp.resources", - JSON.stringify( - state.resources - .filter((resource) => !resource.temporary) - .map((resource) => { - const canvas = document.createElement("canvas"); - canvas.width = resource.image.width; - canvas.height = resource.image.height; + const FetchKeysQuery = resources.getAllKeys(); + FetchKeysQuery.onsuccess = () => { + const keys = FetchKeysQuery.result; + keys.forEach((key) => { + if (!state.resources.find((resource) => resource.id === key)) + resources.delete(key); + }); + }; - const ctx = canvas.getContext("2d"); - ctx.drawImage(resource.image, 0, 0); + state.resources + .filter((resource) => !resource.temporary && resource.dirty) + .forEach((resource) => { + const canvas = document.createElement("canvas"); + canvas.width = resource.image.width; + canvas.height = resource.image.height; - return { - id: resource.id, - name: resource.name, - src: canvas.toDataURL(), - }; - }) - ) - ); + const ctx = canvas.getContext("2d"); + ctx.drawImage(resource.image, 0, 0); + + resources.put({ + id: resource.id, + name: resource.name, + src: canvas.toDataURL(), + }); + + resource.dirty = false; + }); } catch (e) { console.warn( - "[stamp] Failed to synchronize resources with local storage" + "[stamp] Failed to synchronize resources with IndexedDB" ); console.warn(e); } @@ -143,6 +157,7 @@ const stampTool = () => resourceTitle.style.pointerEvents = "none"; resourceTitle.addEventListener("change", () => { resource.name = resourceTitle.value; + resource.dirty = true; resourceTitle.title = resourceTitle.value; syncResources(); @@ -246,6 +261,7 @@ const stampTool = () => id, name, image, + dirty: true, temporary, }; @@ -390,7 +406,6 @@ const stampTool = () => image.onload = () => state.addResource(file.name, image, false); } }); - uploadButton.value = null; }); @@ -441,29 +456,65 @@ const stampTool = () => state.ctxmenu.resourceManager = resourceManager; state.ctxmenu.resourceList = resourceList; - // Performs resource fetch from local storage - (async () => { - const storageResources = localStorage.getItem( - "tools.stamp.resources" - ); - if (storageResources) { - const parsed = JSON.parse(storageResources); + // Performs resource fetch from IndexedDB + + IDBOpenRequest.onerror = (e) => { + console.warn("[stamp] Failed to connect to IndexedDB"); + console.warn(e); + }; + + IDBOpenRequest.onupgradeneeded = (e) => { + const db = e.target.result; + + console.debug(`[stamp] Setting up database version ${db.version}`); + + const resourcesStore = db.createObjectStore("resources", { + keyPath: "id", + }); + resourcesStore.createIndex("name", "name", {unique: false}); + }; + + IDBOpenRequest.onsuccess = async (e) => { + console.debug("[stamp] Connected to IndexedDB"); + + state.stampDB = e.target.result; + + state.stampDB.onerror = (evn) => { + console.warn(`[stamp] Database Error:`); + console.warn(evn.target.errorCode); + }; + + /** @type {IDBDatabase} */ + const db = state.stampDB; + /** @type {IDBRequest<{id: string, name: string, src: string}[]>} */ + const FetchAllTransaction = db + .transaction("resources") + .objectStore("resources") + .getAll(); + + FetchAllTransaction.onsuccess = async () => { + const data = FetchAllTransaction.result; + state.resources.push( ...(await Promise.all( - parsed.map((resource) => { + data.map((resource) => { const image = document.createElement("img"); image.src = resource.src; return new Promise((resolve, reject) => { image.onload = () => - resolve({id: resource.id, name: resource.name, image}); + resolve({ + id: resource.id, + name: resource.name, + image, + }); }); }) )) ); syncResources(); - } - })(); + }; + }; } }, populateContextMenu: (menu, state) => { From 34b995998b593aa6a12cb4fed6a5e52e294ef493 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 12 Dec 2022 10:54:26 -0300 Subject: [PATCH 3/6] fix cropCanvas bounding box Signed-off-by: Victor Seiji Hariki --- js/lib/util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/lib/util.js b/js/lib/util.js index 0f80380..b8aa607 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -232,8 +232,8 @@ function cropCanvas(sourceCanvas, options = {}) { bb.x = minx - options.border; bb.y = miny - options.border; - bb.w = maxx - minx + 2 * options.border; - bb.h = maxy - miny + 2 * options.border; + bb.w = maxx - minx + 1 + 2 * options.border; + bb.h = maxy - miny + 1 + 2 * options.border; if (maxx < 0) throw new NoContentError("Canvas has no content to crop"); From 0a73687556c64881e65bb325b1da5494e3ea9066 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 12 Dec 2022 18:25:32 -0300 Subject: [PATCH 4/6] add prompt history as suggested in #87 Signed-off-by: Victor Seiji Hariki --- css/index.css | 231 +++++++++++++++++++++++++++++++++++- index.html | 37 ++++-- js/index.js | 67 ----------- js/initalize/ui.populate.js | 16 ++- js/lib/events.js | 5 + js/lib/util.js | 27 +++++ js/prompt.js | 189 +++++++++++++++++++++++++++++ js/ui/tool/dream.js | 2 + res/icons/history.svg | 6 + res/icons/library.svg | 7 ++ res/icons/minus-square.svg | 5 + res/icons/minus.svg | 4 + res/icons/plus-square.svg | 6 + res/icons/plus.svg | 5 + 14 files changed, 526 insertions(+), 81 deletions(-) create mode 100644 js/lib/events.js create mode 100644 js/prompt.js create mode 100644 res/icons/history.svg create mode 100644 res/icons/library.svg create mode 100644 res/icons/minus-square.svg create mode 100644 res/icons/minus.svg create mode 100644 res/icons/plus-square.svg create mode 100644 res/icons/plus.svg diff --git a/css/index.css b/css/index.css index f8c634e..3a36855 100644 --- a/css/index.css +++ b/css/index.css @@ -331,13 +331,48 @@ input#host { /* Prompt Fields */ +.content.prompt { + display: flex; + align-items: stretch; +} + +.content.prompt > .inputs { + width: 200px; +} + div.prompt-wrapper { + display: flex; + + width: calc(100%); + overflow: visible; } -div.prompt-wrapper > textarea { +div.prompt-wrapper > * { + flex-shrink: 0; +} + +div.prompt-wrapper textarea { margin: 0; - padding: 0; + + border-radius: 0; + + border: none; + + z-index: 1; +} + +div.prompt-wrapper:not(:first-child) textarea { + border-top: 1px solid black; +} + +div.prompt-wrapper > textarea { + box-sizing: border-box; + width: calc(100% - 20px); + + padding: 2px; + + transition-duration: 200ms; resize: vertical; } @@ -346,6 +381,198 @@ div.prompt-wrapper > textarea:focus { width: 700px; } +div.prompt-wrapper > .prompt-indicator { + display: flex; + + cursor: help; + + width: 20px; +} + +div.prompt-wrapper:first-child > .prompt-indicator { + border-top-left-radius: 5px; +} + +div.prompt-wrapper:last-child > .prompt-indicator { + border-bottom-left-radius: 5px; +} + +div.prompt-wrapper > .prompt-indicator.positive { + background-color: #484; +} + +div.prompt-wrapper > .prompt-indicator.negative { + background-color: #844; +} +div.prompt-wrapper > .prompt-indicator.styles { + background-color: #448; +} + +div.prompt-wrapper > .prompt-indicator::after { + content: ""; + display: block; + margin: auto; + + width: 16px; + height: 16px; + + background-color: var(--c-text); + + mask-size: contain; + -webkit-mask-size: contain; +} + +div.prompt-wrapper > .prompt-indicator.positive::after { + mask-image: url("/res/icons/plus-square.svg"); + -webkit-mask-image: url("/res/icons/plus-square.svg"); +} + +div.prompt-wrapper > .prompt-indicator.negative::after { + mask-image: url("/res/icons/minus-square.svg"); + -webkit-mask-image: url("/res/icons/minus-square.svg"); +} + +div.prompt-wrapper > .prompt-indicator.styles::after { + mask-image: url("/res/icons/library.svg"); + -webkit-mask-image: url("/res/icons/library.svg"); +} + +.prompt-history-wrapper { + position: relative; + + flex-shrink: 0; + + width: 20px; +} + +.prompt-history-container { + display: flex; + + position: absolute; + + top: 0; + left: 0; + + height: 100%; +} + +#prompt-history { + width: 0px; + height: 100%; + + transition-duration: 200ms; + + background-color: #1e1e50; +} + +#prompt-history.expanded { + width: 300px; +} + +#prompt-history .entry { + display: flex; + align-items: stretch; + justify-content: stretch; + + border: 1px #fff3; + + height: 25px; +} + +#prompt-history.expanded .entry > button { + padding: 2px; + padding-left: 5px; +} + +#prompt-history .entry > button { + flex: 1; + + cursor: pointer; + + margin: 0; + border: 0; + padding: 0; + + color: var(--c-text); + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + transition-duration: 100ms; +} + +#prompt-history .entry:hover > button:not(:hover) { + flex-grow: 0; + flex-shrink: 1; + flex-basis: 20%; + width: 20%; +} + +#prompt-history .entry > button.prompt { + background-color: #484; +} + +#prompt-history .entry > button.negative { + background-color: #844; +} + +#prompt-history .entry > button.styles { + background-color: #448; +} + +#prompt-history .entry > button:hover { + filter: brightness(115%); + backdrop-filter: brightness(115%); +} + +#prompt-history .entry > button:active { + filter: brightness(150%); + backdrop-filter: brightness(150%); +} + +button.prompt-history-btn { + cursor: pointer; + + border-radius: 0; + + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + + background-color: #1e1e50; + + margin: 0; + padding: 0; + border: 0; + + width: 20px; +} + +button.prompt-history-btn::after { + content: ""; + display: block; + margin: auto; + + width: 16px; + height: 16px; + + background-color: var(--c-text); + + mask-size: contain; + -webkit-mask-size: contain; + + mask-image: url("/res/icons/history.svg"); + -webkit-mask-image: url("/res/icons/history.svg"); +} + +button.prompt-history-btn:hover { + filter: brightness(115%); +} + +button.prompt-history-btn:active { + filter: brightness(150%); +} + /* Style Field */ select > .style-select-option { position: relative; diff --git a/index.html b/index.html index 77f031d..5e2a5c2 100644 --- a/index.html +++ b/index.html @@ -51,19 +51,31 @@ -
- -
-
- +
+
+
+
+ +
+
+
+ +
+
+
+
+
- -
- +
+
+
+ +
- -
-
-
+
From f63bbafe0f9333c0de4e31c0c8ea2aa818013601 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Mon, 12 Dec 2022 18:49:53 -0300 Subject: [PATCH 6/6] add cancel button to future generations Signed-off-by: Victor Seiji Hariki --- css/ui/tool/dream.css | 2 +- js/ui/tool/dream.js | 51 +++++++++++++++++++++++++++++-------------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/css/ui/tool/dream.css b/css/ui/tool/dream.css index 784ed59..b0ac86e 100644 --- a/css/ui/tool/dream.css +++ b/css/ui/tool/dream.css @@ -1,3 +1,3 @@ -.dream-interrupt-btn { +.dream-stop-btn { width: 100px; } diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index bc96c3d..5bf1baf 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -129,10 +129,35 @@ 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 () => { const stopQueueMarchingAnts = march(bb, {style: "#AAF"}); + // Add cancel Button + const cancelButton = makeElement("button", bb.x + bb.w - 100, bb.y + bb.h); + cancelButton.classList.add("dream-stop-btn"); + cancelButton.textContent = "Cancel"; + cancelButton.addEventListener("click", () => { + cancelled = true; + imageCollection.inputElement.removeChild(cancelButton); + stopQueueMarchingAnts(); + }); + imageCollection.inputElement.appendChild(cancelButton); + let qPromise = null; let qResolve = null; await new Promise((finish) => { @@ -151,8 +176,10 @@ const _generate = async ( finish(); } }); - - stopQueueMarchingAnts(); + if (!cancelled) { + imageCollection.inputElement.removeChild(cancelButton); + stopQueueMarchingAnts(); + } return {promise: qPromise, resolve: qResolve}; }; @@ -167,30 +194,22 @@ const _generate = async ( const initialQ = await waitQueue(); + if (cancelled) { + nextQueue(initialQ); + return; + } + // Images to select through let at = 0; /** @type {Array} */ const images = [null]; /** @type {HTMLDivElement} */ let imageSelectMenu = null; - // Layer for the images const layer = imageCollection.registerLayer(null, { after: maskPaintLayer, }); - 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; - }; - const redraw = (url = images[at]) => { if (url === null) layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height); @@ -216,7 +235,7 @@ const _generate = async ( // Add Interrupt Button const interruptButton = makeElement("button", bb.x + bb.w - 100, bb.y + bb.h); - interruptButton.classList.add("dream-interrupt-btn"); + interruptButton.classList.add("dream-stop-btn"); interruptButton.textContent = "Interrupt"; interruptButton.addEventListener("click", () => { fetch(`${host}${url}interrupt`, {method: "POST"});