Added Replace targeted layer mode.
This commit is contained in:
@@ -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.
|
||||

|
||||
|
||||
|
||||
# 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
@@ -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 => {
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user