Added Replace targeted layer mode.

This commit is contained in:
Neil Graham
2023-11-30 20:45:13 +13:00
parent cdd0a4c1cb
commit ce397abb17
5 changed files with 96 additions and 38 deletions
+31 -14
View File
@@ -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
+42 -23
View File
@@ -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 = $(`<div class="layer_widget ${layer===pic.activeLayer?'active':''}" draggable="true">
const result = $(`<div class="layer_widget ${layer===pic.activeLayer?'active':''} ${targetLayer==layer?'target':''}" draggable="true">
<div class= "visibilitybox ${layer.visible?'showing':''}"> </div>
<canvas class="thumbnail" width="32" height="32"> </canvas>
<div class="layer_name"> ${layer.title} </div>
@@ -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 => {
+1
View File
@@ -19,6 +19,7 @@
<select id="import_mode">
<option value="layer">Add layer <small>(if size matches)</small></option>
<option value="image">Create new Image</option>
<option value="replace_target">Replace Targeted Layer</option>
<option value="ignore">Ignore</option>
</select>
</div>
+17 -1
View File
@@ -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")
+5
View File
@@ -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;