use std::{fs, path::Path};
use crate::{
json::{
Motion3, apply_motion_fade, motion_fade_in_weight, motion_fade_out_weight,
parameter_curve_fade_weight,
},
runtime::ModelRuntime,
};
const PARAMETER_TARGET: &str = "Parameter";
const PART_OPACITY_TARGET: &str = "PartOpacity";
#[derive(Debug, Clone)]
pub struct MotionPlayer {
motion: Motion3,
time: f32,
weight: f32,
looping: bool,
finished: bool,
}
impl MotionPlayer {
pub fn new(motion: Motion3) -> Self {
let looping = motion.meta().is_looping();
Self::with_looping(motion, looping)
}
pub fn new_once(motion: Motion3) -> Self {
Self::with_looping(motion, false)
}
pub fn with_looping(motion: Motion3, looping: bool) -> Self {
Self {
motion,
time: 0.0,
weight: 1.0,
looping,
finished: false,
}
}
pub fn motion(&self) -> &Motion3 {
&self.motion
}
pub fn time(&self) -> f32 {
self.time
}
pub fn weight(&self) -> f32 {
self.weight
}
pub fn is_looping(&self) -> bool {
self.looping
}
pub fn set_weight(&mut self, weight: f32) {
self.weight = weight.clamp(0.0, 1.0);
}
pub fn is_finished(&self) -> bool {
self.finished
}
pub fn restart(&mut self) {
self.time = 0.0;
self.finished = false;
}
pub fn tick(&mut self, delta_seconds: f32) {
if self.finished {
return;
}
self.time += delta_seconds.max(0.0);
let duration = self.motion.meta().duration();
if duration <= 0.0 {
return;
}
if self.looping {
self.time %= duration;
} else if self.time >= duration {
self.time = duration;
self.finished = true;
}
}
pub fn apply(&self, runtime: &mut ModelRuntime) {
let duration = self.motion.meta().duration();
let end_time = if self.looping { -1.0 } else { duration };
let fade_in = motion_fade_in_weight(self.time, 0.0, 0.0);
let fade_out = motion_fade_out_weight(self.time, end_time, 0.0);
for curve in self.motion.curves() {
let Some(sampled) = curve.sample(self.time) else {
continue;
};
let curve_weight = parameter_curve_fade_weight(
self.weight,
fade_in,
fade_out,
curve.fade_in_time(),
curve.fade_out_time(),
self.time,
0.0,
end_time,
);
match curve.target() {
PARAMETER_TARGET => {
let Some(index) = runtime.parameter_index(curve.id()) else {
continue;
};
let Some(current) = runtime.parameter_value_by_index(index) else {
continue;
};
let value = apply_motion_fade(current, sampled, curve_weight);
runtime.set_parameter_by_index(index, value);
}
PART_OPACITY_TARGET => {
let Some(index) = runtime.part_index(curve.id()) else {
continue;
};
let value = apply_motion_fade(1.0, sampled, curve_weight);
runtime.set_part_opacity_by_index(index, value);
}
_ => {}
}
}
}
}
pub fn load_motion(path: impl AsRef<Path>) -> Result<Motion3, MotionLoadError> {
let path = path.as_ref();
let source = fs::read_to_string(path).map_err(|source| MotionLoadError::Io {
path: path.display().to_string(),
source,
})?;
Motion3::from_json_str(&source).map_err(MotionLoadError::Parse)
}
#[derive(Debug, thiserror::Error)]
pub enum MotionLoadError {
#[error("failed to read {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("failed to parse motion3: {0}")]
Parse(#[source] crate::Error),
}