spine2d 0.4.0

Pure Rust runtime for Spine 4.3 (unofficial)
Documentation
use crate::runtime::MixBlend;
use crate::{Skeleton, SkeletonData, apply_animation, build_draw_list};

const SKELETON_SLOT_RGBA_TIMELINE: &str = r#"
{
  "skeleton": { "spine": "4.3.00" },
  "bones": [ { "name": "root" } ],
  "slots": [ { "name": "slot0", "bone": "root", "attachment": "a", "color": "FF0000FF" } ],
  "skins": {
    "default": {
      "slot0": {
        "a": { "type": "region", "path": "a.png", "width": 10, "height": 10 }
      }
    }
  },
  "animations": {
    "anim": {
      "slots": {
        "slot0": {
          "rgba": [
            { "time": 0.0, "color": "00FF00FF" },
            { "time": 1.0, "color": "0000FFFF" }
          ]
        }
      }
    }
  }
}
"#;

const SKELETON_SLOT_RGBA2_TIMELINE: &str = r#"
{
  "skeleton": { "spine": "4.3.00" },
  "bones": [ { "name": "root" } ],
  "slots": [
    {
      "name": "slot0",
      "bone": "root",
      "attachment": "a",
      "color": "FF0000FF",
      "dark": "0000FFFF"
    }
  ],
  "skins": {
    "default": {
      "slot0": {
        "a": { "type": "region", "path": "a.png", "width": 10, "height": 10 }
      }
    }
  },
  "animations": {
    "anim": {
      "slots": {
        "slot0": {
          "rgba2": [
            { "time": 0.0, "light": "00FF00FF", "dark": "FF0000" }
          ]
        }
      }
    }
  }
}
"#;

fn assert_approx(actual: f32, expected: f32) {
    let diff = (actual - expected).abs();
    assert!(
        diff <= 1.0e-6,
        "expected {expected}, got {actual} (diff {diff})"
    );
}

#[test]
fn slot_color_timeline_interpolates_and_affects_draw_list_vertex_colors() {
    let data = SkeletonData::from_json_str(SKELETON_SLOT_RGBA_TIMELINE).unwrap();
    let animation = data.find_animation("anim").unwrap();

    let mut skeleton = Skeleton::new(data.clone());
    skeleton.setup_pose();
    skeleton.update_world_transform_with_physics(crate::Physics::None);

    apply_animation(animation, &mut skeleton, 0.5, false, 1.0, MixBlend::Replace);
    assert_approx(skeleton.slots[0].color[0], 0.0);
    assert_approx(skeleton.slots[0].color[1], 0.5);
    assert_approx(skeleton.slots[0].color[2], 0.5);
    assert_approx(skeleton.slots[0].color[3], 1.0);

    let draw_list = build_draw_list(&skeleton);
    assert!(!draw_list.vertices.is_empty());
    assert_approx(draw_list.vertices[0].color[0], 0.0);
    assert_approx(draw_list.vertices[0].color[1], 0.5);
    assert_approx(draw_list.vertices[0].color[2], 0.5);
    assert_approx(draw_list.vertices[0].color[3], 1.0);
}

#[test]
fn slot_color_timeline_applies_negative_alpha_like_cpp() {
    let data = SkeletonData::from_json_str(SKELETON_SLOT_RGBA_TIMELINE).unwrap();
    let animation = data.find_animation("anim").unwrap();

    let mut skeleton = Skeleton::new(data.clone());
    skeleton.setup_pose();
    skeleton.update_world_transform_with_physics(crate::Physics::None);

    apply_animation(
        animation,
        &mut skeleton,
        0.5,
        false,
        -0.5,
        MixBlend::Replace,
    );
    assert_approx(skeleton.slots[0].color[0], 1.5);
    assert_approx(skeleton.slots[0].color[1], -0.25);
    assert_approx(skeleton.slots[0].color[2], -0.25);
    assert_approx(skeleton.slots[0].color[3], 1.0);
}

#[test]
fn slot_rgba2_timeline_applies_negative_alpha_to_dark_color_like_cpp() {
    let data = SkeletonData::from_json_str(SKELETON_SLOT_RGBA2_TIMELINE).unwrap();
    let animation = data.find_animation("anim").unwrap();

    let mut skeleton = Skeleton::new(data.clone());
    skeleton.setup_pose();
    skeleton.update_world_transform_with_physics(crate::Physics::None);

    apply_animation(
        animation,
        &mut skeleton,
        0.0,
        false,
        -0.5,
        MixBlend::Replace,
    );
    assert_approx(skeleton.slots[0].color[0], 1.5);
    assert_approx(skeleton.slots[0].color[1], -0.5);
    assert_approx(skeleton.slots[0].color[2], 0.0);
    assert_approx(skeleton.slots[0].color[3], 1.0);
    assert!(skeleton.slots[0].has_dark_color());
    assert_approx(skeleton.slots[0].get_dark_color()[0], -0.5);
    assert_approx(skeleton.slots[0].get_dark_color()[1], 0.0);
    assert_approx(skeleton.slots[0].get_dark_color()[2], 1.5);
}