use crate::animate::{MotionDirection, MotionSpeed};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VariationKind {
Png,
Mp4,
}
impl VariationKind {
pub fn ext(self) -> &'static str {
match self {
Self::Png => "png",
Self::Mp4 => "mp4",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VariationMode {
Still,
Video,
Mixed,
}
impl VariationMode {
pub fn accepts(self, kind: VariationKind) -> bool {
match self {
Self::Mixed => true,
Self::Still => kind == VariationKind::Png,
Self::Video => kind == VariationKind::Mp4,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct VariationSpec {
pub label: &'static str,
pub kind: VariationKind,
pub direction: MotionDirection,
pub speed: MotionSpeed,
pub count: usize,
pub orb_size: f32,
pub blur: f32,
pub seed: u64,
pub duration_ms: u64,
}
pub const DEFAULT_VARIATIONS: &[VariationSpec] = &[
VariationSpec {
label: "snapshot_lr_dense",
kind: VariationKind::Png,
direction: MotionDirection::LeftToRight,
speed: MotionSpeed::Slow,
count: 25,
orb_size: 3.0,
blur: 0.5,
seed: 1,
duration_ms: 0,
},
VariationSpec {
label: "snapshot_rl_huge",
kind: VariationKind::Png,
direction: MotionDirection::RightToLeft,
speed: MotionSpeed::VerySlow,
count: 12,
orb_size: 4.5,
blur: 0.6,
seed: 2,
duration_ms: 0,
},
VariationSpec {
label: "snapshot_tb_fine",
kind: VariationKind::Png,
direction: MotionDirection::TopToBottom,
speed: MotionSpeed::Slow,
count: 30,
orb_size: 2.5,
blur: 0.4,
seed: 3,
duration_ms: 0,
},
VariationSpec {
label: "snapshot_bt_blurry",
kind: VariationKind::Png,
direction: MotionDirection::BottomToTop,
speed: MotionSpeed::VerySlow,
count: 20,
orb_size: 3.5,
blur: 0.8,
seed: 4,
duration_ms: 0,
},
VariationSpec {
label: "flow_lr_slow",
kind: VariationKind::Mp4,
direction: MotionDirection::LeftToRight,
speed: MotionSpeed::Slow,
count: 20,
orb_size: 3.0,
blur: 0.5,
seed: 5,
duration_ms: 8000,
},
VariationSpec {
label: "flow_rl_very_slow",
kind: VariationKind::Mp4,
direction: MotionDirection::RightToLeft,
speed: MotionSpeed::VerySlow,
count: 15,
orb_size: 3.8,
blur: 0.6,
seed: 6,
duration_ms: 8000,
},
VariationSpec {
label: "flow_tb_dense",
kind: VariationKind::Mp4,
direction: MotionDirection::TopToBottom,
speed: MotionSpeed::Slow,
count: 28,
orb_size: 2.8,
blur: 0.5,
seed: 7,
duration_ms: 8000,
},
VariationSpec {
label: "flow_bt_blurry",
kind: VariationKind::Mp4,
direction: MotionDirection::BottomToTop,
speed: MotionSpeed::VerySlow,
count: 18,
orb_size: 3.5,
blur: 0.7,
seed: 8,
duration_ms: 8000,
},
VariationSpec {
label: "flow_lr_dense_small",
kind: VariationKind::Mp4,
direction: MotionDirection::LeftToRight,
speed: MotionSpeed::Slow,
count: 50,
orb_size: 1.5,
blur: 0.3,
seed: 9,
duration_ms: 8000,
},
VariationSpec {
label: "flow_rl_huge",
kind: VariationKind::Mp4,
direction: MotionDirection::RightToLeft,
speed: MotionSpeed::Slow,
count: 10,
orb_size: 5.0,
blur: 0.6,
seed: 10,
duration_ms: 8000,
},
];
pub fn select_specs(n: usize, mode: VariationMode) -> Vec<VariationSpec> {
DEFAULT_VARIATIONS
.iter()
.copied()
.filter(|s| mode.accepts(s.kind))
.take(n)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_set_has_ten_specs() {
assert_eq!(DEFAULT_VARIATIONS.len(), 10);
}
#[test]
fn default_set_balance() {
let png = DEFAULT_VARIATIONS
.iter()
.filter(|s| s.kind == VariationKind::Png)
.count();
let mp4 = DEFAULT_VARIATIONS
.iter()
.filter(|s| s.kind == VariationKind::Mp4)
.count();
assert!(png >= 1, "expected at least 1 still variation");
assert!(mp4 >= 1, "expected at least 1 video variation");
}
#[test]
fn default_set_covers_all_directions() {
let mut seen_lr = false;
let mut seen_rl = false;
let mut seen_tb = false;
let mut seen_bt = false;
for s in DEFAULT_VARIATIONS {
match s.direction {
MotionDirection::LeftToRight => seen_lr = true,
MotionDirection::RightToLeft => seen_rl = true,
MotionDirection::TopToBottom => seen_tb = true,
MotionDirection::BottomToTop => seen_bt = true,
}
}
assert!(seen_lr, "LeftToRight not represented");
assert!(seen_rl, "RightToLeft not represented");
assert!(seen_tb, "TopToBottom not represented");
assert!(seen_bt, "BottomToTop not represented");
}
#[test]
fn labels_have_no_color_axis() {
const FORBIDDEN: &[&str] = &[
"warm",
"cool",
"aurora",
"dream",
"hi_key",
"dark_mood",
"drift",
"glow",
"mist",
];
for s in DEFAULT_VARIATIONS {
for tag in FORBIDDEN {
assert!(
!s.label.contains(tag),
"label {:?} contains forbidden color/legacy tag {tag:?}",
s.label
);
}
}
}
#[test]
fn count_is_in_screen_filling_range() {
for s in DEFAULT_VARIATIONS {
assert!(
(10..=50).contains(&s.count),
"spec {:?} has count {} outside the 10..=50 range",
s.label,
s.count
);
}
}
#[test]
fn labels_unique_and_ascii_safe() {
let mut seen = std::collections::HashSet::new();
for s in DEFAULT_VARIATIONS {
assert!(seen.insert(s.label), "duplicate label: {}", s.label);
for ch in s.label.chars() {
assert!(
ch.is_ascii_alphanumeric() || ch == '_',
"non shell-safe char in label {:?}: {ch:?}",
s.label
);
}
}
}
#[test]
fn select_specs_respects_mode() {
let still = select_specs(10, VariationMode::Still);
assert!(still.iter().all(|s| s.kind == VariationKind::Png));
let video = select_specs(10, VariationMode::Video);
assert!(video.iter().all(|s| s.kind == VariationKind::Mp4));
let mixed = select_specs(10, VariationMode::Mixed);
assert_eq!(mixed.len(), 10);
}
#[test]
fn select_specs_respects_n() {
let three = select_specs(3, VariationMode::Mixed);
assert_eq!(three.len(), 3);
let zero = select_specs(0, VariationMode::Mixed);
assert_eq!(zero.len(), 0);
}
}