Files
Dontdrunk-ComfyUI-DD-Nodes/js/Intelligent Layout/LayoutCore.js
T

538 lines
16 KiB
JavaScript

// 智能布局核心模块
// 布局面板相关样式
const layoutPanelStyles = `
.layout-panel {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
/* 按钮彩虹动画 */
@keyframes rainbow {
0% { background-position: 0% 82% }
50% { background-position: 100% 19% }
100% { background-position: 0% 82% }
}
.layout-btn-rainbow:hover {
background-image: linear-gradient(124deg, #ff2400, #e81d1d, #e8b71d, #e3e81d, #1de840, #1ddde8, #2b1de8, #dd00f3, #dd00f3) !important;
background-size: 1800% 1800% !important;
animation: rainbow 6s ease infinite !important;
}
/* 按钮水波纹效果 */
.layout-btn::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 5px;
height: 5px;
background: rgba(255, 255, 255, .5);
opacity: 0;
border-radius: 100%;
transform: scale(1, 1) translate(-50%, -50%);
transform-origin: 50% 50%;
}
.layout-btn:active::after {
animation: ripple 0.6s ease-out;
}
@keyframes ripple {
0% {
transform: scale(0, 0) translate(-50%, -50%);
opacity: 0.5;
}
100% {
transform: scale(20, 20) translate(-50%, -50%);
opacity: 0;
}
}
`;
// 工具函数:向页面注入样式
function injectStyles(styleId, styleContent) {
// 如果已经存在相同ID的样式,则不重复注入
if (document.getElementById(styleId)) return;
const styleElement = document.createElement('style');
styleElement.id = styleId;
styleElement.textContent = styleContent;
document.head.appendChild(styleElement);
}
// 工具函数:移除已注入的样式
function removeStyles(styleId) {
const styleElement = document.getElementById(styleId);
if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement);
}
}
// 跟踪鼠标位置
let mousePosition = { x: 0, y: 0 };
document.addEventListener('mousemove', (e) => {
mousePosition = { x: e.clientX, y: e.clientY };
});
export class LayoutPanel {
constructor() {
this.enabled = true;
this.shortcut = 'alt+x';
this.position = { left: 32, top: 120 }; // 默认位置,将被鼠标位置覆盖
this.visible = false;
this.container = null;
this.buttonsContainer = null;
}
setEnabled(enabled) {
// 始终保持功能启用
this.enabled = true;
if (!this.container) {
this._createPanel();
}
}
setShortcut(shortcut) {
if (typeof shortcut === 'string' && shortcut.includes('+')) {
this.shortcut = shortcut;
}
}
// 只有通过toggle方法才能控制面板显示/隐藏,确保只能通过快捷键激活
toggle() {
if (this.visible) {
this.hide();
} else {
this.show();
}
}
show() {
if (!this.container) {
this._createPanel();
}
// 使用当前鼠标位置
this._showAtMousePosition();
this.container.style.display = 'flex';
this.container.style.pointerEvents = 'auto'; // 显示时允许事件
// 使用动画效果显示
requestAnimationFrame(() => {
this.container.style.opacity = '1';
this.container.style.transform = 'scale(1)';
});
this.visible = true;
}
// 在鼠标位置显示面板
_showAtMousePosition() {
if (!this.container) return;
// 调整为居中显示
this.container.style.left = `${mousePosition.x}px`;
this.container.style.top = `${mousePosition.y}px`;
this.container.style.transform = 'translate(-50%, -50%)';
}
hide() {
if (!this.container) return;
this.container.style.opacity = '0';
this.container.style.transform = 'translate(-50%, -50%) scale(0.95)';
this.container.style.pointerEvents = 'none'; // 隐藏时禁止事件
setTimeout(() => {
if (this.container) {
this.container.style.display = 'none';
}
}, 300);
this.visible = false;
}
_createPanel() {
if (this.container) return;
// 创建容器
this.container = document.createElement('div');
this.container.className = 'layout-panel';
this.container.style.position = 'fixed';
this.container.style.left = `${mousePosition.x}px`;
this.container.style.top = `${mousePosition.y}px`;
this.container.style.transform = 'translate(-50%, -50%)';
this.container.style.zIndex = 99999;
this.container.style.display = 'none';
this.container.style.pointerEvents = 'none'; // 默认禁用事件,确保只有在面板显示时才能交互
this.container.style.transition = 'opacity 0.3s, transform 0.3s';
this.container.style.opacity = '0';
// 更现代的面板样式
this.container.style.padding = '20px';
this.container.style.borderRadius = '16px';
this.container.style.backdropFilter = 'blur(12px)';
this.container.style.boxShadow = '0 8px 32px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1) inset';
this.container.style.backgroundColor = 'rgba(30, 30, 40, 0.85)';
this.container.style.border = '1px solid rgba(255, 255, 255, 0.1)';
this.container.style.display = 'flex';
this.container.style.flexDirection = 'column';
this.container.style.alignItems = 'center';
this.container.style.gap = '16px';
// 创建按钮容器 - 居中对齐
this.buttonsContainer = document.createElement('div');
this.buttonsContainer.className = 'layout-buttons-container';
this.buttonsContainer.style.display = 'flex';
this.buttonsContainer.style.justifyContent = 'center'; // 确保按钮居中
this.buttonsContainer.style.width = '100%';
this.buttonsContainer.style.gap = '12px';
this.buttonsContainer.style.marginTop = '4px';
// 创建随机颜色按钮
const randomColorBtn = this._createButton('随机颜色', 'rainbow');
randomColorBtn.addEventListener('click', (e) => {
e.stopPropagation();
this._setRandomNodeColor();
});
// 创建自定义颜色按钮
const customColorBtn = this._createButton('自定义颜色', 'pick');
customColorBtn.addEventListener('click', (e) => {
e.stopPropagation();
this._openCustomColorPicker();
});
// 添加按钮到容器
this.buttonsContainer.appendChild(randomColorBtn);
this.buttonsContainer.appendChild(customColorBtn);
// 添加按钮容器到主容器
this.container.appendChild(this.buttonsContainer);
// 使用外部样式模块注入样式
injectStyles('layout-panel-styles', layoutPanelStyles);
// 添加面板点击事件,阻止冒泡
this.container.addEventListener('click', (e) => {
e.stopPropagation();
});
// 添加到文档
document.body.appendChild(this.container);
}
// 创建按钮辅助方法 - 更美观的按钮
_createButton(text, type, bgColor) {
const button = document.createElement('button');
button.className = `layout-btn layout-btn-${type}`;
button.textContent = text;
button.title = text;
// 添加更现代的按钮样式
const isPrimary = type === 'pick';
const isRainbow = type === 'rainbow';
// 基础样式
Object.assign(button.style, {
backgroundColor: bgColor || (isRainbow ? 'rgba(138, 43, 226, 0.75)' : 'rgba(16, 130, 246, 0.75)'),
border: 'none',
borderRadius: '10px',
padding: '10px 16px',
color: '#ffffff',
fontSize: '14px',
fontWeight: '600',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
cursor: 'pointer',
transition: 'all 0.2s ease',
textShadow: '0 1px 2px rgba(0, 0, 0, 0.2)',
minWidth: '120px',
textAlign: 'center',
position: 'relative',
overflow: 'hidden',
margin: '2px'
});
// 添加悬停效果
button.addEventListener('mouseover', () => {
const baseColor = bgColor || (isRainbow ? 'rgba(138, 43, 226, 0.75)' : 'rgba(16, 130, 246, 0.75)');
// 提取RGB部分,增加不透明度
const colorPattern = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/;
const match = baseColor.match(colorPattern);
if (match) {
const r = match[1];
const g = match[2];
const b = match[3];
button.style.backgroundColor = `rgba(${r}, ${g}, ${b}, 0.9)`;
} else {
button.style.backgroundColor = baseColor;
}
button.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.25)';
button.style.transform = 'translateY(-2px)';
});
// 添加离开效果
button.addEventListener('mouseout', () => {
button.style.backgroundColor = bgColor || (isRainbow ? 'rgba(138, 43, 226, 0.75)' : 'rgba(16, 130, 246, 0.75)');
button.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
button.style.transform = 'translateY(0)';
});
// 添加点击效果
button.addEventListener('mousedown', () => {
button.style.transform = 'translateY(1px)';
button.style.boxShadow = '0 2px 6px rgba(0, 0, 0, 0.15)';
});
button.addEventListener('mouseup', () => {
button.style.transform = 'translateY(0)';
button.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
});
return button;
}
// 不再需要此方法,因为我们总是使用鼠标位置
_updatePosition() {
// 已弃用:彻底移除
}
// 设置随机节点颜色
_setRandomNodeColor() {
try {
// 获取ComfyUI应用实例
const app = this._getComfyUIApp();
if (!app) {
this._showNotification("无法获取ComfyUI应用实例");
return;
}
// 获取选中的节点和组
const selectedNodes = this._getSelectedNodes(app);
const selectedGroups = this._getSelectedGroups(app);
if (selectedNodes.length === 0 && selectedGroups.length === 0) {
this._showNotification("请先选择要应用颜色的节点或组");
return;
}
// 生成随机颜色函数
const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
// 为每个选中的节点或组应用不同的随机颜色
selectedNodes.forEach(node => {
const randomColor = getRandomColor();
node.color = randomColor;
});
selectedGroups.forEach(group => {
group.color = getRandomColor();
});
app.graph.setDirtyCanvas(true, true);
this._showNotification("已应用随机颜色", "info");
// 应用了颜色后隐藏面板
setTimeout(() => this.hide(), 300);
} catch (error) {
console.error("设置随机颜色失败:", error);
this._showNotification(`设置随机颜色失败: ${error.message}`);
}
}
// 打开自定义颜色选择器
_openCustomColorPicker() {
try {
// 获取ComfyUI应用实例
const app = this._getComfyUIApp();
if (!app) {
this._showNotification("无法获取ComfyUI应用实例");
return;
}
// 获取选中的节点和组
const selectedNodes = this._getSelectedNodes(app);
const selectedGroups = this._getSelectedGroups(app);
if (selectedNodes.length === 0 && selectedGroups.length === 0) {
this._showNotification("请先选择要应用颜色的节点或组");
return;
}
// 隐藏当前面板
this.hide();
// 创建颜色选择器
setTimeout(() => {
const defaultColor = selectedNodes.length > 0 ?
(selectedNodes[0].color || '#3355aa') :
(selectedGroups.length > 0 ? (selectedGroups[0].color || '#3355aa') : '#3355aa');
const colorInput = document.createElement('input');
colorInput.type = 'color';
colorInput.value = defaultColor.replace(/rgba?\(.*\)/, '#000000');
colorInput.style.position = 'absolute';
colorInput.style.visibility = 'hidden';
document.body.appendChild(colorInput);
// 处理颜色变化 -
colorInput.addEventListener('input', (e) => {
const color = e.target.value;
// 应用颜色到节点和组
selectedNodes.forEach(node => {
node.color = color;
});
selectedGroups.forEach(group => {
group.color = color;
});
app.graph.setDirtyCanvas(true, true);
});
colorInput.addEventListener('change', (e) => {
this._showNotification("已应用自定义颜色", "info");
document.body.removeChild(colorInput);
});
// 模拟点击打开颜色选择器
colorInput.click();
}, 100);
} catch (error) {
console.error("打开颜色选择器失败:", error);
this._showNotification(`打开颜色选择器失败: ${error.message}`);
}
}
// 获取ComfyUI应用实例
_getComfyUIApp() {
if (window.app?.canvas && window.app?.graph) {
return window.app;
}
if (window.LiteGraph?.LGraphCanvas?.active_canvas) {
const canvas = LiteGraph.LGraphCanvas.active_canvas;
if (canvas?.graph) {
return { canvas, graph: canvas.graph };
}
}
const canvasElement = document.querySelector(".litegraph.litegraph-canvas");
if (canvasElement?.lgraphcanvas) {
const canvas = canvasElement.lgraphcanvas;
if (canvas?.graph) {
return { canvas, graph: canvas.graph };
}
}
return null;
}
// 获取选中的节点
_getSelectedNodes(app) {
if (app.canvas.selected_nodes?.length) {
return Array.from(app.canvas.selected_nodes);
}
const selectedNodes = [];
if (app.graph?._nodes) {
for (const node of app.graph._nodes) {
if (node.is_selected) {
selectedNodes.push(node);
}
}
}
return selectedNodes;
}
// 获取选中的组
_getSelectedGroups(app) {
const selectedGroups = [];
if (app.canvas?.selected_groups?.length) {
return Array.from(app.canvas.selected_groups);
}
if (app.graph?.groups) {
for (const group of app.graph.groups) {
if (group.selected) {
selectedGroups.push(group);
}
}
}
return selectedGroups;
}
// 显示通知信息
_showNotification(message, type = 'error') {
const notification = document.createElement('div');
notification.className = 'layout-notification';
notification.textContent = message;
// 添加更现代的通知样式
Object.assign(notification.style, {
position: 'fixed',
top: '24px',
left: '50%',
transform: 'translateX(-50%)',
padding: '12px 24px',
borderRadius: '8px',
fontSize: '14px',
fontWeight: '500',
zIndex: '10000',
transition: 'all 0.3s ease',
backdropFilter: 'blur(8px)',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.2)',
border: '1px solid rgba(255, 255, 255, 0.1)',
width: 'auto',
minWidth: '240px',
maxWidth: '90%',
textAlign: 'center',
opacity: '0',
transform: 'translateX(-50%) translateY(-20px)'
});
if (type === 'info') {
notification.style.backgroundColor = 'rgba(20, 120, 60, 0.85)';
notification.style.color = 'white';
} else if (type === 'warn') {
notification.style.backgroundColor = 'rgba(230, 150, 10, 0.85)';
notification.style.color = 'white';
} else {
notification.style.backgroundColor = 'rgba(200, 0, 0, 0.85)';
notification.style.color = 'white';
}
document.body.appendChild(notification);
// 显示动画 - 从上方移入
setTimeout(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateX(-50%) translateY(0)';
}, 10);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateX(-50%) translateY(-20px)'; // 向上方消失
setTimeout(() => {
if (notification.parentNode) {
document.body.removeChild(notification);
}
}, 300);
}, 3000);
}
}
export const DEFAULT_CONFIG = {
enabled: true, // 默认启用
shortcut: 'alt+x'
};