Files
classhelper-island-refactor…/out/sections/14-animations.md
T
ClassIsland Prompt Kit 22c1784bab feat: ClassIsland / ClassHelper 灵动岛提示词套件
- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构)
- ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本)
- 投喂指南、分卷、逐节文件
- 调研记录、一致性基线、生成与质检脚本
2026-11-12 21:59:58 +08:00

44 KiB
Raw Blame History

14. 动画与动效总表

本节是「动效施工图」。实现者必须逐条实现下表的触发、属性、起止值、时长、曲线、延迟与实现方式,不得自行「优化」时长或曲线。所有数值均来自 ClassIsland 源码;标注【推断】的为源码未直接给出、由同类动画或 Web 平台换算得出。

14.0 全局动效规则

14.0.1 动画级别(AnimationLevel)

必须实现 3 档,默认 标准(1)(ClassIsland/Models/Settings.cs:1851;Views/SettingPages/GeneralSettingsPage.axaml:280-293 的三项为「禁用 / 标准 / 华丽」,绑定 SelectedIndex):

级别 文案 行为要求 证据
0 禁用 IThemeService.IsTransientDisabled = true(禁用过渡动画),Popup 入场、抽屉/竖抽屉动画、Workflow 等全部关闭;但主界面的提醒动画与部分控件不在此列(设置项描述明确写「禁用动画选项不适用于应用主界面和部分控件」) App.axaml.cs:725;IThemeService.cs:44;PopupIntroAnimationBehavior.cs:67;DrawerHost.axaml.cs:70;GeneralSettingsPage.axaml:283
1 标准(默认) 列表进入动画关闭、Popup 入场可用;不启用「行/组件淡入」「WrapPanel 尺寸过渡」等高成本动画 StackPanelIntroAnimationBehavior.cs:46;DrawerHost.axaml.cs:70
2 华丽 额外启用:组件/行淡入(250–300ms)、WrapPanel 子项位置隐式动画(225ms 默认) MainWindowLine.cs:382-400;ComponentPresenter.axaml.cs:224-243;WrapPanelResizingAnimationAssist.cs:51-56

实现细节:

  • 该设置必须持久化并在启动时同步到全局单例(对齐 IThemeService.AnimationLevel 静态属性),任何动画入口先查级别再播放(源码模式见 MainWindowLine.cs:384)。
  • 「禁用动画等待」(IsWaitForTransientDisabled)是另一独立开关:默认阻塞操作会等待动画播完,开启后不等待(IThemeService.cs:46-49;Settings.cs:773-782)。Web 实现等价为「不 await 动画 Promise」。
  • 必须同时响应系统 prefers-reduced-motion: reduce:将其视为「级别 0 + 时长压到 1ms」(与 ClassHelper renderer/styles/index.css:909-917 一致),且加载指示器必须放慢而不是停(index.css:919-930)。

14.0.2 缓动别名表(必须集中定义,禁止各组件手写)

别名 cubic-bezier 用途 来源
--ci-ease-out cubic-bezier(0.16, 1, 0.3, 1) 绝大多数进出场(ClassHelper 主缓动) ClassHelper/packages/shared/src/motion.ts:24
--ci-ease-in-out cubic-bezier(0.77, 0, 0.175, 1) 对称来回(抽屉推拉、内容互换) motion.ts:26
--ci-ease-drawer cubic-bezier(0.32, 0.72, 0, 1) 侧向抽屉 motion.ts:28
--ci-ease-fl-1 cubic-bezier(0.65, 0, 0.35, 1) Fluent 行宽过渡/L 型 S 曲线 FluentTheme/Styles.axaml:269
--ci-ease-fl-2 cubic-bezier(0.25, 1, 0.5, 1) 遮罩文字淡入、行淡入 FluentTheme/Styles.axaml:333;MainWindowLine.cs:397
--ci-ease-fl-3 cubic-bezier(0.5, 0, 0.75, 0) 遮罩退出缩放 FluentTheme/Styles.axaml:389
--ci-ease-popup cubic-bezier(0.22, 1, 0.36, 1) Popup 入场(Semi 风格) PopupIntroAnimationBehavior.cs:77,86
--ci-ease-tutorial cubic-bezier(0.16, 1, 0.3, 1) 教程高亮/聚光 TutorialHighlightControl.axaml:13
--ci-ease-list cubic-bezier(0.00, 1.00, 0.00, 1.00) 列表错峰入场 StackPanelIntroAnimation.axaml:22
--ci-ease-text cubic-bezier(0.33, 1, 0.68, 1) OOBE 文字收拢、字符翻入 OobeIntroAnimationControl.axaml:93,105;.axaml.cs:151
--ci-ease-block cubic-bezier(0.76, 0, 0.24, 1) OOBE 方块放大 OobeIntroAnimationControl.axaml:48
--ci-ease-root cubic-bezier(0.00, 0.83, 0.17, 0.90) OOBE 整场缩放 OobeIntroAnimationControl.axaml:16
--ci-ease-char-out cubic-bezier(0.32, 0, 0.67, 0) OOBE 字符翻出 OobeIntroAnimationControl.axaml.cs:117
--ci-ease-spin cubic-bezier(0.50, 0.25, 0.50, 0.75) 更新图标旋转 UpdateSettingsPage.axaml:90
--ci-ease-overshoot cubic-bezier(0.00, 1.00, 0.50, 1.10) 经典主题行宽过冲 ClassicTheme/Styles.axaml:207
--ci-ease-step cubic-bezier(0, 0, 0, 1) 抽屉/关于页 (0,0 0,1) DrawerHost.axaml:46-48;AboutSettingsPage.axaml:137
Avalonia SineEaseIn cubic-bezier(0.12, 0, 0.39, 0)【推断】 遮罩层淡入/淡出 FluentTheme/Styles.axaml:299
Avalonia SineEaseOut cubic-bezier(0.61, 1, 0.88, 1)【推断】 图标宽度/进度透明度 AnimatedIconButton.axaml:24
Avalonia SineEaseInOut cubic-bezier(0.37, 0, 0.63, 1)【推断】 呼吸/进度过渡 WorkflowProgressIndicatorItemControl.axaml:19,32,60
Avalonia QuadraticEaseIn cubic-bezier(0.11, 0, 0.5, 0)【推断】 经典遮罩下压、涟漪放大 ClassicTheme/Styles.axaml:271;RippleEffect.axaml.cs:94
Avalonia QuadraticEaseOut cubic-bezier(0.5, 1, 0.89, 1)【推断】 经典遮罩上滑 ClassicTheme/Styles.axaml:311
Avalonia ExponentialEaseOut cubic-bezier(0.16, 1, 0.3, 1)【推断】 闪屏滑入 SplashWindow.axaml.cs:111,116
Avalonia CubicEaseOut cubic-bezier(0.33, 1, 0.68, 1)【推断】 闪屏进度 SplashWindow.axaml.cs:75
Avalonia CubicEaseInOut cubic-bezier(0.65, 0, 0.35, 1)【推断】 WrapPanel 默认隐式动画 WrapPanelResizingAnimationAssist.cs:40

