ml::docs | Your First App Manifests Events Messaging Plugins Scenes Resources
See all tutorials →
Intermediate 6 sections ~12 min C++17/20

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.

Introduction

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.

TypeRole
ml::AnimateThe fluent handle returned by animate(). Bound to one object.
ml::EasingA 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::AnimationManagerAdvances every live tween once per frame and cancels tracks whose owner has gone.
Animations are time-based, not frame-based. A 0.4s move takes 0.4s at 30fps and at 144fps. Nothing in your code needs to know the frame rate.
1Basics

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);
A new 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.
2Curves

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.

FamilyCurvesUse for
LinearLinearContinuous motion — scrolling, progress bars
Quad / CubicEaseIn·, EaseOut·, EaseInOut·Most UI. EaseOutCubic is the default for good reason
Quart / QuintEaseIn·, EaseOut·, EaseInOut·Longer travel where a stronger settle reads better
Sine / Expo / CircEaseIn·, EaseOut·, EaseInOut·Softer or sharper acceleration
Back / Elastic / BounceEaseOutBack, EaseOutElastic, EaseOutBouncePlayful 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.
3Generic

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.

Capture by reference only when the captured object certainly outlives the animation. Position tweens are cancelled automatically when their owner is destroyed, but a lambda capturing something else that dies first will still dangle.
4Engine

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.

CallEffect
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.
Animations are cancelled automatically when their owning object is destroyed, so a component can be deleted mid-flight without leaving a dangling track. That is the reason tweens are owned by the manager rather than by your code.
5Advanced

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); });
MethodEffect
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.
A looping tween never completes, so anything you were waiting on in onComplete will never run. Use stop() or complete() to end one deliberately.