Drawing Shapes, Text, and Animation
Summary
Students draw lines, rectangles, circles, and polygons and render text with built-in and custom fonts. They learn animation and partial screen updates. After this chapter they can draw a complete clock face on an OLED.
Concepts Covered
This chapter covers the following 19 concepts from the learning graph:
| Concept | Concept Impact Score |
|---|---|
| Text Rendering | 20 |
| Display Coordinates | 3 |
| Line Drawing | 14 |
| Font Loading | 5 |
| Scaled Digits | 5 |
| Partial Screen Update | 2 |
| Rectangle Drawing | 3 |
| Circle Drawing | 3 |
| Polygon Drawing | 2 |
| Custom Fonts | 4 |
| Animation Technique | 3 |
| Triangle Drawing | 1 |
| Font to Py Converter | 2 |
| Arc Drawing | 1 |
| Double Buffering | 1 |
| Scroll Marquee | 1 |
| Bounding Box | 2 |
| Multiple Font Support | 1 |
| Clipping Region | 1 |
Prerequisites
This chapter builds on concepts from:
- Chapter 3: MicroPython Basics: Variables and Data Types
- Chapter 4: Control Flow, Functions, and Modules
- Chapter 6: Getting the Time in MicroPython
- Chapter 10: Coordinates, Trigonometry, and Clock Geometry
- Chapter 11: OLED Displays and Framebuffers
From Pixels to Pictures
Setting one pixel at a time is like painting with a single grain of sand. In this chapter you'll get real brushes: lines, circles, big digits, and smooth motion. By the end you'll draw a complete analog clock face. Let's make time tick!
Drawing With Real Tools
You now know how the framebuffer works and how to set a single pixel. Drawing whole pictures pixel by pixel would be slow and tedious, so MicroPython's framebuf module provides ready-made shapes. This chapter covers the shape tools, text and fonts, and the techniques that make screens update smoothly and efficiently. All the examples use the oled object from Chapter 11.
Display Coordinates
Every drawing call needs positions, and each call interprets its numbers a little differently. The coordinate system is the one from Chapter 10: the origin (0, 0) is at the top-left, x grows to the right, and y grows downward. What changes from call to call is what the numbers mean.
| Call | Arguments | What (x, y) means |
|---|---|---|
pixel(x, y, c) |
position, color | The pixel itself |
line(x1, y1, x2, y2, c) |
two end points | Start and end of the line |
rect(x, y, w, h, c) |
corner, size | Top-left corner, then width and height |
ellipse(x, y, xr, yr, c) |
center, radii | Center of the shape |
text(s, x, y, c) |
string, corner | Top-left of the first character |
poly(x, y, coords, c) |
offset, points | Offset added to every point |
Two of these trip people up. A rectangle takes a width and height, not a second corner, and an ellipse takes its center, not its corner. On the 128 by 64 OLED, valid x values run from 0 to 127 and y values from 0 to 63.
Size, Not Corner
rect(10, 10, 50, 30, 1) draws a box 50 wide and 30 tall, ending at (59, 39), not at (50, 30). Mixing up "width and height" with "second corner" is the most common shape bug, and the giveaway is a rectangle that is too big or too small by exactly the starting offset.
Text Rendering
Text rendering is drawing characters on the screen. The built-in text() method uses a fixed 8 by 8 pixel font, so every character occupies an 8-pixel cell:
1 | |
Because each character is 8 pixels wide, you can predict text width exactly: width = 8 * len(string). On the 128-pixel-wide OLED, 16 characters fit on a line, and with 8-pixel rows, 8 lines fit in 64 pixels. To center a string horizontally:
1 2 3 | |
The built-in font supports basic ASCII letters, digits, and symbols, but it cannot be resized and has no accented characters. The next sections show how to overcome that.
Scaled Digits
A clock should be readable across a room, and 8-pixel digits are tiny. Scaled digits enlarge the built-in font by drawing each font pixel as a block of several screen pixels. The idea is to render the character into a small scratch buffer, then copy each lit pixel as a scale by scale square.
1 2 3 4 5 6 7 8 9 10 11 | |
With scale = 3, each digit is 24 by 24 pixels, so 12:34 needs \( 5 \times 24 = 120 \) pixels and fits across the 128-pixel screen. The MONO_HLSB format is a horizontal layout that suits a scratch buffer, and it does not need to match the display's own layout because you copy pixel by pixel.
Diagram: Digit Scaling Explorer
Digit Scaling Explorer
Type: MicroSim
sim-id: digit-scaling-explorer
Library: p5.js
Status: Specified
Learning objective: Students will apply scaling to make digits fit the screen and analyze the width tradeoff (Bloom: Applying, Analyzing).
Visual elements: A 128 by 64 OLED preview showing a time string, with the 8 by 8 source glyph shown enlarged beside it.
Controls: Text input for the time string, a scale slider (1 to 6), and a readout "Width = 8 × scale × characters" that turns red when it exceeds 128. A checkbox shows the grid of scaled blocks.
Responsive design: The preview scales to the container width.
Implementation: p5.js with a stored 8 by 8 bitmap font for digits and colon.
Font Loading
A font is a set of bitmap pictures, one for each character. To use a font other than the built-in one, you load it from a Python file and use a helper called a writer to draw text with it. The commonly used tool is Peter Hinch's writer.py, together with font modules generated by his converter.
1 2 3 4 5 6 7 | |
A loaded font is stored as Python data, so it takes up flash space and RAM when imported. Big fonts with many characters can use tens of kilobytes. That matters on a Pico with 264 KB of RAM, so import only the fonts you need.
Custom Fonts
A custom font is one you make yourself in exactly the size and style you want, from any TrueType (.ttf) or OpenType (.otf) font file. This is how you get large, clean clock digits that the 8 by 8 font cannot give you.
Font to Py Converter
The font-to-py converter is a program that runs on your computer, not on the Pico, and turns a font file into a Python module at a chosen pixel height. The basic pattern is:
1 | |
The number 32 is the font height in pixels. The -c option lists the characters to include. Here we include only the digits and the colon, so the output file stays small. Copy the resulting freesans32.py to the Pico and import it, as shown above. The Font to Py lesson walks through the whole process.
Multiple Font Support
Some layouts need more than one font, such as big digits for the time and a small font for the date. Create one Writer for each font module and use whichever suits the text:
1 2 | |
Each font takes memory, so limit yourself to what the screen actually needs. Two or three fonts is a practical ceiling on the Pico.
Diagram: Font Pipeline
Font Pipeline
Type: workflow diagram
sim-id: font-pipeline
Library: Mermaid
Status: Specified
Learning objective: Students will understand the steps from a font file to text on the OLED (Bloom: Understanding).
Nodes: TrueType font file on your computer, font_to_py.py (with height and character list), generated .py module, copy to Pico, import on the Pico, Writer, text on screen.
Interactions: Every node has a Mermaid click directive that opens an infobox with the command or a description. A second view estimates the module's memory size from height and number of characters.
Responsive design: The diagram scales to container width.
Implementation: Mermaid flowchart with click callbacks.
Line Drawing
The line(x1, y1, x2, y2, c) method draws a straight line of one-pixel thickness between two points. It also has two faster special cases for horizontal and vertical lines: hline(x, y, w, c) and vline(x, y, h, c).
1 2 | |
Lines are the natural tool for clock hands. Using the hand-tip calculation from Chapter 10, a hand is a line from the center to the tip:
1 2 3 | |
Rectangle Drawing
The rect(x, y, w, h, c) method draws a rectangle outline. Add a fifth argument True (or use fill_rect()) to fill it. Rectangles make borders, progress bars, and thick lines:
1 2 | |
Circle Drawing
The framebuf module has no circle(), but it has ellipse(x, y, xr, yr, c), and an ellipse with equal radii is a circle. The position is the center:
1 2 | |
The ellipse method was added in MicroPython 1.19, so on older firmware you must draw circles by plotting points with the polar formula from Chapter 10.
Polygon Drawing
A polygon is a closed shape with straight sides. The poly(x, y, coords, c) method takes an array of x, y pairs. The coordinates are relative to the offset (x, y), so you can draw the same shape at different places. The array must be a 16-bit array from the array module:
1 2 3 | |
This is a good shape for an hour hand, because it is wide near the center and pointed at the end. Rotating its points with the rotation formula from Chapter 10 aims it at the right hour.
Triangle Drawing
A triangle is the simplest polygon: three points. Triangles make arrows, play buttons, and alarm icons.
1 2 | |
Arc Drawing
An arc is part of a circle. There is no built-in arc call, so you draw one by plotting points along the circle using the polar formula. This is perfect for a progress ring that grows as the seconds pass:
1 2 3 4 5 6 | |
A smaller step gives a smoother line but takes longer to draw.
Animation Technique
Animation is the illusion of motion made by showing a series of slightly different pictures quickly. Each picture is called a frame. Every animation loop follows the same recipe: clear, draw the objects at their current positions, show, then change the positions for the next frame.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
The speed of the animation is set by how far the object moves each frame and how long the frame takes. For smooth motion aim for 20 or more frames per second, meaning the drawing plus show() should take under about 50 ms. Chapter 11 showed how to measure that.
Double Buffering
Double buffering uses two buffers: one that is visible and one you draw into. The framebuffer you already use is a simple form, because you build the picture in RAM before revealing it. A more useful version keeps a second buffer that holds the parts that never change, such as the tick marks of a clock face. Each frame, you copy the face into the main buffer with blit() and add only the moving hands.
1 2 3 4 5 6 7 8 9 | |
This saves the time of redrawing 60 ticks every second.
Draw the Static Stuff Once
Anything that never changes, like the clock face outline and tick marks, only needs to be drawn one time into a second buffer. Copy it in each frame with blit() and spend your drawing time only on the hands.
Scroll Marquee
A scroll marquee slides text across the screen, like a news ticker. The simplest reliable method redraws the text at a changing x position each frame, starting off the right edge and moving left until it leaves:
1 2 3 4 5 | |
Partial Screen Update
A partial screen update redraws only the region that changed instead of the entire screen. When only the seconds digits change each second, the hours, minutes, and date need not be touched. Clear just that rectangle, then draw the new value:
1 2 3 | |
Be clear about what this saves. With the OLED's framebuffer, show() still sends the entire 1,024 bytes over the bus, so the bus time does not shrink, but you save drawing time. On larger color displays whose drivers can send just a window of pixels, a partial update also cuts the bus time dramatically, which becomes essential in later chapters.
Bounding Box
A bounding box is the smallest rectangle that completely encloses an object. It is written as (x, y, w, h). For 8 by 8 text the bounding box is easy to compute:
1 2 | |
The bounding box gives you two useful powers. You can erase an old object exactly, with fill_rect(*box, 0), and you can test whether two objects overlap by comparing their boxes. Erasing the old bounding box before drawing the new one is the core of a partial update.
Every Object Lives in a Box
Whatever you draw, picture the smallest rectangle around it. That one idea gives you a way to erase, move, and protect any item on the screen without touching its neighbors.
Clipping Region
A clipping region is a rectangle outside of which drawing is ignored. It keeps a scrolling message from spilling onto other parts of the screen. framebuf clips only at the edges of the whole buffer, so to clip to a smaller area, draw into a small buffer the size of the region and then blit() it into place. Anything that falls outside the small buffer is discarded automatically.
Diagram: Bounding Box and Partial Update
Bounding Box and Partial Update
Type: MicroSim
sim-id: bounding-box-partial-update
Library: p5.js
Status: Specified
Learning objective: Students will analyze how erasing a bounding box lets part of the screen update without disturbing the rest (Bloom: Analyzing).
Visual elements: A 128 by 64 OLED preview with a clock display: hours, minutes, seconds, and a date line. Each item's bounding box can be shown as a colored outline.
Controls: Toggle "Show bounding boxes"; button "Advance one second." A "Update mode" selector switches between Full redraw (all pixels flash red as they are redrawn) and Partial update (only the seconds box flashes). A counter shows pixels redrawn per frame in each mode.
Responsive design: The preview and counter stack below 600 px.
Implementation: p5.js with per-item rectangles and a redraw counter.
Putting It Together: An Analog OLED Clock
This program uses lines, an ellipse, ticks, and the angle math from Chapter 10 to draw a complete analog clock face on the 128 by 64 OLED. The face has radius 31 so it fits the 64-pixel height.
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 | |
Key Takeaways
- Every drawing call interprets (x, y) differently;
recttakes width and height, andellipsetakes a center. - The built-in font is 8 × 8, so text width is
8 * len(text); scale it or load a custom font for large digits. font_to_py.pyconverts a TrueType file to a Python font module; include only the characters you need.line,rect,ellipse, andpolycover almost every clock shape; arcs are made by plotting points.- Animation is clear, draw, show, and move, repeated; a second buffer holds the static parts.
- Bounding boxes let you erase and update just part of the screen.
A Face Worth Drawing
You can now draw lines, circles, and polygons, scale digits, load fonts, and animate hands on an OLED. You built a full analog clock face from math and pixels. Every second counts!
Practice Questions
- Write the call that draws a filled rectangle 40 wide and 10 tall with its top-left corner at (20, 30).
- How wide in pixels is the string
"09:45"at scale 1, and at scale 3? Does it fit on a 128-pixel screen? - Explain why
ellipse(64, 32, 31, 31, 1)draws a circle centered on the screen. - What is the benefit of drawing the clock face into a second buffer and using
blit()? - Describe how you would update only the seconds on a display without redrawing the hours and minutes.