feat(motion2): 右栗折叠过渡 + 状态层 hover/pressed 过渡
A2 侧边栏(右栗 AI 栏)折叠/展开: - 帧泵第三条轨道(pump/rail):展开 medium1 250ms + Emphasized、收起 short4 200ms + EmphasizedAccelerate;拆出档走 snap(不做过渡);MotionScale::NONE 下不插轨道 - 宽度在 metrics::rail_width 逐帧插值;面板内容按目标宽排版 + 按动画宽裁切 (不重排 / 不溢出),宽度回到 48 时换成图标条(图标贴右端,位置不跳) - pages/agent/mod.rs 的展开档面板纯搬移到 panel.rs(371 → 289 行) A8 状态层过渡: - interaction/layer:令牌化的进度时钟(short1 50ms),NONE 当帧落终值 - interaction/hover:取样控件把起跑搬到 Widget::update(早于订阅表求值) - 按钮五式 + IconButton + 导航项药丸统一走 interaction::layer_color (没有取样控件时逐位退回旧口径,出图基线不变) - components/motion/mod.rs 的 needs_frames / advance 各加一句委托, 帧订阅仍然只有一处判断点(app/subscription.rs 未改)
This commit is contained in:
1 parent
c4706c759c
commit
b61f342431
22 files changed
+1837
-145
No files matched your search
@@ -64,11 +64,14 @@ use unlucky_design::color::{Role, StateLayer};
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
mod hover;
|
||||
mod tracker;
|
||||
|
||||
// 本 mission 没有自绘控件,所以这条再导出在 crate 内暂时无人使用;
|
||||
// 它是给四条家族线(Slider / Switch 轨道要自绘)的公共入口,路径与拆分前一致。
|
||||
#[allow(unused_imports)]
|
||||
/// 状态层**过渡的时间轴**(M257 / A8):hover / pressed 按令牌时长插值,不再是硬切。
|
||||
pub mod layer;
|
||||
|
||||
// 事件 → 态的**状态机**在 [`tracker`]:M257 起它有真消费者——[`hover`] 的取样控件。
|
||||
pub use hover::tracked;
|
||||
pub use tracker::Tracker;
|
||||
|
||||
// 本模块会被四条家族线各扩一次:加东西前先看行数余额(单文件 ≤400,AGENTS.md §3-1)。
|
||||
@@ -151,6 +154,40 @@ pub fn overlay_color(ui: &Ui, state: Interaction, content: Role, container: Role
|
||||
.map(|layer| ui.state_over(layer, content, container))
|
||||
}
|
||||
|
||||
/// 这一帧的状态层颜色(**组件唯一入口**,A8 起带时间轴)。
|
||||
///
|
||||
/// 三种情形,前两种都**逐位等于** M257 之前的输出:
|
||||
///
|
||||
/// | 情形 | 返回 | 为什么 |
|
||||
/// |---|---|---|
|
||||
/// | 没有取样控件(gallery 强制态 / 未包装路径) | [`overlay_color`] | 取证图与旧基线一字不差 |
|
||||
/// | 有取样控件且过渡已到位(`p >= 1`) | 目标态的颜色 | 静息与终态都是**精确**的令牌求值 |
|
||||
/// | 过渡中(`0 < p < 1`) | 两态不透明色的插值 | 中间帧既不是起点也不是终点(可判别的中间态) |
|
||||
///
|
||||
/// 中间态为什么能插值而不产生半透明:两端都是 `state_over` 合成的**不透明**色,线性插值的结果
|
||||
/// 也不透明(D27:GPU/CPU 两档逐像素一致)。`back` 是状态层合成的底(填充按钮 = 容器角色,
|
||||
/// 透明容器 = [`Base`]),当前态不叠层时以它为端点色(那正是「没画状态层」时看到的颜色)。
|
||||
pub fn layer_color(ui: &Ui, state: Interaction, content: Role, back: Role) -> Option<Color> {
|
||||
let Some((from, to, p)) = layer::current() else {
|
||||
return overlay_color(ui, state, content, back);
|
||||
};
|
||||
let source = overlay_color(ui, from, content, back);
|
||||
let target = overlay_color(ui, to, content, back);
|
||||
if p >= 1.0 {
|
||||
return target;
|
||||
}
|
||||
if p <= 0.0 {
|
||||
return source;
|
||||
}
|
||||
// 端点缺失(静息态不叠层)时用**底**补一个不透明色:那正是用户眼里的「没有状态层」。
|
||||
let backdrop = ui.color(back);
|
||||
Some(layer::blend(
|
||||
source.unwrap_or(backdrop),
|
||||
target.unwrap_or(backdrop),
|
||||
p,
|
||||
))
|
||||
}
|
||||
|
||||
mod base;
|
||||
mod disabled;
|
||||
|
||||
@@ -235,6 +272,68 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// **没有取样控件时,[`layer_color`] 逐位等于 M257 之前的 [`overlay_color`]**。
|
||||
///
|
||||
/// 抓得住:有人把时间轴接进求值路径时顺手改了静息/终态的颜色(D30 的旧基线会漂)。
|
||||
/// 抓不住:过渡中间帧(那是下一条用例与 `layer::tests` 的事)。
|
||||
#[test]
|
||||
fn layer_color_without_a_sampler_is_bit_identical_to_overlay_color() {
|
||||
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||
let ui = ui(flavor);
|
||||
for state in Interaction::ALL {
|
||||
for (content, back) in [
|
||||
(Role::OnPrimary, Role::Primary),
|
||||
(Role::OnSurface, Role::Surface),
|
||||
(Role::OnSurfaceVariant, Role::SurfaceContainerLow),
|
||||
] {
|
||||
assert_eq!(
|
||||
layer_color(&ui, state, content, back),
|
||||
overlay_color(&ui, state, content, back),
|
||||
"{flavor:?}/{state:?}/{content:?} 必须逐位退回旧口径"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// **过渡中间帧:颜色落在两端之间,且不透明**(D27:不许把 α 交给渲染器)。
|
||||
///
|
||||
/// 抓得住:①中间帧被写成硬切(`p` 被忽略 ⇒ 起点色或终点色,`assert_ne` 红);
|
||||
/// ②有人为了「淡出」改用 `Color { a: 0.5 }`(末尾的「必须不透明」红)。
|
||||
/// 抓不住:观感(8%/10% 的差在图上只有几级色阶,好不好看得看图)。
|
||||
#[test]
|
||||
fn a_running_transition_yields_an_opaque_colour_between_the_two_ends() {
|
||||
use super::layer;
|
||||
|
||||
let ui = ui(SchemeFlavor::Light);
|
||||
layer::reset();
|
||||
let resting = overlay_color(&ui, Interaction::Enabled, Role::OnSurface, Role::Surface);
|
||||
let hovered = overlay_color(&ui, Interaction::Hovered, Role::OnSurface, Role::Surface)
|
||||
.expect("hover 有颜色");
|
||||
|
||||
let mut state = layer::Layer::new();
|
||||
state.retarget(
|
||||
Interaction::Hovered,
|
||||
unlucky_design::motion::MotionScale::STANDARD,
|
||||
);
|
||||
layer::advance_by(16.0);
|
||||
|
||||
let mid = layer::with_current(state, || {
|
||||
layer_color(&ui, Interaction::Hovered, Role::OnSurface, Role::Surface)
|
||||
})
|
||||
.expect("过渡中必须有颜色");
|
||||
|
||||
assert_eq!(mid.a, 1.0, "中间帧也必须不透明(D27)");
|
||||
assert!(
|
||||
resting.is_none(),
|
||||
"前提:静息态本来不叠层(透明容器的情形)"
|
||||
);
|
||||
assert_ne!(mid, hovered, "中间帧不许等于终点色(那就是硬切)");
|
||||
let backdrop = ui.color(Role::Surface);
|
||||
assert_ne!(mid, backdrop, "中间帧也不许还停在起点色的底上");
|
||||
layer::reset();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_is_weaker_than_pressed_on_the_same_container() {
|
||||
let ui = ui(SchemeFlavor::Light);
|
||||
|
||||
Reference in new issue
Block a user