---
name: 阴影与交互设置
description: 强大的多图层视觉控制器。独立管理“静态长亮阴影”与“悬停交互效果”,支持无限叠加图层。
author: ymjr
version: 1.0.0
license: MIT
usage: 选中画板中的任意元素,运行此脚本打开阴影与交互设置面板,可以为元素添加静态常驻阴影或鼠标悬停时的阴影/发光效果。
features:
- 提供可视化的多图层阴影管理器UI面板
- 支持分别为元素配置“静态常亮阴影”和“鼠标悬停特效”
- 支持向 customData 中注入 shadow 和 hover 配置
dependencies:
- 依赖 Feature-Hover-Engine 处理鼠标移动与悬停渲染
autorun: false
---
/*
```javascript
*/
var locales = {
zh: {
notice_select: "请先选择至少一个元素",
ui_panel_title: "✨ 阴影图层管理器",
ui_static_title: "🌙 静态长亮阴影",
ui_hover_title: "🖱️ 鼠标悬停特效",
ui_hover_pointer: "👆 悬停时显示手指光标",
ui_layer: "图层",
ui_add_layer: "+ 添加新图层",
ui_done: "完成",
ui_editor_add: "✨ 新增",
ui_editor_edit: "✏️ 编辑",
ui_editor_layer_type: "图层",
ui_editor_static: "静态",
ui_editor_hover: "Hover",
ui_editor_color: "🎨 颜色/亮度差",
ui_editor_blur: "🌫️ 模糊 (Blur)",
ui_editor_x: "↔️ 水平偏移 (X)",
ui_editor_y: "↕️ 垂直偏移 (Y)",
ui_editor_tip: "💡 格式提示:
• 负数(如 -20)表示基于原色加深。
• 0.1 表示相对尺寸,10px 表示绝对像素。",
ui_editor_cancel: "取消",
ui_editor_save: "确认保存"
},
en: {
notice_select: "Please select at least one element",
ui_panel_title: "✨ Shadow Layer Manager",
ui_static_title: "🌙 Static Shadow",
ui_hover_title: "🖱️ Hover Effects",
ui_hover_pointer: "👆 Pointer cursor on hover",
ui_layer: "Layer",
ui_add_layer: "+ Add New Layer",
ui_done: "Done",
ui_editor_add: "✨ Add",
ui_editor_edit: "✏️ Edit",
ui_editor_layer_type: "Layer",
ui_editor_static: "Static",
ui_editor_hover: "Hover",
ui_editor_color: "🎨 Color/Brightness",
ui_editor_blur: "🌫️ Blur",
ui_editor_x: "↔️ Offset X",
ui_editor_y: "↕️ Offset Y",
ui_editor_tip: "💡 Format Tips:
• Negative (e.g., -20) darkens base color.
• 0.1 for relative, 10px for absolute.",
ui_editor_cancel: "Cancel",
ui_editor_save: "Save"
}
};
const { Notice } = ea.obsidian;
// ====================== 1. 数据初始化与解析 ======================
const selectedEls = ea.getViewSelectedElements();
if (selectedEls.length === 0) {
new Notice(t("notice_select"));
return;
}
const target = selectedEls[0];
const DEFAULT_SHADOW = { shadowColor: "rgba(0,0,0,0.3)", shadowBlur: "0.1", shadowOffsetX: "0", shadowOffsetY: "0.1" };
// 内部状态管理器 (不污染全局 window)
let state = {
staticEnabled: !!target.customData?.shadow,
staticShadows: [],
hoverEnabled: !!target.customData?.hover,
hoverPointer: target.customData?.hover?.pointer !== false,
hoverShadows: []
};
// 解析静态阴影
if (target.customData?.shadow) {
state.staticShadows = Array.isArray(target.customData.shadow)
? JSON.parse(JSON.stringify(target.customData.shadow))
: [JSON.parse(JSON.stringify(target.customData.shadow))];
}
// 解析 Hover 阴影
if (target.customData?.hover?.shadow) {
state.hoverShadows = Array.isArray(target.customData.hover.shadow)
? JSON.parse(JSON.stringify(target.customData.hover.shadow))
: [JSON.parse(JSON.stringify(target.customData.hover.shadow))];
}
// ====================== 2. 核心应用逻辑 ======================
async function applyToSelected() {
if (selectedEls.length === 0) return;
const api = ea.getExcalidrawAPI();
selectedEls.forEach((el) => {
api.ShapeCache?.cache?.delete(el);
let customData = { ...(el.customData || {}) };
// 应用静态阴影
if (state.staticEnabled && state.staticShadows.length > 0) {
customData.shadow = JSON.parse(JSON.stringify(state.staticShadows));
} else {
delete customData.shadow;
}
// 应用 Hover 阴影
if (state.hoverEnabled) {
customData.hover = {
...(customData.hover || {}),
id: customData.hover?.id || [el.id],
pointer: state.hoverPointer
};
if (state.hoverShadows.length > 0) {
customData.hover.shadow = JSON.parse(JSON.stringify(state.hoverShadows));
} else {
delete customData.hover.shadow;
}
} else {
if (customData.hover) {
delete customData.hover.shadow;
delete customData.hover.pointer;
if (Object.keys(customData.hover).filter(k => k !== 'id').length === 0) {
delete customData.hover;
}
}
}
el.customData = customData;
});
ea.copyViewElementsToEAforEditing(selectedEls);
await ea.addElementsToView(false, false, true);
api.updateScene({ appState: { zoom: { value: api.getAppState().zoom.value + 0.0001 } } });
}
// ====================== 3. UI 界面构建 ======================
function createFloatingPanel() {
if (document.getElementById("ea-multi-shadow-panel")) return;
// --- 主面板容器 ---
const panel = document.createElement('div');
panel.id = "ea-multi-shadow-panel";
panel.style.cssText = `position:fixed; top:100px; right:80px; width:340px; background:var(--background-primary); border:1px solid var(--background-modifier-border); box-shadow:0 12px 32px rgba(0,0,0,0.3); border-radius:10px; z-index:9999; display:flex; flex-direction:column; max-height:80vh; overflow:hidden; font-family: var(--font-interface);`;
// 头部
const header = document.createElement('div');
header.style.cssText = `padding:14px 16px; background:var(--background-secondary); cursor:move; border-bottom:1px solid var(--background-modifier-border); display:flex; justify-content:space-between; align-items:center; user-select:none; font-weight:bold; color:var(--text-normal);`;
header.innerHTML = `${t("ui_panel_title")}`;
panel.appendChild(header);
// 滚动内容区
const content = document.createElement('div');
content.style.cssText = `padding:16px; overflow-y:auto; flex:1; display:flex; flex-direction:column; gap:20px; position:relative;`;
panel.appendChild(content);
// --- 动态渲染图层列表的工厂函数 ---
const renderSection = (title, modeKey) => {
const wrapper = document.createElement('div');
wrapper.id = `section-${modeKey}`;
wrapper.style.cssText = `border: 1px solid var(--background-modifier-border); border-radius: 8px; background:var(--background-primary-alt); overflow:hidden;`;
const shadowsArray = modeKey === 'static' ? state.staticShadows : state.hoverShadows;
const isEnabled = modeKey === 'static' ? state.staticEnabled : state.hoverEnabled;
let html = `