14.0.3 时长常量表

常量 值 使用场景
--ci-dur-instant 90ms 按压反馈、行透明度
--ci-dur-fast 100–140ms 行/组件淡入淡出、抽屉内容、遮罩退出
--ci-dur-base 150–250ms Popup、遮罩文字、列表滑入
--ci-dur-slow 300–360ms 行宽过渡(Fluent)、遮罩分块、组件淡入
--ci-dur-overshoot 550ms 行宽过渡(经典)
--ci-dur-long 750ms OOBE 字符、教程高亮、遮罩缩放
--ci-dur-loop 1000–3000ms 循环/呼吸/旋转/OOBE 整场

14.0.4 动效编排原则(实现时逐条遵守)

  1. 进场慢、退场快:进场承担「让用户理解发生了什么」的职责,可以 200–300ms;退场承担「别挡路」的职责,必须 100–150ms 结束。源码中提醒遮罩文字淡入 250ms、淡出 200ms;抽屉 250ms;弹窗退出 140ms(index.css:638-644)都遵循此规律。实现者不得把退场做得比进场更慢。
  2. 延迟用于建立因果:遮罩分块先扫入(0–360ms),文字随后(延迟 260ms),投影最后(延迟 600ms);OOBE 字符按正弦曲线错峰。延迟必须表达「谁先谁后」,不得所有元素同一时刻启动。
  3. 错峰必须封顶:列表入场 25ms/项(源码)、ClassHelper 35ms/项且第 9 项起封顶、页面块 30ms/块且封顶 180ms(motion.ts:175-179;index.css:857-883)。超过 10 项仍线性累加会让最后一项等超过 1 秒,属于缺陷。
  4. 状态变化必须可打断:任何「进/出」成对动画必须共用同一元素、同一属性、可反向;反向时用新动画直接覆盖旧动画(不要排队)。遮罩的 in/out 伪类互斥实现(MainWindowLine.cs:726-791)就是范例。
  5. 不在动画里做业务:动画只表达状态,不得在 animationend 里触发网络请求、不得用动画时长做业务计时;业务定时器与动画解耦。
  6. 动效不得改变布局语义:动画结束后元素的最终几何必须与「无动画直接跳到终态」完全一致;判定方法:动画播放中与播放后分别测量 getBoundingClientRect,最终值必须逐像素相等。
  7. 一致性优先于炫技:同一个交互(如「抽屉打开」)在全应用只能有一种时长与曲线;新交互必须复用 --ci-ease-* 与 --ci-dur-*,不得新增私有曲线。

14.1 A. 主界面(岛)动效

主界面动效分三类:状态过渡(A1–A4、A8–A9)、几何过渡(A5–A7、A11)、数据过渡(A10、A12)。状态过渡可直接用 CSS transition;几何过渡必须避免回流(见 14.7 第 1 条);数据过渡(进度条)必须做「比例插值」而不是「每帧重算 DOM」。三类不得混用同一个动画实例,否则打断时会互相污染。

编号 名称 触发 变化属性 起止值 时长 曲线 延迟/交错 实现方式 证据
A1 窗口背景换色 应用明暗主题切换 background 旧色 → 新色 200ms 默认线性 无 CSS transition: background-color 200ms MainWindow.axaml:40-44
A2 行淡入 行由隐藏变可见 opacity 0 → 1 250ms (0.25,1,0.5,1) 无 WAAPI/composition opacity,AnimationLevel≥2 MainWindowLine.cs:336-344,382-401
A3 行透明度过渡 悬停淡出开关生效 opacity 1 ↔ 0.05 100ms 默认 无 CSS transition MainWindowLine.axaml:51,73-75;FluentTheme/Styles.axaml:268
A4 组件淡入 组件挂载/由隐藏变可见 opacity 0 → 1 300ms (0.65,0,0.35,1) 无 composition opacity,AnimationLevel≥2 ComponentPresenter.axaml.cs:224-243,263-266
A5 行宽过渡(Fluent) 组件内容变化导致背景宽度变化 width 旧宽 → 新宽 300ms (0.65,0,0.35,1) 无 CSS transition(背景层绝对定位,禁止回流文字) FluentTheme/Styles.axaml:264-273
A6 行宽过渡(经典) 同上 width 旧宽 → 新宽 550ms (0,1,0.5,1.1) 过冲 无 同上;曲线必须带过冲,不得改成 ease-out ClassicTheme/Styles.axaml:202-211
A7 WrapPanel 子项位置过渡 子项增删/重排 offset(x/y) 旧位置 → 新位置 默认 225ms;主界面覆盖 300ms CubicEaseInOut;主界面 (0.65,0,0.35,1) 无 composition 隐式动画;Web 用 FLIP(先读后写) WrapPanelResizingAnimationAssist.cs:45-49,130-137;MainWindow.axaml:197-198
A8 编辑模式背景切换 进入/退出编辑模式 窗口背景、backdrop-filter 透明 → 亚克力 200ms 默认 无 CSS transition(只动背景层) MainWindow.axaml:177-181;MainWindow.axaml.cs:510-517
A9 拖动项抬升 拖动开始/结束 opacity、z-index 1 → 0.5(拖动中) 即时 — 无 直接切 class(不做动画) MainWindow.axaml:478-481
A10 缩放重置 点击「重置视图」 transform: scale 当前 → 1.0 200ms【推断】 (0.16,1,0.3,1) 无 WAAPI 动画 transform MainWindow.axaml:606-609;ZoomBorder MinZoom 0.5/MaxZoom 3/ZoomSpeed 1.2(:199-209)
A11 分体主界面切换 开启/关闭分体 行内边距、圆角、背景宽 合并 ↔ 分体 与 A5 同步 300ms 同 A5 无 class 切换 + 宽度过渡 FluentTheme/Styles.axaml:245-259
A12 进度条值过渡 每秒/每 50ms 更新 value/transform: scaleX 旧比例 → 新比例 50ms 线性 无 CSS transition transform FluentTheme/Styles.axaml:137-145;ClassicTheme/Styles.axaml:123-131

