This write-up outlines a simple animation (or, tweening) system for use with immediate mode user interfaces.
The animation system as described here was designed intentionally with the opinion that most elements of a UI should not be animated, and that of those that are animated only a couple, at most, should be active at any given moment; even though the system would work (or could be adapted) to work with many concurrent animations, it is intended that the user is utilising animations somewhat conservatively — if you want every element in your UI to be animated between states then another system will likely serve your use case better.
The approach is:
The animation system doesn't modify any of the users variables directly: it can be used to animate between values even if the two states are immutable (eg. if we're storing an enum which resolves to a size, we can still animate the size). In the case a variable is being animated the user only ever stores the "target" value in their variable and requests the animated value as needed, this assures the user variables are never in an intermediate state and instead always represent the final "true" value.
The animation itself consists of 3 functions:
The animation_update_all
function would be called each frame to update the animations:
animation_update_all(delta_time);
As a simple example, if we have a variable alpha
and wanted to transition it from its current value to 100
over 3
seconds when a button is pressed, the code might look as such:
uint64_t id = (uint64_t) α
// handle button press
if (button("Start")) {
animation_start(id, alpha, 3);
alpha = 100;
}
// retrieve the animated value for alpha, note that the
// `alpha` variable itself remains as the target value,
// we would use the value returned by `animation_get` at
// the point of rendering
double a = animation_get(id, alpha);
Note: This example uses the pointer of alpha
as its unique ID — any method of generating an id for the animation could be used here, such as an ID system you'd likely already have in place for your immediate mode UI framework to identify a given widget.
As another example, if we had a value which is stored as an enum
used to look up a size
value from a immutable array, we could still animate the value:
uint64_t id = (uint64_t) &size_enum;
if (button("Start")) {
animation_start(id, style.sizes[size_enum], 3);
size_enum = NEW_SIZE;
}
double size = animation_get(id, style.sizes[size_enum]);
The resultant size
value from animation_get
never has to be stored or
maintained by the user.
Since the animation system is only intended to deal with a handful of animated states at any given point and only needs to store state for active animations, all animation state can be stored in a small, fixed size array:
#define ANIMATION_MAX_ITEMS 32
typedef struct {
uint64_t id;
double progress, time, initial, prev;
} AnimationItem;
AnimationItem animation_items[ANIMATION_MAX_ITEMS];
int animation_item_count;
The prev
value of the AnimationItem
struct stores the last returned value; it is used by the animation_start
function for continuing an animation from its currently animated point if animation_start
is called again before the animation has completed.
The animation_update_all
function could be implemented as such:
void animation_update_all(double dt) {
for (int i = animation_item_count - 1; i >= 0; i--) {
AnimationItem *it = &animation_items[i];
// update progress
it->progress += dt / it->time;
// remove item if it has completed
if (it->progress >= 1.0) {
*it = animation_items[--animation_item_count];
}
}
}
Note: we iterate backwards so we can remove items from the array without skipping a value on the next iteration. Since the order of the items doesn't matter, to remove an item we just replace it with the last item and decrement the total item count.
For the animation_start
function our implementation first searches the array for a matching ID, if it finds one it replaces the current animation using its previously-returned animation value as its initial value. If it doesn't it will try to add a new item for the animation if there is room.
void animation_start(uint64_t id, double initial, double time) {
// try to find and replace existing item
for (int i = 0; i < animation_item_count; i++) {
AnimationItem *it = &animation_items[i];
if (it->id == id) {
it->initial = it->prev;
it->time = time;
it->progress = 0;
return;
}
}
// push new item if we have room
if (animation_item_count < ANIMATION_MAX_ITEMS) {
animation_items[animation_item_count++] = (AnimationItem){
.id = id,
.initial = initial,
.prev = initial,
.time = time,
};
}
}
In the case we've reached the maximum number of animation items, the animation is simply not added; this effectively puts the animation in its completed state and causes animation_get
to return the target value.
double animation_get(uint64_t id, double target) {
for (int i = 0; i < animation_item_count; i++) {
AnimationItem *it = &animation_items[i];
if (it->id == id) {
double p = it->progress;
p = 1 - (1 - p) * (1 - p); // apply easing
it->prev = it->initial + p * (target - it->initial);
return it->prev;
}
}
return target;
}
Outlined here is the most basic implementation of this animation system for demonstrative purposes; the implementation could be extended by adding different easing
types which are passed to the start
function, or a delay
parameter to cause an animation to wait before starting.
We could implement a means of chaining animations by having the option for items to wait for other items to be complete before progressing.
For simplicity the example here uses global variables to store it's state, where as we might be better served storing this in an AnimationContext
struct which we pass to the animation functions.
These details are choices left up to the user.