Skip to content

Lopaka Basics 07: Animations ​

An SSD1306 OLED module showing a white silhouette of a galloping horse with a jockey

This horse was photographed in 1887. Now it is a GIF, galloping on a tiny Arduino screen: fifteen frames, and they fit. Make it full screen and a little smoother, and the sketch no longer fits at all.

In this episode you will find out how animation works on a small screen and how much memory it needs. Then you will turn a GIF into code in Lopaka, run it on a simulated Arduino UNO, and put a whole month of moon phases on an ESP32.

What you will build: a galloping horse from a GIF on a 128×64 OLED with an Arduino UNO, and a month of moon phases from a video on the same OLED with an ESP32. What you need: a browser and the simulator setup from episode 1: an SSD1306 OLED and the U8g2 library. For the horse we used Eadweard Muybridge's galloping horse (Animal Locomotion, 1887, public domain); any short GIF works.

Frames and frame rate ​

An animation is a stack of pictures ​

An animation is a stack of still pictures. Show them one after another, fast enough, and your eye stops seeing pictures and starts seeing motion.

That speed is the frame rate, in frames per second (FPS). At two frames per second the horse is a slideshow. At ten, it gallops.

The 15 photos of the galloping horse, with one of them large and the counter 10 frames per second: it gallops

The loop plays it frame by frame ​

A microcontroller plays an animation the same way. Each pass of loop() draws the current frame, sends it to the screen, waits a moment and moves on to the next frame. After the last frame it jumps back to the first.

The animation loop around an OLED showing the horse: draw, send, wait, next, with the counter frame 5 of 15

Every frame costs memory ​

One full-screen frame is 1,024 bytes ​

The OLED has 128 by 64 pixels, and each pixel is one bit: on or off. That is 8,192 bits, or 1,024 bytes for one full-screen frame. A frame of any size takes width × height ÷ 8 bytes.

128 × 64 = 8,192 pixels, 1 pixel = 1 bit, 8,192 bits = 1,024 bytes: one full-screen frame

An Arduino UNO holds 23 full-screen frames ​

The frames live in flash memory, next to your program. An Arduino UNO has 32 KB of flash, and the U8g2 library and the sketch already take about 8 KB of it. That leaves room for 23 full-screen frames. We checked with the compiler: with 23 frames the sketch uses 99% of the UNO's flash, and the 24th frame makes it too big.

The flash of an Arduino UNO, 32 KB: the library and sketch take about 8 KB, then 23 full-screen frames fill it to 99%, and frame 24 won't fit

Why the horse fits, and full screen does not ​

The horse is 96 by 64 pixels, so a frame takes 96 × 64 ÷ 8 = 768 bytes. Fifteen frames come to about 11 KB, and the sketch fits in 62% of the flash.

The UNO flash with 15 horse frames of 768 bytes: about 11 KB, 62%, it fits

Full screen at twenty frames per second needs thirty frames of 1,024 bytes, and the sketch grows to 121% of the flash. The compiler stops with "Sketch too big". But the GIF has only fifteen drawings. To play it at twenty frames per second, the sketch shows every drawing twice, so half of those thirty frames are copies.

The UNO flash overflowing at 121% with 30 full-screen frames, every second frame a copy of the one before

Three ways to make an animation fit ​

  1. Fewer frames. Keep the GIF's own frame rate: a higher one only adds copies.
  2. Smaller frames. Every row and column you cut saves bytes in every frame.
  3. A bigger board. An ESP32 has 1.25 MB of flash for your program: room for about a thousand full-screen frames.

Three ways to fit an animation: fewer frames, smaller frames, a bigger board; an ESP32 has 1.25 MB of app flash, room for 1,000 full-screen frames, while the whole UNO flash is a small sliver

Step 1: import a GIF ​

Create a new project as in episode 1: + New Project, platform U8g2, screen size 128×64.

The animation library ​

Open the Animations tab on the left. Lopaka comes with ready-made animated icons, and the size estimate sits next to the size and quality buttons. A 64×64 icon in High quality takes about 14 KB; at 32×32 and Low quality it is under one kilobyte. Pick an icon there when a small animation is all you need.

Upload your GIF ​

For your own GIF, click Upload at the top of the panel and drop the file into the dialog. Videos work too, as you will see in step 5.

