feat(m192): 动效帧泵——design motion 令牌接进 iced 的有界帧订阅 + 首批 3 条入场动效
令牌早就在 `crates/design/src/motion/` 里齐着(16 档时长 / 可采样缓动 / 弹簧二维表 / MotionScale 降级),但适配层从没消费过,所以一切状态变化都是硬切。本次补的正是那条中间层: 新增 `components/motion/`(时间轴,不是控件家族) - `pump.rs`:`Slot`(boot/screen/detail 三个互斥触发点)× `Spec`(时长档 × 缓动档, **唯一一张表**,页面拿不到「给我 250ms」这种口子,D31)→ 进度采样;时间轴按 16ms 结算 (≈60 次/秒),到期轨道**当场删除**。 - `mod.rs`:进程内单时间轴(thread-local;iced 在事件循环线程上串行调 update/view, 与 `App::update(&mut self)` 同一条纪律,测试也因此天然隔离)+ `enter`/`shifted` 两个页面入口。 - **默认 NONE**:只有产品 `on_boot` 按档位打开;`pages_demo` 这类截图 harness 从不打开。 接线(都接在既有那条链上,不另开循环) - `App::needs_frames` 多一条「有轨道」分支 → 与截图 / Busy / toast / watch_until 共享**同一个布尔**, `window::frames()` 在源码里仍只有一处(守卫:双订阅就红)。 - `on_frame` 每帧推进时间轴;`on_heartbeat` 不变(有帧时不推进)。 - 定档策略集中在 `update/boot_env.rs::motion_scale`:截图档 NONE(D30 出图可复现)、 CPU 渲染档 HALF、GPU 档 STANDARD,`ULL_MOTION` 可强制。 首批 3 条动效(`pages::render` 一个漏斗,静息恒为 (0,0) ⇒ 既有截图基线一张都不用重出) - 首屏进入 16px / 换页 12px(纵向) / 列表↔详情 8px(横向); 时长 400/300/350ms 全部取自 `DurationToken`,曲线 `Easing::Standard` (`EmphasizedDecelerate` 把 75% 位移压在 51ms 内走完,12px 的整页平移会像跳一格)。 守卫与取证 - 11 条帧泵算术守卫 + 4 条接线守卫(含「NONE 档帧采样次数必须为 0」与「帧订阅源码只出现一处」); - 5 条变异证明全部复现(删退订 / 删 NONE 短路 / 删 needs_frames 分支 / 删 16ms 闸门 / 加第二条帧订阅 ⇒ 对应用例红); - 多帧序列图 12 张在 `DevDocs/artifacts/m192/`(t=0 / 半程 / 末 + 静息基线, `--diff --diff-scan` 实测换页 12px/6px、详情 8px); - 空闲实证:`ULL_TICK_TRACE=1` 跑真窗口,帧订阅窗口一过 `clock=idle-1hz` 且 `frames=` 冻结。
This commit is contained in:
1 parent
393d6f188f
commit
a1876a08c0
11 files changed
+1052
-8
No files matched your search
@@ -1,8 +1,11 @@
|
||||
//! 启动期的取证环境变量解析(`ULL_START_ROUTE`)。
|
||||
//! 启动期的取证环境变量解析(`ULL_START_ROUTE` / 动效三个开关)。
|
||||
//!
|
||||
//! 从 `app/update.rs` 原样搬出(那个文件在 M105/M106/M109/M113 之后贴着 400 行硬线):
|
||||
//! **纯函数、无状态**,只是取值表长到了七条。
|
||||
//! **纯函数、无状态**,只是取值表长到了十条。M192 在这里加的是动效那一组
|
||||
//! ([`motion_scale`] / [`env_motion_pin`] / [`env_motion_fixture`]):档位策略只有这一处,
|
||||
//! 帧泵自己不读环境变量。
|
||||
|
||||
use unlucky_design::motion::MotionScale;
|
||||
use unlucky_presenter::router::Route;
|
||||
|
||||
/// 取证开关 `ULL_START_ROUTE` 的取值解析(`settings` / `instances` / `download` / `content` / `logs`)。
|
||||
@@ -25,6 +28,84 @@ pub(super) fn env_start_route() -> Option<Route> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 动效档位与相位钉住(M192):帧泵**只在产品的 `on_boot` 里定档**。
|
||||
///
|
||||
/// ## 为什么档位在这里算
|
||||
///
|
||||
/// 三个输入只有启动这一拍齐全:①是不是截图通道(`--screenshot`)②实际渲染后端
|
||||
/// (`backend::effective_kind()`,不是 `--renderer` 的请求值)③`ULL_MOTION` 的强制值。
|
||||
/// 帧泵自己(`components::motion`)不读环境变量,它只接受一个 [`MotionScale`]——这样动效策略
|
||||
/// 只有一处,测试也不必摆环境变量。
|
||||
///
|
||||
/// | 条件 | 档位 | 为什么 |
|
||||
/// |---|---|---|
|
||||
/// | `ULL_MOTION=off` | `NONE` | 一键关(排障 / 想量基线时用) |
|
||||
/// | 截图通道,且没给 `ULL_MOTION_AT` | `NONE` | D30:出图不许有动画(否则第 3 帧抓到中间态,图就不可复现) |
|
||||
/// | CPU 渲染(`tiny-skia`) | `HALF` | 时长减半 = 帧数减半。位移是**布局量**,CPU 档最贵的就是逐帧重排 |
|
||||
/// | 其余(`wgpu`) | `STANDARD` | —— |
|
||||
/// | `ULL_MOTION=half` / `full` | 强制 | 覆盖上表(出图矩阵要在 GPU 上量同一段动效) |
|
||||
///
|
||||
/// 「减少动效」的用户开关**还不存在**(`core/src/setting` 里没有 animation key,见报告 follow-up),
|
||||
/// 所以本函数只认上面这些来源。
|
||||
pub(super) fn motion_scale(screenshot: bool, gpu: bool, pinned: bool) -> MotionScale {
|
||||
match std::env::var("ULL_MOTION").ok().as_deref().map(str::trim) {
|
||||
Some("off") | Some("none") => return MotionScale::NONE,
|
||||
Some("half") => return MotionScale::HALF,
|
||||
Some("full") => return MotionScale::STANDARD,
|
||||
_ => {}
|
||||
}
|
||||
if pinned {
|
||||
// 取证相位优先于「截图必须静」,否则多帧序列图一张都出不来。
|
||||
return MotionScale::STANDARD;
|
||||
}
|
||||
if screenshot {
|
||||
return MotionScale::NONE;
|
||||
}
|
||||
if gpu {
|
||||
MotionScale::STANDARD
|
||||
} else {
|
||||
MotionScale::HALF
|
||||
}
|
||||
}
|
||||
|
||||
/// `ULL_MOTION_AT=<ms>`:把动画时钟钉在某一毫秒(多帧序列图取证)。
|
||||
///
|
||||
/// 语义与 `ULL_APPEARANCE_*` / `ULL_DIALOG_FIXTURE` 一致:只影响这一次进程。没有它,第 3 帧落在
|
||||
/// 动画的哪个相位取决于这台机器的渲染速度 —— 那样「0% / 50% / 100% 三张图」是抓阄抓出来的。
|
||||
pub(super) fn env_motion_pin() -> Option<f64> {
|
||||
std::env::var("ULL_MOTION_AT")
|
||||
.ok()?
|
||||
.trim()
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
.filter(|ms| ms.is_finite() && *ms >= 0.0)
|
||||
}
|
||||
|
||||
/// `ULL_MOTION_FIXTURE`:第 1 帧之后**真的**做一次状态变化(多帧序列图靠它)。
|
||||
///
|
||||
/// 为什么要制造变化:页面主体的入场只在「内容标识变了」时起跑(`motion::enter`),而截图通道
|
||||
/// 从第一帧起画的就是同一页——不制造一次真实的路由变化,就没有可拍的动效。
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) enum MotionFixture {
|
||||
/// 跳到一个一级页(走「换页」那一档)。
|
||||
Route(Route),
|
||||
/// 「列表 → 详情」:先回列表(第 1 帧),再点进第一条实例(第 2 帧)。
|
||||
Detail,
|
||||
}
|
||||
|
||||
/// `ULL_MOTION_FIXTURE=<instances|download|logs|settings|content|detail>`。
|
||||
pub(super) fn env_motion_fixture() -> Option<MotionFixture> {
|
||||
Some(match std::env::var("ULL_MOTION_FIXTURE").ok()?.trim() {
|
||||
"instances" => MotionFixture::Route(Route::Instances),
|
||||
"download" => MotionFixture::Route(Route::Download),
|
||||
"logs" => MotionFixture::Route(Route::Logs),
|
||||
"settings" => MotionFixture::Route(Route::Settings),
|
||||
"content" => MotionFixture::Route(Route::Content),
|
||||
"detail" => MotionFixture::Detail,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// 把启动那一拍的**事实**写进启动器自己的日志(M187 / r1 复审 P2-4)。
|
||||
///
|
||||
/// ## 为什么这一拍只能「先记、后落」
|
||||
|
||||
Reference in new issue
Block a user