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?

It’s alive!

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.

Trigonometric functions, including the sine

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!