feat(design): 加 unlucky-design 令牌 crate(M3 色彩/排版/形状/动效/图标)
D26 把视觉基线锁到 Material 3 Expressive,本 crate 是「UI 整体性」的地基: 令牌唯一真源,后续 iced 页面只消费不新增。零第三方依赖、零 UI 框架依赖 (cargo tree -p unlucky-design --edges normal 只有它自己一行)。 - 色彩:49 个 M3 语义角色 × light/dark,种子色派生 6 条 tone 调色板; tone 轴严格用 CIELAB L*(偏差 ≤1),色相/彩度用 OKLCh 近似 CAM16 并在 模块文档里写明口径与已知偏差(不宣称是标准 HCT 动态取色) - 对比度:26 组文字对 × 6 组种子 × 2 模式全部 ≥4.5:1;3 条例外显式登记 (Outline 非文字 3:1、OutlineVariant 装饰、SurfaceVariant 是容器色) - 排版:M3 type scale 15 档 + 三平台字体族回退栈(只写族名,不内嵌受限字体) - 形状/高程/动效:Expressive 形状刻度 + shape morph 数据、elevation 级别与 surface container 映射、时长/缓动/弹簧(纯函数可采样) - 图标:24 个从 Slint 版迁移的 SVG path + 4 个新增,IconKind 冻结只可末尾追加; 写明 Slint 的 stroke-width 是绝对像素、SVG 是 viewBox 用户单位的语义差异, 换算封装成 Icon::stroke_width - 组件档位表:20 个组件的形状/高程/字体/动效一次取全 TODO(M63):elevation shadow 的 blur/offset/alpha 与四组弹簧参数是占位值, 接口先定、数值待玻璃视觉规格表(tower 已通告边界)。
This commit is contained in:
1 parent
fef0922073
commit
65ca0ed29a
25 files changed
+4622
-1
No files matched your search
@@ -0,0 +1,305 @@
|
||||
//! 动效令牌:时长、缓动(可采样)、M3 Expressive 弹簧参数。
|
||||
//!
|
||||
//! 三件东西分开表达,因为适配层要的接口不同:
|
||||
//! - [`DurationToken`]:M3 的 short/medium/long/extra-long 十六档,给「固定时长 + easing」用;
|
||||
//! - [`Easing`]:cubic-bezier 曲线,纯函数采样(适配层自己决定帧率);
|
||||
//! - [`Spring`](见 [`spring`]):Expressive 的物理参数(stiffness/damping/mass),
|
||||
//! 可用于实时求解或预估时长。
|
||||
//!
|
||||
//! **CPU / 减少动效路径**:把 [`MotionScale`] 设成 [`MotionScale::NONE`],
|
||||
//! 所有时长归零、`Easing::sample` 仍返回 0/1 端值(适配层按 `t >= 1` 直接落终态)。
|
||||
|
||||
mod spring;
|
||||
|
||||
pub use spring::{
|
||||
MotionScale, Spring, SPRING_EFFECTS_DEFAULT, SPRING_EFFECTS_FAST, SPRING_SPATIAL_DEFAULT,
|
||||
SPRING_SPATIAL_FAST,
|
||||
};
|
||||
|
||||
/// 动效时长档位(M3 动效令牌,单位毫秒)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum DurationToken {
|
||||
/// 50ms:最小状态回弹。
|
||||
Short1,
|
||||
/// 100ms:小图标/勾选。
|
||||
Short2,
|
||||
/// 150ms:开关、小尺寸 fade。
|
||||
Short3,
|
||||
/// 200ms:按钮状态、hover。
|
||||
Short4,
|
||||
/// 250ms:小卡片展开。
|
||||
Medium1,
|
||||
/// 300ms:导航指示条、tab 切换。
|
||||
Medium2,
|
||||
/// 350ms:卡片展开、列表重排。
|
||||
Medium3,
|
||||
/// 400ms:面板开合。
|
||||
Medium4,
|
||||
/// 450ms:页面切换。
|
||||
Long1,
|
||||
/// 500ms:大面板进出。
|
||||
Long2,
|
||||
/// 550ms:全屏浮层。
|
||||
Long3,
|
||||
/// 600ms:对话框进出。
|
||||
Long4,
|
||||
/// 700ms:结构化过渡。
|
||||
ExtraLong1,
|
||||
/// 800ms:结构化过渡。
|
||||
ExtraLong2,
|
||||
/// 900ms:结构化过渡。
|
||||
ExtraLong3,
|
||||
/// 1000ms:整屏形变(shape morph 的慢档)。
|
||||
ExtraLong4,
|
||||
}
|
||||
|
||||
impl DurationToken {
|
||||
/// 毫秒值。
|
||||
pub const fn ms(self) -> u32 {
|
||||
match self {
|
||||
DurationToken::Short1 => 50,
|
||||
DurationToken::Short2 => 100,
|
||||
DurationToken::Short3 => 150,
|
||||
DurationToken::Short4 => 200,
|
||||
DurationToken::Medium1 => 250,
|
||||
DurationToken::Medium2 => 300,
|
||||
DurationToken::Medium3 => 350,
|
||||
DurationToken::Medium4 => 400,
|
||||
DurationToken::Long1 => 450,
|
||||
DurationToken::Long2 => 500,
|
||||
DurationToken::Long3 => 550,
|
||||
DurationToken::Long4 => 600,
|
||||
DurationToken::ExtraLong1 => 700,
|
||||
DurationToken::ExtraLong2 => 800,
|
||||
DurationToken::ExtraLong3 => 900,
|
||||
DurationToken::ExtraLong4 => 1000,
|
||||
}
|
||||
}
|
||||
|
||||
/// 稳定字符串名(`short1` … `extra-long4`),写配置与日志用。
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
DurationToken::Short1 => "short1",
|
||||
DurationToken::Short2 => "short2",
|
||||
DurationToken::Short3 => "short3",
|
||||
DurationToken::Short4 => "short4",
|
||||
DurationToken::Medium1 => "medium1",
|
||||
DurationToken::Medium2 => "medium2",
|
||||
DurationToken::Medium3 => "medium3",
|
||||
DurationToken::Medium4 => "medium4",
|
||||
DurationToken::Long1 => "long1",
|
||||
DurationToken::Long2 => "long2",
|
||||
DurationToken::Long3 => "long3",
|
||||
DurationToken::Long4 => "long4",
|
||||
DurationToken::ExtraLong1 => "extra-long1",
|
||||
DurationToken::ExtraLong2 => "extra-long2",
|
||||
DurationToken::ExtraLong3 => "extra-long3",
|
||||
DurationToken::ExtraLong4 => "extra-long4",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 三次贝塞尔缓动曲线(CSS `cubic-bezier` 同样的参数含义)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CubicBezier {
|
||||
/// 控制点 1 的 x(0..1)。
|
||||
pub x1: f32,
|
||||
/// 控制点 1 的 y。
|
||||
pub y1: f32,
|
||||
/// 控制点 2 的 x(0..1)。
|
||||
pub x2: f32,
|
||||
/// 控制点 2 的 y。
|
||||
pub y2: f32,
|
||||
}
|
||||
|
||||
impl CubicBezier {
|
||||
/// 线性。
|
||||
pub const LINEAR: Self = Self {
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
x2: 1.0,
|
||||
y2: 1.0,
|
||||
};
|
||||
|
||||
/// 给定时间参数 `t`(0..1),返回进度(0..1,标准曲线不越界)。
|
||||
pub fn sample(self, t: f32) -> f32 {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
if t == 0.0 || t == 1.0 {
|
||||
return t;
|
||||
}
|
||||
// 二分求 x(s) = t 的曲线参数 s,再取 y(s)。
|
||||
let mut lo = 0.0_f32;
|
||||
let mut hi = 1.0_f32;
|
||||
let mut s = t;
|
||||
for _ in 0..24 {
|
||||
s = 0.5 * (lo + hi);
|
||||
let x = bezier_axis(self.x1, self.x2, s);
|
||||
if x < t {
|
||||
lo = s;
|
||||
} else {
|
||||
hi = s;
|
||||
}
|
||||
}
|
||||
bezier_axis(self.y1, self.y2, s)
|
||||
}
|
||||
}
|
||||
|
||||
/// 一维三次贝塞尔:端点固定为 0/1,控制点为 `c1`/`c2`。
|
||||
fn bezier_axis(c1: f32, c2: f32, s: f32) -> f32 {
|
||||
let u = 1.0 - s;
|
||||
3.0 * u * u * s * c1 + 3.0 * u * s * s * c2 + s * s * s
|
||||
}
|
||||
|
||||
/// M3 缓动档位(值取自 Material 动效令牌)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Easing {
|
||||
/// 线性(进度指示器)。
|
||||
Linear,
|
||||
/// 标准:起止都平滑。
|
||||
Standard,
|
||||
/// 标准减速:进场(出现在屏幕上)。
|
||||
StandardDecelerate,
|
||||
/// 标准加速:退场。
|
||||
StandardAccelerate,
|
||||
/// 强调:注意力转移的主曲线。
|
||||
Emphasized,
|
||||
/// 强调减速:强调类进场。
|
||||
EmphasizedDecelerate,
|
||||
/// 强调加速:强调类退场。
|
||||
EmphasizedAccelerate,
|
||||
}
|
||||
|
||||
impl Easing {
|
||||
/// 曲线参数。
|
||||
pub const fn curve(self) -> CubicBezier {
|
||||
match self {
|
||||
Easing::Linear => CubicBezier::LINEAR,
|
||||
Easing::Standard | Easing::Emphasized => CubicBezier {
|
||||
x1: 0.2,
|
||||
y1: 0.0,
|
||||
x2: 0.0,
|
||||
y2: 1.0,
|
||||
},
|
||||
Easing::StandardDecelerate => CubicBezier {
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
x2: 0.0,
|
||||
y2: 1.0,
|
||||
},
|
||||
Easing::StandardAccelerate => CubicBezier {
|
||||
x1: 0.3,
|
||||
y1: 0.0,
|
||||
x2: 1.0,
|
||||
y2: 1.0,
|
||||
},
|
||||
Easing::EmphasizedDecelerate => CubicBezier {
|
||||
x1: 0.05,
|
||||
y1: 0.7,
|
||||
x2: 0.1,
|
||||
y2: 1.0,
|
||||
},
|
||||
Easing::EmphasizedAccelerate => CubicBezier {
|
||||
x1: 0.3,
|
||||
y1: 0.0,
|
||||
x2: 0.8,
|
||||
y2: 0.15,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 采样:`t` 0..1 → 进度 0..1。
|
||||
pub fn sample(self, t: f32) -> f32 {
|
||||
self.curve().sample(t)
|
||||
}
|
||||
|
||||
/// 该曲线搭配的常用时长(我们的组件映射口径,非规范值)。
|
||||
pub const fn default_duration(self) -> DurationToken {
|
||||
match self {
|
||||
Easing::Linear => DurationToken::Long2,
|
||||
_ => DurationToken::Medium2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const ALL_EASINGS: [Easing; 7] = [
|
||||
Easing::Linear,
|
||||
Easing::Standard,
|
||||
Easing::StandardDecelerate,
|
||||
Easing::StandardAccelerate,
|
||||
Easing::Emphasized,
|
||||
Easing::EmphasizedDecelerate,
|
||||
Easing::EmphasizedAccelerate,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn durations_are_monotone() {
|
||||
let ladder = [
|
||||
DurationToken::Short1,
|
||||
DurationToken::Short2,
|
||||
DurationToken::Short3,
|
||||
DurationToken::Short4,
|
||||
DurationToken::Medium1,
|
||||
DurationToken::Medium2,
|
||||
DurationToken::Medium3,
|
||||
DurationToken::Medium4,
|
||||
DurationToken::Long1,
|
||||
DurationToken::Long2,
|
||||
DurationToken::Long3,
|
||||
DurationToken::Long4,
|
||||
DurationToken::ExtraLong1,
|
||||
DurationToken::ExtraLong2,
|
||||
DurationToken::ExtraLong3,
|
||||
DurationToken::ExtraLong4,
|
||||
];
|
||||
assert_eq!(ladder.len(), 16);
|
||||
for pair in ladder.windows(2) {
|
||||
assert!(pair[0].ms() < pair[1].ms());
|
||||
}
|
||||
assert_eq!(DurationToken::Short1.ms(), 50);
|
||||
assert_eq!(DurationToken::ExtraLong4.ms(), 1000);
|
||||
// 名字唯一
|
||||
let mut names = std::collections::BTreeSet::new();
|
||||
for token in ladder {
|
||||
assert!(names.insert(token.name()), "时长名重复:{}", token.name());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn easing_hits_endpoints_and_stays_in_range() {
|
||||
for easing in ALL_EASINGS {
|
||||
assert!(
|
||||
(easing.sample(0.0) - 0.0).abs() < 1.0e-6,
|
||||
"{easing:?} 起点不为 0"
|
||||
);
|
||||
assert!(
|
||||
(easing.sample(1.0) - 1.0).abs() < 1.0e-6,
|
||||
"{easing:?} 终点不为 1"
|
||||
);
|
||||
let mut prev = 0.0;
|
||||
for step in 1..=20 {
|
||||
let v = easing.sample(step as f32 / 20.0);
|
||||
assert!((-0.001..=1.001).contains(&v), "{easing:?} 采样越界:{v}");
|
||||
assert!(v + 1.0e-3 >= prev, "{easing:?} 采样非单调:{prev} → {v}");
|
||||
prev = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emphasized_decelerate_is_faster_early_than_accelerate() {
|
||||
let early_decel = Easing::EmphasizedDecelerate.sample(0.25);
|
||||
let early_accel = Easing::EmphasizedAccelerate.sample(0.25);
|
||||
assert!(early_decel > early_accel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_durations_are_registered_tokens() {
|
||||
for easing in ALL_EASINGS {
|
||||
assert!(easing.default_duration().ms() >= 250);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
//! 弹簧参数(M3 Expressive 动效的物理表达)与动效时长缩放。
|
||||
|
||||
/// 弹簧参数。
|
||||
///
|
||||
/// **口径说明**:Material 的弹簧预置是「按体感标定」的一组 token,官方没有公开统一公式
|
||||
/// (Android 侧是 `SpringForce` 参数 + 阻尼比)。[`Spring::sample`] 走的是标准二阶系统阶跃响应
|
||||
/// **闭式解**——数学是真的,参数是我们的标定值(待 M63 视觉规格对齐)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Spring {
|
||||
/// 刚度(越大越快)。
|
||||
pub stiffness: f32,
|
||||
/// 阻尼系数(临界阻尼 = 2·√(k·m))。
|
||||
pub damping: f32,
|
||||
/// 质量。
|
||||
pub mass: f32,
|
||||
}
|
||||
|
||||
impl Spring {
|
||||
/// 构造(`mass` 取 1.0 时 stiffness/damping 就是常见标定值)。
|
||||
pub const fn new(stiffness: f32, damping: f32, mass: f32) -> Self {
|
||||
Self {
|
||||
stiffness,
|
||||
damping,
|
||||
mass,
|
||||
}
|
||||
}
|
||||
|
||||
/// 自然角频率 ωn(rad/s)。
|
||||
pub fn natural_frequency(self) -> f32 {
|
||||
(self.stiffness / self.mass).sqrt()
|
||||
}
|
||||
|
||||
/// 阻尼比 ζ(1.0 = 临界阻尼;< 1 会轻微弹过)。
|
||||
pub fn damping_ratio(self) -> f32 {
|
||||
let denom = 2.0 * (self.stiffness * self.mass).sqrt();
|
||||
if denom <= 0.0 {
|
||||
1.0
|
||||
} else {
|
||||
self.damping / denom
|
||||
}
|
||||
}
|
||||
|
||||
/// 阶跃响应:`t_ms` 毫秒后的进度(0..1,欠阻尼时会轻微超过 1 = 回弹)。
|
||||
pub fn sample(self, t_ms: f32) -> f32 {
|
||||
if t_ms <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let t = t_ms / 1000.0;
|
||||
let wn = self.natural_frequency();
|
||||
let zeta = self.damping_ratio();
|
||||
if wn <= 0.0 {
|
||||
return 1.0;
|
||||
}
|
||||
if zeta < 1.0 {
|
||||
let wd = wn * (1.0 - zeta * zeta).sqrt();
|
||||
let decay = (-zeta * wn * t).exp();
|
||||
1.0 - decay * ((wd * t).cos() + (zeta * wn / wd) * (wd * t).sin())
|
||||
} else if (zeta - 1.0).abs() < 1.0e-4 {
|
||||
let decay = (-wn * t).exp();
|
||||
1.0 - decay * (1.0 + wn * t)
|
||||
} else {
|
||||
let root = (zeta * zeta - 1.0).sqrt();
|
||||
let r1 = -wn * (zeta - root);
|
||||
let r2 = -wn * (zeta + root);
|
||||
let denom = r2 - r1;
|
||||
if denom.abs() < f32::EPSILON {
|
||||
return 1.0;
|
||||
}
|
||||
1.0 - (r2 * (r1 * t).exp() - r1 * (r2 * t).exp()) / denom
|
||||
}
|
||||
}
|
||||
|
||||
/// 收敛到 `threshold`(0.1% 是常用的「停下来」判定)所需毫秒数。
|
||||
pub fn settle_ms(self, threshold: f32) -> f32 {
|
||||
let wn = self.natural_frequency();
|
||||
let zeta = self.damping_ratio();
|
||||
if wn <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let thr = threshold.clamp(1.0e-6, 0.5);
|
||||
let seconds = if zeta < 1.0 {
|
||||
let wd = (1.0 - zeta * zeta).sqrt().max(1.0e-4);
|
||||
-(thr * wd).ln() / (zeta * wn)
|
||||
} else if (zeta - 1.0).abs() < 1.0e-4 {
|
||||
-(thr).ln() / wn
|
||||
} else {
|
||||
let root = (zeta * zeta - 1.0).sqrt();
|
||||
let slow = wn * (zeta - root);
|
||||
-(thr).ln() / slow
|
||||
};
|
||||
(seconds * 1000.0).max(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 空间类快速弹簧(位移/尺寸):紧、几乎不弹。
|
||||
///
|
||||
/// TODO(M63): 取值待视觉规格(四组弹簧是我们的临时标定,M63 的动效表落地后对齐)。
|
||||
pub const SPRING_SPATIAL_FAST: Spring = Spring::new(1400.0, 76.0, 1.0);
|
||||
/// 空间类默认弹簧(面板、卡片):标准手感。
|
||||
///
|
||||
/// TODO(M63): 取值待视觉规格。
|
||||
pub const SPRING_SPATIAL_DEFAULT: Spring = Spring::new(800.0, 55.0, 1.0);
|
||||
/// 效应类快速弹簧(透明度/颜色):不带过冲。
|
||||
///
|
||||
/// TODO(M63): 取值待视觉规格。
|
||||
pub const SPRING_EFFECTS_FAST: Spring = Spring::new(3800.0, 120.0, 1.0);
|
||||
/// 效应类默认弹簧。
|
||||
///
|
||||
/// TODO(M63): 取值待视觉规格。
|
||||
pub const SPRING_EFFECTS_DEFAULT: Spring = Spring::new(1600.0, 80.0, 1.0);
|
||||
|
||||
/// 动效时长的全局缩放(用户「减少动效」/CPU 路径关动画时用)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct MotionScale {
|
||||
factor: f32,
|
||||
}
|
||||
|
||||
impl MotionScale {
|
||||
/// 标准(1.0)。
|
||||
pub const STANDARD: MotionScale = MotionScale { factor: 1.0 };
|
||||
/// 关闭动效(时长归零;适配层应直接落终态)。
|
||||
pub const NONE: MotionScale = MotionScale { factor: 0.0 };
|
||||
/// 半速(弱动效机器/低帧率兜底)。
|
||||
pub const HALF: MotionScale = MotionScale { factor: 0.5 };
|
||||
|
||||
/// 自定义缩放(夹取到 0.0..=1.0;不放大,避免慢到不可用)。
|
||||
pub fn new(factor: f32) -> Self {
|
||||
Self {
|
||||
factor: factor.clamp(0.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// 缩放系数。
|
||||
pub fn factor(self) -> f32 {
|
||||
self.factor
|
||||
}
|
||||
|
||||
/// 是否完全关闭(适配层据此跳过动画)。
|
||||
pub fn is_off(self) -> bool {
|
||||
self.factor <= 0.0
|
||||
}
|
||||
|
||||
/// 把某一档时长换算成实际毫秒。
|
||||
pub fn ms(self, token: super::DurationToken) -> f32 {
|
||||
token.ms() as f32 * self.factor
|
||||
}
|
||||
|
||||
/// 把弹簧的收敛时间换算成实际毫秒。
|
||||
pub fn spring_ms(self, spring: Spring) -> f32 {
|
||||
spring.settle_ms(0.001) * self.factor
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::motion::DurationToken;
|
||||
|
||||
const PRESETS: [Spring; 4] = [
|
||||
SPRING_SPATIAL_FAST,
|
||||
SPRING_SPATIAL_DEFAULT,
|
||||
SPRING_EFFECTS_FAST,
|
||||
SPRING_EFFECTS_DEFAULT,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn spring_presets_behave() {
|
||||
for spring in PRESETS {
|
||||
assert!(spring.damping_ratio() > 0.5, "弹簧应明显欠阻尼/近临界");
|
||||
assert_eq!(spring.sample(0.0), 0.0);
|
||||
let settle = spring.settle_ms(0.001);
|
||||
assert!(
|
||||
(60.0..1200.0).contains(&settle),
|
||||
"收敛时间 {settle}ms 不合理"
|
||||
);
|
||||
let end = spring.sample(settle);
|
||||
assert!((end - 1.0).abs() < 0.01, "收敛后应回到 1.0,实测 {end}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn critically_damped_spring_does_not_overshoot() {
|
||||
let wn = SPRING_SPATIAL_DEFAULT.natural_frequency();
|
||||
let critical = Spring::new(wn * wn, 2.0 * wn, 1.0);
|
||||
assert!((critical.damping_ratio() - 1.0).abs() < 1.0e-3);
|
||||
for step in 1..=40 {
|
||||
let v = critical.sample(step as f32 * 5.0);
|
||||
assert!(v <= 1.0 + 1.0e-3, "临界阻尼不应过冲:{v}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overdamped_spring_converges_slowly_without_overshoot() {
|
||||
let wn = SPRING_SPATIAL_DEFAULT.natural_frequency();
|
||||
let over = Spring::new(wn * wn, 4.0 * wn, 1.0);
|
||||
assert!(over.damping_ratio() > 1.5);
|
||||
let settle = over.settle_ms(0.001);
|
||||
assert!((over.sample(settle) - 1.0).abs() < 0.01);
|
||||
for step in 1..=40 {
|
||||
assert!(over.sample(step as f32 * 10.0) <= 1.0 + 1.0e-3);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motion_scale_scales_or_kills_duration() {
|
||||
assert_eq!(MotionScale::STANDARD.ms(DurationToken::Medium2), 300.0);
|
||||
assert_eq!(MotionScale::HALF.ms(DurationToken::Medium2), 150.0);
|
||||
assert_eq!(MotionScale::NONE.ms(DurationToken::Medium2), 0.0);
|
||||
assert!(MotionScale::NONE.is_off());
|
||||
assert_eq!(MotionScale::new(5.0).factor(), 1.0);
|
||||
assert_eq!(MotionScale::new(-1.0).factor(), 0.0);
|
||||
assert!(MotionScale::STANDARD.spring_ms(SPRING_SPATIAL_DEFAULT) > 0.0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user