From 60c405b1fc7b7a905e07bd1b4a5d51d409e0f851 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Wed, 21 Jan 2026 13:13:58 -0500 Subject: [PATCH] Refactor the animate2 and then start working on the motion feature. --- animate2.cpp | 146 ++++++++++++++++++++++ animate2.hpp | 81 ++++++++++++ animation.cpp | 1 + animation.hpp | 1 - meson.build | 2 + tests/animate2.cpp | 174 ++++++++++++++++++++++++++ tests/animation.cpp | 285 ------------------------------------------- textures.hpp | 4 +- tools/fragviewer.cpp | 4 +- 9 files changed, 408 insertions(+), 290 deletions(-) create mode 100644 animate2.cpp create mode 100644 animate2.hpp create mode 100644 tests/animate2.cpp diff --git a/animate2.cpp b/animate2.cpp new file mode 100644 index 0000000..1346018 --- /dev/null +++ b/animate2.cpp @@ -0,0 +1,146 @@ +#include "animate2.hpp" +#include +#include +#include "dbc.hpp" +#include "rand.hpp" + +namespace animate2 { + std::vector Animate2::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 Animate2::play() { + dbc::check(!playing, "can't call play while playing?"); + $sequence.current = 0; + $sequence.loop_count = 0; + playing = true; + $sequence.timer.start(); + } + + void Animate2::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 Animate2::apply(sf::Sprite& sprite) { + 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 Animate2::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 Animate2::update() { + update_frame(); + } + + void Animate2::motion(sf::Vector2f& pos, sf::Vector2f& scale) { + $transform.twitching($sequence); + $transform.lerp($sequence, pos, scale); + } + + float Transform::twitching(Sequence& seq) { + float subframe = seq.timer.getElapsedTime().asSeconds(); + float tick = 1 - std::powf(ease_rate, subframe + 0.0001); + fmt::println("TICK {}; subframe: {}", tick, subframe); + + switch(easing) { + case ease::NONE: + return 0.0; + case ease::SINE: + return std::abs(std::sin(subframe * ease_rate)); + case ease::OUT_CIRC: + return ease::out_circ(tick); + case ease::OUT_BOUNCE: + return ease::out_bounce(tick); + case ease::IN_OUT_BACK: + return ease::in_out_back(tick); + case ease::RANDOM: + return Random::uniform_real(0.0001f, 1.0f); + case ease::NORM_DIST: + return Random::normal(0.5f, 0.1f); + case ease::LINEAR: + return tick; + default: + dbc::sentinel( + fmt::format("Invalid easing {} given to animation", + int(easing))); + } + } + + void Transform::lerp(Sequence& seq, sf::Vector2f& scale_out, sf::Vector2f& pos_out) { + float tick = twitching(seq); + + if(stationary) { + switch(motion) { + case ease::SHAKE: { + pos_out.x += std::lerp(min_x, max_x, tick); + } break; + case ease::BOUNCE: { + pos_out.y -= std::lerp(min_y, max_y, tick); + } break; + case ease::RUSH: { + scale_out.x = std::lerp(min_x, max_x, tick); + scale_out.y = std::lerp(min_y, max_y, tick); + pos_out.y = pos_out.y - (pos_out.y * scale_out.y - pos_out.y); + } break; + case ease::SQUEEZE: { + scale_out.x *= std::lerp(min_x, max_x, tick); + + } break; + case ease::SQUASH: { + scale_out.y *= std::lerp(min_y, max_y, tick); + } break; + case ease::STRETCH: { + scale_out.x = std::lerp(min_x, max_x, tick); + } break; + case ease::GROW: { + scale_out.y = std::lerp(min_y, max_y, tick); + } break; + case ease::SLIDE: { + pos_out.x = std::lerp(min_x, max_x, tick); + pos_out.y = std::lerp(min_y, max_y, tick); + } break; + default: + dbc::sentinel("Unknown animation.motion setting."); + } + } else { + scale_out.x = std::lerp(scale_out.x * min_x, scale_out.x * max_x, tick); + scale_out.y = std::lerp(scale_out.y * min_y, scale_out.y * max_y, tick); + } + } +} diff --git a/animate2.hpp b/animate2.hpp new file mode 100644 index 0000000..12be222 --- /dev/null +++ b/animate2.hpp @@ -0,0 +1,81 @@ +#pragma once +#include +#include +#include "easings.hpp" +#include +#include +#include +#include +#include + +namespace animate2 { + 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; + + float twitching(Sequence& seq); + void lerp(Sequence& seq, sf::Vector2f& scale_out, sf::Vector2f& pos_out); + }; + + /* Gets the number of times it looped, and returns if it should stop. */ + using OnLoopHandler = std::function; + using OnFrameHandler = std::function; + + inline 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(); + void play(); + void stop(); + void apply(sf::Sprite& sprite); + void update_frame(); + void update(); + void motion(sf::Vector2f& pos, sf::Vector2f& scale); + }; + +} diff --git a/animation.cpp b/animation.cpp index 9089b96..67476f2 100644 --- a/animation.cpp +++ b/animation.cpp @@ -1,5 +1,6 @@ #include "animation.hpp" #include "rand.hpp" +#include "textures.hpp" #include namespace components { diff --git a/animation.hpp b/animation.hpp index cbe0d62..917b645 100644 --- a/animation.hpp +++ b/animation.hpp @@ -1,6 +1,5 @@ #pragma once #include "components.hpp" -#include "textures.hpp" #include "easings.hpp" #include "dinkyecs.hpp" #include diff --git a/meson.build b/meson.build index d99771f..54427f0 100644 --- a/meson.build +++ b/meson.build @@ -86,6 +86,7 @@ dependencies += [ sources = [ 'ai.cpp', 'ai_debug.cpp', + 'animate2.cpp', 'animation.cpp', 'animation.cpp', 'autowalker.cpp', @@ -137,6 +138,7 @@ sources = [ executable('runtests', sources + [ 'tests/ai.cpp', + 'tests/animate2.cpp', 'tests/animation.cpp', 'tests/base.cpp', 'tests/battle.cpp', diff --git a/tests/animate2.cpp b/tests/animate2.cpp new file mode 100644 index 0000000..2245996 --- /dev/null +++ b/tests/animate2.cpp @@ -0,0 +1,174 @@ +#include +#include "animation.hpp" +#include "textures.hpp" +#include "dinkyecs.hpp" +#include "config.hpp" +#include +#include +#include +#include +#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}); +} diff --git a/tests/animation.cpp b/tests/animation.cpp index f6ba528..ad48700 100644 --- a/tests/animation.cpp +++ b/tests/animation.cpp @@ -1,296 +1,11 @@ #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; diff --git a/textures.hpp b/textures.hpp index 2cafe5f..ff3ab0c 100644 --- a/textures.hpp +++ b/textures.hpp @@ -2,7 +2,9 @@ #include #include #include -#include +#include +#include +#include #include #include #include "matrix.hpp" diff --git a/tools/fragviewer.cpp b/tools/fragviewer.cpp index fae1ea4..b08ca5f 100644 --- a/tools/fragviewer.cpp +++ b/tools/fragviewer.cpp @@ -1,7 +1,5 @@ #include "textures.hpp" -#include -#include -#include +#include #include #include #include