Keyframes
The emotion table turned seven expressions into seven rows of data. This lesson does the same trick to something that feels much harder to write down: motion.
Every animation you have built so far was hand-written. A blink was some drawing, then a sleep, then some more drawing. Change the timing and you edit code. But an animation is really just a list of poses and how long each one holds — and a list of things is a table.
Poses on a timeline
Animators have called these poses keyframes for a hundred years. The idea works exactly as well on a four-dollar microcontroller as it does in a cartoon studio.
What a Keyframe Is
A keyframe is a single important pose in a motion, along with how long to sit on it. In hand drawn animation, the lead artist draws only the keyframes — the poses that define the movement — and the in-between drawings follow from those.
On this display you skip the in-betweens entirely and just snap from pose to pose, which at blink speed looks completely convincing.
Each frame here is three numbers:
| Value | What it means |
|---|---|
eye_height |
How tall the eyes are — 10 is wide open, 1 is shut |
eyebrow_lift |
How far the brows rise above their resting position |
hold_ms |
How long to sit on this pose before moving to the next |
And a whole blink is five of them:
1 2 3 4 5 6 7 8 | |
Read it top to bottom and you can see the blink: open, half shut, closed for 70 milliseconds, half open, open again. The closed pose holds longest because that is what makes a blink read as a blink instead of a flicker.
More Animations, Same Format
Once the format exists, new animations cost nothing but numbers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
SURPRISE snaps the eyes wide and the brows up, holds half a second, then settles. DOZE_OFF
closes the eyes in four slow stages with the brows sinking the whole way. Neither one required a
single new line of drawing code.
And because animations are now ordinary Python tuples, you can build new ones out of old ones:
1 | |
That line trims the last frame off a blink and glues a second blink onto the end. Data you can slice and join is data you can compose — something that is simply not possible when your animation is buried inside a function.
The Player Doesn't Know What a Blink Is
That's the whole point. A player that knows nothing about blinking can play a blink, a doze, and every animation you invent next year — because all it knows how to do is walk a list in time.
The Player
The player has four variables of memory and two functions. start() begins an animation;
update() advances it if the current pose has held long enough.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | |
The critical detail is that update() returns instantly when there is nothing to do. It
never sleeps and never waits. That is the non-blocking timer idea from an earlier lesson, applied
to something far more interesting than a single blink — and it is what keeps the buttons alive
while an animation runs.
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Press A halfway through DOZE_OFF and it restarts immediately. Try doing that with an animation
built from sleep() calls — you cannot, because the program is not listening.
Closing an Eye by Shrinking It
An eye squeezed down to one pixel tall isn't a thin eye — it's a shut one. face.eye() notices when the height drops to 2 or less and draws the closed-eye arc instead, so an animation can close an eye just by shrinking it.
Why This Matters Beyond Robot Faces
Separating a player from the thing it plays is one of the most reusable structures in software. You have met it before without noticing:
| Player | What it plays |
|---|---|
| A music app | A playlist |
| A game engine's animation system | A set of keyframed poses |
| A CNC machine or 3D printer | A list of moves and speeds |
This lesson's update() |
A tuple of eye heights and hold times |
In every case the player is written once and is boring on purpose, and all the creativity lives in the data. That is a good sign you have decomposed a problem well.
Things to Try
- Make the blink heavy. Change the 70 in the middle of
BLINKto 400. A snappy reflex becomes a tired droop, and you never touched the player. - Build a triple blink in one line, using the same slicing trick as
DOUBLE_BLINK. - Add a fourth number to every frame — a mouth width — so the mouth animates too. You will
change
draw_frame()once and every animation gains a moving mouth at the same time. - Play one backward by reversing the list. Does
DOZE_OFFreversed read as waking up? Some motions reverse convincingly and some do not, and noticing which is a real design question. - Interrupt an animation on purpose. Press A halfway through a doze and watch it restart cleanly — proof the player never blocks.
Motion is data now
You can now describe a new movement in three lines of numbers, hand it to a teammate who has never read your player, and have it just work.
References
- The Emotion Table — the same data-instead-of-code move, applied to still expressions
- Blinking — the hand-written blink this lesson generalizes
- Sleeping Faces — the drooping, closing motion
DOZE_OFFreproduces as data - MicroPython utime Documentation —
ticks_ms()andticks_diff(), the player's entire sense of time