diff --git a/README-EN.md b/README-EN.md index 9e0f752..dbebe00 100644 --- a/README-EN.md +++ b/README-EN.md @@ -88,7 +88,9 @@ High-performance model loading optimizer with multiple optimization modes:
DD Interface Layout -- Alpha version implemented, can randomly or specifically color node titles, and quickly organize messy workflows into modules +- Beta version implemented, can randomly or specifically color node titles, and quickly organize messy workflows. +- Beautified UI, added theme system, each theme has two different styles to switch between, supports custom themes. +- Added modular presets, you can preset your own layouts and apply them with one click.
@@ -119,6 +121,9 @@ This extension now supports ComfyUI's native internationalization (i18n) system, 3. Currently only supports node translation, frontend functionality translation is not yet supported ## Version History +- v2.3.1 (2025-05-27) + - Improved UI, added theme options, and fixed bugs. + - v2.3.0 (2025-05-14) - Interface Layout Alpha 2.0 version released, with improved interface and basic tools - Now you can synchronize the width, height, and size of selected nodes, and align all selected nodes diff --git a/README.md b/README.md index b846939..541c3d1 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,9 @@
DD 界面布局 --已实现Alpha版本,可以随机或指定节点标题的颜色,可以对杂乱工作流做快速的模块化分类整理 +- 已实现Bate版本,可以随机或指定节点标题的颜色,可以对杂乱节点做快速整理 +- 美化UI,添加主题系统,每一种主题有两种不同风格可以切换,支持自定义主题 +- 添加模块化预设,可以预设自己的布局,一键应用
@@ -110,7 +112,10 @@ 3. 重启 ComfyUI 方法2(推荐) -使用Manager或启动器进行安装 +使用Manager或启动器通过git进行安装 + +方法3(推荐) +直接在Manager中搜索插件名称安装 ## 语言切换 @@ -121,6 +126,9 @@ 3. 暂时只支持节点翻译,暂不支持前端功能翻译切换 ## 版本历史 +- v2.3.1 (2025-05-27) + -完善界面UI添加主题选项,修复BUG。 + - v2.3.0 (2025-05-14) - 界面布局Alpha2.0版本上线,完善界面和添加基本工具,现在可以同步选中节点的宽高与大小,对齐所有的选中节点。 - 依然是测试版本,如果有BUG请及时反馈。 diff --git a/js/Intelligent Layout/LayoutCore.js b/js/Intelligent Layout/LayoutCore.js index a0992ac..dcf6f12 100644 --- a/js/Intelligent Layout/LayoutCore.js +++ b/js/Intelligent Layout/LayoutCore.js @@ -1,73 +1,40 @@ // 智能布局核心模块 -// 布局面板相关样式 -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; - } - } -`; +// 导入统一样式模块 +import { + layoutPanelStyles, + injectStyles, + removeStyles, + createPanelElement, + createButtonsContainer, + createButton, + createModeContainer, + createSelectBox, + addOptionsToSelect, + setPanelBackgroundOpacity, + setButtonsOpacity, + showNotification, + adjustButtonsLayout, + registerTheme, + getRegisteredThemes, + getTheme, + applyTheme, + setDefaultTheme, + getDefaultTheme, + createCoinElement, + showAtMousePosition +} from './styles/UIStyles.js'; -// 工具函数:向页面注入样式 -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); - } -} +// 导入通用工具函数 - 修改导入路径为styles文件夹 +import { + getComfyUIApp, + getSelectedNodes, + getSelectedGroups, + getRandomColor, + syncNodeSize, + syncNodeWidthHeight, + alignNodes +} from './styles/UIUtils.js'; // 跟踪鼠标位置 let mousePosition = { x: 0, y: 0 }; @@ -75,12 +42,15 @@ document.addEventListener('mousemove', (e) => { mousePosition = { x: e.clientX, y: e.clientY }; }); -export class LayoutPanel { constructor() { +export class LayoutPanel { + constructor() { this.shortcut = 'alt+x'; this.visible = false; this.container = null; this.buttonsContainer = null; this.colorMode = '完全随机'; // 默认为完全随机模式 + this.currentTheme = null; + this.coinElement = null; } setEnabled() { @@ -96,6 +66,31 @@ export class LayoutPanel { constructor() { } } + // 设置当前主题 + setTheme(themeId) { + if (!this.container) { + this._createPanel(); + } + + // 保存当前的透明度设置,以便应用主题后恢复 + const bgOpacity = this.container.dataset.bgOpacity; + const btnOpacity = this.container.dataset.btnOpacity; + + // 应用新主题 + if (applyTheme(themeId, this.container, this.coinElement)) { + this.currentTheme = themeId; + + // 主题应用后,恢复保存的透明度设置 + setTimeout(() => { + if (bgOpacity) this.setOpacity(parseFloat(bgOpacity) * 100); + if (btnOpacity) this.setButtonOpacity(parseFloat(btnOpacity) * 100); + }, 50); // 短延迟确保主题样式完全应用 + + return true; + } + return false; + } + // 只有通过toggle方法才能控制面板显示/隐藏,确保只能通过快捷键激活 toggle() { if (this.visible) { @@ -110,35 +105,25 @@ export class LayoutPanel { constructor() { this._createPanel(); } - // 使用当前鼠标位置 - this._showAtMousePosition(); + // 使用当前鼠标位置显示面板 + showAtMousePosition(this.container, mousePosition); this.container.style.display = 'flex'; this.container.style.pointerEvents = 'auto'; // 显示时允许事件 - // 使用动画效果显示 + // 使用动画效果显示 - 修改:使用 opacity 动画,不再修改 transform 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; + // 只淡出,不修改 transform this.container.style.opacity = '0'; - this.container.style.transform = 'translate(-50%, -50%) scale(0.95)'; this.container.style.pointerEvents = 'none'; // 隐藏时禁止事件 setTimeout(() => { @@ -153,105 +138,102 @@ export class LayoutPanel { constructor() { setOpacity(opacity) { // opacity: 0~100 if (!this.container) return; - const percent = Math.max(0, Math.min(100, Number(opacity))); - this.container.style.backgroundColor = `rgba(30, 30, 40, ${percent/100})`; + setPanelBackgroundOpacity(this.container, opacity); } - + + // 添加新方法:设置按钮透明度 + setButtonOpacity(opacity) { + // opacity: 0~100 + if (!this.container) return; + setButtonsOpacity(this.container, opacity); + } + _createPanel() { if (this.container) return; - // 创建容器 - this.container = document.createElement('div'); - this.container.className = 'layout-panel'; - // 只设置一次 position - 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'; - // 移除 backgroundColor 的硬编码,交由 setOpacity 控制 - this.container.style.border = '1px solid rgba(255, 255, 255, 0.1)'; - this.container.style.flexDirection = 'column'; - this.container.style.alignItems = 'center'; - this.container.style.gap = '16px'; + + // 使用外部样式模块创建面板 + this.container = createPanelElement(mousePosition); + + // 存储初始透明度值到容器数据属性中,便于后续恢复 + this.container.dataset.initialSetup = 'pending'; + // 初始化透明度为全局设置值 let opacity = 85; + let buttonOpacity = 90; try { if (window.app?.extensionManager?.setting) { opacity = window.app.extensionManager.setting.get("LayoutPanel.opacity", 85); + buttonOpacity = window.app.extensionManager.setting.get("LayoutPanel.buttonOpacity", 90); } - } catch(e) {} - this.setOpacity(opacity); - // 监听设置变化(如果有API) + } catch(e) { + console.warn("获取透明度设置失败:", e); + } + + // 将初始透明度值存储到数据属性 + this.container.dataset.initialBgOpacity = opacity / 100; + this.container.dataset.initialBtnOpacity = buttonOpacity / 100; + + // 预先保存透明度值到数据属性,但延迟应用 + // 这样可以确保主题应用后再设置透明度 + this.container.dataset.bgOpacity = opacity / 100; + this.container.dataset.btnOpacity = buttonOpacity / 100; + + // 确保面板存在后才设置透明度 + setTimeout(() => { + // 确保面板已经完全创建并添加到DOM中 + this.setOpacity(opacity); + this.setButtonOpacity(buttonOpacity); + }, 50); + + // 监听设置变化 if (window.app?.extensionManager?.setting?.onChange) { window.app.extensionManager.setting.onChange("LayoutPanel.opacity", (val) => { this.setOpacity(val); }); + + window.app.extensionManager.setting.onChange("LayoutPanel.buttonOpacity", (val) => { + this.setButtonOpacity(val); + }); } - // 创建按钮容器 - 居中对齐 - 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'; + + // 创建硬币容器 + this.coinElement = createCoinElement(); - // 创建随机颜色按钮 - const randomColorBtn = this._createButton('随机颜色', 'rainbow'); + // 确保硬币元素添加到面板顶部 + if (this.container.firstChild) { + this.container.insertBefore(this.coinElement, this.container.firstChild); + } else { + this.container.appendChild(this.coinElement); + } + + // 创建按钮容器 + this.buttonsContainer = createButtonsContainer(); + + // 所有按钮使用统一的创建和样式方法 + const randomColorBtn = createButton('随机颜色', 'normal'); randomColorBtn.addEventListener('click', (e) => { e.stopPropagation(); this._setRandomNodeColor(); }); // 创建自定义颜色按钮 - const customColorBtn = this._createButton('自定义颜色', 'pick'); + const customColorBtn = createButton('自定义颜色', 'normal'); customColorBtn.addEventListener('click', (e) => { e.stopPropagation(); this._openCustomColorPicker(); }); // 创建节点同步按钮 - const syncBtn = this._createButton('节点同步', 'sync', '#2db7f5'); + const syncBtn = createButton('节点同步', 'normal'); + // 节点同步模式选择器 - const syncModeContainer = document.createElement('div'); - syncModeContainer.style.display = 'flex'; - syncModeContainer.style.flexDirection = 'column'; - syncModeContainer.style.width = '100%'; - syncModeContainer.style.marginTop = '10px'; - syncModeContainer.style.gap = '8px'; - const syncModeLabel = document.createElement('div'); - syncModeLabel.textContent = '节点同步模式:'; - syncModeLabel.style.color = '#e1e1e1'; - syncModeLabel.style.fontSize = '14px'; - syncModeLabel.style.fontWeight = '500'; - const syncModeSelect = document.createElement('select'); - syncModeSelect.style.width = '100%'; - syncModeSelect.style.padding = '6px 10px'; - syncModeSelect.style.backgroundColor = 'rgba(45, 45, 55, 0.8)'; - syncModeSelect.style.color = '#e1e1e1'; - syncModeSelect.style.border = '1px solid rgba(255, 255, 255, 0.2)'; - syncModeSelect.style.borderRadius = '6px'; - syncModeSelect.style.outline = 'none'; - [ + const { modeContainer: syncModeContainer, modeLabel: syncModeLabel } = createModeContainer('节点同步模式:'); + const syncModeSelect = createSelectBox(); + addOptionsToSelect(syncModeSelect, [ { value: 'size', text: '同步大小' }, { value: 'wh', text: '同步宽高' }, { value: 'align', text: '同步对齐' } - ].forEach(option => { - const opt = document.createElement('option'); - opt.value = option.value; - opt.textContent = option.text; - syncModeSelect.appendChild(opt); - }); - syncModeContainer.appendChild(syncModeLabel); + ]); syncModeContainer.appendChild(syncModeSelect); syncBtn.addEventListener('click', (e) => { e.stopPropagation(); @@ -259,47 +241,24 @@ export class LayoutPanel { constructor() { }); // 创建颜色模式选择器 - const modeContainer = document.createElement('div'); - modeContainer.style.display = 'flex'; - modeContainer.style.flexDirection = 'column'; - modeContainer.style.width = '100%'; - modeContainer.style.marginTop = '10px'; - modeContainer.style.gap = '8px'; - - const modeLabel = document.createElement('div'); - modeLabel.textContent = '随机颜色模式:'; - modeLabel.style.color = '#e1e1e1'; - modeLabel.style.fontSize = '14px'; - modeLabel.style.fontWeight = '500'; - - const modeSelect = document.createElement('select'); - modeSelect.style.width = '100%'; - modeSelect.style.padding = '6px 10px'; - modeSelect.style.backgroundColor = 'rgba(45, 45, 55, 0.8)'; - modeSelect.style.color = '#e1e1e1'; - modeSelect.style.border = '1px solid rgba(255, 255, 255, 0.2)'; - modeSelect.style.borderRadius = '6px'; - modeSelect.style.outline = 'none'; - // 添加选项 - const options = [ + const { modeContainer, modeLabel } = createModeContainer('随机颜色模式:'); + const modeSelect = createSelectBox(); + addOptionsToSelect(modeSelect, [ { value: '完全随机', text: '完全随机(每个节点不同颜色)' }, { value: '统一随机', text: '统一随机(所有节点相同颜色)' } - ]; + ]); - options.forEach(option => { - const opt = document.createElement('option'); - opt.value = option.value; - opt.textContent = option.text; + // 设置默认选中项 + Array.from(modeSelect.options).forEach(option => { if (option.value === this.colorMode) { - opt.selected = true; + option.selected = true; } - modeSelect.appendChild(opt); }); // 监听选择变化 modeSelect.addEventListener('change', (e) => { this.colorMode = e.target.value; - }); modeContainer.appendChild(modeLabel); + }); modeContainer.appendChild(modeSelect); // 添加按钮到容器 @@ -312,6 +271,42 @@ export class LayoutPanel { constructor() { this.container.appendChild(modeContainer); this.container.appendChild(syncModeContainer); + // 添加面板调整大小时重新调整按钮文字大小的功能 + const resizeObserver = new ResizeObserver(() => { + if (this.buttonsContainer) { + const buttons = this.buttonsContainer.querySelectorAll('.layout-btn'); + buttons.forEach(button => { + // 检查是否有adjustTextFontSize函数 + if (typeof adjustTextFontSize === 'function') { + adjustTextFontSize(button); + } else if (window.adjustTextFontSize) { + window.adjustTextFontSize(button); + } + }); + } + }); + + // 观察容器大小变化 + resizeObserver.observe(this.container); + + // 调整按钮布局 + adjustButtonsLayout(this.buttonsContainer); + + // 强制触发一次字体调整,确保初始状态正确 + setTimeout(() => { + const buttons = this.buttonsContainer.querySelectorAll('.layout-btn'); + buttons.forEach(button => { + if (typeof adjustTextFontSize === 'function') { + adjustTextFontSize(button); + } else if (window.adjustTextFontSize) { + window.adjustTextFontSize(button); + } else { + // 如果没有找到调整函数,则确保基本字体大小 + button.style.fontSize = '15px'; + } + }); + }, 200); + // 使用外部样式模块注入样式 injectStyles('layout-panel-styles', layoutPanelStyles); @@ -320,152 +315,57 @@ export class LayoutPanel { constructor() { e.stopPropagation(); }); - // 添加可拖动区域(左上角) - const dragHandle = document.createElement('div'); - dragHandle.style.position = 'absolute'; - dragHandle.style.left = '0'; - dragHandle.style.top = '0'; - dragHandle.style.width = '32px'; - dragHandle.style.height = '32px'; - dragHandle.style.cursor = 'move'; - dragHandle.style.borderTopLeftRadius = '16px'; - dragHandle.style.background = 'linear-gradient(135deg, rgba(255,255,255,0.08) 60%, transparent 100%)'; - dragHandle.style.zIndex = '100001'; - dragHandle.title = '拖动面板'; - this.container.appendChild(dragHandle); - this.container.style.userSelect = 'none'; - // 拖动逻辑 - let isDragging = false; - let dragOffsetX = 0; - let dragOffsetY = 0; - dragHandle.addEventListener('mousedown', (e) => { - e.stopPropagation(); - isDragging = true; - // 记录鼠标与面板左上角的偏移 - const rect = this.container.getBoundingClientRect(); - dragOffsetX = e.clientX - rect.left; - dragOffsetY = e.clientY - rect.top; - document.body.style.cursor = 'move'; - }); - document.addEventListener('mousemove', (e) => { - if (isDragging) { - // 计算新位置 - const left = e.clientX - dragOffsetX; - const top = e.clientY - dragOffsetY; - this.container.style.left = `${left + 0.5}px`; - this.container.style.top = `${top + 0.5}px`; - this.container.style.transform = 'none'; - } - }); - document.addEventListener('mouseup', () => { - if (isDragging) { - isDragging = false; - document.body.style.cursor = ''; - } - }); - // 添加到文档 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; + // 应用当前主题或默认主题 + setTimeout(() => { + let themeId = null; + try { + if (window.app?.extensionManager?.setting) { + themeId = window.app.extensionManager.setting.get("LayoutPanel.theme"); + } + } catch(e) { + console.warn("获取主题设置失败:", e); } - 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; + + if (!themeId) { + themeId = getDefaultTheme(); + } + + if (themeId) { + this.setTheme(themeId); + + // 最后应用透明度,确保不被主题覆盖 + setTimeout(() => { + this.container.dataset.initialSetup = 'completed'; + this.setOpacity(opacity); + this.setButtonOpacity(buttonOpacity); + }, 150); + } + }, 100); } - // 此方法已被移除,使用_showAtMousePosition替代 - // 设置随机节点颜色 + + // 设置随机节点颜色 _setRandomNodeColor() { try { // 获取ComfyUI应用实例 - const app = this._getComfyUIApp(); + const app = getComfyUIApp(); if (!app) { - this._showNotification("无法获取ComfyUI应用实例"); + showNotification("无法获取ComfyUI应用实例"); return; } // 获取选中的节点和组 - const selectedNodes = this._getSelectedNodes(app); - const selectedGroups = this._getSelectedGroups(app); + const selectedNodes = getSelectedNodes(app); + const selectedGroups = getSelectedGroups(app); if (selectedNodes.length === 0 && selectedGroups.length === 0) { - this._showNotification("请先选择要应用颜色的节点或组"); + 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; - }; - // 根据颜色模式应用颜色 + // 根据颜色模式应用颜色 if (this.colorMode === '统一随机') { // 统一随机模式 - 所有节点和组使用同一个随机颜色 const uniformColor = getRandomColor(); @@ -478,7 +378,7 @@ export class LayoutPanel { constructor() { group.color = uniformColor; }); - this._showNotification("已应用统一随机颜色", "info"); + showNotification("已应用统一随机颜色", "info"); } else { // 完全随机模式 - 每个节点和组使用不同的随机颜色 selectedNodes.forEach(node => { @@ -490,13 +390,12 @@ export class LayoutPanel { constructor() { group.color = getRandomColor(); }); - this._showNotification("已应用完全随机颜色", "info"); + showNotification("已应用完全随机颜色", "info"); } app.graph.setDirtyCanvas(true, true); - // 不再自动关闭面板,面板只通过快捷键 toggle() 控制显示/隐藏 } catch (error) { console.error("设置随机颜色失败:", error); - this._showNotification(`设置随机颜色失败: ${error.message}`); + showNotification(`设置随机颜色失败: ${error.message}`); } } @@ -504,22 +403,21 @@ export class LayoutPanel { constructor() { _openCustomColorPicker() { try { // 获取ComfyUI应用实例 - const app = this._getComfyUIApp(); + const app = getComfyUIApp(); if (!app) { - this._showNotification("无法获取ComfyUI应用实例"); + showNotification("无法获取ComfyUI应用实例"); return; } // 获取选中的节点和组 - const selectedNodes = this._getSelectedNodes(app); - const selectedGroups = this._getSelectedGroups(app); + const selectedNodes = getSelectedNodes(app); + const selectedGroups = getSelectedGroups(app); if (selectedNodes.length === 0 && selectedGroups.length === 0) { - this._showNotification("请先选择要应用颜色的节点或组"); + showNotification("请先选择要应用颜色的节点或组"); return; } - // 不再自动关闭面板,面板只通过快捷键 toggle() 控制显示/隐藏 // 创建颜色选择器 setTimeout(() => { const defaultColor = selectedNodes.length > 0 ? @@ -533,7 +431,7 @@ export class LayoutPanel { constructor() { colorInput.style.visibility = 'hidden'; document.body.appendChild(colorInput); - // 处理颜色变化 - + // 处理颜色变化 colorInput.addEventListener('input', (e) => { const color = e.target.value; @@ -547,234 +445,60 @@ export class LayoutPanel { constructor() { }); app.graph.setDirtyCanvas(true, true); - }); colorInput.addEventListener('change', (e) => { this._showNotification("已应用自定义颜色", "info"); + }); + + colorInput.addEventListener('change', (e) => { + showNotification("已应用自定义颜色", "info"); document.body.removeChild(colorInput); }); + // 模拟点击打开颜色选择器 colorInput.click(); }, 100); } catch (error) { console.error("打开颜色选择器失败:", error); - this._showNotification(`打开颜色选择器失败: ${error.message}`); + 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; - // 通知基本样式 - const baseNotificationStyles = { - position: 'fixed', - top: '24px', - left: '50%', - transform: 'translateX(-50%) translateY(-20px)', - 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' - }; - - // 应用样式 - Object.assign(notification.style, baseNotificationStyles); - - 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); - } - - // 辅助函数:判断节点排列主方向 - _getMainDirection(centers) { - let dx = 0, dy = 0; - for (let i = 1; i < centers.length; i++) { - dx += Math.abs(centers[i].x - centers[0].x); - dy += Math.abs(centers[i].y - centers[0].y); - } - return dx > dy ? 'horizontal' : 'vertical'; - } - // 节点同步逻辑 _syncSelectedNodes(mode) { try { - const app = this._getComfyUIApp(); + const app = getComfyUIApp(); if (!app) { - this._showNotification('无法获取ComfyUI应用实例'); + showNotification('无法获取ComfyUI应用实例'); return; } - const selectedNodes = this._getSelectedNodes(app); + + const selectedNodes = getSelectedNodes(app); if (selectedNodes.length < 2) { - this._showNotification('请至少选择两个节点进行同步'); + showNotification('请至少选择两个节点进行同步'); return; } + + let result; + if (mode === 'size') { // 大小同步:统一为最大宽高 - let maxW = 0, maxH = 0; - selectedNodes.forEach(node => { - if (node.size) { - maxW = Math.max(maxW, node.size[0]); - maxH = Math.max(maxH, node.size[1]); - } - }); - selectedNodes.forEach(node => { - if (node.size) { - node.size[0] = maxW; - node.size[1] = maxH; - } - }); - this._showNotification('已同步为统一大小', 'info'); + result = syncNodeSize(selectedNodes); + showNotification('已同步为统一大小', 'info'); } else if (mode === 'wh') { // 宽高同步:判断排列方向 - const centers = selectedNodes.map(node => ({ - node, - x: node.pos ? node.pos[0] + (node.size ? node.size[0] / 2 : 0) : 0, - y: node.pos ? node.pos[1] + (node.size ? node.size[1] / 2 : 0) : 0 - })); - const mainDir = this._getMainDirection(centers); - if (mainDir === 'horizontal') { - // 横向排列,同步高度 - let maxH = 0; - selectedNodes.forEach(node => { - if (node.size) maxH = Math.max(maxH, node.size[1]); - }); - selectedNodes.forEach(node => { - if (node.size) node.size[1] = maxH; - }); - this._showNotification('已同步高度(横向排列)', 'info'); - } else { - // 纵向排列,同步宽度 - let maxW = 0; - selectedNodes.forEach(node => { - if (node.size) maxW = Math.max(maxW, node.size[0]); - }); - selectedNodes.forEach(node => { - if (node.size) node.size[0] = maxW; - }); - this._showNotification('已同步宽度(竖向排列)', 'info'); - } + result = syncNodeWidthHeight(selectedNodes); + const direction = result.direction === 'horizontal' ? '横向排列' : '纵向排列'; + const dimension = result.direction === 'horizontal' ? '高度' : '宽度'; + showNotification(`已同步${dimension}(${direction})`, 'info'); } else if (mode === 'align') { // 同步对齐:自动排列节点 - const centers = selectedNodes.map(node => ({ - node, - x: node.pos ? node.pos[0] + (node.size ? node.size[0] / 2 : 0) : 0, - y: node.pos ? node.pos[1] + (node.size ? node.size[1] / 2 : 0) : 0 - })); - const mainDir = this._getMainDirection(centers); - if (mainDir === 'horizontal') { - // 横向排列,对齐Y轴 - const avgY = centers.reduce((sum, c) => sum + c.y, 0) / centers.length; - selectedNodes.forEach(node => { - if (node.pos) node.pos[1] = avgY - (node.size ? node.size[1] / 2 : 0); - }); - this._showNotification('已横向对齐(Y轴对齐)', 'info'); - } else { - // 纵向排列,对齐X轴 - const avgX = centers.reduce((sum, c) => sum + c.x, 0) / centers.length; - selectedNodes.forEach(node => { - if (node.pos) node.pos[0] = avgX - (node.size ? node.size[0] / 2 : 0); - }); - this._showNotification('已纵向对齐(X轴对齐)', 'info'); - } + result = alignNodes(selectedNodes); + const alignment = result.direction === 'horizontal' ? 'Y轴对齐' : 'X轴对齐'; + const direction = result.direction === 'horizontal' ? '横向' : '纵向'; + showNotification(`已${direction}对齐(${alignment})`, 'info'); } + app.graph.setDirtyCanvas(true, true); } catch (e) { - this._showNotification('节点同步失败: ' + e.message); + showNotification('节点同步失败: ' + e.message); } } } @@ -783,5 +507,6 @@ export const DEFAULT_CONFIG = { enabled: true, // 默认启用 shortcut: 'alt+x', colorMode: '完全随机', // 默认颜色模式 - continueIteration: false // 默认应用颜色后关闭面板 + continueIteration: false, // 默认应用颜色后关闭面板 + theme: '古神之眼' // 默认主题 }; \ No newline at end of file diff --git a/js/Intelligent Layout/LayoutExt.js b/js/Intelligent Layout/LayoutExt.js index 273ab08..1518a27 100644 --- a/js/Intelligent Layout/LayoutExt.js +++ b/js/Intelligent Layout/LayoutExt.js @@ -1,11 +1,24 @@ // 智能布局扩展 import { app } from "/scripts/app.js"; import { LayoutPanel, DEFAULT_CONFIG } from "./LayoutCore.js"; +import { registerTheme, getRegisteredThemes, setDefaultTheme, initDefaultThemes } from "./styles/UIStyles.js"; + +// 初始化默认主题 +try { + // 调用主题初始化函数 + if (initDefaultThemes()) { + console.log("主题系统已成功初始化"); + } else { + console.warn("主题系统初始化返回失败"); + } +} catch (error) { + console.error("主题系统初始化失败:", error); +} // 确保全局唯一实例 function getOrCreateLayoutInstance() { if (!app.canvas) { - console.warn("智能布局:app.canvas未初始化"); + console.warn("界面布局:app.canvas未初始化"); return null; } @@ -58,23 +71,64 @@ export const layoutExt = { this.initLayoutPanel(); }, - initLayoutPanel() { + initLayoutPanel() { // 初始化布局面板 const layoutPanel = getOrCreateLayoutInstance(); if (!layoutPanel) return; - // 应用初始设置 - 功能始终启用 + // 应用所有初始设置 - 确保所有参数都在初始化时应用 const applySettings = () => { - // 默认始终启用,不再依赖设置 - layoutPanel.setEnabled(true); - const shortcut = app.extensionManager.setting.get("LayoutPanel.shortcut", DEFAULT_CONFIG.shortcut); - layoutPanel.setShortcut(shortcut); + try { + // 主要设置 + const shortcut = app.extensionManager?.setting?.get("LayoutPanel.shortcut") ?? DEFAULT_CONFIG.shortcut; + const theme = app.extensionManager?.setting?.get("LayoutPanel.theme") ?? DEFAULT_CONFIG.theme; + + // 透明度设置 + const opacity = app.extensionManager?.setting?.get("LayoutPanel.opacity") ?? 85; + const buttonOpacity = app.extensionManager?.setting?.get("LayoutPanel.buttonOpacity") ?? 90; + + // 启用面板(始终启用) + layoutPanel.setEnabled(true); + + // 应用所有设置 - 顺序很重要 + layoutPanel.setShortcut(shortcut); + layoutPanel.setTheme(theme); + + // 确保透明度设置最后应用,这样不会被主题初始化覆盖 + setTimeout(() => { + layoutPanel.setOpacity(opacity); + layoutPanel.setButtonOpacity(buttonOpacity); + }, 100); + + console.log("布局面板初始设置已加载:", { shortcut, theme, opacity, buttonOpacity }); + } catch (e) { + console.warn("应用设置时出错:", e); + // 应用默认设置作为回退 + layoutPanel.setShortcut(DEFAULT_CONFIG.shortcut); + layoutPanel.setTheme(DEFAULT_CONFIG.theme); + layoutPanel.setOpacity(85); + layoutPanel.setButtonOpacity(90); + } }; applySettings(); } }; +// 获取所有注册主题用于设置选择 +function getThemeOptions() { + try { + const themes = getRegisteredThemes(); + return themes.map(theme => ({ + text: theme.name, + value: theme.id + })); + } catch (e) { + console.error("获取主题选项失败:", e); + return [{ text: '古神之眼', value: '古神之眼' }]; + } +} + // 注册设置面板 app.registerExtension({ name: "ComfyUI.LayoutPanel.Settings", @@ -88,17 +142,45 @@ app.registerExtension({ // 初始化布局面板 const layoutPanel = getOrCreateLayoutInstance(); if (!layoutPanel) return; - // 始终启用布局功能,不再依赖设置 layoutPanel.setEnabled(true); - layoutPanel.setShortcut(app.extensionManager.setting.get("LayoutPanel.shortcut") ?? DEFAULT_CONFIG.shortcut); - }, settings: [ + + // 确保显式加载和应用所有设置,与init方法保持一致 + try { + // 先启用面板 + layoutPanel.setEnabled(true); + + // 然后应用所有保存的设置 + const shortcut = app.extensionManager?.setting?.get("LayoutPanel.shortcut") ?? DEFAULT_CONFIG.shortcut; + const theme = app.extensionManager?.setting?.get("LayoutPanel.theme") ?? DEFAULT_CONFIG.theme; + const opacity = app.extensionManager?.setting?.get("LayoutPanel.opacity") ?? 85; + const buttonOpacity = app.extensionManager?.setting?.get("LayoutPanel.buttonOpacity") ?? 90; + + // 应用设置 - 注意顺序 + layoutPanel.setShortcut(shortcut); + layoutPanel.setTheme(theme); + + // 确保透明度在主题后设置 + setTimeout(() => { + layoutPanel.setOpacity(opacity); + layoutPanel.setButtonOpacity(buttonOpacity); + }, 100); + } catch (e) { + console.warn("应用设置时出错:", e); + // 应用默认设置 + layoutPanel.setShortcut(DEFAULT_CONFIG.shortcut); + layoutPanel.setTheme(DEFAULT_CONFIG.theme); + layoutPanel.setOpacity(85); + layoutPanel.setButtonOpacity(90); + } + }, + settings: [ // 移除启用/禁用设置选项 { id: "LayoutPanel.shortcut", - name: "快捷键", + name: "快捷呼出", type: "text", defaultValue: DEFAULT_CONFIG.shortcut, - tooltip: "弹出节点颜色工具面板的快捷键(如alt+x)", - category: ["🍺界面布局", "1·功能", "快捷键"], + tooltip: "弹出界面布局工具面板的快捷键(如alt+x)", + category: ["🍺界面布局", "1·功能", "快捷呼出"], onChange(value) { if (typeof value === 'string' && value.includes('+')) { const layoutPanel = getOrCreateLayoutInstance(); @@ -110,31 +192,69 @@ app.registerExtension({ }, { id: "LayoutPanel.opacity", - name: "界面透明度", + name: "背景透明", type: "slider", defaultValue: 85, attrs: { min: 0, max: 100, step: 1 }, - tooltip: "设置智能布局主面板的透明度(0~100%)", - category: ["🍺界面布局", "2·外观", "界面透明度"], + tooltip: "设置界面布局主面板背景的透明度(0~100%)", + category: ["🍺界面布局", "2·外观", "背景透明"], onChange(value) { const layoutPanel = getOrCreateLayoutInstance(); if (layoutPanel && typeof layoutPanel.setOpacity === 'function') { layoutPanel.setOpacity(value); } } + }, + { + id: "LayoutPanel.buttonOpacity", + name: "按钮透明", + type: "slider", + defaultValue: 90, + attrs: { min: 0, max: 100, step: 1 }, + tooltip: "设置界面布局面板中按钮的透明度(0~100%)", + category: ["🍺界面布局", "2·外观", "按钮透明"], + onChange(value) { + const layoutPanel = getOrCreateLayoutInstance(); + if (layoutPanel && typeof layoutPanel.setButtonOpacity === 'function') { + layoutPanel.setButtonOpacity(value); + } + } + }, + { + id: "LayoutPanel.theme", + name: "界面主题", + type: "combo", + defaultValue: DEFAULT_CONFIG.theme, + tooltip: "选择界面布局面板的视觉主题", + options: getThemeOptions, + category: ["🍺界面布局", "2·外观", "界面主题"], + onChange(value) { + const layoutPanel = getOrCreateLayoutInstance(); + if (layoutPanel && typeof layoutPanel.setTheme === 'function') { + layoutPanel.setTheme(value); + } + } } ] }); +// 主扩展注册 app.registerExtension({ name: "ComfyUI.LayoutPanel", init() { - setTimeout(() => { - if (app.canvas) { - layoutExt.init(); - } else { - console.error("智能布局:延迟初始化失败,app.canvas仍未准备好"); - } - }, 500); + try { + console.log("正在初始化智能布局面板..."); + // 延迟初始化,确保其他扩展已经加载 + setTimeout(() => { + if (app.canvas) { + layoutExt.init(); + console.log("智能布局面板初始化完成"); + } else { + console.error("智能布局:延迟初始化失败,app.canvas仍未准备好"); + } + }, 1000); + } catch (e) { + console.error("初始化智能布局面板时出错:", e); + } } }); \ No newline at end of file diff --git a/js/Intelligent Layout/styles/UIStyles.js b/js/Intelligent Layout/styles/UIStyles.js new file mode 100644 index 0000000..616c891 --- /dev/null +++ b/js/Intelligent Layout/styles/UIStyles.js @@ -0,0 +1,845 @@ +// 智能布局 UI 样式统一模块 + +// ========================================================== +// 主题管理相关功能 +// ========================================================== + +// 导入主题模块 +import AncientGodTheme from "./古神之眼/AncientGodTheme.js"; + +// 存储所有已注册的主题 +const registeredThemes = {}; +let currentTheme = null; +let defaultTheme = '古神之眼'; + +// 自动注册默认主题 +export function initDefaultThemes() { try { + // 注册主题 + registerTheme('古神之眼', AncientGodTheme); + + setDefaultTheme('古神之眼'); + console.log("默认主题已成功注册"); + return true; + } catch (error) { + console.error("注册默认主题失败:", error); + return false; + } +} + +// 注册主题 - 主题必须实现特定接口 +export function registerTheme(id, themeModule) { + if (!id || typeof id !== 'string') { + console.error('主题ID必须是有效字符串'); + return false; + } + + // 验证主题模块是否符合接口规范 + if (!themeModule || typeof themeModule !== 'object') { + console.error(`主题 "${id}" 必须是有效对象`); + return false; + } + + // 验证必要的方法 + const requiredMethods = ['init', 'applyTheme', 'flipCoin', 'cleanup']; + for (const method of requiredMethods) { + if (typeof themeModule[method] !== 'function') { + console.error(`主题 "${id}" 缺少必要方法: ${method}`); + return false; + } + } + + // 验证必要的属性 + if (!themeModule.name) { + console.error(`主题 "${id}" 缺少名称属性`); + return false; + } + + // 注册主题 + registeredThemes[id] = themeModule; + console.log(`主题 "${id}" (${themeModule.name}) 已成功注册`); + return true; +} + +// 获取已注册的所有主题 +export function getRegisteredThemes() { + const themes = []; + for (const id in registeredThemes) { + themes.push({ + id: id, + name: registeredThemes[id].name || id + }); + } + return themes; +} + +// 获取主题模块 +export function getTheme(id) { + return registeredThemes[id] || null; +} + +// 应用主题 +export function applyTheme(id, container, coinElement) { + // 清理当前主题(如果有) + if (currentTheme && registeredThemes[currentTheme]) { + registeredThemes[currentTheme].cleanup(); + } + + // 获取并应用新主题 + const themeModule = registeredThemes[id]; + if (!themeModule) { + console.error(`主题 "${id}" 未注册`); + return false; + } + + // 初始化并应用新主题 + // 确保硬币元素存在且为初始状态 + if (coinElement) { + // 移除可能的 flipped 类,确保硬币始终从正面开始 + if (coinElement.classList.contains('flipped')) { + coinElement.classList.remove('flipped'); + } + } + + // 强制重新创建背景和硬币 + themeModule.init(container, coinElement); + themeModule.applyTheme(); + currentTheme = id; + + return true; +} + +// 设置默认主题 +export function setDefaultTheme(id) { + if (registeredThemes[id]) { + defaultTheme = id; + return true; + } + return false; +} + +// 获取默认主题 +export function getDefaultTheme() { + return defaultTheme; +} + +// ========================================================== +// 通用工具函数 +// ========================================================== + +// 工具函数:向页面注入样式 +export 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); +} + +// 工具函数:移除已注入的样式 +export function removeStyles(styleId) { + const styleElement = document.getElementById(styleId); + if (styleElement && styleElement.parentNode) { + styleElement.parentNode.removeChild(styleElement); + } +} + +// ========================================================== +// CSS 样式定义 - 简化为最基本的结构,其余移至主题实现 +// ========================================================== + +// 布局面板相关样式 - 只保留必要的基础结构 +export const layoutPanelStyles = ` + .layout-panel { + position: fixed; + z-index: 99999; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + user-select: none; + transition: opacity 0.3s; + border-radius: 16px; + } +`; + +// ========================================================== +// 面板相关函数 +// ========================================================== + +// 创建面板UI元素 +export function createPanelElement(mousePosition) { + // 创建容器 + const container = document.createElement('div'); + container.className = 'layout-panel'; + // 初始位置,后续会根据面板实际大小进行调整 + container.style.left = `${mousePosition.x}px`; + container.style.top = `${mousePosition.y}px`; + container.style.transform = 'none'; + container.style.display = 'none'; + container.style.pointerEvents = 'none'; + container.style.opacity = '0'; + container.style.padding = '30px'; // 增加内边距适应大尺寸硬币 + container.style.width = '360px'; // 设置基础宽度 + container.style.boxSizing = 'border-box'; // 确保padding不增加总宽度 + + // 添加拖动区域 - 改为在面板边缘创建多个拖动区域 + addDragHandles(container); + + // 注入基础样式 + injectStyles('layout-panel-styles', layoutPanelStyles); + + return container; +} + +// 添加多个拖动区域到面板边缘 +function addDragHandles(container) { + // 创建拖动状态对象 - 所有拖动句柄共享 + const dragState = { + isDragging: false, + offsetX: 0, + offsetY: 0 + }; + + // 拖动处理函数 - 所有拖动区域共享 + const handleMouseMove = function(e) { + if (!dragState.isDragging) return; + + // 计算新位置 + const left = e.clientX - dragState.offsetX; + const top = e.clientY - dragState.offsetY; + + // 使用 requestAnimationFrame 减少重绘次数 + requestAnimationFrame(() => { + container.style.left = `${left}px`; + container.style.top = `${top}px`; + // 一旦开始拖动,就固定不使用 transform,避免布局重新计算 + if (container.style.transform !== 'none') { + container.style.transform = 'none'; + } + }); + }; + + const handleMouseUp = function() { + if (!dragState.isDragging) return; + + dragState.isDragging = false; + document.body.style.cursor = ''; + + // 重要:在拖动结束后移除临时添加的事件监听器 + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + // 创建拖动开始函数 + const createDragStart = (handle) => { + return (e) => { + e.stopPropagation(); + e.preventDefault(); + + // 记录鼠标与面板左上角的偏移 + const rect = container.getBoundingClientRect(); + dragState.offsetX = e.clientX - rect.left; + dragState.offsetY = e.clientY - rect.top; + + dragState.isDragging = true; + document.body.style.cursor = 'move'; + + // 只在开始拖动时添加事件监听器 + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }; + }; + + // 定义边缘宽度 + const edgeWidth = 15; // 拖动区域宽度,px + + // 创建顶部拖动区域 + const topHandle = document.createElement('div'); + topHandle.className = 'layout-drag-handle layout-drag-handle-top'; + Object.assign(topHandle.style, { + position: 'absolute', + top: '0', + left: edgeWidth + 'px', + right: edgeWidth + 'px', + height: edgeWidth + 'px', + cursor: 'move', + zIndex: '10001' + }); + topHandle.addEventListener('mousedown', createDragStart(topHandle)); + container.appendChild(topHandle); + + // 创建底部拖动区域 + const bottomHandle = document.createElement('div'); + bottomHandle.className = 'layout-drag-handle layout-drag-handle-bottom'; + Object.assign(bottomHandle.style, { + position: 'absolute', + bottom: '0', + left: edgeWidth + 'px', + right: edgeWidth + 'px', + height: edgeWidth + 'px', + cursor: 'move', + zIndex: '10001' + }); + bottomHandle.addEventListener('mousedown', createDragStart(bottomHandle)); + container.appendChild(bottomHandle); + + // 创建左侧拖动区域 + const leftHandle = document.createElement('div'); + leftHandle.className = 'layout-drag-handle layout-drag-handle-left'; + Object.assign(leftHandle.style, { + position: 'absolute', + top: edgeWidth + 'px', + bottom: edgeWidth + 'px', + left: '0', + width: edgeWidth + 'px', + cursor: 'move', + zIndex: '10001' + }); + leftHandle.addEventListener('mousedown', createDragStart(leftHandle)); + container.appendChild(leftHandle); + + // 创建右侧拖动区域 + const rightHandle = document.createElement('div'); + rightHandle.className = 'layout-drag-handle layout-drag-handle-right'; + Object.assign(rightHandle.style, { + position: 'absolute', + top: edgeWidth + 'px', + bottom: edgeWidth + 'px', + right: '0', + width: edgeWidth + 'px', + cursor: 'move', + zIndex: '10001' + }); + rightHandle.addEventListener('mousedown', createDragStart(rightHandle)); + container.appendChild(rightHandle); + + // 创建四个角落的拖动区域 + const topLeftHandle = document.createElement('div'); + topLeftHandle.className = 'layout-drag-handle layout-drag-handle-top-left'; + Object.assign(topLeftHandle.style, { + position: 'absolute', + top: '0', + left: '0', + width: edgeWidth + 'px', + height: edgeWidth + 'px', + cursor: 'move', + zIndex: '10001', + borderTopLeftRadius: '16px' + }); + topLeftHandle.addEventListener('mousedown', createDragStart(topLeftHandle)); + container.appendChild(topLeftHandle); + + const topRightHandle = document.createElement('div'); + topRightHandle.className = 'layout-drag-handle layout-drag-handle-top-right'; + Object.assign(topRightHandle.style, { + position: 'absolute', + top: '0', + right: '0', + width: edgeWidth + 'px', + height: edgeWidth + 'px', + cursor: 'move', + zIndex: '10001', + borderTopRightRadius: '16px' + }); + topRightHandle.addEventListener('mousedown', createDragStart(topRightHandle)); + container.appendChild(topRightHandle); + + const bottomLeftHandle = document.createElement('div'); + bottomLeftHandle.className = 'layout-drag-handle layout-drag-handle-bottom-left'; + Object.assign(bottomLeftHandle.style, { + position: 'absolute', + bottom: '0', + left: '0', + width: edgeWidth + 'px', + height: edgeWidth + 'px', + cursor: 'move', + zIndex: '10001', + borderBottomLeftRadius: '16px' + }); + bottomLeftHandle.addEventListener('mousedown', createDragStart(bottomLeftHandle)); + container.appendChild(bottomLeftHandle); + + const bottomRightHandle = document.createElement('div'); + bottomRightHandle.className = 'layout-drag-handle layout-drag-handle-bottom-right'; + Object.assign(bottomRightHandle.style, { + position: 'absolute', + bottom: '0', + right: '0', + width: edgeWidth + 'px', + height: edgeWidth + 'px', + cursor: 'move', + zIndex: '10001', + borderBottomRightRadius: '16px' + }); + bottomRightHandle.addEventListener('mousedown', createDragStart(bottomRightHandle)); + container.appendChild(bottomRightHandle); + + // 为调试可视化边框 - 通过注释控制是否添加 + // injectStyles('drag-handles-debug', ` + // .layout-drag-handle { + // border: 1px dashed rgba(255, 255, 255, 0.15); + // background-color: rgba(128, 128, 255, 0.05); + // } + // `); +} + +// ========================================================== +// UI 控件相关函数 +// ========================================================== + +// 创建按钮容器 +export function createButtonsContainer() { + const buttonsContainer = document.createElement('div'); + buttonsContainer.className = 'layout-buttons-container'; + buttonsContainer.style.display = 'flex'; + buttonsContainer.style.flexWrap = 'wrap'; // 允许按钮在需要时换行 + buttonsContainer.style.justifyContent = 'center'; // 居中排列 + buttonsContainer.style.width = '100%'; + buttonsContainer.style.gap = '8px'; // 按钮之间的间距 + + return buttonsContainer; +} + +// 创建按钮 +export function createButton(text, type, bgColor) { + const button = document.createElement('button'); + button.className = `layout-btn layout-btn-${type}`; + button.textContent = text; + button.title = text; + + // 自适应布局的按钮样式 + button.style.padding = '8px 12px'; + button.style.flexGrow = '1'; // 按钮可以增长 + button.style.flexBasis = '0'; // 所有按钮基础宽度相同 + button.style.minWidth = '80px'; // 最小宽度 + button.style.maxWidth = 'none'; // 移除最大宽度限制,让按钮自动适应 + button.style.textAlign = 'center'; // 确保文本居中 + button.style.justifyContent = 'center'; // Flex布局居中 + button.style.alignItems = 'center'; // Flex布局居中 + button.style.display = 'flex'; // 改为flex布局以确保内容居中 + button.style.cursor = 'pointer'; + button.style.borderRadius = '10px'; + button.style.transition = 'all 0.2s ease'; + button.style.margin = '1px'; + button.style.whiteSpace = 'nowrap'; // 防止文字换行 + button.style.overflow = 'hidden'; + + // 使用自定义属性存储原始文本和字体大小,便于调整显示 + button.dataset.originalText = text; + button.dataset.originalFontSize = '15px'; // 调大默认字体大小为15px(原为14px) + button.style.fontSize = button.dataset.originalFontSize; + + // 添加点击效果 + button.addEventListener('mousedown', () => { + button.style.transform = 'translateY(1px)'; + }); + + button.addEventListener('mouseup', () => { + button.style.transform = 'translateY(0)'; + }); + + // 添加自适应字体大小功能 + button.addEventListener('DOMNodeInserted', () => { + setTimeout(() => adjustTextFontSize(button), 0); + }); + + return button; +} + +// 新增函数:调整文字字体大小以适应按钮宽度 +function adjustTextFontSize(button) { + if (!button || !button.dataset.originalText) return; + + // 重置为原始字体大小以便测量 + button.style.fontSize = button.dataset.originalFontSize || '15px'; + + // 计算按钮可用宽度(考虑内边距) + const buttonWidth = button.clientWidth; + const paddingLeft = parseInt(getComputedStyle(button).paddingLeft) || 0; + const paddingRight = parseInt(getComputedStyle(button).paddingRight) || 0; + const availableWidth = buttonWidth - paddingLeft - paddingRight - 4; // 额外留4px边距 + + // 创建临时元素测量文本宽度 + const tempSpan = document.createElement('span'); + tempSpan.style.visibility = 'hidden'; + tempSpan.style.position = 'absolute'; + tempSpan.style.whiteSpace = 'nowrap'; + tempSpan.style.fontSize = button.dataset.originalFontSize || '15px'; + tempSpan.textContent = button.dataset.originalText; + document.body.appendChild(tempSpan); + + const textWidth = tempSpan.offsetWidth; + document.body.removeChild(tempSpan); + + // 如果文本宽度大于可用宽度,则缩小字体 + if (textWidth > availableWidth) { + const scaleFactor = availableWidth / textWidth; + const originalFontSize = parseInt(button.dataset.originalFontSize) || 15; + // 限制字体最小缩小到11px(原为9px),确保字体不会太小 + const newFontSize = Math.max(11, Math.floor(originalFontSize * scaleFactor)); + + button.style.fontSize = `${newFontSize}px`; + } else { + // 恢复原始字体大小 + button.style.fontSize = button.dataset.originalFontSize || '15px'; + } + + // 确保显示完整文本 + button.textContent = button.dataset.originalText; +} + +// 调整按钮布局 - 添加新函数用于根据按钮数量自动调整布局 +export function adjustButtonsLayout(buttonsContainer) { + if (!buttonsContainer) return; + + const buttons = buttonsContainer.querySelectorAll('.layout-btn'); + const count = buttons.length; + if (count === 0) return; + + // 根据按钮数量决定每行显示多少个按钮 + let buttonsPerRow; + if (count <= 3) { + // 1-3个按钮时,放在一行 + buttonsPerRow = count; + } else if (count === 4) { + // 4个按钮时,每行2个 + buttonsPerRow = 2; + } else { + // 5个及以上按钮时,每行3个 + buttonsPerRow = 3; + } + + // 计算每行宽度的百分比 + const widthPercent = Math.floor(100 / buttonsPerRow); + + // 应用样式到每个按钮 + buttons.forEach((button, index) => { + // 计算当前按钮是当前行的第几个按钮 + const positionInRow = index % buttonsPerRow; + + // 设置按钮宽度 + button.style.flexBasis = `calc(${widthPercent}% - 8px)`; + + // 每行最后一个按钮需处理外边距 + if (positionInRow === buttonsPerRow - 1) { + button.style.marginRight = '0'; + } + + // 调整文字大小以适应按钮 + adjustTextFontSize(button); + }); +} + +// 创建模式选择器容器 +export function createModeContainer(labelText) { + const modeContainer = document.createElement('div'); + modeContainer.className = 'layout-mode-container'; + modeContainer.style.display = 'flex'; + modeContainer.style.flexDirection = 'column'; + modeContainer.style.width = '100%'; + modeContainer.style.marginTop = '10px'; + modeContainer.style.gap = '8px'; + + const modeLabel = document.createElement('div'); + modeLabel.className = 'layout-mode-label'; + modeLabel.textContent = labelText; + modeLabel.style.fontSize = '14px'; + + modeContainer.appendChild(modeLabel); + + return { modeContainer, modeLabel }; +} + +// 创建下拉选择框 +export function createSelectBox() { + const selectBox = document.createElement('select'); + selectBox.className = 'layout-select'; + selectBox.style.width = '100%'; + selectBox.style.padding = '6px 10px'; + selectBox.style.borderRadius = '6px'; + selectBox.style.outline = 'none'; + + return selectBox; +} + +// ========================================================== +// 添加选项到选择框 +// ========================================================== + +export function addOptionsToSelect(selectBox, options) { + options.forEach(option => { + const optionElement = document.createElement('option'); + optionElement.value = option.value; + optionElement.textContent = option.text; + selectBox.appendChild(optionElement); + }); + + return selectBox; +} + +// ========================================================== +// 透明度管理系统 - 集中管理所有透明度相关功能 +// ========================================================== + +// 默认透明度值 +const DEFAULT_OPACITY = { + panel: 0.85, // 面板背景默认透明度 (85%) + buttons: 0.90 // 按钮默认透明度 (90%) +}; + +// 当前透明度设置 +let currentOpacity = { + panel: DEFAULT_OPACITY.panel, + buttons: DEFAULT_OPACITY.buttons +}; + +// 设置面板背景透明度 +export function setPanelBackgroundOpacity(container, opacity) { + if (!container) return; + + // 将百分比值转换为0-1范围的小数 + const opacityValue = Math.max(10, Math.min(100, Number(opacity))) / 100; + currentOpacity.panel = opacityValue; + + // 存储透明度值到数据属性,方便主题系统读取 + container.dataset.bgOpacity = String(opacityValue); + + // 设置背景容器的透明度 + const bgContainer = container.querySelector('.theme-svg-background'); + if (bgContainer) { + bgContainer.style.opacity = String(opacityValue); + } + + // 调整面板整体透明度 - 背景透明但内容清晰 + _updatePanelStyles(container); + + // 通知当前主题更新透明度设置 + _notifyThemeOpacityChange(container, opacityValue); + + return opacityValue; +} + +// 设置按钮透明度 +export function setButtonsOpacity(container, opacity) { + if (!container) return; + + // 将百分比值转换为0-1范围的小数 + const opacityValue = Math.max(10, Math.min(100, Number(opacity))) / 100; + currentOpacity.buttons = opacityValue; + + // 存储透明度值到数据属性 + container.dataset.btnOpacity = String(opacityValue); + + // 获取当前按钮背景颜色基础值(不含透明度) + const getCurrentButtonBaseColor = (button) => { + try { + const currentBg = window.getComputedStyle(button).backgroundColor; + // 从rgba中提取rgb部分 + if (currentBg.includes('rgba')) { + return currentBg.replace(/rgba\((.+?), [\d\.]+\)/, 'rgba($1, ' + opacityValue + ')'); + } + // 如果是rgb,转换为rgba + else if (currentBg.includes('rgb')) { + return currentBg.replace(/rgb\((.+?)\)/, 'rgba($1, ' + opacityValue + ')'); + } + return currentBg; + } catch (e) { + console.warn('获取按钮颜色失败:', e); + // 根据当前主题提供默认颜色 + if (currentTheme === '古神之眼') { + return 'rgba(106, 90, 205, ' + opacityValue + ')'; + } else { + return 'rgba(0, 180, 220, ' + opacityValue + ')'; + } + } + }; + + // 获取所有按钮并设置透明度 + const buttons = container.querySelectorAll('.layout-btn'); + if (buttons.length > 0) { + buttons.forEach(button => { + button.style.backgroundColor = getCurrentButtonBaseColor(button); + }); + } + + return opacityValue; +} + +// 保留setPanelOpacity函数,但将其实现为只调用背景透明度函数 +// 这样可以保持与原来代码的兼容性 +export function setPanelOpacity(container, opacity) { + // 调用背景透明度设置函数,不再同时设置按钮透明度 + return setPanelBackgroundOpacity(container, opacity); +} + +// 获取当前面板透明度设置 +export function getPanelOpacity() { + return { + panel: currentOpacity.panel * 100, // 转换为百分比形式 + buttons: currentOpacity.buttons * 100 + }; +} + +// 更新面板整体样式 +function _updatePanelStyles(container) { + if (!container) return; + + // 获取背景透明度 + const bgOpacity = parseFloat(container.dataset.bgOpacity || currentOpacity.panel); + + // 设置适当的背景模糊效果 - 透明度越高模糊越少 + const blurAmount = Math.round(14 - bgOpacity * 8); // 根据透明度计算模糊值 + container.style.backdropFilter = `blur(${blurAmount}px)`; + container.style.webkitBackdropFilter = `blur(${blurAmount}px)`; + + // 设置适当的边框透明度 - 透明度低时边框更明显 + const borderOpacity = Math.min(0.6, 0.8 - bgOpacity * 0.3); + const borderRGBA = container.classList.contains('theme-dark') + ? `rgba(0, 0, 0, ${borderOpacity})` + : `rgba(255, 255, 255, ${borderOpacity})`; + + // 只设置边框颜色,不修改边框宽度和样式 + if (container.style.border) { + container.style.border = container.style.border.replace(/rgba?\(.+?\)/, borderRGBA); + } +} + +// 通知当前主题透明度变化 +function _notifyThemeOpacityChange(container, opacity) { + if (!container) return; + + try { + // 获取当前主题ID + const themeId = currentTheme; + if (themeId) { + const theme = getTheme(themeId); + if (theme && typeof theme.setPanelOpacity === 'function') { + theme.setPanelOpacity(opacity); + } + } + } catch (e) { + console.warn('无法通过主题系统设置透明度:', e); + } +} + +// ========================================================== +// 硬币元素创建相关 +// ========================================================== + +// 创建硬币元素 +export function createCoinElement() { + const coinElement = document.createElement('div'); + coinElement.className = 'layout-coin-container'; + coinElement.style.width = '180px'; + coinElement.style.height = '180px'; + coinElement.style.margin = '5px auto 25px auto'; + coinElement.style.position = 'relative'; + coinElement.style.perspective = '1600px'; // 增强3D效果 + + return coinElement; +} + +// 翻转硬币动画 +export function flipCoin(coinElement) { + if (!coinElement) return; + + // 简单切换flipped类来触发动画 + coinElement.classList.toggle('flipped'); + + // 如果当前主题有自定义的翻转功能,则调用 + const currentTheme = getTheme(currentTheme); + if (currentTheme && typeof currentTheme.flipCoin === 'function') { + currentTheme.flipCoin(coinElement); + } +} + +// ========================================================== +// 面板位置计算函数 +// ========================================================== + +// 在鼠标位置显示面板 +export function showAtMousePosition(container, mousePosition) { + if (!container) return; + + // 修改导入路径 - 从相对路径调整为同目录导入 + try { + const { calculatePanelPosition } = require('./UIUtils.js'); + const position = calculatePanelPosition(mousePosition, container); + + container.style.left = `${position.left}px`; + container.style.top = `${position.top}px`; + container.style.transform = 'none'; // 不再使用 transform 进行定位 + } catch (e) { + // 如果导入失败,使用简单的居中逻辑 + const width = container.offsetWidth || 300; + const height = container.offsetHeight || 400; + + container.style.left = `${mousePosition.x - width/2}px`; + container.style.top = `${mousePosition.y - height/2}px`; + container.style.transform = 'none'; + } +} + +// ========================================================== +// 通知相关函数 +// ========================================================== + +// 显示通知信息 +export function showNotification(message, type = 'error') { + const notification = document.createElement('div'); + notification.className = 'layout-notification'; + notification.textContent = message; + + // 通知基本样式 - 保留,因为通知应该有一致的UI体验 + Object.assign(notification.style, { + position: 'fixed', + top: '24px', + left: '50%', + transform: 'translateX(-50%) translateY(-20px)', + 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' + }); + + 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); +} \ No newline at end of file diff --git a/js/Intelligent Layout/styles/UIUtils.js b/js/Intelligent Layout/styles/UIUtils.js new file mode 100644 index 0000000..6697142 --- /dev/null +++ b/js/Intelligent Layout/styles/UIUtils.js @@ -0,0 +1,214 @@ +// 智能布局 通用工具模块 + +// ========================================================== +// ComfyUI应用和节点相关函数 +// ========================================================== + +// 获取ComfyUI应用实例 +export function 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; +} + +// 获取选中的节点 +export function 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; +} + +// 获取选中的组 +export function 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; +} + +// ========================================================== +// 颜色生成和处理函数 +// ========================================================== + +// 生成随机颜色 +export function getRandomColor() { + const letters = '0123456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; +} + +// ========================================================== +// 节点排列和同步相关功能 +// ========================================================== + +// 判断节点排列主方向 +export function getMainDirection(centers) { + let dx = 0, dy = 0; + for (let i = 1; i < centers.length; i++) { + dx += Math.abs(centers[i].x - centers[0].x); + dy += Math.abs(centers[i].y - centers[0].y); + } + return dx > dy ? 'horizontal' : 'vertical'; +} + +// 计算节点中心位置 +export function getNodeCenters(nodes) { + return nodes.map(node => ({ + node, + x: node.pos ? node.pos[0] + (node.size ? node.size[0] / 2 : 0) : 0, + y: node.pos ? node.pos[1] + (node.size ? node.size[1] / 2 : 0) : 0 + })); +} + +// 同步节点大小 +export function syncNodeSize(nodes) { + let maxW = 0, maxH = 0; + nodes.forEach(node => { + if (node.size) { + maxW = Math.max(maxW, node.size[0]); + maxH = Math.max(maxH, node.size[1]); + } + }); + + nodes.forEach(node => { + if (node.size) { + node.size[0] = maxW; + node.size[1] = maxH; + } + }); + + return { width: maxW, height: maxH }; +} + +// 同步节点宽高(根据排列方向) +export function syncNodeWidthHeight(nodes) { + const centers = getNodeCenters(nodes); + const mainDir = getMainDirection(centers); + + if (mainDir === 'horizontal') { + // 横向排列,同步高度 + let maxH = 0; + nodes.forEach(node => { + if (node.size) maxH = Math.max(maxH, node.size[1]); + }); + nodes.forEach(node => { + if (node.size) node.size[1] = maxH; + }); + return { direction: 'horizontal', height: maxH }; + } else { + // 纵向排列,同步宽度 + let maxW = 0; + nodes.forEach(node => { + if (node.size) maxW = Math.max(maxW, node.size[0]); + }); + nodes.forEach(node => { + if (node.size) node.size[0] = maxW; + }); + return { direction: 'vertical', width: maxW }; + } +} + +// 对齐节点(根据排列方向) +export function alignNodes(nodes) { + const centers = getNodeCenters(nodes); + const mainDir = getMainDirection(centers); + + if (mainDir === 'horizontal') { + // 横向排列,对齐Y轴 + const avgY = centers.reduce((sum, c) => sum + c.y, 0) / centers.length; + nodes.forEach(node => { + if (node.pos) node.pos[1] = avgY - (node.size ? node.size[1] / 2 : 0); + }); + return { direction: 'horizontal', y: avgY }; + } else { + // 纵向排列,对齐X轴 + const avgX = centers.reduce((sum, c) => sum + c.x, 0) / centers.length; + nodes.forEach(node => { + if (node.pos) node.pos[0] = avgX - (node.size ? node.size[0] / 2 : 0); + }); + return { direction: 'vertical', x: avgX }; + } +} + +// ========================================================== +// 面板位置和大小计算 +// ========================================================== + +// 计算面板在鼠标位置的居中定位 +export function calculatePanelPosition(mousePosition, container) { + if (!container) return { left: 0, top: 0 }; + + const width = container.offsetWidth || 300; // 假设默认宽度为300px + const height = container.offsetHeight || 400; // 假设默认高度为400px + + // 确保面板完全在视口内 + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + let left = mousePosition.x - width/2; + let top = mousePosition.y - height/2; + + // 防止面板超出右边界 + if (left + width > viewportWidth) { + left = viewportWidth - width - 10; + } + + // 防止面板超出左边界 + if (left < 10) { + left = 10; + } + + // 防止面板超出下边界 + if (top + height > viewportHeight) { + top = viewportHeight - height - 10; + } + + // 防止面板超出上边界 + if (top < 10) { + top = 10; + } + + return { left, top }; +} diff --git a/js/Intelligent Layout/styles/古神之眼/AncientGod1.js b/js/Intelligent Layout/styles/古神之眼/AncientGod1.js new file mode 100644 index 0000000..06af5bd --- /dev/null +++ b/js/Intelligent Layout/styles/古神之眼/AncientGod1.js @@ -0,0 +1,468 @@ +/** + * AncientGod1.js - 古神之眼 紫色版 + * 这个文件将SVG内容转换为可在JavaScript中直接使用的格式 + * 使用方法:调用createAncientGod1Eye函数,传入目标DOM元素ID和可选的配置参数 + */ + +(function(global) { + // SVG内容作为字符串存储 + const svgContent = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +// 导出函数:将SVG插入到指定DOM元素 +global.createAncientGod1Eye = function(targetId, options = {}) { + const container = document.getElementById(targetId); + if (!container) return; + container.innerHTML = svgContent; + + // 获取SVG元素 + const svg = container.querySelector('svg'); + const eyeball = svg.querySelector('.eyeball'); + const iris = svg.querySelector('g[transform="translate(50, 50)"] > circle[r="8"]'); // 虹膜 + const sclera = svg.querySelector('g[transform="translate(50, 50)"] > circle[r="12.8"]'); // 眼白 + + // 存储原始瞳孔半径 + const pupil = eyeball.querySelector('circle'); + const originalPupilRadius = pupil ? pupil.getAttribute('r') : "3.5"; + + // 最大移动范围(像素) + const maxEyeMovement = 4; + const maxIrisMovement = 2.5; // 虹膜移动范围比瞳孔小一点 + + // 添加变量跟踪鼠标活动状态 + let mouseActive = false; + let mouseTimer = null; + + // 鼠标移动监听函数 + function handleMouseMove(event) { + mouseActive = true; + clearTimeout(mouseTimer); + + // 暂停原有动画 + const animateTransform = eyeball.querySelector('animateTransform'); + if (animateTransform) { + try { + // 完全暂停动画而非设置为indefinite + animateTransform.setAttribute('repeatCount', '0'); + animateTransform.setAttribute('dur', '999999s'); + } catch (e) { + console.error("无法暂停动画:", e); + } + } + + // 获取SVG的位置和尺寸 - 无论鼠标在哪都要获取 + const svgRect = svg.getBoundingClientRect(); + + // 计算鼠标相对于SVG中心的位置 + const centerX = svgRect.width / 2 + svgRect.left; + const centerY = svgRect.height / 2 + svgRect.top; + + // 计算偏移方向 + const deltaX = event.clientX - centerX; + const deltaY = event.clientY - centerY; + + // 计算目光注视方向 + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + const normalizedX = distance > 0 ? deltaX / distance : 0; + const normalizedY = distance > 0 ? deltaY / distance : 0; + + // 计算鼠标与中心的相对距离比例(用于瞳孔大小) + // 根据距离计算瞳孔大小 - 距离越近,瞳孔越大 + const maxDistance = Math.max(svgRect.width, svgRect.height) / 2; + const distanceRatio = 1 - Math.min(distance / maxDistance, 1); // 0到1之间 + + // 瞳孔设置 - 根据距离调整大小 + if (pupil) { + const minPupilSize = 3.0; + const maxPupilSize = 4.2; + const pupilSize = minPupilSize + distanceRatio * (maxPupilSize - minPupilSize); + pupil.setAttribute('r', pupilSize.toFixed(1)); + } + + // 设置眼球跟随,限制在合理范围内 + const eyeballMoveX = normalizedX * maxEyeMovement; + const eyeballMoveY = normalizedY * maxEyeMovement; + + // 设置虹膜跟随,幅度比瞳孔小一些 + const irisMoveX = normalizedX * maxIrisMovement; + const irisMoveY = normalizedY * maxIrisMovement; + + // 应用变换 - 眼瞳 + eyeball.style.transform = `translate(${eyeballMoveX}px, ${eyeballMoveY}px)`; + + // 应用变换 - 虹膜 + if (iris) { + iris.style.transform = `translate(${irisMoveX}px, ${irisMoveY}px)`; + } + + // 鼠标不活动定时器,3秒后恢复原动画 + mouseTimer = setTimeout(() => { + mouseActive = false; + resetEyeAnimations(); + }, 3000); + } + + // 重置眼睛所有动画 + function resetEyeAnimations() { + // 获取当前位置,用于平滑过渡 + const currentTransform = eyeball.style.transform; + let currentX = 0; + let currentY = 0; + + // 如果当前有transform,解析出当前位置 + if (currentTransform && currentTransform.includes('translate')) { + const match = currentTransform.match(/translate\(([^,]+)px,\s*([^)]+)px\)/); + if (match) { + currentX = parseFloat(match[1]); + currentY = parseFloat(match[2]); + } + } + + // 创建平滑返回中心的过渡动画 + if (currentX !== 0 || currentY !== 0) { + // 清除之前可能存在的过渡 + eyeball.style.transition = ''; + + // 先确保移除animateTransform的所有效果 + const animateTransform = eyeball.querySelector('animateTransform'); + if (animateTransform) { + // 完全禁用SVG动画,直到CSS过渡完成 + animateTransform.setAttribute('repeatCount', '0'); + animateTransform.setAttribute('dur', '999999s'); + animateTransform.setAttribute('values', '0,0; 0,0; 0,0'); + } + + // 在下一帧添加过渡效果 + requestAnimationFrame(() => { + eyeball.style.transition = 'transform 1s cubic-bezier(0.4, 0.0, 0.2, 1)'; + eyeball.style.transform = 'translate(0px, 0px)'; + + // 平滑过渡到中心后再恢复原始动画 + setTimeout(() => { + // 完全移除CSS transform和transition + eyeball.style.transition = ''; + eyeball.style.transform = ''; + + // 等待DOM更新完成 + setTimeout(() => { + // 确保没有残留的内联样式 + eyeball.removeAttribute('style'); + + // 重新启用SVG动画 + if (animateTransform) { + // 恢复原始动画内容 + animateTransform.setAttribute('values', '0,0; 2,-1; -1,-2; -2,1; 3,2; 1,3; -3,-1; -1,2; 2,2; 0,0'); + animateTransform.setAttribute('dur', '15s'); + animateTransform.setAttribute('repeatCount', 'indefinite'); + } + }, 50); + + // 同样对虹膜进行过渡处理 + if (iris) { + iris.style.transition = ''; + iris.style.transform = ''; + iris.removeAttribute('style'); + } + }, 1000); // 与过渡时间相匹配 + }); + } else { + // 如果已经在中心位置,直接恢复动画 + eyeball.style.removeProperty('transform'); + eyeball.removeAttribute('style'); + + const animateTransform = eyeball.querySelector('animateTransform'); + if (animateTransform) { + animateTransform.setAttribute('values', '0,0; 2,-1; -1,-2; -2,1; 3,2; 1,3; -3,-1; -1,2; 2,2; 0,0'); + animateTransform.setAttribute('dur', '15s'); + animateTransform.setAttribute('repeatCount', 'indefinite'); + } + } + + // 重置瞳孔大小为原始值,添加平滑过渡 + if (pupil) { + // 获取当前瞳孔大小 + const currentPupilSize = parseFloat(pupil.getAttribute('r')) || parseFloat(originalPupilRadius); + const targetPupilSize = parseFloat(originalPupilRadius); + + // 如果尺寸差异显著,使用动画过渡 + if (Math.abs(currentPupilSize - targetPupilSize) > 0.1) { + // 创建动画过渡 + const duration = 800; // 持续时间800毫秒 + const startTime = performance.now(); + const animate = (time) => { + const elapsedTime = time - startTime; + const progress = Math.min(elapsedTime / duration, 1); + // 使用缓动函数让动画更自然 + const easeProgress = 1 - Math.pow(1 - progress, 3); // 缓出效果 + + const currentSize = currentPupilSize + (targetPupilSize - currentPupilSize) * easeProgress; + pupil.setAttribute('r', currentSize.toFixed(2)); + + if (progress < 1) { + requestAnimationFrame(animate); + } + }; + + requestAnimationFrame(animate); + } else { + // 如果差异很小,直接设置 + pupil.setAttribute('r', originalPupilRadius); + } + } + + // 重置虹膜,也添加平滑过渡 + if (iris) { + const currentIrisTransform = iris.style.transform; + let currentIrisX = 0; + let currentIrisY = 0; + + if (currentIrisTransform && currentIrisTransform.includes('translate')) { + const match = currentIrisTransform.match(/translate\(([^,]+)px,\s*([^)]+)px\)/); + if (match) { + currentIrisX = parseFloat(match[1]); + currentIrisY = parseFloat(match[2]); + } + } + + if (currentIrisX !== 0 || currentIrisY !== 0) { + iris.style.transition = ''; + requestAnimationFrame(() => { + iris.style.transition = 'transform 1s cubic-bezier(0.4, 0.0, 0.2, 1)'; + iris.style.transform = 'translate(0px, 0px)'; + + setTimeout(() => { + iris.style.removeProperty('transform'); + }, 50); + }); + } else { + iris.style.transform = ''; + } + } + } + + // 添加鼠标离开监听 + function handleMouseLeave() { + clearTimeout(mouseTimer); + mouseTimer = setTimeout(() => { + mouseActive = false; + resetEyeAnimations(); + }, 1000); // 鼠标离开后1秒恢复原动画 + } + + // 添加事件监听器 - 改为监听整个文档的鼠标移动 + document.addEventListener('mousemove', handleMouseMove); + + // 返回清理函数 + return function cleanup() { + document.removeEventListener('mousemove', handleMouseMove); + }; +}; + +})(typeof window !== "undefined" ? window : this); \ No newline at end of file diff --git a/js/Intelligent Layout/styles/古神之眼/AncientGod2.js b/js/Intelligent Layout/styles/古神之眼/AncientGod2.js new file mode 100644 index 0000000..8dd9c24 --- /dev/null +++ b/js/Intelligent Layout/styles/古神之眼/AncientGod2.js @@ -0,0 +1,535 @@ +/** + * AncientGod2.js - 古神之眼 血色版 + * 这个文件将SVG内容转换为可在JavaScript中直接使用的格式 + * 使用方法:调用createAncientGod2Eye函数,传入目标DOM元素ID和可选的配置参数 + */ + +(function(global) { + // SVG内容作为字符串存储 + const svgContent = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +// 导出函数:将SVG插入到指定DOM元素 +global.createAncientGod2Eye = function(targetId, options = {}) { + const container = document.getElementById(targetId); + if (!container) return; + container.innerHTML = svgContent; + + // 获取SVG元素 + const svg = container.querySelector('svg'); + const eyeball = svg.querySelector('.eyeball'); + const iris = svg.querySelector('g[transform="translate(50, 50)"] > circle[r="8"]'); // 血色虹膜 + const sclera = svg.querySelector('g[transform="translate(50, 50)"] > circle[r="12.8"]'); // 眼白(暗红色) + + // 存储原始瞳孔半径 + const pupil = eyeball.querySelector('circle'); + const originalPupilRadius = pupil ? pupil.getAttribute('r') : "4"; + + // 最大移动范围(像素)- 血色版眼睛可以有更剧烈的反应 + const maxEyeMovement = 5; + const maxIrisMovement = 3; // 虹膜移动范围比瞳孔小一点,但血色版幅度更大 + + // 添加变量跟踪鼠标活动状态 + let mouseActive = false; + let mouseTimer = null; + let debugLog = false; // 调试模式 + + // 鼠标移动监听函数 + function handleMouseMove(event) { + mouseActive = true; + clearTimeout(mouseTimer); + + // 暂停原有动画 + const animateTransform = eyeball.querySelector('animateTransform'); + if (animateTransform) { + try { + // 完全暂停动画而非设置为indefinite + animateTransform.setAttribute('repeatCount', '0'); + animateTransform.setAttribute('dur', '999999s'); + } catch (e) { + console.error("无法暂停动画:", e); + } + } + + // 获取SVG的位置和尺寸 + const svgRect = svg.getBoundingClientRect(); + + // 计算鼠标相对于SVG中心的位置 + const centerX = svgRect.width / 2 + svgRect.left; + const centerY = svgRect.height / 2 + svgRect.top; + + // 计算偏移方向 + const deltaX = event.clientX - centerX; + const deltaY = event.clientY - centerY; + + // 计算眼睛注视方向 + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + const normalizedX = distance > 0 ? deltaX / distance : 0; + const normalizedY = distance > 0 ? deltaY / distance : 0; + + // 设置眼球跟随,限制在最大范围内 + const eyeballMoveX = normalizedX * maxEyeMovement; + const eyeballMoveY = normalizedY * maxEyeMovement; + + // 设置虹膜跟随,幅度比瞳孔小一些 + const irisMoveX = normalizedX * maxIrisMovement; + const irisMoveY = normalizedY * maxIrisMovement; + + // 应用变换 - 眼瞳 + eyeball.style.transform = `translate(${eyeballMoveX}px, ${eyeballMoveY}px)`; + + // 应用变换 - 虹膜 + if (iris) { + iris.style.transform = `translate(${irisMoveX}px, ${irisMoveY}px)`; + + // 血色版虹膜脉动效果 - 根据鼠标接近程度增强 + // 临时暂停原有动画 + const irisAnimateR = iris.querySelector('animate[attributeName="r"]'); + if (irisAnimateR) { + irisAnimateR.setAttribute('dur', 'indefinite'); + } + + const irisAnimateFill = iris.querySelector('animate[attributeName="fill"]'); + if (irisAnimateFill) { + irisAnimateFill.setAttribute('dur', 'indefinite'); + } + + // 根据距离调整虹膜大小和颜色 - 距离越近,虹膜越紧缩 + const maxDistance = Math.max(svgRect.width, svgRect.height) / 2; + const distanceRatio = Math.min(distance / maxDistance, 1); // 0到1之间 + + const minIrisSize = 7.4; // 接近时紧缩 + const maxIrisSize = 8.6; // 远离时放大 + const irisSize = minIrisSize + distanceRatio * (maxIrisSize - minIrisSize); + iris.setAttribute('r', irisSize.toFixed(1)); + + // 距离越近,血色越深 + const irisColor = distanceRatio < 0.3 ? '#6A0303' : // 非常近 - 深红色 + distanceRatio < 0.6 ? '#7A0404' : // 中等距离 + '#B80000'; // 远距离 - 鲜红色 + iris.setAttribute('fill', irisColor); + } + + // 添加血色瞳孔收缩效应 - 鼠标靠近时瞳孔扩大(血色版特性相反) + if (pupil) { + // 基于距离计算瞳孔大小 + const maxDistance = Math.max(svgRect.width, svgRect.height) / 2; + const distanceRatio = Math.min(distance / maxDistance, 1); // 0到1之间 + + // 血色版特殊效果:越近瞳孔越大(掠食特性),相反于普通眼睛 + // 显著增大变化范围,使效果更加明显 + const minPupilSize = 2.8; // 远时最小 + const maxPupilSize = 5.5; // 近时最大 + const pupilSize = maxPupilSize - distanceRatio * (maxPupilSize - minPupilSize); + + // 直接设置瞳孔大小,并确保应用生效 + pupil.setAttribute('r', pupilSize.toFixed(1)); + + // 调试日志 + if (debugLog) { + console.log(`距离比例: ${distanceRatio.toFixed(2)}, 瞳孔大小: ${pupilSize.toFixed(1)}`); + } + } + + // 设置鼠标不活动定时器,2.5秒后恢复原动画(血色版反应更快) + mouseTimer = setTimeout(() => { + mouseActive = false; + resetEyeAnimations(); + }, 2500); + } + + // 重置眼睛所有动画 + function resetEyeAnimations() { + // 获取当前位置,用于平滑过渡 + const currentTransform = eyeball.style.transform; + let currentX = 0; + let currentY = 0; + + // 如果当前有transform,解析出当前位置 + if (currentTransform && currentTransform.includes('translate')) { + const match = currentTransform.match(/translate\(([^,]+)px,\s*([^)]+)px\)/); + if (match) { + currentX = parseFloat(match[1]); + currentY = parseFloat(match[2]); + } + } + + // 创建平滑返回中心的过渡动画 - 血色版眼睛的过渡比秩序之眼更快速但仍然平滑 + if (currentX !== 0 || currentY !== 0) { + // 清除之前可能存在的过渡 + eyeball.style.transition = ''; + + // 先确保移除animateTransform的所有效果 + const animateTransform = eyeball.querySelector('animateTransform'); + if (animateTransform) { + // 完全禁用SVG动画,直到CSS过渡完成 + animateTransform.setAttribute('repeatCount', '0'); + animateTransform.setAttribute('dur', '999999s'); + animateTransform.setAttribute('values', '0,0; 0,0; 0,0'); + } + + // 在下一帧添加过渡效果 - 血色版使用更短的过渡时间 + requestAnimationFrame(() => { + eyeball.style.transition = 'transform 0.8s cubic-bezier(0.25, 0.1, 0.25, 1)'; + eyeball.style.transform = 'translate(0px, 0px)'; + + // 过渡结束后平滑恢复原始动画 + setTimeout(() => { + // 完全移除CSS transform和transition + eyeball.style.transition = ''; + eyeball.style.transform = ''; + + // 等待DOM更新完成 + setTimeout(() => { + // 确保没有残留的内联样式 + eyeball.removeAttribute('style'); + + // 重新启用SVG动画 + if (animateTransform) { + // 恢复原始动画内容 + animateTransform.setAttribute('values', '0,0; 2,-1; -1,-2; -2,1; 3,2; 1,3; -3,-1; -1,2; 2,2; 0,0'); + animateTransform.setAttribute('dur', '15s'); + animateTransform.setAttribute('repeatCount', 'indefinite'); + } + }, 50); + + // 同样对虹膜进行平滑过渡 + if (iris) { + iris.style.transition = ''; + iris.style.transform = ''; + iris.removeAttribute('style'); + } + }, 800); // 与过渡时间相匹配 + }); + } else { + // 如果已经在中心位置,直接恢复动画 + eyeball.style.removeProperty('transform'); + eyeball.removeAttribute('style'); + + const animateTransform = eyeball.querySelector('animateTransform'); + if (animateTransform) { + animateTransform.setAttribute('values', '0,0; 2,-1; -1,-2; -2,1; 3,2; 1,3; -3,-1; -1,2; 2,2; 0,0'); + animateTransform.setAttribute('dur', '15s'); + animateTransform.setAttribute('repeatCount', 'indefinite'); + } + } + + // 重置瞳孔大小为原始值,添加平滑过渡效果 + if (pupil) { + // 获取当前瞳孔大小 + const currentPupilSize = parseFloat(pupil.getAttribute('r')) || parseFloat(originalPupilRadius); + const targetPupilSize = parseFloat(originalPupilRadius); + + // 如果尺寸差异显著,使用动画过渡 + if (Math.abs(currentPupilSize - targetPupilSize) > 0.1) { + // 创建动画过渡 - 血色版动画更快 + const duration = 600; // 持续时间600毫秒 + const startTime = performance.now(); + const animate = (time) => { + const elapsedTime = time - startTime; + const progress = Math.min(elapsedTime / duration, 1); + // 使用弹性缓动让血色版更有特点 + const easeProgress = 1 - Math.pow(1 - progress, 2.5); // 略微弹性的缓出 + + const currentSize = currentPupilSize + (targetPupilSize - currentPupilSize) * easeProgress; + pupil.setAttribute('r', currentSize.toFixed(2)); + + if (progress < 1) { + requestAnimationFrame(animate); + } + }; + + requestAnimationFrame(animate); + } else { + // 如果差异很小,直接设置 + pupil.setAttribute('r', originalPupilRadius); + } + } + + // 重置虹膜,同样添加平滑过渡 + if (iris) { + // 对虹膜大小和颜色也添加过渡效果 + iris.style.transition = 'fill 0.8s ease'; + iris.removeAttribute('r'); // 让原有动画接管大小变化 + iris.removeAttribute('fill'); // 让原有动画接管颜色变化 + + // 处理虹膜的位置过渡 + const currentIrisTransform = iris.style.transform; + let currentIrisX = 0; + let currentIrisY = 0; + + if (currentIrisTransform && currentIrisTransform.includes('translate')) { + const match = currentIrisTransform.match(/translate\(([^,]+)px,\s*([^)]+)px\)/); + if (match) { + currentIrisX = parseFloat(match[1]); + currentIrisY = parseFloat(match[2]); + } + } + + if (currentIrisX !== 0 || currentIrisY !== 0) { + iris.style.transition = iris.style.transition + ', transform 0.8s cubic-bezier(0.25, 0.1, 0.25, 1)'; + requestAnimationFrame(() => { + iris.style.transform = 'translate(0px, 0px)'; + + setTimeout(() => { + iris.style.transition = ''; + iris.style.transform = ''; + + // 恢复虹膜动画 + const irisAnimateR = iris.querySelector('animate[attributeName="r"]'); + if (irisAnimateR) { + irisAnimateR.setAttribute('dur', '3s'); + } + + const irisAnimateFill = iris.querySelector('animate[attributeName="fill"]'); + if (irisAnimateFill) { + irisAnimateFill.setAttribute('dur', '5s'); + } + }, 800); + }); + } else { + iris.style.transform = ''; + + // 恢复虹膜动画 + const irisAnimateR = iris.querySelector('animate[attributeName="r"]'); + if (irisAnimateR) { + irisAnimateR.setAttribute('dur', '3s'); + } + + const irisAnimateFill = iris.querySelector('animate[attributeName="fill"]'); + if (irisAnimateFill) { + irisAnimateFill.setAttribute('dur', '5s'); + } + } + } + } + + // 添加事件监听器 - 改为监听整个文档的鼠标移动 + document.addEventListener('mousemove', handleMouseMove); + + // 返回清理函数 + return function cleanup() { + document.removeEventListener('mousemove', handleMouseMove); + // 不再需要移除mouseleave事件,因为它现在全局监视 + }; +}; + +})(typeof window !== "undefined" ? window : this); diff --git a/js/Intelligent Layout/styles/古神之眼/AncientGodTheme.js b/js/Intelligent Layout/styles/古神之眼/AncientGodTheme.js new file mode 100644 index 0000000..048d2d8 --- /dev/null +++ b/js/Intelligent Layout/styles/古神之眼/AncientGodTheme.js @@ -0,0 +1,842 @@ +/** + * AncientGodTheme.js - 古神之眼主题 + * 整合秩序和混沌两种风格的面板和硬币效果 + */ + +import './AncientGod1.js'; // 秩序主题硬币 +import './AncientGod2.js'; // 混沌主题硬币 +import { injectStyles, removeStyles } from '../UIStyles.js'; + +// 导入模块化的面板背景SVG +import { OrderPanelSVG } from './OrderPanelStyle.js'; +import { ChaosPanelSVG } from './ChaosPanelStyle.js'; + +// 秩序主题CSS +const OrderPanelCSS = ` + .layout-panel { + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(176, 141, 225, 0.3); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 8px rgba(138, 43, 226, 0.2); + } + + .layout-panel::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(176, 141, 225, 0.7), transparent); + } + + .ancient-god-coin-container { + perspective: 1600px; + width: 180px; + height: 180px; + margin: 5px auto 25px auto; + position: relative; + } + + .ancient-god-coin { + width: 100%; + height: 100%; + position: relative; + transform-style: preserve-3d; + transition: transform 0.9s cubic-bezier(.4,2,.3,1); + cursor: pointer; + } + + .ancient-god-coin.flipped { + transform: rotateY(180deg); + } + + .ancient-god-coin-face { + position: absolute; + width: 100%; + height: 100%; + backface-visibility: hidden; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); + } + + .ancient-god-coin-face.back { + transform: rotateY(180deg); + } + + .layout-btn { + background: rgba(106, 90, 205, 0.4); + border: 1px solid rgba(176, 141, 225, 0.5); + color: #e1e1e1; + } + + .layout-btn:hover { + background: rgba(106, 90, 205, 0.6); + box-shadow: 0 0 15px rgba(138, 43, 226, 0.5); + border: 1px solid rgba(176, 141, 225, 0.8); + color: white; + } + + /* 移除特殊按钮样式,统一所有按钮外观 */ + + select { + border: 1px solid rgba(176, 141, 225, 0.4); + background-color: rgba(42, 30, 74, 0.6); + } +`; + +// 混沌主题CSS +const ChaosPanelCSS = ` + .layout-panel { + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 0, 0, 0.3); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 10px rgba(184, 0, 0, 0.2); + } + + .layout-panel::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255, 0, 0, 0.7), transparent); + } + + .ancient-god-coin-container { + perspective: 1600px; + width: 180px; + height: 180px; + margin: 5px auto 25px auto; + position: relative; + } + + .ancient-god-coin { + width: 100%; + height: 100%; + position: relative; + transform-style: preserve-3d; + transition: transform 0.9s cubic-bezier(.4,2,.3,1); + cursor: pointer; + } + + .ancient-god-coin.flipped { + transform: rotateY(180deg); + } + + .ancient-god-coin-face { + position: absolute; + width: 100%; + height: 100%; + backface-visibility: hidden; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); + } + + .ancient-god-coin-face.back { + transform: rotateY(180deg); + } + + .layout-btn { + background: rgba(170, 0, 0, 0.4); + border: 1px solid rgba(255, 0, 0, 0.5); + color: #e1e1e1; + } + + .layout-btn:hover { + background: rgba(170, 0, 0, 0.6); + box-shadow: 0 0 15px rgba(255, 0, 0, 0.5); + border: 1px solid rgba(255, 0, 0, 0.8); + color: white; + } + + select { + border: 1px solid rgba(255, 0, 0, 0.4); + background-color: rgba(51, 0, 0, 0.6); + } +`; + +// 添加主题切换过渡动画CSS +const themeTransitionCSS = ` + .ancient-god-ripple-container { + pointer-events: none; + z-index: 10000; + } + + .theme-svg-background { + transition: opacity 0.6s ease; + } + + .layout-panel { + transition: border-color 0.8s ease, box-shadow 0.8s ease, opacity 0.5s ease !important; + } + + .layout-btn { + transition: background 0.5s ease, border-color 0.5s ease, box-shadow 0.5s ease; + } + + select { + transition: border-color 0.5s ease, background-color 0.5s ease; + } + + .theme-old-layer, .theme-new-layer { + backface-visibility: hidden; + transform: translateZ(0); + } + + .theme-new-layer { + clip-path: circle(0% at 50% 50%); + } +`; + +// 主题标识符 +const THEME_ID = { + PURPLE: 'purple', // 秩序主题 + BLOOD: 'blood' // 混沌主题 +}; + +let currentThemeId = THEME_ID.PURPLE; +let panelContainer = null; +let coinElement = null; +let coinFlipped = false; +let currentPanelOpacity = 0.8; // 默认透明度设置为80% + +// 主题SVG定义 - 使用导入的模块 +const themeSVGs = { + [THEME_ID.PURPLE]: OrderPanelSVG, + [THEME_ID.BLOOD]: ChaosPanelSVG +}; + +// 主题CSS样式 - 使用导入的模块 +const themeCSSTemplates = { + [THEME_ID.PURPLE]: OrderPanelCSS, + [THEME_ID.BLOOD]: ChaosPanelCSS +}; + +// 主题样式注入函数 +function injectThemeStyles() { + // 注入当前主题的CSS + removeStyles('ancient-god-theme-css'); + injectStyles('ancient-god-theme-css', themeCSSTemplates[currentThemeId]); + + // 注入过渡动画CSS + removeStyles('ancient-god-transition-css'); + injectStyles('ancient-god-transition-css', themeTransitionCSS); +} + +// 辅助函数:设置SVG元素样式,避免重复代码 +function applySVGStyles(svgElement) { + if (!svgElement) return; + + svgElement.style.width = '100%'; + svgElement.style.height = '100%'; + svgElement.style.display = 'block'; + svgElement.setAttribute('preserveAspectRatio', 'xMidYMid slice'); +} + +// 设置SVG背景 +function setSVGBackground(themeId = null) { + if (!panelContainer) return; + + // 如果未指定主题ID,则使用当前主题 + const targetThemeId = themeId || currentThemeId; + + // 创建或获取SVG背景容器 + let svgContainer = panelContainer.querySelector('.theme-svg-background'); + if (!svgContainer) { + svgContainer = document.createElement('div'); + svgContainer.className = 'theme-svg-background'; + svgContainer.style.position = 'absolute'; + svgContainer.style.top = '0'; + svgContainer.style.left = '0'; + svgContainer.style.width = '100%'; + svgContainer.style.height = '100%'; + svgContainer.style.zIndex = '-1'; + svgContainer.style.borderRadius = 'inherit'; + svgContainer.style.overflow = 'hidden'; + + // 确保SVG背景完全覆盖面板,包括边缘和内边距 + svgContainer.style.boxSizing = 'border-box'; + svgContainer.style.margin = '0'; + svgContainer.style.padding = '0'; + + panelContainer.insertBefore(svgContainer, panelContainer.firstChild); + } + + // 设置SVG内容 + svgContainer.innerHTML = themeSVGs[targetThemeId]; + + // 确保SVG元素本身也完全填充容器 + const svgElement = svgContainer.querySelector('svg'); + if (svgElement) { + applySVGStyles(svgElement); + } +} + +// 创建硬币元素 +function createCoinElement() { + // 创建容器 + const container = document.createElement('div'); + container.className = 'ancient-god-coin-container'; + + // 创建硬币元素 + const coin = document.createElement('div'); + coin.className = 'ancient-god-coin'; + + // 创建正面 (紫色) + const front = document.createElement('div'); + front.className = 'ancient-god-coin-face front'; + front.id = 'ancient-god-eye1'; + + // 创建反面 (血色) + const back = document.createElement('div'); + back.className = 'ancient-god-coin-face back'; + back.id = 'ancient-god-eye2'; + + // 组装硬币 + coin.appendChild(front); + coin.appendChild(back); + container.appendChild(coin); + + // 初始化硬币眼睛 + setTimeout(() => { + if (typeof window.createAncientGod1Eye === 'function') { + window.createAncientGod1Eye('ancient-god-eye1'); + } else { + console.warn('无法找到createAncientGod1Eye函数'); + } + if (typeof window.createAncientGod2Eye === 'function') { + window.createAncientGod2Eye('ancient-god-eye2'); + } else { + console.warn('无法找到createAncientGod2Eye函数'); + } + }, 100); + + return {container, coin}; +} + +// 设置面板透明度 - 修改为配合UIStyles.js的统一透明度控制 +export function setPanelOpacity(opacity) { + // 只保存当前透明度,实际的透明度设置由UIStyles.js处理 + currentPanelOpacity = Math.max(0.1, Math.min(1.0, Number(opacity))); + + // 更新数据属性,供UIStyles.js读取 + if (panelContainer) { + panelContainer.dataset.opacity = String(currentPanelOpacity); + } +} + +// 获取当前面板透明度 +export function getPanelOpacity() { + return currentPanelOpacity; +} + +// 从DOM元素或默认值恢复透明度设置 +function restorePanelOpacity() { + if (!panelContainer) return; + + const savedOpacity = panelContainer.dataset.opacity; + const opacity = savedOpacity ? Number(savedOpacity) : currentPanelOpacity; + setPanelOpacity(opacity); +} + +// 翻转硬币并切换主题 +function flipCoin() { + if (!coinElement || !panelContainer) return; + + // 保存当前的透明度设置,以便稍后恢复 + const savedBgOpacity = panelContainer.dataset.bgOpacity || currentPanelOpacity; + const savedBtnOpacity = panelContainer.dataset.btnOpacity || DEFAULT_OPACITY.buttons; + + // 根据目标状态确定硬币的翻转方向 + const targetState = !coinFlipped; + coinFlipped = targetState; // 直接设置为目标状态 + + // 获取当前和目标主题 + const fromTheme = !targetState ? THEME_ID.BLOOD : THEME_ID.PURPLE; + const toTheme = targetState ? THEME_ID.BLOOD : THEME_ID.PURPLE; + + // 更新硬币翻转状态 + if (coinFlipped) { + coinElement.classList.add('flipped'); + currentThemeId = THEME_ID.BLOOD; + } else { + coinElement.classList.remove('flipped'); + currentThemeId = THEME_ID.PURPLE; + } + + // 等待硬币翻转动画完成后再开始扩散效果 + setTimeout(() => { + // 准备新旧主题层 + prepareThemeLayers(fromTheme, toTheme); + + // 获取硬币元素在面板中的位置 + const coinRect = coinElement.getBoundingClientRect(); + const panelRect = panelContainer.getBoundingClientRect(); + + // 计算硬币中心相对于面板的位置 + const coinCenterX = (coinRect.left + coinRect.width / 2) - panelRect.left; + const coinCenterY = (coinRect.top + coinRect.height / 2) - panelRect.top; + + // 计算面板对角线长度 (作为最大扩散半径) + const maxRadius = Math.sqrt(Math.pow(panelRect.width, 2) + Math.pow(panelRect.height, 2)); + + // 创建扩散效果容器 - 仅用于视觉效果 + const rippleContainer = document.createElement('div'); + rippleContainer.className = 'ancient-god-ripple-container'; + rippleContainer.style.position = 'absolute'; + rippleContainer.style.top = '0'; + rippleContainer.style.left = '0'; + rippleContainer.style.width = '100%'; + rippleContainer.style.height = '100%'; + rippleContainer.style.pointerEvents = 'none'; + rippleContainer.style.zIndex = '10000'; + rippleContainer.style.borderRadius = 'inherit'; + rippleContainer.style.overflow = 'hidden'; + + // 创建发光扩散效果元素 - 纯视觉效果 + const glowRipple = document.createElement('div'); + glowRipple.className = 'ancient-god-glow-ripple'; + glowRipple.style.position = 'absolute'; + glowRipple.style.top = coinCenterY + 'px'; + glowRipple.style.left = coinCenterX + 'px'; + glowRipple.style.width = '0'; + glowRipple.style.height = '0'; + glowRipple.style.borderRadius = '50%'; + glowRipple.style.transform = 'translate(-50%, -50%)'; + glowRipple.style.pointerEvents = 'none'; + glowRipple.style.background = coinFlipped + ? 'radial-gradient(circle, rgba(255, 0, 0, 0.3) 0%, rgba(122, 0, 0, 0.1) 70%, transparent 100%)' + : 'radial-gradient(circle, rgba(176, 141, 225, 0.3) 0%, rgba(106, 90, 205, 0.1) 70%, transparent 100%)'; + glowRipple.style.boxShadow = coinFlipped + ? '0 0 20px 10px rgba(255, 0, 0, 0.3)' + : '0 0 20px 10px rgba(138, 43, 226, 0.3)'; + + // 添加CSS动画样式 + const rippleStyle = document.createElement('style'); + rippleStyle.textContent = ` + @keyframes ancient-god-glow-animation { + 0% { + width: 0; + height: 0; + opacity: 0.8; + } + 100% { + width: ${maxRadius * 2}px; + height: ${maxRadius * 2}px; + opacity: 0; + } + } + + @keyframes ancient-god-reveal-animation { + 0% { + clip-path: circle(0% at ${coinCenterX}px ${coinCenterY}px); + } + 100% { + clip-path: circle(${maxRadius}px at ${coinCenterX}px ${coinCenterY}px); + } + } + + .theme-transition-active { + animation: ancient-god-reveal-animation 2s ease-out forwards; + } + + .ancient-god-glow-ripple { + animation: ancient-god-glow-animation 2s ease-out forwards; + } + `; + document.head.appendChild(rippleStyle); + + // 添加扩散元素到容器 + rippleContainer.appendChild(glowRipple); + panelContainer.appendChild(rippleContainer); + // 启动新主题的显示动画 + const newThemeLayer = panelContainer.querySelector('.theme-new-layer'); + if (newThemeLayer) { + // 设置新主题背景的初始clip-path位置到硬币中心 + newThemeLayer.style.clipPath = `circle(0% at ${coinCenterX}px ${coinCenterY}px)`; + // 让CSS接管动画,而不是通过JS重设位置 + newThemeLayer.classList.add('theme-transition-active'); + // 保留新主题中的SVG动画,不要重新渲染 + const svgElement = newThemeLayer.querySelector('svg'); + if (svgElement) { + svgElement.setAttribute('data-preserve-animation', 'true'); + applySVGStyles(svgElement); + } + } + + // 注入当前主题的CSS以确保应用正确的样式 + injectThemeStyles(); + + // 动画结束后清理并恢复透明度 + setTimeout(() => { + // 移除临时元素 + rippleContainer.remove(); + rippleStyle.remove(); + + // 优化主题层,但保留动画状态 + cleanupThemeLayers(); + + // 恢复之前保存的透明度设置 + if (panelContainer) { + // 设置数据属性 + panelContainer.dataset.bgOpacity = savedBgOpacity; + panelContainer.dataset.btnOpacity = savedBtnOpacity; + + // 应用透明度 + const bgContainer = panelContainer.querySelector('.theme-svg-background'); + if (bgContainer) { + bgContainer.style.opacity = savedBgOpacity; + } + + // 通知主题系统透明度已更改 + _updatePanelStyles(panelContainer); + } + }, 2000); + }, 900); // 等待硬币翻转动画完成(0.9秒) +} + +// 准备主题过渡层 +function prepareThemeLayers(fromTheme, toTheme) { + if (!panelContainer) return; + + // 清除可能存在的旧层 + cleanupThemeLayers(); + + // 创建旧主题层 (底层) + const oldLayer = document.createElement('div'); + oldLayer.className = 'theme-old-layer'; + oldLayer.style.position = 'absolute'; + oldLayer.style.top = '0'; + oldLayer.style.left = '0'; + oldLayer.style.width = '100%'; + oldLayer.style.height = '100%'; + oldLayer.style.borderRadius = 'inherit'; + oldLayer.style.zIndex = '-2'; + oldLayer.style.overflow = 'hidden'; + + // 创建新主题层 (顶层) + const newLayer = document.createElement('div'); + newLayer.className = 'theme-new-layer'; + newLayer.style.position = 'absolute'; + newLayer.style.top = '0'; + newLayer.style.left = '0'; + newLayer.style.width = '100%'; + newLayer.style.height = '100%'; + newLayer.style.borderRadius = 'inherit'; + newLayer.style.zIndex = '-1'; + newLayer.style.overflow = 'hidden'; + + // 初始状态下,新主题层是隐藏的(通过clip-path) + newLayer.style.clipPath = 'circle(0% at 50% 50%)'; + + // 为新旧层设置不同的背景和样式 + // 旧主题背景 + const oldBg = document.createElement('div'); + oldBg.innerHTML = themeSVGs[fromTheme]; + oldBg.style.width = '100%'; + oldBg.style.height = '100%'; + oldLayer.appendChild(oldBg); + + // 确保SVG完全覆盖容器 + const oldSvg = oldBg.querySelector('svg'); + if (oldSvg) { + applySVGStyles(oldSvg); + } + + // 新主题背景 + const newBg = document.createElement('div'); + newBg.innerHTML = themeSVGs[toTheme]; + newBg.style.width = '100%'; + newBg.style.height = '100%'; + newLayer.appendChild(newBg); + + // 确保SVG完全覆盖容器 + const newSvg = newBg.querySelector('svg'); + if (newSvg) { + applySVGStyles(newSvg); + } + + // 添加主题样式 + applyThemeToLayer(oldLayer, fromTheme); + applyThemeToLayer(newLayer, toTheme); + + // 添加层到面板 + panelContainer.insertBefore(oldLayer, panelContainer.firstChild); + panelContainer.insertBefore(newLayer, panelContainer.firstChild); + + // 移除原来的SVG背景容器 + const oldSvgContainer = panelContainer.querySelector('.theme-svg-background'); + if (oldSvgContainer) { + oldSvgContainer.remove(); + } +} + +// 应用主题样式到层 +function applyThemeToLayer(layer, themeId) { + // 添加边框样式 + if (themeId === THEME_ID.PURPLE) { + layer.style.border = '1px solid rgba(176, 141, 225, 0.3)'; + layer.style.boxShadow = '0 8px 32px rgba(0, 0, 0, 0.3), 0 0 8px rgba(138, 43, 226, 0.2)'; + } else { + layer.style.border = '1px solid rgba(255, 0, 0, 0.3)'; + layer.style.boxShadow = '0 8px 32px rgba(0, 0, 0, 0.3), 0 0 10px rgba(184, 0, 0, 0.2)'; + } + + // 添加背景模糊效果 + layer.style.backdropFilter = 'blur(12px)'; + layer.style.webkitBackdropFilter = 'blur(12px)'; +} + +// 清理主题层 +function cleanupThemeLayers() { + if (!panelContainer) return; + + // 保存当前透明度设置 + const savedBgOpacity = panelContainer.dataset.bgOpacity || currentPanelOpacity; + + // 移除旧层,但保留新层作为当前背景 + const oldLayer = panelContainer.querySelector('.theme-old-layer'); + if (oldLayer) oldLayer.remove(); + + // 将新层转换为标准背景层 + const newLayer = panelContainer.querySelector('.theme-new-layer'); + if (newLayer) { + // 移除动画类并重设clip-path,保持全覆盖状态 + newLayer.classList.remove('theme-transition-active'); + newLayer.style.clipPath = 'none'; + newLayer.className = 'theme-svg-background'; + newLayer.style.zIndex = '-1'; + + // 保留SVG中的动画状态,不要重新设置内容 + const existingSvg = newLayer.querySelector('svg'); + if (existingSvg && existingSvg.getAttribute('data-preserve-animation') === 'true') { + applySVGStyles(existingSvg); + } + + // 恢复透明度设置 + newLayer.style.opacity = savedBgOpacity; + } else { + // 如果没有新层(异常情况),则创建标准背景 + setSVGBackground(); + + // 恢复透明度 + const bgContainer = panelContainer.querySelector('.theme-svg-background'); + if (bgContainer) { + bgContainer.style.opacity = savedBgOpacity; + } + } + + // 添加以下代码来更新按钮颜色 + setTimeout(() => { + if (panelContainer) { + const buttons = panelContainer.querySelectorAll('.layout-btn'); + const btnOpacity = parseFloat(panelContainer.dataset.btnOpacity || DEFAULT_OPACITY.buttons); + + buttons.forEach(button => { + // 根据当前主题设置按钮背景颜色 + const bgColor = currentThemeId === THEME_ID.PURPLE + ? `rgba(106, 90, 205, ${btnOpacity})` // 秩序主题 - 紫色 + : `rgba(170, 0, 0, ${btnOpacity})`; // 混沌主题 - 红色 + + button.style.backgroundColor = bgColor; + }); + } + }, 50); +} + +// 主题模块接口 +const AncientGodTheme = { + name: '古神之眼', + description: '一款融合秩序与混沌的主题,通过硬币翻转切换两种截然不同的视觉风格', + // 初始化主题 + init(container, existingCoinEl) { + panelContainer = container; + + // 确保每次初始化时硬币都处于正面状态 + coinFlipped = false; + currentThemeId = THEME_ID.PURPLE; + + // 创建或使用硬币元素 + if (existingCoinEl) { + // 完全清空现有容器并重建硬币 + existingCoinEl.innerHTML = ''; + + // 创建硬币元素 + const coin = document.createElement('div'); + coin.className = 'ancient-god-coin'; + // 确保移除任何可能存在的flipped类 + coin.classList.remove('flipped'); + + // 创建正面 (紫色) + const front = document.createElement('div'); + front.className = 'ancient-god-coin-face front'; + front.id = 'ancient-god-eye1'; + front.style.width = '100%'; + front.style.height = '100%'; + front.style.position = 'absolute'; + front.style.backfaceVisibility = 'hidden'; + front.style.borderRadius = '50%'; + + // 创建反面 (血色) + const back = document.createElement('div'); + back.className = 'ancient-god-coin-face back'; + back.id = 'ancient-god-eye2'; + back.style.width = '100%'; + back.style.height = '100%'; + back.style.position = 'absolute'; + back.style.backfaceVisibility = 'hidden'; + back.style.borderRadius = '50%'; + back.style.transform = 'rotateY(180deg)'; + + // 组装硬币 + coin.appendChild(front); + coin.appendChild(back); + existingCoinEl.appendChild(coin); + coinElement = coin; + + // 初始化硬币眼睛 + setTimeout(() => { + if (typeof window.createAncientGod1Eye === 'function') { + window.createAncientGod1Eye('ancient-god-eye1'); + } + if (typeof window.createAncientGod2Eye === 'function') { + window.createAncientGod2Eye('ancient-god-eye2'); + } + }, 100); + } else { + // 如果没有提供容器,则创建完整的硬币容器 + const coinContainer = document.createElement('div'); + coinContainer.className = 'ancient-god-coin-container'; + + // 创建硬币元素 + const coin = document.createElement('div'); + coin.className = 'ancient-god-coin'; + + // 创建正面 (紫色) + const front = document.createElement('div'); + front.className = 'ancient-god-coin-face front'; + front.id = 'ancient-god-eye1'; + + // 创建反面 (血色) + const back = document.createElement('div'); + back.className = 'ancient-god-coin-face back'; + back.id = 'ancient-god-eye2'; + + // 组装硬币 + coin.appendChild(front); + coin.appendChild(back); + coinContainer.appendChild(coin); + coinElement = coin; + + // 初始化硬币眼睛 + setTimeout(() => { + if (typeof window.createAncientGod1Eye === 'function') { + window.createAncientGod1Eye('ancient-god-eye1'); + } + if (typeof window.createAncientGod2Eye === 'function') { + window.createAncientGod2Eye('ancient-god-eye2'); + } + }, 100); + + // 添加到面板 + if (panelContainer.firstChild) { + panelContainer.insertBefore(coinContainer, panelContainer.firstChild); + } else { + panelContainer.appendChild(coinContainer); + } + } + + // 添加硬币点击事件 + coinElement.addEventListener('click', (e) => { + e.stopPropagation(); + this.flipCoin(); + }); + + return true; + }, + // 应用主题 + applyTheme() { + injectThemeStyles(); + + // 总是重新初始化背景,保证背景与硬币状态同步 + // 移除任何现有的背景层 + const existingBgs = panelContainer?.querySelectorAll('.theme-svg-background, .theme-new-layer, .theme-old-layer'); + if (existingBgs) { + existingBgs.forEach(bg => bg.remove()); + } + + // 设置初始状态的背景(与硬币的正面状态对应) + setSVGBackground(); + + // 添加更新按钮样式的过程 + setTimeout(() => { + // 根据当前主题ID更新按钮样式 + if (panelContainer) { + const buttons = panelContainer.querySelectorAll('.layout-btn'); + const btnOpacity = parseFloat(panelContainer.dataset.btnOpacity || DEFAULT_OPACITY.buttons); + + buttons.forEach(button => { + // 根据当前主题设置按钮背景颜色 + const bgColor = currentThemeId === THEME_ID.PURPLE + ? `rgba(106, 90, 205, ${btnOpacity})` // 秩序主题 - 紫色 + : `rgba(170, 0, 0, ${btnOpacity})`; // 混沌主题 - 红色 + + button.style.backgroundColor = bgColor; + }); + } + }, 100); + + return true; + }, + + // 翻转硬币 + flipCoin() { + flipCoin(); + return true; + }, + + // 设置面板透明度 + setPanelOpacity(opacity) { + setPanelOpacity(opacity); + return true; + }, + + // 获取面板透明度 + getPanelOpacity() { + return currentPanelOpacity; + }, + + // 获取当前主题样式ID + getCurrentThemeId() { + return currentThemeId; + }, + // 清理主题资源 + cleanup() { + removeStyles('ancient-god-theme-css'); + removeStyles('ancient-god-transition-css'); + if (panelContainer) { + // 移除所有类型的背景层 + const backgrounds = panelContainer.querySelectorAll('.theme-svg-background, .theme-old-layer, .theme-new-layer'); + backgrounds.forEach(bg => bg.remove()); + + // 移除可能存在的扩散效果容器 + const rippleContainer = panelContainer.querySelector('.ancient-god-ripple-container'); + if (rippleContainer) { + rippleContainer.remove(); + } + } + return true; + } +}; + +export default AncientGodTheme; diff --git a/js/Intelligent Layout/styles/古神之眼/ChaosPanelStyle.js b/js/Intelligent Layout/styles/古神之眼/ChaosPanelStyle.js new file mode 100644 index 0000000..0cca3d5 --- /dev/null +++ b/js/Intelligent Layout/styles/古神之眼/ChaosPanelStyle.js @@ -0,0 +1,70 @@ +/** + * ChaosPanelStyle.js - 古神-混沌-面板 + * 混沌风格面板的SVG背景和CSS样式 - 带有怪奇网络动画效果 + */ + +// 混沌主题面板背景SVG - 带丝线连接的电脑屏幕和神秘中心 +export const ChaosPanelSVG = ` + +`; \ No newline at end of file diff --git a/js/Intelligent Layout/styles/古神之眼/OrderPanelStyle.js b/js/Intelligent Layout/styles/古神之眼/OrderPanelStyle.js new file mode 100644 index 0000000..ea73397 --- /dev/null +++ b/js/Intelligent Layout/styles/古神之眼/OrderPanelStyle.js @@ -0,0 +1,262 @@ +/** + * OrderPanelStyle.js - 古神-秩序-面板 + * 秩序风格面板的SVG背景和CSS样式 + */ + +// 秩序主题面板背景SVG - 星球有序旋转 +export const OrderPanelSVG = ` + +`; \ No newline at end of file