14.2 B. 提醒与强提醒动效

提醒是「岛」最重要的动效场景,必须实现两套完全不同的遮罩形态(Fluent/经典),并共用同一套队列与遮罩生命周期状态机。状态机必须严格按源码实现::mask-anim → :mask-in →(可选):overlay-anim/:overlay-in → :mask-out → :overlay-out,任一状态切换都通过类名驱动(Controls/MainWindowLine.cs:724-791)。禁止用 JS 计时器手动改元素 style 来「模拟」状态机,否则队列切换时会错乱。

强提醒特效是独立置顶窗口(TopmostEffectWindow),不属于岛 DOM;特效元素播放完成后自行移除,窗口内元素清空即隐藏窗口(TopmostEffectWindow.axaml.cs:50-67,90-98)。特效必须支持 NotificationEffectRenderingScale 缩放(默认 1.0,大屏自动降到 0.75/0.5,见 part 03 表)。

编号 名称 触发 属性 起止值 时长 曲线 延迟/交错 实现方式 证据
B1 Fluent 遮罩斜切扫入 :mask-in 5 个区段宽度进度 0 → 1 每段 360ms QuarticEaseOut(≈(0.16,1,0.3,1)【推断】) 段间 120ms:区 0/4 同起,1/3 延迟 90ms,2 延迟 120ms canvas/SVG 自绘 + rAF 或 WAAPI 驱动进度 SlantedMaskControl.cs:166-202,225-251;FluentTheme/Styles.axaml:175-180
B2 Fluent 遮罩文字淡入 :mask-in opacity 0 → 1 250ms (0.25,1,0.5,1) 延迟 260ms CSS animation both FluentTheme/Styles.axaml:329-340
B3 Fluent 遮罩文字缩放 :mask-in scale 1.1 → 1.0 750ms (0.25,1,0.5,1) 延迟 260ms CSS transform animation FluentTheme/Styles.axaml:341-352
B4 Fluent 遮罩投影淡入 :mask-in 背景层 opacity、box-shadow 0 → 1;阴影 0 8 32 #66000000 100ms 默认 延迟 600ms CSS animation + transition: box-shadow 100ms FluentTheme/Styles.axaml:169-173,355-367
B5 Fluent 遮罩文字淡出 :mask-out opacity 1 → 0 200ms 默认 无 CSS animation FluentTheme/Styles.axaml:376-386
B6 Fluent 遮罩文字放大退出 :mask-out scale 1.0 → 1.1 200ms (0.5,0,0.75,0) 无 CSS animation FluentTheme/Styles.axaml:387-398
B7 Fluent 遮罩背景淡出 :mask-out 背景层 opacity 1 → 0 100ms 默认 无 CSS animation FluentTheme/Styles.axaml:401-409
B8 覆盖层淡入/淡出 :overlay-in/:overlay-out opacity 0 ↔ 1 200ms SineEaseIn≈(0.12,0,0.39,0)【推断】 无 CSS transition FluentTheme/Styles.axaml:419-451
B9 内容上滑进入(遮罩层内) 遮罩内内容切换 translateY -50 → 0 200ms SineEaseIn 无 CSS transition ClassicTheme/Styles.axaml:237-249,266-281 相关;主窗口 TranslateTransform Y=-50(ClassicTheme/Styles.axaml:87,148)
B10 Classic 遮罩下压 :mask-in translateY -50 → 0 200ms QuadraticEaseIn 无 CSS animation both ClassicTheme/Styles.axaml:265-281
B11 Classic 遮罩文字淡入 :mask-in opacity 0 → 1 100ms 默认 延迟 200ms CSS animation ClassicTheme/Styles.axaml:288-300
B12 Classic 遮罩上滑退出 :mask-out translateY 0 → 50 200ms QuadraticEaseOut 延迟 100ms CSS animation forward ClassicTheme/Styles.axaml:305-318
B13 Classic 遮罩文字淡出 :mask-out opacity 1 → 0 100ms 默认 无 CSS animation ClassicTheme/Styles.axaml:319-331
B14 Classic 行投影淡入 :mask-in effect(DropShadowEffect) opacity 0 → 1,blur 8 100ms 线性 无 CSS box-shadow transition(禁止 filter) ClassicTheme/Styles.axaml:34-45,283-287
B15 提醒进度条 提醒剩余时间更新 value 旧 → 新 50ms 线性 无 CSS transition transform: scaleX FluentTheme/Styles.axaml:137-145
B16 强提醒涟漪扩散 提醒开始(特效开启) scale 0 → 1 600ms QuadraticEaseIn 无 WAAPI transform,原点=岛中心 RippleEffect.axaml.cs:86-96
B17 强提醒涟漪淡出 同上 opacity 1 → 0 600ms SineEaseIn 无 WAAPI opacity RippleEffect.axaml.cs:102-106
B18 强提醒结束回调 特效完成 — — 总 750ms — 600ms 动画后等 150ms setTimeout(750) 后移除特效元素 RippleEffect.axaml.cs:108-109;TopmostEffectWindow.axaml.cs:90-98
B19 逐字滚动提醒 提醒内容超宽 offset.x 右边界 → 左边界循环 Duration / RepeatCount 线性 无 WAAPI 无限循环,每帧线性位移 RollingTextTemplate.axaml.cs:39-62;RollingTextTemplateData.RepeatCount 默认 2
B20 滚动提醒重排修正 容器尺寸变化 offset.x 按当前进度重算 即时重播 线性 无 重新 startAnimation(源码在 LayoutUpdated 重设) RollingTextTemplate.axaml.cs:75-80

