381 lines
15 KiB
JavaScript
381 lines
15 KiB
JavaScript
import { findWidget, widgetNumber, widgetString, widgetBoolean, hideWidgetsByName, setWidgetValue, setWidgetStringValue, setWidgetStringValuesByName, setWidgetBooleanValue } from "../shared/widgets.js";
|
|
import { ensureState } from "../shared/state.js";
|
|
import {
|
|
setDarkColorInputState,
|
|
styleSoftButton,
|
|
syncDarkColorInputUI
|
|
} from "../shared/dom-styles.js";
|
|
import {
|
|
normalizeDrawColor,
|
|
normalizeDrawEdge,
|
|
normalizeDrawTool,
|
|
normalizeDrawOverlayFormat,
|
|
clampDrawDimension,
|
|
clampDrawOpacity,
|
|
clampDrawSize,
|
|
clampDrawSoftness,
|
|
canvasToOverlayData
|
|
} from "../draw.js";
|
|
const NODE_CLASS = "ImageOpsDraw";
|
|
function isNode(node) {
|
|
return String(node?.comfyClass ?? "") === NODE_CLASS;
|
|
}
|
|
function styleSegmentControl(container) {
|
|
container.style.display = "flex";
|
|
container.style.background = "#181818";
|
|
container.style.borderRadius = "6px";
|
|
container.style.padding = "2px";
|
|
container.style.border = "1px solid #2e2e2e";
|
|
container.style.width = "100%";
|
|
container.style.boxSizing = "border-box";
|
|
}
|
|
function styleSegmentButton(button, active) {
|
|
button.style.flex = "1";
|
|
button.style.border = "none";
|
|
button.style.outline = "none";
|
|
button.style.borderRadius = "4px";
|
|
button.style.padding = "4px 8px";
|
|
button.style.fontSize = "10.5px";
|
|
button.style.fontFamily = "var(--comfy-font-sans, Inter, sans-serif)";
|
|
button.style.cursor = "pointer";
|
|
button.style.transition = "background 0.15s, color 0.15s";
|
|
button.style.lineHeight = "1.2";
|
|
button.style.textAlign = "center";
|
|
if (active) {
|
|
button.classList.add("active");
|
|
button.style.background = "#525252";
|
|
button.style.color = "#ffffff";
|
|
button.style.fontWeight = "600";
|
|
} else {
|
|
button.classList.remove("active");
|
|
button.style.background = "transparent";
|
|
button.style.color = "#aaaaaa";
|
|
button.style.fontWeight = "normal";
|
|
}
|
|
}
|
|
function setupSegmentHover(button) {
|
|
button.addEventListener("mouseenter", () => {
|
|
if (!button.classList.contains("active")) {
|
|
button.style.background = "rgba(255,255,255,0.06)";
|
|
button.style.color = "#ffffff";
|
|
}
|
|
});
|
|
button.addEventListener("mouseleave", () => {
|
|
if (!button.classList.contains("active")) {
|
|
button.style.background = "transparent";
|
|
button.style.color = "#aaaaaa";
|
|
}
|
|
});
|
|
}
|
|
function createDrawControlsUi() {
|
|
const controls = document.createElement("div");
|
|
controls.style.marginTop = "8px";
|
|
controls.style.display = "grid";
|
|
controls.style.gridTemplateColumns = "72px 1fr";
|
|
controls.style.rowGap = "8px";
|
|
controls.style.columnGap = "12px";
|
|
controls.style.alignItems = "center";
|
|
controls.style.width = "100%";
|
|
controls.style.boxSizing = "border-box";
|
|
controls.style.padding = "0 4px";
|
|
const styleLabel = (el) => {
|
|
el.style.fontSize = "11px";
|
|
el.style.opacity = "0.78";
|
|
el.style.fontFamily = "var(--comfy-font-sans, Inter, sans-serif)";
|
|
el.style.whiteSpace = "nowrap";
|
|
};
|
|
const toolLabel = document.createElement("div");
|
|
toolLabel.textContent = "Tool";
|
|
styleLabel(toolLabel);
|
|
const brushButton = document.createElement("button");
|
|
brushButton.type = "button";
|
|
brushButton.textContent = "Brush";
|
|
styleSegmentButton(brushButton, true);
|
|
setupSegmentHover(brushButton);
|
|
const eraserButton = document.createElement("button");
|
|
eraserButton.type = "button";
|
|
eraserButton.textContent = "Eraser";
|
|
styleSegmentButton(eraserButton, false);
|
|
setupSegmentHover(eraserButton);
|
|
const toolGroup = document.createElement("div");
|
|
styleSegmentControl(toolGroup);
|
|
toolGroup.appendChild(brushButton);
|
|
toolGroup.appendChild(eraserButton);
|
|
controls.appendChild(toolLabel);
|
|
controls.appendChild(toolGroup);
|
|
const edgeLabel = document.createElement("div");
|
|
edgeLabel.textContent = "Edge";
|
|
styleLabel(edgeLabel);
|
|
const edgeSelect = document.createElement("select");
|
|
edgeSelect.style.display = "none";
|
|
for (const edge of ["hard", "soft"]) {
|
|
const option = document.createElement("option");
|
|
option.value = edge;
|
|
option.textContent = edge === "hard" ? "Hard" : "Soft";
|
|
edgeSelect.appendChild(option);
|
|
}
|
|
const hardButton = document.createElement("button");
|
|
hardButton.type = "button";
|
|
hardButton.textContent = "Hard";
|
|
styleSegmentButton(hardButton, true);
|
|
setupSegmentHover(hardButton);
|
|
const softButton = document.createElement("button");
|
|
softButton.type = "button";
|
|
softButton.textContent = "Soft";
|
|
styleSegmentButton(softButton, false);
|
|
setupSegmentHover(softButton);
|
|
edgeSelect.__hardButton = hardButton;
|
|
edgeSelect.__softButton = softButton;
|
|
const updateEdgeUi = (edge) => {
|
|
styleSegmentButton(hardButton, edge === "hard");
|
|
styleSegmentButton(softButton, edge === "soft");
|
|
};
|
|
hardButton.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
edgeSelect.value = "hard";
|
|
edgeSelect.dispatchEvent(new Event("change"));
|
|
updateEdgeUi("hard");
|
|
});
|
|
softButton.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
edgeSelect.value = "soft";
|
|
edgeSelect.dispatchEvent(new Event("change"));
|
|
updateEdgeUi("soft");
|
|
});
|
|
const edgeGroup = document.createElement("div");
|
|
styleSegmentControl(edgeGroup);
|
|
edgeGroup.appendChild(hardButton);
|
|
edgeGroup.appendChild(softButton);
|
|
edgeGroup.appendChild(edgeSelect);
|
|
controls.appendChild(edgeLabel);
|
|
controls.appendChild(edgeGroup);
|
|
const clearButton = document.createElement("button");
|
|
clearButton.type = "button";
|
|
clearButton.textContent = "\u21BA Clear";
|
|
clearButton.style.border = "1px solid #3f3f3f";
|
|
clearButton.style.background = "#2a2a2a";
|
|
clearButton.style.color = "#ccc";
|
|
clearButton.style.borderRadius = "4px";
|
|
clearButton.style.padding = "4px 16px";
|
|
clearButton.style.cursor = "pointer";
|
|
clearButton.style.fontSize = "11px";
|
|
clearButton.style.lineHeight = "1.2";
|
|
clearButton.style.transition = "background 0.15s ease, color 0.15s ease, border-color 0.15s ease";
|
|
clearButton.addEventListener("mouseenter", () => {
|
|
clearButton.style.background = "#3e3e3e";
|
|
clearButton.style.color = "#fff";
|
|
clearButton.style.borderColor = "#555";
|
|
});
|
|
clearButton.addEventListener("mouseleave", () => {
|
|
clearButton.style.background = "#2a2a2a";
|
|
clearButton.style.color = "#ccc";
|
|
clearButton.style.borderColor = "#3f3f3f";
|
|
});
|
|
clearButton.addEventListener("mousedown", () => {
|
|
clearButton.style.background = "#1c1c1c";
|
|
clearButton.style.color = "#aaa";
|
|
});
|
|
clearButton.addEventListener("mouseup", () => {
|
|
clearButton.style.background = "#3e3e3e";
|
|
clearButton.style.color = "#fff";
|
|
});
|
|
return {
|
|
controls,
|
|
brushButton,
|
|
eraserButton,
|
|
clearButton,
|
|
colorInput: null,
|
|
edgeSelect,
|
|
softnessInput: null,
|
|
softnessLabel: null,
|
|
opacityInput: null,
|
|
opacityLabel: null,
|
|
sizeInput: null,
|
|
sizeLabel: null,
|
|
widthInput: null,
|
|
heightInput: null,
|
|
linkButton: null,
|
|
bgColorInput: null,
|
|
overlayFormatSelect: null,
|
|
pressureSizeInput: null,
|
|
pressureOpacityInput: null,
|
|
tiltSizeInput: null
|
|
};
|
|
}
|
|
function hideDrawWidgets(node) {
|
|
const namesToHide = [
|
|
"overlay_data",
|
|
"overlay_layers",
|
|
"tool",
|
|
"brush_edge"
|
|
];
|
|
for (const name of namesToHide) {
|
|
hideWidgetsByName(node, name);
|
|
}
|
|
}
|
|
function canvasToDrawSourcePoint(geometry, x, y) {
|
|
if (!geometry) return { x: 0, y: 0, inside: false };
|
|
const localX = (x - geometry.fitDx) / Math.max(1, geometry.fitDrawWidth);
|
|
const localY = (y - geometry.fitDy) / Math.max(1, geometry.fitDrawHeight);
|
|
const inside = localX >= 0 && localX <= 1 && localY >= 0 && localY <= 1;
|
|
return {
|
|
x: Math.max(0, Math.min(geometry.sourceWidth, localX * geometry.sourceWidth)),
|
|
y: Math.max(0, Math.min(geometry.sourceHeight, localY * geometry.sourceHeight)),
|
|
inside
|
|
};
|
|
}
|
|
function getDrawInfoText(node, width, height) {
|
|
const inputConnected = (node.inputs?.[0]?.link ?? null) != null;
|
|
return inputConnected ? `Paint preview (paint over input, ${width}x${height})` : `Paint preview (${width}x${height})`;
|
|
}
|
|
function updateDrawToolButtons(node) {
|
|
const st = ensureState(node);
|
|
const tool = normalizeDrawTool(widgetString(node, "tool", "brush"));
|
|
if (st.drawBrushButton) styleSegmentButton(st.drawBrushButton, tool === "brush");
|
|
if (st.drawEraserButton) styleSegmentButton(st.drawEraserButton, tool === "eraser");
|
|
}
|
|
function updateDrawOverlayWidget(node) {
|
|
const st = ensureState(node);
|
|
const value = canvasToOverlayData(st.drawCanvas, normalizeDrawOverlayFormat(widgetString(node, "overlay_format", "png")));
|
|
const layers = value ? JSON.stringify({ version: 1, active: 0, layers: [{ name: "Layer 1", enabled: true, opacity: 1, data: value }] }) : "";
|
|
st.drawOverlayKey = `${layers}
|
|
${value}`;
|
|
setWidgetStringValue(findWidget(node, "overlay_data"), value);
|
|
setWidgetStringValue(findWidget(node, "overlay_layers"), layers);
|
|
}
|
|
function syncDrawWidgets(node, changedName) {
|
|
if (!isNode(node)) return;
|
|
hideDrawWidgets(node);
|
|
const st = ensureState(node);
|
|
const widthWidget = findWidget(node, "width");
|
|
const heightWidget = findWidget(node, "height");
|
|
const linkWidget = findWidget(node, "sync_dimensions");
|
|
const bgWidget = findWidget(node, "bg_color");
|
|
const colorWidget = findWidget(node, "brush_color");
|
|
const edgeWidget = findWidget(node, "brush_edge");
|
|
const opacityWidget = findWidget(node, "brush_opacity");
|
|
const sizeWidget = findWidget(node, "brush_size");
|
|
const pressureSizeWidget = findWidget(node, "brush_pressure_size");
|
|
const pressureOpacityWidget = findWidget(node, "brush_pressure_opacity");
|
|
const tiltSizeWidget = findWidget(node, "brush_tilt_size");
|
|
const overlayFormatWidget = findWidget(node, "overlay_format");
|
|
const softnessWidget = findWidget(node, "brush_softness");
|
|
if (!widthWidget || !heightWidget) return;
|
|
let width = clampDrawDimension(widgetNumber(node, "width", 1024));
|
|
let height = clampDrawDimension(widgetNumber(node, "height", 1024));
|
|
const linked = widgetBoolean(node, "sync_dimensions", true);
|
|
if (!linked || st.drawAspectRatio == null || changedName === "sync_dimensions") {
|
|
st.drawAspectRatio = Math.max(1, width) / Math.max(1, height);
|
|
}
|
|
if (linked && st.drawAspectRatio) {
|
|
if (changedName === "height") {
|
|
width = clampDrawDimension(Math.round(height * st.drawAspectRatio), width);
|
|
} else if (changedName === "width") {
|
|
height = clampDrawDimension(Math.round(width / st.drawAspectRatio), height);
|
|
}
|
|
}
|
|
setWidgetValue(widthWidget, width);
|
|
setWidgetValue(heightWidget, height);
|
|
const inputConnected = (node.inputs?.[0]?.link ?? null) != null;
|
|
if (st.drawWidthInput) {
|
|
st.drawWidthInput.value = String(width);
|
|
st.drawWidthInput.disabled = inputConnected;
|
|
}
|
|
if (st.drawHeightInput) {
|
|
st.drawHeightInput.value = String(height);
|
|
st.drawHeightInput.disabled = inputConnected;
|
|
}
|
|
if (st.drawLinkButton) {
|
|
st.drawLinkButton.textContent = linked ? "Linked" : "Free";
|
|
styleSoftButton(st.drawLinkButton, linked);
|
|
st.drawLinkButton.disabled = inputConnected;
|
|
st.drawLinkButton.style.opacity = inputConnected ? "0.55" : "1";
|
|
}
|
|
if (st.drawBgColorInput) {
|
|
const bgColor = normalizeDrawColor(widgetString(node, "bg_color", "#000000"), "#000000");
|
|
setWidgetStringValuesByName(node, "bg_color", bgColor, { notify: false, dirty: false });
|
|
syncDarkColorInputUI(st.drawBgColorInput, bgColor);
|
|
setDarkColorInputState(st.drawBgColorInput, inputConnected);
|
|
}
|
|
if (st.drawColorInput) {
|
|
const brushColor = normalizeDrawColor(widgetString(node, "brush_color", "#FFFFFF"), "#FFFFFF");
|
|
setWidgetStringValuesByName(node, "brush_color", brushColor, { notify: false, dirty: false });
|
|
syncDarkColorInputUI(st.drawColorInput, brushColor);
|
|
}
|
|
if (st.drawEdgeSelect) {
|
|
const edgeMode = normalizeDrawEdge(widgetString(node, "brush_edge", "hard"));
|
|
st.drawEdgeSelect.value = edgeMode;
|
|
const hb = st.drawEdgeSelect.__hardButton;
|
|
const sb = st.drawEdgeSelect.__softButton;
|
|
if (hb) styleSegmentButton(hb, edgeMode === "hard");
|
|
if (sb) styleSegmentButton(sb, edgeMode === "soft");
|
|
if (st.drawSoftnessInput) {
|
|
const softnessPercent = Math.round(clampDrawSoftness(widgetNumber(node, "brush_softness", 0.5), 0.5) * 100);
|
|
const hardness = edgeMode === "hard" ? 100 : 100 - softnessPercent;
|
|
st.drawSoftnessInput.value = String(hardness);
|
|
st.drawSoftnessInput.title = `Brush edge hardness ${hardness}%`;
|
|
if (st.drawSoftnessLabel) {
|
|
st.drawSoftnessLabel.textContent = `${hardness}%`;
|
|
}
|
|
}
|
|
}
|
|
if (st.drawOpacityInput) {
|
|
const opacity = Math.round(clampDrawOpacity(widgetNumber(node, "brush_opacity", 1), 1) * 100);
|
|
st.drawOpacityInput.value = String(opacity);
|
|
st.drawOpacityInput.title = `Brush opacity ${opacity}%`;
|
|
if (st.drawOpacityLabel) st.drawOpacityLabel.textContent = `${opacity}%`;
|
|
}
|
|
if (st.drawSizeInput) {
|
|
const size = clampDrawSize(widgetNumber(node, "brush_size", 10), 10);
|
|
st.drawSizeInput.value = String(size);
|
|
st.drawSizeInput.title = `Brush size ${size}`;
|
|
if (st.drawSizeLabel) st.drawSizeLabel.textContent = String(size);
|
|
}
|
|
if (st.drawOverlayFormatSelect) {
|
|
st.drawOverlayFormatSelect.value = normalizeDrawOverlayFormat(widgetString(node, "overlay_format", "png"));
|
|
}
|
|
if (st.drawPressureSizeInput) {
|
|
st.drawPressureSizeInput.checked = widgetBoolean(node, "brush_pressure_size", true);
|
|
}
|
|
if (st.drawPressureOpacityInput) {
|
|
st.drawPressureOpacityInput.checked = widgetBoolean(node, "brush_pressure_opacity", true);
|
|
}
|
|
if (st.drawTiltSizeInput) {
|
|
st.drawTiltSizeInput.checked = widgetBoolean(node, "brush_tilt_size", false);
|
|
}
|
|
setWidgetBooleanValue(linkWidget, linked);
|
|
if (st.drawBgColorInput && !inputConnected) setWidgetStringValue(bgWidget, st.drawBgColorInput.value);
|
|
if (st.drawColorInput) setWidgetStringValue(colorWidget, st.drawColorInput.value);
|
|
if (st.drawSoftnessInput) {
|
|
const hardness = Number(st.drawSoftnessInput.value);
|
|
if (hardness === 100) {
|
|
setWidgetStringValue(edgeWidget, "hard");
|
|
setWidgetValue(softnessWidget, 0);
|
|
} else {
|
|
setWidgetStringValue(edgeWidget, "soft");
|
|
setWidgetValue(softnessWidget, (100 - hardness) / 100);
|
|
}
|
|
}
|
|
if (st.drawOpacityInput) setWidgetValue(opacityWidget, Number(st.drawOpacityInput.value) / 100);
|
|
if (st.drawSizeInput) setWidgetValue(sizeWidget, Number(st.drawSizeInput.value));
|
|
if (st.drawWidthInput && !inputConnected) setWidgetValue(widthWidget, Number(st.drawWidthInput.value));
|
|
if (st.drawHeightInput && !inputConnected) setWidgetValue(heightWidget, Number(st.drawHeightInput.value));
|
|
if (st.drawPressureSizeInput) setWidgetBooleanValue(pressureSizeWidget, st.drawPressureSizeInput.checked);
|
|
if (st.drawPressureOpacityInput) setWidgetBooleanValue(pressureOpacityWidget, st.drawPressureOpacityInput.checked);
|
|
if (st.drawTiltSizeInput) setWidgetBooleanValue(tiltSizeWidget, st.drawTiltSizeInput.checked);
|
|
if (st.drawOverlayFormatSelect) setWidgetStringValue(overlayFormatWidget, st.drawOverlayFormatSelect.value);
|
|
updateDrawToolButtons(node);
|
|
}
|
|
export {
|
|
NODE_CLASS,
|
|
canvasToDrawSourcePoint,
|
|
createDrawControlsUi,
|
|
getDrawInfoText,
|
|
hideDrawWidgets,
|
|
isNode,
|
|
styleSegmentButton,
|
|
syncDrawWidgets,
|
|
updateDrawOverlayWidget,
|
|
updateDrawToolButtons
|
|
};
|