diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9cbe8bf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,83 @@ +name: CI + +on: [push, pull_request, workflow_dispatch] + +concurrency: + group: environment-${{github.ref}} + cancel-in-progress: true + +jobs: + build: + name: ${{matrix.platform.name}} ${{matrix.type.name}} + runs-on: ${{matrix.platform.os}} + defaults: + run: + shell: bash + + strategy: + fail-fast: false + matrix: + platform: + - { name: Windows MSVC, os: windows-2022 } + - { name: Windows ClangCL, os: windows-2022, flags: -T ClangCL } + - { name: Windows Clang, os: windows-2022, flags: -GNinja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ } + - { name: Linux GCC, os: ubuntu-22.04, flags: -GNinja } + - { name: Linux Clang, os: ubuntu-22.04, flags: -GNinja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ } + - { name: macOS, os: macos-12, flags: -GNinja } + type: + - { name: Release } + - { name: Debug } + + steps: + - name: Get CMake and Ninja + uses: lukka/get-cmake@latest + with: + cmakeVersion: latest + ninjaVersion: latest + + - name: Install Linux Dependencies + if: runner.os == 'Linux' + run: | + sudo apt update + sudo apt install ninja-build llvm xorg-dev libxrandr-dev libxcursor-dev libudev-dev libgl1-mesa-dev libegl1-mesa-dev + + - name: Install macOS Tools + if: runner.os == 'macOS' + run: brew install ninja + + - name: Checkout SFML + uses: actions/checkout@v3 + with: + repository: SFML/SFML + path: sfml + ref: 2.6.0 + + - name: Configure SFML + run: | + cmake -S sfml -B sfml/build \ + -DCMAKE_INSTALL_PREFIX=sfml/install \ + -DCMAKE_BUILD_TYPE=${{matrix.type.name}} \ + -DSFML_BUILD_AUDIO=OFF \ + -DSFML_BUILD_NETWORK=OFF \ + ${{matrix.platform.flags}} + + - name: Build SFML + run: cmake --build sfml/build --config ${{matrix.type.name}} --target install + + - name: Checkout SelbaWard + uses: actions/checkout@v3 + with: + path: SelbaWard + + - name: Configure SelbaWard + run: | + cmake -S SelbaWard -B SelbaWard/build \ + -DBUILD_EXAMPLES=ON \ + -DCMAKE_INSTALL_PREFIX=$GITHUB_WORKSPACE/install \ + -DCMAKE_BUILD_TYPE=${{matrix.type.name}} \ + -DCMAKE_VERBOSE_MAKEFILE=ON \ + -DSFML_ROOT=$GITHUB_WORKSPACE/sfml/install \ + ${{matrix.platform.flags}} + + - name: Build SelbaWard + run: cmake --build SelbaWard/build --config ${{matrix.type.name}} diff --git a/.gitignore b/.gitignore index afeb4e8..c981782 100644 --- a/.gitignore +++ b/.gitignore @@ -28,10 +28,12 @@ backup_*/ test/ test-*/ test_*/ +test-*/ temp/ temp_*/ temp_storage/ PropertySheets/ +priv_*/ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..67adf98 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.16) +project(SelbaWard LANGUAGES CXX) + +find_package(SFML 2.6 REQUIRED COMPONENTS graphics) + +add_library(SelbaWard + src/SelbaWard/BitmapFont.cpp + src/SelbaWard/BitmapText.cpp + src/SelbaWard/ConsoleScreen.cpp + src/SelbaWard/Crosshair.cpp + src/SelbaWard/ElasticSprite.cpp + src/SelbaWard/GallerySprite.cpp + src/SelbaWard/Line.cpp + src/SelbaWard/NinePatch.cpp + src/SelbaWard/PieChart.cpp + src/SelbaWard/PixelDisplay.cpp + src/SelbaWard/Polygon.cpp + src/SelbaWard/ProgressBar.cpp + src/SelbaWard/Ring.cpp + src/SelbaWard/SpinningCard.cpp + src/SelbaWard/Spline.cpp + src/SelbaWard/Sprite3d.cpp + src/SelbaWard/Starfield.cpp + src/SelbaWard/Starfield3d.cpp +) +add_library(SelbaWard::SelbaWard ALIAS SelbaWard) +target_include_directories(SelbaWard PUBLIC src) +target_link_libraries(SelbaWard PUBLIC sfml-graphics) +target_compile_features(SelbaWard PUBLIC cxx_std_11) + +# Stop configuration if being consumed by a higher level project +if(NOT PROJECT_IS_TOP_LEVEL) + return() +endif() + +option(BUILD_EXAMPLES "Build examples" OFF) +if(BUILD_EXAMPLES) + add_subdirectory(examples) +endif() diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..ddc2c72 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,26 @@ +add_executable(bitmap-text bitmapTextExample.cpp) +target_link_libraries(bitmap-text PRIVATE SelbaWard::SelbaWard) + +add_executable(line lineExample.cpp) +target_link_libraries(line PRIVATE SelbaWard::SelbaWard) + +add_executable(progress-bar progressBarExample.cpp) +target_link_libraries(progress-bar PRIVATE SelbaWard::SelbaWard) + +add_executable(spinning-card spinningCardExample.cpp) +target_link_libraries(spinning-card PRIVATE SelbaWard::SelbaWard) + +add_executable(spline splineExample.cpp) +target_link_libraries(spline PRIVATE SelbaWard::SelbaWard) + +add_executable(spline2 splineExample2.cpp) +target_link_libraries(spline2 PRIVATE SelbaWard::SelbaWard) + +add_executable(sprite3d sprite3dExample.cpp) +target_link_libraries(sprite3d PRIVATE SelbaWard::SelbaWard) + +add_executable(sprite3d-card-fan sprite3dExampleCardFan.cpp) +target_link_libraries(sprite3d-card-fan PRIVATE SelbaWard::SelbaWard) + +add_executable(sprite3d-spinning-card sprite3dExampleSpinningCard.cpp) +target_link_libraries(sprite3d-spinning-card PRIVATE SelbaWard::SelbaWard) diff --git a/readme.md b/readme.md index 52a817a..cb00a89 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ A collection of SFML drawables by [Hapaxia](http://github.com/Hapaxia) -Contents: **Bitmap Text**, **Console Screen**, **Crosshair**, **Elastic Sprite**, **Gallery Sprite**, **Line**, **Nine Patch**, **Pie Chart**, **Pixel Display**, **Polygon**, **Progress Bar**, **Ring**, **Spinning Card**, **Spline**, **Sprite 3D**, **Starfield**, **Tile Map** +Contents: **Bitmap Text**, **Console Screen**, **Crosshair**, **Elastic Sprite**, **Frame Transition**, **Gallery Sprite**, **Line**, **Nine Patch**, **Pie Chart**, **Pixel Display**, **Polygon**, **Progress Bar**, **Ring**, **Spinning Card**, **Spline**, **Sprite 3D**, **Sprite Batch**, **Starfield**, **Starfield 3D**, **Tile Map** ## For information, view the [Wiki]. diff --git a/src/SelbaWard.hpp b/src/SelbaWard.hpp index 2764541..9cf96cd 100644 --- a/src/SelbaWard.hpp +++ b/src/SelbaWard.hpp @@ -34,6 +34,7 @@ #include "SelbaWard/ConsoleScreen.hpp" #include "SelbaWard/Crosshair.hpp" #include "SelbaWard/ElasticSprite.hpp" +#include "SelbaWard/FrameTransition.hpp" #include "SelbaWard/GallerySprite.hpp" #include "SelbaWard/Line.hpp" #include "SelbaWard/NinePatch.hpp" @@ -46,7 +47,9 @@ #include "SelbaWard/SpinningCard.hpp" #include "SelbaWard/Spline.hpp" #include "SelbaWard/Sprite3d.hpp" +#include "SelbaWard/SpriteBatch.hpp" #include "SelbaWard/Starfield.hpp" +#include "SelbaWard/Starfield3d.hpp" #include "SelbaWard/TileMap.hpp" #endif // SELBAWARD_HPP diff --git a/src/SelbaWard/BitmapFont.cpp b/src/SelbaWard/BitmapFont.cpp index 643f52c..eaf38dc 100644 --- a/src/SelbaWard/BitmapFont.cpp +++ b/src/SelbaWard/BitmapFont.cpp @@ -78,7 +78,7 @@ void BitmapFont::setSmooth(const bool smooth) m_texture.setSmooth(smooth); } -void BitmapFont::setNumberOfTilesPerRow(const unsigned int numberOfTilesPerRow) +void BitmapFont::setNumberOfTilesPerRow(const std::size_t numberOfTilesPerRow) { m_numberOfTilesPerRow = numberOfTilesPerRow; } @@ -105,7 +105,7 @@ void BitmapFont::setDefaultTextureRect(const sf::IntRect& defaultTextureRect) m_defaultTextureRect = defaultTextureRect; } -void BitmapFont::setTextureRect(const sf::IntRect& textureRect, const unsigned int glyphIndex) +void BitmapFont::setTextureRect(const sf::IntRect& textureRect, const std::size_t glyphIndex) { if (!priv_isGlyphIndexValid(glyphIndex)) { @@ -114,20 +114,20 @@ void BitmapFont::setTextureRect(const sf::IntRect& textureRect, const unsigned i return; } - m_glyphs[glyphIndex].useDefaultTextureRect = false;; + m_glyphs[glyphIndex].useDefaultTextureRect = false; m_glyphs[glyphIndex].textureRect = textureRect; m_glyphs[glyphIndex].width = textureRect.size.x; m_glyphs[glyphIndex].baseline = textureRect.size.y - 1; m_glyphs[glyphIndex].startX = 0; } -void BitmapFont::setTextureRects(const std::vector& textureRects, const unsigned int initialGlyphIndex) +void BitmapFont::setTextureRects(const std::vector& textureRects, const std::size_t initialGlyphIndex) { - for (unsigned int i{ 0 }; i < textureRects.size(); ++i) + for (std::size_t i{ 0u }; i < textureRects.size(); ++i) setTextureRect(textureRects[i], initialGlyphIndex + i); } -void BitmapFont::clearTextureRect(const unsigned int glyphIndex) +void BitmapFont::clearTextureRect(const std::size_t glyphIndex) { if (!priv_isGlyphIndexValid(glyphIndex)) { @@ -141,7 +141,7 @@ void BitmapFont::clearTextureRect(const unsigned int glyphIndex) void BitmapFont::clearAllTextureRects() { - for (unsigned int i{ 0 }; i < m_glyphs.size(); ++i) + for (std::size_t i{ 0u }; i < m_glyphs.size(); ++i) clearTextureRect(i); } @@ -150,7 +150,7 @@ void BitmapFont::clearAllTextureRects() -void BitmapFont::setGlyphToDefault(const unsigned int glyphIndex) +void BitmapFont::setGlyphToDefault(const std::size_t glyphIndex) { if (!priv_isGlyphIndexValid(glyphIndex)) { @@ -166,15 +166,15 @@ void BitmapFont::setGlyphToDefault(const unsigned int glyphIndex) m_glyphs[glyphIndex].startX = 0; } -void BitmapFont::setGlyphsToDefault(const unsigned int numberOfGlyphs, const unsigned int initialGlyphIndex) +void BitmapFont::setGlyphsToDefault(const std::size_t numberOfGlyphs, const std::size_t initialGlyphIndex) { - for (unsigned int i{ 0 }; i < numberOfGlyphs; ++i) + for (std::size_t i{ 0u }; i < numberOfGlyphs; ++i) setGlyphToDefault(initialGlyphIndex + i); } void BitmapFont::setAllGlyphsToDefault() { - setGlyphsToDefault(static_cast(m_glyphs.size())); + setGlyphsToDefault(m_glyphs.size()); } @@ -185,7 +185,7 @@ void BitmapFont::setAllGlyphsToDefault() -void BitmapFont::setBaseline(const int baseline, const unsigned int glyphIndex) +void BitmapFont::setBaseline(const int baseline, const std::size_t glyphIndex) { if (!priv_isGlyphIndexValid(glyphIndex)) { @@ -197,7 +197,7 @@ void BitmapFont::setBaseline(const int baseline, const unsigned int glyphIndex) m_glyphs[glyphIndex].baseline = baseline; } -void BitmapFont::setWidth(const int width, const unsigned int glyphIndex) +void BitmapFont::setWidth(const int width, const std::size_t glyphIndex) { if (!priv_isGlyphIndexValid(glyphIndex)) { @@ -209,7 +209,7 @@ void BitmapFont::setWidth(const int width, const unsigned int glyphIndex) m_glyphs[glyphIndex].width = width; } -void BitmapFont::setStartX(const int startX, const unsigned int glyphIndex) +void BitmapFont::setStartX(const int startX, const std::size_t glyphIndex) { if (!priv_isGlyphIndexValid(glyphIndex)) { @@ -221,39 +221,39 @@ void BitmapFont::setStartX(const int startX, const unsigned int glyphIndex) m_glyphs[glyphIndex].startX = startX; } -void BitmapFont::setBaselines(const int baseline, const unsigned int numberOfGlyphs, const unsigned int initialGlyphIndex) +void BitmapFont::setBaselines(const int baseline, const std::size_t numberOfGlyphs, const std::size_t initialGlyphIndex) { - for (unsigned int i{ 0 }; i < numberOfGlyphs; ++i) + for (std::size_t i{ 0u }; i < numberOfGlyphs; ++i) setBaseline(baseline, initialGlyphIndex + i); } -void BitmapFont::setWidths(const int width, const unsigned int numberOfGlyphs, const unsigned int initialGlyphIndex) +void BitmapFont::setWidths(const int width, const std::size_t numberOfGlyphs, const std::size_t initialGlyphIndex) { - for (unsigned int i{ 0 }; i < numberOfGlyphs; ++i) + for (std::size_t i{ 0u }; i < numberOfGlyphs; ++i) setWidth(width, initialGlyphIndex + i); } -void BitmapFont::setStartXs(const int startX, const unsigned int numberOfGlyphs, const unsigned int initialGlyphIndex) +void BitmapFont::setStartXs(const int startX, const std::size_t numberOfGlyphs, const std::size_t initialGlyphIndex) { - for (unsigned int i{ 0 }; i < numberOfGlyphs; ++i) + for (std::size_t i{ 0u }; i < numberOfGlyphs; ++i) setStartX(startX, initialGlyphIndex + i); } -void BitmapFont::setBaselines(const std::vector& baselines, const unsigned int initialGlyphIndex) +void BitmapFont::setBaselines(const std::vector& baselines, const std::size_t initialGlyphIndex) { - for (unsigned int i{ 0 }; i < baselines.size(); ++i) + for (std::size_t i{ 0u }; i < baselines.size(); ++i) setBaseline(baselines[i], initialGlyphIndex + i); } -void BitmapFont::setWidths(const std::vector& widths, const unsigned int initialGlyphIndex) +void BitmapFont::setWidths(const std::vector& widths, const std::size_t initialGlyphIndex) { - for (unsigned int i{ 0 }; i < widths.size(); ++i) + for (std::size_t i{ 0u }; i < widths.size(); ++i) setWidth(widths[i], initialGlyphIndex + i); } -void BitmapFont::setStartXs(const std::vector& startXs, const unsigned int initialGlyphIndex) +void BitmapFont::setStartXs(const std::vector& startXs, const std::size_t initialGlyphIndex) { - for (unsigned int i{ 0 }; i < startXs.size(); ++i) + for (std::size_t i{ 0u }; i < startXs.size(); ++i) setStartX(startXs[i], initialGlyphIndex + i); } @@ -277,17 +277,17 @@ void BitmapFont::setStartX(const int startX, const std::string& glyphs) void BitmapFont::setKerning(const int kerning, const std::string& glyphs) { - if (glyphs.size() < 2) + if (glyphs.size() < 2u) { if (m_throwExceptions) throw Exception(exceptionPrefix + "cannot set kerning - glyph pair not specified."); return; } - for (std::size_t i{ 0u }; i < glyphs.size(); i += 2) + for (std::size_t i{ 0u }; i < glyphs.size(); i += 2u) { - std::string glyphPair{ glyphs.substr(i, 2) }; - if (glyphPair.size() != 2) + std::string glyphPair{ glyphs.substr(i, 2u) }; + if (glyphPair.size() != 2u) { if (m_throwExceptions) throw Exception(exceptionPrefix + "cannot set kerning - final glyph pair is missing second glyph."); @@ -311,7 +311,7 @@ const sf::Texture* BitmapFont::getTexture() const return &m_texture; } -const BitmapFont::Glyph BitmapFont::getGlyph(const unsigned int glyphIndex) const +const BitmapFont::Glyph BitmapFont::getGlyph(const std::size_t glyphIndex) const { if (!priv_isGlyphIndexValid(glyphIndex)) { @@ -326,14 +326,14 @@ const BitmapFont::Glyph BitmapFont::getGlyph(const unsigned int glyphIndex) cons return m_glyphs[glyphIndex]; } -const unsigned int BitmapFont::getNumberOfGlyphs() const +const std::size_t BitmapFont::getNumberOfGlyphs() const { - return static_cast(m_glyphs.size()); + return m_glyphs.size(); } const int BitmapFont::getKerning(const std::string& glyphPair) const { - if (glyphPair.size() != 2) + if (glyphPair.size() != 2u) { if (m_throwExceptions) throw Exception(exceptionPrefix + "cannot get kerning - glyph pair not valid."); @@ -354,7 +354,7 @@ const int BitmapFont::getKerning(const std::string& glyphPair) const // PRIVATE -const bool BitmapFont::priv_isGlyphIndexValid(const unsigned int glyphIndex) const +const bool BitmapFont::priv_isGlyphIndexValid(const std::size_t glyphIndex) const { if (glyphIndex < m_glyphs.size()) return true; @@ -364,16 +364,16 @@ const bool BitmapFont::priv_isGlyphIndexValid(const unsigned int glyphIndex) con return false; } -const BitmapFont::Glyph BitmapFont::priv_getGlyphWithDefaultTextureRect(unsigned int glyphIndex) const +const BitmapFont::Glyph BitmapFont::priv_getGlyphWithDefaultTextureRect(std::size_t glyphIndex) const { if (!priv_isGlyphIndexValid(glyphIndex)) { if (m_throwExceptions) throw Exception(exceptionPrefix + "cannot get default glyph - glyph index (" + std::to_string(glyphIndex) + ") out of range."); - glyphIndex = 0; + glyphIndex = 0u; } - if (m_glyphs.size() == 0) + if (m_glyphs.empty()) throw Exception(exceptionPrefix + "BUG - no glyphs available."); Glyph defaultGlyph; @@ -389,7 +389,7 @@ const BitmapFont::Glyph BitmapFont::priv_getGlyphWithDefaultTextureRect(unsigned void BitmapFont::priv_setKerning(const int kerning, const std::string& glyphPair) { - if (glyphPair.size() != 2) + if (glyphPair.size() != 2u) { if (m_throwExceptions) throw Exception(exceptionPrefix + "cannot set kerning - glyph pair not valid."); diff --git a/src/SelbaWard/BitmapFont.hpp b/src/SelbaWard/BitmapFont.hpp index 729950c..c4348d6 100644 --- a/src/SelbaWard/BitmapFont.hpp +++ b/src/SelbaWard/BitmapFont.hpp @@ -47,7 +47,7 @@ class BitmapFont public: struct Glyph { - bool useDefaultTextureRect = true; + bool useDefaultTextureRect{ true }; sf::IntRect textureRect; int width; // zero and below represent counting from full texture rect width e.g. 0 is full width, -1 is 1 less than full width. int baseline; // negative numbers represent counting from bottom e.g. -1 is bottom line, -2 is 1 above bottom. @@ -58,38 +58,38 @@ class BitmapFont // output const sf::Texture* getTexture() const; - const Glyph getGlyph(unsigned int glyphIndex = 0) const; - const unsigned int getNumberOfGlyphs() const; + const Glyph getGlyph(std::size_t glyphIndex = 0u) const; + const std::size_t getNumberOfGlyphs() const; const int getKerning(const std::string& glyphPair) const; // texture setup void setExternalTexture(const sf::Texture& externalTexture); void loadTexture(const std::string& filename); void setSmooth(bool smooth = true); - void setNumberOfTilesPerRow(unsigned int numberOfTilesPerRow); + void setNumberOfTilesPerRow(std::size_t numberOfTilesPerRow); // texture rect setup void setDefaultTextureRect(const sf::IntRect& defaultTextureRect); - void setTextureRect(const sf::IntRect& textureRect, unsigned int glyphIndex = 0); - void setTextureRects(const std::vector& textureRects, unsigned int initialGlyphIndex = 0); - void clearTextureRect(unsigned int glyphIndex); + void setTextureRect(const sf::IntRect& textureRect, std::size_t glyphIndex = 0u); + void setTextureRects(const std::vector& textureRects, std::size_t initialGlyphIndex = 0u); + void clearTextureRect(std::size_t glyphIndex); void clearAllTextureRects(); // glyph setup - void setGlyphToDefault(unsigned int glyphIndex = 0); - void setGlyphsToDefault(unsigned int numberOfGlyphs = 1, unsigned int glyphIndex = 0); + void setGlyphToDefault(std::size_t glyphIndex = 0u); + void setGlyphsToDefault(std::size_t numberOfGlyphs = 1u, std::size_t glyphIndex = 0u); void setAllGlyphsToDefault(); // glyph attribute setup - void setBaseline(int baseline, unsigned int glyphIndex = 0); - void setWidth(int width, unsigned int glyphIndex = 0); - void setStartX(int startX, unsigned int glyphIndex = 0); - void setBaselines(int baseline, unsigned int numberOfGlyphs = 1, unsigned int initialGlyphIndex = 0); - void setWidths(int width, unsigned int numberOfGlyphs = 1, unsigned int initialGlyphIndex = 0); - void setStartXs(int startX, unsigned int numberOfGlyphs = 1, unsigned int initialGlyphIndex = 0); - void setBaselines(const std::vector& baselines, unsigned int initialGlyphIndex = 0); - void setWidths(const std::vector& widths, unsigned int initialGlyphIndex = 0); - void setStartXs(const std::vector& startXs, unsigned int initialGlyphIndex = 0); + void setBaseline(int baseline, std::size_t glyphIndex = 0u); + void setWidth(int width, std::size_t glyphIndex = 0u); + void setStartX(int startX, std::size_t glyphIndex = 0u); + void setBaselines(int baseline, std::size_t numberOfGlyphs = 1u, std::size_t initialGlyphIndex = 0u); + void setWidths(int width, std::size_t numberOfGlyphs = 1u, std::size_t initialGlyphIndex = 0u); + void setStartXs(int startX, std::size_t numberOfGlyphs = 1u, std::size_t initialGlyphIndex = 0u); + void setBaselines(const std::vector& baselines, std::size_t initialGlyphIndex = 0u); + void setWidths(const std::vector& widths, std::size_t initialGlyphIndex = 0u); + void setStartXs(const std::vector& startXs, std::size_t initialGlyphIndex = 0u); void setBaseline(int baseline, const std::string& glyphs); void setWidth(int width, const std::string& glyphs); void setStartX(int startX, const std::string& glyphs); @@ -119,8 +119,8 @@ class BitmapFont mutable std::map m_kernings; std::vector m_glyphs; - const bool priv_isGlyphIndexValid(unsigned int glyphIndex) const; - const Glyph priv_getGlyphWithDefaultTextureRect(unsigned int glyphIndex = 0) const; + const bool priv_isGlyphIndexValid(std::size_t glyphIndex) const; + const Glyph priv_getGlyphWithDefaultTextureRect(std::size_t glyphIndex = 0u) const; void priv_setKerning(int kerning, const std::string& glyphs); // string must have length of 2 }; diff --git a/src/SelbaWard/BitmapText.cpp b/src/SelbaWard/BitmapText.cpp index 937ef74..ed6100b 100644 --- a/src/SelbaWard/BitmapText.cpp +++ b/src/SelbaWard/BitmapText.cpp @@ -39,7 +39,7 @@ BitmapText::BitmapText() : m_pBitmapFont{ nullptr } , m_vertices(sf::PrimitiveType::Triangles) , m_string() - , m_color(sf::Color::White) + , m_color{ sf::Color::White } , m_tracking{ 1 } { } @@ -67,7 +67,7 @@ const std::string BitmapText::getString() const return m_string; } -void BitmapText::setTracking(int tracking) +void BitmapText::setTracking(const int tracking) { m_tracking = tracking; priv_updateVertices(); @@ -89,17 +89,17 @@ const sf::Color BitmapText::getColor() const return m_color; } -void BitmapText::setScale(unsigned int scale) +void BitmapText::setScale(const std::size_t scale) { setScale(scale, scale); } -void BitmapText::setScale(sf::Vector2u scale) +void BitmapText::setScale(const sf::Vector2u scale) { this->Transformable::setScale(sf::Vector2f(scale)); } -void BitmapText::setScale(unsigned int scaleX, unsigned int scaleY) +void BitmapText::setScale(const std::size_t scaleX, const std::size_t scaleY) { setScale({ scaleX, scaleY }); } @@ -140,15 +140,16 @@ void BitmapText::priv_updateVertices() sf::Vector2f penPosition{ 0.f, 0.f }; - float minX(0.f), minY(0.f), maxX(0.f), maxY(0.f); + sf::Vector2f min{ 0.f, 0.f }; + sf::Vector2f max{ 0.f, 0.f }; - for (unsigned int character{ 0 }; character < m_string.length(); ++character) + for (std::size_t character{ 0u }; character < m_string.length(); ++character) { - const unsigned int glyphNumber{ (m_string[character] >= 0) ? static_cast(m_string[character]) : static_cast(m_string[character] + 256) }; // after 125, 126, 127 is -128, -127, -126. this moves them to 128, 129, 130 + const std::size_t glyphNumber{ (m_string[character] >= 0u) ? static_cast(m_string[character]) : static_cast(m_string[character] + 256) }; // after 125, 126, 127 is -128, -127, -126. this moves them to 128, 129, 130 - const int kerning{ (character < m_string.length() - 1) ? m_pBitmapFont->getKerning(m_string.substr(character, 2)) : 0 }; + const int kerning{ (character < m_string.length() - 1u) ? m_pBitmapFont->getKerning(m_string.substr(character, 2u)) : 0 }; - BitmapFont::Glyph glyph = m_pBitmapFont->getGlyph(glyphNumber); + BitmapFont::Glyph glyph{ m_pBitmapFont->getGlyph(glyphNumber) }; const sf::Vector2f glyphOffset{ 0.f - glyph.startX, (glyph.baseline < 0) ? (0.f - glyph.baseline - glyph.textureRect.size.y) : (0.f - glyph.baseline) }; const sf::Vector2f glyphPosition{ penPosition + glyphOffset }; @@ -174,12 +175,12 @@ void BitmapText::priv_updateVertices() m_vertices[(character * 6u) + 4u] = m_vertices[(character * 6u) + 2u]; m_vertices[(character * 6u) + 5u] = m_vertices[(character * 6u) + 1u]; - penPosition += sf::Vector2f((glyph.width > 0) ? (0.f + m_tracking + kerning + glyph.width) : (0.f + m_tracking + kerning + glyph.width + glyph.textureRect.size.x - glyph.startX), 0); + penPosition.x += (glyph.width > 0) ? (0.f + m_tracking + kerning + glyph.width) : (0.f + m_tracking + kerning + glyph.width + glyph.textureRect.size.x - glyph.startX); - minX = std::min(minX, m_vertices[(character * 6u) + 0u].position.x); - maxX = std::max(maxX, m_vertices[(character * 6u) + 3u].position.x); - minY = std::min(minY, m_vertices[(character * 6u) + 0u].position.y); - maxY = std::max(maxY, m_vertices[(character * 6u) + 3u].position.y); + min.x = std::min(min.x, m_vertices[(character * 6u) + 0u].position.x); + max.x = std::max(max.x, m_vertices[(character * 6u) + 3u].position.x); + min.y = std::min(min.y, m_vertices[(character * 6u) + 0u].position.y); + max.y = std::max(max.y, m_vertices[(character * 6u) + 3u].position.y); } priv_updateColor(); @@ -192,7 +193,7 @@ void BitmapText::priv_updateVertices() void BitmapText::priv_updateColor() { - for (unsigned int v{ 0 }; v < m_vertices.getVertexCount(); ++v) + for (std::size_t v{ 0u }; v < m_vertices.getVertexCount(); ++v) m_vertices[v].color = m_color; } diff --git a/src/SelbaWard/BitmapText.hpp b/src/SelbaWard/BitmapText.hpp index 6b350a5..3ed9ba5 100644 --- a/src/SelbaWard/BitmapText.hpp +++ b/src/SelbaWard/BitmapText.hpp @@ -53,8 +53,8 @@ class BitmapText : public sf::Drawable, public sf::Transformable const int getTracking() const; void setColor(const sf::Color& color); const sf::Color getColor() const; - void setScale(unsigned int scale); - void setScale(unsigned int scaleX, unsigned int scaleY); + void setScale(std::size_t scale); + void setScale(std::size_t scaleX, std::size_t scaleY); void setScale(sf::Vector2u scale); sf::FloatRect getGlobalBounds() const; sf::FloatRect getLocalBounds() const; diff --git a/src/SelbaWard/FrameTransition.cpp b/src/SelbaWard/FrameTransition.cpp new file mode 100644 index 0000000..859b6aa --- /dev/null +++ b/src/SelbaWard/FrameTransition.cpp @@ -0,0 +1,765 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Selba Ward (https://github.com/Hapaxia/SelbaWard) +// -- +// +// Frame Transition +// +// Copyright(c) 2023-2024 M.J.Silk +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions : +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software.If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +// M.J.Silk +// MJSilk2@gmail.com +// +////////////////////////////////////////////////////////////////////////////// + +#include "FrameTransition.hpp" + +#include // for lround + +namespace +{ + +inline float linearInterpolation(const float a, const float b, const float alpha) +{ + return (a * (1.f - alpha)) + (b * alpha); +} +inline sf::Color colorFromColorAndAlpha(sf::Color color, const unsigned int alpha) +{ + color.a = static_cast(alpha); + return color; +} + +} // namespace + +namespace selbaward +{ + +FrameTransition::FrameTransition(const sf::Vector2f size) + : m_ratio{ 0.f } + , m_size{} + , m_transitionId{} + , m_drawAOverB{ true } + , m_parameter1{} + , m_parameter2{} + , m_frameA{} + , m_frameB{} + + // mutable + , m_isUpdateRequired{ true } + , m_vertices() +{ + setTransition(T::TexCrop_A_Start | T::TexCrop_B_End | T::Direction_Right); +} +void FrameTransition::setSize(const sf::Vector2f size) +{ + m_isUpdateRequired = true; + m_size = size; +} +void FrameTransition::setDrawOrderToAOverB() +{ + m_drawAOverB = true; +} +void FrameTransition::setDrawOrderToBOverA() +{ + m_drawAOverB = false; +} + +void FrameTransition::setPercentage(const float percentage) +{ + setRatio(percentage / 100.f); +} +void FrameTransition::setRatio(const float ratio) +{ + m_isUpdateRequired = true; + m_ratio = ratio; + if (m_ratio < 0.f) + m_ratio = 0.f; + else if (m_ratio > 1.f) + m_ratio = 1.f; +} +void FrameTransition::setTransition(const T transitionId) +{ + m_isUpdateRequired = true; + m_transitionId = transitionId; + + switch (m_transitionId & T::Type) + { + case T::Type_Zoom: + case T::Type_TexCrop: + default: + m_frameA.numberOfVertices = 6u; + m_frameB.numberOfVertices = 6u; + } +} + +void FrameTransition::setColors(const sf::Color colorA, const sf::Color colorB) +{ + setColor(FrameId::A, colorA); + setColor(FrameId::B, colorB); +} +void FrameTransition::setColors(const sf::Color color) +{ + setColors(color, color); +} +void FrameTransition::setColor(const FrameId frameId, const sf::Color color) +{ + priv_getFrame(frameId).color = color; +} +sf::Color FrameTransition::getColor(const FrameId frameId) const +{ + return priv_getFrame(frameId).color; +} + +void FrameTransition::setTextures(const sf::Texture& textureA, const sf::Texture& textureB) +{ + setTexture(FrameId::A, textureA); + setTexture(FrameId::B, textureB); +} +void FrameTransition::setTextures(const sf::Texture& texture) +{ + setTextures(texture, texture); +} +void FrameTransition::setTextures() +{ + setTexture(FrameId::A); + setTexture(FrameId::B); +} +void FrameTransition::setTexture(const FrameId frameId, const sf::Texture& texture, const bool resetRect) +{ + m_isUpdateRequired = true; + Frame& frame{ priv_getFrame(frameId) }; + frame.pTexture = &texture; + if (resetRect) + { + frame.textureRect.width = frame.pTexture->getSize().x; + frame.textureRect.height = frame.pTexture->getSize().y; + } +} +void FrameTransition::setTexture(const FrameId frameId) +{ + m_isUpdateRequired = true; + priv_getFrame(frameId).pTexture = nullptr; +} +void FrameTransition::setTextureRect(const FrameId frameId, const sf::IntRect& textureRect) +{ + m_isUpdateRequired = true; + priv_getFrame(frameId).textureRect = textureRect; +} +const sf::Texture& FrameTransition::getTexture(const FrameId frameId) const +{ + return *(priv_getFrame(frameId).pTexture); +} + +void FrameTransition::setParameter1(const float parameterValue) +{ + m_parameter1 = parameterValue; +} +void FrameTransition::setParameter2(const float parameterValue) +{ + m_parameter2 = parameterValue; +} +float FrameTransition::getParameter1() const +{ + return m_parameter1; +} +float FrameTransition::getParameter2() const +{ + return m_parameter2; +} +void FrameTransition::resetParameters() +{ + setParameter1(); + setParameter2(); +} + +sf::FloatRect FrameTransition::getLocalBounds() const +{ + return{ { 0.f, 0.f }, m_size }; +} +sf::FloatRect FrameTransition::getGlobalBounds() const +{ + return getTransform().transformRect(getLocalBounds()); +} + + + +// PRIVATE +void FrameTransition::draw(sf::RenderTarget& target, sf::RenderStates states) const +{ + states.transform *= getTransform(); + + if (m_isUpdateRequired) + priv_update(); + + if (m_frameA.pTexture == m_frameB.pTexture) + { + states.texture = m_frameA.pTexture; + if (m_drawAOverB) + target.draw(m_vertices.data(), m_vertices.size(), sf::PrimitiveType::Triangles, states); + else + { + target.draw(m_vertices.data() + m_frameB.numberOfVertices, m_frameA.numberOfVertices, sf::PrimitiveType::Triangles, states); + target.draw(m_vertices.data(), m_frameB.numberOfVertices, sf::PrimitiveType::Triangles, states); + } + } + else + { + if (m_drawAOverB) + { + states.texture = m_frameB.pTexture; + target.draw(m_vertices.data(), m_frameB.numberOfVertices, sf::PrimitiveType::Triangles, states); + states.texture = m_frameA.pTexture; + target.draw(m_vertices.data() + m_frameB.numberOfVertices, m_frameA.numberOfVertices, sf::PrimitiveType::Triangles, states); + } + else + { + states.texture = m_frameA.pTexture; + target.draw(m_vertices.data() + m_frameB.numberOfVertices, m_frameA.numberOfVertices, sf::PrimitiveType::Triangles, states); + states.texture = m_frameB.pTexture; + target.draw(m_vertices.data(), m_frameB.numberOfVertices, sf::PrimitiveType::Triangles, states); + } + } +} +void FrameTransition::priv_update() const +{ + m_isUpdateRequired = false; + m_vertices.resize(m_frameA.numberOfVertices + m_frameB.numberOfVertices); + + + + enum class Type + { + TexCrop, + Zoom, + } type; + + if ((m_transitionId & T::Type) == T::Type_Zoom) + type = Type::Zoom; + else + type = Type::TexCrop; + + switch (type) + { + case Type::Zoom: + priv_updateFromZoom(); + break; + case Type::TexCrop: + priv_updateFromTexCrop(); + break; + } + + + + sf::Color colorA{}; + sf::Color colorB{}; + + switch (m_transitionId & T::Fade_A) + { + case T::Fade_A_Step: + colorA = (m_ratio > 0.5f) ? colorFromColorAndAlpha(m_frameA.color, 0u) : m_frameA.color; + break; + case T::Fade_A_Linear: + colorA = colorFromColorAndAlpha(m_frameA.color, std::lround(m_frameA.color.a * (1.f - m_ratio))); + break; + case T::Fade_A_Off: + colorA = colorFromColorAndAlpha(m_frameA.color, 0u); + break; + case T::Fade_A_None: + colorA = m_frameA.color; + break; + } + switch (m_transitionId & T::Fade_B) + { + case T::Fade_B_Step: + colorB = (m_ratio > 0.5f) ? m_frameB.color : colorFromColorAndAlpha(m_frameB.color, 0u); + break; + case T::Fade_B_Linear: + colorB = colorFromColorAndAlpha(m_frameB.color, std::lround(m_frameA.color.a * m_ratio)); + break; + case T::Fade_B_Off: + colorB = colorFromColorAndAlpha(m_frameB.color, 0u); + break; + case T::Fade_B_None: + colorB = m_frameB.color; + break; + } + + for (std::size_t i{ 0u }; i < m_frameB.numberOfVertices; ++i) + m_vertices[i].color = colorB; + for (std::size_t i{ 0u }; i < m_frameA.numberOfVertices; ++i) + m_vertices[m_frameB.numberOfVertices + i].color = colorA; +} +void FrameTransition::priv_updateFromTexCrop() const +{ + float ratio{ m_ratio }; + float inverseRatio{ 1.f - m_ratio }; + bool swapDirection{ false }; + + + + enum class Direction + { + Right, + Left, + Down, + Up, + } direction; + if ((m_transitionId & T::Direction_Up) == T::Direction_Up) + direction = Direction::Up; + else if ((m_transitionId & T::Direction_Left) != T::None) + direction = Direction::Left; + else if ((m_transitionId & T::Direction_Down) != T::None) + direction = Direction::Down; + else + direction = Direction::Right; + + switch (direction) + { + case Direction::Left: + case Direction::Up: + swapDirection = true; + break; + } + + + + // front is left/up (depending on horizontal/vertical direction) + // back is right/down (depending on horizontal/vertical direction) + // note that this is different from "start" and "end" which refers to different edges depending on direction (e.g. start is right edge when direction is left) + enum class TexCrop + { + None, + Front, + Back, + Both, + Shuffle, + }; + TexCrop texCropA{ TexCrop::None }; + TexCrop texCropB{ TexCrop::None }; + + T transitionTexCropA{ m_transitionId & T::TexCrop_A }; + T transitionTexCropB{ m_transitionId & T::TexCrop_B }; + + if ((transitionTexCropA & T::TexCrop_A_Shuffle) != T::None) + texCropA = TexCrop::Shuffle; + else if ((m_transitionId & T::TexCrop_A_Both) == T::TexCrop_A_Both) + texCropA = TexCrop::Both; + else if ((m_transitionId & T::TexCrop_A_Start) != T::None) + texCropA = TexCrop::Front; + else if ((m_transitionId & T::TexCrop_A_End) != T::None) + texCropA = TexCrop::Back; + else + texCropA = TexCrop::None; + + if ((transitionTexCropB & T::TexCrop_B_Shuffle) != T::None) + texCropB = TexCrop::Shuffle; + else if ((m_transitionId & T::TexCrop_B_Both) == T::TexCrop_B_Both) + texCropB = TexCrop::Both; + else if ((m_transitionId & T::TexCrop_B_Start) != T::None) + texCropB = TexCrop::Front; + else if ((m_transitionId & T::TexCrop_B_End) != T::None) + texCropB = TexCrop::Back; + else + texCropB = TexCrop::None; + + + if (swapDirection) + { + // since front and back are currenty equal to start and end, flip them if we are facing in opposite direction + if (texCropA == TexCrop::Front) + texCropA = TexCrop::Back; + else if (texCropA == TexCrop::Back) + texCropA = TexCrop::Front; + if (texCropB == TexCrop::Back) + texCropB = TexCrop::Front; + else if (texCropB == TexCrop::Front) + texCropB = TexCrop::Back; + } + + + + sf::FloatRect texRectA(m_frameA.textureRect); + sf::FloatRect texRectB(m_frameB.textureRect); + float scaleSizeA{}; + float scaleSizeB{}; + switch (direction) + { + case Direction::Left: + case Direction::Right: + scaleSizeA = texRectA.width; + scaleSizeB = texRectB.width; + break; + case Direction::Up: + case Direction::Down: + scaleSizeA = texRectA.height; + scaleSizeB = texRectB.height; + break; + } + + + + float offsetPosA{}; + float offsetPosB{}; + switch (texCropA) + { + case TexCrop::None: + break; + case TexCrop::Back: + offsetPosA = 0.f; + break; + case TexCrop::Front: + offsetPosA = scaleSizeA * ratio; + break; + case TexCrop::Both: + offsetPosA = scaleSizeA * ratio * 0.5f; + break; + case TexCrop::Shuffle: + if (swapDirection) + offsetPosA = scaleSizeA * ((ratio > 0.5f) ? inverseRatio : ratio); + else + offsetPosA = (ratio < 0.5f) ? 0.f : scaleSizeA * ((ratio * 2.f) - 1.f); + break; + } + switch (texCropB) + { + case TexCrop::None: + break; + case TexCrop::Back: + offsetPosB = 0.f; + break; + case TexCrop::Front: + offsetPosB = scaleSizeB * inverseRatio; + break; + case TexCrop::Both: + offsetPosB = scaleSizeB * inverseRatio * 0.5f; + break; + case TexCrop::Shuffle: + if (swapDirection) + offsetPosB = (ratio > 0.5f) ? 0.f : scaleSizeB * (1.f - (ratio * 2.f)); + else + offsetPosB = scaleSizeB * ((ratio > 0.5f) ? inverseRatio : ratio); + break; + } + + scaleSizeA *= inverseRatio; + scaleSizeB *= ratio; + switch (direction) + { + case Direction::Left: + case Direction::Right: + texRectA.left = offsetPosA; + texRectA.width = scaleSizeA; + texRectB.left = offsetPosB; + texRectB.width = scaleSizeB; + break; + case Direction::Down: + case Direction::Up: + texRectA.top = offsetPosA; + texRectA.height = scaleSizeA; + texRectB.top = offsetPosB; + texRectB.height = scaleSizeB; + break; + } + + + + + + if (texCropA == TexCrop::None) + texRectA = sf::FloatRect(m_frameA.textureRect); + if (texCropB == TexCrop::None) + texRectB = sf::FloatRect(m_frameB.textureRect); + + + + Quad quadA; + Quad quadB; + + Quad* qa{ &quadA }; + Quad* qb{ &quadB }; + + if (swapDirection) + { + std::swap(ratio, inverseRatio); + std::swap(qa, qb); + } + + switch (direction) + { + case Direction::Right: + case Direction::Left: + qa->topLeft.position = { m_size.x * ratio, 0.f }; + qa->bottomLeft.position = { m_size.x * ratio, m_size.y }; + qa->bottomRight.position = m_size; + qa->topRight.position = { m_size.x, 0.f }; + + qb->topLeft.position = { 0.f, 0.f }; + qb->bottomLeft.position = { 0.f, m_size.y }; + qb->bottomRight.position = { m_size.x * ratio, m_size.y }; + qb->topRight.position = { m_size.x * ratio, 0.f }; + break; + case Direction::Down: + case Direction::Up: + qa->topLeft.position = { 0.f, m_size.y * ratio }; + qa->bottomLeft.position = { 0.f, m_size.y }; + qa->bottomRight.position = m_size; + qa->topRight.position = { m_size.x, m_size.y * ratio }; + + qb->topLeft.position = { 0.f, 0.f }; + qb->bottomLeft.position = { 0.f, m_size.y * ratio }; + qb->bottomRight.position = { m_size.x, m_size.y * ratio }; + qb->topRight.position = { m_size.x, 0.f }; + break; + } + + + + priv_setQuadTextureCoordsFromRect(quadA, texRectA); + priv_setQuadTextureCoordsFromRect(quadB, texRectB); + + priv_addQuad(0u, quadB); + priv_addQuad(m_frameB.numberOfVertices, quadA); +} +void FrameTransition::priv_updateFromZoom() const +{ + const float ratio{ m_ratio }; + + enum class ZoomType + { + Crop, + Scale, + }; + const ZoomType zoomTypeA{ ((m_transitionId & T::ZoomType_A) == T::ZoomType_A_Scale) ? ZoomType::Scale : ZoomType::Crop }; + const ZoomType zoomTypeB{ ((m_transitionId & T::ZoomType_B) == T::ZoomType_B_Scale) ? ZoomType::Scale : ZoomType::Crop }; + + const float zoomScaleA{ m_parameter1 }; + const float zoomScaleB{ m_parameter2 }; + + // we use divide by this reciprical (later) instead of the standard multiplication so that texture cropping is as if it was "un-scaled" + // this allows cropping and scaling to be used together and they match movement + const float zoomScaleARecip{ 1.f / ((zoomScaleA > 0.f) ? zoomScaleA : 0.001f) }; + const float zoomScaleBRecip{ 1.f / ((zoomScaleB > 0.f) ? zoomScaleB : 0.001f) }; + + float zoomSizeScaleA{ zoomTypeA == ZoomType::Scale ? zoomScaleA : zoomScaleARecip }; + float zoomSizeScaleB{ zoomTypeB == ZoomType::Scale ? zoomScaleB : zoomScaleBRecip }; + + enum class ZoomMovementType + { + None, + In, + Out, + InOut, + OutIn, + }; + ZoomMovementType zoomMovementTypeA{}; + ZoomMovementType zoomMovementTypeB{}; + switch (m_transitionId & T::Zoom_A) + { + case T::Zoom_A_OutIn: + zoomMovementTypeA = (zoomTypeA == ZoomType::Scale) ? ZoomMovementType::InOut : ZoomMovementType::OutIn; + break; + case T::Zoom_A_InOut: + zoomMovementTypeA = (zoomTypeA == ZoomType::Scale) ? ZoomMovementType::OutIn : ZoomMovementType::InOut; + break; + case T::Zoom_A_Out: + zoomMovementTypeA = (zoomTypeA == ZoomType::Scale) ? ZoomMovementType::In : ZoomMovementType::Out; + break; + case T::Zoom_A_In: + zoomMovementTypeA = (zoomTypeA == ZoomType::Scale) ? ZoomMovementType::Out : ZoomMovementType::In; + break; + case T::Zoom_A_None: + default: + zoomMovementTypeA = ZoomMovementType::None; + } + switch (m_transitionId & T::Zoom_B) + { + case T::Zoom_B_OutIn: + zoomMovementTypeB = (zoomTypeB == ZoomType::Scale) ? ZoomMovementType::InOut : ZoomMovementType::OutIn; + break; + case T::Zoom_B_InOut: + zoomMovementTypeB = (zoomTypeB == ZoomType::Scale) ? ZoomMovementType::OutIn : ZoomMovementType::InOut; + break; + case T::Zoom_B_Out: + zoomMovementTypeB = (zoomTypeB == ZoomType::Scale) ? ZoomMovementType::In : ZoomMovementType::Out; + break; + case T::Zoom_B_In: + zoomMovementTypeB = (zoomTypeB == ZoomType::Scale) ? ZoomMovementType::Out : ZoomMovementType::In; + break; + case T::Zoom_B_None: + default: + zoomMovementTypeB = ZoomMovementType::None; + } + + float multiplierA{}; + float multiplierB{}; + switch (zoomMovementTypeA) + { + case ZoomMovementType::OutIn: + multiplierA = (ratio > 0.5f) ? linearInterpolation(1.f, zoomSizeScaleA, (ratio * 2.f) - 1.f) : linearInterpolation(zoomSizeScaleA, 1.f, ratio * 2.f); + break; + case ZoomMovementType::InOut: + multiplierA = (ratio > 0.5f) ? linearInterpolation(zoomSizeScaleA, 1.f, (ratio * 2.f) - 1.f) : linearInterpolation(1.f, zoomSizeScaleA, ratio * 2.f); + break; + case ZoomMovementType::Out: + multiplierA = linearInterpolation(zoomSizeScaleA, 1.f, ratio); + break; + case ZoomMovementType::In: + multiplierA = linearInterpolation(1.f, zoomSizeScaleA, ratio); + break; + case ZoomMovementType::None: + default: + multiplierA = 1.f; + } + switch (zoomMovementTypeB) + { + case ZoomMovementType::OutIn: + multiplierB = (ratio > 0.5f) ? linearInterpolation(1.f, zoomSizeScaleB, (ratio * 2.f) - 1.f) : linearInterpolation(zoomSizeScaleB, 1.f, ratio * 2.f); + break; + case ZoomMovementType::InOut: + multiplierB = (ratio > 0.5f) ? linearInterpolation(zoomSizeScaleB, 1.f, (ratio * 2.f) - 1.f) : linearInterpolation(1.f, zoomSizeScaleB, ratio * 2.f); + break; + case ZoomMovementType::Out: + multiplierB = linearInterpolation(zoomSizeScaleB, 1.f, ratio); + break; + case ZoomMovementType::In: + multiplierB = linearInterpolation(1.f, zoomSizeScaleB, ratio); + break; + case ZoomMovementType::None: + default: + multiplierB = 1.f; + } + + + + sf::FloatRect texRectA(m_frameA.textureRect); + sf::FloatRect texRectB(m_frameB.textureRect); + + sf::Vector2f quadSizeA{ m_size }; + sf::Vector2f quadSizeB{ m_size }; + sf::Vector2f texSizeA{ texRectA.getSize() }; + sf::Vector2f texSizeB{ texRectB.getSize() }; + switch (zoomTypeA) + { + case ZoomType::Scale: + quadSizeA *= multiplierA; + break; + case ZoomType::Crop: + texSizeA /= multiplierA; + break; + } + switch (zoomTypeB) + { + case ZoomType::Scale: + quadSizeB *= multiplierB; + break; + case ZoomType::Crop: + texSizeB /= multiplierB; + break; + } + + + + // top lefts calculated to place the resized rectangles in the centre of their originals + sf::Vector2f texTopLeftA{ (texRectA.getSize() - texSizeA) * 0.5f }; + sf::Vector2f texTopLeftB{ (texRectB.getSize() - texSizeB) * 0.5f }; + sf::Vector2f posTopLeftA{ (m_size - quadSizeA) * 0.5f }; + sf::Vector2f posTopLeftB{ (m_size - quadSizeB) * 0.5f }; + + // resized texture rectangles + texRectA.left = texTopLeftA.x; + texRectA.top = texTopLeftA.y; + texRectA.width = texSizeA.x; + texRectA.height = texSizeA.y; + + texRectB.left = texTopLeftB.x; + texRectB.top = texTopLeftB.y; + texRectB.width = texSizeB.x; + texRectB.height = texSizeB.y; + + // resized quad rectangles + sf::FloatRect posRectA{}; + sf::FloatRect posRectB{}; + + posRectA.left = posTopLeftA.x; + posRectA.top = posTopLeftA.y; + posRectA.width = quadSizeA.x; + posRectA.height = quadSizeA.y; + + posRectB.left = posTopLeftB.x; + posRectB.top = posTopLeftB.y; + posRectB.width = quadSizeB.x; + posRectB.height = quadSizeB.y; + + + + Quad quadA{}; + Quad quadB{}; + + priv_setQuadPositionsFromRect(quadA, posRectA); + priv_setQuadPositionsFromRect(quadB, posRectB); + + priv_setQuadTextureCoordsFromRect(quadA, texRectA); + priv_setQuadTextureCoordsFromRect(quadB, texRectB); + + priv_addQuad(0u, quadB); + priv_addQuad(m_frameB.numberOfVertices, quadA); +} +void FrameTransition::priv_addQuad(const std::size_t startVertex, const sf::Vertex topLeft, const sf::Vertex bottomLeft, const sf::Vertex bottomRight, const sf::Vertex topRight) const +{ + m_vertices[startVertex + 0u] = topLeft; + m_vertices[startVertex + 1u] = bottomLeft; + m_vertices[startVertex + 2u] = bottomRight; + m_vertices[startVertex + 3u] = topLeft; + m_vertices[startVertex + 4u] = bottomRight; + m_vertices[startVertex + 5u] = topRight; +} +void FrameTransition::priv_addQuad(const std::size_t startVertex, const Quad& quad) const +{ + priv_addQuad(startVertex, quad.topLeft, quad.bottomLeft, quad.bottomRight, quad.topRight); +} +void FrameTransition::priv_setQuadPositionsFromRect(Quad& quad, sf::FloatRect rect) const +{ + const sf::Vector2f topLeft(rect.getPosition()); + const sf::Vector2f bottomRight{ sf::Vector2f(rect.getSize()) + topLeft }; + quad.topLeft.position = topLeft; + quad.bottomRight.position = bottomRight; + quad.bottomLeft.position = { topLeft.x, bottomRight.y }; + quad.topRight.position = { bottomRight.x, topLeft.y }; +} +void FrameTransition::priv_setQuadTextureCoordsFromRect(Quad& quad, sf::FloatRect rect) const +{ + const sf::Vector2f topLeft(rect.getPosition()); + const sf::Vector2f bottomRight{ sf::Vector2f(rect.getSize()) + topLeft }; + quad.topLeft.texCoords = topLeft; + quad.bottomRight.texCoords = bottomRight; + quad.bottomLeft.texCoords = { topLeft.x, bottomRight.y }; + quad.topRight.texCoords = { bottomRight.x, topLeft.y }; +} +const FrameTransition::Frame& FrameTransition::priv_getFrame(const FrameId frameId) const +{ + return (frameId == FrameId::A) ? m_frameA : m_frameB; +} +FrameTransition::Frame& FrameTransition::priv_getFrame(const FrameId frameId) +{ + return (frameId == FrameId::A) ? m_frameA : m_frameB; +} + +} // namespace selbward diff --git a/src/SelbaWard/FrameTransition.hpp b/src/SelbaWard/FrameTransition.hpp new file mode 100644 index 0000000..874a17f --- /dev/null +++ b/src/SelbaWard/FrameTransition.hpp @@ -0,0 +1,352 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Selba Ward (https://github.com/Hapaxia/SelbaWard) +// -- +// +// Frame Transition +// +// Copyright(c) 2023-2024 M.J.Silk +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions : +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software.If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +// M.J.Silk +// MJSilk2@gmail.com +// +////////////////////////////////////////////////////////////////////////////// + +#ifndef SELBAWARD_FRAMETRANSITION_HPP +#define SELBAWARD_FRAMETRANSITION_HPP + +#include "Common.hpp" + +#include + +namespace selbaward +{ + +// SW Frame Transition v1.0.0 +class FrameTransition : public sf::Drawable, public sf::Transformable +{ +public: + enum class FrameId + { + A, // "start" or "source" frame, shown fully (usually) at 0 ratio + B, // "end" or "destination" frame, shown fully (usually) at 1 ratio + }; + + enum class T + { + // prefixes-only are bitmasks that cover all of that group + // e.g. Type can be used to check types only + + None = 0u, // used as a general "none" (useful for non-overly-verbose conditionals) + + + + // "_" in names are separators that show groups and each group can be (bit)masked using the name before the "_". + // e.g. TexCrop_A masks TexCrop_A_None, TexCrop_A_Start, TexCrop_A_End... + // whereas TexCrop masks all of TexCrop_A as well as TexCrop_B + + // can combine a type with any - or all - of the groups it "uses" + // e.g. with a TexCrop type, you can set the TexCrop group, the Direction group, both, or neither. + // and, within those groups, you can set A, B, both, or neither (if the group allows separate A and B settings e.g. Direction cannot be set separately for A and B) + + // Types + // + // [] = global (available for all types) + // "" = set outside of the Transition ID (T) (e.g. "parameters" are set via setParameter1 and setParameter2 methods) + // + // TexCrop + // uses: + // TexCrop_ + // Direction_ + // [Fade_] + // + // Zoom + // uses: + // Zoom_ + // ZoomType_ + // [Fade_] + // "parameters" + + Type_TexCrop = 0u, + Type_Zoom = 1u << 0u, + Type = 1u << 0u, // 1 bit + + + + // global + // applies to all types + + // fade + // can be set separately for A and B + // "none" : keeps colours exactly as they are - does not modify alpha for fades + // "off" : sets alpha to zero, allowing that frame to be invisible for the entire time + // "step" : for A, starts at normal colour and changes alpha to zero half way through; for B, start with alpha of zero and resets it half way through + // "linear" : linearly interpolates alpha. for A, starts at normal colour and interpolates alpha to zero; for B, starts with alpha of zero and interpolates to normal colour's alpha + + Fade_A_None = 0u, + Fade_A_Off = 1u << 1u, + Fade_A_Step = 2u << 1u, + Fade_A_Linear = 3u << 1u, + Fade_A = 3u << 1u, // 2 bits + Fade_B_None = 0u, + Fade_B_Off = 1u << 3u, + Fade_B_Step = 2u << 3u, + Fade_B_Linear = 3u << 3u, + Fade_B = 3u << 3u, // 2 bits + Fade = Fade_A | Fade_B, // 4 bits + + + + // tex crop + // "none" scales the texture: the entire texture (rect) is fit inside the quad. (others keep the same size texture but crop it) + // "start" and "end" are which side crops + // "start" is the beginning side of the direction (e.g. left side when direction is "right") + // "end" is the end side of the direction (e.g. right side when direction is "right") + // "both" crops both opposite sides (start and end) equally and keeps the center in the center of the quad + // "shuffle" is a special form that is designed to animate the shuffling of the frames (A slides "behind" during 2nd half, B slides "out" during 1st half) + // front edge is left for horizontal directions (left/right) and top for vertical directions (up/down) + // back edge is right for horizontal directions (left/right) and bottom for vertical directions (up/down) + // + // direction: + // this is the direction of the separating edge (the line that separates the two quads - they don't overlap) + // right: vertical separator starts from the left and moves to the right + // left: vertical separator starts from the right and moves to the left + // down: horizontal separator starts from the top and moves down to the bottom + // up: horizontal separator starts from the bottom and moves up to the top + + TexCrop_A_None = 0u, + TexCrop_A_Start = 1u << 5u, + TexCrop_A_End = 2u << 5u, + TexCrop_A_Both = TexCrop_A_Start | TexCrop_A_End, + TexCrop_A_Shuffle = 4u << 5u, + TexCrop_A = 7u << 5u, // 3 bits + TexCrop_B_None = 0u, + TexCrop_B_Start = 1u << 8u, + TexCrop_B_End = 2u << 8u, + TexCrop_B_Both = TexCrop_B_Start | TexCrop_B_End, + TexCrop_B_Shuffle = 4u << 8u, + TexCrop_B = 7u << 8u, // 3 bits + TexCrop = TexCrop_A | TexCrop_B, // 6 bits + + Direction_Right = 0u, + Direction_Left = 1u << 11u, + Direction_Down = 2u << 11u, + Direction_Up = 3u << 11u, + Direction = 3u << 11u, // 2 bits + + + + // zooming (can also be set separately for A and B): + // zooming in means the image will be larger at the end + // zooming in means the image will be smaller at the end + // zooming in&out means the image will be larger in the middle and back to normal by the end + // zooming out&in means the image will be smaller in the middle and back to normal by the end + // + // zoomtype is whether is crops the texture rect or scales the quad to zoom in and/or out (can be set separately for A and B) + // note that some combinations can cause an start or end state that isn't the full texture rect/quad of a frame (e.g. zoom out with scale ends with small, scaled B) + // + // parameters: + // parameter1: "A scale" (scales the quad or texture rect for zooming in or out) for A. this should be in the range 0-1 + // parameter2: "B scale" (scales the quad or texture rect for zooming in or out) for B. this should be in the range 0-1 + // low values (under 0.1) are only recommended for scale zoom types (not for crop zoom types) but experimentation is encouraged + + + Zoom_A_None = 0u, + Zoom_A_In = 1u << 5u, + Zoom_A_Out = 2u << 5u, + Zoom_A_InOut = 3u << 5u, + Zoom_A_OutIn = 4u << 5u, + Zoom_A = 7u << 5u, // 3 bits + Zoom_B_None = 0u, + Zoom_B_In = 1u << 8u, + Zoom_B_Out = 2u << 8u, + Zoom_B_InOut = 3u << 8u, + Zoom_B_OutIn = 4u << 8u, + Zoom_B = 7u << 8u, // 3 bits + Zoom = Zoom_A | Zoom_B, // 6 bits + + ZoomType_A_Crop = 0u, + ZoomType_A_Scale = 1u << 11u, + ZoomType_A = 1u << 11u, // 1 bit + ZoomType_B_Crop = 0u, + ZoomType_B_Scale = 1u << 12u, + ZoomType_B = 1u << 12u, // 1 bit + ZoomType = ZoomType_A | ZoomType_B, // 2 bits + }; + + // creation and size + FrameTransition(sf::Vector2f size = { 64.f, 8.f }); + void setSize(sf::Vector2f size); + sf::Vector2f getSize() const; + + // draw order + void setDrawOrderToAOverB(); // this should be always be used except for specific reasons. this places A over B and can optimise draw calls if possible. + void setDrawOrderToBOverA(); // this forces B to be drawn over A. always takes 2 separate draw calls (one for A and one for B) + + // transition ID (all transitions information; the T bitwise enum class) + void setTransition(T transitionId); + T getTransition() const; + + // transition ratio/alpha (0-1) + void setRatio(float ratio); + float getRatio() const; + void setPercentage(float percentage); // (0-100 percent instead of 0-1 ratio/alpha) + float getPercentage() const; // (0-100 percent instead of 0-1 ratio/alpha) + template + void setFromValueInRange(const Type& value, const Type& min, const Type& max); // (min-max) + template + void setFromValueInRange(const Type& value, const Type& range); // (0-range) + + // visual representation + void setColors(sf::Color colorA, sf::Color colorB); + void setColors(sf::Color color); + void setColor(FrameId frameId, sf::Color color); + sf::Color getColor(FrameId frameId) const; + + // texturing + void setTextures(const sf::Texture& textureA, const sf::Texture& textureB); + void setTextures(const sf::Texture& texture); + void setTextures(); // clear/nullify textures + void setTexture(FrameId frameId, const sf::Texture& texture, bool resetRect = false); + void setTexture(FrameId frameId); // clear/nullify texture + void setTextureRect(FrameId, const sf::IntRect& textureRectangle = sf::IntRect{}); + const sf::Texture& getTexture(FrameId frameId) const; + + // parameters + void setParameter1(float parameterValue = 0.f); + void setParameter2(float parameterValue = 0.f); + float getParameter1() const; + float getParameter2() const; + void resetParameters(); + + // bounds + sf::FloatRect getLocalBounds() const; + sf::FloatRect getGlobalBounds() const; + +private: + float m_ratio; + sf::Vector2f m_size; + T m_transitionId; + bool m_drawAOverB; + float m_parameter1; + float m_parameter2; + + struct Frame + { + const sf::Texture* pTexture{}; + sf::IntRect textureRect{}; + sf::Color color{ sf::Color::White }; + std::size_t numberOfVertices{}; + }; + Frame m_frameA; + Frame m_frameB; + + struct Quad + { + sf::Vertex topLeft; + sf::Vertex bottomLeft; + sf::Vertex bottomRight; + sf::Vertex topRight; + + void setColor(const sf::Color color) + { + topLeft.color = color; + bottomLeft.color = color; + bottomRight.color = color; + topRight.color = color; + } + }; + + mutable bool m_isUpdateRequired; + mutable std::vector m_vertices; + + virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const; + void priv_update() const; + void priv_updateFromTexCrop() const; + void priv_updateFromZoom() const; + void priv_addQuad( + const std::size_t startVertex, + const sf::Vertex topLeft, + const sf::Vertex bottomLeft, + const sf::Vertex bottomRight, + const sf::Vertex topRight) const; + void priv_addQuad(const std::size_t startVertex, const Quad& quad) const; + void priv_setQuadPositionsFromRect(Quad& quad, sf::FloatRect rect) const; + void priv_setQuadTextureCoordsFromRect(Quad& quad, sf::FloatRect rect) const; + const Frame& priv_getFrame(const FrameId frameId) const; + Frame& priv_getFrame(const FrameId frameId); +}; + +inline FrameTransition::T operator|(FrameTransition::T a, FrameTransition::T b) +{ + return static_cast(static_cast(a) | static_cast(b)); +} +inline FrameTransition::T operator^(FrameTransition::T a, FrameTransition::T b) +{ + return static_cast(static_cast(a) ^ static_cast(b)); +} +inline FrameTransition::T operator&(FrameTransition::T a, FrameTransition::T b) +{ + return static_cast(static_cast(a) & static_cast(b)); +} +inline FrameTransition::T operator~(FrameTransition::T a) +{ + return static_cast(~static_cast(a)); +} +inline FrameTransition::T operator>>(FrameTransition::T a, std::size_t bits) +{ + return static_cast(static_cast(a) >> bits); +} +inline FrameTransition::T operator<<(FrameTransition::T a, std::size_t bits) +{ + return static_cast(static_cast(a) << bits); +} + +inline sf::Vector2f FrameTransition::getSize() const +{ + return m_size; +} +inline FrameTransition::T FrameTransition::getTransition() const +{ + return m_transitionId; +} +inline float FrameTransition::getRatio() const +{ + return m_ratio; +} +inline float FrameTransition::getPercentage() const +{ + return m_ratio * 100.f; +} + +template +void FrameTransition::setFromValueInRange(const Type& value, const Type& minimum, const Type& maximum) +{ + setRatio(static_cast(value - minimum) / static_cast(maximum - minimum)); +} +template +void FrameTransition::setFromValueInRange(const Type& value, const Type& range) +{ + setRatio(static_cast(value) / static_cast(range)); +} + +} // namespace selbaward +#endif // SELBAWARD_PROGRESSBAR_HPP diff --git a/src/SelbaWard/Polygon.cpp b/src/SelbaWard/Polygon.cpp index 7749971..c41ce70 100644 --- a/src/SelbaWard/Polygon.cpp +++ b/src/SelbaWard/Polygon.cpp @@ -615,79 +615,79 @@ void Polygon::priv_triangulateEarClip() std::function isEar = [&](const std::size_t i, const std::size_t p, const std::size_t n, const std::size_t current) + { + bool aPointIsInside{ false }; + for (std::size_t other{ 0u }; other < vertexNumbers.size(); ++other) { - bool aPointIsInside{ false }; - for (std::size_t other{ 0u }; other < vertexNumbers.size(); ++other) - { - if ((vertexNumbers[other] == vertexNumbers[i]) || (vertexNumbers[other] == vertexNumbers[p]) || (vertexNumbers[other] == vertexNumbers[n]) || (vertexNumbers[other] == vertexNumbers[current])) - continue; + if ((vertexNumbers[other] == vertexNumbers[i]) || (vertexNumbers[other] == vertexNumbers[p]) || (vertexNumbers[other] == vertexNumbers[n]) || (vertexNumbers[other] == vertexNumbers[current])) + continue; - if (pointIsInsideTriangle({ m_vertices[vertexNumbers[p]].position, m_vertices[vertexNumbers[i]].position, m_vertices[vertexNumbers[n]].position }, m_vertices[vertexNumbers[other]].position)) - { - aPointIsInside = true; - break; - } + if (pointIsInsideTriangle({ m_vertices[vertexNumbers[p]].position, m_vertices[vertexNumbers[i]].position, m_vertices[vertexNumbers[n]].position }, m_vertices[vertexNumbers[other]].position)) + { + aPointIsInside = true; + break; } - return !aPointIsInside; - }; + } + return !aPointIsInside; + }; std::function isEarAnalysis = [&](const std::size_t i, const std::size_t p, const std::size_t n) - { - return isEar(i, p, n, i); - }; + { + return isEar(i, p, n, i); + }; std::function retest = [&](const std::size_t i, const std::size_t p, const std::size_t n, const std::size_t current) + { + std::vector::iterator reflexIt{ std::find(reflex.begin(), reflex.end(), indices[i]) }; + if (reflexIt != reflex.end()) { - std::vector::iterator reflexIt{ std::find(reflex.begin(), reflex.end(), indices[i]) }; - if (reflexIt != reflex.end()) - { - // if reflex, re-test - const sf::Vector2f pLine{ m_vertices[vertexNumbers[indices[i]]].position - m_vertices[vertexNumbers[indices[p]]].position }; - const sf::Vector2f nLine{ m_vertices[vertexNumbers[indices[n]]].position - m_vertices[vertexNumbers[indices[i]]].position }; - - if (m_reverseDirection != isSecondVectorAntiClockwiseOfFirstVector(pLine, nLine)) - { - reflex.erase(reflexIt); - convex.push_back(indices[i]); - } - } + // if reflex, re-test + const sf::Vector2f pLine{ m_vertices[vertexNumbers[indices[i]]].position - m_vertices[vertexNumbers[indices[p]]].position }; + const sf::Vector2f nLine{ m_vertices[vertexNumbers[indices[n]]].position - m_vertices[vertexNumbers[indices[i]]].position }; - std::vector::iterator convexIt{ std::find(convex.begin(), convex.end(), indices[i]) }; - if (convexIt != convex.end()) + if (m_reverseDirection != isSecondVectorAntiClockwiseOfFirstVector(pLine, nLine)) { - // if convex, re-test for ear only (must still be convex) - const bool isNowEar{ isEar(indices[i], indices[p], indices[n], indices[current]) }; - const std::vector::iterator it{ std::find(ear.begin(), ear.end(), indices[i]) }; - if (isNowEar && (it == ear.end())) - ear.push_back(indices[i]); - else if (!isNowEar && (it != ear.end())) - ear.erase(it); + reflex.erase(reflexIt); + convex.push_back(indices[i]); } - }; + } + + std::vector::iterator convexIt{ std::find(convex.begin(), convex.end(), indices[i]) }; + if (convexIt != convex.end()) + { + // if convex, re-test for ear only (must still be convex) + const bool isNowEar{ isEar(indices[i], indices[p], indices[n], indices[current]) }; + const std::vector::iterator it{ std::find(ear.begin(), ear.end(), indices[i]) }; + if (isNowEar && (it == ear.end())) + ear.push_back(indices[i]); + else if (!isNowEar && (it != ear.end())) + ear.erase(it); + } + }; std::function analysePoints = [&]() + { + for (std::size_t i{ 0u }; i < indicesSize; ++i) { - for (std::size_t i{ 0u }; i < indicesSize; ++i) - { - const std::size_t prev{ (i > 0u) ? (i - 1u) : (indicesSize - 1u) }; - const std::size_t next{ (i < (indicesSize - 1u)) ? (i + 1u) : 0u }; + const std::size_t prev{ (i > 0u) ? (i - 1u) : (indicesSize - 1u) }; + const std::size_t next{ (i < (indicesSize - 1u)) ? (i + 1u) : 0u }; - const sf::Vector2f prevLine{ m_vertices[vertexNumbers[indices[i]]].position - m_vertices[vertexNumbers[indices[prev]]].position }; - const sf::Vector2f nextLine{ m_vertices[vertexNumbers[indices[next]]].position - m_vertices[vertexNumbers[indices[i]]].position }; + const sf::Vector2f prevLine{ m_vertices[vertexNumbers[indices[i]]].position - m_vertices[vertexNumbers[indices[prev]]].position }; + const sf::Vector2f nextLine{ m_vertices[vertexNumbers[indices[next]]].position - m_vertices[vertexNumbers[indices[i]]].position }; - if (m_reverseDirection != !isSecondVectorAntiClockwiseOfFirstVector(prevLine, nextLine)) - reflex.push_back(indices[i]); - else - { - convex.push_back(indices[i]); - if (isEarAnalysis(indices[i], indices[prev], indices[next])) - ear.push_back(indices[i]); - } + if (m_reverseDirection != !isSecondVectorAntiClockwiseOfFirstVector(prevLine, nextLine)) + reflex.push_back(indices[i]); + else + { + convex.push_back(indices[i]); + if (isEarAnalysis(indices[i], indices[prev], indices[next])) + ear.push_back(indices[i]); } - }; + } + }; reflex.reserve(m_vertices.size() - 3u); // impossible for vertices to be reflex without enough convex (need at least 3 convex to make a polygon) convex.reserve(m_vertices.size()); // any number (up to all) of the vertices may be convex although at least 3 are required @@ -883,7 +883,7 @@ void Polygon::priv_triangulateEarClip() else return; } - + std::size_t currentPoint{ ear.front() }; std::vector::iterator currentIt{ std::find(indices.begin(), indices.end(), currentPoint) }; @@ -940,57 +940,57 @@ void Polygon::priv_triangulateBasicEarClip() std::function isEar = [&](const std::size_t i, const std::size_t p, const std::size_t n, const std::size_t current) + { + bool aPointIsInside{ false }; + for (std::size_t other{ 0u }; other < indices.size(); ++other) { - bool aPointIsInside{ false }; - for (std::size_t other{ 0u }; other < indices.size(); ++other) - { - if ((other == i) || (other == p) || (other == n) || (other == current)) - continue; + if ((other == i) || (other == p) || (other == n) || (other == current)) + continue; - if (pointIsInsideTriangle({ m_vertices[indices[p]].position, m_vertices[indices[i]].position, m_vertices[indices[n]].position }, m_vertices[indices[other]].position)) - { - aPointIsInside = true; - break; - } + if (pointIsInsideTriangle({ m_vertices[indices[p]].position, m_vertices[indices[i]].position, m_vertices[indices[n]].position }, m_vertices[indices[other]].position)) + { + aPointIsInside = true; + break; } - return !aPointIsInside; - }; + } + return !aPointIsInside; + }; std::function isEarAnalysis = [&](const std::size_t i, const std::size_t p, const std::size_t n) - { - return isEar(i, p, n, i); - }; + { + return isEar(i, p, n, i); + }; std::function retest = [&](const std::size_t i, const std::size_t p, const std::size_t n, const std::size_t current) + { + std::vector::iterator reflexIt{ std::find(reflex.begin(), reflex.end(), indices[i]) }; + if (reflexIt != reflex.end()) { - std::vector::iterator reflexIt{ std::find(reflex.begin(), reflex.end(), indices[i]) }; - if (reflexIt != reflex.end()) - { - // if reflex, re-test - const sf::Vector2f pLine{ m_vertices[indices[i]].position - m_vertices[indices[p]].position }; - const sf::Vector2f nLine{ m_vertices[indices[n]].position - m_vertices[indices[i]].position }; - - if (m_reverseDirection != isSecondVectorAntiClockwiseOfFirstVector(pLine, nLine)) - { - reflex.erase(reflexIt); - convex.push_back(indices[i]); - } - } + // if reflex, re-test + const sf::Vector2f pLine{ m_vertices[indices[i]].position - m_vertices[indices[p]].position }; + const sf::Vector2f nLine{ m_vertices[indices[n]].position - m_vertices[indices[i]].position }; - std::vector::iterator convexIt{ std::find(convex.begin(), convex.end(), indices[i]) }; - if (convexIt != convex.end()) + if (m_reverseDirection != isSecondVectorAntiClockwiseOfFirstVector(pLine, nLine)) { - // if convex, re-test for ear only (must still be convex) - const bool isNowEar{ isEar(i, p, n, current) }; - const std::vector::iterator it{ std::find(ear.begin(), ear.end(), indices[i]) }; - if (isNowEar && (it == ear.end())) - ear.push_back(indices[i]); - else if (!isNowEar && (it != ear.end())) - ear.erase(it); + reflex.erase(reflexIt); + convex.push_back(indices[i]); } - }; + } + + std::vector::iterator convexIt{ std::find(convex.begin(), convex.end(), indices[i]) }; + if (convexIt != convex.end()) + { + // if convex, re-test for ear only (must still be convex) + const bool isNowEar{ isEar(i, p, n, current) }; + const std::vector::iterator it{ std::find(ear.begin(), ear.end(), indices[i]) }; + if (isNowEar && (it == ear.end())) + ear.push_back(indices[i]); + else if (!isNowEar && (it != ear.end())) + ear.erase(it); + } + }; // analyse points for (std::size_t i{ 0u }; i < indicesSize; ++i) diff --git a/src/SelbaWard/SpriteBatch.cpp b/src/SelbaWard/SpriteBatch.cpp new file mode 100644 index 0000000..65d222c --- /dev/null +++ b/src/SelbaWard/SpriteBatch.cpp @@ -0,0 +1,519 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Selba Ward (https://github.com/Hapaxia/SelbaWard) +// -- +// +// Sprite Batch +// +// Copyright(c) 2023-2024 M.J.Silk +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions : +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software.If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +// M.J.Silk +// MJSilk2@gmail.com +// +////////////////////////////////////////////////////////////////////////////// + +#include "SpriteBatch.hpp" + +#include + +namespace +{ + +const std::string exceptionPrefix{ "Sprite Batch: " }; + +constexpr std::size_t numberOfVerticesPerQuad{ 6u }; + +} // namespace + +namespace selbaward +{ + +SpriteBatch::SpriteBatch() + : m_texture{ nullptr } + , m_orderFunction{ nullptr } + , m_orderIndices() + , m_sprites() + , m_isGlobalUpdateRequired { false } + , m_vertices() +{ + +} + +void SpriteBatch::setTexture(const sf::Texture& texture) +{ + m_texture = &texture; +} + +void SpriteBatch::setTexture() +{ + m_texture = nullptr; +} + +void SpriteBatch::setNumberOfSprites(const std::size_t numberOfSprites) +{ + if (numberOfSprites == m_sprites.size()) + return; + + m_sprites.resize(numberOfSprites); + m_isGlobalUpdateRequired = true; +} + +std::size_t SpriteBatch::getNumberOfSprites() const +{ + return m_sprites.size(); +} + +std::size_t SpriteBatch::insertSprite(std::size_t insertIndex, const std::size_t numberOfSprites, const sf::Sprite& sprite) +{ + if (numberOfSprites == 0u) + return m_sprites.size(); + + if (insertIndex > m_sprites.size()) + insertIndex = m_sprites.size(); + m_sprites.insert(m_sprites.begin() + insertIndex, numberOfSprites, Sprite{ false, sprite }); + m_isGlobalUpdateRequired = true; + return m_sprites.size(); +} + +std::size_t SpriteBatch::addSprite(const std::size_t numberOfSprites, const sf::Sprite& sprite) +{ + return insertSprite(m_sprites.size(), numberOfSprites, sprite); +} + +std::size_t SpriteBatch::removeSprite(const std::size_t removeIndex, const std::size_t numberOfSprites) +{ + if (numberOfSprites == 0u) + return m_sprites.size(); + + if (m_sprites.empty()) + throw Exception(exceptionPrefix + "Cannot remove sprite; no sprites available."); + + assert(removeIndex < m_sprites.size()); + if (removeIndex >= m_sprites.size()) + throw Exception(exceptionPrefix + "Cannot remove sprite; invalid sprite index."); + + std::size_t endIndex{ removeIndex + numberOfSprites }; + if (endIndex > m_sprites.size()) + endIndex = m_sprites.size(); + m_sprites.erase(m_sprites.begin() + removeIndex, m_sprites.begin() + endIndex); + m_isGlobalUpdateRequired = true; + return m_sprites.size(); +} + +std::size_t SpriteBatch::removeSprite(const std::size_t numberOfSprites) +{ + if (m_sprites.empty()) + throw Exception(exceptionPrefix + "Cannot remove sprite; no sprites available."); + + return removeSprite(m_sprites.size() - 1u, numberOfSprites); +} + +void SpriteBatch::batchSprites(const std::vector& sprites) +{ + const std::size_t numberOfSprites{ sprites.size() }; + m_sprites.resize(numberOfSprites); + for (std::size_t i{ 0u }; i < numberOfSprites; ++i) + m_sprites[i] = { false, sprites[i] }; + m_isGlobalUpdateRequired = true; +} + +void SpriteBatch::batchSprites(const std::vector& sprites) +{ + const std::size_t numberOfSprites{ sprites.size() }; + m_sprites.resize(numberOfSprites); + for (std::size_t i{ 0u }; i < numberOfSprites; ++i) + m_sprites[i] = { false, *sprites[i] }; + m_isGlobalUpdateRequired = true; +} + +void SpriteBatch::updateSprite(const std::size_t index, const sf::Sprite& sprite) +{ + priv_testIsIndexValid(index); + + m_sprites[index].isUpdateRequired = true; + m_sprites[index].sprite = sprite; +} + +sf::Sprite SpriteBatch::getSprite(const std::size_t index) +{ + priv_testIsIndexValid(index); + return m_sprites[index].sprite; +} + +sf::Sprite SpriteBatch::operator[](const std::size_t index) +{ + priv_testIsIndexValid(index); + return m_sprites[index].sprite; +} + +void SpriteBatch::setOrderFunction(const std::function& orderFunction) +{ + m_orderFunction = orderFunction; + m_orderIndices.clear(); +} + +void SpriteBatch::setOrderFunction() +{ + m_orderFunction = nullptr; +} + +void SpriteBatch::setOrder(const std::vector& orderIndices) +{ + m_orderIndices = orderIndices; +} + +void SpriteBatch::setOrder() +{ + m_orderIndices.clear(); +} + +void SpriteBatch::clearAllOrdering() +{ + setOrderFunction(); + setOrder(); +} + +void SpriteBatch::setPosition(const std::size_t index, const sf::Vector2f position) +{ + priv_testIsIndexValid(index); + m_sprites[index].sprite.setPosition(position); + m_sprites[index].isUpdateRequired = true; +} + +void SpriteBatch::setOrigin(const std::size_t index, const sf::Vector2f origin) +{ + priv_testIsIndexValid(index); + m_sprites[index].sprite.setOrigin(origin); + m_sprites[index].isUpdateRequired = true; +} + +void SpriteBatch::setRotation(const std::size_t index, const float rotation) +{ + priv_testIsIndexValid(index); + m_sprites[index].sprite.setRotation(rotation); + m_sprites[index].isUpdateRequired = true; +} + +void SpriteBatch::setScale(const std::size_t index, const sf::Vector2f scale) +{ + priv_testIsIndexValid(index); + m_sprites[index].sprite.setScale(scale); + m_sprites[index].isUpdateRequired = true; +} + +void SpriteBatch::setScale(const std::size_t index, const float scale) +{ + setScale(index, { scale, scale }); +} + +void SpriteBatch::setTextureRect(const std::size_t index, const sf::IntRect textureRect) +{ + priv_testIsIndexValid(index); + m_sprites[index].sprite.setTextureRect(textureRect); + m_sprites[index].isUpdateRequired = true; +} + +void SpriteBatch::setColor(const std::size_t index, const sf::Color& color) +{ + priv_testIsIndexValid(index); + m_sprites[index].sprite.setColor(color); + m_sprites[index].isUpdateRequired = true; +} + +void SpriteBatch::move(const std::size_t index, const sf::Vector2f offset) +{ + priv_testIsIndexValid(index); + m_sprites[index].sprite.move(offset); + m_sprites[index].isUpdateRequired = true; +} + +void SpriteBatch::rotate(const std::size_t index, const float angle) +{ + priv_testIsIndexValid(index); + m_sprites[index].sprite.rotate(angle); + m_sprites[index].isUpdateRequired = true; +} + +void SpriteBatch::scale(const std::size_t index, const sf::Vector2f factor) +{ + priv_testIsIndexValid(index); + m_sprites[index].sprite.scale(factor); + m_sprites[index].isUpdateRequired = true; +} + +void SpriteBatch::scale(const std::size_t index, const float factor) +{ + scale(index, { factor, factor }); +} + +sf::Vector2f SpriteBatch::getPosition(const std::size_t index) const +{ + priv_testIsIndexValid(index); + return m_sprites[index].sprite.getPosition(); +} + +sf::Vector2f SpriteBatch::getOrigin(const std::size_t index) const +{ + priv_testIsIndexValid(index); + return m_sprites[index].sprite.getOrigin(); +} + +float SpriteBatch::getRotation(const std::size_t index) const +{ + priv_testIsIndexValid(index); + return m_sprites[index].sprite.getRotation(); +} + +sf::Vector2f SpriteBatch::getScale(const std::size_t index) const +{ + priv_testIsIndexValid(index); + return m_sprites[index].sprite.getScale(); +} + +sf::IntRect SpriteBatch::getTextureRect(const std::size_t index) const +{ + priv_testIsIndexValid(index); + return m_sprites[index].sprite.getTextureRect(); +} + +sf::Color SpriteBatch::getColor(const std::size_t index) const +{ + priv_testIsIndexValid(index); + return m_sprites[index].sprite.getColor(); +} + +sf::FloatRect SpriteBatch::getLocalBounds(const std::size_t index) const +{ + priv_testIsIndexValid(index); + return m_sprites[index].sprite.getLocalBounds(); +} + +sf::FloatRect SpriteBatch::getGlobalBounds(const std::size_t index) const +{ + priv_testIsIndexValid(index); + return m_sprites[index].sprite.getGlobalBounds(); +} + +sf::Transform SpriteBatch::getTransform(const std::size_t index) const +{ + priv_testIsIndexValid(index); + return m_sprites[index].sprite.getTransform(); +} + +sf::Transform SpriteBatch::getInverseTransform(const std::size_t index) const +{ + priv_testIsIndexValid(index); + return m_sprites[index].sprite.getInverseTransform(); +} + +void SpriteBatch::move(const sf::Vector2f offset) +{ + for (auto& sprite : m_sprites) + sprite.sprite.move(offset); + m_isGlobalUpdateRequired = true; +} + +void SpriteBatch::rotate(const float angle) +{ + for (auto& sprite : m_sprites) + sprite.sprite.rotate(angle); + m_isGlobalUpdateRequired = true; +} + +void SpriteBatch::scale(const sf::Vector2f factor) +{ + for (auto& sprite : m_sprites) + sprite.sprite.scale(factor); + m_isGlobalUpdateRequired = true; +} + +void SpriteBatch::scale(const float factor) +{ + scale({ factor, factor }); + m_isGlobalUpdateRequired = true; +} + + + + + + + + + + + + + + + + + + +// PRIVATE + +void SpriteBatch::draw(sf::RenderTarget& target, sf::RenderStates states) const +{ + if ((m_orderFunction != nullptr) || (m_isGlobalUpdateRequired) || (!m_orderIndices.empty())) + priv_updateAll(); + else + priv_updateRequired(); + + states.texture = m_texture; + target.draw(m_vertices.data(), m_vertices.size(), sf::PrimitiveType::Triangles, states); +} + +void SpriteBatch::priv_testIsIndexValid(const std::size_t index) const +{ + const std::size_t numberOfSprites{ m_sprites.size() }; + assert(index < numberOfSprites); + if (index >= numberOfSprites) + throw Exception(exceptionPrefix + "Sprite index invalid."); +} + +void SpriteBatch::priv_updateAll() const +{ + if (m_sprites.empty()) + { + m_vertices.clear(); + return; + } + + const std::size_t numberOfSprites{ m_sprites.size() }; + m_vertices.resize(numberOfSprites * numberOfVerticesPerQuad); + + if (!m_orderIndices.empty()) + { + // create a vector of orig(ordered) indices + std::vector origIndices(numberOfSprites); + for (std::size_t i{ 0u }; i < numberOfSprites; ++i) + origIndices[i] = i; + + // copy order indices in reverse + std::vector orderIndices{ m_orderIndices }; + std::sort(orderIndices.rbegin(), orderIndices.rend()); + + // remove all indices in order indices from orig(ordered) indices + for (auto& orderIndex : orderIndices) + origIndices.erase(origIndices.begin() + orderIndex); + + // rebuild order indices (copy) to contain order indices followed by remaining orig(ordered) indices + orderIndices.resize(numberOfSprites); + const std::size_t sizeOfOrderIndices{ m_orderIndices.size() }; + for (std::size_t i{ 0u }; i < numberOfSprites; ++i) + { + if (i < sizeOfOrderIndices) + orderIndices[i] = m_orderIndices[i]; + else + orderIndices[i] = origIndices[i - sizeOfOrderIndices]; + } + + // update quads + for (std::size_t i{ 0u }; i < numberOfSprites; ++i) + { + priv_updateQuad(i, &(m_sprites[orderIndices[i]].sprite)); + m_sprites[i].isUpdateRequired = false; // this "i" doesn't need to match as we're clearing the update for all anyway + } + } + else if (m_orderFunction != nullptr) + { + // create vector of pointers to sort + std::vector pointers(numberOfSprites); + for (std::size_t i{ 0u }; i < numberOfSprites; ++i) + pointers[i] = &(m_sprites[i].sprite); + + // sort pointers using custom order function + std::sort(pointers.begin(), pointers.end(), m_orderFunction); + + // update quads + for (std::size_t i{ 0u }; i < numberOfSprites; ++i) + { + priv_updateQuad(i, pointers[i]); + m_sprites[i].isUpdateRequired = false; + } + } + else + { + // update quads + for (std::size_t i{ 0u }; i < numberOfSprites; ++i) + { + priv_updateQuad(i, &(m_sprites[i].sprite)); + m_sprites[i].isUpdateRequired = false; + } + } + + m_isGlobalUpdateRequired = false; +} + +void SpriteBatch::priv_updateRequired() const +{ + const std::size_t numberOfSprites{ m_sprites.size() }; + for (std::size_t i{ 0u }; i < numberOfSprites; ++i) + { + if (m_sprites[i].isUpdateRequired) + { + priv_updateQuad(i, &(m_sprites[i].sprite)); + m_sprites[i].isUpdateRequired = false; + } + } +} + +void SpriteBatch::priv_updateQuad(const std::size_t quadIndex, const sf::Sprite* sprite) const +{ + const std::size_t startVertex{ quadIndex * numberOfVerticesPerQuad }; + + const sf::Transform transform{ sprite->getTransform() }; + const sf::Color color{ sprite->getColor() }; + const sf::IntRect rect{ sprite->getTextureRect() }; + + sf::Vector2f shapeTopLeft{ 0.f, 0.f }; + sf::Vector2f shapeBottomRight(rect.getSize()); + sf::Vector2f shapeTopRight{ shapeBottomRight.x, shapeTopLeft.y }; + sf::Vector2f shapeBottomLeft{ shapeTopLeft.x, shapeBottomRight.y }; + sf::Vector2f textureTopLeft(rect.getPosition()); + sf::Vector2f textureBottomRight{ textureTopLeft + shapeBottomRight }; + sf::Vector2f textureTopRight{ textureBottomRight.x, textureTopLeft.y }; + sf::Vector2f textureBottomLeft{ textureTopLeft.x, textureBottomRight.y }; + + + shapeTopLeft = transform.transformPoint(shapeTopLeft); + shapeBottomRight = transform.transformPoint(shapeBottomRight); + shapeTopRight = transform.transformPoint(shapeTopRight); + shapeBottomLeft = transform.transformPoint(shapeBottomLeft); + + m_vertices[startVertex + 0u].position = shapeTopLeft; + m_vertices[startVertex + 0u].texCoords = textureTopLeft; + m_vertices[startVertex + 0u].color = color; + m_vertices[startVertex + 1u].position = shapeBottomLeft; + m_vertices[startVertex + 1u].texCoords = textureBottomLeft; + m_vertices[startVertex + 1u].color = color; + m_vertices[startVertex + 2u].position = shapeBottomRight; + m_vertices[startVertex + 2u].texCoords = textureBottomRight; + m_vertices[startVertex + 2u].color = color; + m_vertices[startVertex + 5u].position = shapeTopRight; + m_vertices[startVertex + 5u].texCoords = textureTopRight; + m_vertices[startVertex + 5u].color = color; + + m_vertices[startVertex + 3u] = m_vertices[startVertex + 0u]; + m_vertices[startVertex + 4u] = m_vertices[startVertex + 2u]; +} + +} // namespace selbaward diff --git a/src/SelbaWard/SpriteBatch.hpp b/src/SelbaWard/SpriteBatch.hpp new file mode 100644 index 0000000..a7cec28 --- /dev/null +++ b/src/SelbaWard/SpriteBatch.hpp @@ -0,0 +1,168 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Selba Ward (https://github.com/Hapaxia/SelbaWard) +// -- +// +// Sprite Batch +// +// Copyright(c) 2023-2024 M.J.Silk +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions : +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software.If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +// M.J.Silk +// MJSilk2@gmail.com +// +////////////////////////////////////////////////////////////////////////////// + +#ifndef SELBAWARD_SPRITEBATCH_HPP +#define SELBAWARD_SPRITEBATCH_HPP + +#include "Common.hpp" + +#include +#include + +#include + +namespace selbaward +{ + +// Sprite Batch v1.0.0 +class SpriteBatch : public sf::Drawable +{ +public: + SpriteBatch(); + + void setTexture(const sf::Texture& texture); + void setTexture(); + + void setNumberOfSprites(std::size_t numberOfSprites); + std::size_t getNumberOfSprites() const; + + std::size_t insertSprite(std::size_t insertIndex, std::size_t numberOfSprites = 1u, const sf::Sprite& sprite = sf::Sprite()); + std::size_t addSprite(std::size_t numberOfSprites = 1u, const sf::Sprite& sprite = sf::Sprite()); // to back + + std::size_t removeSprite(std::size_t removeIndex, std::size_t numberOfSprites = 1u); + std::size_t removeSprite(std::size_t numberOfSprites = 1u); // from back + + void batchSprites(const std::vector& sprites); // copy entire vector of sprites into batch and prepares it for entire update + void batchSprites(const std::vector& sprites); // copy entire vector of sprites (from the pointers) into batch and prepares it for entire update + + void updateSprite(std::size_t index, const sf::Sprite& sprite); + sf::Sprite getSprite(std::size_t index); // this sf::Sprite is a copy, not access to the internally stored one! + sf::Sprite operator[](std::size_t index); // this sf::Sprite is a copy, not access to the internally stored one! + + void setOrderFunction(const std::function& orderFunction); // sets order function and clears any manual order + void setOrderFunction(); // clears order function and but does not clear any manual order + + void setOrder(const std::vector& orderIndices); // sets manual order, which overrides order function (but doesn't clear the order function) + void setOrder(); // clears manual order and reinstates order function, if available + + void clearAllOrdering(); // clears order function and also the manual order + + + + + + + + // standard SFML sprite methods + + // setters - absolute + void setPosition(std::size_t index, sf::Vector2f position); + void setOrigin(std::size_t index, sf::Vector2f origin); + void setRotation(std::size_t index, float rotation); + void setScale(std::size_t index, sf::Vector2f scale); + void setScale(std::size_t index, float scale); // sets both x and y to the scale + void setTextureRect(std::size_t index, sf::IntRect textureRect); + void setColor(std::size_t index, const sf::Color& color); + + // setters - relative + void move(std::size_t index, sf::Vector2f offset); + void rotate(std::size_t index, float angle); + void scale(std::size_t index, sf::Vector2f factor); + void scale(std::size_t index, float factor); // scales both x and y by the same factor + + // getters (that match the setters) + sf::Vector2f getPosition(std::size_t index) const; + sf::Vector2f getOrigin(std::size_t index) const; + float getRotation(std::size_t index) const; + sf::Vector2f getScale(std::size_t index) const; + sf::IntRect getTextureRect(std::size_t index) const; + sf::Color getColor(std::size_t index) const; + + // getters (extra - no matching setter) + sf::FloatRect getLocalBounds(std::size_t index) const; + sf::FloatRect getGlobalBounds(std::size_t index) const; + sf::Transform getTransform(std::size_t index) const; + sf::Transform getInverseTransform(std::size_t index) const; + + + + // global sprite methods (affects all sprites) - relative + void move(sf::Vector2f offset); + void rotate(float angle); + void scale(sf::Vector2f factor); + void scale(float factor); // scales both x and y by the same factor + + + + + + + + + + + + + + + + + + + + + +private: + struct Sprite + { + bool isUpdateRequired; + sf::Sprite sprite; + }; + + const sf::Texture* m_texture; + + std::function m_orderFunction; + std::vector m_orderIndices; + + mutable std::vector m_sprites; + mutable bool m_isGlobalUpdateRequired; + mutable std::vector m_vertices; + + void draw(sf::RenderTarget& target, sf::RenderStates states) const; + void priv_testIsIndexValid(const std::size_t index) const; + void priv_updateAll() const; + void priv_updateRequired() const; + void priv_updateQuad(const std::size_t quadIndex, const sf::Sprite* sprite) const; +}; + +} // namespace selbaward +#endif // SELBAWARD_SPRITEBATCH_HPP diff --git a/src/SelbaWard/Starfield3d.cpp b/src/SelbaWard/Starfield3d.cpp new file mode 100644 index 0000000..7e43b12 --- /dev/null +++ b/src/SelbaWard/Starfield3d.cpp @@ -0,0 +1,332 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Selba Ward (https://github.com/Hapaxia/SelbaWard) +// -- +// +// Starfield 3D +// +// Copyright(c) 2023-2024 M.J.Silk +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions : +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software.If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +// M.J.Silk +// MJSilk2@gmail.com +// +////////////////////////////////////////////////////////////////////////////// + +#include "Starfield3d.hpp" + +#include +#include +#include + +namespace +{ + +std::mt19937 randomGenerator; +std::uniform_int_distribution randomDistributionAlpha(1u, 255u); +std::function randomAlpha; + +inline void randomSeed() +{ + std::random_device rd; + randomGenerator.seed(rd()); + randomAlpha = std::bind(randomDistributionAlpha, randomGenerator); +} + +inline float randomValue(const float low, const float high) +{ + return std::uniform_real_distribution{low, high}(randomGenerator); +} + +template +inline T linearTween(const T start, const T end, const float alpha) +{ + return static_cast((start * (1.f - alpha)) + (end * alpha)); +} + +inline sf::Color linearTween(const sf::Color& start, const sf::Color& end, const float alpha) +{ + return{ linearTween(start.r, end.r, alpha), linearTween(start.g, end.g, alpha), linearTween(start.b, end.b, alpha), linearTween(start.a, end.a, alpha) }; +} + +} // namespace + +namespace selbaward +{ + +Starfield3d::Starfield3d(const sf::Vector2f size, const std::size_t numberOfStars, const float maxDepth, const sf::Color& frontColor, const sf::Color& backColor, const float frontScale, const float backScale) + : m_depthCalibration{ 0.001f } + , m_depthSpeedCalibration{ 0.93f } + , m_isUpdateRequired{ true } + , m_size{ size } + , m_numberOfStars{ numberOfStars } + , m_positions(numberOfStars) + , m_starTemplate(6u) + , m_primitiveType{ sf::PrimitiveType::Triangles } + , m_vertices(numberOfStars) + , m_deepestSliceBounds() + , m_maxDepth{ maxDepth } + , m_frontColor{ frontColor } + , m_backColor{ backColor } + , m_frontScale{ frontScale } + , m_backScale{ backScale } +{ + randomSeed(); + regenerate(); + + m_starTemplate[0u].position = { 0.f, 1.f }; + m_starTemplate[1u].position = { 1.f, -0.5f }; + m_starTemplate[2u].position = { -1.f, -0.5f }; + m_starTemplate[3u].position = { 0.f, -1.f }; + m_starTemplate[4u].position = { -1.f, 0.5f }; + m_starTemplate[5u].position = { 1.f, 0.5f }; + + // color + for (auto& v : m_starTemplate) + v.color = sf::Color::White; +} + +void Starfield3d::move(const sf::Vector3f movement) +{ + m_isUpdateRequired = true; + for (auto& position : m_positions) + { + // move + position -= movement * std::pow(m_maxDepth * m_depthCalibration, m_depthSpeedCalibration); + + // wrap depth + if (position.z < 0.f) + position = priv_generateRandomStarPosition(EdgeLock::Back); + else if (position.z > m_maxDepth) + position = priv_generateRandomStarPosition(EdgeLock::Front); + + // wrap 2D (xy slice) + if (position.x < m_deepestSliceBounds.left) + position = priv_generateRandomStarPosition(EdgeLock::Right); + else if (position.x > (m_deepestSliceBounds.left + m_deepestSliceBounds.width)) + position = priv_generateRandomStarPosition(EdgeLock::Left); + if (position.y < m_deepestSliceBounds.top) + position = priv_generateRandomStarPosition(EdgeLock::Bottom); + else if (position.y > (m_deepestSliceBounds.top + m_deepestSliceBounds.height)) + position = priv_generateRandomStarPosition(EdgeLock::Top); + + } +} + +void Starfield3d::pan(const sf::Vector2f panAmount) +{ + m_isUpdateRequired = true; + const sf::Vector3f movement{ panAmount.x, panAmount.y, 0.f }; + for (auto& position : m_positions) + { + // move + position -= movement * (((1.f + position.z) * m_depthCalibration - 1.f)); + + // wrap 2D (xy slice) + if (position.x < m_deepestSliceBounds.left) + position = priv_generateRandomStarPosition(EdgeLock::Right); + else if (position.x > (m_deepestSliceBounds.left + m_deepestSliceBounds.width)) + position = priv_generateRandomStarPosition(EdgeLock::Left); + if (position.y < m_deepestSliceBounds.top) + position = priv_generateRandomStarPosition(EdgeLock::Bottom); + else if (position.y > (m_deepestSliceBounds.top + m_deepestSliceBounds.height)) + position = priv_generateRandomStarPosition(EdgeLock::Top); + + } +} + +void Starfield3d::regenerate() +{ + m_deepestSliceBounds = priv_calculateFrustumSliceBounds(m_maxDepth); + + m_positions.resize(m_numberOfStars); + for (auto& position : m_positions) + position = priv_generateRandomStarPosition(); +} + +void Starfield3d::regenerate(const sf::Vector2f size) +{ + m_size = size; + regenerate(); +} + +void Starfield3d::regenerate(const sf::Vector2f size, const std::size_t numberOfStars) +{ + m_numberOfStars = numberOfStars; + regenerate(size); +} + +void Starfield3d::regenerate(const std::size_t numberOfStars) +{ + regenerate(m_size, numberOfStars); +} + +void Starfield3d::setMaxDepth(const float maxDepth) +{ + m_isUpdateRequired = true; + m_maxDepth = maxDepth; +} + +void Starfield3d::setFrontColor(const sf::Color& color) +{ + m_isUpdateRequired = true; + m_frontColor = color; +} + +void Starfield3d::setBackColor(const sf::Color& color) +{ + m_isUpdateRequired = true; + m_backColor = color; +} + +void Starfield3d::setFrontScale(const float frontScale) +{ + m_isUpdateRequired = true; + m_frontScale = frontScale; +} + +void Starfield3d::setBackScale(const float backScale) +{ + m_isUpdateRequired = true; + m_backScale = backScale; +} + +void Starfield3d::setStarTemplate(const std::vector& vertices) +{ + m_isUpdateRequired = true; + m_starTemplate = vertices; +} + +void Starfield3d::setStarTemplate(const std::vector& vertices) +{ + m_isUpdateRequired = true; + m_starTemplate.resize(vertices.size()); + for (std::size_t i{ 0u }; i < m_starTemplate.size(); ++i) + { + m_starTemplate[i].position = vertices[i]; + m_starTemplate[i].color = sf::Color::White; + } +} + +void Starfield3d::setStarTemplate(const sf::VertexArray& vertexArray) +{ + m_isUpdateRequired = true; + m_starTemplate.resize(vertexArray.getVertexCount()); + for (std::size_t i{ 0u }; i < m_starTemplate.size(); ++i) + m_starTemplate[i] = vertexArray[i]; +} + + + +// PRIVATE + +void Starfield3d::draw(sf::RenderTarget& target, sf::RenderStates states) const +{ + if (m_isUpdateRequired) + priv_updateVertices(); + + states.transform *= getTransform(); + const std::size_t size{ m_vertices.size() }; + if (size > 0) + target.draw(m_vertices.data(), size, m_primitiveType, states); +} + +void Starfield3d::priv_updateVertices() const +{ + // positions' indices sorted by depth + m_positionIndices.resize(m_positions.size()); + for (std::size_t i{ 0u }; i < m_positionIndices.size(); ++i) + m_positionIndices[i] = i; + std::sort(m_positionIndices.begin(), m_positionIndices.end(), [&](std::size_t a, std::size_t b) { return m_positions[a].z > m_positions[b].z; }); + + // vertices + const std::size_t numberOfVerticesPerStar{ m_starTemplate.size() }; + m_vertices.resize(m_numberOfStars * numberOfVerticesPerStar); + for (std::size_t star{ 0u }; star < m_numberOfStars; ++star) + { + const std::size_t starIndex{ m_positionIndices[star] }; + const float depthRatio{ m_positions[starIndex].z / m_maxDepth }; + const float depthInverseRatio{ 1.f - depthRatio }; + const sf::Color color{ linearTween(m_frontColor, m_backColor, depthRatio) }; + const float depthScale{ linearTween(m_frontScale, m_backScale, depthRatio) }; + sf::Vector2f starPosition{ priv_projectPoint(m_positions[starIndex]) }; + for (std::size_t vertex{ 0u }; vertex < numberOfVerticesPerStar; ++vertex) + { + m_vertices[star * numberOfVerticesPerStar + vertex] = starPosition + (sf::Vector2f{ m_starTemplate[vertex].position.x, m_starTemplate[vertex].position.y } * depthScale); + m_vertices[star * numberOfVerticesPerStar + vertex].color = m_starTemplate[vertex].color * color; + } + } +} + +sf::Vector2f Starfield3d::priv_projectPoint(const sf::Vector3f point) const +{ + const sf::Vector2f center{ m_size / 2.f }; + const float depth{ (point.z < 0.f) ? 1.f : (m_depthCalibration * point.z) + 1.f }; + return { ((point.x - center.x) / depth) + center.x, ((point.y - center.y) / depth) + center.y }; +} + +sf::FloatRect Starfield3d::priv_calculateFrustumSliceBounds(float z) const +{ + z *= m_depthCalibration; + ++z; + const sf::Vector2f center{ m_size / 2.f }; + const sf::Vector2f topLeft{ (-center * z) + center }; + const sf::Vector2f bottomRight{ (center * z) + center }; + return { topLeft, bottomRight - topLeft }; +} + +sf::Vector3f Starfield3d::priv_generateRandomStarPosition() const +{ + sf::Vector3f position; + position.z = randomValue(0.f, m_maxDepth); + position.x = randomValue(m_deepestSliceBounds.left, m_deepestSliceBounds.left + m_deepestSliceBounds.width); + position.y = randomValue(m_deepestSliceBounds.top, m_deepestSliceBounds.top + m_deepestSliceBounds.height); + return position; +} + +sf::Vector3f Starfield3d::priv_generateRandomStarPosition(const EdgeLock edgeLock) const +{ + sf::Vector3f position; + + if (edgeLock == EdgeLock::Front) + position.z = 0.f; + else if (edgeLock == EdgeLock::Back) + position.z = m_maxDepth; + else + position.z = randomValue(0.f, m_maxDepth); + + if (edgeLock == EdgeLock::Left) + position.x = m_deepestSliceBounds.left; + else if (edgeLock == EdgeLock::Right) + position.x = m_deepestSliceBounds.left + m_deepestSliceBounds.width; + else + position.x = randomValue(m_deepestSliceBounds.left, m_deepestSliceBounds.left + m_deepestSliceBounds.width); + + if (edgeLock == EdgeLock::Top) + position.y = m_deepestSliceBounds.top; + else if (edgeLock == EdgeLock::Bottom) + position.y = m_deepestSliceBounds.top + m_deepestSliceBounds.height; + else + position.y = randomValue(m_deepestSliceBounds.top, m_deepestSliceBounds.top + m_deepestSliceBounds.height); + + return position; +} + +} // namespace selbaward diff --git a/src/SelbaWard/Starfield3d.hpp b/src/SelbaWard/Starfield3d.hpp new file mode 100644 index 0000000..7e3f80c --- /dev/null +++ b/src/SelbaWard/Starfield3d.hpp @@ -0,0 +1,107 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Selba Ward (https://github.com/Hapaxia/SelbaWard) +// -- +// +// Starfield 3D +// +// Copyright(c) 2023-2024 M.J.Silk +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions : +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software.If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +// M.J.Silk +// MJSilk2@gmail.com +// +////////////////////////////////////////////////////////////////////////////// + +#ifndef SELBAWARD_STARFIELD3D_HPP +#define SELBAWARD_STARFIELD3D_HPP + +#include "Common.hpp" +#include +#include + +namespace selbaward +{ + +// SW Starfield3d v1.0.1 +class Starfield3d : public sf::Drawable, public sf::Transformable +{ +public: + Starfield3d(sf::Vector2f size = { 0.f, 0.f }, std::size_t numberOfStars = 400u, float maxDepth = 1000000.f, const sf::Color& frontColor = sf::Color(255u, 255u, 255u, 255u), const sf::Color& backColor = sf::Color(0u, 0u, 0u, 255u), float frontScale = 1.f, float backScale = 0.f); + void regenerate(); + void regenerate(sf::Vector2f size); + void regenerate(sf::Vector2f size, std::size_t numberOfStars); + void regenerate(std::size_t numberOfStars); + + void setMaxDepth(float maxDepth); + void setFrontColor(const sf::Color& color); + void setBackColor(const sf::Color& color); + void setFrontScale(float frontScale); + void setBackScale(float backScale); + + void setStarTemplate(const std::vector& vertices); + void setStarTemplate(const std::vector& vertices); + void setStarTemplate(const sf::VertexArray& vertexArray); // ignores primitive type - uses given vertices with sf::Triangles primitive type + + void move(sf::Vector3f movement); + void pan(sf::Vector2f panAmount); + +private: + const float m_depthCalibration; + const float m_depthSpeedCalibration; + bool m_isUpdateRequired; + sf::Vector2f m_size; + std::size_t m_numberOfStars; + std::vector m_positions; + float m_maxDepth; + sf::Color m_frontColor; + sf::Color m_backColor; + float m_frontScale; + float m_backScale; + + std::vector m_starTemplate; + + sf::PrimitiveType m_primitiveType; + mutable std::vector m_vertices; + mutable std::vector m_positionIndices; + mutable sf::FloatRect m_deepestSliceBounds; + + enum class EdgeLock + { + Left, + Right, + Top, + Bottom, + Front, + Back, + }; + + virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const; + + void priv_updateVertices() const; + sf::Vector2f priv_projectPoint(sf::Vector3f point) const; + sf::FloatRect priv_calculateFrustumSliceBounds(float z) const; + sf::Vector3f priv_generateRandomStarPosition() const; + sf::Vector3f priv_generateRandomStarPosition(EdgeLock edgeLock) const; + +}; + +} // namespace selbaward +#endif // SELBAWARD_STARFIELD3D_HPP diff --git a/src/SelbaWard/TileMap.inl b/src/SelbaWard/TileMap.inl index 614ce41..b4c56d5 100644 --- a/src/SelbaWard/TileMap.inl +++ b/src/SelbaWard/TileMap.inl @@ -189,7 +189,7 @@ template void TileMap::setGridSize(const sf::Vector2u gridSize) { m_gridSize = { gridSize.x + 1, gridSize.y + 1 }; - m_grid.resize(m_gridSize.x * m_gridSize.y); + m_grid.resize(static_cast(m_gridSize.x) * m_gridSize.y); priv_recreateRenderTexture(); } @@ -390,7 +390,7 @@ void TileMap::draw(sf::RenderTarget& target, sf::RenderStates states) const template void TileMap::priv_updateVertices() const { - m_vertices.resize(m_gridSize.x * m_gridSize.y * 6u); + m_vertices.resize(static_cast(m_gridSize.x) * m_gridSize.y * 6u); if (m_gridSize.x == 0 || m_gridSize.y == 0) return; @@ -500,7 +500,7 @@ unsigned int TileMap::priv_getTileAtGridPosition(const sf::Vector2i gridPosit static_cast(gridPosition.y) >= m_gridSize.y) return 0u; - return m_grid[gridPosition.y * m_gridSize.x + gridPosition.x]; + return m_grid[static_cast(gridPosition.y) * m_gridSize.x + gridPosition.x]; } template