use crate::{
cacl::curve::{prebuild_curve, sample_curve, Bezier, CurveSampling, Pt2},
custom_index::NameMap,
data::runtime::skeleton::{BoneIdx, Pose},
data::runtime::skin::AttachmentNameIdx,
new_index,
};
new_index!(AnimIdx);
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum BoneChannel {
TranslateX,
TranslateY,
Rotate,
ScaleX,
ScaleY,
ShearX,
ShearY,
}
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SlotChannel {
Color,
Attachment,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BezierCurve {
pub cx1: f32,
pub cy1: f32,
pub cx2: f32,
pub cy2: f32,
}
pub const DEFAULT_CURVE_SAMPLING: CurveSampling = CurveSampling::Adaptive { eps: 0.01 };
fn scaled_sampling(method: CurveSampling, sy: f32) -> CurveSampling {
match method {
CurveSampling::Adaptive { eps } => CurveSampling::Adaptive {
eps: sy.max(1e-6) * eps,
},
other => other,
}
}
impl BezierCurve {
pub fn new(cx1: f32, cy1: f32, cx2: f32, cy2: f32) -> Self {
Self { cx1, cy1, cx2, cy2 }
}
pub fn to_bezier(&self, time1: f32, value1: f32, time2: f32, value2: f32) -> Bezier {
Bezier {
p0: [time1, value1],
p1: [self.cx1, self.cy1],
p2: [self.cx2, self.cy2],
p3: [time2, value2],
}
}
pub fn prebuild(
&self,
method: CurveSampling,
time1: f32,
value1: f32,
time2: f32,
value2: f32,
) -> Vec<Pt2> {
prebuild_curve(&self.to_bezier(time1, value1, time2, value2), method)
}
pub fn eval(&self, time: f32, time1: f32, value1: f32, time2: f32, value2: f32) -> f32 {
let method = scaled_sampling(DEFAULT_CURVE_SAMPLING, (value2 - value1).abs());
let pts = self.prebuild(method, time1, value1, time2, value2);
sample_curve(&pts, time)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Curve {
Linear,
Step,
Bezier(BezierCurve, Option<Vec<Pt2>>),
}
impl Curve {
pub fn eval(&self, time: f32, time1: f32, value1: f32, time2: f32, value2: f32) -> f32 {
match self {
Curve::Linear => {
let t = (time - time1) / (time2 - time1);
value1 + (value2 - value1) * t
}
Curve::Step => value1,
Curve::Bezier(_, Some(points)) => sample_curve(points, time),
Curve::Bezier(curve, None) => curve.eval(time, time1, value1, time2, value2),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Keyframe {
pub time: f32,
pub value: f32,
pub curve: Curve,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SlotKeyframe {
Color {
time: f32,
rgba: [f32; 4],
curves: [Curve; 4],
},
Attachment { time: f32, name: Option<AttachmentNameIdx> },
}
impl SlotKeyframe {
pub fn time(&self) -> f32 {
match self {
SlotKeyframe::Color { time, .. } | SlotKeyframe::Attachment { time, .. } => *time,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SlotTimeline {
pub slot: crate::data::runtime::slot::SlotIdx,
pub channel: SlotChannel,
pub frames: Vec<SlotKeyframe>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Timeline {
pub bone: BoneIdx,
pub channel: BoneChannel,
pub frames: Vec<Keyframe>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConstraintKeyframe {
Ik {
time: f32,
mix: Option<f32>,
bend_positive: Option<bool>,
compress: Option<bool>,
stretch: Option<bool>,
},
Transform {
time: f32,
mix_rotate: Option<f32>,
mix_x: Option<f32>,
mix_y: Option<f32>,
mix_scale_x: Option<f32>,
mix_scale_y: Option<f32>,
mix_shear_y: Option<f32>,
},
}
impl ConstraintKeyframe {
pub fn time(&self) -> f32 {
match self {
ConstraintKeyframe::Ik { time, .. } | ConstraintKeyframe::Transform { time, .. } => {
*time
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConstraintTimeline {
pub index: crate::data::runtime::constraint::ConstraintIdx,
pub frames: Vec<ConstraintKeyframe>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DeformFrame {
pub time: f32,
pub vertices: Vec<f32>,
pub curve: Curve,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DeformTimeline {
pub slot: crate::data::runtime::slot::SlotIdx,
pub mesh: crate::data::runtime::mesh::MeshIdx,
pub frames: Vec<DeformFrame>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DrawOrderFrame {
pub time: f32,
pub order: Option<Vec<i32>>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DrawOrderTimeline {
pub frames: Vec<DrawOrderFrame>,
}
#[derive(Debug, Clone)]
pub struct AnimationRT {
pub name: String,
pub duration: f32,
pub timelines: Vec<Timeline>,
pub slot_timelines: Vec<SlotTimeline>,
pub constraint_timelines: Vec<ConstraintTimeline>,
pub deform_timelines: Vec<DeformTimeline>,
pub draw_order_timeline: Option<DrawOrderTimeline>,
}
impl AnimationRT {
pub fn prebuild_curves(&mut self, method: CurveSampling) {
for tl in &mut self.timelines {
let n = tl.frames.len();
for i in 0..n.saturating_sub(1) {
let (t0, v0) = (tl.frames[i].time, tl.frames[i].value);
let (t1, v1) = (tl.frames[i + 1].time, tl.frames[i + 1].value);
if let Curve::Bezier(bc, _) = tl.frames[i].curve {
let m = scaled_sampling(method, (v1 - v0).abs());
tl.frames[i].curve = Curve::Bezier(bc, Some(bc.prebuild(m, t0, v0, t1, v1)));
}
}
}
for tl in &mut self.slot_timelines {
let n = tl.frames.len();
for i in 0..n.saturating_sub(1) {
let (t0, t1) = (tl.frames[i].time(), tl.frames[i + 1].time());
let (SlotKeyframe::Color { rgba: c0, curves, .. }, SlotKeyframe::Color { rgba: c1, .. }) =
(&tl.frames[i], &tl.frames[i + 1])
else {
continue;
};
let mut new_curves = [Curve::Linear, Curve::Linear, Curve::Linear, Curve::Linear];
for k in 0..4 {
new_curves[k] = match &curves[k] {
Curve::Bezier(bc, _) => {
let m = scaled_sampling(method, (c1[k] - c0[k]).abs());
Curve::Bezier(*bc, Some(bc.prebuild(m, t0, c0[k], t1, c1[k])))
}
other => other.clone(),
};
}
if let SlotKeyframe::Color { curves: dest, .. } = &mut tl.frames[i] {
*dest = new_curves;
}
}
}
for tl in &mut self.deform_timelines {
let n = tl.frames.len();
for i in 0..n.saturating_sub(1) {
let (t0, t1) = (tl.frames[i].time, tl.frames[i + 1].time);
if let Curve::Bezier(bc, _) = &tl.frames[i].curve {
let m = scaled_sampling(method, 1.0); tl.frames[i].curve =
Curve::Bezier(*bc, Some(bc.prebuild(m, t0, 0.0, t1, 1.0)));
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct AnimateSet {
data: NameMap<AnimIdx, AnimationRT>,
}
impl Default for AnimateSet {
fn default() -> Self {
Self::new()
}
}
impl AnimateSet {
pub fn new() -> Self {
Self {
data: NameMap::new(),
}
}
pub fn insert(&mut self, name: impl Into<String>, animation: AnimationRT) -> AnimIdx {
self.data.insert(name.into(), animation)
}
pub fn get_index(&self, name: &str) -> Option<AnimIdx> {
self.data.get_index(name)
}
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.data.keys()
}
pub fn duration(&self, anim: AnimIdx) -> f32 {
self.data[anim].duration
}
pub fn slot_timeline_count(&self, anim: AnimIdx) -> usize {
self.data[anim].slot_timelines.len()
}
pub fn slot_timelines(&self, anim: AnimIdx) -> &[SlotTimeline] {
&self.data[anim].slot_timelines
}
pub fn slot_timelines_mut(&mut self, anim: AnimIdx) -> &mut Vec<SlotTimeline> {
&mut self.data[anim].slot_timelines
}
pub fn sample(&self, anim: AnimIdx, config: &mut AnimateConfig, t: f32) -> SampledAnimate {
let animation = &self.data[anim];
if config.anim_idx != Some(anim) {
config.anim_idx = Some(anim);
config.last_frames.clear();
config.last_frames.resize(
animation.timelines.len() + animation.slot_timelines.len(),
0,
);
}
let mut values = Vec::with_capacity(animation.timelines.len());
for (i, timeline) in animation.timelines.iter().enumerate() {
if let Some(value) = sample_frames(&timeline.frames, t, &mut config.last_frames[i]) {
values.push((timeline.bone, timeline.channel, value));
}
}
let mut slot_values = Vec::with_capacity(animation.slot_timelines.len());
for (i, timeline) in animation.slot_timelines.iter().enumerate() {
let cache = &mut config.last_frames[animation.timelines.len() + i];
let value = sample_slot_frames(timeline, t, cache);
slot_values.push((timeline.slot, timeline.channel, value));
}
let mut constraint_values = Vec::with_capacity(animation.constraint_timelines.len());
for timeline in &animation.constraint_timelines {
let frame = timeline.frames.iter().rev().find(|f| f.time() <= t);
if let Some(frame) = frame {
constraint_values.push((timeline.index, frame.clone()));
}
}
let mut deform_values = Vec::with_capacity(animation.deform_timelines.len());
for timeline in &animation.deform_timelines {
let Some(frame) = sample_deform_frames(&timeline.frames, t) else {
continue;
};
deform_values.push((timeline.slot, timeline.mesh, frame));
}
let draw_order = animation
.draw_order_timeline
.as_ref()
.and_then(|tl| {
tl.frames
.iter()
.rev()
.find(|f| f.time <= t)
.map(|f| f.order.clone())
})
.flatten();
SampledAnimate {
values,
slot_values,
constraint_values,
deform_values,
draw_order,
}
}
}
fn sample_deform_frames(frames: &[DeformFrame], t: f32) -> Option<Vec<f32>> {
let (idx, _) = frames.iter().enumerate().rev().find(|(_, f)| f.time <= t)?;
if idx + 1 >= frames.len() {
return Some(frames[idx].vertices.clone());
}
let prev = &frames[idx];
let next = &frames[idx + 1];
let p = match &prev.curve {
Curve::Linear => {
let span = next.time - prev.time;
if span <= 0.0 {
0.0
} else {
(t - prev.time) / span
}
}
Curve::Step => 0.0,
Curve::Bezier(_, Some(points)) => sample_curve(points, t),
Curve::Bezier(curve, None) => curve.eval(t, prev.time, 0.0, next.time, 1.0),
};
let len = prev.vertices.len().min(next.vertices.len());
let mut out = Vec::with_capacity(len);
for i in 0..len {
out.push(prev.vertices[i] + (next.vertices[i] - prev.vertices[i]) * p);
}
Some(out)
}
#[derive(Debug, Clone, Default)]
pub struct AnimateConfig {
anim_idx: Option<AnimIdx>,
last_frames: Vec<u32>,
}
impl AnimateConfig {
pub fn new() -> Self {
Self {
anim_idx: None,
last_frames: vec![],
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SampledAnimate {
pub values: Vec<(BoneIdx, BoneChannel, f32)>,
pub slot_values: Vec<(
crate::data::runtime::slot::SlotIdx,
SlotChannel,
SampledSlotValue,
)>,
pub constraint_values: Vec<(
crate::data::runtime::constraint::ConstraintIdx,
ConstraintKeyframe,
)>,
pub deform_values: Vec<(
crate::data::runtime::slot::SlotIdx,
crate::data::runtime::mesh::MeshIdx,
Vec<f32>,
)>,
pub draw_order: Option<Vec<i32>>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SampledSlotValue {
Color(Option<[f32; 4]>),
Attachment(SampledAttachment),
}
#[derive(Debug, Clone, PartialEq)]
pub enum SampledAttachment {
NotStarted,
Set(Option<AttachmentNameIdx>),
}
impl SampledAnimate {
pub fn apply_pose(&self, pose: &mut Pose) {
if self.values.is_empty() {
return;
}
for &(bone, channel, value) in &self.values {
let data = &mut pose.bone_data[bone];
match channel {
BoneChannel::TranslateX => data.translate_x += value,
BoneChannel::TranslateY => data.translate_y += value,
BoneChannel::Rotate => data.rotation_deg += value,
BoneChannel::ScaleX => data.scale_x *= value,
BoneChannel::ScaleY => data.scale_y *= value,
BoneChannel::ShearX => data.shear_x_deg += value,
BoneChannel::ShearY => data.shear_y_deg += value,
}
}
pose.dirty = true;
}
pub fn apply_constraints(&self, state: &mut crate::data::runtime::constraint::ConstraintState) {
for (index, frame) in &self.constraint_values {
use crate::data::runtime::constraint::ConstraintIdx;
match (index, frame) {
(
ConstraintIdx::Ik(i),
ConstraintKeyframe::Ik {
mix,
bend_positive,
compress,
stretch,
..
},
) => {
let s = &mut state.ik[*i as usize];
if let Some(v) = mix {
s.mix = *v;
}
if let Some(v) = bend_positive {
s.bend_positive = *v;
}
if let Some(v) = compress {
s.compress = *v;
}
if let Some(v) = stretch {
s.stretch = *v;
}
}
(
ConstraintIdx::Transform(i),
ConstraintKeyframe::Transform {
mix_rotate,
mix_x,
mix_y,
mix_scale_x,
mix_scale_y,
mix_shear_y,
..
},
) => {
let s = &mut state.transform[*i as usize];
if let Some(v) = mix_rotate {
s.mix_rotate = *v;
}
if let Some(v) = mix_x {
s.mix_x = *v;
}
if let Some(v) = mix_y {
s.mix_y = *v;
}
if let Some(v) = mix_scale_x {
s.mix_scale_x = *v;
}
if let Some(v) = mix_scale_y {
s.mix_scale_y = *v;
}
if let Some(v) = mix_shear_y {
s.mix_shear_y = *v;
}
}
_ => {}
}
}
}
}
fn sample_frames(frames: &[Keyframe], t: f32, last: &mut u32) -> Option<f32> {
debug_assert!(!frames.is_empty());
if t < frames[0].time {
return None;
}
if frames.len() == 1 {
return Some(frames[0].value);
}
let last_time = frames.last().unwrap().time;
let t = t.min(last_time);
let mut idx = (*last as usize).min(frames.len() - 2);
if t < frames[idx].time || t >= frames[idx + 1].time {
let pp = frames.partition_point(|f| f.time <= t);
idx = pp.saturating_sub(1).min(frames.len() - 2);
*last = idx as u32;
}
let f0 = &frames[idx];
let f1 = &frames[idx + 1];
let span = f1.time - f0.time;
if span <= 0.0 {
return Some(f1.value);
}
Some(f0.curve.eval(t, f0.time, f0.value, f1.time, f1.value))
}
fn sample_slot_frames(timeline: &SlotTimeline, t: f32, last: &mut u32) -> SampledSlotValue {
match timeline.channel {
SlotChannel::Color => {
SampledSlotValue::Color(sample_color_frames(&timeline.frames, t, last))
}
SlotChannel::Attachment => {
SampledSlotValue::Attachment(sample_attachment_frames(&timeline.frames, t))
}
}
}
fn sample_color_frames(frames: &[SlotKeyframe], t: f32, last: &mut u32) -> Option<[f32; 4]> {
debug_assert!(!frames.is_empty());
if t < frames[0].time() {
*last = 0;
return None;
}
if frames.len() == 1 {
return Some(color_at(&frames[0]));
}
let last_time = frames.last().unwrap().time();
let t = t.min(last_time);
let mut idx = (*last as usize).min(frames.len() - 2);
if t < frames[idx].time() || t >= frames[idx + 1].time() {
let pp = frames.partition_point(|f| f.time() <= t);
idx = pp.saturating_sub(1).min(frames.len() - 2);
*last = idx as u32;
}
let (f0, f1) = (&frames[idx], &frames[idx + 1]);
let (t0, c0) = (f0.time(), color_at(f0));
let (t1, c1) = (f1.time(), color_at(f1));
let span = t1 - t0;
if span <= 0.0 {
return Some(c1);
}
let mut out = [0.0f32; 4];
for i in 0..4 {
out[i] = curves_of(f0)[i].eval(t, t0, c0[i], t1, c1[i]);
}
Some(out)
}
fn sample_attachment_frames(frames: &[SlotKeyframe], t: f32) -> SampledAttachment {
debug_assert!(!frames.is_empty());
if t < frames[0].time() {
return SampledAttachment::NotStarted;
}
let idx = frames.partition_point(|f| f.time() <= t);
let SlotKeyframe::Attachment { name, .. } = &frames[idx.saturating_sub(1)] else {
return SampledAttachment::Set(None);
};
SampledAttachment::Set(*name)
}
fn color_at(frame: &SlotKeyframe) -> [f32; 4] {
let SlotKeyframe::Color { rgba, .. } = frame else {
panic!("expected color frame");
};
*rgba
}
fn curves_of(frame: &SlotKeyframe) -> &[Curve; 4] {
let SlotKeyframe::Color { curves, .. } = frame else {
panic!("expected color frame");
};
curves
}
#[cfg(test)]
mod tests {
use super::*;
use crate::custom_index::AsIndex;
fn simple_animation() -> AnimationRT {
AnimationRT {
name: "test".to_string(),
duration: 1.0,
slot_timelines: vec![],
constraint_timelines: vec![],
deform_timelines: vec![],
draw_order_timeline: None,
timelines: vec![Timeline {
bone: unsafe { BoneIdx::from_index(0) },
channel: BoneChannel::TranslateX,
frames: vec![
Keyframe {
time: 0.0,
value: 0.0,
curve: Curve::Linear,
},
Keyframe {
time: 1.0,
value: 10.0,
curve: Curve::Linear,
},
],
}],
}
}
#[test]
fn slot_color_sampling_interpolates() {
use crate::data::runtime::slot::SlotIdx;
let mut set = AnimateSet::new();
let mut anim = simple_animation();
anim.slot_timelines.push(SlotTimeline {
slot: unsafe { SlotIdx::from_index(0) },
channel: SlotChannel::Color,
frames: vec![
SlotKeyframe::Color {
time: 0.0,
rgba: [1.0, 0.0, 0.0, 1.0],
curves: [
Curve::Linear,
Curve::Linear,
Curve::Linear,
Curve::Linear,
],
},
SlotKeyframe::Color {
time: 1.0,
rgba: [0.0, 1.0, 0.0, 1.0],
curves: [
Curve::Linear,
Curve::Linear,
Curve::Linear,
Curve::Linear,
],
},
],
});
let idx = set.insert("c", anim);
let mut config = AnimateConfig::new();
let s = set.sample(idx, &mut config, 0.5);
let SampledSlotValue::Color(Some(c)) = s.slot_values[0].2 else {
panic!("expected color");
};
assert!((c[0] - 0.5).abs() < 1e-5);
assert!((c[1] - 0.5).abs() < 1e-5);
assert_eq!(c[2], 0.0);
assert_eq!(c[3], 1.0);
}
#[test]
fn slot_color_before_first_frame_is_none() {
use crate::data::runtime::slot::SlotIdx;
let mut set = AnimateSet::new();
let mut anim = simple_animation();
anim.slot_timelines.push(SlotTimeline {
slot: unsafe { SlotIdx::from_index(0) },
channel: SlotChannel::Color,
frames: vec![SlotKeyframe::Color {
time: 0.5,
rgba: [1.0, 0.0, 0.0, 1.0],
curves: [
Curve::Linear,
Curve::Linear,
Curve::Linear,
Curve::Linear,
],
}],
});
let idx = set.insert("c", anim);
let mut config = AnimateConfig::new();
let s = set.sample(idx, &mut config, 0.0);
assert_eq!(s.slot_values[0].2, SampledSlotValue::Color(None));
let s = set.sample(idx, &mut config, 0.5);
assert_eq!(
s.slot_values[0].2,
SampledSlotValue::Color(Some([1.0, 0.0, 0.0, 1.0]))
);
}
#[test]
fn slot_color_out_of_range_clamps() {
use crate::data::runtime::slot::SlotIdx;
let mut set = AnimateSet::new();
let mut anim = simple_animation();
anim.slot_timelines.push(SlotTimeline {
slot: unsafe { SlotIdx::from_index(0) },
channel: SlotChannel::Color,
frames: vec![SlotKeyframe::Color {
time: 0.0,
rgba: [0.2, 0.4, 0.6, 0.8],
curves: [
Curve::Linear,
Curve::Linear,
Curve::Linear,
Curve::Linear,
],
}],
});
let idx = set.insert("c", anim);
let mut config = AnimateConfig::new();
let s = set.sample(idx, &mut config, 100.0);
assert_eq!(
s.slot_values[0].2,
SampledSlotValue::Color(Some([0.2, 0.4, 0.6, 0.8]))
);
}
#[test]
fn slot_attachment_switching() {
use crate::data::runtime::slot::SlotIdx;
let open = unsafe { AttachmentNameIdx::from_index(0) };
let closed = unsafe { AttachmentNameIdx::from_index(1) };
let mut set = AnimateSet::new();
let mut anim = simple_animation();
anim.slot_timelines.push(SlotTimeline {
slot: unsafe { SlotIdx::from_index(0) },
channel: SlotChannel::Attachment,
frames: vec![
SlotKeyframe::Attachment {
time: 0.0,
name: None,
},
SlotKeyframe::Attachment {
time: 0.5,
name: Some(open),
},
SlotKeyframe::Attachment {
time: 0.8,
name: Some(closed),
},
],
});
let idx = set.insert("a", anim);
let mut config = AnimateConfig::new();
let s = set.sample(idx, &mut config, 0.0);
assert_eq!(
s.slot_values[0].2,
SampledSlotValue::Attachment(SampledAttachment::Set(None))
);
let s = set.sample(idx, &mut config, 0.5);
assert_eq!(
s.slot_values[0].2,
SampledSlotValue::Attachment(SampledAttachment::Set(Some(open)))
);
let s = set.sample(idx, &mut config, 0.9);
assert_eq!(
s.slot_values[0].2,
SampledSlotValue::Attachment(SampledAttachment::Set(Some(closed)))
);
let s = set.sample(idx, &mut config, 100.0);
assert_eq!(
s.slot_values[0].2,
SampledSlotValue::Attachment(SampledAttachment::Set(Some(closed)))
);
}
#[test]
fn slot_attachment_before_first_frame_is_not_started() {
use crate::data::runtime::slot::SlotIdx;
let mut set = AnimateSet::new();
let mut anim = simple_animation();
anim.slot_timelines.push(SlotTimeline {
slot: unsafe { SlotIdx::from_index(0) },
channel: SlotChannel::Attachment,
frames: vec![SlotKeyframe::Attachment {
time: 0.5,
name: Some(unsafe { AttachmentNameIdx::from_index(0) }),
}],
});
let idx = set.insert("a", anim);
let mut config = AnimateConfig::new();
let s = set.sample(idx, &mut config, 0.0);
assert_eq!(
s.slot_values[0].2,
SampledSlotValue::Attachment(SampledAttachment::NotStarted)
);
let s = set.sample(idx, &mut config, 0.5);
assert_eq!(
s.slot_values[0].2,
SampledSlotValue::Attachment(SampledAttachment::Set(Some(unsafe {
AttachmentNameIdx::from_index(0)
})))
);
}
#[test]
fn bone_channel_before_first_frame_not_emitted() {
let mut set = AnimateSet::new();
let mut anim = simple_animation();
anim.timelines[0].frames = vec![
Keyframe {
time: 1.0,
value: 10.0,
curve: Curve::Linear,
},
Keyframe {
time: 2.0,
value: 20.0,
curve: Curve::Linear,
},
];
let idx = set.insert("a", anim);
let mut config = AnimateConfig::new();
let s = set.sample(idx, &mut config, 0.5);
assert!(s.values.is_empty());
let s = set.sample(idx, &mut config, 1.0);
assert_eq!(
s.values,
vec![(
unsafe { BoneIdx::from_index(0) },
BoneChannel::TranslateX,
10.0
)]
);
let s = set.sample(idx, &mut config, 1.5);
assert_eq!(s.values[0].2, 15.0);
}
#[test]
fn linear_sample() {
let mut set = AnimateSet::new();
let idx = set.insert("a", simple_animation());
let mut config = AnimateConfig::new();
let s = set.sample(idx, &mut config, 0.5);
assert_eq!(
s.values,
vec![(
unsafe { BoneIdx::from_index(0) },
BoneChannel::TranslateX,
5.0
)]
);
}
#[test]
fn prebuild_curves_converts_bezier_to_sampled() {
let mut anim = simple_animation();
anim.timelines[0].frames[0].curve =
Curve::Bezier(BezierCurve::new(0.3, 0.0, 0.7, 10.0), None);
anim.prebuild_curves(DEFAULT_CURVE_SAMPLING);
assert!(matches!(
anim.timelines[0].frames[0].curve,
Curve::Bezier(_, Some(_))
));
assert!(matches!(
anim.timelines[0].frames[1].curve,
Curve::Linear
));
let mut set = AnimateSet::new();
let idx = set.insert("a", anim);
let mut config = AnimateConfig::new();
let curve = BezierCurve::new(0.3, 0.0, 0.7, 10.0);
for t in [0.0, 0.1, 0.5, 0.9, 1.0] {
let s = set.sample(idx, &mut config, t);
let v = s.values[0].2;
let exact = curve.eval(t, 0.0, 0.0, 1.0, 10.0);
assert!((v - exact).abs() < 1e-5, "t={t} v={v} exact={exact}");
}
}
#[test]
fn step_curve() {
let mut anim = simple_animation();
anim.timelines[0].frames[0].curve = Curve::Step;
let mut set = AnimateSet::new();
let idx = set.insert("a", anim);
let mut config = AnimateConfig::new();
let s = set.sample(idx, &mut config, 0.99);
assert_eq!(s.values[0].2, 0.0);
}
#[test]
fn clamp_out_of_range() {
let mut set = AnimateSet::new();
let idx = set.insert("a", simple_animation());
let mut config = AnimateConfig::new();
let s = set.sample(idx, &mut config, 100.0);
assert_eq!(s.values[0].2, 10.0);
}
#[test]
fn single_frame() {
let mut anim = simple_animation();
anim.timelines[0].frames = vec![Keyframe {
time: 0.0,
value: 7.0,
curve: Curve::Linear,
}];
anim.duration = 0.0;
let mut set = AnimateSet::new();
let idx = set.insert("a", anim);
let mut config = AnimateConfig::new();
let s = set.sample(idx, &mut config, 0.0);
assert_eq!(s.values[0].2, 7.0);
}
#[test]
fn config_switch_animation() {
let mut set = AnimateSet::new();
let idx_a = set.insert("a", simple_animation());
let idx_b = {
let mut anim = simple_animation();
anim.timelines[0].frames[1].value = 20.0;
set.insert("b", anim)
};
let mut config = AnimateConfig::new();
let s = set.sample(idx_a, &mut config, 0.5);
assert_eq!(s.values[0].2, 5.0);
let s = set.sample(idx_b, &mut config, 0.5);
assert_eq!(s.values[0].2, 10.0);
let s = set.sample(idx_a, &mut config, 0.5);
assert_eq!(s.values[0].2, 5.0);
}
#[test]
fn bezier_spine_coordinates() {
let curve = BezierCurve::new(0.3, 0.0, 0.7, 10.0);
assert!((curve.eval(0.0, 0.0, 0.0, 1.0, 10.0) - 0.0).abs() < 1e-4);
assert!((curve.eval(1.0, 0.0, 0.0, 1.0, 10.0) - 10.0).abs() < 1e-4);
let mid = curve.eval(0.5, 0.0, 0.0, 1.0, 10.0);
assert!(mid > 0.0 && mid < 10.0, "mid={mid}");
assert!((mid - 5.0).abs() < 0.1, "mid={mid}");
}
#[test]
fn bezier_monotonic_in_frame_space() {
let curve = BezierCurve::new(0.3, 2.0, 0.7, 0.5);
let mid = curve.eval(0.5, 0.0, 2.0, 1.0, 0.0);
assert!(mid > 0.0 && mid < 2.0, "mid={mid}");
let near_start = curve.eval(0.2, 0.0, 2.0, 1.0, 0.0);
assert!((near_start - 2.0).abs() < 0.5, "near_start={near_start}");
}
#[test]
fn apply_pose_adds_delta() {
use crate::data::runtime::skeleton::BoneDataStatic;
let mut skel = crate::data::runtime::skeleton::Skeleton::new();
skel.insert_bone("root", BoneDataStatic::default()).unwrap();
let mut pose = skel.default_pose();
pose.bone_data[unsafe { BoneIdx::from_index(0) }].translate_x = 100.0;
let mut set = AnimateSet::new();
let mut anim = simple_animation();
anim.timelines.push(Timeline {
bone: unsafe { BoneIdx::from_index(0) },
channel: BoneChannel::Rotate,
frames: vec![Keyframe {
time: 0.0,
value: 45.0,
curve: Curve::Linear,
}],
});
let idx = set.insert("a", anim);
let mut config = AnimateConfig::new();
let s = set.sample(idx, &mut config, 0.0);
s.apply_pose(&mut pose);
assert!(pose.dirty);
assert_eq!(
pose.bone_data[unsafe { BoneIdx::from_index(0) }].translate_x,
100.0
);
assert_eq!(
pose.bone_data[unsafe { BoneIdx::from_index(0) }].rotation_deg,
45.0
);
let _ = pose.get_world_affine(&skel, unsafe { BoneIdx::from_index(0) });
}
#[test]
fn apply_pose_scale_multiplies_delta() {
use crate::data::runtime::skeleton::BoneDataStatic;
let mut skel = crate::data::runtime::skeleton::Skeleton::new();
skel.insert_bone("root", BoneDataStatic::default()).unwrap();
let mut pose = skel.default_pose();
pose.bone_data[unsafe { BoneIdx::from_index(0) }].scale_x = 2.0;
pose.bone_data[unsafe { BoneIdx::from_index(0) }].scale_y = 3.0;
let mut set = AnimateSet::new();
let mut anim = simple_animation();
anim.timelines.push(Timeline {
bone: unsafe { BoneIdx::from_index(0) },
channel: BoneChannel::ScaleY,
frames: vec![Keyframe {
time: 0.0,
value: 1.5,
curve: Curve::Linear,
}],
});
let idx = set.insert("a", anim);
let mut config = AnimateConfig::new();
let s = set.sample(idx, &mut config, 0.0);
s.apply_pose(&mut pose);
assert_eq!(
pose.bone_data[unsafe { BoneIdx::from_index(0) }].scale_x,
2.0
);
assert_eq!(
pose.bone_data[unsafe { BoneIdx::from_index(0) }].scale_y,
4.5
);
}
}