1 Commits
20 changed files with 132 additions and 184 deletions
+16 -16
View File
@@ -19,8 +19,8 @@ This node can flip latent and merge original and flipped version.
- `latent`
**Usage:**
![sample](https://i.imgur.com/YMyYorQ.png)
![sample](https://i.imgur.com/W5BasCO.png)
![sample](assets/latent-mirror-usage-1.png)
![sample](assets/latent-mirror-usage-2.png)
## Latent shift
This node can shift latent along x and y-axis.
@@ -36,7 +36,7 @@ This node can shift latent along x and y-axis.
- `latent`
**Usage:**
![sample](https://i.imgur.com/1Dp5dSw.png)
![sample](assets/latent-shift-usage.png)
## ~~TSampler with transforms (Latent Control)~~
Removed from version 2.0.0
@@ -55,8 +55,8 @@ exactly matches the base KSampler
exactly matches the base KSampler
**Usage:**
![sample](https://i.imgur.com/PlGnAtA.png)
![sample](https://i.imgur.com/CtrBRPn.png)
![sample](assets/tsampler-usage-1.png)
![sample](assets/tsampler-usage-2.png)
Multiply, Mirror and Shift transform nodes parameters exactly match the corresponding `KSampler with transforms (Latent Control)` parameters.
@@ -78,9 +78,9 @@ You can apply specific offset for transform nodes.
- `offset`
**Usage:**
![sample](https://i.imgur.com/ExZacqG.png)
![sample](https://i.imgur.com/tR6KSmI.png)
![sample](https://i.imgur.com/MGVLfve.png)
![sample](assets/offset-usage-1.png)
![sample](assets/offset-usage-2.png)
![sample](assets/offset-usage-3.png)
You can combine different offsets to achieve interesting patterns. For example:
**0 0 0 1** and **0 0 1** give this pattern: **0 0 1 1 0 1 0 1 1 0 0 1**.
@@ -90,7 +90,7 @@ You can combine different offsets to achieve interesting patterns. For example:
Each transform node has own one-time version. They allow to make one transform action at specified step.
**Usage:**
![sample](https://i.imgur.com/Q1Vyob0.png)
![sample](assets/one-time-nodes-usage.png)
## Latent normalize
@@ -105,21 +105,21 @@ exactly matches the `VAE Decode` node
When you multiply latent by negative or big positive (bigger than 2) number and paste this latent in sampler, you can see that the
image will be generated very poorly. This is because stable diffusion cannot work with such set of numbers (meaning the numbers contained in latent).
![sample](https://i.imgur.com/3FXk8n7.png)
![sample](assets/latent-normalize-poor-result.png)
But you can prevent this behavior by sequential decode and encode latent using vae. Node `Latent normalize` make this process easier.
![sample](https://i.imgur.com/hkFYYVh.png)
![sample](assets/latent-normalize-fix.png)
This node also change some results even if output without this node looks good.
![sample](https://i.imgur.com/kP0f6vh.png)
![sample](https://i.imgur.com/YI8ZqLd.png)
![sample](assets/latent-normalize-comparison-1.png)
![sample](assets/latent-normalize-comparison-2.png)
And it very slightly changes results from latent, which have not been modified.
![sample](https://i.imgur.com/xTU08xm.png)
![sample](https://i.imgur.com/yzgW7QT.png)
![sample](assets/latent-normalize-unmodified-1.png)
![sample](assets/latent-normalize-unmodified-2.png)
## Transform hijack
@@ -138,7 +138,7 @@ during sampling.
- model
**Usage:**
![sample](https://i.imgur.com/YwVhHYF.png)
![sample](assets/transform-hijack-usage.png)
> **Breaking change in 3.0.0:** `Transform hijack` now takes and returns a `MODEL` instead of a
> `LATENT`. This replaces the old global `common_ksampler` monkey-patch, which conflicted with the
Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

+38 -97
View File
@@ -1,122 +1,63 @@
import { app } from "/scripts/app.js";
import {addCanvas, computeCanvasSize, generatePattern, recursiveLinkUpstream, renameNodeInputs, removeNodeInputs} from "./utils.js";
import {addCustomCanvasWidget, generatePattern, recursiveLinkUpstream, renameNodeInputs, removeNodeInputs} from "./utils.js";
function drawSquares(ctx, widgetX, widgetY, squareSize, pattern) {
const actualSquareSize = squareSize - Math.floor(squareSize / 16);
widgetY += Math.floor(squareSize / 16) / 2;
widgetX += Math.floor(squareSize / 16) / 2;
function drawSquares(ctx, startX, startY, squareSize, pattern) {
const inset = Math.max(1, Math.floor(squareSize / 16));
const cell = squareSize - inset;
pattern.forEach((value, index) => {
const x = widgetX + index * squareSize;
const x = startX + index * squareSize + inset / 2;
const y = startY + inset / 2;
ctx.fillStyle = value === 1 ? "#222223" : "#00000000";
ctx.lineWidth = inset;
ctx.strokeStyle = "#222223";
ctx.lineWidth = Math.floor(squareSize / 16);
if (value === 1) {
ctx.fillRect(x, widgetY, actualSquareSize, actualSquareSize);
ctx.fillStyle = "#222223";
ctx.fillRect(x, y, cell, cell);
}
ctx.strokeRect(x, widgetY, actualSquareSize, actualSquareSize);
ctx.strokeRect(x, y, cell, cell);
if (squareSize >= 24) {
ctx.font = `bold ${squareSize/3}px Arial`;
ctx.font = `bold ${squareSize / 3}px Arial`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.text
ctx.fillStyle = value === 1 ? "#dbdbdc" : "#222223";
ctx.fillText(value.toString(), x + actualSquareSize/2, widgetY + actualSquareSize/2);
ctx.fillText(value.toString(), x + cell / 2, y + cell / 2);
}
});
}
const offsetWidget = {
type: "customCanvas",
name: "Offset-Canvas",
get value() {
return this.canvas.value;
},
set value(x) {
this.canvas.value = x;
},
draw: function (ctx, node, widgetWidth, widgetY) {
if (!node.canvasHeight) {
computeCanvasSize(node, node.size)
}
// Builds the 0/1 preview pattern from the node's own widgets, or (for
// OffsetCombine) from every upstream offset node feeding into it.
function getOffsetPattern(node) {
let patterns = []
let patterns = []
if (node.type === "OffsetCombine") {
const connectedNodes = recursiveLinkUpstream(node, node.inputs[0].type, node.type, 0)
if (connectedNodes.length !== 0) {
for (let [node_ID, depth] of connectedNodes) {
const connectedNode = node.graph._nodes_by_id[node_ID]
if (connectedNode.type !== "OffsetCombine") {
const pattern = {
process_every: connectedNode.widgets[0].value,
offset: connectedNode.widgets[1].value + node.widgets[0].value,
mode: connectedNode.widgets[2].value
}
patterns.push(pattern)
}
}
if (node.type === "OffsetCombine") {
const connectedNodes = recursiveLinkUpstream(node, node.inputs[0].type, node.type, 0)
for (let [node_ID] of connectedNodes) {
const connectedNode = node.graph._nodes_by_id[node_ID]
if (connectedNode && connectedNode.type !== "OffsetCombine") {
patterns.push({
process_every: connectedNode.widgets[0].value,
offset: connectedNode.widgets[1].value + node.widgets[0].value,
mode: connectedNode.widgets[2].value,
})
}
} else {
const pattern = {
process_every: node.widgets[0].value,
offset: node.widgets[1].value,
mode: node.widgets[2].value}
patterns.push(pattern)
}
} else {
patterns.push({
process_every: node.widgets[0].value,
offset: node.widgets[1].value,
mode: node.widgets[2].value,
})
}
const pattern = generatePattern(patterns)
if (patterns.length === 0) return []
const visible = true
const t = ctx.getTransform();
const margin = 10
const widgetHeight = node.canvasHeight
const width = pattern.length * 32
const height = 32
const scale = Math.min((widgetWidth-margin*2)/width, (widgetHeight-margin*2)/height)
Object.assign(this.canvas.style, {
left: `${t.e}px`,
top: `${t.f + (widgetY*t.d)}px`,
width: `${widgetWidth * t.a}px`,
height: `${widgetHeight * t.d}px`,
position: "absolute",
zIndex: 1,
fontSize: `${t.d * 10.0}px`,
pointerEvents: "none",
});
this.canvas.hidden = !visible;
let backgroundWidth = width * scale
let backgroundHeight = height * scale
let xOffset = margin
if (backgroundWidth < widgetWidth) {
xOffset += (widgetWidth-backgroundWidth)/2 - margin
}
let yOffset = margin
if (backgroundHeight < widgetHeight) {
yOffset += (widgetHeight-backgroundHeight)/2 - margin
}
let widgetX = xOffset
widgetY = widgetY + yOffset
const squareSize = backgroundWidth / pattern.length;
drawSquares(ctx, widgetX, widgetY, squareSize, pattern)
ctx.fillStyle = "#ffffff88"
ctx.fillRect(widgetX, widgetY, backgroundWidth, backgroundHeight);
},
};
return generatePattern(patterns)
}
app.registerExtension({
name: "Comfy.LatentControl.TransformOffset",
@@ -126,7 +67,7 @@ app.registerExtension({
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
addCanvas(this, app, offsetWidget)
addCustomCanvasWidget(this, "Offset-Canvas", getOffsetPattern, drawSquares)
return r;
}
@@ -142,7 +83,7 @@ app.registerExtension({
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
addCanvas(this, app, offsetWidget)
addCustomCanvasWidget(this, "Offset-Canvas", getOffsetPattern, drawSquares)
this.getExtraMenuOptions = function(_, options) {
options.unshift(
+77 -70
View File
@@ -1,52 +1,3 @@
export function computeCanvasSize(node, size) {
if (node.widgets[0].last_y == null) return;
const MIN_SIZE = 64;
const inputs = node.inputs === undefined ? 0 : node.inputs.length
const outputs = node.outputs === undefined ? 0 : node.outputs.length
let y = LiteGraph.NODE_WIDGET_HEIGHT * Math.max(inputs, outputs) + 5;
let freeSpace = size[1] - y;
// Compute the height of all non customtext widgets
let widgetHeight = 0;
for (let i = 0; i < node.widgets.length; i++) {
const w = node.widgets[i];
if (w.type !== "customCanvas") {
if (w.computeSize) {
widgetHeight += w.computeSize()[1] + 4;
} else {
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 5;
}
}
}
// See how large the canvas can be
freeSpace -= widgetHeight;
// There isnt enough space for all the widgets, increase the size of the node
if (freeSpace < MIN_SIZE) {
freeSpace = MIN_SIZE;
node.size[1] = y + widgetHeight + freeSpace;
node.graph.setDirtyCanvas(true);
}
// Position each of the widgets
for (const w of node.widgets) {
w.y = y;
if (w.type === "customCanvas") {
y += freeSpace;
} else if (w.computeSize) {
y += w.computeSize()[1] + 4;
} else {
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
node.canvasHeight = freeSpace;
}
function gcd(a, b) {
// Функция для вычисления наибольшего общего делителя (НОД)
while (b !== 0) {
@@ -123,34 +74,90 @@ export function removeNodeInputs(node, indexesToRemove) {
node.removeInput(i)
}
node.onResize(node.size)
node.setSize(node.computeSize())
node.graph?.setDirtyCanvas(true, true)
}
export function addCanvas(node, app, widget) {
widget.canvas = document.createElement("canvas");
widget.canvas.className = "latent-control-custom-canvas";
const MARGIN_X = 15; // side padding — matches the node's standard widgets
const MARGIN_Y = 10; // top/bottom padding around the grid (node-local px)
const MIN_SQUARE = 24; // smallest square that still shows the row on tiny/long patterns
const MAX_SQUARE = 72; // largest square, so a short pattern doesn't blow up
const MIN_NODE_WIDTH = 220; // enough width for the squares to read
widget.parent = node;
document.body.appendChild(widget.canvas);
// Square side that fits the given (full node) width and column count. Squares
// shrink to fit a long pattern (down to 1px) and are capped so a short one stays
// reasonable. Guarantees squareSize * cols <= width - 2*MARGIN_X, i.e. no overflow.
function fitSquareSize(width, cols) {
const availWidth = width - MARGIN_X * 2;
return Math.max(1, Math.min(availWidth / Math.max(cols, 1), MAX_SQUARE));
}
/**
* Adds a self-contained canvas widget that draws a pattern preview directly onto
* the LiteGraph node context. Works with the current ComfyUI frontend (Nodes v2):
* it paints in node-local coordinates, so it scales and positions correctly with
* zoom and DPI — no DOM overlay and no manual ctx.getTransform() math.
*
* The preview auto-sizes to the space available inside the node: computeSize()
* reports exactly the height the grid needs for the current width and pattern
* length, so the node reserves the right amount of room, the squares grow/shrink
* as the node gets wider/narrower or the pattern longer/shorter, and the drawing
* always stays inside the node body.
*
* @param node the LiteGraph node
* @param name widget name
* @param getPattern (node) => number[] the 0/1 pattern to render
* @param drawSquares (ctx, startX, startY, squareSize, pattern) => void
*/
export function addCustomCanvasWidget(node, name, getPattern, drawSquares) {
const widget = {
type: "customCanvas",
name,
value: undefined,
options: { serialize: false },
computeSize() {
// Use the real node width, not the passed value: LiteGraph feeds a
// widened width to selected nodes, which would inflate the reserve.
const nodeWidth = node.size[0];
const cols = getPattern(node).length || 1;
const rowHeight = Math.max(fitSquareSize(nodeWidth, cols), MIN_SQUARE);
return [nodeWidth, rowHeight + MARGIN_Y * 2];
},
draw(ctx, node, width, widgetY) {
const pattern = getPattern(node);
if (!pattern || !pattern.length) return;
// Use node.size[0] rather than the `width` argument: on a selected node
// LiteGraph passes an enlarged width, which would push the grid right and
// peg the square size at its cap. node.size[0] is stable in both states.
const nodeWidth = node.size[0];
const cols = pattern.length;
const squareSize = fitSquareSize(nodeWidth, cols);
// Same band height computeSize reserved -> the grid stays inside the node.
const bandHeight = Math.max(squareSize, MIN_SQUARE) + MARGIN_Y * 2;
const gridWidth = squareSize * cols;
const startX = (nodeWidth - gridWidth) / 2;
const startY = widgetY + (bandHeight - squareSize) / 2;
// Light backing drawn first so empty cells stay readable on dark nodes.
ctx.fillStyle = "#ffffffcc";
ctx.fillRect(startX, startY, gridWidth, squareSize);
drawSquares(ctx, startX, startY, squareSize, pattern);
},
};
node.addCustomWidget(widget);
app.canvas.onDrawBackground = function () {
for (let n in app.graph._nodes) {
n = graph._nodes[n];
for (let w in n.widgets) {
let wid = n.widgets[w];
if (Object.hasOwn(wid, "canvas")) {
wid.canvas.style.left = -8000 + "px";
wid.canvas.style.position = "absolute";
}
}
}
};
// Widen a touch and reserve the preview height on creation. On workflow load
// LiteGraph applies the saved size after onNodeCreated, so this does not fight
// persisted sizes.
node.size[0] = Math.max(node.size[0], MIN_NODE_WIDTH);
node.setSize(node.computeSize());
node.onResize = function (size) {
computeCanvasSize(node, size);
}
return widget;
}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-advanced-latent-control"
description = "This custom node helps to transform latent in different ways."
version = "3.0.1"
version = "3.1.0"
license = "LICENSE"
[project.urls]