diff --git a/CHANGELOG.md b/CHANGELOG.md index babe564..5bf3082 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # Tweeny Changelog +- Version 4.1.0 + - Added `easing::byName` for runtime easing selection by identifier name + - Version 4.0.0 - **Breaking:** Requires C++17 - **Breaking:** `tweeny::from()` returns a builder; call `.build()` to create a tween diff --git a/CMakeLists.txt b/CMakeLists.txt index 062f0ad..144a729 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ cmake_minimum_required(VERSION 3.23...3.28) cmake_policy(SET CMP0063 NEW) -project(Tweeny LANGUAGES CXX VERSION 4.0.0) +project(Tweeny LANGUAGES CXX VERSION 4.1.0) # Enforce C++17 for targets built in this project set(CMAKE_CXX_STANDARD 17) @@ -73,6 +73,7 @@ target_sources(tweeny INTERFACE include/tweeny/detail/value-container.h include/tweeny/detail/easing/back.h include/tweeny/detail/easing/bounce.h + include/tweeny/detail/easing/by-name.h include/tweeny/detail/easing/circular.h include/tweeny/detail/easing/cubic.h include/tweeny/detail/easing/def.h diff --git a/include/tweeny/detail/easing/by-name.h b/include/tweeny/detail/easing/by-name.h new file mode 100644 index 0000000..e49d724 --- /dev/null +++ b/include/tweeny/detail/easing/by-name.h @@ -0,0 +1,184 @@ +/* +This file is part of the Tweeny library. + +Copyright (c) 2016-2026 Leonardo Guilherme Lucena de Freitas +Copyright (c) 2016 Guilherme R. Costa + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef TWEENY_DETAIL_EASING_BY_NAME_H +#define TWEENY_DETAIL_EASING_BY_NAME_H + +#include +#include +#include + +namespace tweeny::easing { + +/** + * @brief Callable easing selected by name at runtime. + * + * Holds only an internal id (not the name string). Construct via @ref byName. + */ +struct byNameEasing { +private: + enum class id { + linear, + def, + stepped, + quadraticIn, + quadraticOut, + quadraticInOut, + cubicIn, + cubicOut, + cubicInOut, + quarticIn, + quarticOut, + quarticInOut, + quinticIn, + quinticOut, + quinticInOut, + sinusoidalIn, + sinusoidalOut, + sinusoidalInOut, + exponentialIn, + exponentialOut, + exponentialInOut, + circularIn, + circularOut, + circularInOut, + elasticIn, + elasticOut, + elasticInOut, + backIn, + backOut, + backInOut, + bounceIn, + bounceOut, + bounceInOut, + }; + + id id_; + + explicit byNameEasing(id i) : id_(i) {} + + static id parse(std::string_view name) { + if (name == "linear") return id::linear; + if (name == "def") return id::def; + if (name == "stepped") return id::stepped; + if (name == "quadraticIn") return id::quadraticIn; + if (name == "quadraticOut") return id::quadraticOut; + if (name == "quadraticInOut") return id::quadraticInOut; + if (name == "cubicIn") return id::cubicIn; + if (name == "cubicOut") return id::cubicOut; + if (name == "cubicInOut") return id::cubicInOut; + if (name == "quarticIn") return id::quarticIn; + if (name == "quarticOut") return id::quarticOut; + if (name == "quarticInOut") return id::quarticInOut; + if (name == "quinticIn") return id::quinticIn; + if (name == "quinticOut") return id::quinticOut; + if (name == "quinticInOut") return id::quinticInOut; + if (name == "sinusoidalIn") return id::sinusoidalIn; + if (name == "sinusoidalOut") return id::sinusoidalOut; + if (name == "sinusoidalInOut") return id::sinusoidalInOut; + if (name == "exponentialIn") return id::exponentialIn; + if (name == "exponentialOut") return id::exponentialOut; + if (name == "exponentialInOut") return id::exponentialInOut; + if (name == "circularIn") return id::circularIn; + if (name == "circularOut") return id::circularOut; + if (name == "circularInOut") return id::circularInOut; + if (name == "elasticIn") return id::elasticIn; + if (name == "elasticOut") return id::elasticOut; + if (name == "elasticInOut") return id::elasticInOut; + if (name == "backIn") return id::backIn; + if (name == "backOut") return id::backOut; + if (name == "backInOut") return id::backInOut; + if (name == "bounceIn") return id::bounceIn; + if (name == "bounceOut") return id::bounceOut; + if (name == "bounceInOut") return id::bounceInOut; + throw std::invalid_argument("unknown easing name: " + std::string(name)); + } + + friend byNameEasing byName(std::string_view name); + +public: + template + T operator()(const float position, T start, T end) const { + switch (id_) { + case id::linear: return linear(position, start, end); + case id::def: return def(position, start, end); + case id::stepped: return stepped(position, start, end); + case id::quadraticIn: return quadraticIn(position, start, end); + case id::quadraticOut: return quadraticOut(position, start, end); + case id::quadraticInOut: return quadraticInOut(position, start, end); + case id::cubicIn: return cubicIn(position, start, end); + case id::cubicOut: return cubicOut(position, start, end); + case id::cubicInOut: return cubicInOut(position, start, end); + case id::quarticIn: return quarticIn(position, start, end); + case id::quarticOut: return quarticOut(position, start, end); + case id::quarticInOut: return quarticInOut(position, start, end); + case id::quinticIn: return quinticIn(position, start, end); + case id::quinticOut: return quinticOut(position, start, end); + case id::quinticInOut: return quinticInOut(position, start, end); + case id::sinusoidalIn: return sinusoidalIn(position, start, end); + case id::sinusoidalOut: return sinusoidalOut(position, start, end); + case id::sinusoidalInOut: return sinusoidalInOut(position, start, end); + case id::exponentialIn: return exponentialIn(position, start, end); + case id::exponentialOut: return exponentialOut(position, start, end); + case id::exponentialInOut: return exponentialInOut(position, start, end); + case id::circularIn: return circularIn(position, start, end); + case id::circularOut: return circularOut(position, start, end); + case id::circularInOut: return circularInOut(position, start, end); + case id::elasticIn: return elasticIn(position, start, end); + case id::elasticOut: return elasticOut(position, start, end); + case id::elasticInOut: return elasticInOut(position, start, end); + case id::backIn: return backIn(position, start, end); + case id::backOut: return backOut(position, start, end); + case id::backInOut: return backInOut(position, start, end); + case id::bounceIn: return bounceIn(position, start, end); + case id::bounceOut: return bounceOut(position, start, end); + case id::bounceInOut: return bounceInOut(position, start, end); + } + throw std::invalid_argument("invalid easing id"); + } +}; + +/** + * @brief Select a bundled easing by its identifier name. + * + * Names match the existing identifiers exactly (`linear`, `cubicInOut`, `bounceOut`, + * `def`, `stepped`, …). Unknown names throw `std::invalid_argument` immediately. + * + * @code + * auto e = tweeny::easing::byName("cubicInOut"); + * auto t = tweeny::from(0.f).to(1.f).via(e).during(60U).build(); + * // or: .via(tweeny::easing::byName("linear")) + * @endcode + * + * @param name Exact easing identifier + * @return Callable easing holding an internal id + * @throws std::invalid_argument if @p name is not a known easing + */ +inline byNameEasing byName(std::string_view name) { + return byNameEasing{byNameEasing::parse(name)}; +} + +} // namespace tweeny::easing + +#endif // TWEENY_DETAIL_EASING_BY_NAME_H diff --git a/include/tweeny/easing.h b/include/tweeny/easing.h index da7c814..5f781c3 100644 --- a/include/tweeny/easing.h +++ b/include/tweeny/easing.h @@ -1465,4 +1465,7 @@ namespace tweeny::easing { inline constexpr detail::steppedEasing stepped{}; } +#include "detail/easing/by-name.h" + #endif //TWEENY_EASING_H + diff --git a/src/doc/manual.dox b/src/doc/manual.dox index 869a762..5a46fca 100644 --- a/src/doc/manual.dox +++ b/src/doc/manual.dox @@ -168,6 +168,20 @@ namespace tweeny { See tweeny::easing namespace documentation for all available easings, or visit http://easings.net for visualizations. + @subsubsection by_name Runtime Easing Selection + + When the easing must be chosen at runtime (for example from configuration or user input), + use `easing::byName()`. It returns a callable usable with `via()`: + + @code + auto tween = tweeny::from(0).to(100).during(60U) + .via(tweeny::easing::byName("cubicInOut")) + .build(); + @endcode + + Names match the identifier names exactly (`linear`, `cubicInOut`, `bounceOut`, …). + Unknown names throw `std::invalid_argument` at the call to `byName()`. + @subsubsection custom_easing Custom Easing Functions You can provide custom easing functions as any callable matching the signature `T(float, T, T)`: diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 55e8c5d..9355ab2 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -29,6 +29,7 @@ add_executable(tweeny_tests easings/bounce.cpp easings/def.cpp easings/stepped.cpp + easings/by-name.cpp ) target_compile_features(tweeny_tests PRIVATE cxx_std_17) diff --git a/src/tests/easings/by-name.cpp b/src/tests/easings/by-name.cpp new file mode 100644 index 0000000..e891e5e --- /dev/null +++ b/src/tests/easings/by-name.cpp @@ -0,0 +1,36 @@ +#include +#include +#include +#include +#include + +TEST_CASE("byName(cubicInOut) matches easing::cubicInOut", "[easing][byName]") { + const auto named = tweeny::easing::byName("cubicInOut"); + for (float p : {0.f, 0.25f, 0.5f, 0.75f, 1.f}) { + REQUIRE(named(p, 0.f, 1.f) == Catch::Approx(tweeny::easing::cubicInOut(p, 0.f, 1.f))); + } +} + +TEST_CASE("via(byName(linear)) matches via(linear) over 0..100", "[easing][byName][tween]") { + auto named = tweeny::from(0.f).to(100.f).via(tweeny::easing::byName("linear")).during(100U).build(); + auto direct = tweeny::from(0.f).to(100.f).via(tweeny::easing::linear).during(100U).build(); + for (uint32_t frame = 0; frame <= 100U; ++frame) { + REQUIRE(named.peek(frame) == Catch::Approx(direct.peek(frame))); + } +} + +TEST_CASE("byName throws on unknown name", "[easing][byName]") { + REQUIRE_THROWS_AS(tweeny::easing::byName("notAnEasing"), std::invalid_argument); + REQUIRE_THROWS_AS(tweeny::easing::byName("cubic-in-out"), std::invalid_argument); + REQUIRE_THROWS_AS(tweeny::easing::byName(""), std::invalid_argument); +} + +TEST_CASE("byName works with int values", "[easing][byName]") { + const auto named = tweeny::easing::byName("linear"); + REQUIRE(named(0.f, 0, 100) == 0); + REQUIRE(named(0.5f, 0, 100) == 50); + REQUIRE(named(1.f, 0, 100) == 100); + + auto tween = tweeny::from(0).to(100).via(named).during(100U).build(); + REQUIRE(tween.peek(50U) == 50); +}