Animating bugs with Python and Pillow
Last weekend I spent some more time tinkering with the bug rendering project. Bugs don’t really feel like bugs until they start crawling, and that’s what I did. Let’s talk about this a little bit. I also published the project at https://bugs.pablart.com for anyone to play around.
From static images to animations⌗
An animation is nothing more than a sequence of consecutive images that give the illusion of movement. Did you ever stop to think why we call them motion pictures?
With digital movies, it’s the same idea. You make a series of pictures showing slightly modified versions of your subject, and when you put them together, it appears as if they move. In my project, I animate the bug’s legs, and to do so we had to reorganize the code a little bit.
Essentially, things start to get pretty complicated if you mix your rendering code with your shape transformation code. The solution to that mess is simple: Separate the code that computes the shape of the bug from the code that actually draws the bug itself. The flow becomes something like the following list.
- Load the bug specification
- Transform the bug into a specific shape, given a key frame number
- Write the new specification into an “intermediate format”
- Turn that intermediate format into pixels
- Repeat a number of times
- Stitch together the images into a single animation
In the second step I mention a key frame. For the bugs to appear to be moving like they’re crawling, we need a smooth, “circle-like” paramerization. One clean way to represent this is to iterate the entire angle of a circle (from 0 to 2*pi radians), computing its sine
which is convenient because this function is at zero (rest) when the input is 0, then it quickly rises toward 1, slowing down until it reaches its maximum, and it repeats in the opposite direction until -1. If you use the range [-1,1]
to represent the possible values of your animation (such as moving a single leg), this is a good way to parameterize a natural motion.
Now that we have a way to represent the frame of our animation, we can use it to transform each leg given a bend angle which varies depending on whether the leg is odd or even, and if it’s on the left or right side of our critter. This is part of the relevant code:
gait_range = math.pi / 24.0
gait_shift = frame * gait_range
joint_spacing = [j['gap'] for j in legs]
angle_ref = -ANGLE_RANGE
angle_step = 2*ANGLE_RANGE / (len(legs)-1) # Break the entire angle range into these many steps
joint_heights = [sum(joint_spacing[:i]) for i in range(1, len(joint_spacing))]
oddity = True
for jh, leg in zip(joint_heights, bug['legs']):
y_ratio = jh/den
h = int(tail_point[1] + y_ratio*base_height)
joint_center = (width/2, h)
# draw.ellipse(circle_box(joint_center, base_thickness*.5), fill=fill)
num_segments = leg['segs']
tot_len = leg['len']
width_factor = leg['w_mult']
segment_len = width*tot_len/num_segments
# One leg in each direction
gait = gait_shift if oddity else -gait_shift
computed_legs.append({
'bones': build_leg(joint_center, segment_len, num_segments, angle_ref+gait, direction=1),
'width_factor': width_factor,
})
computed_legs.append({
'bones': build_leg(joint_center, segment_len, num_segments, angle_ref-gait, direction=-1),
'width_factor': width_factor,
})
angle_ref += angle_step
oddity = not oddity
Don’t pay too much attention to the details. The key values are the gait*
and angle*
variables, and how they interact with the oddity
. Once we have a way to take a frame key value and transform the bug, we can render it, doing so once per each frame, and using Pillow
to compose every still image into one single animation:
def draw_animated_bug(bug, width, height, steps, seconds, name=None):
"""Optionally save the animation, or skip if serving live on the web"""
images = []
for i in range(steps):
angle = (2*math.pi*i) / steps
frame = math.sin(angle)
images.append(draw_bug(bug, width=width, height=height, frame=frame))
if name:
images[0].save(name, save_all=True, append_images=images[1:], optimize=False, duration=seconds/steps, loop=0)
return images
Live on the web⌗
Now that I have this setup, I wrote a simple Vue.js app that lets users pick one of four similar bugs, spawning the next generation. This simulates a form of artificial selection which can produce quite mesmerizing shapes.
Once it was time to deploy this to the web at https://bugs.pablart.com, I ran into a small annoyance when dealing with some environment differences between AWS Lambda (which runs the code in a serverless way) and API Gateway (which serves Lambda and puts up a secure frontend, including DNS routing). In order to tell the app how to request each next version of the bug, I had to provide an APPLICATION_ROOT
as an environment variable (with the values /dev/
or /prod/
depending on the deployment target). However, if I want to get rid of that prefix to produce a nicer URL via API Gateway, I had to drop the /prod/
prefix, effectively making the production Lambda environment only work via API Gateway.
# zappa_settings.json
{
"dev": {
"app_function": "app.app",
[...],
"environment_variables": {
"APPLICATION_ROOT": "/dev/"
}
},
"prod": {
"app_function": "app.app",
[...],
"environment_variables": {
"APPLICATION_ROOT": "/prod/"
}
}
}
In other words, the promise of a simpler setup via serverless
is not without its pitfalls and inconveniences. But it sure beats paying for a server that sits idle 99.9% of the time.
Technologies used⌗
- Pillow is used to render the individual images, and then to stitch them together as an animated GIF file.
- Flask is a minimalistic web framework that I use to create the images and serve them to your computer.
- Lambda is the serverless computing platform that makes it easy to deploy the project.
- API Gateway is another offering by AWS that encapsulates your Lambda deployments and makes it possible to serve via a custom domain such as https://bugs.pablart.com.
- Zappa is a neat little tool that makes deployment of web services to Lambda much easier. Highly recommended.
Future work⌗
That’s all for now! I have other ideas that are calling my attention right now, so I’ll probably put this project on hold. Some pointers for the future:
- Explore social media sharing for growth hacking learning.
- A nicer bug-showcase view to focus on the winning bug.
- Maybe I’ll post the code on Github… Hit me up if you’re curious.
If you found this interesting, or if you have any comments or questions, drop me a line. My Twitter handle is at the top of this page!