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