From 4d414186c0ef89dc69e0b18cd80b67ddeea23030 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sat, 3 Dec 2022 23:07:53 -0300 Subject: [PATCH 01/26] another fix for resource persistence Signed-off-by: Victor Seiji Hariki --- js/ui/tool/select.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index cec7c45..964d070 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -593,8 +593,10 @@ const selectTransformTool = () => createResourceButton.onclick = () => { const image = document.createElement("img"); image.src = state.selected.image.toDataURL(); - tools.stamp.state.addResource("Selection Resource", image); - tools.stamp.enable(); + image.onload = () => { + tools.stamp.state.addResource("Selection Resource", image); + tools.stamp.enable(); + }; }; actionArray.appendChild(saveSelectionButton); From 583f9245b69f46507e84ec675169b3280e21145c Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sun, 4 Dec 2022 00:22:29 -0300 Subject: [PATCH 02/26] fix eyedropper interactions and remove unused code Signed-off-by: Victor Seiji Hariki --- css/index.css | 53 ------------------ js/index.js | 116 --------------------------------------- js/lib/layers.js | 42 ++++++++------ js/ui/tool/colorbrush.js | 25 +++++++-- 4 files changed, 45 insertions(+), 191 deletions(-) diff --git a/css/index.css b/css/index.css index acfb655..64b7cc8 100644 --- a/css/index.css +++ b/css/index.css @@ -20,30 +20,6 @@ body { overflow: clip; } -.container { - position: relative; -} - -.backgroundCanvas { - background-color: #ccc; -} - -.mainHSplit { - display: grid; - grid-template-columns: 1fr; - grid-template-rows: repeat(2, 1fr); - grid-column-gap: 5px; - grid-row-gap: 5px; -} - -.uiWrapper { - display: grid; - grid-template-columns: 1fr 15fr; - grid-template-rows: 1fr; - grid-column-gap: 5px; - grid-row-gap: 5px; -} - .collapsible { background-color: rgb(0, 0, 0); color: rgb(255, 255, 255); @@ -88,28 +64,6 @@ body { cursor: auto; } -.canvasHolder { - position: relative; - width: 2560px; - height: 1440px; -} - -.mainCanvases { - position: absolute; - top: 0px; - left: 0px; - width: 2560px; - height: 1440px; -} - -.masks { - display: grid; - grid-template-columns: repeat(3, 1fr); - grid-template-rows: 1fr; - grid-column-gap: 0px; - grid-row-gap: 0px; -} - /* Mask colors for mask inversion */ /* Filters are some magic acquired at https://codepen.io/sosuke/pen/Pjoqqp */ .mask-canvas { @@ -135,13 +89,6 @@ body { brightness(103%) contrast(108%); } -.strokeText { - -webkit-text-stroke: 1px #000; - font-size: 150%; - font-weight: 600; - color: #fff; -} - .wideSelect { width: 100%; text-overflow: ellipsis; diff --git a/js/index.js b/js/index.js index da9d063..afdb7ef 100644 --- a/js/index.js +++ b/js/index.js @@ -114,7 +114,6 @@ function startup() { changeSampler(); changeMaskBlur(); changeSeed(); - changeOverMaskPx(); changeHiResFix(); } @@ -340,106 +339,10 @@ function newImage(evt) { }); } -function prevImg(evt) { - if (imageIndex == 0) { - imageIndex = totalImagesReturned; - } - changeImg(false); -} - -function nextImg(evt) { - if (imageIndex == totalImagesReturned - 1) { - imageIndex = -1; - } - changeImg(true); -} - -function changeImg(forward) { - const img = new Image(); - tempCtx.clearRect(0, 0, tempCtx.width, tempCtx.height); - img.onload = function () { - tempCtx.drawImage(img, tmpImgXYWH.x, tmpImgXYWH.y); //imgCtx for actual image, tmp for... holding? - }; - var tmpIndex = document.getElementById("currentImgIndex"); - if (forward) { - imageIndex++; - } else { - imageIndex--; - } - tmpIndex.innerText = imageIndex + 1; - // load the image data after defining the closure - img.src = "data:image/png;base64," + returnedImages[imageIndex]; //TODO need way to dream batches and select from results -} - -function removeChoiceButtons(evt) { - const element = document.getElementById("veryTempDiv"); - element.remove(); - tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height); -} - -function backupAndClearMask(x, y, w, h) { - var clearArea = maskPaintCtx.createImageData(w, h); - backupMaskChunk = maskPaintCtx.getImageData(x, y, w, h); - backupMaskX = x; - backupMaskY = y; - 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; - } - maskPaintCtx.putImageData(clearArea, x, y); -} - -function restoreBackupMask() { - // reapply mask if exists - if (backupMaskChunk != null && backupMaskX != null && backupMaskY != null) { - // backup mask data exists - var iData = new ImageData( - backupMaskChunk.data, - backupMaskChunk.height, - backupMaskChunk.width - ); - maskPaintCtx.putImageData(iData, backupMaskX, backupMaskY); - } -} - -function clearBackupMask() { - // clear backupmask - backupMaskChunk = null; - backupMaskX = null; - backupMaskY = null; -} - -function clearImgMask() { - imgCtx.clearRect(0, 0, imgCanvas.width, imgCanvas.height); -} - function clearPaintedMask() { maskPaintCtx.clearRect(0, 0, maskPaintCanvas.width, maskPaintCanvas.height); } -function placeImage() { - const img = new Image(); - img.onload = function () { - commands.runCommand("drawImage", "Image Dream", { - x: tmpImgXYWH.x, - y: tmpImgXYWH.y, - image: img, - }); - tmpImgXYWH = {}; - returnedImages = null; - }; - // load the image data after defining the closure - img.src = "data:image/png;base64," + returnedImages[imageIndex]; -} - -function sleep(ms) { - // what was this even for, anyway? - return new Promise((resolve) => setTimeout(resolve, ms)); -} - function march(bb) { const expanded = {...bb}; expanded.x--; @@ -563,10 +466,6 @@ makeSlider( makeSlider("Steps", document.getElementById("steps"), "steps", 1, 70, 5, 30, 1); -function changeSnapMode() { - snapToGrid = document.getElementById("cbxSnap").checked; -} - function changeMaskBlur() { stableDiffusionData.mask_blur = parseInt( document.getElementById("maskBlur").value @@ -579,11 +478,6 @@ function changeSeed() { localStorage.setItem("seed", stableDiffusionData.seed); } -function changeOverMaskPx() { - // overMaskPx = document.getElementById("overMaskPx").value; - // localStorage.setItem("overmask_px", overMaskPx); -} - function changeHiResFix() { stableDiffusionData.enable_hr = Boolean( document.getElementById("cbxHRFix").checked @@ -936,15 +830,6 @@ function loadSettings() { ? false : localStorage.getItem("enable_hr") ); - var _enable_erase = Boolean( - localStorage.getItem("enable_erase") == (null || "false") - ? false - : localStorage.getItem("enable_erase") - ); - var _overmask_px = - localStorage.getItem("overmask_px") == null - ? 0 - : localStorage.getItem("overmask_px"); // set the values into the UI document.getElementById("prompt").value = String(_prompt); @@ -955,7 +840,6 @@ function loadSettings() { document.getElementById("maskBlur").value = Number(_mask_blur); document.getElementById("seed").value = Number(_seed); document.getElementById("cbxHRFix").checked = Boolean(_enable_hr); - // document.getElementById("overMaskPx").value = Number(_overmask_px); } imageCollection.element.addEventListener( diff --git a/js/lib/layers.js b/js/lib/layers.js index cede377..908ae27 100644 --- a/js/lib/layers.js +++ b/js/lib/layers.js @@ -77,8 +77,6 @@ const layers = { size, resolution: options.resolution, - active: null, - /** * Registers a new layer * @@ -87,6 +85,7 @@ const layers = { * @param {string} options.name * @param {?BoundingBox} options.bb * @param {{w: number, h: number}} options.resolution + * @param {?string} options.group * @param {object} options.after * @returns */ @@ -101,9 +100,12 @@ const layers = { // Bounding box for layer bb: {x: 0, y: 0, w: collection.size.w, h: collection.size.h}, - // Bounding box for layer + // Resolution for layer resolution: null, + // Group for the layer ("group/subgroup/subsubgroup") + group: null, + // If set, will insert the layer after the given one after: null, }); @@ -168,6 +170,24 @@ const layers = { canvas, ctx, + /** + * Moves this layer to another level (after given layer) + * + * @param {Layer} layer Will move layer to after this one + */ + moveAfter(layer) { + layer.canvas.after(this.canvas); + }, + + /** + * Moves this layer to another level (before given layer) + * + * @param {Layer} layer Will move layer to before this one + */ + moveBefore(layer) { + layer.canvas.before(this.canvas); + }, + /** * Moves this layer to another location * @@ -204,14 +224,8 @@ const layers = { unhide() { this.canvas.style.display = "block"; }, - - // Activates this layer - activate() { - collection.active = this; - }, }, - _layerlogpath, - ["active"] + _layerlogpath ); // Add to indexers @@ -260,8 +274,7 @@ const layers = { else console.debug(`[layers] Anonymous layer '${lobj.id}' deleted`); }, }, - _logpath, - ["active"] + _logpath ); layers._collections.push(collection); @@ -271,11 +284,6 @@ const layers = { `[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/tool/colorbrush.js b/js/ui/tool/colorbrush.js index 27a7366..a14d6e5 100644 --- a/js/ui/tool/colorbrush.js +++ b/js/ui/tool/colorbrush.js @@ -44,7 +44,7 @@ const colorBrushTool = () => resolution: {w: 7, h: 7}, after: maskPaintLayer, }); - state.glassLayer.canvas.style.display = "none"; + state.glassLayer.hide(); state.glassLayer.canvas.style.imageRendering = "pixelated"; state.glassLayer.canvas.style.borderRadius = "50%"; @@ -55,10 +55,11 @@ const colorBrushTool = () => after: imgLayer, }); state.eraseLayer.canvas.style.display = "none"; + state.eraseLayer.hide(); state.eraseBackup = imageCollection.registerLayer(null, { after: imgLayer, }); - state.eraseBackup.canvas.style.display = "none"; + state.eraseBackup.hide(); // Start Listeners mouse.listen.world.onmousemove.on(state.movecb); @@ -131,13 +132,13 @@ const colorBrushTool = () => state.enableDropper = () => { state.eyedropper = true; state.movecb(lastMouseMoveEvn); - state.glassLayer.canvas.style.display = "block"; + state.glassLayer.unhide(); }; state.disableDropper = () => { state.eyedropper = false; state.movecb(lastMouseMoveEvn); - state.glassLayer.canvas.style.display = "none"; + state.glassLayer.hide(); }; let lastMouseMoveEvn = {x: 0, y: 0}; @@ -218,6 +219,13 @@ const colorBrushTool = () => state.setColor( "#" + ((dat[0] << 16) | (dat[1] << 8) | dat[2]).toString(16) ); + state.disableDropper(); + } + }; + + state.rightclickcb = (evn) => { + if (evn.target === imageCollection.inputElement && state.eyedropper) { + state.disableDropper(); } }; @@ -255,6 +263,8 @@ const colorBrushTool = () => }; state.erasestartcb = (evn) => { + if (state.eyedropper) return; + state.erasing = true; if (state.affectMask) _mask_brush_erase_callback(evn, state); // Make a backup of the current image to apply erase later @@ -270,6 +280,7 @@ const colorBrushTool = () => }; state.erasecb = (evn) => { + if (state.eyedropper || !state.erasing) return; if (state.affectMask) _mask_brush_erase_callback(evn, state); imgCtx.globalCompositeOperation = "destination-out"; _color_brush_erase_callback(evn, state, imgCtx); @@ -278,6 +289,9 @@ const colorBrushTool = () => }; state.eraseendcb = (evn) => { + if (!state.erasing) return; + state.erasing = false; + const canvas = state.eraseLayer.canvas; const ctx = state.eraseLayer.ctx; @@ -364,7 +378,8 @@ const colorBrushTool = () => "eyedropper" ); brushColorEyeDropper.addEventListener("click", () => { - state.enableDropper(); + if (state.eyedropper) state.disableDropper(); + else state.enableDropper(); }); brushColorPickerWrapper.appendChild(brushColorPicker); From aec096b856ab0c50c581ce02ab0b35d9b53588f8 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sun, 4 Dec 2022 08:00:39 -0300 Subject: [PATCH 03/26] layers... now exist? Signed-off-by: Victor Seiji Hariki --- css/ui/layers.css | 16 ++++++++++ index.html | 15 ++++++++- js/index.js | 6 ++-- js/initalize/layers.populate.js | 12 ++++++- js/lib/commands.js | 4 +-- js/lib/util.js | 4 +-- js/ui/floating/layers.js | 56 +++++++++++++++++++++++++++++++++ js/ui/tool/colorbrush.js | 23 ++++++++------ js/ui/tool/dream.js | 12 +++---- js/ui/tool/select.js | 8 ++--- 10 files changed, 128 insertions(+), 28 deletions(-) create mode 100644 css/ui/layers.css create mode 100644 js/ui/floating/layers.js diff --git a/css/ui/layers.css b/css/ui/layers.css new file mode 100644 index 0000000..283d635 --- /dev/null +++ b/css/ui/layers.css @@ -0,0 +1,16 @@ +#layer-list { + height: 200px; +} + +#layer-list .ui-layer { + cursor: pointer; + background-color: #fff3; +} + +#layer-list .ui-layer:hover { + filter: brightness(90%); +} + +#layer-list .ui-layer:active { + filter: brightness(80%); +} diff --git a/index.html b/index.html index 69ee594..7992b6a 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,7 @@ + @@ -194,11 +195,22 @@ + +
+
Layers
+ +
+
+ style="right: 270px; top: 10px">
@@ -225,6 +237,7 @@ + diff --git a/js/index.js b/js/index.js index afdb7ef..bb1cb6b 100644 --- a/js/index.js +++ b/js/index.js @@ -334,8 +334,8 @@ function newImage(evt) { commands.runCommand("eraseImage", "Clear Canvas", { x: 0, y: 0, - w: imgCanvas.width, - h: imgCanvas.height, + w: uiLayers.active.canvas.width, + h: uiLayers.active.canvas.height, }); } @@ -765,7 +765,7 @@ async function upscaleAndDownload() { // get cropped canvas, send it to upscaler, download result var upscale_factor = 2; // TODO: make this a user input 1.x - 4.0 or something var upscaler = document.getElementById("upscalers").value; - var croppedCanvas = cropCanvas(imgCanvas); + var croppedCanvas = cropCanvas(uiLayers.active.canvas); if (croppedCanvas != null) { var upscaler = document.getElementById("upscalers").value; var url = diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index 1f1a39b..d383609 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -52,7 +52,17 @@ const getVisible = (bb) => { canvas.width = bb.w; canvas.height = bb.h; ctx.drawImage(bgLayer.canvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); - ctx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + ctx.drawImage( + uiLayers.active.canvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + bb.w, + bb.h + ); return canvas; }; diff --git a/js/lib/commands.js b/js/lib/commands.js index 91bcaae..1e3bee9 100644 --- a/js/lib/commands.js +++ b/js/lib/commands.js @@ -192,7 +192,7 @@ commands.createCommand( // Check if we have state if (!state.context) { - const context = options.ctx || imgCtx; + const context = options.ctx || uiLayers.active.ctx; state.context = context; // Saving what was in the canvas before the command @@ -252,7 +252,7 @@ commands.createCommand( // Check if we have state if (!state.context) { - const context = options.ctx || imgCtx; + const context = options.ctx || uiLayers.active.ctx; state.context = context; // Saving what was in the canvas before the command diff --git a/js/lib/util.js b/js/lib/util.js index 5fa5f6d..24202b0 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -250,14 +250,14 @@ function cropCanvas(sourceCanvas, options = {}) { * * @param {Object} options - Optional Information * @param {boolean} [options.cropToContent] - If we wish to crop to content first (default: true) - * @param {HTMLCanvasElement} [options.canvas] - The source canvas (default: imgCanvas) + * @param {HTMLCanvasElement} [options.canvas] - The source canvas (default: uiLayers.active.canvas) * @param {string} [options.filename] - The filename to save as (default: '[ISO date] [Hours] [Minutes] [Seconds] openOutpaint image.png').\ * If null, opens image in new tab. */ function downloadCanvas(options = {}) { defaultOpt(options, { cropToContent: true, - canvas: imgCanvas, + canvas: uiLayers.active.canvas, filename: new Date() .toISOString() diff --git a/js/ui/floating/layers.js b/js/ui/floating/layers.js new file mode 100644 index 0000000..3026382 --- /dev/null +++ b/js/ui/floating/layers.js @@ -0,0 +1,56 @@ +/** + * The layering UI window + */ + +const uiLayers = { + layers: [], + active: null, + + _syncLayers() { + const layersEl = document.getElementById("layer-list"); + + const children = Array.from(layersEl.children); + + this.layers.forEach((uiLayer) => { + if (!uiLayer.entry) { + uiLayer.entry = document.createElement("div"); + uiLayer.entry.textContent = uiLayer.name; + + uiLayer.entry.id = `ui-layer-${uiLayer.id}`; + uiLayer.entry.classList.add("ui-layer"); + uiLayer.entry.addEventListener( + "click", + () => (this.active = uiLayer.layer) + ); + + if (true || children.length === 0) layersEl.appendChild(uiLayer.entry); + } + }); + }, + + addLayer(group, name) { + const layer = imageCollection.registerLayer(null, { + name, + after: + (this.layers.length > 0 && this.layers[this.layers.length - 1].layer) || + bgLayer, + }); + + const uiLayer = { + id: layer.id, + group, + name, + entry: null, + layer, + }; + this.layers.push(uiLayer); + + this.active = uiLayer.layer; + + this._syncLayers(); + + return uiLayer; + }, +}; +uiLayers.addLayer(null, "Default Image Layer"); +uiLayers.addLayer(null, "Test Extra Layer"); diff --git a/js/ui/tool/colorbrush.js b/js/ui/tool/colorbrush.js index a14d6e5..5a78380 100644 --- a/js/ui/tool/colorbrush.js +++ b/js/ui/tool/colorbrush.js @@ -271,20 +271,20 @@ const colorBrushTool = () => const bkpcanvas = state.eraseBackup.canvas; const bkpctx = state.eraseBackup.ctx; bkpctx.clearRect(0, 0, bkpcanvas.width, bkpcanvas.height); - bkpctx.drawImage(imgCanvas, 0, 0); + bkpctx.drawImage(uiLayers.active.canvas, 0, 0); - imgCtx.globalCompositeOperation = "destination-out"; - _color_brush_erase_callback(evn, state, imgCtx); - imgCtx.globalCompositeOperation = "source-over"; + uiLayers.active.ctx.globalCompositeOperation = "destination-out"; + _color_brush_erase_callback(evn, state, uiLayers.active.ctx); + uiLayers.active.ctx.globalCompositeOperation = "source-over"; _color_brush_erase_callback(evn, state, state.eraseLayer.ctx); }; state.erasecb = (evn) => { if (state.eyedropper || !state.erasing) return; if (state.affectMask) _mask_brush_erase_callback(evn, state); - imgCtx.globalCompositeOperation = "destination-out"; - _color_brush_erase_callback(evn, state, imgCtx); - imgCtx.globalCompositeOperation = "source-over"; + uiLayers.active.ctx.globalCompositeOperation = "destination-out"; + _color_brush_erase_callback(evn, state, uiLayers.active.ctx); + uiLayers.active.ctx.globalCompositeOperation = "source-over"; _color_brush_erase_callback(evn, state, state.eraseLayer.ctx); }; @@ -300,8 +300,13 @@ const colorBrushTool = () => const cropped = cropCanvas(canvas, {border: 10}); const bb = cropped.bb; - imgCtx.clearRect(0, 0, imgCanvas.width, imgCanvas.height); - imgCtx.drawImage(bkpcanvas, 0, 0); + uiLayers.active.ctx.clearRect( + 0, + 0, + uiLayers.active.canvas.width, + uiLayers.active.canvas.height + ); + uiLayers.active.ctx.drawImage(bkpcanvas, 0, 0); commands.runCommand("eraseImage", "Color Brush Erase", { mask: cropped.canvas, diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index fd37ac9..4240f0a 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -367,7 +367,7 @@ const dream_generate_callback = async (evn, state) => { blockNewImages = true; // Use txt2img if canvas is blank - if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) { + if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, uiLayers.active.canvas)) { // Dream _generate("txt2img", request, bb); } else { @@ -384,7 +384,7 @@ const dream_generate_callback = async (evn, state) => { // Get init image auxCtx.fillRect(0, 0, request.width, request.height); auxCtx.drawImage( - imgCanvas, + uiLayers.active.canvas, bb.x, bb.y, bb.w, @@ -417,7 +417,7 @@ const dream_generate_callback = async (evn, state) => { auxCtx.globalCompositeOperation = "destination-in"; auxCtx.drawImage( - imgCanvas, + uiLayers.active.canvas, bb.x, bb.y, bb.w, @@ -430,7 +430,7 @@ const dream_generate_callback = async (evn, state) => { } else { auxCtx.globalCompositeOperation = "destination-in"; auxCtx.drawImage( - imgCanvas, + uiLayers.active.canvas, bb.x, bb.y, bb.w, @@ -536,7 +536,7 @@ const dream_img2img_callback = (evn, state) => { ); // Do nothing if no image exists - if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) return; + if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, uiLayers.active.canvas)) return; // Build request to the API const request = {}; @@ -565,7 +565,7 @@ const dream_img2img_callback = (evn, state) => { // Get init image auxCtx.fillRect(0, 0, request.width, request.height); auxCtx.drawImage( - imgCanvas, + uiLayers.active.canvas, bb.x, bb.y, bb.w, diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 964d070..083a167 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -84,7 +84,7 @@ const selectTransformTool = () => // Clears selection and make things right state.reset = () => { if (state.selected) - imgCtx.drawImage( + uiLayers.active.ctx.drawImage( state.original.image, state.original.x, state.original.y @@ -312,7 +312,7 @@ const selectTransformTool = () => // If something is selected, commit changes to the canvas if (state.selected) { - imgCtx.drawImage( + uiLayers.active.ctx.drawImage( state.selected.image, state.original.x, state.original.y @@ -406,7 +406,7 @@ const selectTransformTool = () => const ctx = cvs.getContext("2d"); ctx.drawImage( - imgCanvas, + uiLayers.active.canvas, state.selected.x, state.selected.y, state.selected.w, @@ -417,7 +417,7 @@ const selectTransformTool = () => state.selected.h ); - imgCtx.clearRect( + uiLayers.active.ctx.clearRect( state.selected.x, state.selected.y, state.selected.w, From 559f4493ca04e0a6558a7b4d2338348be63f33a9 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sun, 4 Dec 2022 08:31:43 -0300 Subject: [PATCH 04/26] rename button (resources) also stops evn propag Signed-off-by: Victor Seiji Hariki --- js/ui/tool/stamp.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 66e0c4b..917121e 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -157,15 +157,20 @@ const stampTool = () => actionArray.classList.add("actions"); const renameButton = document.createElement("button"); - renameButton.addEventListener("click", () => { - const name = prompt("Rename your resource:", resource.name); - if (name) { - resource.name = name; - resourceTitle.textContent = name; + renameButton.addEventListener( + "click", + (evn) => { + evn.stopPropagation(); + const name = prompt("Rename your resource:", resource.name); + if (name) { + resource.name = name; + resourceTitle.textContent = name; - syncResources(); - } - }); + syncResources(); + } + }, + {passive: false} + ); renameButton.title = "Rename Resource"; renameButton.appendChild(document.createElement("div")); renameButton.classList.add("rename-btn"); From 14df643326d9a8bd8ec620ac86016d2991d6b1e0 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sun, 4 Dec 2022 16:22:35 -0300 Subject: [PATCH 05/26] layers? Signed-off-by: Victor Seiji Hariki --- css/icons.css | 24 ++++ css/layers.css | 18 +++ css/ui/generic.css | 45 ++++++ css/ui/layers.css | 124 ++++++++++++++++- index.html | 40 +++++- js/index.js | 27 ++-- js/initalize/layers.populate.js | 46 +++---- js/lib/commands.js | 4 +- js/lib/util.js | 10 +- js/ui/floating/layers.js | 236 ++++++++++++++++++++++++++++++-- js/ui/tool/colorbrush.js | 78 +++++++---- js/ui/tool/dream.js | 82 +++++++---- js/ui/tool/maskbrush.js | 98 ++++++------- js/ui/tool/select.js | 8 +- js/ui/tool/stamp.js | 2 + res/icons/chevron-down.svg | 5 + res/icons/chevron-up.svg | 4 + res/icons/eye-off.svg | 7 + res/icons/eye.svg | 5 + res/icons/file-plus.svg | 7 + 20 files changed, 692 insertions(+), 178 deletions(-) create mode 100644 css/icons.css create mode 100644 res/icons/chevron-down.svg create mode 100644 res/icons/chevron-up.svg create mode 100644 res/icons/eye-off.svg create mode 100644 res/icons/eye.svg create mode 100644 res/icons/file-plus.svg diff --git a/css/icons.css b/css/icons.css new file mode 100644 index 0000000..0309e12 --- /dev/null +++ b/css/icons.css @@ -0,0 +1,24 @@ +.ui.icon > .icon-eye-off { + -webkit-mask-image: url("/res/icons/eye-off.svg"); + mask-image: url("/res/icons/eye-off.svg"); +} + +.ui.icon > .icon-eye { + -webkit-mask-image: url("/res/icons/eye.svg"); + mask-image: url("/res/icons/eye.svg"); +} + +.ui.icon > .icon-file-plus { + -webkit-mask-image: url("/res/icons/file-plus.svg"); + mask-image: url("/res/icons/file-plus.svg"); +} + +.ui.icon > .icon-chevron-down { + -webkit-mask-image: url("/res/icons/chevron-down.svg"); + mask-image: url("/res/icons/chevron-down.svg"); +} + +.ui.icon > .icon-chevron-up { + -webkit-mask-image: url("/res/icons/chevron-up.svg"); + mask-image: url("/res/icons/chevron-up.svg"); +} diff --git a/css/layers.css b/css/layers.css index 8675b29..67edccf 100644 --- a/css/layers.css +++ b/css/layers.css @@ -44,3 +44,21 @@ bottom: 0; right: 0; } + +#layer-overlay { + position: fixed; + + top: 0; + left: 0; + + width: 100%; + height: 100%; + + pointer-events: none; + + z-index: 15; +} + +#layer-render.pixelated canvas { + image-rendering: crisp-edges; +} diff --git a/css/ui/generic.css b/css/ui/generic.css index dd97f48..bd673a7 100644 --- a/css/ui/generic.css +++ b/css/ui/generic.css @@ -113,3 +113,48 @@ select > option:checked::after { mask-image: url("/res/icons/check.svg"); mask-size: contain; } +/*************/ +/* UI styles */ +/*************/ + +/* The separator */ +.ui.separator { + width: 80%; + margin: auto; + align-self: center; + border-top: 1px var(--c-hover) solid; +} + +/* Icon button */ +.ui.squaer { + aspect-ratio: 1; +} + +.ui.button.icon { + display: flex; + align-items: stretch; + + cursor: pointer; + + padding: 0; + margin: 0; + border: 0; + background-color: transparent; +} + +.ui.button.icon > *:first-child { + flex: 1; + margin: 3px; + + mask-position: center; + + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background-color: var(--c-text); +} + +.ui.button.icon:hover { + background-color: var(--c-hover); +} diff --git a/css/ui/layers.css b/css/ui/layers.css index 283d635..001cc97 100644 --- a/css/ui/layers.css +++ b/css/ui/layers.css @@ -1,16 +1,132 @@ +.layer-manager { + display: flex; + flex-direction: column; + align-items: stretch; + + border-radius: 5px; + overflow: hidden; + + background-color: var(--c-primary); +} + #layer-list { height: 200px; + + background-color: var(--c-primary); + + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +#layer-list > *:first-child { + border-top-left-radius: 5px; + border-top-right-radius: 5px; } #layer-list .ui-layer { + display: flex; + align-items: center; + justify-content: space-between; + + height: 25px; + padding-left: 5px; + padding-right: 5px; + cursor: pointer; - background-color: #fff3; + + color: var(--c-text); + + transition-duration: 50ms; } +#layer-list .ui-layer.active { + background-color: var(--c-active); +} +#layer-list .ui-layer.active:hover, #layer-list .ui-layer:hover { - filter: brightness(90%); + background-color: var(--c-hover); +} +#layer-list .ui-layer.active:active, +#layer-list .ui-layer:active { + background-color: var(--c-hover); + filter: brightness(120%); } -#layer-list .ui-layer:active { - filter: brightness(80%); +#layer-list .ui-layer > .title { + flex: 1; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + + background-color: transparent; + + border: 0; + color: var(--c-text); +} + +#layer-list .ui-layer > .actions { + display: flex; + align-self: stretch; +} + +#layer-list .actions > button { + display: flex; + align-items: stretch; + + padding: 0; + + width: 25px; + aspect-ratio: 1; + + background-color: transparent; + border: 0; + cursor: pointer; +} + +#layer-list .ui-layer > .actions > *:hover > * { + margin: 2px; +} + +#layer-list .actions > button > *:first-child { + flex: 1; + margin: 3px; + + -webkit-mask-size: contain; + mask-size: contain; + background-color: var(--c-text); +} + +#layer-list .actions > .rename-btn > *:first-child { + -webkit-mask-image: url("/res/icons/edit.svg"); + mask-image: url("/res/icons/edit.svg"); +} + +#layer-list .actions > .delete-btn > *:first-child { + -webkit-mask-image: url("/res/icons/trash.svg"); + mask-image: url("/res/icons/trash.svg"); +} + +#layer-list .actions > .hide-btn > *:first-child { + -webkit-mask-image: url("/res/icons/eye.svg"); + mask-image: url("/res/icons/eye.svg"); +} +#layer-list .hidden .actions > .hide-btn > *:first-child { + -webkit-mask-image: url("/res/icons/eye-off.svg"); + mask-image: url("/res/icons/eye-off.svg"); +} + +.layer-manager > .separator { + width: calc(100% - 10px); +} + +.layer-manager > .layer-list-actions { + display: flex; + padding: 0; + + justify-content: stretch; +} + +.layer-manager > .layer-list-actions > * { + flex: 1; + height: 25px; } diff --git a/index.html b/index.html index 7992b6a..89755b3 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,7 @@ openOutpaint 🐠 + @@ -110,6 +111,12 @@ step="1" onchange="changeMaskBlur()" />
+ +
@@ -202,7 +209,35 @@ style="right: 10px; bottom: 10px">
Layers
@@ -221,6 +256,9 @@
+ + + diff --git a/js/index.js b/js/index.js index bb1cb6b..822a4e1 100644 --- a/js/index.js +++ b/js/index.js @@ -330,12 +330,14 @@ async function testHostConnection() { function newImage(evt) { clearPaintedMask(); - clearBackupMask(); - commands.runCommand("eraseImage", "Clear Canvas", { - x: 0, - y: 0, - w: uiLayers.active.canvas.width, - h: uiLayers.active.canvas.height, + uil.layers.forEach(({layer}) => { + commands.runCommand("eraseImage", "Clear Canvas", { + x: 0, + y: 0, + w: layer.canvas.width, + h: layer.canvas.height, + ctx: layer.ctx, + }); }); } @@ -484,9 +486,16 @@ function changeHiResFix() { ); localStorage.setItem("enable_hr", stableDiffusionData.enable_hr); } +function changeSmoothRendering() { + const layers = document.getElementById("layer-render"); + if (document.getElementById("cbxSmooth").checked) { + layers.classList.remove("pixelated"); + } else { + layers.classList.add("pixelated"); + } +} -function isCanvasBlank(x, y, w, h, specifiedCanvas) { - var canvas = document.getElementById(specifiedCanvas.id); +function isCanvasBlank(x, y, w, h, canvas) { return !canvas .getContext("2d") .getImageData(x, y, w, h) @@ -765,7 +774,7 @@ async function upscaleAndDownload() { // get cropped canvas, send it to upscaler, download result var upscale_factor = 2; // TODO: make this a user input 1.x - 4.0 or something var upscaler = document.getElementById("upscalers").value; - var croppedCanvas = cropCanvas(uiLayers.active.canvas); + var croppedCanvas = cropCanvas(uil.canvas); if (croppedCanvas != null) { var upscaler = document.getElementById("upscalers").value; var url = diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index d383609..01f88da 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -37,35 +37,12 @@ const ovCtx = ovLayer.ctx; const debugCanvas = debugLayer.canvas; // where mouse cursor renders const debugCtx = debugLayer.ctx; -/** - * Function that returns a canvas with full visible information of a certain bounding box. - * - * For now, only the img is used. - * - * @param {BoundingBox} bb The bouding box to get visible data from - * @returns {HTMLCanvasElement} The canvas element containing visible image data - */ -const getVisible = (bb) => { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - - canvas.width = bb.w; - canvas.height = bb.h; - ctx.drawImage(bgLayer.canvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); - ctx.drawImage( - uiLayers.active.canvas, - bb.x, - bb.y, - bb.w, - bb.h, - 0, - 0, - bb.w, - bb.h - ); - - return canvas; -}; +/* WIP: Most cursors shouldn't need a zoomable canvas */ +/** @type {HTMLCanvasElement} */ +const uiCanvas = document.getElementById("layer-overlay"); // where mouse cursor renders +uiCanvas.width = uiCanvas.clientWidth; +uiCanvas.height = uiCanvas.clientHeight; +const uiCtx = uiCanvas.getContext("2d"); debugLayer.hide(); // Hidden by default @@ -141,6 +118,15 @@ const viewport = { get h() { return (window.innerHeight * 1) / this.zoom; }, + viewToCanvas(x, y) { + return {x, y}; + }, + canvasToView(x, y) { + return { + x: window.innerWidth * ((x - this.cx) / this.w) + window.innerWidth / 2, + y: window.innerHeight * ((y - this.cy) / this.h) + window.innerHeight / 2, + }; + }, /** * Apply transformation * @@ -220,4 +206,6 @@ mouse.listen.window.btn.middle.onpaintend.on((evn) => { window.addEventListener("resize", () => { viewport.transform(imageCollection.element); + uiCanvas.width = uiCanvas.clientWidth; + uiCanvas.height = uiCanvas.clientHeight; }); diff --git a/js/lib/commands.js b/js/lib/commands.js index 1e3bee9..99c4b84 100644 --- a/js/lib/commands.js +++ b/js/lib/commands.js @@ -192,7 +192,7 @@ commands.createCommand( // Check if we have state if (!state.context) { - const context = options.ctx || uiLayers.active.ctx; + const context = options.ctx || uil.ctx; state.context = context; // Saving what was in the canvas before the command @@ -252,7 +252,7 @@ commands.createCommand( // Check if we have state if (!state.context) { - const context = options.ctx || uiLayers.active.ctx; + const context = options.ctx || uil.ctx; state.context = context; // Saving what was in the canvas before the command diff --git a/js/lib/util.js b/js/lib/util.js index 24202b0..207ab2c 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -250,14 +250,20 @@ function cropCanvas(sourceCanvas, options = {}) { * * @param {Object} options - Optional Information * @param {boolean} [options.cropToContent] - If we wish to crop to content first (default: true) - * @param {HTMLCanvasElement} [options.canvas] - The source canvas (default: uiLayers.active.canvas) + * @param {HTMLCanvasElement} [options.canvas] - The source canvas (default: visible) * @param {string} [options.filename] - The filename to save as (default: '[ISO date] [Hours] [Minutes] [Seconds] openOutpaint image.png').\ * If null, opens image in new tab. */ function downloadCanvas(options = {}) { + console.debug(imageCollection); defaultOpt(options, { cropToContent: true, - canvas: uiLayers.active.canvas, + canvas: uil.getVisible({ + x: 0, + y: 0, + w: imageCollection.size.w, + h: imageCollection.size.h, + }), filename: new Date() .toISOString() diff --git a/js/ui/floating/layers.js b/js/ui/floating/layers.js index 3026382..c8f3be8 100644 --- a/js/ui/floating/layers.js +++ b/js/ui/floating/layers.js @@ -2,32 +2,139 @@ * The layering UI window */ -const uiLayers = { +const uil = { + _ui_layer_list: document.getElementById("layer-list"), layers: [], - active: null, + _active: null, + set active(v) { + Array.from(this._ui_layer_list.children).forEach((child) => { + child.classList.remove("active"); + }); + v.entry.classList.add("active"); + + this._active = v; + }, + get active() { + return this._active; + }, + + get layer() { + return this.active && this.active.layer; + }, + + get canvas() { + return this.layer && this.active.layer.canvas; + }, + + get ctx() { + return this.layer && this.active.layer.ctx; + }, + + /** + * Synchronizes layer array to DOM + */ _syncLayers() { const layersEl = document.getElementById("layer-list"); - const children = Array.from(layersEl.children); + const copy = this.layers.map((i) => i); + copy.reverse(); - this.layers.forEach((uiLayer) => { + copy.forEach((uiLayer, index) => { + // If we have the correct layer here, then do nothing + if ( + layersEl.children[index] && + layersEl.children[index].id === `ui-layer-${uiLayer.id}` + ) + return; + + // If the layer we are processing does not exist, then create it and add before current element if (!uiLayer.entry) { uiLayer.entry = document.createElement("div"); - uiLayer.entry.textContent = uiLayer.name; - uiLayer.entry.id = `ui-layer-${uiLayer.id}`; uiLayer.entry.classList.add("ui-layer"); - uiLayer.entry.addEventListener( - "click", - () => (this.active = uiLayer.layer) - ); + uiLayer.entry.addEventListener("click", () => { + this.active = uiLayer; + }); - if (true || children.length === 0) layersEl.appendChild(uiLayer.entry); + // Title Element + const titleEl = document.createElement("input"); + titleEl.classList.add("title"); + titleEl.value = uiLayer.name; + titleEl.style.pointerEvents = "none"; + + const deselect = () => { + titleEl.style.pointerEvents = "none"; + titleEl.setSelectionRange(0, 0); + }; + + titleEl.addEventListener("blur", deselect); + uiLayer.entry.appendChild(titleEl); + + uiLayer.entry.addEventListener("change", () => { + const name = titleEl.value.trim(); + titleEl.value = name; + uiLayer.entry.title = name; + + uiLayer.name = name; + + this._syncLayers(); + + titleEl.blur(); + }); + uiLayer.entry.addEventListener("dblclick", () => { + titleEl.style.pointerEvents = "auto"; + titleEl.focus(); + titleEl.select(); + }); + + // Add action buttons + const actionArray = document.createElement("div"); + actionArray.classList.add("actions"); + + const hideButton = document.createElement("button"); + hideButton.addEventListener( + "click", + (evn) => { + evn.stopPropagation(); + uiLayer.hidden = !uiLayer.hidden; + if (uiLayer.hidden) { + uiLayer.entry.classList.add("hidden"); + } else uiLayer.entry.classList.remove("hidden"); + }, + {passive: false} + ); + hideButton.title = "Hide/Unhide Layer"; + hideButton.appendChild(document.createElement("div")); + hideButton.classList.add("hide-btn"); + + actionArray.appendChild(hideButton); + uiLayer.entry.appendChild(actionArray); + + if (layersEl.children[index]) + layersEl.children[index].before(uiLayer.entry); + else layersEl.appendChild(uiLayer.entry); } + // If the layer already exists, just move it here + else { + layersEl.children[index].before(uiLayer.entry); + } + }); + + // Synchronizes with the layer lib + this.layers.forEach((uiLayer, index) => { + if (index === 0) uiLayer.layer.moveAfter(bgLayer); + else uiLayer.layer.moveAfter(copy[index - 1].layer); }); }, + /** + * Adds a user-manageable layer for image editing. + * + * @param {string} group The group the layer belongs to. [does nothing for now] + * @param {string} name The name of the new layer. + * @returns + */ addLayer(group, name) { const layer = imageCollection.registerLayer(null, { name, @@ -40,17 +147,116 @@ const uiLayers = { id: layer.id, group, name, + _hidden: false, + set hidden(v) { + if (v) { + this._hidden = true; + this.layer.hide(v); + } else { + this._hidden = false; + this.layer.unhide(v); + } + }, + get hidden() { + return this._hidden; + }, entry: null, layer, }; this.layers.push(uiLayer); - this.active = uiLayer.layer; - this._syncLayers(); + this.active = uiLayer; + return uiLayer; }, + + /** + * Moves a layer to a specified position + * + * @param {UserLayer} layer Layer to move + * @param {number} position Position to move the layer to + */ + moveLayerTo(layer, position) { + if (position < 0 || position >= this.layers.length) + throw new RangeError("Position out of bounds"); + + const index = this.layers.indexOf(layer); + if (index !== -1) { + if (this.layers.length < 2) return; // Do nothing if moving a layer doesn't make sense + + this.layers.splice(index, 1); + this.layers.splice(position, 0, layer); + + this._syncLayers(); + + return; + } + throw new ReferenceError("Layer could not be found"); + }, + /** + * Moves a layer up a single position + * + * @param {UserLayer} [layer=uil.active] Layer to move + */ + moveLayerUp(layer = uil.active) { + const index = this.layers.indexOf(layer); + if (index === -1) throw new ReferenceError("Layer could not be found"); + try { + this.moveLayerTo(layer, index + 1); + } catch (e) {} + }, + /** + * Moves a layer down a single position + * + * @param {UserLayer} [layer=uil.active] Layer to move + */ + moveLayerDown(layer = uil.active) { + const index = this.layers.indexOf(layer); + if (index === -1) throw new ReferenceError("Layer could not be found"); + try { + this.moveLayerTo(layer, index - 1); + } catch (e) {} + }, + /** + * Function that returns a canvas with full visible information of a certain bounding box. + * + * For now, only the img is used. + * + * @param {BoundingBox} bb The bouding box to get visible data from + * @param {object} [options] Options + * @param {boolean} [options.includeBg=false] Whether to include the background + * @returns {HTMLCanvasElement} The canvas element containing visible image data + */ + getVisible(bb, options = {}) { + defaultOpt(options, { + includeBg: false, + }); + + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + canvas.width = bb.w; + canvas.height = bb.h; + if (options.includeBg) + ctx.drawImage(bgLayer.canvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + this.layers.forEach((layer) => { + if (!layer.hidden) + ctx.drawImage( + layer.layer.canvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + bb.w, + bb.h + ); + }); + + return canvas; + }, }; -uiLayers.addLayer(null, "Default Image Layer"); -uiLayers.addLayer(null, "Test Extra Layer"); +uil.addLayer(null, "Default Image Layer"); diff --git a/js/ui/tool/colorbrush.js b/js/ui/tool/colorbrush.js index 5a78380..ea49547 100644 --- a/js/ui/tool/colorbrush.js +++ b/js/ui/tool/colorbrush.js @@ -35,8 +35,14 @@ const colorBrushTool = () => "Color Brush", (state, opt) => { // Draw new cursor immediately - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); - state.movecb({...mouse.coords.world.pos}); + uiCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + state.movecb({ + ...mouse.coords.world.pos, + evn: { + clientX: mouse.coords.window.pos.x, + clientY: mouse.coords.window.pos.y, + }, + }); // Layer for eyedropper magnifiying glass state.glassLayer = imageCollection.registerLayer(null, { @@ -106,6 +112,8 @@ const colorBrushTool = () => // Cancel any eyedropping state.drawing = false; state.disableDropper(); + + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); }, { init: (state) => { @@ -146,26 +154,43 @@ const colorBrushTool = () => state.movecb = (evn) => { lastMouseMoveEvn = evn; - // draw drawing cursor - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + const vcp = {x: evn.evn.clientX, y: evn.evn.clientY}; + // draw drawing cursor + uiCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + + uiCtx.beginPath(); + uiCtx.arc( + vcp.x, + vcp.y, + state.eyedropper ? 50 : (state.brushSize / 2) * viewport.zoom, + 0, + 2 * Math.PI, + true + ); + uiCtx.strokeStyle = "black"; + uiCtx.stroke(); + + // Draw eyedropper cursor and magnifiying glass if (state.eyedropper) { const bb = getBoundingBox(evn.x, evn.y, 7, 7, false); - const canvas = getVisible(bb); + const canvas = uil.getVisible(bb, {includeBg: true}); state.glassLayer.ctx.clearRect(0, 0, 7, 7); state.glassLayer.ctx.drawImage(canvas, 0, 0); state.glassLayer.moveTo(evn.x - 50, evn.y - 50); - - ovCtx.beginPath(); - ovCtx.arc(evn.x, evn.y, 50, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 7x on a line??? - ovCtx.strokeStyle = "black"; - ovCtx.stroke(); } else { - 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 7x on a line??? - ovCtx.fillStyle = state.color + "50"; - ovCtx.fill(); + uiCtx.beginPath(); + uiCtx.arc( + vcp.x, + vcp.y, + (state.brushSize / 2) * viewport.zoom, + 0, + 2 * Math.PI, + true + ); + uiCtx.fillStyle = state.color + "50"; + uiCtx.fill(); } }; @@ -175,7 +200,7 @@ const colorBrushTool = () => state.brushSize - Math.floor(state.config.brushScrollSpeed * evn.delta) ); - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + uiCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); state.movecb(evn); } }; @@ -271,20 +296,20 @@ const colorBrushTool = () => const bkpcanvas = state.eraseBackup.canvas; const bkpctx = state.eraseBackup.ctx; bkpctx.clearRect(0, 0, bkpcanvas.width, bkpcanvas.height); - bkpctx.drawImage(uiLayers.active.canvas, 0, 0); + bkpctx.drawImage(uil.canvas, 0, 0); - uiLayers.active.ctx.globalCompositeOperation = "destination-out"; - _color_brush_erase_callback(evn, state, uiLayers.active.ctx); - uiLayers.active.ctx.globalCompositeOperation = "source-over"; + uil.ctx.globalCompositeOperation = "destination-out"; + _color_brush_erase_callback(evn, state, uil.ctx); + uil.ctx.globalCompositeOperation = "source-over"; _color_brush_erase_callback(evn, state, state.eraseLayer.ctx); }; state.erasecb = (evn) => { if (state.eyedropper || !state.erasing) return; if (state.affectMask) _mask_brush_erase_callback(evn, state); - uiLayers.active.ctx.globalCompositeOperation = "destination-out"; - _color_brush_erase_callback(evn, state, uiLayers.active.ctx); - uiLayers.active.ctx.globalCompositeOperation = "source-over"; + uil.ctx.globalCompositeOperation = "destination-out"; + _color_brush_erase_callback(evn, state, uil.ctx); + uil.ctx.globalCompositeOperation = "source-over"; _color_brush_erase_callback(evn, state, state.eraseLayer.ctx); }; @@ -300,13 +325,8 @@ const colorBrushTool = () => const cropped = cropCanvas(canvas, {border: 10}); const bb = cropped.bb; - uiLayers.active.ctx.clearRect( - 0, - 0, - uiLayers.active.canvas.width, - uiLayers.active.canvas.height - ); - uiLayers.active.ctx.drawImage(bkpcanvas, 0, 0); + uil.ctx.clearRect(0, 0, uil.canvas.width, uil.canvas.height); + uil.ctx.drawImage(bkpcanvas, 0, 0); commands.runCommand("eraseImage", "Color Brush Erase", { mask: cropped.canvas, diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 4240f0a..0fdeec6 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -366,8 +366,12 @@ const dream_generate_callback = async (evn, state) => { // Don't allow another image until is finished blockNewImages = true; + // Get visible pixels + const visibleCanvas = uil.getVisible(bb); + console.debug(visibleCanvas); + // Use txt2img if canvas is blank - if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, uiLayers.active.canvas)) { + if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) { // Dream _generate("txt2img", request, bb); } else { @@ -384,9 +388,9 @@ const dream_generate_callback = async (evn, state) => { // Get init image auxCtx.fillRect(0, 0, request.width, request.height); auxCtx.drawImage( - uiLayers.active.canvas, - bb.x, - bb.y, + visibleCanvas, + 0, + 0, bb.w, bb.h, 0, @@ -417,9 +421,9 @@ const dream_generate_callback = async (evn, state) => { auxCtx.globalCompositeOperation = "destination-in"; auxCtx.drawImage( - uiLayers.active.canvas, - bb.x, - bb.y, + visibleCanvas, + 0, + 0, bb.w, bb.h, 0, @@ -430,9 +434,9 @@ const dream_generate_callback = async (evn, state) => { } else { auxCtx.globalCompositeOperation = "destination-in"; auxCtx.drawImage( - uiLayers.active.canvas, - bb.x, - bb.y, + visibleCanvas, + 0, + 0, bb.w, bb.h, 0, @@ -535,8 +539,13 @@ const dream_img2img_callback = (evn, state) => { state.snapToGrid && basePixelCount ); + // Get visible pixels + const visibleCanvas = uil.getVisible(bb); + + console.debug(visibleCanvas); + // Do nothing if no image exists - if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, uiLayers.active.canvas)) return; + if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) return; // Build request to the API const request = {}; @@ -565,9 +574,9 @@ const dream_img2img_callback = (evn, state) => { // Get init image auxCtx.fillRect(0, 0, request.width, request.height); auxCtx.drawImage( - uiLayers.active.canvas, - bb.x, - bb.y, + visibleCanvas, + 0, + 0, bb.w, bb.h, 0, @@ -637,13 +646,20 @@ const _reticle_draw = (evn, state) => { state.snapToGrid && basePixelCount ); + const cvp = viewport.canvasToView(evn.x, evn.y); + const bbvp = { + ...viewport.canvasToView(bb.x, bb.y), + w: viewport.zoom * bb.w, + h: viewport.zoom * bb.h, + }; + // 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 + uiCtx.lineWidth = 1; + uiCtx.strokeStyle = "#FFF"; + uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h); //origin is middle of the frame return () => { - ovCtx.clearRect(bb.x - 10, bb.y - 10, bb.w + 20, bb.h + 20); + uiCtx.clearRect(bbvp.x - 10, bbvp.y - 10, bbvp.w + 20, bbvp.h + 20); }; }; @@ -652,6 +668,7 @@ const _reticle_draw = (evn, state) => { */ const _dream_onwheel = (evn, state) => { + state.mousemovecb(evn); if (!evn.evn.ctrlKey) { const v = state.cursorSize - @@ -670,7 +687,7 @@ const dreamTool = () => "Dream", (state, opt) => { // Draw new cursor immediately - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.mousemovecb({ ...mouse.coords.world.pos, }); @@ -693,6 +710,8 @@ const dreamTool = () => // Hide Mask setMask("none"); + + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); }, { init: (state) => { @@ -707,7 +726,7 @@ const dreamTool = () => state.overMaskPx = 0; state.erasePrevReticle = () => - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.mousemovecb = (evn) => { state.erasePrevReticle(); @@ -789,7 +808,7 @@ const img2imgTool = () => "Img2Img", (state, opt) => { // Draw new cursor immediately - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.mousemovecb({ ...mouse.coords.world.pos, }); @@ -802,6 +821,8 @@ const img2imgTool = () => // Display Mask setMask(state.invertMask ? "hold" : "clear"); + + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); }, (state, opt) => { // Clear Listeners @@ -812,6 +833,7 @@ const img2imgTool = () => // Hide mask setMask("none"); + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); }, { init: (state) => { @@ -829,7 +851,7 @@ const img2imgTool = () => state.keepBorderSize = 64; state.erasePrevReticle = () => - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.mousemovecb = (evn) => { state.erasePrevReticle(); @@ -848,6 +870,12 @@ const img2imgTool = () => height: stableDiffusionData.height, }; + const bbvp = { + ...viewport.canvasToView(bb.x, bb.y), + w: viewport.zoom * bb.w, + h: viewport.zoom * bb.h, + }; + // For displaying border mask const auxCanvas = document.createElement("canvas"); auxCanvas.width = request.width; @@ -870,16 +898,16 @@ const img2imgTool = () => request.width, state.keepBorderSize ); - ovCtx.drawImage( + uiCtx.drawImage( auxCanvas, 0, 0, request.width, request.height, - bb.x, - bb.y, - bb.w, - bb.h + bbvp.x, + bbvp.y, + bbvp.w, + bbvp.h ); } }; diff --git a/js/ui/tool/maskbrush.js b/js/ui/tool/maskbrush.js index 6e6e8ce..dd9de32 100644 --- a/js/ui/tool/maskbrush.js +++ b/js/ui/tool/maskbrush.js @@ -52,50 +52,14 @@ const _mask_brush_erase_callback = (evn, state) => { maskPaintCtx.stroke(); }; -const _paint_mb_cursor = (state) => { - const v = state.brushSize; - state.cursorLayer.resize(v + 20, v + 20); - - const ctx = state.cursorLayer.ctx; - - ctx.clearRect(0, 0, v + 20, v + 20); - ctx.beginPath(); - ctx.arc( - (v + 20) / 2, - (v + 20) / 2, - state.brushSize / 2, - 0, - 2 * Math.PI, - true - ); - ctx.fillStyle = "#FFFFFF50"; - - ctx.fill(); - - if (state.preview) { - ctx.strokeStyle = "#000F"; - ctx.setLineDash([4, 2]); - ctx.stroke(); - ctx.setLineDash([]); - } -}; - const maskBrushTool = () => toolbar.registerTool( "res/icons/paintbrush.svg", "Mask Brush", (state, opt) => { - // New layer for the cursor - state.cursorLayer = imageCollection.registerLayer(null, { - after: maskPaintLayer, - bb: {x: 0, y: 0, w: state.brushSize + 20, h: state.brushSize + 20}, - }); - - _paint_mb_cursor(state); - // Draw new cursor immediately - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); - state.movecb({...mouse.coords.world.pos}); + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); + state.redraw(); // Start Listeners mouse.listen.world.onmousemove.on(state.movecb); @@ -109,10 +73,6 @@ const maskBrushTool = () => setMask("neutral"); }, (state, opt) => { - // Don't want to keep hogging resources - imageCollection.deleteLayer(state.cursorLayer); - state.cursorLayer = null; - // Clear Listeners mouse.listen.world.onmousemove.clear(state.movecb); mouse.listen.world.onwheel.clear(state.wheelcb); @@ -126,6 +86,8 @@ const maskBrushTool = () => state.ctxmenu.previewMaskButton.classList.remove("active"); maskPaintCanvas.classList.remove("opaque"); state.preview = false; + + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); }, { init: (state) => { @@ -145,21 +107,41 @@ const maskBrushTool = () => state.preview = false; state.clearPrevCursor = () => - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); + + state.redraw = () => { + state.movecb({ + ...mouse.coords.world.pos, + evn: { + clientX: mouse.coords.window.pos.x, + clientY: mouse.coords.window.pos.y, + }, + }); + }; state.movecb = (evn) => { - state.cursorLayer.moveTo( - evn.x - state.brushSize / 2 - 10, - evn.y - state.brushSize / 2 - 10 - ); + const vcp = {x: evn.evn.clientX, y: evn.evn.clientY}; + const scp = state.brushSize * viewport.zoom; - state.clearPrevCursor = () => - ovCtx.clearRect( - evn.x - state.brushSize / 2 - 10, - evn.y - state.brushSize / 2 - 10, - evn.x + state.brushSize / 2 + 10, - evn.y + state.brushSize / 2 + 10 + state.clearPrevCursor(); + state; + clearPrevCursor = () => + uiCtx.clearRect( + vcp.x - scp / 2 - 10, + vcp.y - scp / 2 - 10, + vcp.x + scp / 2 + 10, + vcp.y + scp / 2 + 10 ); + + uiCtx.beginPath(); + uiCtx.arc(vcp.x, vcp.y, scp / 2, 0, 2 * Math.PI, true); + uiCtx.strokeStyle = "black"; + uiCtx.stroke(); + + uiCtx.beginPath(); + uiCtx.arc(vcp.x, vcp.y, scp / 2, 0, 2 * Math.PI, true); + uiCtx.fillStyle = "#FFFFFF50"; + uiCtx.fill(); }; state.wheelcb = (evn) => { @@ -169,6 +151,8 @@ const maskBrushTool = () => Math.floor(state.config.brushScrollSpeed * evn.delta) ); state.movecb(evn); + } else { + state.movecb(evn); } }; @@ -189,7 +173,8 @@ const maskBrushTool = () => textStep: 1, cb: (v) => { if (!state.cursorLayer) return; - _paint_mb_cursor(state); + + state.redraw(); }, } ); @@ -221,11 +206,12 @@ const maskBrushTool = () => if (previewMaskButton.classList.contains("active")) { maskPaintCanvas.classList.remove("opaque"); state.preview = false; - _paint_mb_cursor(state); + + state.redraw(); } else { maskPaintCanvas.classList.add("opaque"); state.preview = true; - _paint_mb_cursor(state); + state.redraw(); } previewMaskButton.classList.toggle("active"); }; diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 083a167..e133797 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -84,7 +84,7 @@ const selectTransformTool = () => // Clears selection and make things right state.reset = () => { if (state.selected) - uiLayers.active.ctx.drawImage( + uil.ctx.drawImage( state.original.image, state.original.x, state.original.y @@ -312,7 +312,7 @@ const selectTransformTool = () => // If something is selected, commit changes to the canvas if (state.selected) { - uiLayers.active.ctx.drawImage( + uil.ctx.drawImage( state.selected.image, state.original.x, state.original.y @@ -406,7 +406,7 @@ const selectTransformTool = () => const ctx = cvs.getContext("2d"); ctx.drawImage( - uiLayers.active.canvas, + uil.canvas, state.selected.x, state.selected.y, state.selected.w, @@ -417,7 +417,7 @@ const selectTransformTool = () => state.selected.h ); - uiLayers.active.ctx.clearRect( + uil.ctx.clearRect( state.selected.x, state.selected.y, state.selected.w, diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 917121e..faf0611 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -46,6 +46,8 @@ const stampTool = () => Array.from(state.ctxmenu.resourceList.children).forEach((child) => { child.classList.remove("selected"); }); + + ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); }, { init: (state) => { diff --git a/res/icons/chevron-down.svg b/res/icons/chevron-down.svg new file mode 100644 index 0000000..367a2bb --- /dev/null +++ b/res/icons/chevron-down.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/res/icons/chevron-up.svg b/res/icons/chevron-up.svg new file mode 100644 index 0000000..7bfc938 --- /dev/null +++ b/res/icons/chevron-up.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/res/icons/eye-off.svg b/res/icons/eye-off.svg new file mode 100644 index 0000000..995e056 --- /dev/null +++ b/res/icons/eye-off.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/res/icons/eye.svg b/res/icons/eye.svg new file mode 100644 index 0000000..36329e0 --- /dev/null +++ b/res/icons/eye.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/res/icons/file-plus.svg b/res/icons/file-plus.svg new file mode 100644 index 0000000..1611710 --- /dev/null +++ b/res/icons/file-plus.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file From 6222975825d5e7b25166d0d6b2033ce0327d1a94 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sun, 4 Dec 2022 17:02:46 -0300 Subject: [PATCH 06/26] some... i guess performance improvements for chrome also fixes pixelated view for chrome Signed-off-by: Victor Seiji Hariki --- css/layers.css | 1 + css/ui/generic.css | 1 + js/initalize/layers.populate.js | 4 +++- js/lib/commands.js | 20 +++++++++++++------- js/lib/layers.js | 6 +++++- js/ui/tool/colorbrush.js | 2 ++ 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/css/layers.css b/css/layers.css index 67edccf..6d426c0 100644 --- a/css/layers.css +++ b/css/layers.css @@ -60,5 +60,6 @@ } #layer-render.pixelated canvas { + image-rendering: pixelated; image-rendering: crisp-edges; } diff --git a/css/ui/generic.css b/css/ui/generic.css index bd673a7..eae9671 100644 --- a/css/ui/generic.css +++ b/css/ui/generic.css @@ -146,6 +146,7 @@ select > option:checked::after { flex: 1; margin: 3px; + -webkit-mask-position: center; mask-position: center; -webkit-mask-size: contain; diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index 01f88da..d6ee370 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -12,9 +12,11 @@ const bgLayer = imageCollection.registerLayer("bg", { }); const imgLayer = imageCollection.registerLayer("image", { name: "Image", + ctxOptions: {desynchronized: true}, }); const maskPaintLayer = imageCollection.registerLayer("mask", { name: "Mask Paint", + ctxOptions: {desynchronized: true}, }); const ovLayer = imageCollection.registerLayer("overlay", { name: "Overlay", @@ -42,7 +44,7 @@ const debugCtx = debugLayer.ctx; const uiCanvas = document.getElementById("layer-overlay"); // where mouse cursor renders uiCanvas.width = uiCanvas.clientWidth; uiCanvas.height = uiCanvas.clientHeight; -const uiCtx = uiCanvas.getContext("2d"); +const uiCtx = uiCanvas.getContext("2d", {desynchronized: true}); debugLayer.hide(); // Hidden by default diff --git a/js/lib/commands.js b/js/lib/commands.js index 99c4b84..42c5c6f 100644 --- a/js/lib/commands.js +++ b/js/lib/commands.js @@ -256,12 +256,6 @@ commands.createCommand( state.context = context; // Saving what was in the canvas before the command - const imgData = context.getImageData( - options.x, - options.y, - options.w, - options.h - ); state.box = { x: options.x, y: options.y, @@ -272,7 +266,19 @@ commands.createCommand( const cutout = document.createElement("canvas"); cutout.width = state.box.w; cutout.height = state.box.h; - cutout.getContext("2d").putImageData(imgData, 0, 0); + cutout + .getContext("2d") + .drawImage( + context.canvas, + options.x, + options.y, + options.w, + options.h, + 0, + 0, + options.w, + options.h + ); state.original = new Image(); state.original.src = cutout.toDataURL(); } diff --git a/js/lib/layers.js b/js/lib/layers.js index 908ae27..7c32372 100644 --- a/js/lib/layers.js +++ b/js/lib/layers.js @@ -87,6 +87,7 @@ const layers = { * @param {{w: number, h: number}} options.resolution * @param {?string} options.group * @param {object} options.after + * @param {object} options.ctxOptions * @returns */ registerLayer: (key = null, options = {}) => { @@ -108,6 +109,9 @@ const layers = { // If set, will insert the layer after the given one after: null, + + // Context creation options + ctxOptions: {}, }); // Calculate resolution @@ -137,7 +141,7 @@ const layers = { options.after.canvas.after(canvas); } - const ctx = canvas.getContext("2d"); + const ctx = canvas.getContext("2d", options.ctxOptions); // Path used for logging purposes const _layerlogpath = key diff --git a/js/ui/tool/colorbrush.js b/js/ui/tool/colorbrush.js index ea49547..738c6ae 100644 --- a/js/ui/tool/colorbrush.js +++ b/js/ui/tool/colorbrush.js @@ -56,9 +56,11 @@ const colorBrushTool = () => state.drawLayer = imageCollection.registerLayer(null, { after: imgLayer, + ctxOptions: {willReadFrequently: true}, }); state.eraseLayer = imageCollection.registerLayer(null, { after: imgLayer, + ctxOptions: {willReadFrequently: true}, }); state.eraseLayer.canvas.style.display = "none"; state.eraseLayer.hide(); From 8df0649553537c724f7464801bae9d2e93d45d49 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sun, 4 Dec 2022 18:57:26 -0300 Subject: [PATCH 07/26] first gradient tooling test (img2img gradient border mask) Signed-off-by: Victor Seiji Hariki --- js/ui/tool/dream.js | 87 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 0fdeec6..2bf04d1 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -610,14 +610,48 @@ const dream_img2img_callback = (evn, state) => { 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, @@ -849,6 +883,7 @@ const img2imgTool = () => state.denoisingStrength = 0.7; state.keepBorderSize = 64; + state.gradient = true; state.erasePrevReticle = () => uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); @@ -884,14 +919,58 @@ const img2imgTool = () => if (state.keepBorderSize > 0) { auxCtx.fillStyle = "#6A6AFF30"; + if (state.gradient) { + const lg = auxCtx.createLinearGradient( + 0, + 0, + state.keepBorderSize, + 0 + ); + lg.addColorStop(0, "#6A6AFF30"); + 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, "#6A6AFF30"); + tg.addColorStop(1, "#6A6AFF00"); + 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, "#6A6AFF30"); + rg.addColorStop(1, "#6A6AFF00"); + 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, "#6A6AFF30"); + bg.addColorStop(1, "#6A6AFF00"); + auxCtx.fillStyle = bg; + } auxCtx.fillRect( 0, request.height - state.keepBorderSize, @@ -976,6 +1055,13 @@ const img2imgTool = () => } ).slider; + // Border Mask Gradient Checkbox + state.ctxmenu.borderMaskGradientCheckbox = _toolbar_input.checkbox( + state, + "gradient", + "Border Mask Gradient" + ).label; + // Border Mask Size Slider state.ctxmenu.borderMaskSlider = _toolbar_input.slider( state, @@ -998,6 +1084,7 @@ const img2imgTool = () => menu.appendChild(state.ctxmenu.fullResolutionLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.denoisingStrengthSlider); + menu.appendChild(state.ctxmenu.borderMaskGradientCheckbox); menu.appendChild(state.ctxmenu.borderMaskSlider); }, shortcut: "I", From d49b68ae2958160d54f060bf35420040c89144f3 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sun, 4 Dec 2022 19:35:44 -0300 Subject: [PATCH 08/26] add options to brushes for opacity, and mask blur Signed-off-by: Victor Seiji Hariki --- js/ui/tool/colorbrush.js | 27 +++++++++++++++++++-- js/ui/tool/maskbrush.js | 52 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/js/ui/tool/colorbrush.js b/js/ui/tool/colorbrush.js index 738c6ae..c460534 100644 --- a/js/ui/tool/colorbrush.js +++ b/js/ui/tool/colorbrush.js @@ -3,7 +3,12 @@ const _color_brush_draw_callback = (evn, state) => { ctx.strokeStyle = state.color; - ctx.filter = "blur(" + state.brushBlur + "px)"; + ctx.filter = + "blur(" + + state.brushBlur + + "px) opacity(" + + state.brushOpacity * 100 + + "%)"; ctx.lineWidth = state.brushSize; ctx.beginPath(); ctx.moveTo( @@ -13,6 +18,7 @@ const _color_brush_draw_callback = (evn, state) => { ctx.lineTo(evn.x, evn.y); ctx.lineJoin = ctx.lineCap = "round"; ctx.stroke(); + ctx.filter = null; }; const _color_brush_erase_callback = (evn, state, ctx) => { @@ -130,6 +136,7 @@ const colorBrushTool = () => state.color = "#FFFFFF"; state.brushSize = 32; state.brushBlur = 0; + state.brushOpacity = 1; state.affectMask = true; state.setBrushSize = (size) => { state.brushSize = size; @@ -327,6 +334,7 @@ const colorBrushTool = () => const cropped = cropCanvas(canvas, {border: 10}); const bb = cropped.bb; + uil.ctx.filter = null; uil.ctx.clearRect(0, 0, uil.canvas.width, uil.canvas.height); uil.ctx.drawImage(bkpcanvas, 0, 0); @@ -366,7 +374,21 @@ const colorBrushTool = () => state.ctxmenu.brushSizeSlider = brushSizeSlider.slider; state.setBrushSize = brushSizeSlider.setValue; - // Brush size slider + // Brush opacity slider + const brushOpacitySlider = _toolbar_input.slider( + state, + "brushOpacity", + "Brush Opacity", + { + min: 0, + max: 1, + step: 0.05, + textStep: 0.001, + } + ); + state.ctxmenu.brushOpacitySlider = brushOpacitySlider.slider; + + // Brush blur slider const brushBlurSlider = _toolbar_input.slider( state, "brushBlur", @@ -417,6 +439,7 @@ const colorBrushTool = () => menu.appendChild(state.ctxmenu.affectMaskCheckbox); menu.appendChild(state.ctxmenu.brushSizeSlider); + menu.appendChild(state.ctxmenu.brushOpacitySlider); menu.appendChild(state.ctxmenu.brushBlurSlider); menu.appendChild(state.ctxmenu.brushColorPicker); }, diff --git a/js/ui/tool/maskbrush.js b/js/ui/tool/maskbrush.js index dd9de32..fee3b91 100644 --- a/js/ui/tool/maskbrush.js +++ b/js/ui/tool/maskbrush.js @@ -22,10 +22,12 @@ const setMask = (state) => { } }; -const _mask_brush_draw_callback = (evn, state) => { +const _mask_brush_draw_callback = (evn, state, opacity = 100) => { maskPaintCtx.globalCompositeOperation = "source-over"; maskPaintCtx.strokeStyle = "black"; + maskPaintCtx.filter = + "blur(" + state.brushBlur + "px) opacity(" + opacity + "%)"; maskPaintCtx.lineWidth = state.brushSize; maskPaintCtx.beginPath(); maskPaintCtx.moveTo( @@ -35,12 +37,16 @@ const _mask_brush_draw_callback = (evn, state) => { maskPaintCtx.lineTo(evn.x, evn.y); maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round"; maskPaintCtx.stroke(); + maskPaintCtx.filter = null; }; -const _mask_brush_erase_callback = (evn, state) => { +const _mask_brush_erase_callback = (evn, state, opacity = 100) => { maskPaintCtx.globalCompositeOperation = "destination-out"; maskPaintCtx.strokeStyle = "black"; + maskPaintCtx.filter = "blur(" + state.brushBlur + "px)"; + maskPaintCtx.filter = + "blur(" + state.brushBlur + "px) opacity(" + opacity + "%)"; maskPaintCtx.lineWidth = state.brushSize; maskPaintCtx.beginPath(); maskPaintCtx.moveTo( @@ -50,6 +56,7 @@ const _mask_brush_erase_callback = (evn, state) => { maskPaintCtx.lineTo(evn.x, evn.y); maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round"; maskPaintCtx.stroke(); + maskPaintCtx.filter = null; }; const maskBrushTool = () => @@ -95,9 +102,13 @@ const maskBrushTool = () => brushScrollSpeed: 1 / 4, minBrushSize: 10, maxBrushSize: 500, + minBlur: 0, + maxBlur: 30, }; state.brushSize = 64; + state.brushBlur = 0; + state.brushOpacity = 1; state.setBrushSize = (size) => { state.brushSize = size; state.ctxmenu.brushSizeRange.value = size; @@ -156,12 +167,16 @@ const maskBrushTool = () => } }; - state.drawcb = (evn) => _mask_brush_draw_callback(evn, state); - state.erasecb = (evn) => _mask_brush_erase_callback(evn, state); + state.drawcb = (evn) => + _mask_brush_draw_callback(evn, state, state.brushOpacity * 100); + state.erasecb = (evn) => + _mask_brush_erase_callback(evn, state, state.brushOpacity * 100); }, populateContextMenu: (menu, state) => { if (!state.ctxmenu) { state.ctxmenu = {}; + + // Brush size slider const brushSizeSlider = _toolbar_input.slider( state, "brushSize", @@ -181,6 +196,33 @@ const maskBrushTool = () => state.ctxmenu.brushSizeSlider = brushSizeSlider.slider; state.setBrushSize = brushSizeSlider.setValue; + // Brush opacity slider + const brushOpacitySlider = _toolbar_input.slider( + state, + "brushOpacity", + "Brush Opacity", + { + min: 0, + max: 1, + step: 0.05, + textStep: 0.001, + } + ); + state.ctxmenu.brushOpacitySlider = brushOpacitySlider.slider; + + // Brush blur slider + const brushBlurSlider = _toolbar_input.slider( + state, + "brushBlur", + "Brush Blur", + { + min: state.config.minBlur, + max: state.config.maxBlur, + step: 1, + } + ); + state.ctxmenu.brushBlurSlider = brushBlurSlider.slider; + // Some mask-related action buttons const actionArray = document.createElement("div"); actionArray.classList.add("button-array"); @@ -225,6 +267,8 @@ const maskBrushTool = () => } menu.appendChild(state.ctxmenu.brushSizeSlider); + menu.appendChild(state.ctxmenu.brushOpacitySlider); + menu.appendChild(state.ctxmenu.brushBlurSlider); menu.appendChild(state.ctxmenu.actionArray); }, shortcut: "M", From 986466f6d864ed598e53b621a5d9fac977067b9f Mon Sep 17 00:00:00 2001 From: tim h Date: Sun, 4 Dec 2022 20:19:34 -0600 Subject: [PATCH 09/26] fixes issue 68 --- js/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/js/index.js b/js/index.js index 822a4e1..6ffb60e 100644 --- a/js/index.js +++ b/js/index.js @@ -774,7 +774,14 @@ async function upscaleAndDownload() { // get cropped canvas, send it to upscaler, download result var upscale_factor = 2; // TODO: make this a user input 1.x - 4.0 or something var upscaler = document.getElementById("upscalers").value; - var croppedCanvas = cropCanvas(uil.canvas); + var croppedCanvas = cropCanvas( + uil.getVisible({ + x: 0, + y: 0, + w: imageCollection.size.w, + h: imageCollection.size.h, + }) + ); if (croppedCanvas != null) { var upscaler = document.getElementById("upscalers").value; var url = From 4a76eecd92e2e770719fe7091ad7f91f0bf41a27 Mon Sep 17 00:00:00 2001 From: tim h Date: Sun, 4 Dec 2022 13:42:24 -0600 Subject: [PATCH 10/26] surprise interrogate tool --- index.html | 1 + js/initalize/shortcuts.populate.js | 3 + js/initalize/toolbar.populate.js | 5 + js/ui/tool/interrogate.js | 151 +++++++++++++++++++++++++++++ res/icons/microscope.svg | 9 ++ 5 files changed, 169 insertions(+) create mode 100644 js/ui/tool/interrogate.js create mode 100644 res/icons/microscope.svg diff --git a/index.html b/index.html index 89755b3..7443ab7 100644 --- a/index.html +++ b/index.html @@ -283,6 +283,7 @@ +