#include #include "animation.hpp" #include "textures.hpp" #include "dinkyecs.hpp" #include "config.hpp" #include #include #include #include #include "rand.hpp" using namespace components; using namespace textures; using namespace std::chrono_literals; struct Sheet { int width{0}; int height{0}; int frame_width{0}; int frame_height{0}; }; struct Sequence { std::vector frames{}; std::vector durations{}; size_t current{0}; int loop_count{0}; size_t frame_count{frames.size()}; sf::Clock timer{}; }; struct Transform { // how to know when a transform ends? float min_x{1.0f}; float min_y{1.0f}; float max_x{1.0f}; float max_y{1.0f}; bool simple{true}; bool flipped{false}; float ease_rate{0.5f}; bool scaled{false}; bool stationary{false}; // these can handled by the onLoop, same as ganim8 does it bool toggled{false}; bool looped{false}; std::shared_ptr shader{nullptr}; // change to using a callback function for these ease::Style easing = ease::IN_OUT_BACK; ease::Motion motion = ease::RUSH; }; /* Gets the number of times it looped, and returns if it should stop. */ using OnLoopHandler = std::function; using OnFrameHandler = std::function; bool DefaultOnLoop(Sequence& seq, Transform& tr) { seq.current = 0; return tr.looped; } class Animate2 { public: Sheet $sheet; Sequence $sequence; Transform $transform; OnFrameHandler onFrame = nullptr; std::vector $frame_rects{calc_frames()}; OnLoopHandler onLoop = DefaultOnLoop; bool playing = false; std::vector calc_frames() { std::vector frames; for(int frame_i : $sequence.frames) { frames.emplace_back( sf::Vector2i{$sheet.frame_width * frame_i, 0}, // NOTE: one row only for now sf::Vector2i{$sheet.frame_width, $sheet.frame_height}); } return frames; } void play() { dbc::check(!playing, "can't call play while playing?"); $sequence.current = 0; $sequence.loop_count = 0; playing = true; $sequence.timer.start(); } void stop() { playing = false; $sequence.timer.reset(); } // need one for each kind of thing to animate // NOTE: possibly find a way to only run apply on frame change? void apply(sf::Sprite& sprite, sf::Vector2f pos) { dbc::check($sequence.current < $frame_rects.size(), "current frame past $frame_rects"); // NOTE: pos is not updated yet auto& rect = $frame_rects.at($sequence.current); sprite.setTextureRect(rect); } // replaces step void update_frame() { dbc::check(playing, "attempt to update animation that's not playing"); dbc::check($sequence.frame_count == $sequence.frames.size(), "frame_count doesn't match frame.size()"); auto duration = $sequence.durations.at($sequence.current); bool frame_change = false; if($sequence.timer.getElapsedTime() >= duration) { $sequence.timer.restart(); $sequence.current++; frame_change = true; } if($sequence.current >= $sequence.frame_count) { $sequence.loop_count++; playing = onLoop($sequence, $transform); } if(frame_change && onFrame != nullptr) onFrame(); dbc::check($sequence.current < $sequence.frame_count, "onLoop fail: current frame out of frames.size()"); } void update() { update_frame(); } }; Animate2 crafter() { Sheet sheet{ .width{720*2}, .height{720}, .frame_width{720}, .frame_height{720}, }; Sequence sequence{ .frames{0,1}, .durations{Random::milliseconds(1, 100), Random::milliseconds(1, 100)} }; REQUIRE(sequence.frame_count == sequence.frames.size()); REQUIRE(sequence.frame_count == sequence.durations.size()); Transform transform{ .min_x{1.0f}, .min_y{1.0f}, .max_x{1.0f}, .max_y{1.0f}, .simple{true}, .flipped{false}, .ease_rate{0.5f}, .scaled{false}, .stationary{false}, .toggled{false}, .looped{false}, }; return {sheet, sequence, transform}; } void PLAY_TEST(Animate2 &anim) { anim.play(); while(anim.playing) { anim.update(); std::this_thread::sleep_for(Random::milliseconds(1, 100)); } REQUIRE(anim.playing == false); } /* * Animation is a Sheet + Sequence + Transform. * * A Sheet is just a grid of images with a predefined size for each cell. Arbitrary sized cells not supported. * * A Sequence is a list of Sheet cells _in any order_. See https://github.com/yottahmd/ganim8-lib. Sequences have a timing element for the cells, possibly a list of durations or a single duration. * * A Transform is combinations of scale and/or position easing/motion, shader effects, and things like if it's looped or toggled. * * I like the ganim8 onLoop concept, just a callback that says what to do when the animation has looped. */ TEST_CASE("new animation system", "[animation-new]") { textures::init(); auto anim = crafter(); PLAY_TEST(anim); bool onLoop_ran = false; anim.onLoop = [&](auto& seq, auto& tr) -> bool { seq.current = 0; onLoop_ran = true; return tr.looped; }; PLAY_TEST(anim); REQUIRE(onLoop_ran == true); // only runs twice anim.onLoop = [](auto& seq, auto& tr) -> bool { if(seq.loop_count == 2) { seq.current = 0; return false; } else { seq.current = seq.current % seq.frame_count; return true; } }; PLAY_TEST(anim); REQUIRE(anim.$sequence.loop_count == 2); // stops at end anim.onLoop = [](auto& seq, auto& tr) -> bool { if(seq.loop_count == 1) { seq.current = seq.frame_count - 1; return false; } else { return true; } }; } TEST_CASE("confirm frame sequencing works", "[animation-new]") { textures::init(); animation::init(); auto anim = crafter(); auto blanket = textures::get_sprite("ritual_crafting_area"); sf::IntRect init_rect{{0,0}, {anim.$sheet.frame_width, anim.$sheet.frame_height}}; anim.play(); bool loop_ran = false; // this will check that it moved to the next frame anim.onLoop = [&](auto& seq, auto& tr) -> bool { seq.current = 0; loop_ran = true; REQUIRE(blanket.sprite->getTextureRect() != init_rect); return false; }; anim.onFrame = [&](){ anim.apply(*blanket.sprite, {}); }; sf::Vector2f pos{0,0}; while(anim.playing) { anim.update(); // NOTE: possibly find a way to only run apply on frame change? } REQUIRE(loop_ran == true); REQUIRE(anim.playing == false); // this confirms it went back to the first frame REQUIRE(blanket.sprite->getTextureRect() == init_rect); } TEST_CASE("confirm transition changes work", "[animation-new]") { return; textures::init(); animation::init(); auto blanket = textures::get_sprite("ritual_crafting_area"); sf::Vector2f pos{0,0}; auto anim = crafter(); anim.play(); while(anim.playing) { anim.update(); anim.apply(*blanket.sprite, pos); } REQUIRE(anim.playing == false); REQUIRE(blanket.sprite->getPosition() != sf::Vector2f{0,0}); REQUIRE(pos != sf::Vector2f{0,0}); } TEST_CASE("animation easing tests", "[animation]") { Animation anim; anim.easing = ease::NONE; float res = anim.twitching(); REQUIRE(res == 0.0); anim.easing = ease::SINE; anim.subframe = 1.0f; res = anim.twitching(); REQUIRE(!std::isnan(res)); anim.easing = ease::OUT_CIRC; res = anim.twitching(); REQUIRE(!std::isnan(res)); anim.easing = ease::OUT_BOUNCE; res = anim.twitching(); REQUIRE(!std::isnan(res)); anim.easing = ease::IN_OUT_BACK; res = anim.twitching(); REQUIRE(!std::isnan(res)); } TEST_CASE("animation utility API", "[animation]") { textures::init(); animation::init(); auto blanket = textures::get_sprite("ritual_crafting_area"); auto anim = animation::load("ritual_crafting_area"); anim.play(); while(anim.apply(*blanket.sprite, {0,0})) { fmt::println("animation: {}", anim.subframe); } }