AIRI 舞台 UI 焕新实录:纯 CSS 波浪动画与自定义主题色的工程实现
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
本篇技术指南围绕 AIRI(Project AIRI,自托管的 AI 伴生桌面项目)2025 年 3 月 10 日发布的一篇开发日志展开,系统复盘了这次"舞台 UI 与设置 UI 风格重构"背后落地到仓库中的三项核心技术:新一代设置界面的视觉升级、用纯 CSS 实现的舞台波浪动画,以及让舞台与 Logo 可以整体换肤的动态主题色体系(含"RGB ON"全光谱动态模式)。读完本文,你将理解这些功能在 packages/stage-layouts 与 packages/stage-ui 中的真实实现细节、可复用的参数与调用链,并了解仓库为简化构建而将独立包剥离到@proj-airi组织背后的工程思路。
背景:一次 UI 风格的"灵光一现"
根据开发日志的记述,3 月 7 日(周五)作者一直在尝试为 AIRI 舞台 UI 与设置 UI 构思新风格,直到当天开发直播结束时灵感才浮现。从 3 月 7 日开始,团队着手实现新的设置 UI,并在接下来几天内取得了快速进展。参与这次迭代的贡献者包括 LemonNekoGH、sumimakito、kwaa、luoling8192 与 junkwarrior87 等。
在仓库中,这些 UI 的最终落点主要分布在:
- packages/stage-layouts/src/layouts/settings.vue:设置页的整体布局框架;
- packages/stage-ui/src/components/scenarios/settings:设置项组件(模型设置、预览舞台等);
- packages/stage-pages/src/pages/settings:设置页面的业务编排模块;
- packages/i18n/src/locales/zh-Hans/settings.yaml:与设置相关的多语言文案(该语言目录下同时存在 en、ja、ko、es、fr、ru、vi 等对应文件,说明设置界面本身是完整国际化的)。
新设置 UI:从基础版本到"点状节奏感"
开发日志记录了设置 UI 的两次关键视觉迭代:
- 基础版本(new-ui-v1):由作者首先完成设置设计的基础版本,呈现了全新的菜单结构;
- 点状按钮效果(new-ui-v2):随后 sumimakito 上线帮忙为按钮实现了"点状效果"(dotted effect),让菜单在视觉上获得了更强的节奏感。
现在我们能从菜单中感受到更多的节奏感,对吧?!
这种"点状"视觉语言与舞台整体的氛围相互呼应。值得注意的是,这些 UI 组件并非一次性完成,而是经过多轮 PR 持续打磨——开发日志特别提到,最终成果依赖一组连续的提交记录(PR #53、#60、#61、#63 等,涉及颜色自定义;PR #54、#55、#65 等,涉及波浪动画)。
仓库瘦身:把独立包迁往 @proj-airi 组织
在重构 UI 的过程中,团队发现packages/目录下有一部分包实际上是独立的包,并不在 Project AIRI 的核心工作流中。把它们继续留在主仓库会无谓地增大仓库的安装体积、拖慢构建流程,因此团队决定将这些包迁移到新注册的 GitHub 组织@proj-airi下。首批迁移的公开仓库包括:
- webai-examples:用于制作 WebGPU 及相关内容的演示;
- lobe-icons:Lobe Icons 的移植版本,面向 Iconify JSON 与 UnoCSS 使用。
这两个仓库保持开源并沿用 MIT 许可证。
仓库内也能找到这次组织拆分留下的痕迹:例如 packages/stage-layouts/src/composables/theme-color.ts 中直接以@proj-airi/stage-ui/libs、@proj-airi/stage-ui/stores/settings的形式引用stage-ui包——即stage-ui这类被广泛复用的包已经以@proj-airi作用域发布,与主仓库解耦。可以推断,这样的拆分让stage-ui等包可以被 AIRI 之外的 Web 应用(如stage-web、ui-server-auth等)独立消费,同时主仓库自身只保留与其业务强相关的代码,从而精简依赖图与构建链。
纯 CSS 波浪动画:从"不可能"到"太疯狂了"
3 月 8 日,junkwarrior87 上线并帮助团队用纯 CSS制作了舞台上的波浪动画。开发日志感叹"这简直太疯狂了,我从来没想到这居然能实现"。这一实现的最终形态可以在 packages/stage-layouts/src/components/Backgrounds/default/part-animated-wave.vue 中找到。
组件参数一览
该组件是一个高度可配置的波浪背景单元,对外暴露了以下 Props(均带默认值):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
height | number | 40 | 波浪主体高度(像素) |
amplitude | number | 14 | 波幅,即波浪起伏的最大幅度(像素) |
waveLength | number | 250 | 单个完整波周期的像素长度 |
fillColor | string | oklch(95% 0.10 var(--chromatic-hue)) | 波浪的填充颜色 |
direction | 'up' \| 'down' | 'down' | 波浪拱起的方向 |
movementDirection | 'left' \| 'right' | 'left' | 波浪滚动的方向 |
animationSpeed | number | 50 | 动画速度(像素/秒) |
实现原理:正弦波路径 + mask-image 技巧
组件通过generateSineWavePath()以 1px 为步长逐点生成 SVG 正弦波路径:先用Math.ceil(width / waveLength)计算需要铺满宽度的完整波数,再以Math.PI * 2 / waveLength作为相位因子,逐点计算amplitude * Math.sin(factor * x)的纵向偏移,最终闭合路径形成可填充的区域。根据direction的不同,基准 Y 坐标与闭合边会相应调整,从而决定波浪是向上还是向下拱起。
真正的关键在于填充色的控制:SVG 作为background-image时无法直接修改内部 fill 颜色,因此组件采用mask-image方案——先把正弦波路径编码为 data URL SVG 作为遮罩,再让一个普通 div 以fillColor作为background被遮罩裁剪出波浪形状。这样,填充色就完全交给 CSS 变量与主题体系驱动,实现了"同一个波浪、随主题变色"的效果。
动画本身则是一段纯 CSS 关键帧:波浪层宽度设为200vw,配合mask-repeat: repeat-x横向平铺;@keyframes wave-animation让元素从translate(0)平移到translate(var(--wave-translate)),其中--wave-translate恰好是-waveLength(平移一个整周期后无缝衔接),--animation-duration由waveLength / animationSpeed计算得出;当movementDirection为'right'时,通过animation-direction: reverse反转滚动方向。
波浪与主题色的联动
在 packages/stage-layouts/src/components/Backgrounds/default/index.vue 中,波浪被叠加在PatternCross(交叉纹理)之上,并且明暗模式的填充色都直接引用主题色变量:
- 暗色模式:
oklch(35% calc(var(--chromatic-chroma) * 0.6) var(--chromatic-hue)) - 亮色模式:
color-mix(in srgb, oklch(95% calc(var(--chromatic-chroma-50) * 0.5) var(--chromatic-hue)) 80%, oklch(100% 0 360))
也就是说,波浪的颜色由全局 CSS 变量--chromatic-hue(色相)与--chromatic-chroma(饱和度)统一决定——这正是下一节"自定义主题色"能够从设置面板一处改动、全舞台即时生效的底层机制。
舞台颜色自定义:几小时完成的惊喜
3 月 8 日临近结束时,LemonNekoGH 和 junkwarrior87 完成了整个舞台的颜色自定义功能,开发日志感叹"我从来没想过这能在短短几个小时内完成"。更让人惊喜的是,连 Logo 也能跟随自定义颜色变化。
主题色状态:OKLCH 色相即主题
主题色状态集中在 packages/stage-ui/src/stores/settings/theme.ts 中,实现思路非常简洁——整套主题只由一个"色相值"驱动:
DEFAULT_THEME_COLORS_HUE = 220.44:默认主题色相(偏蓝);themeColorsHue:持久化在localStorage的settings/theme/colors/hue键下,保存当前色相数值;themeColorsHueDynamic:持久化在settings/theme/colors/hue-dynamic键下,标记是否开启动态(RGB ON)模式;setThemeColorsHue(hue):写入色相并自动关闭动态模式;applyPrimaryColorFrom(color):借助culori的converter('oklch')将任意颜色字符串转换为 OKLCH,再提取其h值作为主题色相,实现"从任意颜色一键取色";isColorSelectedForPrimary(hexColor):把候选色也转为 OKLCH 后与当前色相比较,容差为hueDifference < 0.01 || hueDifference > 359.99(即色相相同或恰好首尾相接);若处于动态模式则一律返回false,因为此时没有任何"手动选中"的预设色。
选择 OKLCH 而非 hex/RGB 是有意为之:OKLCH 将色相与明度、饱和度解耦,使"只改色相、不动明暗层次"成为可能,从而保证无论用户选什么颜色,界面的明暗对比与材质层次都保持稳定。
CSS 变量如何驱动全舞台
色相值最终以 CSS 变量--chromatic-hue(以及配套的--chromatic-chroma、--chromatic-chroma-50等)的形式注入组件树,例如 packages/stage-ui/src/constants/inject.ts 定义了chromaticHue注入键,packages/stage-ui/src/libs/color-from-element.ts 中也会读取--chromatic-hue(默认回退到'200')。波浪填充色、交叉纹理(pattern-cross.vue 中的--cross-color)都引用同一组变量,因此一处改色、全局生效。
Logo 跟随变色则更有意思:packages/stage-layouts/src/components/Layouts/HeaderLink.vue 对 Logo 应用了filter: hue-rotate(calc(var(--chromatic-hue, 0) * 1deg))——直接利用 CSS 的hue-rotate滤镜,把 Logo 的每个像素沿色相环旋转--chromatic-hue度,从而让 Logo 与当前主题色保持一致,无需准备多套 Logo 素材。
浏览器主题色(meta theme-color)的实时同步
为了让浏览器标签栏/地址栏的theme-color也与舞台保持一致,packages/stage-layouts/src/composables/theme-color.ts 提供了一套完整的同步机制:
useThemeColor(colorFrom):从颜色提供函数取值,经colorjs.io转为 sRGB hex 后写入meta[name="theme-color"];- 对于波浪背景,通过
themeColorFromPropertyOf('.widgets.top-widgets .colored-area', 'background-color')直接读取舞台上波浪实际渲染出的background-color,确保浏览器主题色与所见即所得; - 对于图片背景,使用
colorFromElement(基于 html2canvas 克隆采样,采样区域为顶部 140px、步长 10px、缩放 0.5)计算平均色; - 当背景为波浪且开启动态色相时,用
useIntervalFn以250ms 间隔刷新theme-color——注释明确说明这是为了"在不做逐帧渲染的前提下,让动画波浪背景的 theme-color 保持新鲜"。
(完整的动态效果可查看仓库内的演示视频:customizable-theme-colors.mp4。)
RGB ON:让色相在整个光谱中闪耀
颜色自定义的最后一块拼图来自 PR #64 保留的功能:"我想要动态的!"——开发日志把它形象地称为RGB ON模式(色相在整个 RGB 光谱中连续变化)。这一能力此前由 LemonNekoGH 演示,最终由 junkwarrior87 保留进主题体系。
从源码看,RGB ON 的开关正是themeColorsHueDynamic:当它为true时,isColorSelectedForPrimary恒返回false(没有固定选中色),而色相值会随时间持续变化;useBackgroundThemeColor中的 250ms 定时同步器也仅在"波浪背景 +themeColorsHueDynamic"组合下启用,驱动theme-color跟随色相环滚动。可以推断,动态模式下--chromatic-hue由一个持续递增的动画/定时器驱动,波浪、纹理、Logo 乃至浏览器主题色因此得以像呼吸灯一样在完整色相环上循环——这正是"RGB ON"名字的由来。
结语:三天的协作成果
回顾这次迭代:3 月 7 日确定新设置 UI 风格,3 月 8 日完成纯 CSS 波浪动画与整个舞台的颜色自定义(含动态 RGB 模式),期间还完成了独立包向@proj-airi组织的迁移以精简主仓库。这些改动背后,是 stage-layouts 中"组件 + composable + CSS 变量"的分层设计:part-animated-wave.vue提供可参数化的波浪基元,theme-color.ts提供主题色同步管线,settings/theme.ts提供以 OKLCH 色相为核心的轻量状态管理。无论读者是想复刻同样的波浪动画、搭建可换肤的舞台系统,还是设计"一个变量控制全局主题"的架构,这份实现都提供了可直接参考的范本。开发日志最后也表示,AIRI 对所有人开放且友好,即使是编程新手也欢迎参与贡献。
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考