use serde::{Deserialize, Serialize};
use super::PortableError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Interp {
Step,
#[default]
Linear,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Target {
LineX {
axes: usize,
index: usize,
},
LineY {
axes: usize,
index: usize,
},
Offsets {
axes: usize,
index: usize,
},
ImageData {
axes: usize,
index: usize,
},
Xlim {
axes: usize,
},
Ylim {
axes: usize,
},
}
impl Target {
#[must_use]
pub fn axes(self) -> usize {
match self {
Target::LineX { axes, .. }
| Target::LineY { axes, .. }
| Target::Offsets { axes, .. }
| Target::ImageData { axes, .. }
| Target::Xlim { axes }
| Target::Ylim { axes } => axes,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Track {
pub target: Target,
pub times: Vec<f64>,
pub values: Vec<f64>,
pub stride: usize,
pub interp: Interp,
}
impl Track {
pub fn new(
target: Target,
times: Vec<f64>,
frames: Vec<Vec<f64>>,
interp: Interp,
) -> Result<Track, PortableError> {
if times.is_empty() {
return Err(PortableError::Unsupported(
"an animation track needs at least one keyframe".to_string(),
));
}
if times.len() != frames.len() {
return Err(PortableError::Unsupported(format!(
"track has {} keyframe times but {} frames",
times.len(),
frames.len()
)));
}
if !times.windows(2).all(|w| w[0] < w[1]) {
return Err(PortableError::Unsupported(
"keyframe times must be strictly increasing".to_string(),
));
}
let stride = frames[0].len();
if let Some(bad) = frames.iter().position(|f| f.len() != stride) {
return Err(PortableError::Unsupported(format!(
"frame {bad} has {} values but frame 0 has {stride}; every frame \
of a track must be the same width",
frames[bad].len()
)));
}
Ok(Track {
target,
times,
values: frames.concat(),
stride,
interp,
})
}
pub fn line_y(
axes: usize,
index: usize,
times: Vec<f64>,
frames: Vec<Vec<f64>>,
) -> Result<Track, PortableError> {
Track::new(Target::LineY { axes, index }, times, frames, Interp::Linear)
}
pub fn line_x(
axes: usize,
index: usize,
times: Vec<f64>,
frames: Vec<Vec<f64>>,
) -> Result<Track, PortableError> {
Track::new(Target::LineX { axes, index }, times, frames, Interp::Linear)
}
pub fn xlim_sweep(
axes: usize,
duration: f64,
from: (f64, f64),
to: (f64, f64),
) -> Result<Track, PortableError> {
Track::new(
Target::Xlim { axes },
vec![0.0, duration],
vec![vec![from.0, from.1], vec![to.0, to.1]],
Interp::Linear,
)
}
#[must_use]
pub fn len(&self) -> usize {
self.times.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.times.is_empty()
}
pub(crate) fn validate(&self) -> Result<(), String> {
if self.times.is_empty() {
return Err("has no keyframes".to_string());
}
if !self.times.windows(2).all(|w| w[0] < w[1]) {
return Err("keyframe times are not strictly increasing".to_string());
}
if self.times.len().checked_mul(self.stride) != Some(self.values.len()) {
return Err(format!(
"declares {} keyframes of stride {} but carries {} values",
self.times.len(),
self.stride,
self.values.len()
));
}
Ok(())
}
#[must_use]
pub fn sample(&self, t: f64) -> Vec<f64> {
let frame = |i: usize| &self.values[i * self.stride..(i + 1) * self.stride];
let last = self.times.len() - 1;
if t <= self.times[0] {
return frame(0).to_vec();
}
if t >= self.times[last] {
return frame(last).to_vec();
}
let hi = self.times.partition_point(|&k| k <= t);
let lo = hi - 1;
if self.interp == Interp::Step {
return frame(lo).to_vec();
}
let span = self.times[hi] - self.times[lo];
let u = if span > 0.0 {
(t - self.times[lo]) / span
} else {
0.0
};
let (a, b) = (frame(lo), frame(hi));
a.iter().zip(b).map(|(x, y)| x + (y - x) * u).collect()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Timeline {
pub duration: f64,
#[serde(rename = "loop")]
pub looping: bool,
pub tracks: Vec<Track>,
}
impl Timeline {
#[must_use]
pub fn new(duration: f64) -> Timeline {
Timeline {
duration,
looping: true,
tracks: Vec::new(),
}
}
pub fn push(&mut self, track: Track) -> &mut Self {
self.tracks.push(track);
self
}
#[must_use]
pub fn with_looping(mut self, looping: bool) -> Self {
self.looping = looping;
self
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.tracks.is_empty()
}
pub(crate) fn validate(&self) -> Result<(), PortableError> {
for (i, track) in self.tracks.iter().enumerate() {
track
.validate()
.map_err(|e| PortableError::Malformed(format!("timeline track {i}: {e}")))?;
}
Ok(())
}
#[must_use]
pub fn normalize(&self, t: f64) -> f64 {
if !t.is_finite() || self.duration <= 0.0 {
return 0.0;
}
if self.looping && t > self.duration {
t.rem_euclid(self.duration)
} else {
t.clamp(0.0, self.duration)
}
}
}