Animation & Easing
Time-based animation built into every positionable object. Move things, fade them, drive any interpolatable property — without owning an animation object or writing an update loop.
Animation in One Call
Anything positionable in Malena can animate itself. There is no animation object to own, no update loop to write, and no timer to cancel when the object goes away — you ask a component to move and it does.
// Slide a panel to a new position over 0.4 seconds.
panel.animate().move({400.f, 300.f}, 0.4f);
// Nudge it 20px up, then run something when it lands.
panel.animate().moveBy({0.f, -20.f}, 0.2f, ml::Easing::EaseOutBack,
[&]{ panel.setEnabled(true); });
Three pieces sit behind that call. You will normally only touch the first.
| Type | Role |
|---|---|
ml::Animate | The fluent handle returned by animate(). Bound to one object. |
ml::Easing | A library of curves — EaseOutCubic, EaseInOutQuad, and so on. |
ml::Tweener<T> | The generic engine that interpolates a value over time. Reach for it directly only when animating something Malena does not know about. |
ml::AnimationManager | Advances every live tween once per frame and cancels tracks whose owner has gone. |
Moving Things
move takes an absolute destination; moveBy takes a delta. Both take a duration in seconds, an optional easing curve, and an optional completion callback.
ml::Animate& move (sf::Vector2f to, float seconds,
Easing::Fn ease = Easing::EaseOutCubic,
std::function<void()> onComplete = {});
ml::Animate& moveBy(sf::Vector2f delta, float seconds,
Easing::Fn ease = Easing::EaseOutCubic,
std::function<void()> onComplete = {});
Both return the Animate handle, so calls chain:
card.animate()
.moveBy({0.f, -8.f}, 0.12f, ml::Easing::EaseOutQuad)
.moveBy({0.f, 8.f}, 0.12f, ml::Easing::EaseInQuad);
move or moveBy cancels this object's previous position animation first. Two tweens can never fight over the same position, so you can call move on every hover without accumulating conflicting animations.Choosing an Easing Curve
The curve decides how the value travels between its endpoints. Malena ships the standard set, all as static float f(float t) taking and returning 0..1.
| Family | Curves | Use for |
|---|---|---|
| Linear | Linear | Continuous motion — scrolling, progress bars |
| Quad / Cubic | EaseIn·, EaseOut·, EaseInOut· | Most UI. EaseOutCubic is the default for good reason |
| Quart / Quint | EaseIn·, EaseOut·, EaseInOut· | Longer travel where a stronger settle reads better |
| Sine / Expo / Circ | EaseIn·, EaseOut·, EaseInOut· | Softer or sharper acceleration |
| Back / Elastic / Bounce | EaseOutBack, EaseOutElastic, EaseOutBounce | Playful emphasis — use sparingly |
Pick by what the motion means. Something entering the screen should decelerate into place (EaseOut…); something leaving should accelerate away (EaseIn…).
toast.animate().move(restingPlace, 0.28f, ml::Easing::EaseOutBack); // arrives
toast.animate().move(offScreen, 0.18f, ml::Easing::EaseInQuad, // departs
[&]{ toast.setVisible(false); });
Easing::Fn is a plain function pointer, so your own curve works anywhere a built-in does — any float(float) mapping 0→0 and 1→1.Animating Anything Else
Position is just the common case. value<T> animates anything interpolatable and hands each step to a setter you supply, so the engine never needs to know what property it is driving.
// Fade a colour.
comp.animate().value<sf::Color>(
sf::Color(255,255,255,0), sf::Color::White, 0.3f, ml::Easing::Linear,
[&](const sf::Color& c){ shape.setFillColor(c); });
// Drive a scalar — opacity, scale, scroll offset, anything.
comp.animate().value<float>(0.f, 1.f, 0.25f, ml::Easing::EaseOutQuad,
[&](float v){ pane.setScrollOffsetY(v * maxScroll); });
Because the setter is a callback, this drives properties on objects that are not components at all — an sf::Shape, a shader uniform, an audio gain.
Advancing & Cancelling
One call per frame advances everything:
ml::AnimationManager::advance(dt); // dt in seconds
ml::Application already does this, so a normal Malena app needs no wiring. You only call it yourself if you are driving the framework from your own loop.
| Call | Effect |
|---|---|
AnimationManager::advance(dt) | Step every live tween by dt seconds. |
AnimationManager::cancel(owner) | Drop every animation belonging to one object. |
AnimationManager::hasActive(owner) | Is anything still animating for this object? |
AnimationManager::activeCount() | Total live tracks — useful in tests. |
animate().cancel() | The same as cancel(this), from the component. |
Loops, Ping-Pong & Delays
For loops, ping-pong, and delays, use a Tweener<T> directly.
auto pulse = std::make_unique<ml::Tweener<float>>(0.6f, 1.0f, 0.8f);
pulse->easing(ml::Easing::EaseInOutSine)
->pingPong()
->loop()
->onUpdate([&](float a){ badge.setAlpha(a); });
| Method | Effect |
|---|---|
delay(seconds) | Wait before starting. |
easing(fn) | Set the curve. |
loop(bool) | Restart on completion. |
pingPong(bool) | Reverse each pass rather than jumping back. |
onUpdate(cb) | Called with the interpolated value each step. |
onComplete(cb) | Called once at the end. Never fires while looping. |
complete() | Jump to the end and fire onComplete. |
stop() | Halt where it is; onComplete does not fire. |
onComplete will never run. Use stop() or complete() to end one deliberately.