🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Tweeny, A Modern C++ Tweening Library

Started by
3 comments, last by Oberon_Command 7 years, 11 months ago

Hello! This post is to announce Tweeny, an inbetweening library for C++. It provides a fluid API for defining tweens, like:


auto tween = tweeny::from(0).to(50).during(100);
while (tween.progress() < 1) tween.step(1);

It supports tweens with multiple points (e.g, from 0 to 100 then to 200), multiple values (from 0, 0 to 100, 200) and multiple types (from 0, 0.0f to 10, 2.5f). It also has 30+ easing functions (based on those by Robert Penner http://robertpenner.com/easing/).

Here is a demo, compiled with emscripten, of a sprite using tweens: http://mobius3.github.io/tweeny/examples/sprite/sprite.html

- Source: http://github.com/mobius3/tweeny

- Site: http://mobius3.github.io/tweeny

- API Doc and Manual: http://mobius3.github.io/tweeny/doc

For those wondering what a tween is or why is it useful, every Game UI nowadays has some sort of tween in them: panels fade in/out, buttons wobble when hovered, things slide in/out with acceleration/deacceleration, etc. All of those pretty animations are possible through a tween with a specific easing.

The purpose of Tweeny is to facilitate that. For instance, to animate a button size, this is a possible solution:


auto tween = tweeny::from(button.w, button.h).to(button.w + 10, button.h + 10).during(200).via(easing::backOut);
tween.onStep([](int w, int h) { button.w = w; button.h = h; return false; });

/* later, in game loop */
tween.step(dt);

Tweeny is MIT licensed.

I hope this can be useful to you. Feedback is much appreciated!

Advertisement
One thing I immediately noticed on looking at tween.h was that you're using std::vector to hold your points and callbacks. Had you given any thought to adding support for custom allocators in some form?

On a style note:
// I don't like this.
// It means that if I put a breakpoint here, the breakpoint will trigger for the condition and not the result
// From experience, a good 80% of the time when I put a breakpoint near an if statement it's inside the result
// because I want the breakpoint to fire when the condition is true.
if (dismiss) dismissed.push_back(i);

// Oddly, in other parts of the code you do things this way, which is much better - not only can I put a breakpoint
// on either the condition or the result if I so choose, but the braces make merge failures really obvious and
// prevent macro mistakes (should someone decide to modify this to use a borked macro). Never omit braces in your if statements 
// even if you really want them on one line, which is not my preference. 
// 
// Style guides tend to ban single line/non-braced ifs for all of the above reasons. Yes, yes, it's easy to spot-fix,
// but in an environment where compiles can take multiple hours you want to make any part of your code "debuggable" with as
// few modifications as possible - preferably none.
if (position < 1) {
    return static_cast<T>(((end - start) / 2) * powf(2, 10 * (position - 1)) + start);
}

A couple of other things that jumped out at me from a casual read:
static T run(float position, T start, T end) {
    float s = 1.70158f;
    float t = position; // why have a separate local for this? you don't appear to modify position anywhere herein
    T b = start;
    T c = end - start;
    float d = 1;
    s *= (1.525f);
    if ((t /= d / 2) < 1) return static_cast<T>(c / 2 * (t * t * (((s) + 1) * t - s)) + b);
    float postFix = t -= 2; // if you're just 2-decrementing t, why have a separate local? Or did you mean postFix = t - 2?
    return static_cast<T>(c / 2 * ((postFix) * t * (((s) + 1) * t + s) + 2) + b);
}

My first-look impression of the API was "Oh crap, method chaining..." (which is something I don't really like a lot in general).

However, looking at it for longer than 2 seconds, I have to say: "Congrats!". You have found one of the few usage cases where it not kind-of-works but is indeed a vastly superior approach. I wouldn't know how to write any-tuple-thingie-to-any-tuple-thingie-plus-some-args in a more readable way, in one line. :)

Very nice!

One thing I immediately noticed on looking at tween.h was that you're using std::vector to hold your points and callbacks. Had you given any thought to adding support for custom allocators in some form?


I haven't, mainly because I've never used them and I don't know how to. Other than that, I don't know how to incorporate it in the API itself (a template parameter, maybe?) and how it would affect the way its used. I'm very open to suggestions, though. If you could point how the API would look like, I can see into implementing it.

On a style note:

// I don't like this.
// It means that if I put a breakpoint here, the breakpoint will trigger for the condition and not the result
// From experience, a good 80% of the time when I put a breakpoint near an if statement it's inside the result
// because I want the breakpoint to fire when the condition is true.
if (dismiss) dismissed.push_back(i);


Yeah that's a valid point. Hopefully you won't need to put a breakpoint in there, but I'll change those lines.

// Oddly, in other parts of the code you do things this way, which is much better - not only can I put a breakpoint
// on either the condition or the result if I so choose, but the braces make merge failures really obvious and
// prevent macro mistakes (should someone decide to modify this to use a borked macro). Never omit braces in your if statements 
// even if you really want them on one line, which is not my preference. 
// 
// Style guides tend to ban single line/non-braced ifs for all of the above reasons. Yes, yes, it's easy to spot-fix,
// but in an environment where compiles can take multiple hours you want to make any part of your code "debuggable" with as
// few modifications as possible - preferably none.
if (position < 1) {
    return static_cast<T>(((end - start) / 2) * powf(2, 10 * (position - 1)) + start);
}


Well that's from copy/pasting. My personal preference is to not use braces for single line ifs, but I tend to agree with you that ease to debug is more important than prettiness.

A couple of other things that jumped out at me from a casual read:

static T run(float position, T start, T end) {
    float s = 1.70158f;
    float t = position; // why have a separate local for this? you don't appear to modify position anywhere herein
    T b = start;
    T c = end - start;
    float d = 1;
    s *= (1.525f);
    if ((t /= d / 2) < 1) return static_cast<T>(c / 2 * (t * t * (((s) + 1) * t - s)) + b);
    float postFix = t -= 2; // if you're just 2-decrementing t, why have a separate local? Or did you mean postFix = t - 2?
    return static_cast<T>(c / 2 * ((postFix) * t * (((s) + 1) * t + s) + 2) + b);
}


That's bad code porting, easing functions need a bit of review.

Thanks for your feedback!

My first-look impression of the API was "Oh crap, method chaining..." (which is something I don't really like a lot in general).

However, looking at it for longer than 2 seconds, I have to say: "Congrats!". You have found one of the few usage cases where it not kind-of-works but is indeed a vastly superior approach. I wouldn't know how to write any-tuple-thingie-to-any-tuple-thingie-plus-some-args in a more readable way, in one line. :)

Very nice!


Thanks!

Yeah, there were drafts of code like


tween.add(values..., durations..., easings...)

which is simply unreadable.

There are alternatives, though, like having a definition structure and using that to add a new tween point but it was not streamlined enough. One of the things that I like most of Tweeny is that you don't need to specify types, except on callbacks.

I also based myself on GreenSock (and other JS libraries) that explores method chaining a lot and are very readable. The end result was very good.

One thing I immediately noticed on looking at tween.h was that you're using std::vector to hold your points and callbacks. Had you given any thought to adding support for custom allocators in some form?


1. I haven't, mainly because I've never used them and I don't know how to.
2. Other than that, I don't know how to incorporate it in the API itself (a template parameter, maybe?) and how it would affect the way its used. I'm very open to suggestions, though. If you could point how the API would look like, I can see into implementing it.


1. There should be a fair bit of sample code around showing how to do it. In any case, you probably don't need to know the fine details, you just need to pass an extra template argument to std::vector.
2. A template parameter is the usual way. The way I would probably do it is implement the actual library code with the custom allocator support (ie. the extra template argument) in a special namespace and then have your main API simply alias the implementation using the default allocator. I couldn't tell you how to make that play nice with variadic templates, but someone else here probably could.

This topic is closed to new replies.

Advertisement