14.3 C. 启动、OOBE、欢迎动效

启动链路是「闪屏 → 主界面」两段式:闪屏自身有滑入与进度动画,主界面出现时行/组件按动画级别淡入(A2/A4)。OOBE 是唯一允许使用 3 秒级长动画的场景,必须整段可跳过(用户点击任意处立即进入下一步),且在 AnimationLevel=0 时直接呈现终态(OobeIntroAnimationControl.axaml.cs:161-164 只在 Loaded 时播放;实现时须先判断级别)。

OOBE 的字符动画是「11 个方块 + 11 个字符」双轨并行的复杂编排,必须按源码公式逐字计算延迟(C6),不得改成固定 50ms 等间隔——源码用正弦分布让中部字符更密、两端更疏(OobeIntroAnimationControl.axaml.cs:37)。

编号 名称 触发 属性 起止值 时长 曲线 延迟/交错 实现方式 证据
C1 闪屏 Logo 滑入 闪屏启动 translateX 32 → 0 300ms ExponentialEaseInOut≈(0.16,1,0.3,1)【推断】 0ms WAAPI transform SplashWindow.axaml.cs:101-119
C2 闪屏状态栏滑入 闪屏启动 translateX 32 → 0 300ms 同上 150ms WAAPI transform SplashWindow.axaml.cs:53-54,109-119
C3 闪屏淡入 闪屏启动 opacity 0 → 1 300ms CubicEaseOut 与 C1/C2 同延迟 WAAPI opacity SplashWindow.axaml.cs:114-120
C4 闪屏进度条 启动进度更新 translateX -400·(1-p) 到目标 max((Δp)/8, 0.5)s;结束时 150ms CubicEaseOut 无 WAAPI transform SplashWindow.axaml.cs:64-82,126
C5 OOBE 整场进场 欢迎页加载 scale、opacity 1.25 → 1.0;0 → 1 3000ms (0,0.83,0.17,0.90) 无 CSS animation both OobeIntroAnimationControl.axaml:14-30
C6 OOBE 字符错峰翻入 进场后逐字 translateY、opacity 50 → 0;0 → 1 delay·9·1.5 + 750 ms (0.25,1,0.5,1)(入) 逐字延迟 sin((i+2)/(n+2)·π/2)·500/n ms WAAPI 逐元素动画,串行 await OobeIntroAnimationControl.axaml.cs:30-54,65-124
C7 OOBE 字符翻出 同一字符动画后段 rotate3D.angleX、opacity 0 → 90°;1 → 0 同上 (0.32,0,0.67,0) 内嵌于 C6 关键帧 WAAPI 关键帧(0%/timeMs/timeMs+750/100%) OobeIntroAnimationControl.axaml.cs:92-118
C8 OOBE 文字块翻入 字符对转向后 rotate3D.angleX、opacity -90° → 0;0 → 1 delay·9·2 + 750 ms (0.33,1,0.68,1) 内嵌 WAAPI 关键帧 OobeIntroAnimationControl.axaml.cs:126-158
C9 OOBE 方块放大 第 7 个方块 scaleX、background 1 → 2.17;灰 → #00BFFF 250ms (0.76,0,0.24,1) 延迟 307ms CSS animation OobeIntroAnimationControl.axaml:44-58
C10 OOBE 文字收拢 动画末尾 gap/letter-spacing、min-width 4px → 0;32px → 0 700ms (0.33,1,0.68,1) 无 CSS animation OobeIntroAnimationControl.axaml:88-116
C11 欢迎页内容淡入 欢迎页显示 opacity 0 → 1 200ms 默认 无 CSS animation both Views/WelcomePages/WelcomePage.axaml:11-24
C12 欢迎页 Logo 布局 欢迎页显示 尺寸 80×80 — — — 静态,非动画 WelcomePage.axaml:29-33

14.4 D. 弹层、抽屉、列表与设置动效

本组是「应用外壳」动效,必须复用 ClassHelper 既有 token 与 Element Plus 覆盖层(renderer/styles/index.css:567-756 已把弹窗/抽屉/浮层/消息条的过渡统一替换为弹簧与 --ch-ease-out)。实现时不得再写第二套弹窗动画;新弹层一律继承既有 class。

列表错峰(D12)必须是「真实逐项启动」而不是一次性 animation-delay 批量:源码用 25ms 间隔的 DispatcherTimer 逐项置位(StackPanelIntroAnimationBehavior.cs:74-90),逐项启动才能保证「列表在动画过程中被重新排序/过滤」时新项不等待旧延迟。

