Skip to content

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:

Simulated output of 10-happy-face.py

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.