· 主题与样式机制(Theme & Style)
设计目标
引擎不把颜色 / 间距 / 圆角 / 动效硬编码在组件里,而是抽成一套 设计 token; 引擎提供机制,应用层决定语义 —— 引擎保证「同一个值在任何地方都一致地解析出来」, 但不规定你的品牌色叫什么、什么时候变。
- ① base —— 原始值:Tailwind 风格色 ramp(blue/emerald/amber/… 50–900)、spacing、radius、fontSize、fontWeight。
- ② semantic —— 用途语义:primary / success / danger / info / text / muted / border / background /
palette(数据系列配色)/motion(时长 + 缓动)。动画系统直接读semantic.motion(见 18 · 动画机制)。 - ③ chrome —— 交互外壳(引擎自己画的那层):选中框 / 变换手柄 / 连线端点 / 连接插槽 / 对齐引导线 / 连线标签 / 文本选区 / 阴影色 / 调试框 / 蚂蚁线管壁色。以前这些是写死在源码里的十几处色值,深色主题下不会跟着变;现在同属主 题,可用
ice.setChrome({...})单独覆盖。 - ④ component —— 命名预设:把 token 组合成
card/button/title/gradient等可序列化预设,merge 进props.style,随setTheme热切换重新展开。
优先级链:用户 props > 组件 preset > 主题(semantic / base)> 引擎默认。
主题引用:样式在「绘制那一刻」解析
样式里的色值可以直接引用 token,而不是写死字面量:
new ICERect({ style: { fillStyle: token('primary'), strokeStyle: '$border' } });
// 渐变 stops 也能引用
new ICERect({
style: {
fillGradient: { type: 'linear', from: [0, 0], to: [0, 100], stops: [[0, token('primary')], [1, '$background']] },
},
});
token(path)是推荐写法(返回{ $token }的纯对象,可序列化);字符串简写'$primary'/'$palette.2'/'$chrome.slot.fill'/'$base.radius.md'等价。- 解析发生在 paint 时,所以
setTheme()之后任意组件(不只是用了 preset 的)都会跟着换 —— 自定义组件也能跟随主题热切换。 - token 名写错(解析成
undefined)时跳过赋值、保留 ctx 原值:把fillStyle赋成undefined会让整块画布消失,比"颜色没变"严重得多。 - SVG 导出走同一套解析(否则会出现「画布上有颜色、导出的 SVG 没有」)。
交互状态样式
new ICERect({
style: { fillStyle: token('background') },
states: {
hover: { fillStyle: token('primary'), lineWidth: 2 },
selected: { strokeStyle: token('primary'), lineWidth: 2 },
disabled: { globalAlpha: 0.4 },
},
});
component.setInteractionState('selected', true); // 应用层按自己的语义驱动
- 叠加顺序:
基础样式 → focus → hover → active → selected → disabled(越靠后越优先), 最后运行时state.style之上的状态样式生效(状态是"当前交互反馈",按定义应该可见)。 - 引擎可以自动驱动 hover / active(
ice.enableInteractionStates()),但默认关闭 —— 引擎的 mousemove 本来不做命中检测(高频事件 + 脏矩形渲染),打开它等于给每个移动事件 加一次场景命中测试,由应用按场景决定值不值。
命名主题与实例级切换
主题机制提供「注册表 + 切换」:
registerTheme(name, theme)—— 运行时注入命名主题(多品牌 / 多租户)。内置default+dark。setTheme(...)—— 模块级默认主题(影响此后新建的 ICE)。ice.setTheme(...)—— 实例级主题,不污染模块级当前主题,多实例 / 多品牌各自独立。ice.setChrome(patch)—— 只改交互外壳那一组(保留主题其余部分,只换品牌色手柄这类场景)。- 子树作用域:
new ICEGroup({ theme: {...} })只影响该子树(分屏大屏 / 暗底卡片)。
主题的对象写法是部分主题(深合并),三种形态都支持:
{ primary: '#0d6efd' }(平铺 semantic)、{ semantic: { primary } }(显式分层)、
{ base: { radius: { md: 6 } }, motion: { duration: { fast: 50 } } }(深层局部改)。
两条硬约束:深合并(局部改 motion.duration 不会抹掉 motion.easing)、
不改原主题(合并返回新对象,DEFAULT_THEME / 已注册主题不会被就地污染)。
ICETheme.ts 关键签名(节选,已落地):
export function registerTheme(name: string, theme: ICETheme): void;
export function resolveTheme(input: ICEThemeInput, base?: ICETheme): ICETheme;
export function mergeThemes(theme: ICETheme, patch: ICEThemePatch): ICETheme;
export function registerPreset(name: string, factory: StylePresetFactory): void; // 唯一写入口
export function token(path: string): { $token: string };
export function validateTheme(theme: unknown): ThemeDiagnostic[]; // 含 WCAG 对比度
export const STYLE_PRESETS: { [name: string]: StylePresetFactory }; // 只读视图
主题进快照
ice.serializer.toJSONString();
// { version, createTime, lastModifyTime, theme: { name: 'dark' } | { name, patch: {...} }, childNodes: [...] }
- 只存
{ name, patch }:name是命名主题基线,patch是相对它的真实差异 (形状与主题同构);「当初怎么设置的」(整份对象 / 部分补丁 /setChrome)不影响存下来的内容。 - 与命名主题一致时只写
{ name };没动过主题不写该字段(旧快照格式不变)。 - 还原时先切命名主题 → 再叠补丁 → 最后建组件(preset 与默认样式都是在构造时展开的)。