Lab 12: Winking with a Smile
A closed eye is an arc — the top half of an ellipse, drawn with quadrant mask TOP_HALF (3) instead of a full circle. Only the right eye closes here; the left stays open, which is what makes it read as a wink instead of a blink.
Sample Program Code
The left eye and the smile are drawn once and never touched again; only the right eye's box gets erased and rebuilt:
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101 | # Lab 12: Winking with a Smile
# A closed eye is an arc -- the top half of an ellipse, drawn with
# quadrant mask TOP_HALF (3) instead of a full circle. Only one eye
# closes; the other stays open, which is what makes it read as a wink
# instead of a blink.
#
# Only the right eye changes between the two frames, so only the right
# eye's box gets erased and rebuilt. The left eye and the smile are drawn
# once and then left alone for as long as the program runs.
import config
import shapes
from utime import sleep
display = config.init_display()
WHITE = config.WHITE
BLACK = config.BLACK
NO_FILL = config.NO_FILL
FILL = config.FILL
TOP_HALF = 3 # 1 (top right) + 2 (top left)
BOTTOM_HALF = 12 # 4 (bottom left) + 8 (bottom right)
HALF_WIDTH = config.WIDTH // 2
EYE_SPACING = 48
LEFT_EYE_X = HALF_WIDTH - EYE_SPACING
RIGHT_EYE_X = HALF_WIDTH + EYE_SPACING
EYE_Y = 100
EYE_RADIUS = 28
PUPIL_RADIUS = 10
WINK_RADIUS_Y = 14
WINK_Y = EYE_Y + 7
STROKE = 4
MOUTH_Y = 168
MOUTH_RADIUS_X = 48
MOUTH_RADIUS_Y = 24
# Big enough to cover an open eye, which is the largest thing that ever
# appears in this box.
EYE_BOX = EYE_RADIUS + 4
def draw_open_eye(x):
shapes.ellipse(display, x, EYE_Y, EYE_RADIUS, EYE_RADIUS, WHITE, FILL)
shapes.ellipse(display, x, EYE_Y, PUPIL_RADIUS, PUPIL_RADIUS, BLACK, FILL)
def draw_winking_eye(x):
for offset in range(STROKE):
shapes.ellipse(display, x, WINK_Y + offset, EYE_RADIUS,
WINK_RADIUS_Y, WHITE, NO_FILL, TOP_HALF)
def draw_smile():
for offset in range(STROKE):
shapes.ellipse(display, HALF_WIDTH, MOUTH_Y - offset,
MOUTH_RADIUS_X, MOUTH_RADIUS_Y,
WHITE, NO_FILL, BOTTOM_HALF)
def erase_eye(x):
display.fill_rect(x - EYE_BOX, EYE_Y - EYE_BOX,
EYE_BOX * 2, EYE_BOX * 2, BLACK)
def draw_resting_face():
"""Everything, once. After this only the right eye is ever touched."""
display.fill(BLACK)
draw_open_eye(LEFT_EYE_X)
draw_open_eye(RIGHT_EYE_X)
draw_smile()
def set_right_eye(winking):
erase_eye(RIGHT_EYE_X)
if winking:
draw_winking_eye(RIGHT_EYE_X)
else:
draw_open_eye(RIGHT_EYE_X)
draw_resting_face()
while True:
set_right_eye(True) # eye snaps shut
sleep(0.35) # hold the wink just long enough to be seen
set_right_eye(False) # eye opens again
sleep(2.5) # normal face until the next wink
# Things to try:
#
# 1. The wink is now two small rectangles of work instead of two whole
# screens. Add a print() of ticks_ms() around set_right_eye() and see
# how little time a frame actually takes when you only redraw the part
# that moved.
#
# 2. Close the LEFT eye instead. On a face, left and right are not
# interchangeable -- most people read a left-eye wink as slightly
# different in tone. Try it on a few people.
|
Here's what that program draws while winking:

One Eye, Not the Whole Face
Because only the right eye ever changes, set_right_eye() erases and redraws just that one region — a small rectangle, not the screen. Time it on real hardware and a wink becomes two small boxes of work instead of two whole screens, which is exactly what keeps this animation from stuttering the way a full-screen redraw would.
Left and right aren't interchangeable on a face. Try closing the left eye instead of the right and show it to a few people — most read the two as having a slightly different tone, even though the geometry is a mirror image. Faces carry more information than the code that draws them admits to.