Generating a 88×31 button
Since creating my website, about two years ago, I have been inspired by 88×31 buttons made by other people and always wanted to make one. Finally, a few weeks ago¹, I decided that i should finally stop procrastinating and simply create one.
To make a tiny 88×31 button, you generally make use of a pixel art editor. I had dabbled with few of them in the past, but I never spent enough time with them to find one that fits me, maybe because I am not very much into pixel art creation (even though i like the style). Due to this, I put off making a button to later, for a long time. But I had to create it someday, thus, I turned to tools with which I already had some experience which may help me create pixel art. I already used GIMP to make icons and pixel precise mock-ups, so i knew what to expect with that tool. Thus, we can finally start! yay!
I was first thinking of creating a static 88×31 button… then slowly felt like adding more frames, and make it animated, to allow more variety to sweep in. GIMP has a GIF exporter that allows you to export each layer as a separate frame, so it was only natural to consider it. But, if you have layer groups, they need to be manually combined which quickly becomes tedious (or you can use a script to do that, but it's more involved), so I was still on the fence. As I explored GIMP's artistic options, I quickly became fully sold by the idea of making it animated after finding the ripple effect in GIMP's filter library. Adding a ripple effect between pages of my button was too hard to resist!

This brought up questions on how to organize my project: i couldn't continue with layer groups, as the animated ripple effect works on an existing image layer and modifies it. Furthermore, GIMP is an image editor, it doesn't have a timeline, so I cannot transform an image over a period of time and have any kind of animation. It is not the right tool for the job for that kind of stuff. For a second, I had even considered using Kdenlive, but, even by abusing the tool, I don't think that it would do a good job with pixel art. So, we necessarily have to go from the artsy side to the computer side.


Little known GEGL
There had to be a way to call these filters from outside of GIMP, right? It's GNU and open-source software; it has to be composable with other tools. I read about GIMP having a batch mode for executing commands headless and scripting support, with scripts written in Scheme (a dialect of Lisp), and wanted to make a small script that would take an input image, transform it with the ripple effect and spit out an output image. Sadly, I didn't get very far… I wasn't able to find the ripple effect anywhere in the procedure and the plug-in browsers, but it must still be somewhere. Where was that effect? Can we call it outside of GIMP? If not, what function does it apply on the pixels to generate these effects?
I had to elucidate that mystery… naturally, by looking around in the source code of GIMP! Well, downloading the tarball and grep it is then! Vim's quickfixes feature was handy in following found matches. Anyways, after figuring out to which string "Onduler" maps to, the most I found was "gegl:ripple". There was nothing else of interest in the "gimp" tarball. What is this "gegl" thing? Apparently, filters in GIMP are split into another library: the Generic Graphics Library, or GEGL. We had to look there! After doing a little more grepping, this time, in the "gegl" repository, I finally found it: the implementation of our effect is in the operations/common-gpl3+/ripple.c file! Now, what do we do with this?
$ ffmpeg -loop 1 -i To\ ondulate/username.png -t 5 -f lavfi -i nullsrc=s=88x31,lutrgb=128:128:128 -f lavfi -i nullsrc=s=88x31,geq='r=r(X+(6*(abs((((((X*cos(0)-Y*sin(0))+(1000-T*2)*3/4)//(1000-T*2))/(1000-T*2))*4)-2)-1))*sin(0),Y+(6*(abs((((((X*cos(0)-Y*sin(0))+(1000-T*2)*3/4)//(1000-T*2))/(1000-T*2))*4)-2)-1))*cos(0)):g=g(X+(6*(abs((((((X*cos(0)-Y*sin(0))+(1000-T*2)*3/4)//(1000-T*2))/(1000-T*2))*4)-2)-1))*sin(0),Y+(6*(abs((((((X*cos(0)-Y*sin(0))+(1000-T*2)*3/4)//(1000-T*2))/(1000-T*2))*4)-2)-1))*cos(0)):b=b(X+(6*(abs((((((X*cos(0)-Y*sin(0))+(1000-T*2)*3/4)//(1000-T*2))/(1000-T*2))*4)-2)-1))*sin(0),Y+(6*(abs((((((X*cos(0)-Y*sin(0))+(1000-T*2)*3/4)//(1000-T*2))/(1000-T*2))*4)-2)-1))*cos(0))' -lavfi '[0][1][2]displace' test.gif(note that lavfi is equivalent to filter_complex)
Not knowing any better, I was remaking the ripple effect as a geq filter in ffmpeg, but didn't have any luck: the labour was too unwieldy and i wasn't making much progress. It was a dead end and clearly not suited to my application.
Nevertheless, i did not give up: after some more searching, I learned that gegl effects can be simply ran directly from the command line. Finally, we were starting to go somewhere, all what's needed is to make a small bash loop to vary the period over time: gegl -i To\ ondulate/username.png -o test.png -- gegl:ripple amplitude=6 period=1000 phi=0 angle=0 sampler-type=nearest wave-type=triangle abyss-policy=none tileable=no
We then have to figure out a way to collate the images together and make an animation that has multiple pages with different information. One wavy effect won't cut it…
Behold ffmpeg
Collating images to make a video, that's the work for ffmpeg! Though, when passing a sequence of image files (with -i frame%04d.png), they all need to have the same dimensions and I noticed that this isn't the case with the output files of gegl for some reason. Thus, I centre them with ImageMagick, making sure that they are all 88×31 as expected. Also, the output video has to be lossless and preserve transparency (i had some trouble with ImageMagick for a moment), so we copy the frames as PNG, making sure that the pixel aspect ratio and display aspect ratios have expected values (as some of the images managed to have different aspect ratios): ffmpeg -hide_banner -framerate "$fps" -i frame%04d.png -filter_complex "[0:v:0]scale=88:31:force_original_aspect_ratio=disable,setsar=sar=1,setdar=dar=88/31[v]" -map "[v]" -c:v png -pix_fmt rgba64be -y out.mkv (yes, re-encoding a PNG as a PNG has overhead, but it's completely negligible in the whole build process). The complete script is available right here.

The procedure above applies a ripple effect to an image and gives us a video file, but this is not enough to make a button. We need more images and we need to chain them together to make something complete. As I only apply the effect to the foreground layer, we must not forget a backdrop to wrap everything up. To simplify the work and make sure that everything stays readable, I opted for a static background image. ffmpeg gives us the tools to combine video files and make what we want. We just need to orchestrate (not with Kubernetes!) multiple calls to ffmpeg with different image files to programatically generate a 88×31 button. It's too late now, we are too deep in the depths of the computer, we cannot step back anymore…
Makefiles
Makefiles may sound daunting, opaque and scary at first, but once your start understanding how they work, they aren't that bad. Before this project, I knew nearly nothing about them! I won't go much into details here and strongly recommend reading this great tutorial. I will just explain that they work with the concept of dependencies that have to be built following certain rules before building their dependents. This makes the build process very efficient as only a subset of files — those that have been modified since the last build — are rebuilt, instead of starting from scratch every time.
I started by writing a rule to generate ripple animations:
%.ond.mkv: ondulate/%.png ripple ./ripple "$<" 500 10 $(ripple_time) $(fps) $(still_time) "build/$@" 6
This rule works with wildcards which avoids us from having to hard code input and output file names. The name on the left hand side of the colon is the target name (output) and on the right we have the dependencies of that target, which may be files or the names of other targets (in that case, these other targets will generate output files that have the same name). In our case, we look for the PNG specified by another target under the ondulate directory and at the ripple script, which generates our ripple, as seen in the previous section. Following the rules of Make, the %.ond.mkv target would only be rebuilt if its dependencies are newer than the output file of the target. This makes sure that any modifications of the original files or of the ripple script results in rebuilding of the ripple video. Note that $< and $@ are automatic variables that are replaced with the first dependency and the target name respectively. The other variables, of the form $(var_name), have been manually declared.
In a similar fashion, i created other Make targets that generate "animations": %.sta.mkv for static images (with no ripple), wallpaper.mkv for the background image and %.qr.mkv for static images that embed a QR Code (also generated in the Makefile). The complete definitions are available here, but note that the latter one should probably be divided into multiple smaller rules to avoid repetition and be more efficient.

We can then build on these rules to concatenate, or string together, these animations:
concat.mkv: username.ond.mkv ponies.ond.mkv themes_en.sta.mkv themes_fr.sta.mkv multilingue.sta.mkv scannez.qr.mkv $(ffmpeg) $(addprefix -i ,$+) -filter_complex "concat=n=$(num_banners):v=1:a=0 [outv]" -map "[outv]" -c:v png -pix_fmt rgba64be build/concat.mkv
This "concat.mkv" target depends on multiple animation targets before calling ffmpeg. The $(addprefix -i ,$+) prefixes each dependency with "-i " to make it an input file to ffmpeg. The concat filter implicitly operates on the input video streams, in order, but we still have to specify the number of input streams.
Finally, we can combine the concat.mkv file and the wallpaper to compose the final 88×31 button:
layers.mkv: concat.mkv wallpaper.mkv $(ffmpeg) -i build/wallpaper.mkv -i build/concat.mkv -filter_complex "[0:v:0][1:v:0] overlay=format=rgb:eof_action=repeat" -c:v png -framerate $(fps) build/layers.mkv build/button.apng: layers.mkv $(ffmpeg) -i build/layers.mkv -framerate $(fps) -plays 0 -final_delay 0.0 build/button.apng
We have to make sure that the videos are overlayed in the RGB colourspace instead of using the YUV colourspace, as this creates perceptual degradations. For instance, purple or red pixels that overlay the light parts of the wallpaper lose their colour as if it were partially "eaten" by the cream colour in the background. We also don't want the button animation to end prematurely if the video ends, so we use the eof_action=repeat option. Anyways, after these hurdles, our Makefile creates the final button below!!
Feel free to put my 88x31 button on your website!
If you want to trade buttons, just send me a message and I'll add yours to my website.
View HTML embed
<a href="https://twilightsparkle.space/">
<img alt="88x31 button presenting ConfuSomu's website" style="image-rendering: pixelated;" src="https://twilightsparkle.space/btn/button1.apng">
</a>
N.B: Note that you may copy the image onto your website. The image displayed above has longer alt text than the one in this example HTML code.
And we are done! Using ffmpeg and Makefiles, we can generate a 88×31 button. Feel free to make your own buttons using the Makefile and the script shared, it's not too difficult once we got going, and it's fun!
Nevertheless, I already have ideas for a few improvements for the future: it should be possible to change the background image over time and apply a transition effect — like the ripple effect, a basic change of frame or a fade effect — to the whole button instead of just the foreground. More effects should also be added, maybe even being able to include downscaled videos in the button. When I will make a second version, I will implement some of these effects.
PS: And yes, I shall translate my website into more languages, as to be faithful to my button! :P
¹: Sadly, I was sick while writing this, so it's more like a month now ↸