Lopaka splits the GIF into frames and opens the import dialog. The frames are on the left, the preview in the middle, the settings on the right:

  • Dithering: switch it from Floyd-Steinberg to No dithering. The photo turns into a clean white silhouette instead of a cloud of dots. Dithering helps with shades, as in episode 2; a silhouette needs none.
  • Frames per second: keep 10, the GIF's own speed.

The row at the bottom does the memory maths for you: 15 source frames, 15 output frames, frame size 96×64, about 11.3 KiB. Lopaka scaled the 300×200 GIF to the height of the screen, 64 pixels, which makes it 96 pixels wide. One frame is 768 bytes, the number at the top of the settings.

The import dialog with the horse GIF: 15 frames on the left, the white silhouette in the preview, Dithering set to No dithering, 10 frames per second, 15 source and output frames, frame size 96×64 and an estimated size of 11.3 KiB

A common mistake is to raise the frame rate to make the GIF smoother. At 20 FPS the dialog shows 15 source frames but 30 output frames, and the estimate doubles to 22.5 KiB: the same fifteen drawings, each one twice.

Click Import.

Step 2: the frame editor ​

The frames open in the frame editor, with all fifteen in the timeline at the bottom.

  • Play (the triangle button) checks the loop.
  • With the Onion switch on, the previous frame shows through in grey, so you can see how far the legs move from one frame to the next.

The old photos have numbers along the bottom edge, and the import turned them into white marks. Remove them from all frames at once: click the frame size next to the frame rate (96×64 px), set Height to 60 and click Resize. The pixels stay anchored to the top-left corner, so the bottom four rows go away on every frame, and the memory counter drops from 11.3 to 10.6 KiB. If a frame still has a few dots of a number at the bottom edge, step to it and wipe them with the eraser.

The frame editor on frame 2 of 15: the horse at 96×60 pixels with the previous frame showing through in grey, 10 FPS, 96×60 px and 10.6 KiB at the bottom, Onion on

Click Save. The animation sits on the screen like any other layer, named horse, at X 16 and Y 0: (128 − 96) ÷ 2 = 16 puts it in the middle.

Step 3: the code ​

Frames straight into the code ​

In the switches next to the code, untick Animation helpers. Now Lopaka writes the frames straight into the code, and your own loop() decides when to draw them:

cpp
// [BEGIN lopaka generated]
static const unsigned char animation_horse_frames[15][720] U8X8_PROGMEM = {
  {0x00,0x00,0x00, /* ... */ 0x00,0x00},   // frame 1: 720 bytes, shortened here
  // ... 13 more frames ...
  {0x00,0x00,0x00, /* ... */ 0xff,0xff}    // frame 15
};
static int animation_horse_frame = 0;
void drawScreen_1(void) {
    u8g2.setFontMode(1);
    u8g2.setBitmapMode(1);
    // horse
    u8g2.drawXBMP(16, 0, 96, 60, animation_horse_frames[animation_horse_frame]);
    animation_horse_frame = (animation_horse_frame + 1) % 15;
}
// [END lopaka generated]

What the numbers mean:

  • [15][720]: one array of fifteen frames, 720 bytes each. A row of 96 pixels takes 96 ÷ 8 = 12 bytes, and 60 rows make 720. In total that is 10,800 bytes, the 10.6 KiB from the frame editor.
  • U8X8_PROGMEM keeps the frames in flash, as with the images in episode 2. Without it, an UNO would try to copy them into its 2 KB of RAM.
  • drawXBMP(16, 0, 96, 60, ...) draws the current frame at the position and size of the layer.
  • (animation_horse_frame + 1) % 15 steps to the next frame. % 15 is the remainder after dividing by 15, so after frame 14 (the fifteenth, counting from zero) the counter jumps back to 0.

The rest of the sketch ​

Around the generated block, write the rest of the sketch. You can type it in Lopaka's code panel: lines above // [BEGIN lopaka generated] and below // [END lopaka generated] are yours, they stay in the project when you change the design, and Copy takes them along.

cpp
#include <U8g2lib.h>

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// Paste the Lopaka code here

void setup() {
  u8g2.begin();
}

void loop() {
  u8g2.clearBuffer();
  drawScreen_1();
  u8g2.sendBuffer();
  delay(56);
}

Each pass of loop() is one frame: clear, draw, send, wait. This is the loop from the beginning of the article.

