Skip to content

Lab 22: Face Parameters — Live Tuning

Every expression up to this point has used fixed numbers. This lab makes exactly one number live: button A widens the smile, button B narrows it into a frown, and the face redraws instantly so you can watch a single parameter bend the whole face's mood in real time.

Sample Program Code

The eyes are drawn once and never touched again; only the mouth's box and the readout strip change on a press:

  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
121
122
123
124
125
126
127
128
# Lab 22: Face Parameters -- Live Tuning
# Every expression so far has used fixed numbers. This lab makes one
# number live: button A widens the smile, button B narrows it into a
# frown, and the face redraws instantly so you can watch a single
# parameter bend the whole face's mood in real time.
#
# "Instantly" is doing real work here. The eyes never change, so they are
# drawn once and never touched again -- only the mouth's box and the
# readout strip get erased and rebuilt on a press. Redraw the whole
# screen and the response stops feeling instant.

import config
import shapes
from utime import sleep

display = config.init_display()
button_a, button_b = config.init_buttons()

WHITE = config.WHITE
BLACK = config.BLACK
NO_FILL = config.NO_FILL
FILL = config.FILL
FONT = config.SMALL_FONT
BOTTOM_HALF = 12
TOP_HALF = 3

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
EYE_RADIUS = 24
PUPIL_RADIUS = 8
MOUTH_Y = 164
MOUTH_WIDTH = 54
STROKE = 4
LABEL_Y = 30

MOUTH_CURVE_MIN = -24
MOUTH_CURVE_MAX = 32
MOUTH_CURVE_STEP = 4

# The box the mouth can never escape: widest radius, deepest curve in
# either direction, plus a margin. Work this out from the numbers above
# rather than guessing, or you will be chasing leftovers all afternoon.
MOUTH_BOX_X = HALF_WIDTH - MOUTH_WIDTH - 4
MOUTH_BOX_Y = MOUTH_Y - MOUTH_CURVE_MAX - STROKE - 4
MOUTH_BOX_W = (MOUTH_WIDTH + 4) * 2
MOUTH_BOX_H = (MOUTH_CURVE_MAX + STROKE + 4) * 2


def draw_eyes():
    for x in (LEFT_EYE_X, RIGHT_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_mouth(mouth_curve):
    display.fill_rect(MOUTH_BOX_X, MOUTH_BOX_Y,
                      MOUTH_BOX_W, MOUTH_BOX_H, BLACK)

    # a positive curve smiles (BOTTOM_HALF), a negative curve frowns (TOP_HALF)
    if mouth_curve >= 0:
        mask = BOTTOM_HALF
        radius_y = mouth_curve + 4
    else:
        mask = TOP_HALF
        radius_y = -mouth_curve + 4

    for offset in range(STROKE):
        shapes.ellipse(display, HALF_WIDTH, MOUTH_Y - offset,
                       MOUTH_WIDTH, radius_y, WHITE, NO_FILL, mask)


def draw_readout(mouth_curve):
    text = "curve: " + str(mouth_curve)
    display.fill_rect(0, LABEL_Y, config.WIDTH, FONT.HEIGHT, BLACK)
    x = HALF_WIDTH - (len(text) * FONT.WIDTH) // 2
    display.text(FONT, text, x, LABEL_Y, WHITE, BLACK)


def update(mouth_curve):
    draw_mouth(mouth_curve)
    draw_readout(mouth_curve)


def pressed(button):
    if button.value() == 1:
        return False
    sleep(0.02)
    return button.value() == 0


def wait_for_release(button):
    while button.value() == 0:
        sleep(0.01)


display.fill(BLACK)
draw_eyes()

mouth_curve = 8
update(mouth_curve)

while True:
    if pressed(button_a):
        mouth_curve = min(MOUTH_CURVE_MAX, mouth_curve + MOUTH_CURVE_STEP)
        update(mouth_curve)
        wait_for_release(button_a)

    if pressed(button_b):
        mouth_curve = max(MOUTH_CURVE_MIN, mouth_curve - MOUTH_CURVE_STEP)
        update(mouth_curve)
        wait_for_release(button_b)

    sleep(0.01)

# Things to try:
#
# 1. Find the exact curve value where the face stops reading as happy and
#    starts reading as neutral. Then find where neutral becomes sad. Those
#    two numbers are a real finding about how people read faces, and you
#    measured them.
#
# 2. Shrink MOUTH_BOX_H by twenty and run to the extremes. The old mouth's
#    ends survive the erase and pile up. The box has to be big enough for
#    the LARGEST thing that can appear in it, not the current one.

Here's the starting curve:

Simulated output of 22-face-parameters.py

Instant Only Because Almost Nothing Redraws

"Instant" is doing real work in that sentence. The eyes never change here, so they're drawn once and left alone for the program's whole lifetime — only the mouth's bounding box and the small readout strip get erased and rebuilt on each press. Redraw the whole screen instead and the response stops feeling instant, even though the change itself (one number, mouth_curve) is exactly as small either way.

MOUTH_BOX_X/Y/W/H are computed from the widest possible curve the mouth could ever reach, not the current one — work backward from MOUTH_CURVE_MIN/MAX to see why, and shrink MOUTH_BOX_H on purpose to watch old pixels survive at the extremes when the box is too small.