diff --git a/index.html b/index.html index 6d0955c..317521b 100644 --- a/index.html +++ b/index.html @@ -86,16 +86,15 @@ value="-1" step="1" />
+ + +
-
- - -

diff --git a/js/index.js b/js/index.js index 7828b35..da9d063 100644 --- a/js/index.js +++ b/js/index.js @@ -60,7 +60,6 @@ var frameX = 512; var frameY = 512; var drawThis = {}; const basePixelCount = 64; //64 px - ALWAYS 64 PX -var scaleFactor = 8; //x64 px var snapToGrid = true; var backupMaskPaintCanvas; //??? var backupMaskPaintCtx; //...? look i am bad at this @@ -117,7 +116,6 @@ function startup() { changeSeed(); changeOverMaskPx(); changeHiResFix(); - document.getElementById("scaleFactor").value = scaleFactor; } /** @@ -497,15 +495,20 @@ const makeSlider = ( textStep = null, valuecb = null ) => { - const local = localStorage.getItem(lsKey); + const local = lsKey && localStorage.getItem(lsKey); const def = parseFloat(local === null ? defaultValue : local); + let cb = (v) => { + stableDiffusionData[lsKey] = v; + if (lsKey) localStorage.setItem(lsKey, v); + }; + if (valuecb) { + cb = (v) => { + valuecb(v); + localStorage.setItem(lsKey, v); + }; + } return createSlider(label, el, { - valuecb: - valuecb || - ((v) => { - stableDiffusionData[lsKey] = v; - localStorage.setItem(lsKey, v); - }), + valuecb: cb, min, max, step, @@ -514,6 +517,21 @@ const makeSlider = ( }); }; +makeSlider( + "Resolution", + document.getElementById("resolution"), + "resolution", + 64, + 1024, + 64, + 512, + 2, + (v) => { + stableDiffusionData.width = stableDiffusionData.height = v; + stableDiffusionData.firstphase_width = + stableDiffusionData.firstphase_height = v / 2; + } +); makeSlider( "CFG Scale", document.getElementById("cfgScale"), @@ -542,19 +560,6 @@ makeSlider( 1, 2 ); -makeSlider( - "Scale Factor", - document.getElementById("scaleFactor"), - "scale_factor", - 1, - 16, - 1, - 8, - null, - (v) => { - scaleFactor = v; - } -); makeSlider("Steps", document.getElementById("steps"), "steps", 1, 70, 5, 30, 1); diff --git a/js/lib/util.js b/js/lib/util.js index 6dd4c9a..5fa5f6d 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -145,21 +145,14 @@ function makeWriteOnce(obj, name = "write-once object", exceptions = []) { * Snaps a single value to an infinite grid * * @param {number} i Original value to be snapped - * @param {boolean} scaled If grid will change alignment for odd scaleFactor values (default: true) - * @param {number} gridSize Size of the grid + * @param {number} [offset=0] Value to offset the grid. Should be in the rande [0, gridSize[ + * @param {number} [gridSize=64] Size of the grid * @returns an offset, in which [i + offset = (a location snapped to the grid)] */ -function snap(i, scaled = true, gridSize = 64) { - // very cheap test proof of concept but it works surprisingly well - var scaleOffset = 0; - if (scaled) { - if (scaleFactor % 2 != 0) { - // odd number, snaps to center of cell, oops - scaleOffset = gridSize / 2; - } - } - const modulus = i % gridSize; - var snapOffset = modulus - scaleOffset; +function snap(i, offset = 0, gridSize = 64) { + const modulus = (i - offset) % gridSize; + var snapOffset = modulus; + if (modulus > gridSize / 2) snapOffset = modulus - gridSize; if (snapOffset == 0) { @@ -175,19 +168,20 @@ function snap(i, scaled = true, gridSize = 64) { * @param {number} cy - y-coordinate of the center of the box * @param {number} w - the width of the box * @param {height} h - the height of the box - * @param {number | null} gridSnap - The size of the grid to snap to + * @param {?number} gridSnap - The size of the grid to snap to + * @param {number} [offset=0] - How much to offset the grid by * @returns {BoundingBox} - A bounding box object centered at (cx, cy) */ -function getBoundingBox(cx, cy, w, h, gridSnap = null) { - const offset = {x: 0, y: 0}; +function getBoundingBox(cx, cy, w, h, gridSnap = null, offset = 0) { + const offs = {x: 0, y: 0}; const box = {x: 0, y: 0}; if (gridSnap) { - offset.x = snap(cx, true, gridSnap); - offset.y = snap(cy, true, gridSnap); + offs.x = snap(cx, offset, gridSnap); + offs.y = snap(cy, offset, gridSnap); } - box.x = offset.x + cx; - box.y = offset.y + cy; + box.x = offs.x + cx; + box.y = offs.y + cy; return { x: Math.floor(box.x - w / 2), diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 568c7d6..0281a47 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -125,13 +125,27 @@ const _generate = async (endpoint, request, bb) => { image.src = "data:image/png;base64," + images[at]; image.addEventListener("load", () => { layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height); - if (images[at]) layer.ctx.drawImage(image, bb.x, bb.y); + if (images[at]) + layer.ctx.drawImage( + image, + 0, + 0, + image.width, + image.height, + bb.x, + bb.y, + bb.w, + bb.h + ); }); }; const stopMarchingAnts = march(bb); // First Dream Run + console.info(`[dream] Generating images for prompt '${request.prompt}'`); + console.debug(request); + let stopProgress = _monitorProgress(bb); images.push(...(await _dream(endpoint, requestCopy))); stopProgress(); @@ -161,6 +175,8 @@ const _generate = async (endpoint, request, bb) => { commands.runCommand("drawImage", "Image Dream", { x: bb.x, y: bb.y, + w: bb.w, + h: bb.h, image: img, }); clean(true); @@ -316,8 +332,8 @@ const dream_generate_callback = async (evn, state) => { const bb = getBoundingBox( evn.x, evn.y, - basePixelCount * scaleFactor, - basePixelCount * scaleFactor, + state.cursorSize, + state.cursorSize, state.snapToGrid && basePixelCount ); @@ -332,13 +348,6 @@ const dream_generate_callback = async (evn, state) => { // Don't allow another image until is finished blockNewImages = true; - // Setup some basic information for SD - request.width = bb.w; - request.height = bb.h; - - request.firstphase_width = bb.w / 2; - request.firstphase_height = bb.h / 2; - // Use txt2img if canvas is blank if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) { // Dream @@ -355,13 +364,23 @@ const dream_generate_callback = async (evn, state) => { auxCtx.fillStyle = "#000F"; // Get init image - auxCtx.fillRect(0, 0, bb.w, bb.h); - auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + auxCtx.fillRect(0, 0, request.width, request.height); + auxCtx.drawImage( + imgCanvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + request.width, + request.height + ); request.init_images = [auxCanvas.toDataURL()]; // Get mask image auxCtx.fillStyle = "#000F"; - auxCtx.fillRect(0, 0, bb.w, bb.h); + 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 @@ -374,23 +393,45 @@ const dream_generate_callback = async (evn, state) => { bb.h, 0, 0, - bb.w, - bb.h + request.width, + request.height ); auxCtx.globalCompositeOperation = "destination-in"; - auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + auxCtx.drawImage( + imgCanvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + request.width, + request.height + ); } else { auxCtx.globalCompositeOperation = "destination-in"; - auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + auxCtx.drawImage( + imgCanvas, + bb.x, + bb.y, + 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, bb.w, bb.h); + auxCtx.fillRect(0, 0, request.width, request.height); + downloadCanvas({canvas: auxCanvas, filename: null}); applyOvermask(auxCanvas, auxCtx, state.overMaskPx); + downloadCanvas({canvas: auxCanvas, filename: null}); } auxCtx.globalCompositeOperation = "destination-out"; // ??? @@ -402,13 +443,13 @@ const dream_generate_callback = async (evn, state) => { bb.h, 0, 0, - bb.w, - bb.h + request.width, + request.height ); } auxCtx.globalCompositeOperation = "destination-atop"; auxCtx.fillStyle = "#FFFF"; - auxCtx.fillRect(0, 0, bb.w, bb.h); + auxCtx.fillRect(0, 0, request.width, request.height); request.mask = auxCanvas.toDataURL(); // Dream _generate("img2img", request, bb); @@ -419,8 +460,8 @@ const dream_erase_callback = (evn, state) => { const bb = getBoundingBox( evn.x, evn.y, - basePixelCount * scaleFactor, - basePixelCount * scaleFactor, + state.cursorSize, + state.cursorSize, state.snapToGrid && basePixelCount ); commands.runCommand("eraseImage", "Erase Area", bb); @@ -436,14 +477,25 @@ function applyOvermask(canvas, ctx, px) { if (ctxImgData.data[i] == 255) { // white pixel? // just blotch all over the thing - var rando = Math.floor(Math.random() * px); + /** + * This should probably have a better randomness profile for the overmasking + * + * Essentially, we want to have much more smaller values for randomness than big ones, + * because big values overshadow smaller circles and kinda ignores their randomness. + * + * And also, we want the profile to become more extreme the bigger the overmask size, + * because bigger px values also make bigger circles ocuppy more horizontal space. + */ + let lowRandom = + Math.atan(Math.random() * 10 - 10) / Math.abs(Math.atan(-10)) + 1; + lowRandom = Math.pow(lowRandom, px / 8); + + var rando = Math.floor(lowRandom * px); ctx.beginPath(); ctx.arc( (i / 4) % canvas.width, Math.floor(i / 4 / canvas.width), - scaleFactor + - rando + - (rando > scaleFactor ? rando / scaleFactor : scaleFactor / rando), // was 4 * sf + rando, too big, but i think i want it more ... random + rando, // was 4 * sf + rando, too big, but i think i want it more ... random 0, 2 * Math.PI, true @@ -462,8 +514,8 @@ const dream_img2img_callback = (evn, state) => { const bb = getBoundingBox( evn.x, evn.y, - basePixelCount * scaleFactor, - basePixelCount * scaleFactor, + state.cursorSize, + state.cursorSize, state.snapToGrid && basePixelCount ); @@ -484,13 +536,6 @@ const dream_img2img_callback = (evn, state) => { // Don't allow another image until is finished blockNewImages = true; - // Setup some basic information for SD - request.width = bb.w; - request.height = bb.h; - - request.firstphase_width = bb.w / 2; - request.firstphase_height = bb.h / 2; - // Use img2img // Temporary canvas for init image and mask generation @@ -502,36 +547,56 @@ const dream_img2img_callback = (evn, state) => { auxCtx.fillStyle = "#000F"; // Get init image - auxCtx.fillRect(0, 0, bb.w, bb.h); - auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + auxCtx.fillRect(0, 0, request.width, request.height); + auxCtx.drawImage( + imgCanvas, + bb.x, + bb.y, + 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, bb.w, bb.h); + 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, bb.w, bb.h); + 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, bb.w, bb.h); + auxCtx.fillRect(0, 0, request.width, request.height); // Border Mask if (state.keepBorderSize > 0) { auxCtx.globalCompositeOperation = "source-over"; auxCtx.fillStyle = "#000F"; - auxCtx.fillRect(0, 0, state.keepBorderSize, bb.h); - auxCtx.fillRect(0, 0, bb.w, state.keepBorderSize); + auxCtx.fillRect(0, 0, state.keepBorderSize, request.height); + auxCtx.fillRect(0, 0, request.width, state.keepBorderSize); auxCtx.fillRect( - bb.w - state.keepBorderSize, + request.width - state.keepBorderSize, 0, state.keepBorderSize, - bb.h + request.height ); auxCtx.fillRect( 0, - bb.h - state.keepBorderSize, - bb.w, + request.height - state.keepBorderSize, + request.width, state.keepBorderSize ); } @@ -547,13 +612,13 @@ const dream_img2img_callback = (evn, state) => { /** * Dream and img2img tools */ -const _reticle_draw = (evn, snapToGrid = true) => { +const _reticle_draw = (evn, state) => { const bb = getBoundingBox( evn.x, evn.y, - basePixelCount * scaleFactor, - basePixelCount * scaleFactor, - snapToGrid && basePixelCount + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount ); // draw targeting square reticle thingy cursor @@ -566,6 +631,20 @@ const _reticle_draw = (evn, snapToGrid = true) => { }; }; +/** + * Generic wheel handler + */ + +const _dream_onwheel = (evn, state) => { + if (!evn.evn.ctrlKey) { + const v = + state.cursorSize - + Math.floor(state.config.cursorSizeScrollSpeed * evn.delta); + state.cursorSize = state.setCursorSize(v + snap(v, 0, 128)); + state.mousemovecb(evn); + } +}; + /** * Registers Tools */ @@ -582,6 +661,7 @@ const dreamTool = () => // Start Listeners mouse.listen.world.onmousemove.on(state.mousemovecb); + mouse.listen.world.onwheel.on(state.wheelcb); mouse.listen.world.btn.left.onclick.on(state.dreamcb); mouse.listen.world.btn.right.onclick.on(state.erasecb); @@ -591,6 +671,7 @@ const dreamTool = () => (state, opt) => { // Clear Listeners mouse.listen.world.onmousemove.clear(state.mousemovecb); + mouse.listen.world.onwheel.clear(state.wheelcb); mouse.listen.world.btn.left.onclick.clear(state.dreamcb); mouse.listen.world.btn.right.onclick.clear(state.erasecb); @@ -599,6 +680,12 @@ const dreamTool = () => }, { init: (state) => { + state.config = { + cursorSizeScrollSpeed: 1, + }; + + state.cursorSize = 512; + state.snapToGrid = true; state.invertMask = false; state.overMaskPx = 0; @@ -608,7 +695,10 @@ const dreamTool = () => state.mousemovecb = (evn) => { state.erasePrevReticle(); - state.erasePrevReticle = _reticle_draw(evn, state.snapToGrid); + state.erasePrevReticle = _reticle_draw(evn, state); + }; + state.wheelcb = (evn) => { + _dream_onwheel(evn, state); }; state.dreamcb = (evn) => { dream_generate_callback(evn, state); @@ -619,6 +709,22 @@ const dreamTool = () => if (!state.ctxmenu) { state.ctxmenu = {}; + // Cursor Size Slider + const cursorSizeSlider = _toolbar_input.slider( + state, + "cursorSize", + "Cursor Size", + { + min: 0, + max: 2048, + step: 128, + textStep: 2, + } + ); + + state.setCursorSize = cursorSizeSlider.setValue; + state.ctxmenu.cursorSizeSlider = cursorSizeSlider.slider; + // Snap to Grid Checkbox state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox( state, @@ -643,12 +749,14 @@ const dreamTool = () => "Overmask px", { min: 0, - max: 128, - step: 1, + max: 64, + step: 5, + textStep: 1, } ).slider; } + menu.appendChild(state.ctxmenu.cursorSizeSlider); menu.appendChild(state.ctxmenu.snapToGridLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.invertMaskLabel); @@ -672,6 +780,7 @@ const img2imgTool = () => // Start Listeners mouse.listen.world.onmousemove.on(state.mousemovecb); + mouse.listen.world.onwheel.on(state.wheelcb); mouse.listen.world.btn.left.onclick.on(state.dreamcb); mouse.listen.world.btn.right.onclick.on(state.erasecb); @@ -681,6 +790,7 @@ const img2imgTool = () => (state, opt) => { // Clear Listeners mouse.listen.world.onmousemove.clear(state.mousemovecb); + mouse.listen.world.onwheel.clear(state.wheelcb); mouse.listen.world.btn.left.onclick.clear(state.dreamcb); mouse.listen.world.btn.right.onclick.clear(state.erasecb); @@ -689,6 +799,11 @@ const img2imgTool = () => }, { init: (state) => { + state.config = { + cursorSizeScrollSpeed: 1, + }; + + state.cursorSize = 512; state.snapToGrid = true; state.invertMask = true; state.fullResolution = false; @@ -702,40 +817,59 @@ const img2imgTool = () => state.mousemovecb = (evn) => { state.erasePrevReticle(); - state.erasePrevReticle = _reticle_draw(evn, state.snapToGrid); + state.erasePrevReticle = _reticle_draw(evn, state); const bb = getBoundingBox( evn.x, evn.y, - basePixelCount * scaleFactor, - basePixelCount * scaleFactor, + state.cursorSize, + state.cursorSize, state.snapToGrid && basePixelCount ); + // Resolution + const request = { + width: stableDiffusionData.width, + height: stableDiffusionData.height, + }; + // For displaying border mask const auxCanvas = document.createElement("canvas"); - auxCanvas.width = bb.w; - auxCanvas.height = bb.h; + auxCanvas.width = request.width; + auxCanvas.height = request.height; const auxCtx = auxCanvas.getContext("2d"); if (state.keepBorderSize > 0) { auxCtx.fillStyle = "#6A6AFF30"; - auxCtx.fillRect(0, 0, state.keepBorderSize, bb.h); - auxCtx.fillRect(0, 0, bb.w, state.keepBorderSize); + auxCtx.fillRect(0, 0, state.keepBorderSize, request.height); + auxCtx.fillRect(0, 0, request.width, state.keepBorderSize); auxCtx.fillRect( - bb.w - state.keepBorderSize, + request.width - state.keepBorderSize, 0, state.keepBorderSize, - bb.h + request.height ); auxCtx.fillRect( 0, - bb.h - state.keepBorderSize, - bb.w, + request.height - state.keepBorderSize, + request.width, state.keepBorderSize ); - ovCtx.drawImage(auxCanvas, bb.x, bb.y); + ovCtx.drawImage( + auxCanvas, + 0, + 0, + request.width, + request.height, + bb.x, + bb.y, + bb.w, + bb.h + ); } }; + state.wheelcb = (evn) => { + _dream_onwheel(evn, state); + }; state.dreamcb = (evn) => { dream_img2img_callback(evn, state); }; @@ -744,6 +878,23 @@ const img2imgTool = () => populateContextMenu: (menu, state) => { if (!state.ctxmenu) { state.ctxmenu = {}; + + // Cursor Size Slider + const cursorSizeSlider = _toolbar_input.slider( + state, + "cursorSize", + "Cursor Size", + { + min: 0, + max: 2048, + step: 128, + textStep: 2, + } + ); + + state.setCursorSize = cursorSizeSlider.setValue; + state.ctxmenu.cursorSizeSlider = cursorSizeSlider.slider; + // Snap To Grid Checkbox state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox( state, @@ -795,6 +946,7 @@ const img2imgTool = () => ).slider; } + menu.appendChild(state.ctxmenu.cursorSizeSlider); menu.appendChild(state.ctxmenu.snapToGridLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.invertMaskLabel); diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 92e667b..bdf8cfa 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -192,8 +192,8 @@ const selectTransformTool = () => let x = evn.x; let y = evn.y; if (state.snapToGrid) { - x += snap(evn.x, true, 64); - y += snap(evn.y, true, 64); + x += snap(evn.x, 0, 64); + y += snap(evn.y, 0, 64); } // Update scale @@ -337,8 +337,8 @@ const selectTransformTool = () => let ix = evn.ix; let iy = evn.iy; if (state.snapToGrid) { - ix += snap(evn.ix, true, 64); - iy += snap(evn.iy, true, 64); + ix += snap(evn.ix, 0, 64); + iy += snap(evn.iy, 0, 64); } // If is selected, check if drag is in handles/body and act accordingly @@ -368,8 +368,8 @@ const selectTransformTool = () => let x = evn.x; let y = evn.y; if (state.snapToGrid) { - x += snap(evn.x, true, 64); - y += snap(evn.y, true, 64); + x += snap(evn.x, 0, 64); + y += snap(evn.y, 0, 64); } // If we are scaling, stop scaling and do some handler magic @@ -502,7 +502,6 @@ const selectTransformTool = () => if (state.useClipboard) { // If we use the clipboard, do some proccessing of clipboard data (ugly but kind of minimum required) navigator.clipboard.read().then((items) => { - console.info(items[0]); for (const item of items) { for (const type of item.types) { if (type.startsWith("image/")) { diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index adc21c5..705991f 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -213,8 +213,8 @@ const stampTool = () => let x = evn.x; let y = evn.y; if (state.snapToGrid) { - x += snap(evn.x, true, 64); - y += snap(evn.y, true, 64); + x += snap(evn.x, 0, 64); + y += snap(evn.y, 0, 64); } state.lastMouseMove = evn; @@ -242,8 +242,8 @@ const stampTool = () => let x = evn.x; let y = evn.y; if (state.snapToGrid) { - x += snap(evn.x, true, 64); - y += snap(evn.y, true, 64); + x += snap(evn.x, 0, 64); + y += snap(evn.y, 0, 64); } const resource = state.selected;