Refactor the animate2 and then start working on the motion feature.

This commit is contained in:
Zed A. Shaw 2026-01-21 13:13:58 -05:00
parent 992b8f0e0a
commit 60c405b1fc
9 changed files with 408 additions and 290 deletions

174
tests/animate2.cpp Normal file
View file

@ -0,0 +1,174 @@
#include <catch2/catch_test_macros.hpp>
#include "animation.hpp"
#include "textures.hpp"
#include "dinkyecs.hpp"
#include "config.hpp"
#include <iostream>
#include <memory>
#include <chrono>
#include <thread>
#include "rand.hpp"
#include "animate2.hpp"
using namespace components;
using namespace textures;
using namespace std::chrono_literals;
using namespace animate2;
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);
};
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]") {
textures::init();
animation::init();
auto anim = crafter();
sf::Vector2f pos{0,0};
sf::Vector2f scale{0,0};
// also testing that onFrame being null means it's not run
REQUIRE(anim.onFrame == nullptr);
anim.play();
while(anim.playing) {
anim.update();
anim.motion(pos, scale);
}
REQUIRE(anim.playing == false);
REQUIRE(scale != sf::Vector2f{0,0});
REQUIRE(pos != sf::Vector2f{0,0});
}

View file

@ -1,296 +1,11 @@
#include <catch2/catch_test_macros.hpp>
#include "animation.hpp"
#include "textures.hpp"
#include "dinkyecs.hpp"
#include "config.hpp"
#include <iostream>
#include <memory>
#include <chrono>
#include <thread>
#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<int> frames{};
std::vector<std::chrono::milliseconds> 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<sf::Shader> 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<bool(Sequence& seq, Transform& tr)>;
using OnFrameHandler = std::function<void()>;
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<sf::IntRect> $frame_rects{calc_frames()};
OnLoopHandler onLoop = DefaultOnLoop;
bool playing = false;
std::vector<sf::IntRect> calc_frames() {
std::vector<sf::IntRect> 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;