编号 名称 触发 属性 起止值 时长 曲线 延迟/交错 实现方式 证据
D1 Popup 淡入 任意 Popup 打开 opacity 0 → 1 150ms (0.22,1,0.36,1) 无 WAAPI(AnimationLevel≥1) PopupIntroAnimationBehavior.cs:73-78
D2 Popup 缩放淡入 同上 scale 0.925 → 1.0 150ms (0.22,1,0.36,1) 无 WAAPI;transform-origin 按 placement 决定(Bottom→上中、Right→左中、Pointer→左上、Center→中心) PopupIntroAnimationBehavior.cs:80-112
D3 横抽屉推入 抽屉打开 translateX、opacity 抽屉宽 → 0;0 → 1 250ms cubic-bezier(0,0,0,1) 无 CSS transition DrawerHost.axaml:42-59
D4 横抽屉遮罩 抽屉打开 opacity 0 → 1 250ms (0,0,0,1) 无 CSS transition DrawerHost.axaml:52-58,68-70
D5 竖抽屉推入 抽屉打开/收起 translateY(Canvas.Bottom)、height 关闭高度 → 0 250ms (0,0,0,1) 无 CSS transition(用 transform 代替 bottom/height) VerticalDrawer.axaml:29-36,136-155
D6 竖抽屉遮罩 抽屉打开 opacity 0 → 1 250ms (0,0,0,1) 无 CSS transition VerticalDrawer.axaml:126-134,150-153
D7 图标按钮展开 展开/收起 width、文字 opacity 图标宽 ↔ 全宽;0 ↔ 1 Duration 属性(默认【推断】200ms) SineEaseOut(宽)/SineEaseInOut(文字) 无 CSS transition AnimatedIconButton.axaml:22-46
D8 图标按钮底色 悬停/主题变化 background 旧色 → 新色 150ms 默认 无 CSS transition AnimatedIconButton.axaml:81-88
D9 Shimmer 骨架淡出 内容加载完成 opacity 1 → 0(骨架);0 → 1(内容) 200ms (0.65,0,0.35,1) 无 CSS transition Shimmer.axaml:22-38,43-54
D10 列表滑入 选中项变化 translateY、opacity -40 → 0;0 → 1 250ms (0.65,0,0.35,1) 无 CSS animation forward SlidingListBox.axaml:38-55
D11 列表滑出 同上 translateY、opacity 0 → 40;1 → 0 250ms (0.65,0,0.35,1) 无 CSS animation SlidingListBox.axaml:57-74
D12 堆叠面板错峰入场 面板首次加载 translateY、opacity 50 → 0;0 → 1 750ms (0,1,0,1) 每项间隔 25ms(DispatcherTimer) rAF/setInterval(25ms) 逐项加 class StackPanelIntroAnimationBehavior.cs:74-90;StackPanelIntroAnimation.axaml:11-35
D13 工作流进度条颜色 状态变化 background 灰 ↔ accent 500ms SineEaseInOut 无 CSS transition WorkflowProgressIndicatorItemControl.axaml:16-22
D14 工作流不确定态呼吸 进行中且无进度 opacity 0.85 → 0.4 → 0.85 750ms SineEaseInOut 无限交替 CSS animation alternate infinite WorkflowProgressIndicatorItemControl.axaml:28-44
D15 工作流确定性进度 有进度值 value 旧 → 新 180ms SineEaseInOut 无 CSS transition transform WorkflowProgressIndicatorItemControl.axaml:57-67
D16 工作流完成淡入 完成后 opacity 0 → 1 300ms SineEaseOut 无 CSS transition WorkflowProgressIndicatorItemControl.axaml:63-65
D17 TeachingTip 箭头循环 教学提示显示 translateY、rotateY 0→-8→0;0→180° 1000ms (0.5,1,0.89,1)/(0.11,0,0.5,0)/(0.66,0,0.34,1) 无限 CSS animation infinite MyTeachingTipStyles.axaml:223-251
D18 教程高亮入场 教程步骤开始 margin、opacity -64 → 0;0 → 1 750ms (0.16,1,0.3,1) 无 CSS animation both TutorialHighlightControl.axaml:11-24
D19 教程高亮呼吸循环 教程步骤持续 margin、opacity -64→0→0;0→1→1 1750ms (0.16,1,0.3,1) 延迟 1750ms,无限 CSS animation infinite TutorialHighlightControl.axaml:25-44
D20 教程聚光入场 教程步骤开始 margin、opacity 64 → 0;0 → 1 750ms (0.16,1,0.3,1) 无 CSS animation TutorialSpotlightControl.axaml:11-35
D21 更新图标旋转 下载更新中 rotate 0 → 360° 1000ms (0.5,0.25,0.5,0.75) 无限 CSS animation infinite UpdateSettingsPage.axaml:88-99
D22 关于页头部淡入 页面加载 opacity 0 → 1 200ms (0,0,0,1) 延迟 250ms CSS animation both AboutSettingsPage.axaml:133-147
D23 拖拽高亮脉冲 拖动组件中 opacity 0.1 → 0.6 → 0.1 1000ms SineEaseInOut 无限交替 CSS animation alternate infinite EditableComponentsListBox.axaml:307-335
D24 设置面板显隐 子设置展开 opacity 0 ↔ 1 200ms (0,1,0,1) 无 CSS transition WeatherSettingsPage.axaml:207-214
D25 自动化项显隐 条件切换 opacity 0 ↔ 1 100ms 默认 无 CSS transition AutomationSettingsPage.axaml:140-142
D26 列表项淡入(课表) 课表项状态变化 opacity 0 ↔ 1 150ms 默认 无 CSS transition LessonsListBox.axaml:69-77

14.5 E. 组件动效

组件动效的原则是「动内容不动容器」:滚动、滑动、倒计时进度都在组件内部完成,绝不触发行高/行宽变化(否则会触发 A5/A6 的行宽过渡,产生连锁抖动)。滚动组件的速度单位是「像素/秒」,必须由设置项驱动并做下限保护(Math.Clamp(SpeedPixelPerSecond, 1, int.MaxValue),RollingComponent.axaml.cs:96),暂停时长默认 10 秒,暂停位置默认 0(RollingComponentSettings.cs)。

时钟、天气图标、时间线色块不做动画:它们是高频更新的信息元素,任何过渡都会让「时间在走」变得可感知延迟。源码只在字号/颜色上做静态样式(ClockComponent.axaml:14、WeatherComponent.axaml:261-305)。

