diff --git a/meson.build b/meson.build index dbcf83e..982c449 100644 --- a/meson.build +++ b/meson.build @@ -10,7 +10,7 @@ foreach module : ['system', 'window', 'graphics', 'audio'] sfml += [dependency('sfml-'+module, version : '>=2.5.1')] endforeach -add_project_link_arguments(['-lstdc++', '-lstdc++fs'], language : 'cpp') +add_project_link_arguments(['-lstdc++', '-lstdc++fs', '-lm'], language : 'cpp') sources = [ 'src/Main.cpp', @@ -36,9 +36,11 @@ sources = [ # 'src/Screens/Result.hpp', 'src/Resources/CoverAtlas.hpp', 'src/Resources/CoverAtlas.cpp', + 'src/Toolkit/AffineTransform.hpp', + 'src/Toolkit/EasingFunctions.hpp', + 'src/Toolkit/EasingFunctions.cpp', 'src/Toolkit/QuickRNG.hpp', 'src/Toolkit/QuickRNG.cpp', - 'src/Toolkit/AffineTransform.hpp', ] executable( diff --git a/src/Screens/MusicSelect/MusicSelect.cpp b/src/Screens/MusicSelect/MusicSelect.cpp index 744fd99..accfd4d 100644 --- a/src/Screens/MusicSelect/MusicSelect.cpp +++ b/src/Screens/MusicSelect/MusicSelect.cpp @@ -6,8 +6,8 @@ MusicSelect::Screen::Screen(const Data::SongList& t_song_list) : song_list(t_song_list), - ribbon(MusicSelect::Ribbon::test2_sort()), - button_highlight(panel_size) + button_highlight(panel_size), + ribbon(resources) { for (const auto& song : song_list.songs) { if (song.cover) { @@ -18,6 +18,7 @@ MusicSelect::Screen::Screen(const Data::SongList& t_song_list) : } } } + ribbon.test2_sort(); } void MusicSelect::Screen::select_chart(sf::RenderWindow& window) { @@ -38,20 +39,7 @@ void MusicSelect::Screen::select_chart(sf::RenderWindow& window) { break; } } - - // draw the ribbon - for (size_t panel = 0; panel < 12; panel++) { - ribbon.at(panel)->draw( - resources, - window, - sf::FloatRect( - (panel%4)*150.f, - (panel/4)*150.f, - 150.f, - 150.f - ) - ); - } + window.draw(ribbon); window.draw(button_highlight); window.display(); window.clear(sf::Color::Black); diff --git a/src/Screens/MusicSelect/Panel.cpp b/src/Screens/MusicSelect/Panel.cpp index 3280eb6..b8238ac 100644 --- a/src/Screens/MusicSelect/Panel.cpp +++ b/src/Screens/MusicSelect/Panel.cpp @@ -4,7 +4,7 @@ #include "MusicSelect.hpp" -void MusicSelect::ColorPanel::draw(Resources& resources, sf::RenderTarget& target, sf::FloatRect area) { +void MusicSelect::ColorPanel::draw(const Resources& resources, sf::RenderTarget& target, sf::FloatRect area) { sf::RectangleShape panel{{area.width*0.9f, area.height*0.9f}}; panel.setFillColor(this->color); panel.setOrigin(panel.getSize().x / 2.f, panel.getSize().y / 2.f); @@ -16,7 +16,7 @@ void MusicSelect::CategoryPanel::click(Screen& screen) { } -void MusicSelect::CategoryPanel::draw(Resources& resources, sf::RenderTarget& target, sf::FloatRect area) { +void MusicSelect::CategoryPanel::draw(const Resources& resources, sf::RenderTarget& target, sf::FloatRect area) { sf::RectangleShape red_rectangle; red_rectangle.setFillColor(sf::Color::Transparent); red_rectangle.setOutlineColor(sf::Color::Red); @@ -68,7 +68,7 @@ void MusicSelect::SongPanel::click(Screen& screen) { } -void MusicSelect::SongPanel::draw(Resources& resources, sf::RenderTarget& target, sf::FloatRect area) { +void MusicSelect::SongPanel::draw(const Resources& resources, sf::RenderTarget& target, sf::FloatRect area) { } diff --git a/src/Screens/MusicSelect/Panel.hpp b/src/Screens/MusicSelect/Panel.hpp index eaf7acc..ab08bac 100644 --- a/src/Screens/MusicSelect/Panel.hpp +++ b/src/Screens/MusicSelect/Panel.hpp @@ -17,21 +17,21 @@ namespace MusicSelect { // What happens when you click on the panel virtual void click(Screen& screen) = 0; // How the panel should be displayed - virtual void draw(Resources& resources, sf::RenderTarget& target, sf::FloatRect area) = 0; + virtual void draw(const Resources& resources, sf::RenderTarget& target, sf::FloatRect area) = 0; virtual ~Panel() = default; }; class EmptyPanel final : public Panel { public: void click(Screen& screen) override {return;}; - void draw(Resources& resources, sf::RenderTarget& target, sf::FloatRect area) override {return;}; + void draw(const Resources& resources, sf::RenderTarget& target, sf::FloatRect area) override {return;}; }; class ColorPanel final : public Panel { public: explicit ColorPanel(const sf::Color& t_color) : color(t_color) {}; void click(Screen& screen) override {return;}; - void draw(Resources& resources, sf::RenderTarget& target, sf::FloatRect area) override; + void draw(const Resources& resources, sf::RenderTarget& target, sf::FloatRect area) override; private: const sf::Color color; }; @@ -40,7 +40,7 @@ namespace MusicSelect { public: explicit CategoryPanel(const std::string& t_label) : label(t_label) {}; void click(Screen& screen) override; - void draw(Resources& resources, sf::RenderTarget& target, sf::FloatRect area) override; + void draw(const Resources& resources, sf::RenderTarget& target, sf::FloatRect area) override; private: std::string label; }; @@ -49,7 +49,7 @@ namespace MusicSelect { public: explicit SongPanel(const Data::Song& t_song) : song(t_song) {}; void click(Screen& screen) override; - void draw(Resources& resources, sf::RenderTarget& target, sf::FloatRect area) override; + void draw(const Resources& resources, sf::RenderTarget& target, sf::FloatRect area) override; private: const Data::Song& song; }; diff --git a/src/Screens/MusicSelect/Ribbon.cpp b/src/Screens/MusicSelect/Ribbon.cpp index ba0d60d..f423e0b 100644 --- a/src/Screens/MusicSelect/Ribbon.cpp +++ b/src/Screens/MusicSelect/Ribbon.cpp @@ -7,50 +7,92 @@ #include "Panel.hpp" #include "../../Toolkit/QuickRNG.hpp" -MusicSelect::Ribbon MusicSelect::Ribbon::title_sort(const Data::SongList& song_list) { - std::map> categories; - for (const auto& song : song_list.songs) { - if (song.title.size() > 0) { - char letter = song.title[0]; - if ('A' <= letter and letter <= 'Z') { - categories[letter].push_back(song); - } else if ('a' <= letter and letter <= 'z') { - categories['A' + (letter - 'a')].push_back(song); - } else { - categories['?'].push_back(song); - } - } else { - categories['?'].push_back(song); - } - } - Ribbon ribbon; - for (auto& [letter, songs] : categories) { - std::vector> panels; - panels.emplace_back( - std::make_shared( - std::string(1, letter) - ) - ); - std::sort(songs.begin(), songs.end(), Data::Song::sort_by_title); - for (const auto& song : songs) { - panels.push_back(std::make_shared(song)); - } - while (panels.size() % 3 != 0) { - panels.push_back(std::make_shared()); - } - for (size_t i = 0; i < panels.size(); i += 3) { - ribbon.layout.emplace_back(); - for (size_t j = 0; j < 3; j++) { - ribbon.layout.back()[j] = std::move(panels[i+j]); - } - } - } - return ribbon; +MusicSelect::MoveAnimation::MoveAnimation(unsigned int previous_pos, unsigned int next_pos, size_t ribbon_size, Direction direction) : + normalized_to_pos(create_transform(previous_pos, next_pos, ribbon_size, direction)), + seconds_to_normalized(0.f, 0.3f, 0.f, 1.f), + clock(), + ease_expo(-7.f) +{ + } -MusicSelect::Ribbon MusicSelect::Ribbon::test_sort() { - Ribbon ribbon; - ribbon.layout.push_back( +Toolkit::AffineTransform MusicSelect::MoveAnimation::create_transform(unsigned int previous_pos, unsigned int next_pos, size_t ribbon_size, Direction direction) { + // We first deal with cases were we cross the end of the ribbon + if (direction == Direction::Left and next_pos > previous_pos) { + return Toolkit::AffineTransform( + 0.f, + 1.f, + static_cast(previous_pos), + static_cast(next_pos-ribbon_size) + ); + } else if (direction == Direction::Right and next_pos < previous_pos) { + return Toolkit::AffineTransform( + 0.f, + 1.f, + static_cast(previous_pos), + static_cast(next_pos+ribbon_size) + ); + } else { + return Toolkit::AffineTransform( + 0.f, + 1.f, + static_cast(previous_pos), + static_cast(next_pos) + ); + } +} + +float MusicSelect::MoveAnimation::get_position() { + return normalized_to_pos.transform( + ease_expo.transform( + seconds_to_normalized.clampedTransform( + clock.getElapsedTime().asSeconds() + ) + ) + ); +} + +bool MusicSelect::MoveAnimation::ended() { + return clock.getElapsedTime() > sf::milliseconds(300); +} + +void MusicSelect::Ribbon::title_sort(const Data::SongList& song_list) { + std::vector> songs; + for (auto &&song : song_list.songs) { + songs.push_back(std::cref(song)); + } + + (song_list.songs.begin(), song_list.songs.end()); + std::sort(songs.begin(), songs.end(), Data::Song::sort_by_title); + std::map>> categories; + for (const auto& song : songs) { + if (song.get().title.size() > 0) { + char letter = song.get().title[0]; + if ('A' <= letter and letter <= 'Z') { + categories + [std::string(1, letter)] + .push_back( + std::make_shared(song) + ); + } else if ('a' <= letter and letter <= 'z') { + categories + [std::string(1, 'A' + (letter - 'a'))] + .push_back( + std::make_shared(song) + ); + } else { + categories["?"].push_back(std::make_shared(song)); + } + } else { + categories["?"].push_back(std::make_shared(song)); + } + } + layout_from_category_map(categories); +} + +void MusicSelect::Ribbon::test_sort() { + layout.clear(); + layout.push_back( { std::make_shared(), std::make_shared("A"), @@ -58,7 +100,7 @@ MusicSelect::Ribbon MusicSelect::Ribbon::test_sort() { } ); for (size_t i = 0; i < 3; i++) { - ribbon.layout.push_back( + layout.push_back( { std::make_shared(), std::make_shared(), @@ -66,12 +108,11 @@ MusicSelect::Ribbon MusicSelect::Ribbon::test_sort() { } ); } - return ribbon; } -MusicSelect::Ribbon MusicSelect::Ribbon::test2_sort() { +void MusicSelect::Ribbon::test2_sort() { std::string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - std::map>> categories; + std::map>> categories; Toolkit::UniformIntRNG category_size_generator{1,10}; Toolkit::UniformIntRNG panel_hue_generator{0,255}; for (auto &&letter : alphabet) { @@ -88,7 +129,7 @@ MusicSelect::Ribbon MusicSelect::Ribbon::test2_sort() { ); } } - return Ribbon::layout_from_map(categories); + layout_from_category_map(categories); } const std::shared_ptr& MusicSelect::Ribbon::at(unsigned int button_index) const { @@ -99,15 +140,15 @@ const std::shared_ptr& MusicSelect::Ribbon::at(unsigned int ); } -MusicSelect::Ribbon MusicSelect::Ribbon::layout_from_map(const std::map>>& categories) { - Ribbon ribbon; +void MusicSelect::Ribbon::layout_from_category_map(const std::map>>& categories) { + layout.clear(); for (auto &&[category, panels] : categories) { if (not panels.empty()) { std::vector> current_column; current_column.push_back(std::make_shared(category)); for (auto &&panel : panels) { if (current_column.size() == 3) { - ribbon.layout.push_back({current_column[0], current_column[1], current_column[2]}); + layout.push_back({current_column[0], current_column[1], current_column[2]}); current_column.clear(); } else { current_column.push_back(std::move(panel)); @@ -117,21 +158,71 @@ MusicSelect::Ribbon MusicSelect::Ribbon::layout_from_map(const std::map()); } - ribbon.layout.push_back({current_column[0], current_column[1], current_column[2]}); + layout.push_back({current_column[0], current_column[1], current_column[2]}); } } } - return ribbon; } void MusicSelect::Ribbon::move_right() { + move_animation.emplace(position, position + 1, layout.size(), Direction::Right); position = (position + 1) % layout.size(); } void MusicSelect::Ribbon::move_left() { + move_animation.emplace(position, static_cast(position)-1, layout.size(), Direction::Left); if (position == 0) { position = layout.size() - 1; } else { position--; } } + +void MusicSelect::Ribbon::draw(sf::RenderTarget& target, sf::RenderStates states) const { + if (move_animation) { + if (not move_animation->ended()) { + return draw_with_animation(target, states); + } else { + move_animation.reset(); + } + } + draw_without_animation(target, states); +} + +void MusicSelect::Ribbon::draw_with_animation(sf::RenderTarget& target, sf::RenderStates states) const { + auto float_position = move_animation->get_position(); + unsigned int column_zero = (static_cast(float_position) + layout.size()) % layout.size(); + for (int column_offset = -1; column_offset <= 4; column_offset++) { + unsigned int actual_column = (column_zero + column_offset + layout.size()) % layout.size(); + for (int row = 0; row < 3; row++) { + layout.at(actual_column).at(row)->draw( + resources, + target, + sf::FloatRect( + (static_cast(column_zero + column_offset)-float_position)*150.f, + row*150.f, + 150.f, + 150.f + ) + ); + } + } +} + +void MusicSelect::Ribbon::draw_without_animation(sf::RenderTarget& target, sf::RenderStates states) const { + for (int column = -1; column <= 4; column++) { + int actual_column_index = (column + position + layout.size()) % layout.size(); + for (int row = 0; row < 3; row++) { + layout.at(actual_column_index).at(row)->draw( + resources, + target, + sf::FloatRect( + column*150.f, + row*150.f, + 150.f, + 150.f + ) + ); + } + } +} diff --git a/src/Screens/MusicSelect/Ribbon.hpp b/src/Screens/MusicSelect/Ribbon.hpp index 49076f1..9ea5c26 100644 --- a/src/Screens/MusicSelect/Ribbon.hpp +++ b/src/Screens/MusicSelect/Ribbon.hpp @@ -1,24 +1,51 @@ #pragma once +#include + #include "Panel.hpp" #include "../../Data/SongList.hpp" +#include "../../Toolkit/AffineTransform.hpp" +#include "../../Toolkit/EasingFunctions.hpp" namespace MusicSelect { + + enum class Direction { + Right, + Left, + }; + + struct MoveAnimation { + MoveAnimation(unsigned int previous_pos, unsigned int next_pos, size_t ribbon_size, Direction direction); + Toolkit::AffineTransform normalized_to_pos; + Toolkit::AffineTransform seconds_to_normalized; + sf::Clock clock; + Toolkit::EaseExponential ease_expo; + float get_position(); + bool ended(); + private: + Toolkit::AffineTransform create_transform(unsigned int previous_pos, unsigned int next_pos, size_t ribbon_size, Direction direction); + }; + // The Ribbon is the moving part of the Music Select Screen // It can be sorted in a number of ways - class Ribbon { + class Ribbon final : public sf::Drawable { public: - Ribbon() = default; - static Ribbon title_sort(const Data::SongList& song_list); - static Ribbon test_sort(); - static Ribbon test2_sort(); + Ribbon(const Resources& t_resources) : resources(t_resources) {}; + void title_sort(const Data::SongList& song_list); + void test_sort(); + void test2_sort(); const auto& get_layout() {return layout;}; const std::shared_ptr& at(unsigned int button_index) const; void move_right(); void move_left(); private: - static Ribbon layout_from_map(const std::map>>& categories); + void draw(sf::RenderTarget& target, sf::RenderStates states) const override; + void draw_with_animation(sf::RenderTarget& target, sf::RenderStates states) const; + void draw_without_animation(sf::RenderTarget& target, sf::RenderStates states) const; + void layout_from_category_map(const std::map>>& categories); std::vector,3>> layout; unsigned int position = 0; + mutable std::optional move_animation; + const Resources& resources; }; } \ No newline at end of file diff --git a/src/Toolkit/EasingFunctions.cpp b/src/Toolkit/EasingFunctions.cpp new file mode 100644 index 0000000..02049ed --- /dev/null +++ b/src/Toolkit/EasingFunctions.cpp @@ -0,0 +1,10 @@ +#include "EasingFunctions.hpp" + +float Toolkit::EaseExponential::transform(float in) { + // (1-2^(a*x))/(1-2^a) + return ( + 1.f - std::pow(2.f, easing_factor*in) + ) / ( + 1.f - std::pow(2.f, easing_factor) + ); +} diff --git a/src/Toolkit/EasingFunctions.hpp b/src/Toolkit/EasingFunctions.hpp new file mode 100644 index 0000000..a499990 --- /dev/null +++ b/src/Toolkit/EasingFunctions.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace Toolkit { + class EaseExponential { + public: + EaseExponential(float t_easing_factor) : easing_factor(t_easing_factor) {}; + float transform(float in); + float easing_factor; + }; +} \ No newline at end of file