The code panel in Lopaka with the include and the display above the generated block, the frames array animation_horse_frames[15][720] U8X8_PROGMEM, the draw function, and setup and loop with delay(56) below; Animation helpers is unticked

Why the delay is 56 milliseconds ​

Sending a full frame to the OLED over I2C takes about 44 milliseconds on the simulated UNO. Add a 56-millisecond delay, and every frame takes 100 milliseconds: ten frames per second, the speed of the GIF. On a real board the time to send a frame depends on the I2C speed; if the horse runs too fast or too slow, change the delay.

Click Copy.

Step 4: run it on an Arduino UNO ​

Open your simulator project from episode 1: an Arduino UNO and an SSD1306 OLED wired to GND, 5V, A5 (SCL) and A4 (SDA), with the U8g2 library. Paste the code into sketch.ino and press Play.

The horse gallops. Compiled for an UNO, the sketch reports:

Sketch uses 19324 bytes (59%) of program storage space. Maximum is 32256 bytes.
Global variables use 1558 bytes (76%) of dynamic memory, leaving 490 bytes for local variables. Maximum is 2048 bytes.

That is 59% of the flash, down from 62% before we cut the four rows. As in episode 1, most of the RAM is the 1,024-byte frame buffer.

Step 5: a video on an ESP32 ​

Import the video ​

Lopaka also imports video (MP4, MOV and WebM). We used a NASA Scientific Visualization Studio clip that shows a whole month of moon phases in ten seconds.

Create another U8g2 project at 128×64, open Animations and click Upload. The import dialog for a video does the maths while you change the settings:

  1. At the video's 30 frames per second, ten seconds make 300 frames at 64×64: 150 KiB.
  2. A slow moon looks fine at 12 frames per second: type 12, and it becomes 120 frames, 60 KiB.
  3. Turn on Invert result. Lopaka lights the dark parts of a picture, which is why the dark horse on a light background came out white. The moon is the opposite, bright on a dark sky, so invert the result: now the moon lights up and the sky stays dark.

Keep Floyd-Steinberg dithering this time: the dots draw the shading of the moon's surface.

The video import dialog with the moon: the full moon in the preview as a pattern of dots, Floyd-Steinberg dithering, Invert result on, 12 frames per second, duration 0:10, 120 frames, frame size 64×64 and an estimated size of 60.0 KiB

Click Import, then Save in the frame editor. Lopaka centres the 64×64 animation on the screen, at X 32. Untick Animation helpers again, and the code gets animation_moon_frames[120][512] and the line animation_moon_frame = (animation_moon_frame + 1) % 120;.

60 KB: twice the UNO, under 5% of an ESP32 ​

At 64 by 64 pixels each frame is 64 × 64 ÷ 8 = 512 bytes, so 120 frames take 61,440 bytes, 60 KB. That is about twice the UNO's whole flash.

The 60 KB moon clip, 120 frames, lying across the 32 KB flash of an Arduino UNO: about 2 times the whole UNO

An ESP32 has 1,310,720 bytes of flash for the program, and the same clip takes 4.7% of it.

Run it on the ESP32 ​

Use the same sketch with the moon's code in its place, and change the delay to delay(32);. The ESP32 sends a frame in about 51 milliseconds in the simulator, so each frame takes about 83 milliseconds: 12 frames per second.

In the simulator, create an ESP32 project, add the SSD1306 OLED and the U8g2 library, and wire the display to the ESP32's I2C pins: GND to GND, VCC to 3V3, SCL to GPIO 22 and SDA to GPIO 21. Paste the sketch and press Play. The compiler reports 352,240 bytes, 26% of the flash, and the moon grows and shrinks through a whole month.

An ESP32 wired to an SSD1306 OLED in the simulator; the display shows the full moon drawn in dots

Recap ​

  1. An animation is a stack of frames, shown fast enough to look like motion. Each pass of loop() clears, draws one frame, sends it and waits.
  2. Every frame costs width × height ÷ 8 bytes of flash. A full-screen frame on this OLED is a kilobyte, and an UNO holds about twenty of them.
  3. To make a GIF fit, use fewer frames, smaller frames or a bigger board.

Next episode: Touch UI with LVGL. Touch screens, with buttons and sliders, like on a phone. Read episode 8.

Ready to try? Open Lopaka and bring your own GIF to life.