use crate::{AnimationError, Frame, Inertia, Motion, MotionValue, Target, Transition};
use std::time::Duration;
#[derive(Debug)]
pub struct Variants<K> {
entries: Vec<(K, Motion)>,
}
impl<K: PartialEq> Variants<K> {
pub fn new(entries: impl IntoIterator<Item = (K, Motion)>) -> Result<Self, AnimationError> {
let entries: Vec<_> = entries.into_iter().collect();
if entries
.iter()
.enumerate()
.any(|(ix, (key, _))| entries[..ix].iter().any(|(old, _)| old == key))
{
return Err(AnimationError::Label);
}
Ok(Self { entries })
}
pub fn get(&self, key: &K) -> Option<Motion> {
self.entries
.iter()
.find(|(k, _)| k == key)
.map(|(_, motion)| *motion)
}
}
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub struct Interaction {
pub hovered: bool,
pub pressed: bool,
pub focused: bool,
pub dragging: bool,
pub in_view: bool,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct GestureTargets {
hover: Option<Target>,
press: Option<Target>,
focus: Option<Target>,
drag: Option<Target>,
view: Option<Target>,
}
impl GestureTargets {
pub fn while_hover(mut self, target: Target) -> Self {
self.hover = Some(target);
self
}
pub fn while_press(mut self, target: Target) -> Self {
self.press = Some(target);
self
}
pub fn while_focus(mut self, target: Target) -> Self {
self.focus = Some(target);
self
}
pub fn while_drag(mut self, target: Target) -> Self {
self.drag = Some(target);
self
}
pub fn while_in_view(mut self, target: Target) -> Self {
self.view = Some(target);
self
}
pub fn resolve(self, mut base: Target, state: Interaction) -> Target {
for (active, target) in [
(state.in_view, self.view),
(state.hovered, self.hover),
(state.focused, self.focus),
(state.pressed, self.press),
(state.dragging, self.drag),
] {
if active && let Some(target) = target {
base = base.overlay(target);
}
}
base
}
}
#[derive(Debug)]
pub struct Drag {
values: [MotionValue; 2],
min: [f64; 2],
max: [f64; 2],
origin: [f64; 2],
pointer: [f64; 2],
dragging: bool,
last_move: Duration,
elasticity: f64,
snap: Option<f64>,
momentum: bool,
direction_lock: Option<f64>,
locked_axis: Option<usize>,
}
impl Drag {
pub fn new(initial: [f64; 2], min: [f64; 2], max: [f64; 2]) -> Self {
assert!(
(0..2).all(|i| initial[i].is_finite()
&& min[i].is_finite()
&& max[i].is_finite()
&& min[i] <= initial[i]
&& initial[i] <= max[i]),
"invalid drag bounds"
);
Self {
values: initial.map(MotionValue::new),
min,
max,
origin: initial,
pointer: [0.0; 2],
dragging: false,
last_move: Duration::ZERO,
elasticity: 0.15,
snap: None,
momentum: true,
direction_lock: None,
locked_axis: None,
}
}
pub fn unconstrained(initial: [f64; 2]) -> Self {
Self::new(initial, [f64::from(f32::MIN); 2], [f64::from(f32::MAX); 2])
}
pub fn elasticity(mut self, value: f64) -> Self {
assert!((0.0..=1.0).contains(&value), "invalid elasticity");
self.elasticity = value;
self
}
pub fn snap_grid(mut self, spacing: f64) -> Self {
assert!(spacing.is_finite() && spacing > 0.0, "invalid snap grid");
self.snap = Some(spacing);
self
}
pub fn momentum(mut self, enabled: bool) -> Self {
self.momentum = enabled;
self
}
pub fn direction_lock(mut self, threshold: f64) -> Self {
assert!(
threshold.is_finite() && threshold > 0.0,
"invalid direction threshold"
);
self.direction_lock = Some(threshold);
self
}
pub fn locked_axis(&self) -> Option<usize> {
self.locked_axis
}
pub fn set_bounds(&mut self, min: [f64; 2], max: [f64; 2], now: Duration) {
assert!(
(0..2).all(|i| min[i].is_finite() && max[i].is_finite() && min[i] <= max[i]),
"invalid drag bounds"
);
if self.min == min && self.max == max {
return;
}
self.min = min;
self.max = max;
if !self.dragging {
for i in 0..2 {
let target = self.values[i].target().clamp(min[i], max[i]);
self.values[i].animate(target, Transition::default(), now);
}
}
}
pub fn presentation(&self) -> ([f64; 2], [f64; 2]) {
(
std::array::from_fn(|i| self.values[i].get()),
std::array::from_fn(|i| self.values[i].velocity()),
)
}
pub fn begin(&mut self, pointer: [f64; 2], now: Duration) {
assert!(pointer.iter().all(|v| v.is_finite()), "invalid pointer");
for ix in 0..2 {
let value = self.values[ix].sample(now, false).value();
self.values[ix].jump(value, now);
self.origin[ix] = value;
}
self.pointer = pointer;
self.locked_axis = None;
self.last_move = now;
self.dragging = true;
}
pub fn move_to(&mut self, pointer: [f64; 2], now: Duration) {
assert!(pointer.iter().all(|v| v.is_finite()), "invalid pointer");
if !self.dragging {
return;
}
self.last_move = self.last_move.max(now);
if let Some(threshold) = self.direction_lock
&& self.locked_axis.is_none()
{
let delta = std::array::from_fn::<_, 2, _>(|i| (pointer[i] - self.pointer[i]).abs());
if delta[0].max(delta[1]) < threshold {
return;
}
self.locked_axis = Some(usize::from(delta[1] > delta[0]));
}
for (ix, point) in pointer.into_iter().enumerate() {
if self.locked_axis.is_some_and(|axis| axis != ix) {
continue;
}
let raw = crate::value::finite(
self.origin[ix] + crate::value::finite(point - self.pointer[ix]),
);
let clamped = raw.clamp(self.min[ix], self.max[ix]);
self.values[ix].set(clamped + (raw - clamped) * self.elasticity, now);
}
}
pub fn end(&mut self, now: Duration) {
if !self.dragging {
return;
}
self.dragging = false;
if now.saturating_sub(self.last_move) > Duration::from_millis(100) {
for value in &mut self.values {
value.jump(value.get(), now);
}
}
for ix in 0..2 {
let inertia = Inertia::default().bounds(self.min[ix], self.max[ix]);
if let Some(grid) = self.snap {
let projected = if self.momentum {
inertia.project(self.values[ix].get(), self.values[ix].velocity())
} else {
self.values[ix].get()
};
let target = (projected / grid)
.round()
.mul_add(grid, 0.0)
.clamp(self.min[ix], self.max[ix]);
self.values[ix].animate(target, Transition::default(), now);
} else if self.momentum {
self.values[ix].coast(inertia, now);
} else {
let target = self.values[ix].get().clamp(self.min[ix], self.max[ix]);
self.values[ix].animate(target, Transition::default(), now);
}
}
}
pub fn cancel(&mut self, now: Duration) {
if !self.dragging {
return;
}
self.dragging = false;
for ix in 0..2 {
self.values[ix].animate(
self.origin[ix].clamp(self.min[ix], self.max[ix]),
Transition::default(),
now,
);
}
}
pub fn move_by(&mut self, delta: [f64; 2], now: Duration) {
assert!(delta.iter().all(|v| v.is_finite()), "invalid drag delta");
self.dragging = false;
for (ix, delta) in delta.into_iter().enumerate() {
let target = (self.values[ix].target() + delta).clamp(self.min[ix], self.max[ix]);
self.values[ix].animate(target, Transition::default(), now);
}
}
pub fn sample(&mut self, now: Duration, reduced_motion: bool) -> Frame {
let values = self
.values
.each_mut()
.map(|value| value.sample(now, reduced_motion));
Frame::animated(
Target::default()
.x(values[0].value().clamp(-(f32::MAX as f64), f32::MAX as f64) as f32)
.y(values[1].value().clamp(-(f32::MAX as f64), f32::MAX as f64) as f32),
[
values[0].velocity(),
values[1].velocity(),
0.0,
0.0,
0.0,
0.0,
],
values.iter().any(|v| v.is_animating()),
values.iter().any(|v| v.just_completed()) && values.iter().all(|v| !v.is_animating()),
)
}
pub fn is_dragging(&self) -> bool {
self.dragging
}
}
pub fn transform(value: f64, input: [f64; 2], output: [f64; 2], clamp: bool) -> f64 {
assert!(
[value, input[0], input[1], output[0], output[1]]
.iter()
.all(|v| v.is_finite())
&& input[0] != input[1],
"invalid transform range"
);
let progress = (value - input[0]) / (input[1] - input[0]);
let progress = if clamp {
progress.clamp(0.0, 1.0)
} else {
progress
};
output[0] + (output[1] - output[0]) * progress
}
pub fn scroll_progress(offset: f64, content: f64, viewport: f64) -> f64 {
assert!(
[offset, content, viewport].iter().all(|v| v.is_finite())
&& content >= 0.0
&& viewport >= 0.0,
"invalid scroll metrics"
);
if content <= viewport || offset <= 0.0 {
0.0
} else {
(offset / (content - viewport)).clamp(0.0, 1.0)
}
}
#[derive(Clone, Copy, Debug)]
pub struct InView {
amount: f64,
once: bool,
entered: bool,
visible: bool,
}
impl InView {
pub fn new(amount: f64, once: bool) -> Self {
assert!(
(0.0..=1.0).contains(&amount),
"invalid visibility threshold"
);
Self {
amount,
once,
entered: false,
visible: false,
}
}
pub fn update(&mut self, viewport: [f64; 4], element: [f64; 4]) -> bool {
assert!(
viewport.iter().chain(element.iter()).all(|v| v.is_finite())
&& viewport[2..]
.iter()
.chain(element[2..].iter())
.all(|v| *v >= 0.0),
"invalid viewport bounds"
);
let width =
(viewport[0] + viewport[2]).min(element[0] + element[2]) - viewport[0].max(element[0]);
let height =
(viewport[1] + viewport[3]).min(element[1] + element[3]) - viewport[1].max(element[1]);
let area = element[2] * element[3];
let fraction = if area > 0.0 {
width.max(0.0) * height.max(0.0) / area
} else {
0.0
};
let visible = (self.once && self.entered) || (fraction > 0.0 && fraction >= self.amount);
let changed = self.visible != visible;
self.visible = visible;
self.entered |= visible;
changed
}
pub fn is_visible(&self) -> bool {
self.visible
}
}
pub fn reorder_index(centers: &[f64], position: f64) -> Result<usize, AnimationError> {
if centers.is_empty()
|| !position.is_finite()
|| centers.iter().any(|v| !v.is_finite())
|| centers.windows(2).any(|p| p[0] >= p[1])
{
return Err(AnimationError::Index);
}
Ok(centers
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| (*a - position).abs().total_cmp(&(*b - position).abs()))
.unwrap()
.0)
}
pub fn move_item<T>(items: &mut [T], from: usize, to: usize) -> Result<(), AnimationError> {
if from >= items.len() || to >= items.len() {
return Err(AnimationError::Index);
}
if from < to {
items[from..=to].rotate_left(1);
} else {
items[to..=from].rotate_right(1);
}
Ok(())
}