Lab 10: Happy Face
The first complete expression: two eyes, two eyebrows, and a curved smile, built from three small functions. Every later emotion in this kit reuses this exact pattern with different numbers — which is the whole point, and Lab 24 turns that observation into a table.
Sample Program Code
Fill the screen black, draw two filled eyes with punched pupils, two thick eyebrow lines, and a smile arc:
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 | # Lab 10: Happy Face
# The first complete expression, built from three small functions --
# draw_eyes(), draw_eyebrows(), and a curved mouth. Later labs reuse this
# exact same pattern with different numbers to draw every other emotion.
#
# The numbers are bigger than the OLED kit's, and not by a simple factor.
# The OLED was 128 wide and 64 tall -- twice as wide as it was tall, so
# the face had to be squashed. This screen is square (and round), so the
# face finally gets to be face-shaped.
import config
import shapes
display = config.init_display()
WHITE = config.WHITE
BLACK = config.BLACK
NO_FILL = config.NO_FILL
FILL = config.FILL
BOTTOM_HALF = 12
HALF_WIDTH = config.WIDTH // 2
EYE_SPACING = 48
LEFT_EYE_X = HALF_WIDTH - EYE_SPACING
RIGHT_EYE_X = HALF_WIDTH + EYE_SPACING
EYE_Y = 102
PUPIL_RADIUS = 8
EYEBROW_HALF_WIDTH = 24
EYEBROW_Y = EYE_Y - 40
MOUTH_Y = 164
# One pixel is invisible on a screen this size, so lines and arcs get
# drawn several times, a pixel apart, to thicken them.
STROKE = 4
def draw_eye(x, rx, ry):
shapes.ellipse(display, x, EYE_Y, rx, ry, WHITE, FILL)
shapes.ellipse(display, x, EYE_Y, PUPIL_RADIUS, PUPIL_RADIUS, BLACK, FILL)
def draw_eyes(rx, ry):
draw_eye(LEFT_EYE_X, rx, ry)
draw_eye(RIGHT_EYE_X, rx, ry)
def draw_eyebrow(x, side, lift):
# side: 1 for the left eyebrow (nose to the right), -1 for the right
y = EYEBROW_Y - lift
for offset in range(STROKE):
display.line(x - EYEBROW_HALF_WIDTH * side, y + offset,
x + EYEBROW_HALF_WIDTH * side, y + offset, WHITE)
def draw_eyebrows(lift):
draw_eyebrow(LEFT_EYE_X, 1, lift)
draw_eyebrow(RIGHT_EYE_X, -1, lift)
def draw_mouth_curve(radius_x, radius_y, mask):
for offset in range(STROKE):
shapes.ellipse(display, HALF_WIDTH, MOUTH_Y - offset,
radius_x, radius_y, WHITE, NO_FILL, mask)
def draw_happy_face():
display.fill(BLACK)
draw_eyes(24, 24)
draw_eyebrows(lift=5)
draw_mouth_curve(50, 24, BOTTOM_HALF)
draw_happy_face()
# Things to try:
#
# 1. Count how long the face takes to appear. Most of that is
# display.fill(BLACK) -- 115,200 bytes of black. Comment it out and
# run twice in a row to see how much faster the rest is on its own.
#
# 2. Move EYE_Y from 102 to 60. The eyes climb toward the rim and start
# losing their outer edges to the bezel, because the screen gets
# narrower the further you get from the middle.
|
Here's what that program draws:

Numbers Bigger Than the OLED Kit's — But Not by a Simple Factor
The OLED kit was 128 pixels wide and only 64 tall — twice as wide as it was tall — so every face on it had to be squashed vertically to fit. This screen is 240 by 240: square, and round. The face finally gets to be face-shaped, which is why EYE_Y, EYEBROW_Y, and MOUTH_Y here aren't simply the OLED kit's numbers scaled up by some constant — the whole layout was rethought for a screen with a different shape, not just a bigger one.
Notice the strokes: every eyebrow line and mouth arc is drawn several times, a pixel apart, via a loop over STROKE. A single-pixel line is nearly invisible at this size and viewing distance; thickening it into a real stroke is what makes it read as a face feature instead of a scratch.