An API For a Keyframe Animation Package
Spent the day building a key-frame animator for an Ebitengine project. All coded in Go, which should make it usable for some other projects in theory. I don’t have a lot of experience with animation frameworks — other than as an end-user of apps that consist of a timeline where I place keyframes — so I’m not aware of what constitutes best practice for a programatic API. So I set out to build one with an API that made sense to me. After one major iteration, this is the approach I came up with.
The core model type is the Var, which represents an animatable value. These are essentially handles describing the type of value the (package) user wants to animate. At the moment there is only a single type, which is a float:
xPos := animator.NewFloatVar()
yPos := animator.NewFloatVar()
To animate the value, the user will need to create an Animation and place any created Vars on a track. Tracks consist of key-frames, which is essentially a time offset, and one or more values a Var should have at that time. These values can be created from the Var itself, by calling Set(). This returns a type holding what that Var should be set to at that key-frame (“set” is probably not the best term for this):
moveDown := animator.NewAnimation()
posTrack := moveDown.NewTrack()
posTrack.KeyFrame(0.0, []animator.FloatSet{
xPos.Set(0.0), yPos.Set(0.0),
})
posTrack.KeyFrame(3.0, []animator.FloatSet{
xPos.Set(100.0), yPos.Set(100.0),
})
Not all Vars need to be included on a particular keyframe, but prior to animating, each of the Vars known to the track will need to be tweened if there isn’t a “set” for them on the key-frame, so it’s generally good practice to set all the Vars for each track. Different tracks can have a different of Vars, so if different keyframes are needed, it probably should exist on a separate track.
Animations are designed to be reusable and simply encode what the Animation is. The motivation here is that all the expensive work should be done once, when the Animation is created. To actually animate the Vars, the user will need to create a timeline and set it to the Animation.
tl := animator.NewTimeline()
tl.SetAnimation(moveDown)
This will initialise all the Var values to what they would be at frame 0 (at the moment, frame 0 must be a time 0, but down the line I’d like to remove this to allow for blending between Animations). The value of each Var can now be retrieved, as Vars only have a value within the scope of a Timeline.
x := xPos.Value(tl)
y := yPos.Value(tl)
// x and y == 0, as that is the value they were set to in the first frame
drawRect(x, y, 100, 100)
The Timeline clock is advanced by calling the Advance() method, which takes a delta in fractions of a second. This will tween all the Vars based on the current action, and the new value can be retrieved by calling Value() again. Thus the core use of this package is advancing the clock and querying the values for each tweened frame.
Within an Ebitengine project, this is done by calling Advance within Update function and sampling the values within Draw:
var (
tl *animator.Timeline
xPos *animator.FloatVar
yPos *animator.FloatVar
)
func Update() {
dt := 1.0 / float64(ebiten.TPS())
tl.Advance(dt)
}
func Draw(screen *ebiten.Image) {
vector.FillRect(screen,
float32(xPos.Value(tl)), float32(yPos.Value(tl)),
100, 100,
col, true,
)
}
Here’s an test animation:
I’m quite happy with how this turns out. I like how little “magic” is involved in the API: values and clocks are not changed from underneath you: you need to explicitly advance the clock yourself, and read the values when you need them.
Obviously there’s room for improvement. Tracks and keyframes cannot be changed; only linear interpolation of float values are supported; and there’s no looping, bouncing, or reversing of animations. I’d hope to add all this in due time, probably based on the needs of the project I’m working on. There are also a few things I’m a little wary of, such as a global atomic int responsible for allocating an ID for each newly created Var. And the Timeline holds Var values in a map which I hope is fast enough for rendering on a screen.
But it’s nice to have something like this in my toolbox. It’s been something I’ve been wishing for a while.