Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
184 changes: 184 additions & 0 deletions include/tweeny/detail/easing/by-name.h
Original file line number Diff line number Diff line change
@@ -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 <stdexcept>
#include <string>
#include <string_view>

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 <typename T>
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
3 changes: 3 additions & 0 deletions include/tweeny/easing.h
Original file line number Diff line number Diff line change
Expand Up @@ -1465,4 +1465,7 @@ namespace tweeny::easing {
inline constexpr detail::steppedEasing stepped{};
}

#include "detail/easing/by-name.h"

#endif //TWEENY_EASING_H

14 changes: 14 additions & 0 deletions src/doc/manual.dox
Original file line number Diff line number Diff line change
Expand Up @@ -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)`:
Expand Down
1 change: 1 addition & 0 deletions src/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions src/tests/easings/by-name.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <stdexcept>
#include <tweeny/easing.h>
#include <tweeny/tweeny.h>

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);
}
Loading