parlorsky's picture
Upload folder using huggingface_hub
3a4bb96 verified
Raw
History Blame Contribute Delete
27.6 kB
import { app } from "../../scripts/app.js";
// 基础虚拟节点类
class BaseVirtualNode extends LGraphNode {
constructor(title = "BaseVirtualNode") {
super(title);
this.comfyClass = "BaseVirtualNode";
this.isVirtualNode = true;
this.properties = this.properties || {};
this.widgets = this.widgets || [];
}
static setUp() {
if (!this.type) {
throw new Error(`Missing type for BaseVirtualNode: ${this.title}`);
}
LiteGraph.registerNodeType(this.type, this);
if (this._category) {
this.category = this._category;
}
}
}
// LG节点类
export class lg_note extends BaseVirtualNode {
constructor(title = lg_note.title) {
super(title);
this.comfyClass = "LG_Note";
this.resizable = true; // 启用可调整大小
// 默认属性
this.properties["fontSize"] = 12;
this.properties["fontFamily"] = "Arial";
this.properties["fontColor"] = "#ffffff";
this.properties["textAlign"] = "center";
this.properties["backgroundColor"] = "transparent";
this.properties["padding"] = 0;
this.properties["borderRadius"] = 0;
this.properties["autoScale"] = true; // 是否启用自动缩放
this.properties["baseFontSize"] = 36; // 基础字体大小,用于缩放计算
// 初始大小
this.size = [160, 40];
this.baseSize = [160, 40]; // 基础大小,用于缩放计算
this.color = "#fff0";
this.bgcolor = "#fff0";
}
// 计算缩放后的字体大小
calculateScaledFontSize() {
if (!this.properties["autoScale"]) {
return this.properties["fontSize"];
}
// 计算缩放比例(基于宽度和高度的平均值)
const widthScale = this.size[0] / this.baseSize[0];
const heightScale = this.size[1] / this.baseSize[1];
const scale = (widthScale + heightScale) / 2;
// 计算缩放后的字体大小,不限制最大最小值
const scaledFontSize = this.properties["baseFontSize"] * scale;
// 确保字体大小至少为1像素,避免无效值
return Math.max(1, scaledFontSize);
}
draw(ctx) {
this.flags = this.flags || {};
this.flags.allow_interaction = !this.flags.pinned;
ctx.save();
this.color = "#fff0";
this.bgcolor = "#fff0";
const fontColor = this.properties["fontColor"] || "#ffffff";
const backgroundColor = this.properties["backgroundColor"] || "";
// 使用缩放后的字体大小
const currentFontSize = this.calculateScaledFontSize();
this.properties["fontSize"] = currentFontSize; // 更新当前字体大小属性
ctx.font = `${Math.max(currentFontSize || 0, 1)}px ${this.properties["fontFamily"] ?? "Arial"}`;
const padding = Number(this.properties["padding"]) ?? 0;
const lines = this.title.replace(/\n*$/, "").split("\n");
// 如果不是自动缩放模式,则根据文本内容调整节点大小
if (!this.properties["autoScale"]) {
const maxWidth = Math.max(...lines.map((s) => ctx.measureText(s).width));
this.size[0] = maxWidth + padding * 2;
this.size[1] = currentFontSize * lines.length + padding * 2;
}
// 绘制背景
if (backgroundColor) {
ctx.beginPath();
const borderRadius = Number(this.properties["borderRadius"]) || 0;
ctx.roundRect(0, 0, this.size[0], this.size[1], [borderRadius]);
ctx.fillStyle = backgroundColor;
ctx.fill();
}
// 设置文本对齐
ctx.textAlign = "left";
let textX = padding;
if (this.properties["textAlign"] === "center") {
ctx.textAlign = "center";
textX = this.size[0] / 2;
} else if (this.properties["textAlign"] === "right") {
ctx.textAlign = "right";
textX = this.size[0] - padding;
}
// 绘制文本
ctx.textBaseline = "middle"; // 使用middle基线
ctx.fillStyle = fontColor;
const lineHeight = currentFontSize * 1.2; // 行高稍微大于字体大小
const totalTextHeight = lines.length * lineHeight;
// 计算垂直居中的起始位置
let startY = (this.size[1] - totalTextHeight) / 2 + lineHeight / 2;
for (let i = 0; i < lines.length; i++) {
const currentY = startY + i * lineHeight;
ctx.fillText(lines[i] || " ", textX, currentY);
}
ctx.restore();
}
onDblClick(event, pos, canvas) {
LGraphCanvas.active_canvas.showShowNodePanel(this);
}
onShowCustomPanelInfo(panel) {
// 移除不需要的属性面板项
panel.querySelector('div.property[data-property="Mode"]')?.remove();
panel.querySelector('div.property[data-property="Color"]')?.remove();
panel.querySelector('div.property[data-property="baseFontSize"]')?.remove(); // 隐藏baseFontSize属性
// 为所有属性添加实时回调
setTimeout(() => {
this.convertTitleToTextarea(panel);
this.addRealTimeCallbacks(panel);
this.createColorPickerRow(panel);
this.moveSliderPropertiesToEnd(panel);
this.convertToSlider(panel, "fontSize", 1, 500, 1);
this.convertToSlider(panel, "padding", 0, 50, 1);
this.convertToSlider(panel, "borderRadius", 0, 50, 1);
}, 10);
}
// 将 Title 转换为多行文本框
convertTitleToTextarea(panel) {
const titleElement = panel.querySelector('div.property[data-property="Title"]');
if (!titleElement) return;
const valueElement = titleElement.querySelector('.property_value');
if (!valueElement) return;
// 移除原有的contenteditable属性
valueElement.removeAttribute('contenteditable');
// 创建多行文本框
const textarea = document.createElement('textarea');
textarea.value = this.title || '';
textarea.style.cssText = `
width: 100%;
min-height: 60px;
max-height: 150px;
background: rgba(255,255,255,0.1);
border: 1px solid #555;
border-radius: 4px;
padding: 8px;
color: #fff;
font-size: 13px;
font-family: inherit;
resize: vertical;
outline: none;
box-sizing: border-box;
`;
// 实时更新事件
textarea.addEventListener('input', (e) => {
this.title = e.target.value;
this.setDirtyCanvas(true, true);
});
// 回车键不关闭面板,允许多行输入
textarea.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
// 允许回车换行,不阻止默认行为
e.stopPropagation(); // 阻止事件冒泡,防止关闭面板
}
});
// 失去焦点时确保更新
textarea.addEventListener('blur', (e) => {
this.title = e.target.value;
this.setDirtyCanvas(true, true);
});
// 替换原有的值显示元素
valueElement.style.display = 'none';
titleElement.appendChild(textarea);
// 调整标题元素的布局
titleElement.style.cssText = `
display: block;
margin: 8px 0;
padding: 8px 0;
`;
// 调整标签样式
const nameElement = titleElement.querySelector('.property_name');
if (nameElement) {
nameElement.style.cssText = `
display: block;
margin-bottom: 6px;
font-weight: bold;
color: #ccc;
`;
}
}
// 为所有属性添加实时回调
addRealTimeCallbacks(panel) {
// 为文本输入框添加实时回调
const textProperties = ["fontFamily"];
textProperties.forEach(propName => {
const element = panel.querySelector(`div.property[data-property="${propName}"] .property_value`);
if (element) {
element.addEventListener('input', () => {
this.properties[propName] = element.textContent;
this.setDirtyCanvas(true, true);
});
element.addEventListener('blur', () => {
this.properties[propName] = element.textContent;
this.setDirtyCanvas(true, true);
});
}
});
// 为下拉选择框添加实时回调
const comboProperties = ["textAlign"];
comboProperties.forEach(propName => {
const element = panel.querySelector(`div.property[data-property="${propName}"] .property_value`);
if (element) {
// 下拉选择的回调已经在原有代码中处理
}
});
// 为布尔值属性添加实时回调
const booleanProperties = ["autoScale"];
booleanProperties.forEach(propName => {
const element = panel.querySelector(`div.property[data-property="${propName}"]`);
if (element) {
// 布尔值的点击事件已经在原有代码中处理,但我们确保它有实时更新
element.addEventListener('click', () => {
// 延迟一点确保值已经更新
setTimeout(() => {
this.setDirtyCanvas(true, true);
// 当 autoScale 状态改变时,更新 fontSize 滑条显示
this.updateFontSizeSlider(panel);
}, 10);
});
}
});
}
// 更新 fontSize 滑条显示
updateFontSizeSlider(panel) {
const fontSizeSlider = panel.querySelector('div.property[data-property="fontSize"] input[type="range"]');
const fontSizeValue = panel.querySelector('div.property[data-property="fontSize"] .property_value');
if (fontSizeSlider && fontSizeValue) {
let displayValue;
if (this.properties["autoScale"]) {
// auto 模式下显示 baseFontSize
displayValue = this.properties["baseFontSize"];
} else {
// 非 auto 模式下显示实际 fontSize
displayValue = this.properties["fontSize"];
}
fontSizeSlider.value = displayValue.toString();
fontSizeValue.textContent = displayValue.toString();
}
}
// 创建颜色选择器行
createColorPickerRow(panel) {
// 找到颜色相关的属性元素
const fontColorElement = panel.querySelector('div.property[data-property="fontColor"]');
const backgroundColorElement = panel.querySelector('div.property[data-property="backgroundColor"]');
if (!fontColorElement || !backgroundColorElement) return;
// 创建颜色选择器容器
const colorRow = document.createElement('div');
colorRow.style.cssText = `
display: flex;
gap: 10px;
margin: 10px 0;
padding: 8px;
background: rgba(255,255,255,0.05);
border-radius: 6px;
`;
// 创建字体颜色选择器
const fontColorPicker = this.createColorPicker("Font Color", this.properties.fontColor, (color) => {
this.properties.fontColor = color;
this.setDirtyCanvas(true, true);
});
// 创建背景颜色选择器
const backgroundColorPicker = this.createColorPicker("Background", this.properties.backgroundColor, (color) => {
this.properties.backgroundColor = color;
this.setDirtyCanvas(true, true);
});
colorRow.appendChild(fontColorPicker);
colorRow.appendChild(backgroundColorPicker);
// 移除原有的颜色属性元素
fontColorElement.remove();
backgroundColorElement.remove();
// 在合适的位置插入颜色选择器行
const titleElement = panel.querySelector('div.property[data-property="Title"]');
if (titleElement && titleElement.nextSibling) {
panel.content.insertBefore(colorRow, titleElement.nextSibling);
} else {
panel.content.appendChild(colorRow);
}
}
// 创建颜色选择器
createColorPicker(label, currentColor, callback) {
const container = document.createElement('div');
container.style.cssText = `
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
`;
// 标签
const labelElement = document.createElement('span');
labelElement.textContent = label;
labelElement.style.cssText = `
font-size: 12px;
color: #ccc;
font-weight: bold;
`;
// 颜色选择器容器
const pickerContainer = document.createElement('div');
pickerContainer.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
width: 100%;
`;
// 处理初始颜色值
const initialColor = currentColor || '#ffffff';
const isHexColor = /^#[0-9A-Fa-f]{6}$/i.test(initialColor);
// 颜色输入框
const colorInput = document.createElement('input');
colorInput.type = 'color';
// 只有十六进制颜色才设置到颜色选择器
colorInput.value = isHexColor ? initialColor : '#ffffff';
colorInput.style.cssText = `
width: 40px;
height: 30px;
border: none;
border-radius: 6px;
cursor: pointer;
background: none;
padding: 0;
`;
// 颜色值显示
const colorValue = document.createElement('input');
colorValue.type = 'text';
colorValue.value = initialColor;
colorValue.style.cssText = `
flex: 1;
background: rgba(255,255,255,0.1);
border: 1px solid #555;
border-radius: 4px;
padding: 4px 8px;
color: #fff;
font-size: 11px;
font-family: monospace;
`;
// 添加占位符提示
colorValue.placeholder = 'e.g. #ff0000, red, transparent';
// 颜色选择器事件
colorInput.addEventListener('input', (e) => {
const color = e.target.value;
colorValue.value = color;
callback(color);
});
// 文本输入事件 - 支持实时验证
colorValue.addEventListener('input', (e) => {
const formattedColor = formatColor(e.target.value);
if (isValidColor(formattedColor)) {
// 如果是十六进制颜色,同步到颜色选择器
if (/^#[0-9A-Fa-f]{6}$/i.test(formattedColor)) {
colorInput.value = formattedColor;
}
callback(formattedColor);
}
});
// 颜色格式化函数 - 简化版本
const formatColor = (input) => {
let color = input.trim();
// 如果没有 # 且是6位十六进制,自动添加 #
if (!color.startsWith('#') && /^[0-9A-Fa-f]{6}$/i.test(color)) {
color = '#' + color;
}
return color;
};
// 颜色验证函数 - 使用浏览器原生验证
const isValidColor = (color) => {
// 空值不是有效颜色
if (!color || color.trim() === '') return false;
// 创建一个临时元素来测试颜色是否有效
const tempElement = document.createElement('div');
const originalColor = tempElement.style.color;
try {
tempElement.style.color = color;
// 如果浏览器接受这个颜色值,style.color 会被设置且不等于原始值
const isValid = tempElement.style.color !== originalColor || tempElement.style.color !== '';
console.log(`Color validation: "${color}" -> ${isValid}`); // 调试信息
return isValid;
} catch (e) {
console.log(`Color validation error: "${color}" -> false`); // 调试信息
return false;
}
};
// 回车键事件
colorValue.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const formattedColor = formatColor(e.target.value);
if (isValidColor(formattedColor)) {
colorValue.value = formattedColor;
// 如果是十六进制颜色,同步到颜色选择器
if (/^#[0-9A-Fa-f]{6}$/i.test(formattedColor)) {
colorInput.value = formattedColor;
}
callback(formattedColor);
e.target.blur(); // 失去焦点
} else {
// 如果格式不正确,恢复到之前的值
colorValue.value = colorInput.value;
}
}
});
colorValue.addEventListener('blur', (e) => {
const formattedColor = formatColor(e.target.value);
if (isValidColor(formattedColor)) {
// 失去焦点时如果格式正确,应用颜色
colorValue.value = formattedColor;
// 如果是十六进制颜色,同步到颜色选择器
if (/^#[0-9A-Fa-f]{6}$/i.test(formattedColor)) {
colorInput.value = formattedColor;
}
callback(formattedColor);
} else {
// 如果格式不正确,恢复到之前的值
colorValue.value = colorInput.value;
}
});
pickerContainer.appendChild(colorInput);
pickerContainer.appendChild(colorValue);
container.appendChild(labelElement);
container.appendChild(pickerContainer);
return container;
}
// 将滑条属性移动到最后
moveSliderPropertiesToEnd(panel) {
const sliderProperties = ["fontSize", "padding", "borderRadius"];
const content = panel.content;
sliderProperties.forEach(propName => {
const element = panel.querySelector(`div.property[data-property="${propName}"]`);
if (element) {
content.appendChild(element);
}
});
}
// 将数字输入框转换为滑条样式
convertToSlider(panel, propertyName, min, max, step) {
const propertyElement = panel.querySelector(`div.property[data-property="${propertyName}"]`);
if (!propertyElement) return;
const valueElement = propertyElement.querySelector('.property_value');
if (!valueElement) return;
// 移除原有的contenteditable属性
valueElement.removeAttribute('contenteditable');
// 重新设置属性元素的布局
propertyElement.style.cssText = `
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
margin: 4px 0;
`;
// 创建滑条容器
const sliderContainer = document.createElement('div');
sliderContainer.style.cssText = `
display: flex;
align-items: center;
flex: 1;
margin-left: 10px;
gap: 10px;
`;
// 创建滑条输入
const slider = document.createElement('input');
slider.type = 'range';
slider.min = min.toString();
slider.max = max.toString();
slider.step = step.toString();
// 特殊处理 fontSize:在 auto 模式下使用 baseFontSize
let initialValue;
if (propertyName === 'fontSize' && this.properties["autoScale"]) {
initialValue = this.properties["baseFontSize"];
} else {
initialValue = this.properties[propertyName];
}
slider.value = initialValue.toString();
// 设置滑条样式 - 更显眼的设计
slider.style.cssText = `
flex: 1;
height: 6px;
background: linear-gradient(to right, #555, #777);
outline: none;
border-radius: 3px;
appearance: none;
-webkit-appearance: none;
cursor: pointer;
`;
// 添加更显眼的滑条样式
if (!document.getElementById('slider-styles')) {
const style = document.createElement('style');
style.id = 'slider-styles';
style.textContent = `
input[type="range"]::-webkit-slider-thumb {
appearance: none;
-webkit-appearance: none;
height: 20px;
width: 20px;
border-radius: 50%;
background: linear-gradient(45deg, #4CAF50, #45a049);
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
border: 2px solid #fff;
}
input[type="range"]::-webkit-slider-thumb:hover {
background: linear-gradient(45deg, #5CBF60, #55b059);
transform: scale(1.1);
}
input[type="range"]::-moz-range-thumb {
height: 20px;
width: 20px;
border-radius: 50%;
background: linear-gradient(45deg, #4CAF50, #45a049);
cursor: pointer;
border: 2px solid #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
input[type="range"]::-moz-range-track {
background: linear-gradient(to right, #555, #777);
height: 6px;
border-radius: 3px;
}
`;
document.head.appendChild(style);
}
// 修改数值显示样式 - 简洁的数字显示
valueElement.style.cssText = `
min-width: 35px;
text-align: center;
color: #fff;
font-size: 12px;
font-weight: bold;
background: rgba(255,255,255,0.1);
border-radius: 4px;
padding: 2px 6px;
`;
// 设置初始显示值
valueElement.textContent = initialValue.toString();
// 滑条事件处理 - 实时更新
slider.addEventListener('input', (e) => {
const newValue = parseFloat(e.target.value);
// 特殊处理 fontSize:在 auto 模式下调整 baseFontSize
if (propertyName === 'fontSize' && this.properties["autoScale"]) {
this.properties["baseFontSize"] = newValue;
// 显示的值仍然是 baseFontSize
valueElement.textContent = newValue.toString();
} else {
this.properties[propertyName] = newValue;
valueElement.textContent = newValue.toString();
}
// 立即更新节点显示
this.setDirtyCanvas(true, true);
});
// 滑条拖拽结束事件
slider.addEventListener('change', (e) => {
const newValue = parseFloat(e.target.value);
// 特殊处理 fontSize:在 auto 模式下调整 baseFontSize
if (propertyName === 'fontSize' && this.properties["autoScale"]) {
this.properties["baseFontSize"] = newValue;
valueElement.textContent = newValue.toString();
} else {
this.properties[propertyName] = newValue;
valueElement.textContent = newValue.toString();
}
this.setDirtyCanvas(true, true);
});
// 清空原有内容并重新组织布局
const nameElement = propertyElement.querySelector('.property_name');
propertyElement.innerHTML = '';
// 重新添加元素:名称在左边,滑条和数值在右边
propertyElement.appendChild(nameElement);
sliderContainer.appendChild(slider);
sliderContainer.appendChild(valueElement);
propertyElement.appendChild(sliderContainer);
}
// 处理节点大小改变
onResize(size) {
if (this.properties["autoScale"]) {
// 当节点大小改变时,重新计算字体大小
this.setDirtyCanvas(true, true);
}
}
}
// 节点类型配置
lg_note.type = "LG_Note";
lg_note.title = "🎈LG_Note";
lg_note.title_mode = LiteGraph.NO_TITLE;
lg_note.collapsable = false;
lg_note._category = "🎈LAOGOU/Utils";
// 属性定义
lg_note["@fontSize"] = { type: "number" };
lg_note["@baseFontSize"] = { type: "number" }; // 基础字体大小,用于序列化
lg_note["@fontFamily"] = { type: "string" };
lg_note["@fontColor"] = { type: "string" };
lg_note["@textAlign"] = { type: "combo", values: ["left", "center", "right"] };
lg_note["@backgroundColor"] = { type: "string" };
lg_note["@padding"] = { type: "number" };
lg_note["@borderRadius"] = { type: "number" };
lg_note["@autoScale"] = { type: "boolean" };
// 全局状态管理
const labelNodeState = {
processingMouseDown: false,
lastAdjustedMouseEvent: null
};
// 重写绘制节点方法
const oldDrawNode = LGraphCanvas.prototype.drawNode;
LGraphCanvas.prototype.drawNode = function (node, ctx) {
if (node.constructor === lg_note) {
node.bgcolor = "transparent";
node.color = "transparent";
const v = oldDrawNode.apply(this, arguments);
node.draw(ctx);
return v;
}
const v = oldDrawNode.apply(this, arguments);
return v;
};
// 重写鼠标事件处理
const oldGetNodeOnPos = LGraph.prototype.getNodeOnPos;
LGraph.prototype.getNodeOnPos = function (x, y, nodes_list, margin) {
if (nodes_list &&
labelNodeState.processingMouseDown &&
labelNodeState.lastAdjustedMouseEvent?.type.includes("down") &&
labelNodeState.lastAdjustedMouseEvent?.which === 1) {
let isDoubleClick = LiteGraph.getTime() - LGraphCanvas.active_canvas.last_mouseclick < 300;
if (!isDoubleClick) {
nodes_list = [...nodes_list].filter((n) => !(n instanceof lg_note) || !n.flags?.pinned);
}
}
return oldGetNodeOnPos.apply(this, [x, y, nodes_list, margin]);
};
// 鼠标事件监听
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
LGraphCanvas.prototype.processMouseDown = function (e) {
labelNodeState.processingMouseDown = true;
const returnVal = processMouseDown.apply(this, [...arguments]);
labelNodeState.processingMouseDown = false;
return returnVal;
};
const adjustMouseEvent = LGraphCanvas.prototype.adjustMouseEvent;
LGraphCanvas.prototype.adjustMouseEvent = function (e) {
adjustMouseEvent.apply(this, [...arguments]);
labelNodeState.lastAdjustedMouseEvent = e;
};
// 注册扩展
app.registerExtension({
name: "LG_Note",
registerCustomNodes() {
lg_note.setUp();
},
});