Keyframes
The emotion table turned seven expressions into seven rows of data. This lab does the same trick to motion.
Every animation so far has been hand-written: a blink was some drawing, a sleep, 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 — which is a table. Animators have called those poses keyframes for a hundred years, and the idea works exactly as well on a $4 microcontroller as it does in a cartoon studio.
Motion is data too
Write the player once and every animation becomes three lines of numbers that anyone on your team can tune without touching a single drawing call.
One Frame Is Three Numbers
1 2 3 4 5 6 7 8 | |
| Column | Meaning | Range that matters |
|---|---|---|
eye_height |
How tall the eyes are | 36 is open, 3 is shut |
eyebrow_lift |
How far the brows rise above resting | −11 droops, 27 is startled |
hold_ms |
How long to sit on this pose | 0 means "this is the last one" |
Read BLINK down the first column: 36, 21, 3, 21, 36. Open, half, shut, half, open. The animation
is right there in the numbers, and you can see it without running anything.
Notice which of those three columns changed when this lab crossed over from the smaller kit. The
first two are pixels, so they were multiplied by 1.5 — the smartwatch kit's 24 became 36, and its
18-degree startled lift became 27. The third column is time, and a millisecond means the same thing
on a 240-pixel screen and a 360-pixel one, so every hold_ms below is the exact number the
animator originally chose.
The Player Knows Nothing About Blinking
Four variables are the player's entire memory. It does not know what a blink is, or what surprise looks like. It only knows how to walk a list of poses in time — which is why it can play all four animations in the lab, and every one you invent later.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
That function returns instantly when there is nothing to do, so the main loop stays free to watch the buttons — the lesson from the no-blocking lab, applied to something more interesting than a single blink.
Animations Built From Other Animations
Because the animations are data, ordinary list operations work on them:
1 | |
That single line trims the last frame off BLINK and glues another BLINK onto it. No new drawing
code, no new player logic. Try building that from a hand-written animation and you will appreciate
the difference.
Data Composes; Code Does Not
You can slice a table, reverse it, glue two together, or sort it. None of those things make sense on a block of hand-written drawing calls — and that is the whole argument for keeping motion in a list.
Sample Program Code
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 full program is 27-keyframes.py in the kit.
Here's the first animation, at rest:

A Real Bug the Erase Box Caused — and a Font That Would Not Fit
There is a comment in this lab worth reading in full, because it documents a bug that a rendered screenshot caught and a person did not:
1 2 3 4 5 6 7 8 9 | |
That is the failure mode of partial redraw in one paragraph. The erase box has to know about everything it overlaps, and nothing warns you when it does not.
On the smartwatch kit, fixing this was a one-line move: slide LABEL_Y up and keep calling
face.label(). Here, moving the number was not enough, because face.label() draws in the 16×32
font on this kit. "Blink x2" at that size is 128 pixels wide, and the widest safe row 24 rows down
a 360-pixel circle is only about 114 pixels — no LABEL_Y would have made it fit. So this caption
uses face.centered_text() instead, the same 8×16 font every raw text() call in this book already
uses, which needs only 64 pixels and clears with room to spare. Bigger screen, smaller relative
text, and this time the fix cost a whole font, not just a row number.
Things to Try
- Make the blink slower by changing only numbers — turn the 70 in the middle of
BLINKinto - A snappy reflex becomes a heavy, tired droop, and you never touched the player. (That 70 is milliseconds, so it is the same number on both kits.)
- Build a
TRIPLE_BLINKin one line, the same wayDOUBLE_BLINKwas built. - Add a fourth number to every frame — a mouth width — so the mouth animates too. You change
draw_frame()once and every animation gains a moving mouth. You will also need a second erase box, and working out where it goes is most of the work. - Play an animation backward by reversing the list. Does
DOZE_OFFreversed read as waking up? Some motions are reversible and some are not, which is a real animation-design question. - Put
face.clear()at the top ofdraw_frame()instead offace.erase(). Same picture, and the animation turns into a flickering slideshow — and it costs more here than it did on the smartwatch kit, because clearing this screen means sending 259,200 bytes instead of 115,200. - Set
face.DEBUG_ERASE = config.REDbefore playingSURPRISE. The erase box turns into a visible red rectangle with the pose drawn on top of it, so you can watch exactly how much glass each frame repaints — and confirm for yourself that the caption atLABEL_Ysits safely outside it.
References
- The Emotion Table — the same data-over-code move, applied to appearance
- Don't Block the Loop — why the player checks the clock instead of sleeping
- Only Redraw What Changed — the erase-box discipline this lab depends on
- Keyframes on the 1.2" kit — the same tables at 240×240, where the caption never had to change font