编号 名称 触发 属性 起止值 时长 曲线 延迟/交错 实现方式 证据
E1 滚动组件滚动 内容宽于容器 translateX 0 → -(W+offset) → 容器宽 → 0 (暂停+滚动) 秒;速度 40px/s 线性 暂停 10s(默认) WAAPI 关键帧无限循环 Controls/Components/RollingComponent.axaml.cs:85-135;RollingComponentSettings.cs(SpeedPixelPerSecond=40、PauseSeconds=10)
E2 滚动组件重置 停止滚动/规则命中 translateX 归零 1s(空动画占位) 线性 无 用 1s 静止关键帧覆盖旧动画 RollingComponent.axaml.cs:137-157
E3 滚动组件停止规则 规则命中 滚动状态 滚动 → 停止 即时 — 无 取消动画并归位 RollingComponent.axaml.cs:171-179
E4 滑动组件切换 选中项变化 translateY、opacity -40→0 / 0→40;0↔1 250ms (0.65,0,0.35,1) 无 CSS animation forward Controls/Components/SlideComponent.axaml:69-107
E5 滑动组件禁用动画 设置关闭动画 — 立即切换 0ms — — 关闭时直接切 display SlideComponent.axaml:51-53
E6 倒计时进度 计时更新 进度值 旧 → 新 50ms(同 A12) 线性 无 CSS transition;降低精度时按 max(10, 总秒/500) 取整 CountDownComponent.axaml.cs:288-295;Settings.cs:850
E7 时钟秒变化 秒变化 文本内容 — 无动画 — — 直接替换文本(不要做数字翻转动画) ClockComponent.axaml:14(仅字号)
E8 天气图标切换 天气数据更新 图标字形 旧 → 新 无动画 — — 直接替换(4 套模板,见 part 15) Controls/WeatherIcons/*.axaml
E9 时间线色块 时间点类型 颜色/高度 类型色 无动画 — — 静态 Themes/Generic.xaml:65-128
E10 时间线装饰器出现 悬停/选中 透明度 0 → 1 200ms【推断】 默认 无 由行为控制(TimeLineListItemBehavior),Web 用 CSS transition Themes/Generic.xaml:190-290
E11 提醒遮罩分块绘制 遮罩打开 5 段平行四边形宽度 0 → 全宽 同 B1 同 B1 同 B1 canvas 绘制(每帧按进度重绘) SlantedMaskControl.cs:273-343

14.6 F. ClassHelper 既有动效(必须复用,不得重造)

ClassHelper 已经沉淀了一套完整动效体系,必须原样复用,只在「ClassIsland 特有场景」上做增量:

既有能力 位置 复用方式
动效 token 注入 packages/shared/src/motion.ts → 两端 composables/motion.ts 在 app.mount() 之前写入 :root(首帧即就位,无 FOUC)
弹簧曲线转 CSS linear() motion.ts:152-164 直接调用 springLinearEasing(),禁止手写采样点
减少动态效果 composables/motion.ts:53-72 主界面/管理中心统一使用 useReducedMotion()
悬停能力检测 composables/motion.ts:81-99 所有 hover-only 效果必须先过 useHoverCapable()(教室触摸屏幽灵 hover)
数字滚动 composables/motion.ts:129-167 倒计时/统计数字直接复用 useCountUp()
错峰入场 composables/motion.ts:175-179 列表/卡片直接复用 useStaggerIn()
整页换肤 composables/motion.ts:201-237 应用主题切换复用 useThemeReveal()

硬约束:不得在岛渲染进程里引入主渲染进程的 --ch-* 动效 token(ClassHelper 明确「岛是另一个渲染进程,刻意不参与这里的动效体系」,motion.ts:29-31)。岛必须使用自己的 --island-* 与 SpringValue。

编号 名称 触发 属性 起止值 时长 曲线 延迟/交错 实现方式 证据
F1 岛尺寸弹簧 模式/尺寸变化 width、height、radius 当前 → 目标 由物理积分决定 弹簧 stiffness=0.10、damping=0.68 无 每帧 SpringValue.step(),60fps 单位 ClassHelper/src/island/spring.ts:26-114;IslandApp.vue:216-322
F2 岛换向限速 目标方向反转 velocity 反向保留 35%,帧位移 ≤ 剩余 20% 每帧 — 无 redirectVelocity spring.ts:66-82
F3 岛零过冲钳制 越过目标 value、velocity 吸附目标、速度清零 即时 — 无 step() 内判定 spring.ts:100-107
F4 岛淡入(窗口) 窗口显示 窗口 opacity 当前 → 目标 每帧 +0.16,16ms 步进(约 60–110ms) 线性 无 主进程 setOpacity 定时步进 src/main/island.ts:1674-1691
F5 岛淡出(窗口) 窗口隐藏 窗口 opacity 当前 → 0 每帧 -0.2,16ms 步进(约 50–80ms) 线性 无 主进程 setOpacity,到 ≤0.02 后 hide() island.ts:1693-1715
F6 岛显示方式 显示 焦点 showInactive() 不抢焦点 即时 — 无 主进程 island.ts:1426,1458,1490
F7 弹窗弹簧入场 弹窗打开 transform、opacity translateY(-12px) scale(.97) → 0/1 SPRING_PANEL 自然落定(约 463ms) 弹簧 linear() 或 cubic-bezier(.16,1,.3,1) 回退 无 CSS animation index.css:596-605,634-636;motion.ts:52,220-230
F8 弹窗快速退出 弹窗关闭 transform、opacity → translateY(-6px) scale(.985) 140ms (0.16,1,0.3,1) 无 CSS animation index.css:607-616,642-644
F9 抽屉弹簧 抽屉打开/关闭 transform、opacity 侧边 ↔ 0 SPRING_PANEL 463ms / --ch-dur-base 200ms 弹簧 / ease-out 无 CSS transition index.css:699-703
F10 浮层缩放 下拉/选择器 transform、opacity scale(.95) → 1 SPRING_SWAP 约 263ms 弹簧 无 CSS transition index.css:710-737;motion.ts:50,228-229
F11 消息条进出 消息出现/消失 transform、opacity、top/bottom 位移 弹簧 263ms / 140ms 弹簧 / ease-out 无 CSS transition index.css:739-756
F12 按钮按压 按下/抬起 transform: scale 1 → 0.97 → 1 按下 90ms;抬起 SPRING_PRESS 约 194ms【推断】 按下 ease-out;抬起弹簧 无 CSS transition(按下与基础规则分离) index.css:769-782;motion.ts:48,178
F13 卡片抬升 悬停(仅 hover 设备) transform: translateY、box-shadow 0 → -1px 263ms 弹簧/ease-out 无 CSS transition,必须包在 @media (hover:hover) and (pointer:fine) 内 index.css:791-802
F14 列表错峰入场 列表渲染 opacity、translateY 0/6px → 1/0 320ms (0.16,1,0.3,1) 每项 35ms,第 9 项起封顶 CSS animation + 内联 --ch-reveal-delay index.css:814-828;motion.ts:175-179
F15 页面块错峰入场 路由切换 opacity、translateY 0/6px → 1/0 320ms (0.16,1,0.3,1) 30ms/块,封顶 180ms CSS animation nth-child index.css:846-883
F16 整页换肤扩散 切换主题 clip-path: circle() 0px → 最远角半径 480ms(320×1.5) (0.16,1,0.3,1) 无 View Transition API + WAAPI composables/motion.ts:201-237
F17 侧栏折叠 折叠/展开 width、gap 全宽 ↔ 64–80px 260ms cubic-bezier(0.4,0,0.2,1) 无 CSS transition index.css:56-75
F18 减少动态效果 系统偏好 全部时长 → 1ms 1ms — — 全局媒体查询覆盖(加载指示器例外,放慢到 1.6s) index.css:904-930

14.7 实现方式规范(必须遵守)

  1. 优先使用合成器友好属性:只对 transform 与 opacity 启 CSS transition/WAAPI。源码中 BackgroundWidth(A5/A6)与 Margin(D18/D19)属于布局属性,Web 实现必须做等价转换:
    • 行宽过渡:背景层绝对定位于内容层之下,宽度用 clip-path: inset(0 calc(100% - var(--w)) 0 0 round <r>) 或 transform: scaleX()(配合 transform-origin: left)实现,不得对包含文字的容器直接 transition: width(会导致逐帧重排)。
    • 教程高亮:把 margin 动画换成 transform: scale()(围绕中心放大)或 inset 的 clip-path 动画,视觉等价。
  2. 弹簧动画:岛尺寸/圆角必须用 ClassHelper 的 SpringValue(F1–F3),不得改用 CSS 弹簧曲线;两条路径不能混用(ClassHelper motion.ts:29-33 明确「岛是另一个渲染进程,不参与本动效体系」)。
  3. 弹簧曲线用于 CSS 时:过冲曲线只能用于 transform/clip-path,禁止用于 opacity(会被裁剪到 [0,1],观感变成「到顶就停」)(motion.ts:148-151)。
  4. filter 禁忌:卡片类元素禁止用 filter: drop-shadow 做投影(会新建渲染表面,开合瞬间闪帧),必须用 box-shadow(classic-island.css:114-120)。
  5. will-change:只允许在动画进行中的元素上临时添加 will-change: transform, opacity,动画结束(animationend/transitionend)立即移除;禁止全局常驻。
  6. 禁止动画的属性:z-index、display(用 visibility/opacity 或先 display 后下一帧启动)、pointer-events。需要「显隐 + 动画」时用双 rAF 或 requestAnimationFrame 起手。
  7. 中断语义:任何进场动画被反向触发时必须能平滑打断(FillMode.Both/Forward 对应 animation-fill-mode;用 getAnimations().forEach(a => a.cancel()) 清理旧动画)。
  8. 统一调度:动画必须在 AnimationLevel=0 时统一短路(除主界面提醒);同一时刻同一元素不得叠加两条竞争动画。
  9. 帧率与预算:岛内动画(弹簧步进)必须跑在独立渲染进程的 rAF 上;主渲染进程的 60fps 预算为 16.7ms,任何单帧主线程任务禁止超过 8ms;动画期间不得读取 offsetWidth 等强制同步布局属性。
  10. 禁止「看起来像动画的定时器」:所有时长必须来自 14.0.3 常量表,不得在组件里写 300、0.3s 之类字面量。

14.7.1 几何动画的 FLIP 流程(必须按此实现)

源码大量动画布局属性(宽度、间距、边距),Web 端必须用 FLIP 换算成合成器动画:

  1. First:在状态改变前读取元素 getBoundingClientRect() 并缓存(只在必要时读一次,避免强制同步布局)。
  2. Last:改完 class/数据后,下一帧再读一次新几何。
  3. Invert:计算 dx = old.left - new.left、dy = old.top - new.top、sx = old.width / new.width,对元素设置 transform: translate(dx,dy) scale(sx) 并临时关掉 transition。
  4. Play:强制一次重排(读 offsetHeight 或 getBoundingClientRect),再恢复 transition 并清空 transform,让浏览器从「反演位置」过渡到「最终位置」。

FLIP 的硬性要求:整个流程必须在同一帧内完成读取与写入的分离(先批量读、再批量写),否则会触发多次回流;对列表批量移动时必须逐个元素做,不得在父容器上做 scale(会把文字一起拉伸)。

14.7.2 行宽过渡的推荐实现(替换源码的 BackgroundWidth 动画)

源码动画的是背景层 Width(FluentTheme/Styles.axaml:81、ClassicTheme/Styles.axaml:63),因为背景层只承载底色与描边,不承载文字。Web 端必须照此分层:

<div class="island-line" style="--line-w: 420px; --line-r: 8px">
  <div class="island-bg"></div>   <!-- 绝对定位;只动画这一层 -->
  <div class="island-content">…</div>
</div>
.island-bg{
  position:absolute; inset:0 auto 0 0; width:var(--line-w);
  border-radius:var(--line-r); background:var(--ci-island-bg);
  transition: width var(--ci-line-w-dur) var(--ci-line-w-ease);
  will-change: width; /* 仅动画期间由 JS 加上 */
}
  • Fluent:--ci-line-w-dur:300ms; --ci-line-w-ease:cubic-bezier(.65,0,.35,1)。
  • 经典:--ci-line-w-dur:550ms; --ci-line-w-ease:cubic-bezier(0,1,0.5,1.1)。
  • 内容层宽度变化必须瞬时(不加过渡),否则文字会在动画中抖动。
  • 背景层宽度变化会触发布局,但因为它只有底色、且 contain: strict 可限制影响范围,实测可接受;若追求纯合成器动画,可改用 clip-path: inset(0 calc(100% - var(--line-w)) 0 0 round var(--line-r)),但圆角与描边的贴合需要额外验证。

