Sprite animation on original classic Pebble smartwatch

SDK 3.x for Pebble Time smartwatch offers cool and very convenient set of functions to create animation from your existing GIF or MP4 via APNG support. APNG is an obscure “Animated PNG” format (at the time of the post only Mozilla Firefox supports it) but it’s very powerful and can store animation in much more compressed format than traditional animated GIF, so Pebble chose it for a reason. So if you have a GIF, convert it to APNG with Gif2Apng (or if you have a video, convert MP4 to GIF first and then to APNG) and you’re ready to use it on Pebble Time. Just keep the size in check, since Pebble has to load entire APNG sequence in memory, try not to go overboard. The first video is showing animation from my “Vortex” watchface using this approach on Pebble Time.

But what about original classic Pebbles? Eventually they will get firmware 3.x and SDK 3.x support and with that APNG functions among other advantages, but at the time of this writing it is still hazy when this is going to happen. But where there’s a will there’s a way – you can still use your MP4/GIF source for animation it’s just a bit more tricky. Instead of dealing with a single APNG file as your resource and relying on Pebble firmware to draw the frames you will need to help it a little.

First you will need to split your source into individual frames, for example using this service. Yes, you will be dealing with individual frames, so don’t go creating a Hollywood blockbuster. But don’t fret, it’s a bit more manual work, but you won’t have to hand-crank the moving pictures all the way.

Next add your split frame images to your Pebble project (you will need to convert them to PNG if they’re not in that format already). Give them resource identifiers that would makes sense to you and indicate their sequentialliness (is this a word?), e.g.


Next declare an array that points to these ids:

const int my_blockbuster_res[] = {  


also variable to hold current displayed frame and total number of frames:

int frame_no;    
#define NO_OF_FRAMES 20

and bitmap to hold current frame as well as bitmap layer to display the image:

static BitmapLayer *s_bitmap_layer;
static GBitmap *s_bitmap = NULL;

With this is place (and assuming you created bitmap layer, which you should, e.g. in window load) you’re ready to roll.

Create a starter function that would reset frame counter and kick-off animation:

static void load_sequence() {
  frame_no = 0;
  app_timer_register(1, timer_handler, NULL);

As you can see it sets frame counter to 0 and kicks off animation timer callback functions with 1ms delay, which is pretty much immediate.
Next before that function create timer callback function:

static void timer_handler(void *context) {

  if(frame_no < NO_OF_FRAMES) {
    if (s_bitmap != NULL) {
      s_bitmap = NULL;
    s_bitmap = gbitmap_create_with_resource(my_blockbuster_res[frame_no]);
    bitmap_layer_set_bitmap(s_bitmap_layer, s_bitmap);

    app_timer_register(80, timer_handler, NULL);

First this function checks whether we reached limit of frames (Line 03) and continues if we didn’t. Next it destroys temporary bitmap from the previous frame if it exists (Lines 05-08, noone wants memory leaks). Then code creates bitmap from array we defined above based on current frame number (Line 10). Next we display the bitmap on bitmap layer and mark layer as dirty so it will redraw (Lines 12-13). And lastly we increment frame number and kick off another timer callback so the code can repeat again with the new frame (Lines 15-16). Note that this example uses 80ms as a delay between frame displays, so for example a clip 37-38 frames long will run for about 3 seconds. Your mileage may vary so adjust this parameter as needed.

To start the animation call load_sequence(); anywhere in your code. The result of this code is showing on the second video above, that’s Vortex watchface running on classic Pebble.

Pro tip: Sometimes you need a resource to be compiled just for a Basalt platform. In the article above APNG is only meant for Pebble Time, but by default it is compiled for both platforms, which is undesirable as it may lead to failure to install due to size overload. If you’re using installed SDK, you can simple specify "targetPlatforms": ["basalt"] for your resource, but CloudPebble IDE doesn’t have this luxury. So, if you’re using CloudPebble, as a workaround just add a small dummy file as a main resource – and large meant-for-basalt file as secondary “colour” resource. This way only Basalt binary will get that large resource.

Useful Links:

Leave a Reply

Your email address will not be published. Required fields are marked *