diff --git a/README.md b/README.md index 2322a8e..098e69f 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,56 @@ # canvas_tab ComfyUI canvas editor page +## Updates + + - 2023-11-30 Added "Replace Targeted Layer" as an input mode, right click on a layer to set it as target to be replaced + This plugin provides two nodes to provide a full page editor that runs in another tab. There is an input node `Edit in another Tab ` and an output node `Send to Editor Tab`. Both are stored in the images submenu. +## Installation +The recommended way to install is via comfy manager. This will automatically handle the web directory. + +### Manual installation +You can manually install by cloning this repo into the custom_nodes directory and either creating a symlink or copying the files in the web sub directory into web/extensions/canvas_tab. + +using a symlink from web/extensions/canvas_tab ==> custom_nodes/canvas_tab/web should avoid having to re-copy files when you update. + +## User Interface +You can edit multiple images at once. +Drag images around with the middle mouse button and scale them with the mouse wheel. There is a green Tab on the side of images in the editor, click on that tab to highlight it. The image with the highlighted tab is sent through to the comfyUI node. You can have multiple image layers and you can select generated images to be -added as new layers or new images. +added as a new layer, replace an existing layers, or as a new image. -You can drag images around with the middle mouse button and scale them with the mouseWheel. +You can delete layers by clicking on the layer widget with Ctrl-LeftClick. The layer must be visible for this to work as a protection against unwittingly deleting something important. -You can delete layers by clicking on the layer widget with Ctrl-LefTClick. Ctrl-click on palette entries reassigns the palette color tho the current color. Middle-click on palette entries sets the palette color to the current foreground color. Both nodes provided by this extension support receiving files by drag and drop to send images directly to the editor. +## Hotkeys + + - B for Brush tool + - E for Erase tool + - Z for pixel editing tool + - P for color Picker + - BackSpace to clear a layer + - ALT_BackSpace to fill the layer with the foreground color + - CTRL_Backspace to fill the layer with the background color + - CTRL_Z undo + - CTRL_SHIFT_Z redo + + +##Why would you do such a thing? My main motivation for making this was to develop an inpainting workflow, but I have also found it quite useful for scribble based images, @@ -41,16 +69,5 @@ That workflow should be embedded in this image. ![Image with embedded Inpaint workflow](https://raw.githubusercontent.com/Lerc/canvas_tab/main/Inpaint_Onion.png) -# Hotkeys - - - B for Brush tool - - E for Erase tool - - Z for pixel editing tool - - P for color Picker - - BackSpace to clear a layer - - ALT_BackSpace to fill the layer with the foreground color - - CTRL_Backspace to fill the layer with the background color - - CTRL_Z undo - - CTRL_SHIFT_Z redo diff --git a/web/page/bspaint.js b/web/page/bspaint.js index 6bd0586..8172ced 100644 --- a/web/page/bspaint.js +++ b/web/page/bspaint.js @@ -43,6 +43,7 @@ var brushSizeControl = brushDiameterControl("tip_diameter"); var picStack = []; var activePic; +var targetLayer = null; function setActivePic(newValue) { activePic=newValue; @@ -91,7 +92,8 @@ class Layer { compositeOperation = "source-over"; opacity = 1; _maskColor = lastUsedMaskColor; - constructor (title, {width,height}, mask = false) { + constructor (pic,title, {width,height}, mask = false) { + this.parentPic = pic; this.canvas.width=width; this.canvas.height=height; this.mask = mask; @@ -183,14 +185,7 @@ function createDrawArea(canvas = blankCanvas()) { const undoStack = []; const redoStack = []; const layers = []; - - layers.push( new Layer("base", canvas)); - var mask =new Layer("mask", canvas,true); - mask.opacity = 0.6; - layers.push(mask); - - layers[0].ctx.putImageData(image,0,0); - activeOperationCanvas.ctx.putImageData(image,0,0); + var mask; eventOverlay.addEventListener("mousedown", handleMouseDown); eventOverlay.addEventListener("mouseup", handleMouseUp); @@ -198,7 +193,7 @@ function createDrawArea(canvas = blankCanvas()) { eventOverlay.addEventListener("contextmenu",function(e){e.preventDefault(); return false;}); - const result = {element,eventOverlay,image, + const pic = {element,eventOverlay,image, get layers() {return layers}, get canvas() {return canvas}, get mask() {return mask}, @@ -208,7 +203,7 @@ function createDrawArea(canvas = blankCanvas()) { offsetX:0, offsetY:0, isDrawing:false, - activeLayer:layers[0], + activeLayer:null, strokeCoordinates :[], strokeModifier: a=>a, setTransform( ) { @@ -393,7 +388,7 @@ function createDrawArea(canvas = blankCanvas()) { }, */ addEmptyLayer() { - const newLayer = new Layer("new layer", canvas); + const newLayer = new Layer(this,"new layer", canvas); const newList = [...layers, newLayer]; this.updateLayerList(newList); return newLayer; @@ -456,16 +451,28 @@ function createDrawArea(canvas = blankCanvas()) { } this.updateVisualRepresentation(true); updateLayerList(); - }, - + }, } - eventOverlay.pic = result; - sidebar.addEventListener("mousedown",_=> setExportPic(result)) - closeButton.addEventListener("click",_=> closePic(result)) - result.setTransform(); - setActivePic(result) - return result; + layers.push( new Layer(pic,"base", canvas)); + + mask = new Layer(pic,"mask", canvas,true); + mask.opacity = 0.6; + layers.push(mask); + + pic.activeLayer= layers[0]; + + layers[0].ctx.putImageData(image,0,0); + activeOperationCanvas.ctx.putImageData(image,0,0); + + + eventOverlay.pic = pic; + sidebar.addEventListener("mousedown",_=> setExportPic(pic)) + closeButton.addEventListener("click",_=> closePic(pic)) + + pic.setTransform(); + setActivePic(pic) + return pic; } @@ -639,7 +646,7 @@ function addNewLayer(image) { } let pic = selectedExport; if (pic.canvas.width === image.width && pic.canvas.height==image.height) { - const layer = new Layer("generation",pic.canvas); + const layer = new Layer(pic,"generation",pic.canvas); layer.ctx.drawImage(image,0,0); console.log("new layer", layer) console.log(layer.canvas.width, layer.canvas.height) @@ -1141,7 +1148,7 @@ function updateLayerTitle(newTitle, layer) { function updateLayerList() { function makeLayerWidget(layer) { const pic=activePic; - const result = $(`
+ const result = $(`
${layer.title}
@@ -1155,7 +1162,7 @@ function updateLayerList() { ctx.drawImage(layer.canvas,0,0,canvas.width,canvas.height); result.layer=layer; - // New Code for Renaming + // Renaming handler const layerNameDiv = result.querySelector('.layer_name'); layerNameDiv.ondblclick = function() { const input = document.createElement('input'); @@ -1179,12 +1186,24 @@ function updateLayerList() { input.focus(); }; + + result.querySelector(".visibilitybox").onmousedown = e => { e.stopPropagation(); layer.visible=!layer.visible; pic.updateVisualRepresentation(true); updateLayerList(); } + result.addEventListener("contextmenu", e=>e.preventDefault()) + + result.addEventListener('mousedown', (e) => { + // Check for non-mask layer, middle mouse button click or left click with Ctrl key + if (!layer.mask && (e.button === 2) ) { + targetLayer = (targetLayer === layer) ? null : layer; + e.preventDefault(); + updateLayerList(); + } + }); if (layer.mask) { result.querySelector(".checkbox").onmousedown = e => { diff --git a/web/page/index.html b/web/page/index.html index cd45c42..1fbd678 100644 --- a/web/page/index.html +++ b/web/page/index.html @@ -19,6 +19,7 @@
diff --git a/web/page/main.js b/web/page/main.js index ce94619..3d87e74 100644 --- a/web/page/main.js +++ b/web/page/main.js @@ -58,8 +58,13 @@ function loadImagefromURL(url) { break; case "image": setExportPic(addNewImage(img)); - break; + case "replace_target": + if (targetLayer !== null) { + replaceLayerContent(targetLayer, img); + } + break; + case "ignore": default: } @@ -67,6 +72,17 @@ function loadImagefromURL(url) { img.src = url; } +function replaceLayerContent(layer, image) { + const ctx = layer.canvas.getContext('2d'); + ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height); + ctx.drawImage(image, 0, 0, layer.canvas.width, layer.canvas.height); + + if (activePic===layer.parentPic) { + updateLayerList(); + } + layer.parentPic.updateVisualRepresentation(false); +} + window.addEventListener('load', () => { if (getSignal('clientPage')) { console.log("but we're the client page, something is amiss") diff --git a/web/page/style.css b/web/page/style.css index e9c833f..1da06da 100644 --- a/web/page/style.css +++ b/web/page/style.css @@ -417,6 +417,11 @@ input[type="color"] { line-height: 36px; width: 60%; } + +.layer_widget.target .layer_name::before { + color:red; + content: "◎"; +} #layer_control .layer_name_input { width: 80%; border: none;