14.7.3 斜切遮罩的 canvas 实现要点

若选择 canvas 复刻 SlantedMaskControl:

  • 每个区段的几何:中心点 rCenter、当前宽度 currentW = fullW × progress、斜切偏移 offset = tan(30°) × h(SlantedMaskControl.cs:281-343)。
  • 绘制顺序:区 0/4(两端)→ 区 1/3 → 区 2(中间),与 B1 的交错延迟一致。
  • 进度驱动:requestAnimationFrame 中累加时间,按 progress = easeOutQuart(t) 重算并 ctx.clearRect + 重绘;RegionDurationMs=360、StageStaggerMs=120 必须可配置(主题可覆盖,见 part 09 的 --ci-island-mask-*)。
  • 绘制完成后必须停止 rAF,不得常驻渲染循环;遮罩关闭时按 B5–B7 反向播放。
  • 若用 DOM/SVG 实现,5 个 polygon 的 points 用 CSS 变量驱动 clip-path,但注意 clip-path 的百分比无法表达斜切角,必须用 path() 或 polygon() 的绝对像素值并在 resize 时重算。

14.7.4 动效的测试钩子(必须提供)

为便于验收,实现必须暴露以下测试接口(仅开发构建可用):

  • window.__ciMotion.getState(id):返回某动画的 {playing, elapsed, duration, easing}。
  • window.__ciMotion.list():返回当前所有运行中动画的 id 与元素选择器。
  • window.__ciMotion.setLevel(0|1|2):运行时切换动画级别。
  • window.__ciMotion.disable():等价 prefers-reduced-motion。

