1087 lines
35 KiB
JavaScript
1087 lines
35 KiB
JavaScript
import { app } from "../../../scripts/app.js";
|
|
import { allow_debug } from "./js_shared.js";
|
|
|
|
import {
|
|
Shapes,
|
|
Colors,
|
|
lightenColor,
|
|
canvasRatios,
|
|
canvasScales,
|
|
commonColors,
|
|
trackMouseColor,
|
|
getIndexByDimensions,
|
|
} from "./utils.js";
|
|
|
|
import { BaseSmartWidgetManager } from "./makadi/BaseSmartWidget.js";
|
|
import { domCtx } from "./makadi/DomCtx.js";
|
|
import { SmartButton } from "./makadi/SmartButton.js";
|
|
import { SmartWidget } from "./makadi/SmartWidget.js";
|
|
import { SmartSlider } from "./makadi/SmartSlider.js";
|
|
import { SmartSwitch } from "./makadi/SmartSwitch.js";
|
|
import { SmartPaintArea } from "./makadi/SmartPaintArea.js";
|
|
import { SmartPreview } from "./makadi/SmartPreview.js";
|
|
import { SmartColorPicker } from "./makadi/SmartColorPicker.js";
|
|
import { SmartDropdownMenu } from "./makadi/SmartDropdownMenu.js";
|
|
import { SmartImage } from "./makadi/SmartImage.js";
|
|
import { SmartInfo } from "./makadi/SmartInfo.js";
|
|
import { SmartText } from "./makadi/SmartText.js";
|
|
|
|
app.registerExtension({
|
|
name: "iTools.paintNode",
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeData.name !== "iToolsPaintNode") {
|
|
return;
|
|
}
|
|
},
|
|
|
|
async nodeCreated(node) {
|
|
if (node.comfyClass !== "iToolsPaintNode") {
|
|
return;
|
|
}
|
|
|
|
// wait fo init
|
|
for (let i = 0; i < 30 && node.graph === null; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
if (node.graph === null) {
|
|
if (allow_debug) console.log("Timeout waiting for graph initialization");
|
|
return;
|
|
}
|
|
|
|
// NODE SETTINGS
|
|
const NODE_W = 512;
|
|
const NODE_H = 592;
|
|
// Opt this node into DOM-canvas mode. BaseSmartWidget, DomCtx and the
|
|
// manager check this flag to pick the DOM path; all other hosts (crop,
|
|
// preview, compare, text_entry) leave it unset and keep legacy behavior.
|
|
node._useDomCtx = true;
|
|
node.setSize([NODE_W, NODE_H]);
|
|
node.resizable = false;
|
|
if (allow_debug) console.log("node", node);
|
|
|
|
// DOM container + inner canvas that hosts all Smart* widget rendering.
|
|
// Every Smart* widget will draw into innerCanvas instead of the litegraph
|
|
// foreground, and read mouse state via the domCtx singleton.
|
|
const container = document.createElement("div");
|
|
container.className = "itools-paint-widget";
|
|
container.style.cssText = `
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 100%;
|
|
height: 100%;
|
|
padding: 0;
|
|
margin: 0;
|
|
background-color: #2a2a2a;
|
|
overflow: hidden;
|
|
`;
|
|
|
|
const innerCanvas = document.createElement("canvas");
|
|
innerCanvas.width = NODE_W;
|
|
innerCanvas.height = NODE_H;
|
|
innerCanvas.style.cssText = `
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
touch-action: none;
|
|
outline: none;
|
|
`;
|
|
innerCanvas.tabIndex = 0;
|
|
container.appendChild(innerCanvas);
|
|
// Display canvas: no getImageData reads happen here (trackMouseColor
|
|
// reads via a separate pipeline), so leave willReadFrequently off to
|
|
// keep the GPU-accelerated backend.
|
|
const innerCtx = innerCanvas.getContext("2d", { alpha: false });
|
|
|
|
let _lastGraphScale = 0;
|
|
const widget = node.addDOMWidget("PaintWidget", "custom", container, {
|
|
tooltip: 'Paint Widget',
|
|
getValue: () => ({}),
|
|
setValue: (v) => {},
|
|
getMinHeight: () => NODE_H,
|
|
getMaxHeight: () => NODE_H * 2,
|
|
margin: 0,
|
|
onDraw: (ctx) => {
|
|
// Litegraph calls this every graph redraw. Use it to detect zoom
|
|
// changes (CSS transform on parent) so the backing bitmap can be
|
|
// resized to match the new on-screen footprint.
|
|
const s = app?.canvas?.ds?.scale || 1;
|
|
if (s !== _lastGraphScale) {
|
|
_lastGraphScale = s;
|
|
domCtx.requestRedraw();
|
|
}
|
|
},
|
|
});
|
|
widget.tooltip = 'Paint Widget';
|
|
// Intercept addCustomWidget BEFORE any Smart* widget is constructed, so
|
|
// widgets land in node._smartWidgets (owned by us) rather than in
|
|
// node.widgets (where litegraph would also try to draw them onto its own
|
|
// canvas). _smartWidgets preserves insertion order for our draw loop.
|
|
node._smartWidgets = [];
|
|
node.addCustomWidget = (w) => {
|
|
node._smartWidgets.push(w);
|
|
return w;
|
|
};
|
|
|
|
// START POINT
|
|
let canvasImgs = [];
|
|
let selectedImg = null;
|
|
let loadedImageFile = null;
|
|
let keyPick = false;
|
|
let isHoldingShift = false;
|
|
let isHoldingAlt = false;
|
|
let isHoldingSpace = false;
|
|
let loadedWidth = 0;
|
|
let loadedHeight = 0;
|
|
let loadedScale = 0;
|
|
let loader = null;
|
|
let mouseInNode = null;
|
|
|
|
// create canvas buttons
|
|
let bc = [];
|
|
let bCloseCanvas = null;
|
|
let bMaskCanvas = null;
|
|
let bStampCanvas = null;
|
|
let bFitCanvas = null;
|
|
let bFillCanvas = null;
|
|
let lCanvasInfo = null;
|
|
|
|
const ui = new SmartWidget(0, 30, node.width, 50, node, {
|
|
color: lightenColor(LiteGraph.WIDGET_BGCOLOR, 5),
|
|
shape: Shapes.SQUARE,
|
|
allowVisualPress: false,
|
|
allowVisualHover: false,
|
|
outline: false,
|
|
});
|
|
|
|
const startPosX = 5 || 512 / 2;
|
|
const bLoad = new SmartButton(startPosX, 5, 85, 20, node, "Load Image", {
|
|
textXoffset: 0,
|
|
isVisible: true,
|
|
shape: Shapes.ROUND_L,
|
|
});
|
|
bLoad.onClick = () => {
|
|
const fileInput = document.createElement("input");
|
|
fileInput.type = "file";
|
|
fileInput.accept = ".png, .jpg, .jpeg, .webp";
|
|
//fileInput.accept = ".png";
|
|
|
|
fileInput.addEventListener("change", (event) => {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
loadedImageFile = file;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
const imageUrl = e.target.result;
|
|
|
|
// Create a temporary image element to get its dimensions
|
|
const tempImg = new Image();
|
|
tempImg.src = imageUrl;
|
|
|
|
// Wait for the image to load
|
|
tempImg.onload = () => {
|
|
const naturalWidth = tempImg.naturalWidth;
|
|
const naturalHeight = tempImg.naturalHeight;
|
|
|
|
// Calculate the aspect ratio
|
|
const aspectRatio = naturalWidth / naturalHeight;
|
|
|
|
// Set a base height (or width) and calculate the other dimension based on the ratio
|
|
const baseHeight = 256; // You can adjust this value as needed
|
|
const calculatedWidth = baseHeight * aspectRatio;
|
|
|
|
// Calculate the center position for the image
|
|
const centerX = (node.width - calculatedWidth) / 2;
|
|
const centerY = (80 + node.height - baseHeight) / 2;
|
|
|
|
// Create a new SmartImage instance with dynamic dimensions
|
|
const img = new SmartImage(centerX, centerY, calculatedWidth, baseHeight, node, {});
|
|
|
|
// Update the image source dynamically
|
|
img.updateImage(imageUrl);
|
|
|
|
// Add the SmartImage instance to the canvasImgs array
|
|
canvasImgs.push(img);
|
|
// if (allow_debug) {
|
|
// console.log("canvasImgs", canvasImgs);
|
|
// }
|
|
|
|
// Define the onImgLoaded callback
|
|
img.onImgLoaded = () => {
|
|
// Remove old selected
|
|
canvasImgs.forEach((i) => {
|
|
i.isSelected = false;
|
|
});
|
|
// mark last loaded image as selected
|
|
canvasImgs[canvasImgs.length - 1].isSelected = true;
|
|
if (allow_debug) console.log("Image loaded successfully!");
|
|
// open canvas buttons
|
|
openCanvas();
|
|
};
|
|
img.onImgClosed = (img) => {
|
|
onImageClosed(img);
|
|
};
|
|
};
|
|
|
|
// Handle errors if the image fails to load
|
|
tempImg.onerror = () => {
|
|
if (allow_debug) console.error("Failed to load the image.");
|
|
};
|
|
};
|
|
|
|
// Read the file as a data URL
|
|
reader.readAsDataURL(file);
|
|
}
|
|
});
|
|
|
|
fileInput.click();
|
|
};
|
|
const bText = new SmartButton(startPosX + 85, 5, 85, 20, node, "Add Text", {
|
|
textXoffset: 0,
|
|
isVisible: true,
|
|
shape: Shapes.SQUARE,
|
|
});
|
|
bText.onClick = () => {
|
|
// window.prompt
|
|
window["app"].extensionManager.dialog
|
|
.prompt({
|
|
title: "iTools Paint Node",
|
|
message: "Add custom text",
|
|
default: "iTools",
|
|
})
|
|
.then((value) => {
|
|
if (!value || value.trim().length === 0) {
|
|
info2.showWarning("No Valid Text!");
|
|
return;
|
|
}
|
|
// Calculate the center position for the text
|
|
const centerX = (node.width - 102 * 2.5) / 2; // 26*2.5 is starting font size
|
|
const centerY = (80 + node.height - 102 * 2.5) / 2;
|
|
|
|
const t = new SmartText(centerX, centerY, node);
|
|
t.text = value;
|
|
t.textColor = bColor.color === "rgba(255, 255, 255, 0.0)" ? "black" : bColor.color;
|
|
cp.onValueChange = (v) => {
|
|
if (allow_debug) {
|
|
console.log("value changed");
|
|
}
|
|
t.textColor = v;
|
|
};
|
|
canvasImgs.push(t);
|
|
t.onClosed = () => onImageClosed();
|
|
// Remove old selected
|
|
canvasImgs.forEach((i) => {
|
|
i.isSelected = false;
|
|
});
|
|
// mark last loaded image as selected
|
|
canvasImgs[canvasImgs.length - 1].isSelected = true;
|
|
if (allow_debug) console.log("Image loaded successfully!");
|
|
// open canvas buttons
|
|
openCanvas("text");
|
|
});
|
|
};
|
|
const bPaste = new SmartButton(startPosX + 170, 5, 85, 20, node, "Paste Image", {
|
|
textXoffset: 0,
|
|
isVisible: true,
|
|
shape: Shapes.ROUND_R,
|
|
});
|
|
bPaste.onClick = async () => {
|
|
try {
|
|
const clipboardItems = await navigator.clipboard.read();
|
|
for (const item of clipboardItems) {
|
|
for (const type of item.types) {
|
|
if (type.startsWith("image/")) {
|
|
const blob = await item.getType(type);
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = function (event) {
|
|
const imageUrl = event.target.result;
|
|
|
|
// Create a temporary image element to get its dimensions
|
|
const tempImg = new Image();
|
|
tempImg.src = imageUrl;
|
|
// Wait for the image to load
|
|
tempImg.onload = () => {
|
|
const naturalWidth = tempImg.naturalWidth;
|
|
const naturalHeight = tempImg.naturalHeight;
|
|
|
|
// Calculate the aspect ratio
|
|
const aspectRatio = naturalWidth / naturalHeight;
|
|
|
|
// Set a base height (or width) and calculate the other dimension based on the ratio
|
|
const baseHeight = 256; // You can adjust this value as needed
|
|
const calculatedWidth = baseHeight * aspectRatio;
|
|
|
|
// Calculate the center position for the image
|
|
const centerX = (node.width - calculatedWidth) / 2;
|
|
const centerY = (80 + node.height - baseHeight) / 2;
|
|
|
|
// Create a new SmartImage instance with dynamic dimensions
|
|
const img = new SmartImage(centerX, centerY, calculatedWidth, baseHeight, node, {});
|
|
|
|
// Update the image source dynamically
|
|
img.updateImage(imageUrl);
|
|
|
|
// Add the SmartImage instance to the canvasImgs array
|
|
canvasImgs.push(img);
|
|
// if (allow_debug) {
|
|
// console.log("canvasImgs", canvasImgs);
|
|
// }
|
|
|
|
// Define the onImgLoaded callback
|
|
img.onImgLoaded = () => {
|
|
// Remove old selected
|
|
canvasImgs.forEach((i) => {
|
|
i.isSelected = false;
|
|
});
|
|
// mark last loaded image as selected
|
|
canvasImgs[canvasImgs.length - 1].isSelected = true;
|
|
if (allow_debug) console.log("Image loaded successfully!");
|
|
// open canvas buttons
|
|
openCanvas();
|
|
};
|
|
img.onImgClosed = (img) => {
|
|
onImageClosed(img);
|
|
};
|
|
};
|
|
};
|
|
|
|
reader.readAsDataURL(blob); // Read image as Base64 URL
|
|
return;
|
|
} else {
|
|
info2.showWarning("No Image In Clipboard!", 150);
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to read clipboard:", err);
|
|
}
|
|
};
|
|
|
|
const bColor = new SmartButton(5, 35, 40, 40, node);
|
|
bColor.shape = Shapes.HVL_CIRCLE;
|
|
bColor.color = "crimson";
|
|
(bColor.allowVisualHover = false),
|
|
(bColor.allowVisualPress = false),
|
|
(bColor.onPress = () => {
|
|
cp.open();
|
|
});
|
|
|
|
const bColor2 = new SmartButton(5, 35, 40, 40, node);
|
|
bColor2.shape = Shapes.HVR_CIRCLE;
|
|
bColor2.color = "orange";
|
|
(bColor2.allowVisualHover = false),
|
|
(bColor2.allowVisualPress = false),
|
|
(bColor2.onPress = () => {
|
|
cp.openHidden();
|
|
});
|
|
|
|
const brushSlider = new SmartSlider(55, 35, 150, 20, node, {
|
|
minValue: 1,
|
|
maxValue: 100,
|
|
value: 20,
|
|
textColorNormalize: true,
|
|
isProgressBar: true,
|
|
text: "Brush Size: ",
|
|
onValueChange: (value) => {
|
|
p.brushSize = value;
|
|
pa.brushSize = value;
|
|
},
|
|
});
|
|
|
|
const pa = new SmartPaintArea(0, 80, 512, 512, node);
|
|
const p = new SmartPreview(0, 80, 512, 512, node);
|
|
const cp = new SmartColorPicker(0, 80, 170, 170, node);
|
|
const info = new SmartInfo(512 / 2 - 40, 85, 80, 15, node, "canvas size");
|
|
const info2 = new SmartInfo(512 / 2 - 40, 85, 80, 15, node, "");
|
|
|
|
// create canvas drop menus
|
|
const ratiosArray = Array.from(canvasRatios.entries());
|
|
const sizesArray = Array.from(canvasScales.entries());
|
|
const ratioNames = Array.from(canvasRatios.keys());
|
|
const sizeNames = Array.from(canvasScales.keys());
|
|
let dmR = null;
|
|
let dmS = null;
|
|
let dmInfo = null;
|
|
|
|
function createDropMenus() {
|
|
dmR = new SmartDropdownMenu(5, 85, 40, 15, node, "Ratio", ratioNames);
|
|
dmS = new SmartDropdownMenu(5 + 45, 85, 40, 15, node, "Size", sizeNames);
|
|
const infoXpos = 512 / 2 - 40 || 5 + 45 + 45;
|
|
dmInfo = new SmartButton(infoXpos, 85, 80, 15, node, `${pa.width} x ${pa.height}`);
|
|
// start invisible any way
|
|
dmInfo.isVisible = false;
|
|
dmR.isVisible = false;
|
|
dmS.isVisible = false;
|
|
|
|
dmR.onSelect = () => {
|
|
if (dmR.isVisible) resizeCanvas(dmR, dmS, ratiosArray, sizesArray, pa, dmInfo);
|
|
};
|
|
dmS.onSelect = () => {
|
|
if (dmS.isVisible) resizeCanvas(dmR, dmS, ratiosArray, sizesArray, pa, dmInfo);
|
|
};
|
|
}
|
|
createDropMenus();
|
|
|
|
const bCanvas = new SmartButton(55, 60, 50, 15, node, "Canvas", {
|
|
textXoffset: 0,
|
|
resetColor: false,
|
|
});
|
|
bCanvas.onClick = () => {
|
|
bCanvas.toggleActive();
|
|
if (dmR.isVisible) {
|
|
dmR.isVisible = false;
|
|
dmS.isVisible = false;
|
|
dmInfo.isVisible = false;
|
|
} else {
|
|
createDropMenus(); // recreate menus to draw last
|
|
updateDMindexes(); // re-update indexes
|
|
dmInfo.isVisible = true;
|
|
dmR.isVisible = true;
|
|
dmS.isVisible = true;
|
|
}
|
|
};
|
|
|
|
const bUndo = new SmartButton(190 - 20, 60, 15, 15, node, "↺", {
|
|
textXoffset: 0,
|
|
});
|
|
bUndo.onClick = () => {
|
|
pa.undo();
|
|
};
|
|
|
|
const bRedo = new SmartButton(190, 60, 15, 15, node, "↻", {
|
|
textXoffset: 0,
|
|
});
|
|
bRedo.onClick = () => {
|
|
pa.redo();
|
|
};
|
|
|
|
const bFill = new SmartButton(215, 35, 40, 15, node, "Fill", {
|
|
withTagWidth: 10,
|
|
textXoffset: 5,
|
|
});
|
|
bFill.onClick = () => {
|
|
pa.fillWithColor(bColor.color);
|
|
};
|
|
|
|
const bHold = new SmartButton(215 + 45, 35, 40, 15, node, "Hold", {
|
|
textXoffset: 0,
|
|
});
|
|
bHold.onClick = () => {
|
|
pa.saveTempImage();
|
|
};
|
|
|
|
const bFetch = new SmartButton(215 + 45 + 45, 35, 40, 15, node, "Fetch", {
|
|
textXoffset: 0,
|
|
});
|
|
bFetch.onClick = () => {
|
|
pa.loadTempImage();
|
|
};
|
|
|
|
const bClear = new SmartButton(215 + 45 + 45 + 45, 35, 40, 15, node, "Clear", {
|
|
textXoffset: 0,
|
|
});
|
|
bClear.onClick = () => {
|
|
pa.clearWithColor("white");
|
|
let text = pa.isPaintingBackground ? "Background" : "Foreground";
|
|
info2.showWarning(`${text} cleared`, 140, pa.isPaintingBackground ? "#cd7f32" : "#5f9ea0");
|
|
};
|
|
|
|
// Create layer switch
|
|
const layerSwitch = new SmartSwitch(215, 55, 175, 20, node);
|
|
layerSwitch.textOn = "Foreground";
|
|
layerSwitch.textOff = "Background";
|
|
layerSwitch.onValueChange = () => {
|
|
pa.switchLayer();
|
|
if (lCanvasInfo) {
|
|
lCanvasInfo.text = getActiveCtxText().text;
|
|
lCanvasInfo.color = getActiveCtxText().color;
|
|
}
|
|
};
|
|
|
|
// Create colors buttons
|
|
const bh = [];
|
|
let bhIndex = 0;
|
|
for (let i = 1; i < 6; i++) {
|
|
for (let j = 0; j < 2; j++) {
|
|
const color = commonColors[bhIndex % commonColors.length]; // Capture color in scope
|
|
const t = color === "rgba(255, 255, 255, 0.0)" ? "E" : "";
|
|
const widget = new SmartButton(512 - 22 * i - 3, 35 + 22 * j, 18, 18, node, "", {
|
|
color: color,
|
|
text: t,
|
|
shape: Shapes.CIRCLE,
|
|
onClick: () => {
|
|
bColor.color = color;
|
|
bFill.tagColor = color;
|
|
pa.brushColor = color;
|
|
canvasImgs.forEach((item) => {
|
|
if (item.isSelected && item.isTextObject) {
|
|
if (color === "rgba(255, 255, 255, 0.0)") {
|
|
item.textColor = "black";
|
|
} else {
|
|
item.textColor = color;
|
|
}
|
|
}
|
|
});
|
|
if (this.allowDebug) console.log(`Widget ${bhIndex} clicked`);
|
|
},
|
|
});
|
|
|
|
bh.push(widget);
|
|
bhIndex++; // Increment unique index
|
|
}
|
|
}
|
|
|
|
// COMMON FUNCTIONS
|
|
// function showWarning(msg, newWidth = 120, newColor = "#cd7f32") {
|
|
// info.color = newColor;
|
|
// info.textColor = "black";
|
|
// info.show(msg, newWidth, 85 + 20, 20);
|
|
|
|
// setTimeout(() => {
|
|
// info.color = info.originalColor;
|
|
// info.textColor = info.originalTextColor;
|
|
// info.done = true;
|
|
// }, info.previewDuration);
|
|
// }
|
|
|
|
function getActiveCtxText() {
|
|
let text = pa.isPaintingBackground ? "Background" : "Foreground";
|
|
let color = pa.isPaintingBackground ? "#cd7f32" : "#5f9ea0"; //"#deb887";
|
|
return { text: text, color: color };
|
|
}
|
|
|
|
function onImageClosed() {
|
|
canvasImgs = canvasImgs.filter((img) => !img.markDelete);
|
|
}
|
|
|
|
function closeCanvas() {
|
|
bCloseCanvas.isVisible = false;
|
|
// enable brush preview
|
|
p.isVisible = true;
|
|
pa.isCheckerboardOn = false;
|
|
// mark delete all canvas images
|
|
canvasImgs.forEach((img) => {
|
|
if (img.loader) img.loader.markDelete = true;
|
|
img.markDelete = true;
|
|
});
|
|
//clear canvas images list
|
|
canvasImgs = [];
|
|
// mark delete all buttons
|
|
bc.forEach((b) => (b.markDelete = true));
|
|
}
|
|
|
|
function openCanvas(mode = "image") {
|
|
createCanvasButtons(mode);
|
|
// disable brush preview
|
|
p.isVisible = false;
|
|
pa.isCheckerboardOn = true;
|
|
// open all buttons
|
|
bc.forEach((b) => (b.isVisible = true));
|
|
}
|
|
|
|
function toggleImagesCloseButton() {
|
|
//if(allow_debug){console.log('canvasImgs.length',canvasImgs.length);}
|
|
// allow close when multiple images loaded
|
|
if (canvasImgs.length > 1) {
|
|
canvasImgs.forEach((i) => {
|
|
i.closeButton = true;
|
|
});
|
|
} else {
|
|
canvasImgs.forEach((i) => {
|
|
i.closeButton = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
function fitCanvasImg(dim) {
|
|
const img = canvasImgs.filter((img) => img.isSelected)[0];
|
|
img?.fitImage([pa.width, pa.height], dim, 1 / pa.scaleFactor);
|
|
if (!img || img.markDelete) info2.showWarning("No Image Selected!");
|
|
}
|
|
|
|
function fillCanvasImg() {
|
|
const img = canvasImgs.filter((img) => img.isSelected)[0];
|
|
img?.fillImage([pa.width, pa.height], 1 / pa.scaleFactor);
|
|
if (!img || img.markDelete) info2.showWarning("No Image Selected!");
|
|
}
|
|
|
|
function selectCanvasImg() {
|
|
if (bc.some((button) => button.isMouseIn?.())) return;
|
|
if (selectedImg && selectedImg.isMouseInResizeArea()) return; //prevent cancel resizing
|
|
let found = false; // Flag to stop after selecting the topmost image
|
|
for (let i = canvasImgs.length - 1; i >= 0; i--) {
|
|
let img = canvasImgs[i];
|
|
if (!found && img.isMouseIn(20)) {
|
|
img.isSelected = true;
|
|
found = true; // Stop selecting after the first match (topmost image)
|
|
} else {
|
|
img.isSelected = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function pickColor(e, caller) {
|
|
//if (bCloseCanvas?.isVisible) return; // prevent change color in canvas mode
|
|
|
|
function updateColor(trackedColor, apply = true) {
|
|
bFill.tagColor = trackedColor;
|
|
pa.brushColor = trackedColor;
|
|
cp.selectedColor = trackedColor;
|
|
(cp.isGhost ? bColor2 : bColor).color = trackedColor;
|
|
canvasImgs.forEach((item) => {
|
|
if (item.isSelected && item.isTextObject) {
|
|
if (trackedColor === "rgba(255, 255, 255, 0.0)") {
|
|
item.textColor = "black";
|
|
} else {
|
|
item.textColor = trackedColor;
|
|
}
|
|
}
|
|
});
|
|
// if (apply) {
|
|
// } // ToDO
|
|
}
|
|
if (caller === "click" || caller === "drag") {
|
|
let trackedColor = trackMouseColor(e, innerCanvas);
|
|
cp.allowPickVis = true;
|
|
|
|
if ((caller === "click" && isHoldingShift) || (caller === "drag" && cp.isVisible && !isHoldingShift)) {
|
|
//const applyColor = caller === "click" ? true : false;
|
|
updateColor(trackedColor);
|
|
} else {
|
|
cp.allowPickVis = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function resizeCanvas(dmR, dmS, ratiosArray, sizesArray, pa, info) {
|
|
if (dmR.selectedItemIndex === -1) dmR.selectedItemIndex = 0;
|
|
if (dmS.selectedItemIndex === -1) dmS.selectedItemIndex = 0;
|
|
const itemA = ratiosArray[dmR.selectedItemIndex][1];
|
|
const itemB = sizesArray[dmS.selectedItemIndex][1];
|
|
pa.setNewSize(itemA, itemB);
|
|
//info.show(`${itemA.width * itemB} x ${itemA.height * itemB}`);
|
|
info.text = `${itemA.width * itemB} x ${itemA.height * itemB}`;
|
|
// if (allow_debug) console.log(itemA, itemB);
|
|
}
|
|
|
|
function saveImgToDesk(delay) {
|
|
// override delay
|
|
const longestSide = Math.max(pa.width, pa.height);
|
|
// if(allow_debug){console.log('longestSide',longestSide);}
|
|
if (longestSide <= 512) {
|
|
delay = 500;
|
|
} else if (longestSide <= 1024) {
|
|
delay = 2000;
|
|
} else {
|
|
delay = 5000;
|
|
}
|
|
|
|
setTimeout(() => {
|
|
if (!pa.isPainting) pa.sendDrawingToAPI();
|
|
}, delay);
|
|
}
|
|
|
|
function createCanvasButtons(mode = "image") {
|
|
const bcw = 50;
|
|
const bch = 20;
|
|
const bcx = 512 / 2 - bcw / 2;
|
|
const bcy = 592 - 50;
|
|
const fY = -490;
|
|
lCanvasInfo = new SmartButton(512 / 2 - 40, bcy + 15, 80, bch, node, getActiveCtxText().text || "", {
|
|
textAlign: "center",
|
|
textColor: "black",
|
|
color: getActiveCtxText().color || "brown",
|
|
textYoffset: 2.2,
|
|
font: "13px Arial Bold",
|
|
allowVisualPress: true,
|
|
allowVisualHover: false,
|
|
resetColor: false,
|
|
});
|
|
lCanvasInfo.onClick = () => {
|
|
layerSwitch.callClick();
|
|
// pa.switchLayer()
|
|
//layerSwitch.onValueChange()
|
|
// lCanvasInfo.text = getActiveCtxText().text;
|
|
// lCanvasInfo.color = getActiveCtxText().color;
|
|
};
|
|
bc.push(lCanvasInfo);
|
|
|
|
bCloseCanvas = new SmartButton(bcx - bcw * 2, bcy, bcw, bch, node, "Exit", {
|
|
textXoffset: 0,
|
|
shape: Shapes.ROUND_L,
|
|
});
|
|
bc.push(bCloseCanvas);
|
|
bCloseCanvas.onClick = () => {
|
|
closeCanvas();
|
|
};
|
|
|
|
if (mode === "image") {
|
|
bMaskCanvas = new SmartButton(bcx - bcw * 1, bcy, bcw, bch, node, "Mask", {
|
|
textXoffset: 0,
|
|
shape: Shapes.SQUARE,
|
|
});
|
|
bMaskCanvas.onClick = () => {
|
|
let img = canvasImgs.find((img) => img.isSelected);
|
|
|
|
if (img.isMasked) {
|
|
info2.showWarning("Image Already Masked!", 140);
|
|
return;
|
|
}
|
|
// Get the value of a setting
|
|
const allow_masking = app.extensionManager.setting.get("iTools.Nodes.Mask Tool", false);
|
|
if (!allow_masking) {
|
|
info2.showWarning("Must Allow Masking in iTools Settings", 260);
|
|
return;
|
|
}
|
|
if (img && !img.markDelete) {
|
|
img.requestMaskedImage(loadedImageFile);
|
|
} else {
|
|
info2.showWarning("No Image Selected");
|
|
}
|
|
};
|
|
bc.push(bMaskCanvas);
|
|
} else if (mode === "text") {
|
|
bMaskCanvas = new SmartButton(bcx - bcw * 1, bcy, bcw, bch, node, "Font", {
|
|
textXoffset: 0,
|
|
shape: Shapes.SQUARE,
|
|
});
|
|
bMaskCanvas.onClick = () => {
|
|
const text = canvasImgs.find((item) => item.isSelected && item.isTextObject);
|
|
text.cycleFont();
|
|
info2.showWarning(text.fontName);
|
|
};
|
|
bc.push(bMaskCanvas);
|
|
}
|
|
|
|
bStampCanvas = new SmartButton(bcx, bcy, bcw, bch, node, "Stamp", {
|
|
textXoffset: 0,
|
|
shape: Shapes.SQUARE,
|
|
});
|
|
bStampCanvas.onClick = () => {
|
|
const ctx = pa.isPaintingBackground ? pa.backgroundCtx : pa.foregroundCtx;
|
|
let img = canvasImgs.find((img) => img.isSelected);
|
|
if (img && !img.markDelete) {
|
|
img.plotImageOnCanvas(ctx, pa.myX, pa.myY, dmS.selectedItemIndex);
|
|
} else {
|
|
info2.showWarning("No Image Selected!");
|
|
}
|
|
};
|
|
bc.push(bStampCanvas);
|
|
if (mode === "image") {
|
|
bFitCanvas = new SmartButton(bcx + bcw * 1, bcy, bcw, bch, node, "Fit", {
|
|
textXoffset: 0,
|
|
shape: Shapes.SQUARE,
|
|
});
|
|
bFitCanvas.onClick = () => {
|
|
fitCanvasImg("w");
|
|
};
|
|
bc.push(bFitCanvas);
|
|
} else if (mode === "text") {
|
|
bFitCanvas = new SmartButton(bcx + bcw * 1, bcy, bcw, bch, node, "Bold", {
|
|
textXoffset: 0,
|
|
shape: Shapes.SQUARE,
|
|
});
|
|
bFitCanvas.onClick = () => {
|
|
const text = canvasImgs.find((item) => item.isSelected && item.isTextObject);
|
|
text.cycleFontWeight();
|
|
//info2.showWarning(text.fontWeight)
|
|
};
|
|
bc.push(bFitCanvas);
|
|
}
|
|
if (mode === "image") {
|
|
bFillCanvas = new SmartButton(bcx + bcw * 2, bcy, bcw, bch, node, "Fill", {
|
|
textXoffset: 0,
|
|
shape: Shapes.ROUND_R,
|
|
});
|
|
bFillCanvas.onClick = () => {
|
|
fillCanvasImg();
|
|
};
|
|
bc.push(bFillCanvas);
|
|
} else if (mode === "text") {
|
|
bFillCanvas = new SmartButton(bcx + bcw * 2, bcy, bcw, bch, node, "Italic", {
|
|
textXoffset: 0,
|
|
shape: Shapes.ROUND_R,
|
|
});
|
|
bFillCanvas.onClick = () => {
|
|
const text = canvasImgs.find((item) => item.isSelected && item.isTextObject);
|
|
text.toggleItalic();
|
|
//info2.showWarning(text.isItalic)
|
|
};
|
|
bc.push(bFillCanvas);
|
|
}
|
|
bc.forEach((b) => (b.isVisible = false));
|
|
}
|
|
|
|
let c = 20; // change brush size with wheel BUGGED
|
|
function changeBrushSizeWithKey(e) {
|
|
if (!e) return;
|
|
function clamp(value, min, max) {
|
|
return Math.min(Math.max(value, min), max);
|
|
}
|
|
|
|
if (e.deltaY < 0) {
|
|
c += 2.5;
|
|
c = clamp(c, 1, 100);
|
|
brushSlider.callMove(c);
|
|
brushSlider.onValueChange(c);
|
|
} else {
|
|
c -= 2.5;
|
|
c = clamp(c, 1, 100);
|
|
brushSlider.callMove(c);
|
|
brushSlider.onValueChange(c);
|
|
}
|
|
}
|
|
|
|
function updateDMindexes() {
|
|
// Update dmR and dmS values after init
|
|
const w = pa.width;
|
|
const h = pa.height;
|
|
|
|
const longSide = Math.max(w, h);
|
|
let scale = 1;
|
|
let scaleIndex = 0;
|
|
|
|
if (longSide <= 512) {
|
|
scale = 1;
|
|
scaleIndex = 0;
|
|
} else if (longSide <= 1024) {
|
|
scale = 2;
|
|
scaleIndex = 1;
|
|
} else if (longSide <= 2048) {
|
|
scale = 4;
|
|
scaleIndex = 2;
|
|
}
|
|
|
|
dmS.selectedItemIndex = scaleIndex;
|
|
dmR.selectedItemIndex = getIndexByDimensions(w / scale, h / scale);
|
|
}
|
|
|
|
// COMMON ACTIONS
|
|
pa.onReInit = () => {
|
|
updateDMindexes();
|
|
};
|
|
|
|
pa.onPress = () => {
|
|
if (!isHoldingSpace) pa.blockPainting = false;
|
|
|
|
// if (allow_debug) {
|
|
// console.log("isHoldingShift", isHoldingShift);
|
|
// }
|
|
|
|
// Block painting if holding shift
|
|
if (isHoldingShift) {
|
|
pa.blockPainting = true;
|
|
}
|
|
|
|
// Block painting when drop menus open
|
|
if (dmR?.isMouseInMenu() || dmR?.isMouseIn() || dmS?.isMouseInMenu() || dmS?.isMouseIn()) {
|
|
pa.blockPainting = true;
|
|
}
|
|
|
|
// Block painting on canvas buttons
|
|
bc.forEach((b) => {
|
|
if (b.isMouseIn?.()) {
|
|
pa.blockPainting = true;
|
|
}
|
|
});
|
|
|
|
// EITHER -- Block painting on images when mouse in
|
|
// canvasImgs.forEach((img) => {
|
|
// if (img.isMouseIn(10) || img.isResizing || img.isRotating) {
|
|
// pa.blockPainting = true;
|
|
// }
|
|
// });
|
|
|
|
// OR -- Block painting on images permanently
|
|
if (canvasImgs.length > 0) {
|
|
pa.blockPainting = true;
|
|
selectCanvasImg();
|
|
}
|
|
};
|
|
|
|
pa.onUpdate = () => {};
|
|
|
|
// COMMON NODE EVENTS
|
|
node.clone = () => {
|
|
info2.showWarning("Cloning is disabled for this node", 220);
|
|
//console.warn("Cloning is disabled for this node.");
|
|
return null;
|
|
};
|
|
|
|
innerCanvas.addEventListener("mousedown", (e) => {
|
|
pickColor(e, "click");
|
|
selectedImg = canvasImgs.find((img) => img.isSelected);
|
|
if (selectedImg) {
|
|
selectedImg.isUnderCover = false;
|
|
}
|
|
});
|
|
|
|
innerCanvas.addEventListener("mouseup", () => {
|
|
saveImgToDesk(200);
|
|
});
|
|
|
|
innerCanvas.addEventListener("keydown", (e) => {
|
|
if (e.code === "Space") {
|
|
isHoldingSpace = true;
|
|
pa.blockPainting = true;
|
|
}
|
|
});
|
|
|
|
innerCanvas.addEventListener("mousemove", (e) => {
|
|
pickColor(e, "drag");
|
|
|
|
if (canvasImgs.length > 0 && bc.some((b) => b.isMouseIn())) {
|
|
if (selectedImg) {
|
|
selectedImg.isUnderCover = true;
|
|
}
|
|
}
|
|
|
|
toggleImagesCloseButton();
|
|
});
|
|
|
|
innerCanvas.addEventListener("mouseenter", () => {
|
|
mouseInNode = true;
|
|
});
|
|
|
|
innerCanvas.addEventListener("mouseleave", () => {
|
|
mouseInNode = false;
|
|
saveImgToDesk(500);
|
|
});
|
|
|
|
node.onIdle = () => {
|
|
if (allow_debug) console.log("idle");
|
|
};
|
|
|
|
// COMMON CLICKS EVENTS
|
|
const onKeyDown = (event) => {
|
|
if (!mouseInNode) return;
|
|
if (event.key === "Alt") {
|
|
if (!isHoldingAlt) info.show("Alt", 40);
|
|
isHoldingAlt = true;
|
|
event.preventDefault();
|
|
|
|
canvasImgs.forEach((img) => {
|
|
if (img.isSelected) {
|
|
const ctx = pa.isPaintingBackground ? pa.backgroundCtx : pa.foregroundCtx;
|
|
img.plotImageOnCanvas(ctx, pa.myX, pa.myY, dmS.selectedItemIndex);
|
|
}
|
|
});
|
|
|
|
if (cp.isVisible) {
|
|
cp.x === 0 ? (cp.x = 512 - cp.width) : (cp.x = 0);
|
|
}
|
|
domCtx.requestRedraw();
|
|
}
|
|
|
|
if (event.key === "Shift" && !isHoldingShift) {
|
|
if (!isHoldingShift) info.show("Shift", 40);
|
|
isHoldingShift = true;
|
|
domCtx.requestRedraw();
|
|
}
|
|
};
|
|
window.addEventListener("keydown", onKeyDown);
|
|
|
|
innerCanvas.addEventListener("dblclick", () => {
|
|
canvasImgs.forEach((item) => {
|
|
item.rotationAngle = 0;
|
|
item.width = item.originalWidth;
|
|
item.height = item.originalHeight;
|
|
});
|
|
domCtx.requestRedraw();
|
|
});
|
|
|
|
const onKeyUp = () => {
|
|
info.done = true;
|
|
isHoldingShift = false;
|
|
isHoldingAlt = false;
|
|
isHoldingSpace = false;
|
|
domCtx.requestRedraw();
|
|
// if (allow_debug) {
|
|
// console.log("canvasImgs.length", canvasImgs.length);
|
|
// }
|
|
|
|
// if (allow_debug) {
|
|
// console.log("keyUp");
|
|
// }
|
|
|
|
// if (event.key === "Shift") {
|
|
// isHoldingShift = false;
|
|
// }
|
|
};
|
|
window.addEventListener("keyup", onKeyUp);
|
|
|
|
const onWheel = (e) => {
|
|
changeBrushSizeWithKey(e);
|
|
domCtx.requestRedraw();
|
|
e.preventDefault();
|
|
};
|
|
innerCanvas.addEventListener("wheel", onWheel, { passive: false });
|
|
|
|
globalThis.oncopy = (...args) => {};
|
|
|
|
// Wire the Smart* widget manager onto the inner DOM canvas and start the
|
|
// draw loop. drawAll iterates node._smartWidgets (populated by our
|
|
// addCustomWidget shim) and calls widget.draw(innerCtx).
|
|
const m = new BaseSmartWidgetManager(node, "iToolsPaintNode");
|
|
m.attachDomCanvas(innerCanvas, NODE_W, NODE_H);
|
|
|
|
// Keep the backing bitmap matched to the real on-screen pixel footprint:
|
|
// cssSize * devicePixelRatio * graphZoom. ResizeObserver catches layout
|
|
// changes, but litegraph applies graph zoom via CSS transform on the
|
|
// widget's parent — transforms do NOT fire ResizeObserver, so we also
|
|
// re-check the size every frame (cheap: no-op when unchanged).
|
|
let _scaleX = 1;
|
|
let _scaleY = 1;
|
|
const resizeBackingCanvas = () => {
|
|
const cssW = innerCanvas.offsetWidth;
|
|
const cssH = innerCanvas.offsetHeight;
|
|
if (cssW === 0 || cssH === 0) return;
|
|
const dpr = window.devicePixelRatio || 1;
|
|
const graphScale = app?.canvas?.ds?.scale || 1;
|
|
const targetW = Math.max(1, Math.round(cssW * dpr * graphScale));
|
|
const targetH = Math.max(1, Math.round(cssH * dpr * graphScale));
|
|
if (innerCanvas.width !== targetW || innerCanvas.height !== targetH) {
|
|
innerCanvas.width = targetW;
|
|
innerCanvas.height = targetH;
|
|
_scaleX = innerCanvas.width / NODE_W;
|
|
_scaleY = innerCanvas.height / NODE_H;
|
|
domCtx.requestRedraw();
|
|
}
|
|
};
|
|
|
|
const ro = new ResizeObserver(resizeBackingCanvas);
|
|
ro.observe(innerCanvas);
|
|
resizeBackingCanvas();
|
|
|
|
const renderFrame = () => {
|
|
resizeBackingCanvas();
|
|
innerCtx.save();
|
|
innerCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
innerCtx.fillStyle = LiteGraph.WIDGET_BGCOLOR || "#333";
|
|
innerCtx.fillRect(0, 0, innerCanvas.width, innerCanvas.height);
|
|
innerCtx.setTransform(_scaleX, 0, 0, _scaleY, 0, 0);
|
|
innerCtx.imageSmoothingEnabled = true;
|
|
innerCtx.imageSmoothingQuality = "low";
|
|
m.drawAll(innerCtx);
|
|
innerCtx.restore();
|
|
};
|
|
|
|
domCtx.attach(innerCanvas, innerCtx, node, renderFrame);
|
|
|
|
// Clean up on node removal
|
|
const origOnRemoved = node.onRemoved;
|
|
node.onRemoved = function () {
|
|
origOnRemoved?.apply(this, arguments);
|
|
if (allow_debug) console.log("Cleaning up paint node widget");
|
|
ro.disconnect();
|
|
m.destroy();
|
|
domCtx.detach();
|
|
innerCanvas.removeEventListener("wheel", onWheel);
|
|
window.removeEventListener("keydown", onKeyDown);
|
|
window.removeEventListener("keyup", onKeyUp);
|
|
};
|
|
},
|
|
});
|