Skip to content

Lab 16: Sleeping Face

Closed eyes, drooping eyebrows, a quiet round mouth, and three drifting Zzz characters that bob up and down as if floating away.

Sample Program Code

Everything that never moves is drawn once; only the Zzz group's box is erased and rebuilt on every frame:

  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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# Lab 16: Sleeping Face
# Closed eyes, drooping eyebrows, a quiet mouth, and three drifting Z
# characters. The bob value shifts the whole Zzz group up and down so it
# looks like it is floating away instead of glued in place.
#
# On the OLED the Zzz sat in the top-right CORNER. This screen has no
# corners, so they drift up and to the right from BESIDE THE MOUTH,
# rising into the empty quarter below the right eye.
#
# That placement is the round screen making a decision for you. The
# obvious spot -- up beside the right eye, where the OLED put them -- is
# already occupied: on a 240x240 circle the eyebrow reaches out to x=192
# at the same height, and the first Z lands on top of it. There is no
# free corner to retreat to, so the Zzz go somewhere the face is not.
#
# Only the Zzz move, so only the Zzz box is erased between frames.

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
FONT = config.SMALL_FONT

TOP_HALF = 3  # 1 (top right) + 2 (top left)

HALF_WIDTH = config.WIDTH // 2
EYE_SPACING = 48
LEFT_EYE_X = HALF_WIDTH - EYE_SPACING
RIGHT_EYE_X = HALF_WIDTH + EYE_SPACING
EYE_Y = 106
EYE_RADIUS = 28
SLEEP_RADIUS_Y = 14
SLEEP_Y = EYE_Y + 7
STROKE = 4

EYEBROW_HALF_WIDTH = 24
EYEBROW_Y = EYE_Y - 34
EYEBROW_DROOP = 6  # outer corners sag, the way real brows relax before sleep

MOUTH_Y = 172
MOUTH_RADIUS = 10

ZZZ_X = 150
ZZZ_Y = 174
ZZZ_STEP_X = 12
ZZZ_STEP_Y = 20
BOB_RANGE = 5

# The rectangle the three Z's live in, with room for the bob at both
# ends. Get this box wrong and the old Z's stay on screen forever.
ZZZ_BOX_X = ZZZ_X - 4
ZZZ_BOX_Y = ZZZ_Y - ZZZ_STEP_Y * 2 - BOB_RANGE - 2
ZZZ_BOX_W = ZZZ_STEP_X * 2 + 16
ZZZ_BOX_H = ZZZ_STEP_Y * 2 + BOB_RANGE * 2 + 20


def draw_closed_eye(x):
    for offset in range(STROKE):
        shapes.ellipse(display, x, SLEEP_Y + offset, EYE_RADIUS,
                       SLEEP_RADIUS_Y, WHITE, NO_FILL, TOP_HALF)


def draw_eyebrows():
    for offset in range(STROKE):
        display.line(LEFT_EYE_X + EYEBROW_HALF_WIDTH, EYEBROW_Y + offset,
                     LEFT_EYE_X - EYEBROW_HALF_WIDTH,
                     EYEBROW_Y + EYEBROW_DROOP + offset, WHITE)
        display.line(RIGHT_EYE_X - EYEBROW_HALF_WIDTH, EYEBROW_Y + offset,
                     RIGHT_EYE_X + EYEBROW_HALF_WIDTH,
                     EYEBROW_Y + EYEBROW_DROOP + offset, WHITE)


def draw_mouth():
    for offset in range(STROKE):
        shapes.circle(display, HALF_WIDTH, MOUTH_Y, MOUTH_RADIUS - offset,
                      WHITE, NO_FILL)


def draw_zzz(bob):
    display.fill_rect(ZZZ_BOX_X, ZZZ_BOX_Y, ZZZ_BOX_W, ZZZ_BOX_H, BLACK)
    display.text(FONT, 'Z', ZZZ_X, ZZZ_Y + bob, WHITE, BLACK)
    display.text(FONT, 'Z', ZZZ_X + ZZZ_STEP_X,
                 ZZZ_Y - ZZZ_STEP_Y + bob, WHITE, BLACK)
    display.text(FONT, 'z', ZZZ_X + ZZZ_STEP_X * 2,
                 ZZZ_Y - ZZZ_STEP_Y * 2 + bob, WHITE, BLACK)


def draw_sleeping_face():
    """The parts that never move, drawn once."""
    display.fill(BLACK)
    draw_closed_eye(LEFT_EYE_X)
    draw_closed_eye(RIGHT_EYE_X)
    draw_eyebrows()
    draw_mouth()


draw_sleeping_face()

while True:
    for bob in range(-BOB_RANGE, BOB_RANGE):
        draw_zzz(bob)
        sleep(0.15)
    for bob in range(BOB_RANGE, -BOB_RANGE, -1):
        draw_zzz(bob)
        sleep(0.15)

# Things to try:
#
# 1. Comment out the fill_rect() in draw_zzz(). The Z's smear into a
#    solid block within a few seconds -- the exact bug lab 25 plants on
#    purpose, and the one you will meet most often on this display.
#
# 2. Swap FONT for config.BIG_FONT and adjust the box. Bigger Z's read
#    better from a distance, but they need more room inside the circle
#    than you expect.

Here's what that program draws:

Simulated output of 16-sleepy.py

Where Do You Put a Sleeping Robot's Zzz's?

On the OLED kit, the Zzz sat in the top-right corner — an option this screen doesn't have. The obvious next choice, tucked up beside the right eye where the OLED put them, turns out to already be occupied: on this layout the right eyebrow reaches out to x=192 at almost exactly that height, so the first Z would land right on top of it. There's no free corner to retreat to on a circle, so the Zzz drift up out of the empty space below and to the right of the mouth instead.

That's a real design decision forced by the shape of the screen, not an arbitrary choice — and it's worth remembering the next time you're placing a detail near a busy part of a round layout.