这些钩子必须不影响生产构建(通过 import.meta.env.DEV 或独立 dev 入口注入)。

14.8 与源码的差异说明(必须写进实现注释)

  • 源码用 Avalonia Transitions(属性级隐式动画)与 Animation(关键帧)两套机制;Web 端对应 transition 与 @keyframes/WAAPI。二者的「延迟 + 填充模式」语义必须一一对应,特别是 FillMode="Both"(对应 animation-fill-mode: both),漏掉会在延迟期间闪出终态(StackPanelIntroAnimation.axaml:22、FluentTheme/Styles.axaml:332)。
  • 源码 ClassicTheme 的行宽过渡带过冲(1.10),这是「经典主题手感」的一部分,不得为了「统一」改成 Fluent 的曲线。
  • 源码 SlantedMaskControl 用 canvas 逐帧重绘 5 段平行四边形(SlantedMaskControl.cs:273-343),Web 端可以用 canvas 2D 或 5 个 clip-path 的多边形,但分块权重必须保持 [0.0, 0.1, 0.2, 0.4, 0.2, 0.1](SlantedMaskControl.cs:298),且斜切角固定 60°(偏移 tan(30°)·h,:284)。
  • 源码的时间轴用 DispatcherTimer(25ms) 驱动列表项逐个入场(StackPanelIntroAnimationBehavior.cs:74-90);Web 端用 setTimeout 链或 requestAnimationFrame 时间片,间隔仍然 25ms,不得改成 CSS animation-delay 批量(会与「列表顺序变化」脱节)。

14.8.1 性能预算与降级矩阵

场景 目标帧率 单帧主线程预算 降级顺序 说明
岛弹簧(独立渲染进程) 60fps 该进程 8ms ① 降低步进频率到 30fps ② 关闭圆角形变只动尺寸 ③ 直接落位 必须在独立进程,不得与主界面共用事件循环
提醒遮罩(canvas 重绘) 60fps 4ms ① 减到 3 段 ② 降到 30fps ③ 改为纯透明度过渡 重绘面积大,必须先算好路径再绘制
列表错峰入场 60fps 8ms ① 只动画 opacity ② 减少同屏项数 ③ 直接显示 禁止对每项做 FLIP
窗口级淡入淡出(主进程) 30fps 足够 — 直接落位 主进程 setOpacity 步进不占渲染线程
整页换肤(View Transition) 60fps 16ms ① 缩短到 320ms ② 关掉圆形扩散改交叉淡入 ③ 直接切换 文档不可见时必须同步切换(motion.ts:195-200)

降级必须是自动的:连续 30 帧低于 45fps 时自动降一级,并在 5 秒后尝试恢复;降级不得改变最终视觉终态,只改变过程。

14.8.2 可访问性要求

  1. 减少动态效果:必须响应系统偏好,且提供应用内「动画级别=禁用」作为等效手段(两者任一命中即降级)。
  2. 不得用闪烁传达信息:任何闪烁频率必须低于 3Hz;源码没有任何闪烁动画,实现也不得新增。
  3. 动画不得是唯一反馈:按钮按压、开关切换等必须同时有颜色/状态变化,不能只靠缩放。
  4. 焦点不丢:动画期间焦点元素不得被移除;抽屉/弹窗关闭后焦点必须回到触发按钮。
  5. 文字可读:动画过程中文字不得低于 4.5:1 对比度(遮罩淡入时文字必须等遮罩底色到位后再出现,源码用延迟 260ms 实现,见 B2)。
  6. 超大字号:用户把 4 档字号调到 40px 时,所有含固定高度的动画容器必须允许内容溢出或自适应,不得裁切文字。

14.9 验收标准

  1. 逐条对照:本节 A1–A12、B1–B20、C1–C12、D1–D26、E1–E11、F1–F18 共 99 条动效全部实现;抽测任意 10 条,时长误差 ≤ 1 帧(16.7ms),曲线肉眼无差异。
  2. 动画级别:切到「禁用」后,抽屉/Popup/列表错峰动画不再播放(主界面提醒动画保留);切到「华丽」后行淡入与组件淡入出现;切回「标准」后二者消失。
  3. 减少动态效果:系统开启 prefers-reduced-motion 后,全部动画在 1 帧内完成,页面不出现闪烁或「终态先行」;加载指示器仍在转(放慢至 1.6s)。
  4. 打断测试:连续快速开关同一抽屉/Popup/遮罩 10 次,无卡死、无重影、无「停在半路」;遮罩快速 in→out 切换后元素状态正确复位。
  5. 性能:动画期间主线程帧耗时 P95 < 8ms、掉帧 < 2 帧/秒;岛弹簧动画在独立渲染进程内不掉帧(对照 ClassHelper 的实现位置)。
  6. 填充模式:所有带延迟的动画在延迟期间不显示终态(检查 D12、D18、B2、C6)。
  7. 曲线一致性:--ci-ease-* 只在一处定义;grep 组件样式不得出现 cubic-bezier( 字面量(动画表定义处除外)。
  8. 层叠与裁剪:遮罩动画期间文字不被裁切、圆角外的像素不泄漏;分体主界面下每行动画独立,不出现整排一起抖动。
  9. 焦点与命中:动画期间点击穿透状态正确(非交互态不拦截点击,交互态可点);窗口淡出后不残留不可见但可点击的层。
  10. 可复现:同一操作重复 20 次,动画起始/结束时刻的视觉状态一致,无累积误差(尤其是 A5/A6 的行宽与 E1 的滚动偏移)。