Eye Saccade
Here is a change that costs almost nothing and transforms how alive your robot looks: stop sweeping the eyes, and make them jump.
The Eye Scanner lesson glides the pupils smoothly from side to side. Real eyes almost never do that. Yours are jumping across this line of text right now, three or four times a second, in quick flicks with brief stops in between — and that pattern is what your brain reads as something is looking.
Watch how I really look around
Every pixel tells a story, and this one is about timing rather than drawing. Not a single line of my drawing code changes in this lesson.
What Real Eyes Actually Do
Eye movement has two modes, and they have names worth knowing:
| Movement | What it is | How long it lasts |
|---|---|---|
| Saccade | A fast, ballistic jump to a new target | 30–80 ms |
| Fixation | Holding almost perfectly still while you actually look | 200–400 ms |
| Smooth pursuit | Gliding steadily — what the eye scanner does | only while tracking something moving |
That last row is the punchline. Smooth motion is a tracking behavior. An eye only glides when it is following something that moves, so a robot whose eyes glide constantly looks like a machine sweeping a sensor, no matter how fast you make it.
Motion, Not Drawing
draw_eyes() here is the eye scanner's, unchanged — it still erases the two eye boxes and
rebuilds them. What changes is when it gets called.
The program keeps a short list of places worth looking, picks one at random, jumps there a few pixels at a time, and then holds still:
1 2 3 4 5 | |
TARGETS is the list of gaze positions, spaced SACCADE_STEP apart. Real eyes do not drift to
arbitrary coordinates — they jump between things, so a short list of destinations is closer to
the truth than a random number out of a range.
The jump steps toward the target and redraws each time, timing itself as it goes:
1 2 3 4 5 6 7 8 9 | |
ticks_us() reads a microsecond clock and ticks_diff() subtracts two readings safely. Compare
the printed number against the 30–80 ms a real saccade takes — that is how you know whether your
robot's eyes move at a speed people recognize.
Irregular beats fast
Make every pause exactly 300 ms and the face turns into a metronome. The randomness in the fixation length is doing more work here than any drawing trick.
Rare Work Is Allowed to Be Expensive
Erasing two 92 × 56 boxes and rebuilding both eyes is a lot of pixels. On this display there is no frame buffer, so every one of them travels down a wire — which is exactly why the eye scanner lesson worked so hard to shrink that box.
This lesson makes that whole worry mostly disappear, without changing the drawing at all:
| Behavior | Frames to cross the face | Then what? |
|---|---|---|
| Eye scanner sweep | 60 | turn around and sweep back, forever |
| Eye saccade | at most 12 | draw nothing at all for 200–400 ms |
A dozen expensive redraws followed by a rest beat sixty of them followed by sixty more. Work that happens rarely is allowed to be expensive — a rule worth remembering every time you are tempted to optimize something that hardly ever runs.
And notice what you get for it: the cheaper version is also the one that looks alive. Being convincing and being cheap are usually opposites. Here they are the same choice, because both come from one fact: eyes are still most of the time.
Sample Output
A saccade caught mid-fixation, with the gaze held steady:

Both pupils always point the same direction. That is what makes a face read as looking at something, instead of in two directions at once.
Things to Try
- Run this and the eye scanner back to back and just watch, without looking at any numbers. One looks like a machine sweeping a sensor; the other looks like something making up its mind.
- Set both fixation constants to 300 so every pause is the same length, and watch the face turn into a metronome. Then put the randomness back.
- Set
ERASE_COLOR = config.RED. Now you can see exactly how much repainting one saccade costs — and, more usefully, how much of the time the screen is completely still. That stillness is the saving. - Set
SACCADE_STEPto 1. The jump becomes a slow glide, the printed microseconds jump by about five times, and the face stops looking alive. Slower, smoother and more expensive all at once — a rare chance to make something worse in three ways with one number. - Make the gaze mean something. Feed the target choice from a distance sensor instead of a random number, so the robot looks toward whichever side has more room. Now the face is not performing thought — it is reporting it, and anyone watching can read the robot's next move off its eyes before the wheels turn.
That is a face with a mind behind it
You just made a robot look like it is thinking by changing when you draw, not what you draw. Great expression!
References
- Eye Scanner — the smooth sweep this lesson replaces
- Only Redraw What Changed — measuring what a redraw actually costs
- How Fast Is a Face? — where timing your own drawing is introduced
- A Face With a Memory — the natural home for gaze that reacts to events