完善界面UI添加主题选项,修复BUG
This commit is contained in:
+6
-1
@@ -88,7 +88,9 @@ High-performance model loading optimizer with multiple optimization modes:
|
||||
<details>
|
||||
<summary>DD Interface Layout</summary>
|
||||
|
||||
- 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.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -90,7 +90,9 @@
|
||||
<details>
|
||||
<summary>DD 界面布局</summary>
|
||||
|
||||
-已实现Alpha版本,可以随机或指定节点标题的颜色,可以对杂乱工作流做快速的模块化分类整理
|
||||
- 已实现Bate版本,可以随机或指定节点标题的颜色,可以对杂乱节点做快速整理
|
||||
- 美化UI,添加主题系统,每一种主题有两种不同风格可以切换,支持自定义主题
|
||||
- 添加模块化预设,可以预设自己的布局,一键应用
|
||||
|
||||
</details>
|
||||
|
||||
@@ -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请及时反馈。
|
||||
|
||||
+246
-521
@@ -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: '古神之眼' // 默认主题
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
/**
|
||||
* AncientGod1.js - 古神之眼 紫色版
|
||||
* 这个文件将SVG内容转换为可在JavaScript中直接使用的格式
|
||||
* 使用方法:调用createAncientGod1Eye函数,传入目标DOM元素ID和可选的配置参数
|
||||
*/
|
||||
|
||||
(function(global) {
|
||||
// SVG内容作为字符串存储
|
||||
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet">
|
||||
<!-- 古神之眼SVG:星盘与齿轮 --> <defs>
|
||||
<linearGradient id="destinyGlow" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#9370DB" stop-opacity="1" />
|
||||
<stop offset="100%" stop-color="#483D8B" stop-opacity="1" />
|
||||
</linearGradient>
|
||||
|
||||
|
||||
<!-- 脉动模糊滤镜 -->
|
||||
<filter id="pulsatingBlur" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="1.2" result="blur">
|
||||
<animate attributeName="stdDeviation" values="1.2;1.8;1.2" dur="5s" repeatCount="indefinite" />
|
||||
</feGaussianBlur>
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
|
||||
<!-- 金属质感齿轮渐变 -->
|
||||
<linearGradient id="gearGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#8A6FD0" />
|
||||
<stop offset="30%" stop-color="#63438F" />
|
||||
<stop offset="70%" stop-color="#533178" />
|
||||
<stop offset="100%" stop-color="#422261" />
|
||||
</linearGradient>
|
||||
|
||||
|
||||
<!-- 齿轮发光边缘 -->
|
||||
<filter id="glowingEdge" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="0.8" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
|
||||
<!-- 齿轮阴影滤镜 -->
|
||||
<filter id="gearShadow">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="1" />
|
||||
<feOffset dx="0.5" dy="0.5" result="offsetblur" />
|
||||
<feFlood flood-color="#000000" flood-opacity="0.4" />
|
||||
<feComposite in2="offsetblur" operator="in" />
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
</defs>
|
||||
<!-- 背景元素组 - 添加旋转动画 -->
|
||||
<g id="background">
|
||||
<!-- 添加旋转动画 -->
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 50 50"
|
||||
to="360 50 50"
|
||||
dur="60s"
|
||||
repeatCount="indefinite" />
|
||||
|
||||
<!-- 背景圆形 -->
|
||||
<circle cx="50" cy="50" r="45" fill="url(#destinyGlow)" stroke="#B08DE1" stroke-width="1.5" />
|
||||
|
||||
<!-- 克苏鲁风格脉动辉光 -->
|
||||
<circle cx="50" cy="50" r="42" fill="none" stroke="#6A5ACD" stroke-width="0.5" opacity="0.3" filter="url(#pulsatingBlur)">
|
||||
<animate attributeName="r" values="42;43;42" dur="8s" repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="0.3;0.5;0.3" dur="8s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="50" cy="50" r="38" fill="#341E6A" opacity="0.4">
|
||||
<animate attributeName="r" values="38;40;38" dur="10s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<!-- 星盘元素 -->
|
||||
<circle cx="50" cy="50" r="35" fill="none" stroke="#FFFFFF" stroke-width="0.5" opacity="0.7" />
|
||||
<circle cx="50" cy="50" r="25" fill="none" stroke="#FFFFFF" stroke-width="0.5" opacity="0.8" />
|
||||
|
||||
<!-- 神秘符文 -->
|
||||
<g opacity="0.6">
|
||||
<!-- 符文1 - 外圈 -->
|
||||
<path d="M50,15 L55,15 L53,18 Z" fill="none" stroke="#FFFFFF" stroke-width="0.3" />
|
||||
<path d="M85,50 L85,55 L82,53 Z" fill="none" stroke="#FFFFFF" stroke-width="0.3" />
|
||||
<path d="M50,85 L45,85 L47,82 Z" fill="none" stroke="#FFFFFF" stroke-width="0.3" />
|
||||
<path d="M15,50 L15,45 L18,47 Z" fill="none" stroke="#FFFFFF" stroke-width="0.3" />
|
||||
|
||||
<!-- 符文2 - 内圈不规则符号 -->
|
||||
<path d="M40,25 L45,27 L43,31 L38,29 Z" fill="none" stroke="#FFFFFF" stroke-width="0.3" />
|
||||
<path d="M75,40 L73,45 L69,43 L71,38 Z" fill="none" stroke="#FFFFFF" stroke-width="0.3" />
|
||||
<path d="M60,75 L55,73 L57,69 L62,71 Z" fill="none" stroke="#FFFFFF" stroke-width="0.3" />
|
||||
<path d="M25,60 L27,55 L31,57 L29,62 Z" fill="none" stroke="#FFFFFF" stroke-width="0.3" />
|
||||
</g>
|
||||
</g>
|
||||
<!-- 主齿轮与克苏鲁之眼 - 行星中心 -->
|
||||
<g transform="translate(50, 50)">
|
||||
|
||||
<!-- 齿轮轮廓 - 增强质感 -->
|
||||
<path d="M0,-20 L5,-18 L7,-23 L12,-22 L10,-16 L15,-14 L18,-19 L22,-16 L20,-11 L25,-8 L28,-12 L30,-8 L26,-4 L30,0 L26,4 L30,8 L28,12 L25,8 L20,11 L22,16 L18,19 L15,14 L10,16 L12,22 L7,23 L5,18 L0,20 L-5,18 L-7,23 L-12,22 L-10,16 L-15,14 L-18,19 L-22,16 L-20,11 L-25,8 L-28,12 L-30,8 L-26,4 L-30,0 L-26,-4 L-30,-8 L-28,-12 L-25,-8 L-20,-11 L-22,-16 L-18,-19 L-15,-14 L-10,-16 L-12,-22 L-7,-23 L-5,-18 Z"
|
||||
fill="url(#gearGradient)" stroke="#DDC6FF" stroke-width="1" filter="url(#gearShadow)">
|
||||
<!-- 生物机械感 - 微变形 -->
|
||||
<animate attributeName="d"
|
||||
values="M0,-20 L5,-18 L7,-23 L12,-22 L10,-16 L15,-14 L18,-19 L22,-16 L20,-11 L25,-8 L28,-12 L30,-8 L26,-4 L30,0 L26,4 L30,8 L28,12 L25,8 L20,11 L22,16 L18,19 L15,14 L10,16 L12,22 L7,23 L5,18 L0,20 L-5,18 L-7,23 L-12,22 L-10,16 L-15,14 L-18,19 L-22,16 L-20,11 L-25,8 L-28,12 L-30,8 L-26,4 L-30,0 L-26,-4 L-30,-8 L-28,-12 L-25,-8 L-20,-11 L-22,-16 L-18,-19 L-15,-14 L-10,-16 L-12,-22 L-7,-23 L-5,-18 Z;
|
||||
M0,-19 L5,-17 L8,-23 L13,-22 L11,-15 L16,-14 L19,-20 L22,-15 L19,-10 L25,-7 L29,-12 L31,-7 L27,-4 L31,0 L27,4 L31,8 L29,13 L25,7 L19,12 L22,17 L18,20 L16,13 L11,16 L13,23 L8,24 L5,17 L0,21 L-5,17 L-7,23 L-12,22 L-10,16 L-15,14 L-18,19 L-22,16 L-20,11 L-25,8 L-28,12 L-30,8 L-26,4 L-30,0 L-26,-4 L-30,-7 L-29,-12 L-25,-7 L-19,-10 L-22,-15 L-19,-20 L-16,-14 L-11,-15 L-13,-22 L-8,-23 L-5,-17 Z;
|
||||
M0,-20 L5,-18 L7,-23 L12,-22 L10,-16 L15,-14 L18,-19 L22,-16 L20,-11 L25,-8 L28,-12 L30,-8 L26,-4 L30,0 L26,4 L30,8 L28,12 L25,8 L20,11 L22,16 L18,19 L15,14 L10,16 L12,22 L7,23 L5,18 L0,20 L-5,18 L-7,23 L-12,22 L-10,16 L-15,14 L-18,19 L-22,16 L-20,11 L-25,8 L-28,12 L-30,8 L-26,4 L-30,0 L-26,-4 L-30,-8 L-28,-12 L-25,-8 L-20,-11 L-22,-16 L-18,-19 L-15,-14 L-10,-16 L-12,-22 L-7,-23 L-5,-18 Z"
|
||||
dur="20s"
|
||||
repeatCount="indefinite"
|
||||
calcMode="spline"
|
||||
keySplines="0.42 0 0.58 1; 0.42 0 0.58 1" />
|
||||
</path>
|
||||
|
||||
<!-- 发光齿轮边缘 -->
|
||||
<path d="M0,-20 L5,-18 L7,-23 L12,-22 L10,-16 L15,-14 L18,-19 L22,-16 L20,-11 L25,-8 L28,-12 L30,-8 L26,-4 L30,0 L26,4 L30,8 L28,12 L25,8 L20,11 L22,16 L18,19 L15,14 L10,16 L12,22 L7,23 L5,18 L0,20 L-5,18 L-7,23 L-12,22 L-10,16 L-15,14 L-18,19 L-22,16 L-20,11 L-25,8 L-28,12 L-30,8 L-26,4 L-30,0 L-26,-4 L-30,-8 L-28,-12 L-25,-8 L-20,-11 L-22,-16 L-18,-19 L-15,-14 L-10,-16 L-12,-22 L-7,-23 L-5,-18 Z"
|
||||
fill="none" stroke="#DDC6FF" stroke-width="0.3" opacity="0.6" filter="url(#glowingEdge)">
|
||||
<animate attributeName="opacity" values="0.6;0.2;0.6" dur="8s" repeatCount="indefinite" />
|
||||
</path>
|
||||
|
||||
<!-- 微血管效果 -->
|
||||
<g stroke="#AA3377" stroke-width="0.1" opacity="0.3">
|
||||
<path d="M-15,-10 Q-10,-5 -15,0 Q-10,5 -5,0" />
|
||||
<path d="M5,-15 Q10,-10 15,-15 Q10,-5 15,0" />
|
||||
<path d="M-5,15 Q0,10 5,15 Q0,5 -5,5" />
|
||||
<path d="M-10,-5 Q-5,0 0,-5 Q5,0 10,-5" />
|
||||
<path d="M-8,8 Q-4,4 0,8 Q4,4 8,8" />
|
||||
|
||||
<!-- 脉动动画 -->
|
||||
<animate attributeName="opacity" values="0.3;0.1;0.3" dur="10s" repeatCount="indefinite" />
|
||||
</g>
|
||||
|
||||
<!-- 眼白(取代内圆) - 保持克苏鲁眼睛特性 -->
|
||||
<circle cx="0" cy="0" r="12.8" fill="#2A1E4A" stroke="#B08DE1"/>
|
||||
|
||||
<!-- 眼球内部花纹 -->
|
||||
<path d="M-9,0 A9,12 0 0,1 9,0 A9,12 0 0,1 -9,0"
|
||||
fill="none" stroke="#8A2BE2" stroke-width="0.3" opacity="0.7" />
|
||||
<path d="M0,-9 A12,9 0 0,1 0,9 A12,9 0 0,1 0,-9"
|
||||
fill="none" stroke="#8A2BE2" stroke-width="0.3" opacity="0.7" />
|
||||
|
||||
<!-- 虹膜 -->
|
||||
<circle cx="0" cy="0" r="8" fill="#341E6A" stroke="#9370DB" stroke-width="0.3">
|
||||
<!-- 虹膜脉动动画 -->
|
||||
<animate attributeName="r" values="7.8;8.2;7.8" dur="3s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
|
||||
<!-- 眼瞳(不规则移动)-->
|
||||
<g class="eyeball">
|
||||
<!-- 复合动画:眼瞳不规则移动 -->
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="translate"
|
||||
values="0,0; 2,-1; -1,-2; -2,1; 3,2; 1,3; -3,-1; -1,2; 2,2; 0,0"
|
||||
keyTimes="0;0.11;0.22;0.33;0.44;0.55;0.66;0.77;0.88;1"
|
||||
dur="15s"
|
||||
repeatCount="indefinite"
|
||||
calcMode="spline"
|
||||
keySplines="0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1" />
|
||||
<!-- 瞳孔 -->
|
||||
<circle cx="0" cy="0" r="3.5" fill="#090418" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="stars">
|
||||
<!-- 添加与背景相同的旋转动画 -->
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 50 50"
|
||||
to="360 50 50"
|
||||
dur="60s"
|
||||
repeatCount="indefinite" />
|
||||
<!-- 明亮星星 -->
|
||||
<g fill="#FFFFFF">
|
||||
<circle cx="15" cy="35" r="0.6" opacity="0.8" filter="url(#glowingEdge)">
|
||||
<animate attributeName="opacity" values="0.8;0.3;0.8" dur="3s" repeatCount="indefinite" />
|
||||
<animate attributeName="r" values="0.6;0.8;0.6" dur="6s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="85" cy="65" r="0.7" opacity="0.8" filter="url(#glowingEdge)">
|
||||
<animate attributeName="opacity" values="0.8;0.3;0.8" dur="4s" repeatCount="indefinite" />
|
||||
<animate attributeName="r" values="0.7;0.9;0.7" dur="7s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="40" cy="15" r="0.5" opacity="0.8" filter="url(#glowingEdge)">
|
||||
<animate attributeName="opacity" values="0.8;0.3;0.8" dur="5s" repeatCount="indefinite" />
|
||||
<animate attributeName="r" values="0.5;0.7;0.5" dur="5s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="60" cy="85" r="0.6" opacity="0.8" filter="url(#glowingEdge)">
|
||||
<animate attributeName="opacity" values="0.8;0.3;0.8" dur="4.5s" repeatCount="indefinite" />
|
||||
<animate attributeName="r" values="0.6;0.8;0.6" dur="8s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
</g>
|
||||
|
||||
<!-- 装饰星星 - 额外增加 -->
|
||||
<g fill="#FFFFFF">
|
||||
<circle cx="20" cy="20" r="0.4" opacity="0.6">
|
||||
<animate attributeName="opacity" values="0.6;0.2;0.6" dur="6s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="80" cy="20" r="0.3" opacity="0.5">
|
||||
<animate attributeName="opacity" values="0.5;0.15;0.5" dur="7s" repeatCount="indefinite" begin="1s" />
|
||||
</circle>
|
||||
<circle cx="20" cy="80" r="0.35" opacity="0.55">
|
||||
<animate attributeName="opacity" values="0.55;0.2;0.55" dur="9s" repeatCount="indefinite" begin="3s" />
|
||||
</circle>
|
||||
<circle cx="80" cy="80" r="0.25" opacity="0.45">
|
||||
<animate attributeName="opacity" values="0.45;0.1;0.45" dur="8s" repeatCount="indefinite" begin="4s" />
|
||||
</circle>
|
||||
<circle cx="50" cy="15" r="0.3" opacity="0.5">
|
||||
<animate attributeName="opacity" values="0.5;0.15;0.5" dur="6s" repeatCount="indefinite" begin="2s" />
|
||||
</circle>
|
||||
<circle cx="50" cy="85" r="0.35" opacity="0.55">
|
||||
<animate attributeName="opacity" values="0.55;0.2;0.55" dur="7s" repeatCount="indefinite" begin="1s" />
|
||||
</circle>
|
||||
<circle cx="15" cy="50" r="0.25" opacity="0.45">
|
||||
<animate attributeName="opacity" values="0.45;0.1;0.45" dur="8s" repeatCount="indefinite" begin="3s" />
|
||||
</circle>
|
||||
<circle cx="85" cy="50" r="0.3" opacity="0.5">
|
||||
<animate attributeName="opacity" values="0.5;0.15;0.5" dur="9s" repeatCount="indefinite" begin="2s" />
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
// 导出函数:将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);
|
||||
@@ -0,0 +1,535 @@
|
||||
/**
|
||||
* AncientGod2.js - 古神之眼 血色版
|
||||
* 这个文件将SVG内容转换为可在JavaScript中直接使用的格式
|
||||
* 使用方法:调用createAncientGod2Eye函数,传入目标DOM元素ID和可选的配置参数
|
||||
*/
|
||||
|
||||
(function(global) {
|
||||
// SVG内容作为字符串存储
|
||||
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet">
|
||||
<!-- 古神之眼SVG:血色版 -->
|
||||
<defs>
|
||||
<!-- 诡异血色主辉光渐变 -->
|
||||
<linearGradient id="bloodGlow" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FF2D2D" stop-opacity="1" />
|
||||
<stop offset="100%" stop-color="#7A0404" stop-opacity="1" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 脉动血色滤镜 -->
|
||||
<filter id="bloodPulsatingBlur" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="1.2" result="blur">
|
||||
<animate attributeName="stdDeviation" values="1.2;1.8;1.2" dur="5s" repeatCount="indefinite" />
|
||||
</feGaussianBlur>
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
|
||||
<!-- 血色金属齿轮渐变 -->
|
||||
<linearGradient id="bloodGearGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FF4B4B" />
|
||||
<stop offset="30%" stop-color="#B80000" />
|
||||
<stop offset="70%" stop-color="#7A0404" />
|
||||
<stop offset="100%" stop-color="#3A0000" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 齿轮血色发光边缘 -->
|
||||
<filter id="bloodGlowingEdge" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="0.8" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
|
||||
<!-- 齿轮阴影滤镜 - 更深血色 -->
|
||||
<filter id="bloodGearShadow">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="1" />
|
||||
<feOffset dx="0.5" dy="0.5" result="offsetblur" />
|
||||
<feFlood flood-color="#7A0404" flood-opacity="0.6" />
|
||||
<feComposite in2="offsetblur" operator="in" />
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<!-- 血滴效果滤镜 -->
|
||||
<filter id="bloodDropEffect" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.05" numOctaves="2" result="noise" />
|
||||
<feDisplacementMap in="SourceGraphic" in2="noise" scale="3" xChannelSelector="R" yChannelSelector="G" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 背景元素组 - 血色旋转动画 -->
|
||||
<g id="background">
|
||||
<!-- 血色背景旋转 -->
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 50 50"
|
||||
to="360 50 50"
|
||||
dur="120s"
|
||||
repeatCount="indefinite" />
|
||||
|
||||
<!-- 血红色背景圆形 -->
|
||||
<circle cx="50" cy="50" r="45" fill="url(#bloodGlow)" stroke="#FF0000" stroke-width="1.5" />
|
||||
|
||||
<!-- 诡异血色脉动辉光 -->
|
||||
<circle cx="50" cy="50" r="42" fill="none" stroke="#FF2D2D" stroke-width="0.5" opacity="0.4" filter="url(#bloodPulsatingBlur)">
|
||||
<animate attributeName="r" values="42;43;42" dur="8s" repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="0.4;0.6;0.4" dur="8s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
|
||||
<!-- 暗红色内圈 -->
|
||||
<circle cx="50" cy="50" r="38" fill="#4A0000" opacity="0.6">
|
||||
<animate attributeName="r" values="38;40;38" dur="10s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
|
||||
<!-- 血丝纹路 - 不规则血管效果 -->
|
||||
<g stroke="#FF0000" stroke-width="0.3" opacity="0.7" fill="none">
|
||||
<path d="M20,20 Q30,40 50,30 T80,20" filter="url(#bloodDropEffect)">
|
||||
<animate attributeName="d" values="M20,20 Q30,40 50,30 T80,20;M22,22 Q35,45 55,33 T78,18;M20,20 Q30,40 50,30 T80,20" dur="15s" repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="0.7;0.5;0.7" dur="8s" repeatCount="indefinite" />
|
||||
</path>
|
||||
<path d="M25,75 Q40,60 60,70 T85,75" filter="url(#bloodDropEffect)">
|
||||
<animate attributeName="d" values="M25,75 Q40,60 60,70 T85,75;M28,72 Q45,57 62,73 T82,76;M25,75 Q40,60 60,70 T85,75" dur="17s" repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="0.6;0.4;0.6" dur="9s" repeatCount="indefinite" />
|
||||
</path>
|
||||
<path d="M15,50 Q30,40 50,50 T85,50" filter="url(#bloodDropEffect)">
|
||||
<animate attributeName="d" values="M15,50 Q30,40 50,50 T85,50;M17,48 Q32,35 52,54 T83,52;M15,50 Q30,40 50,50 T85,50" dur="16s" repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="0.7;0.5;0.7" dur="7s" repeatCount="indefinite" />
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- 主齿轮与克苏鲁之眼 - 行星中心 -->
|
||||
<g transform="translate(50, 50)">
|
||||
<!-- 齿轮轮廓 - 增强血色质感 -->
|
||||
<path d="M0,-20 L5,-18 L7,-23 L12,-22 L10,-16 L15,-14 L18,-19 L22,-16 L20,-11 L25,-8 L28,-12 L30,-8 L26,-4 L30,0 L26,4 L30,8 L28,12 L25,8 L20,11 L22,16 L18,19 L15,14 L10,16 L12,22 L7,23 L5,18 L0,20 L-5,18 L-7,23 L-12,22 L-10,16 L-15,14 L-18,19 L-22,16 L-20,11 L-25,8 L-28,12 L-30,8 L-26,4 L-30,0 L-26,-4 L-30,-8 L-28,-12 L-25,-8 L-20,-11 L-22,-16 L-18,-19 L-15,-14 L-10,-16 L-12,-22 L-7,-23 L-5,-18 Z"
|
||||
fill="url(#bloodGearGradient)" stroke="#FF9999" stroke-width="1" filter="url(#bloodGearShadow)">
|
||||
<!-- 生物机械感 - 血肉蠕动效果 -->
|
||||
<animate attributeName="d"
|
||||
values="M0,-20 L5,-18 L7,-23 L12,-22 L10,-16 L15,-14 L18,-19 L22,-16 L20,-11 L25,-8 L28,-12 L30,-8 L26,-4 L30,0 L26,4 L30,8 L28,12 L25,8 L20,11 L22,16 L18,19 L15,14 L10,16 L12,22 L7,23 L5,18 L0,20 L-5,18 L-7,23 L-12,22 L-10,16 L-15,14 L-18,19 L-22,16 L-20,11 L-25,8 L-28,12 L-30,8 L-26,4 L-30,0 L-26,-4 L-30,-8 L-28,-12 L-25,-8 L-20,-11 L-22,-16 L-18,-19 L-15,-14 L-10,-16 L-12,-22 L-7,-23 L-5,-18 Z;
|
||||
M0,-22 L6,-19 L9,-25 L14,-23 L12,-16 L18,-15 L20,-21 L24,-17 L21,-11 L27,-7 L30,-13 L33,-8 L28,-4 L33,0 L28,5 L33,9 L29,14 L26,7 L20,12 L24,18 L19,22 L16,14 L12,18 L15,25 L9,26 L6,19 L0,23 L-6,19 L-9,26 L-15,25 L-12,18 L-18,14 L-20,22 L-24,18 L-21,12 L-27,7 L-32,14 L-33,9 L-28,5 L-33,0 L-28,-5 L-33,-8 L-30,-13 L-26,-7 L-21,-11 L-24,-17 L-20,-21 L-18,-15 L-12,-16 L-14,-23 L-9,-25 L-6,-19 Z;
|
||||
M0,-20 L5,-18 L7,-23 L12,-22 L10,-16 L15,-14 L18,-19 L22,-16 L20,-11 L25,-8 L28,-12 L30,-8 L26,-4 L30,0 L26,4 L30,8 L28,12 L25,8 L20,11 L22,16 L18,19 L15,14 L10,16 L12,22 L7,23 L5,18 L0,20 L-5,18 L-7,23 L-12,22 L-10,16 L-15,14 L-18,19 L-22,16 L-20,11 L-25,8 L-28,12 L-30,8 L-26,4 L-30,0 L-26,-4 L-30,-8 L-28,-12 L-25,-8 L-20,-11 L-22,-16 L-18,-19 L-15,-14 L-10,-16 L-12,-22 L-7,-23 L-5,-18 Z"
|
||||
dur="15s"
|
||||
repeatCount="indefinite"
|
||||
calcMode="spline"
|
||||
keySplines="0.42 0 0.58 1; 0.42 0 0.58 1" />
|
||||
</path>
|
||||
|
||||
<!-- 血色发光齿轮边缘 -->
|
||||
<path d="M0,-20 L5,-18 L7,-23 L12,-22 L10,-16 L15,-14 L18,-19 L22,-16 L20,-11 L25,-8 L28,-12 L30,-8 L26,-4 L30,0 L26,4 L30,8 L28,12 L25,8 L20,11 L22,16 L18,19 L15,14 L10,16 L12,22 L7,23 L5,18 L0,20 L-5,18 L-7,23 L-12,22 L-10,16 L-15,14 L-18,19 L-22,16 L-20,11 L-25,8 L-28,12 L-30,8 L-26,4 L-30,0 L-26,-4 L-30,-8 L-28,-12 L-25,-8 L-20,-11 L-22,-16 L-18,-19 L-15,-14 L-10,-16 L-12,-22 L-7,-23 L-5,-18 Z"
|
||||
fill="none" stroke="#FF5555" stroke-width="0.3" opacity="0.8" filter="url(#bloodGlowingEdge)">
|
||||
<!-- 血脉跳动 -->
|
||||
<animate attributeName="opacity" values="0.8;0.4;0.8" dur="4s" repeatCount="indefinite" />
|
||||
</path>
|
||||
|
||||
<!-- 血管效果 -->
|
||||
<g stroke="#FF0000" stroke-width="0.2" opacity="0.5">
|
||||
<path d="M-15,-10 Q-10,-5 -15,0 Q-10,5 -5,0" />
|
||||
<path d="M5,-15 Q10,-10 15,-15 Q10,-5 15,0" />
|
||||
<path d="M-5,15 Q0,10 5,15 Q0,5 -5,5" />
|
||||
<path d="M-10,-5 Q-5,0 0,-5 Q5,0 10,-5" />
|
||||
<path d="M-8,8 Q-4,4 0,8 Q4,4 8,8" />
|
||||
|
||||
<!-- 脉动动画 -->
|
||||
<animate attributeName="opacity" values="0.5;0.3;0.5" dur="5s" repeatCount="indefinite" />
|
||||
<animate attributeName="stroke-width" values="0.2;0.3;0.2" dur="3s" repeatCount="indefinite" />
|
||||
</g>
|
||||
|
||||
<!-- 眼白(暗红色) -->
|
||||
<circle cx="0" cy="0" r="12.8" fill="#330000" stroke="#FF0000" stroke-width="0.4"/>
|
||||
|
||||
<!-- 眼球内部血丝 -->
|
||||
<path d="M-9,0 A9,12 0 0,1 9,0 A9,12 0 0,1 -9,0"
|
||||
fill="none" stroke="#FF0000" stroke-width="0.3" opacity="0.7" />
|
||||
<path d="M0,-9 A12,9 0 0,1 0,9 A12,9 0 0,1 0,-9"
|
||||
fill="none" stroke="#FF0000" stroke-width="0.3" opacity="0.7" />
|
||||
|
||||
<!-- 血色虹膜 -->
|
||||
<circle cx="0" cy="0" r="8" fill="#7A0404" stroke="#FF0000" stroke-width="0.3">
|
||||
<!-- 虹膜脉动 - 更血腥的感觉 -->
|
||||
<animate attributeName="r" values="7.8;8.5;7.8" dur="3s" repeatCount="indefinite" />
|
||||
<animate attributeName="fill" values="#7A0404;#B80000;#7A0404" dur="5s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
|
||||
<!-- 诡异眼瞳(不规则移动)-->
|
||||
<g class="eyeball">
|
||||
<!-- 更加诡异的眼神移动 - 同步为与紫色之眼相同的轨迹 -->
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="translate"
|
||||
values="0,0; 2,-1; -1,-2; -2,1; 3,2; 1,3; -3,-1; -1,2; 2,2; 0,0"
|
||||
keyTimes="0;0.11;0.22;0.33;0.44;0.55;0.66;0.77;0.88;1"
|
||||
dur="15s"
|
||||
repeatCount="indefinite"
|
||||
calcMode="spline"
|
||||
keySplines="0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1; 0.42 0 0.58 1" />
|
||||
|
||||
<!-- 深不见底的黑色瞳孔 - 移除脉动动画 -->
|
||||
<circle cx="0" cy="0" r="4" fill="#000000" />
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- 血滴和血迹效果 -->
|
||||
<g id="bloodSplatter">
|
||||
<!-- 血滴1 -->
|
||||
<path d="M15,25 Q15,20 20,20" fill="none" stroke="#FF0000" stroke-width="1" opacity="0.8">
|
||||
<animate attributeName="d" values="M15,25 Q15,20 20,20;M14,24 Q13,18 19,19;M15,25 Q15,20 20,20" dur="10s" repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="0.8;0.5;0.8" dur="8s" repeatCount="indefinite" />
|
||||
</path>
|
||||
|
||||
<!-- 血滴2 -->
|
||||
<path d="M80,30 Q80,25 75,25" fill="none" stroke="#FF0000" stroke-width="1" opacity="0.7">
|
||||
<animate attributeName="d" values="M80,30 Q80,25 75,25;M81,31 Q82,26 74,26;M80,30 Q80,25 75,25" dur="12s" repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="0.7;0.4;0.7" dur="9s" repeatCount="indefinite" />
|
||||
</path>
|
||||
|
||||
<!-- 血滴3 -->
|
||||
<path d="M30,75 Q35,75 35,70" fill="none" stroke="#FF0000" stroke-width="1" opacity="0.9">
|
||||
<animate attributeName="d" values="M30,75 Q35,75 35,70;M29,74 Q34,73 33,68;M30,75 Q35,75 35,70" dur="11s" repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="0.9;0.6;0.9" dur="7s" repeatCount="indefinite" />
|
||||
</path>
|
||||
|
||||
<!-- 血迹斑点 -->
|
||||
<circle cx="20" cy="65" r="1.5" fill="#B80000" opacity="0.7" />
|
||||
<circle cx="22" cy="67" r="0.8" fill="#B80000" opacity="0.8" />
|
||||
<circle cx="18" cy="64" r="0.6" fill="#B80000" opacity="0.6" />
|
||||
|
||||
<circle cx="75" cy="40" r="1.2" fill="#B80000" opacity="0.7" />
|
||||
<circle cx="78" cy="42" r="0.7" fill="#B80000" opacity="0.8" />
|
||||
<circle cx="73" cy="38" r="0.5" fill="#B80000" opacity="0.6" />
|
||||
|
||||
<circle cx="45" cy="80" r="1.3" fill="#B80000" opacity="0.7" />
|
||||
<circle cx="48" cy="82" r="0.9" fill="#B80000" opacity="0.8" />
|
||||
<circle cx="43" cy="78" r="0.7" fill="#B80000" opacity="0.6" />
|
||||
</g>
|
||||
|
||||
<!-- 诡异漂浮的血色粒子 -->
|
||||
<g id="bloodParticles">
|
||||
<circle cx="25" cy="35" r="0.7" fill="#FF0000" opacity="0.7" filter="url(#bloodGlowingEdge)">
|
||||
<animate attributeName="cx" values="25;28;22;30;25" dur="7s" repeatCount="indefinite" />
|
||||
<animate attributeName="cy" values="35;32;38;30;35" dur="8s" repeatCount="indefinite" />
|
||||
<animate attributeName="r" values="0.7;1.2;0.5;1;0.7" dur="6s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="75" cy="65" r="0.9" fill="#B80000" opacity="0.6" filter="url(#bloodGlowingEdge)">
|
||||
<animate attributeName="cx" values="75;70;80;77;75" dur="8s" repeatCount="indefinite" />
|
||||
<animate attributeName="cy" values="65;70;60;68;65" dur="7s" repeatCount="indefinite" />
|
||||
<animate attributeName="r" values="0.9;1.5;0.6;1.1;0.9" dur="7s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="60" cy="25" r="0.6" fill="#7A0404" opacity="0.8" filter="url(#bloodGlowingEdge)">
|
||||
<animate attributeName="cx" values="60;65;55;62;60" dur="6s" repeatCount="indefinite" />
|
||||
<animate attributeName="cy" values="25;20;30;28;25" dur="9s" repeatCount="indefinite" />
|
||||
<animate attributeName="r" values="0.6;1.1;0.4;0.8;0.6" dur="5s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="40" cy="75" r="0.8" fill="#FF0000" opacity="0.7" filter="url(#bloodGlowingEdge)">
|
||||
<animate attributeName="cx" values="40;45;35;42;40" dur="7s" repeatCount="indefinite" />
|
||||
<animate attributeName="cy" values="75;70;80;78;75" dur="8s" repeatCount="indefinite" />
|
||||
<animate attributeName="r" values="0.8;1.3;0.5;1;0.8" dur="8s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
// 导出函数:将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);
|
||||
@@ -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;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user