mod animatable;
mod util;
use std::ops::{Add, Deref, Mul};
use std::time::Duration;
use bevy_app::{App, Plugin, PostUpdate};
use bevy_asset::{Asset, AssetApp, Assets, Handle};
use bevy_core::Name;
use bevy_ecs::prelude::*;
use bevy_hierarchy::{Children, Parent};
use bevy_math::{FloatExt, Quat, Vec3};
use bevy_reflect::Reflect;
use bevy_render::mesh::morph::MorphWeights;
use bevy_time::Time;
use bevy_transform::{prelude::Transform, TransformSystem};
use bevy_utils::{tracing::warn, HashMap};
#[allow(missing_docs)]
pub mod prelude {
#[doc(hidden)]
pub use crate::{
animatable::*, AnimationClip, AnimationPlayer, AnimationPlugin, EntityPath, Interpolation,
Keyframes, VariableCurve,
};
}
#[derive(Reflect, Clone, Debug)]
pub enum Keyframes {
Rotation(Vec<Quat>),
Translation(Vec<Vec3>),
Scale(Vec<Vec3>),
Weights(Vec<f32>),
}
impl Keyframes {
pub fn len(&self) -> usize {
match self {
Keyframes::Weights(vec) => vec.len(),
Keyframes::Translation(vec) | Keyframes::Scale(vec) => vec.len(),
Keyframes::Rotation(vec) => vec.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Reflect, Clone, Debug)]
pub struct VariableCurve {
pub keyframe_timestamps: Vec<f32>,
pub keyframes: Keyframes,
pub interpolation: Interpolation,
}
impl VariableCurve {
pub fn find_current_keyframe(&self, seek_time: f32) -> Option<usize> {
let search_result = self
.keyframe_timestamps
.binary_search_by(|probe| probe.partial_cmp(&seek_time).unwrap());
let last_keyframe = self.keyframes.len() - 1;
let step_start = match search_result {
Ok(n) if n >= last_keyframe => return None,
Ok(i) => i,
Err(0) => return None,
Err(n) if n > last_keyframe => return None,
Err(i) => i - 1,
};
assert!(step_start < self.keyframe_timestamps.len());
Some(step_start)
}
}
#[derive(Reflect, Clone, Debug)]
pub enum Interpolation {
Linear,
Step,
CubicSpline,
}
#[derive(Reflect, Clone, Debug, Hash, PartialEq, Eq, Default)]
pub struct EntityPath {
pub parts: Vec<Name>,
}
#[derive(Asset, Reflect, Clone, Debug, Default)]
pub struct AnimationClip {
curves: Vec<Vec<VariableCurve>>,
paths: HashMap<EntityPath, usize>,
duration: f32,
}
impl AnimationClip {
#[inline]
pub fn curves(&self) -> &Vec<Vec<VariableCurve>> {
&self.curves
}
#[inline]
pub fn get_curves(&self, bone_id: usize) -> Option<&'_ Vec<VariableCurve>> {
self.curves.get(bone_id)
}
#[inline]
pub fn get_curves_by_path(&self, path: &EntityPath) -> Option<&'_ Vec<VariableCurve>> {
self.paths.get(path).and_then(|id| self.curves.get(*id))
}
#[inline]
pub fn duration(&self) -> f32 {
self.duration
}
pub fn add_curve_to_path(&mut self, path: EntityPath, curve: VariableCurve) {
self.duration = self
.duration
.max(*curve.keyframe_timestamps.last().unwrap_or(&0.0));
if let Some(bone_id) = self.paths.get(&path) {
self.curves[*bone_id].push(curve);
} else {
let idx = self.curves.len();
self.curves.push(vec![curve]);
self.paths.insert(path, idx);
}
}
pub fn compatible_with(&self, name: &Name) -> bool {
self.paths.keys().any(|path| &path.parts[0] == name)
}
}
#[derive(Reflect, Debug, PartialEq, Eq, Copy, Clone, Default)]
pub enum RepeatAnimation {
#[default]
Never,
Count(u32),
Forever,
}
#[derive(Debug, Reflect)]
struct PlayingAnimation {
repeat: RepeatAnimation,
speed: f32,
elapsed: f32,
seek_time: f32,
animation_clip: Handle<AnimationClip>,
path_cache: Vec<Vec<Option<Entity>>>,
completions: u32,
}
impl Default for PlayingAnimation {
fn default() -> Self {
Self {
repeat: RepeatAnimation::default(),
speed: 1.0,
elapsed: 0.0,
seek_time: 0.0,
animation_clip: Default::default(),
path_cache: Vec::new(),
completions: 0,
}
}
}
impl PlayingAnimation {
#[inline]
pub fn is_finished(&self) -> bool {
match self.repeat {
RepeatAnimation::Forever => false,
RepeatAnimation::Never => self.completions >= 1,
RepeatAnimation::Count(n) => self.completions >= n,
}
}
#[inline]
fn update(&mut self, delta: f32, clip_duration: f32) {
if self.is_finished() {
return;
}
self.elapsed += delta;
self.seek_time += delta * self.speed;
let over_time = self.speed > 0.0 && self.seek_time >= clip_duration;
let under_time = self.speed < 0.0 && self.seek_time < 0.0;
if over_time || under_time {
self.completions += 1;
if self.is_finished() {
return;
}
}
if self.seek_time >= clip_duration {
self.seek_time %= clip_duration;
}
if self.seek_time < 0.0 {
self.seek_time += clip_duration;
}
}
fn replay(&mut self) {
self.completions = 0;
self.elapsed = 0.0;
self.seek_time = 0.0;
}
}
struct AnimationTransition {
current_weight: f32,
weight_decline_per_sec: f32,
animation: PlayingAnimation,
}
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
pub struct AnimationPlayer {
paused: bool,
animation: PlayingAnimation,
#[reflect(ignore)]
transitions: Vec<AnimationTransition>,
}
impl AnimationPlayer {
pub fn start(&mut self, handle: Handle<AnimationClip>) -> &mut Self {
self.animation = PlayingAnimation {
animation_clip: handle,
..Default::default()
};
self.transitions.clear();
self
}
pub fn start_with_transition(
&mut self,
handle: Handle<AnimationClip>,
transition_duration: Duration,
) -> &mut Self {
let mut animation = PlayingAnimation {
animation_clip: handle,
..Default::default()
};
std::mem::swap(&mut animation, &mut self.animation);
self.transitions.push(AnimationTransition {
current_weight: 1.0,
weight_decline_per_sec: 1.0 / transition_duration.as_secs_f32(),
animation,
});
self
}
pub fn play(&mut self, handle: Handle<AnimationClip>) -> &mut Self {
if !self.is_playing_clip(&handle) || self.is_paused() {
self.start(handle);
}
self
}
pub fn play_with_transition(
&mut self,
handle: Handle<AnimationClip>,
transition_duration: Duration,
) -> &mut Self {
if !self.is_playing_clip(&handle) || self.is_paused() {
self.start_with_transition(handle, transition_duration);
}
self
}
pub fn animation_clip(&self) -> &Handle<AnimationClip> {
&self.animation.animation_clip
}
pub fn is_playing_clip(&self, handle: &Handle<AnimationClip>) -> bool {
self.animation_clip() == handle
}
pub fn is_finished(&self) -> bool {
self.animation.is_finished()
}
pub fn repeat(&mut self) -> &mut Self {
self.animation.repeat = RepeatAnimation::Forever;
self
}
pub fn set_repeat(&mut self, repeat: RepeatAnimation) -> &mut Self {
self.animation.repeat = repeat;
self
}
pub fn repeat_mode(&self) -> RepeatAnimation {
self.animation.repeat
}
pub fn completions(&self) -> u32 {
self.animation.completions
}
pub fn is_playback_reversed(&self) -> bool {
self.animation.speed < 0.0
}
pub fn pause(&mut self) {
self.paused = true;
}
pub fn resume(&mut self) {
self.paused = false;
}
pub fn is_paused(&self) -> bool {
self.paused
}
pub fn speed(&self) -> f32 {
self.animation.speed
}
pub fn set_speed(&mut self, speed: f32) -> &mut Self {
self.animation.speed = speed;
self
}
pub fn elapsed(&self) -> f32 {
self.animation.elapsed
}
pub fn seek_time(&self) -> f32 {
self.animation.seek_time
}
pub fn seek_to(&mut self, seek_time: f32) -> &mut Self {
self.animation.seek_time = seek_time;
self
}
pub fn replay(&mut self) {
self.animation.replay();
}
}
fn entity_from_path(
root: Entity,
path: &EntityPath,
children: &Query<&Children>,
names: &Query<&Name>,
path_cache: &mut Vec<Option<Entity>>,
) -> Option<Entity> {
let mut current_entity = root;
path_cache.resize(path.parts.len(), None);
let mut parts = path.parts.iter().enumerate();
let (_, root_name) = parts.next()?;
if names.get(current_entity) != Ok(root_name) {
return None;
}
for (idx, part) in parts {
let mut found = false;
let children = children.get(current_entity).ok()?;
if let Some(cached) = path_cache[idx] {
if children.contains(&cached) {
if let Ok(name) = names.get(cached) {
if name == part {
current_entity = cached;
found = true;
}
}
}
}
if !found {
for child in children.deref() {
if let Ok(name) = names.get(*child) {
if name == part {
current_entity = *child;
path_cache[idx] = Some(*child);
found = true;
break;
}
}
}
}
if !found {
warn!("Entity not found for path {:?} on part {:?}", path, part);
return None;
}
}
Some(current_entity)
}
fn verify_no_ancestor_player(
player_parent: Option<&Parent>,
parents: &Query<(Has<AnimationPlayer>, Option<&Parent>)>,
) -> bool {
let Some(mut current) = player_parent.map(Parent::get) else {
return true;
};
loop {
let Ok((has_player, parent)) = parents.get(current) else {
return true;
};
if has_player {
return false;
}
if let Some(parent) = parent {
current = parent.get();
} else {
return true;
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn animation_player(
time: Res<Time>,
animations: Res<Assets<AnimationClip>>,
children: Query<&Children>,
names: Query<&Name>,
transforms: Query<&mut Transform>,
morphs: Query<&mut MorphWeights>,
parents: Query<(Has<AnimationPlayer>, Option<&Parent>)>,
mut animation_players: Query<(Entity, Option<&Parent>, &mut AnimationPlayer)>,
) {
animation_players
.par_iter_mut()
.for_each(|(root, maybe_parent, mut player)| {
update_transitions(&mut player, &time);
run_animation_player(
root,
player,
&time,
&animations,
&names,
&transforms,
&morphs,
maybe_parent,
&parents,
&children,
);
});
}
#[allow(clippy::too_many_arguments)]
fn run_animation_player(
root: Entity,
mut player: Mut<AnimationPlayer>,
time: &Time,
animations: &Assets<AnimationClip>,
names: &Query<&Name>,
transforms: &Query<&mut Transform>,
morphs: &Query<&mut MorphWeights>,
maybe_parent: Option<&Parent>,
parents: &Query<(Has<AnimationPlayer>, Option<&Parent>)>,
children: &Query<&Children>,
) {
let paused = player.paused;
if paused && !player.is_changed() {
return;
}
apply_animation(
1.0,
&mut player.animation,
paused,
root,
time,
animations,
names,
transforms,
morphs,
maybe_parent,
parents,
children,
);
for AnimationTransition {
current_weight,
animation,
..
} in &mut player.transitions
{
apply_animation(
*current_weight,
animation,
paused,
root,
time,
animations,
names,
transforms,
morphs,
maybe_parent,
parents,
children,
);
}
}
fn lerp_morph_weights(weights: &mut [f32], keyframe: impl Iterator<Item = f32>, key_lerp: f32) {
let zipped = weights.iter_mut().zip(keyframe);
for (morph_weight, keyframe) in zipped {
*morph_weight = morph_weight.lerp(keyframe, key_lerp);
}
}
fn get_keyframe(target_count: usize, keyframes: &[f32], key_index: usize) -> &[f32] {
let start = target_count * key_index;
let end = target_count * (key_index + 1);
&keyframes[start..end]
}
fn cubic_spline_interpolation<T>(
value_start: T,
tangent_out_start: T,
tangent_in_end: T,
value_end: T,
lerp: f32,
step_duration: f32,
) -> T
where
T: Mul<f32, Output = T> + Add<Output = T>,
{
value_start * (2.0 * lerp.powi(3) - 3.0 * lerp.powi(2) + 1.0)
+ tangent_out_start * (step_duration) * (lerp.powi(3) - 2.0 * lerp.powi(2) + lerp)
+ value_end * (-2.0 * lerp.powi(3) + 3.0 * lerp.powi(2))
+ tangent_in_end * step_duration * (lerp.powi(3) - lerp.powi(2))
}
#[allow(clippy::too_many_arguments)]
fn apply_animation(
weight: f32,
animation: &mut PlayingAnimation,
paused: bool,
root: Entity,
time: &Time,
animations: &Assets<AnimationClip>,
names: &Query<&Name>,
transforms: &Query<&mut Transform>,
morphs: &Query<&mut MorphWeights>,
maybe_parent: Option<&Parent>,
parents: &Query<(Has<AnimationPlayer>, Option<&Parent>)>,
children: &Query<&Children>,
) {
let Some(animation_clip) = animations.get(&animation.animation_clip) else {
return;
};
animation.update(
if paused { 0.0 } else { time.delta_seconds() },
animation_clip.duration,
);
if animation.path_cache.len() != animation_clip.paths.len() {
let new_len = animation_clip.paths.len();
animation.path_cache.iter_mut().for_each(|v| v.clear());
animation.path_cache.resize_with(new_len, Vec::new);
}
if !verify_no_ancestor_player(maybe_parent, parents) {
warn!("Animation player on {:?} has a conflicting animation player on an ancestor. Cannot safely animate.", root);
return;
}
let mut any_path_found = false;
for (path, bone_id) in &animation_clip.paths {
let cached_path = &mut animation.path_cache[*bone_id];
let curves = animation_clip.get_curves(*bone_id).unwrap();
let Some(target) = entity_from_path(root, path, children, names, cached_path) else {
continue;
};
any_path_found = true;
let Ok(mut transform) = (unsafe { transforms.get_unchecked(target) }) else {
continue;
};
let mut morphs = unsafe { morphs.get_unchecked(target) }.ok();
for curve in curves {
if curve.keyframe_timestamps.len() == 1 {
match &curve.keyframes {
Keyframes::Rotation(keyframes) => {
transform.rotation = transform.rotation.slerp(keyframes[0], weight);
}
Keyframes::Translation(keyframes) => {
transform.translation = transform.translation.lerp(keyframes[0], weight);
}
Keyframes::Scale(keyframes) => {
transform.scale = transform.scale.lerp(keyframes[0], weight);
}
Keyframes::Weights(keyframes) => {
if let Some(morphs) = &mut morphs {
let target_count = morphs.weights().len();
lerp_morph_weights(
morphs.weights_mut(),
get_keyframe(target_count, keyframes, 0).iter().copied(),
weight,
);
}
}
}
continue;
}
let Some(step_start) = curve.find_current_keyframe(animation.seek_time) else {
continue;
};
let timestamp_start = curve.keyframe_timestamps[step_start];
let timestamp_end = curve.keyframe_timestamps[step_start + 1];
let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, animation.seek_time);
apply_keyframe(
curve,
step_start,
weight,
lerp,
timestamp_end - timestamp_start,
&mut transform,
&mut morphs,
);
}
}
if !any_path_found {
warn!("Animation player on {root:?} did not match any entity paths.");
}
}
#[inline(always)]
fn apply_keyframe(
curve: &VariableCurve,
step_start: usize,
weight: f32,
lerp: f32,
duration: f32,
transform: &mut Mut<Transform>,
morphs: &mut Option<Mut<MorphWeights>>,
) {
match (&curve.interpolation, &curve.keyframes) {
(Interpolation::Step, Keyframes::Rotation(keyframes)) => {
transform.rotation = transform.rotation.slerp(keyframes[step_start], weight);
}
(Interpolation::Linear, Keyframes::Rotation(keyframes)) => {
let rot_start = keyframes[step_start];
let mut rot_end = keyframes[step_start + 1];
if rot_end.dot(rot_start) < 0.0 {
rot_end = -rot_end;
}
let rot = rot_start.normalize().slerp(rot_end.normalize(), lerp);
transform.rotation = transform.rotation.slerp(rot, weight);
}
(Interpolation::CubicSpline, Keyframes::Rotation(keyframes)) => {
let value_start = keyframes[step_start * 3 + 1];
let tangent_out_start = keyframes[step_start * 3 + 2];
let tangent_in_end = keyframes[(step_start + 1) * 3];
let value_end = keyframes[(step_start + 1) * 3 + 1];
let result = cubic_spline_interpolation(
value_start,
tangent_out_start,
tangent_in_end,
value_end,
lerp,
duration,
);
transform.rotation = transform.rotation.slerp(result.normalize(), weight);
}
(Interpolation::Step, Keyframes::Translation(keyframes)) => {
transform.translation = transform.translation.lerp(keyframes[step_start], weight);
}
(Interpolation::Linear, Keyframes::Translation(keyframes)) => {
let translation_start = keyframes[step_start];
let translation_end = keyframes[step_start + 1];
let result = translation_start.lerp(translation_end, lerp);
transform.translation = transform.translation.lerp(result, weight);
}
(Interpolation::CubicSpline, Keyframes::Translation(keyframes)) => {
let value_start = keyframes[step_start * 3 + 1];
let tangent_out_start = keyframes[step_start * 3 + 2];
let tangent_in_end = keyframes[(step_start + 1) * 3];
let value_end = keyframes[(step_start + 1) * 3 + 1];
let result = cubic_spline_interpolation(
value_start,
tangent_out_start,
tangent_in_end,
value_end,
lerp,
duration,
);
transform.translation = transform.translation.lerp(result, weight);
}
(Interpolation::Step, Keyframes::Scale(keyframes)) => {
transform.scale = transform.scale.lerp(keyframes[step_start], weight);
}
(Interpolation::Linear, Keyframes::Scale(keyframes)) => {
let scale_start = keyframes[step_start];
let scale_end = keyframes[step_start + 1];
let result = scale_start.lerp(scale_end, lerp);
transform.scale = transform.scale.lerp(result, weight);
}
(Interpolation::CubicSpline, Keyframes::Scale(keyframes)) => {
let value_start = keyframes[step_start * 3 + 1];
let tangent_out_start = keyframes[step_start * 3 + 2];
let tangent_in_end = keyframes[(step_start + 1) * 3];
let value_end = keyframes[(step_start + 1) * 3 + 1];
let result = cubic_spline_interpolation(
value_start,
tangent_out_start,
tangent_in_end,
value_end,
lerp,
duration,
);
transform.scale = transform.scale.lerp(result, weight);
}
(Interpolation::Step, Keyframes::Weights(keyframes)) => {
if let Some(morphs) = morphs {
let target_count = morphs.weights().len();
let morph_start = get_keyframe(target_count, keyframes, step_start);
lerp_morph_weights(morphs.weights_mut(), morph_start.iter().copied(), weight);
}
}
(Interpolation::Linear, Keyframes::Weights(keyframes)) => {
if let Some(morphs) = morphs {
let target_count = morphs.weights().len();
let morph_start = get_keyframe(target_count, keyframes, step_start);
let morph_end = get_keyframe(target_count, keyframes, step_start + 1);
let result = morph_start
.iter()
.zip(morph_end)
.map(|(a, b)| a.lerp(*b, lerp));
lerp_morph_weights(morphs.weights_mut(), result, weight);
}
}
(Interpolation::CubicSpline, Keyframes::Weights(keyframes)) => {
if let Some(morphs) = morphs {
let target_count = morphs.weights().len();
let morph_start = get_keyframe(target_count, keyframes, step_start * 3 + 1);
let tangents_out_start = get_keyframe(target_count, keyframes, step_start * 3 + 2);
let tangents_in_end = get_keyframe(target_count, keyframes, (step_start + 1) * 3);
let morph_end = get_keyframe(target_count, keyframes, (step_start + 1) * 3 + 1);
let result = morph_start
.iter()
.zip(tangents_out_start)
.zip(tangents_in_end)
.zip(morph_end)
.map(
|(((&value_start, &tangent_out_start), &tangent_in_end), &value_end)| {
cubic_spline_interpolation(
value_start,
tangent_out_start,
tangent_in_end,
value_end,
lerp,
duration,
)
},
);
lerp_morph_weights(morphs.weights_mut(), result, weight);
}
}
}
}
fn update_transitions(player: &mut AnimationPlayer, time: &Time) {
player.transitions.retain_mut(|animation| {
animation.current_weight -= animation.weight_decline_per_sec * time.delta_seconds();
animation.current_weight > 0.0
});
}
#[derive(Default)]
pub struct AnimationPlugin;
impl Plugin for AnimationPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<AnimationClip>()
.register_asset_reflect::<AnimationClip>()
.register_type::<AnimationPlayer>()
.register_type::<VariableCurve>()
.register_type::<Vec<VariableCurve>>()
.register_type::<Interpolation>()
.register_type::<Keyframes>()
.add_systems(
PostUpdate,
animation_player.before(TransformSystem::TransformPropagate),
);
}
}
#[cfg(test)]
mod tests {
use crate::VariableCurve;
use bevy_math::Vec3;
fn test_variable_curve() -> VariableCurve {
let keyframe_timestamps = vec![1.0, 2.0, 3.0, 4.0];
let keyframes = vec![
Vec3::ONE * 0.0,
Vec3::ONE * 3.0,
Vec3::ONE * 6.0,
Vec3::ONE * 9.0,
];
let interpolation = crate::Interpolation::Linear;
let variable_curve = VariableCurve {
keyframe_timestamps,
keyframes: crate::Keyframes::Translation(keyframes),
interpolation,
};
assert!(variable_curve.keyframe_timestamps.len() == variable_curve.keyframes.len());
let mut maybe_last_timestamp = None;
for current_timestamp in &variable_curve.keyframe_timestamps {
assert!(current_timestamp.is_finite());
if let Some(last_timestamp) = maybe_last_timestamp {
assert!(current_timestamp > last_timestamp);
}
maybe_last_timestamp = Some(current_timestamp);
}
variable_curve
}
#[test]
fn find_current_keyframe_is_in_bounds() {
let curve = test_variable_curve();
let min_time = *curve.keyframe_timestamps.first().unwrap();
let second_last_keyframe = curve.keyframe_timestamps.len() - 2;
let max_time = curve.keyframe_timestamps[second_last_keyframe];
let elapsed_time = max_time - min_time;
let n_keyframes = curve.keyframe_timestamps.len();
let n_test_points = 5;
for i in 0..=n_test_points {
let normalized_time = i as f32 / n_test_points as f32;
let seek_time = min_time + normalized_time * elapsed_time;
assert!(seek_time >= min_time);
assert!(seek_time <= max_time);
let maybe_current_keyframe = curve.find_current_keyframe(seek_time);
assert!(
maybe_current_keyframe.is_some(),
"Seek time: {seek_time}, Min time: {min_time}, Max time: {max_time}"
);
assert!(maybe_current_keyframe.unwrap() < n_keyframes);
}
}
#[test]
fn find_current_keyframe_returns_none_on_unstarted_animations() {
let curve = test_variable_curve();
let min_time = *curve.keyframe_timestamps.first().unwrap();
let seek_time = 0.0;
assert!(seek_time < min_time);
let maybe_keyframe = curve.find_current_keyframe(seek_time);
assert!(
maybe_keyframe.is_none(),
"Seek time: {seek_time}, Minimum time: {min_time}"
);
}
#[test]
fn find_current_keyframe_returns_none_on_finished_animation() {
let curve = test_variable_curve();
let max_time = *curve.keyframe_timestamps.last().unwrap();
assert!(max_time < f32::INFINITY);
let maybe_keyframe = curve.find_current_keyframe(f32::INFINITY);
assert!(maybe_keyframe.is_none());
let maybe_keyframe = curve.find_current_keyframe(max_time);
assert!(maybe_keyframe.is_none());
}
#[test]
fn second_last_keyframe_is_found_correctly() {
let curve = test_variable_curve();
let second_last_keyframe = curve.keyframe_timestamps.len() - 2;
let second_last_time = curve.keyframe_timestamps[second_last_keyframe];
let maybe_keyframe = curve.find_current_keyframe(second_last_time);
assert!(maybe_keyframe.unwrap() == second_last_keyframe);
let seek_time = second_last_time + 0.001;
let last_time = curve.keyframe_timestamps[second_last_keyframe + 1];
assert!(seek_time < last_time);
let maybe_keyframe = curve.find_current_keyframe(seek_time);
assert!(maybe_keyframe.unwrap() == second_last_keyframe);
}
#[test]
fn exact_keyframe_matches_are_found_correctly() {
let curve = test_variable_curve();
let second_last_keyframe = curve.keyframes.len() - 2;
for i in 0..=second_last_keyframe {
let seek_time = curve.keyframe_timestamps[i];
let keyframe = curve.find_current_keyframe(seek_time).unwrap();
assert!(keyframe == i);
}
}
#[test]
fn exact_and_inexact_keyframes_correspond() {
let curve = test_variable_curve();
let second_last_keyframe = curve.keyframes.len() - 2;
for i in 0..=second_last_keyframe {
let seek_time = curve.keyframe_timestamps[i];
let exact_keyframe = curve.find_current_keyframe(seek_time).unwrap();
let inexact_seek_time = seek_time + 0.0001;
let final_time = *curve.keyframe_timestamps.last().unwrap();
assert!(inexact_seek_time < final_time);
let inexact_keyframe = curve.find_current_keyframe(inexact_seek_time).unwrap();
assert!(exact_keyframe == inexact_keyframe);
}
}
}