Framework for generating geometrical animations using Python
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
anim
.gitignore
README
setup.py

README

anim_framework is a set of Python 3 classes for constructing non-real-time animations
using Cairo graphics via the Qahirah wrapper ([GitLab](https://gitlab.com/ldo/qahirah),
[GitHub](https://github.com/ldo/qahirah)). The module `anim.common` contains common
definitions for managing time and drawing, while additional modules `anim.lissa`,
`anim.maurer`, `anim.rose`, `anim.spirolat`, `anim.troch` and `anim.whirl` build on this
to render animations of Lissajous, Maurer, rose, spirolateral and trochoid and whirl
curves respectively.

Installation instructions are in the `setup.py` script.

`anim.common` provides a convenience routine, `render_anim`, which will render out an
entire animation to a sequence of numbered PNG frames. These can then be re-encoded
through a tool like [FFmpeg](http://ffmpeg.org) into any convenient delivery format, like
OGV, MPEG-4 etc.

To illustrate the process, let’s start by defining some example parameters: the duration
of the animation in seconds, the width and height of each frame, the frame rate, and the
number of linear steps used to approximate curves:

    anim_duration = 10.0
    dimensions = qahirah.Vector(1280, 720)
    frame_rate = 25.0
    nr_steps = 500

Now let’s make a simple animating Lissajous curve:

    lissa = anim.lissa.make_draw \
      (
        x_amp = 300,
        x_freq = 2,
        x_phase = anim.common.linear_interpolator(0, anim_duration, 0, 1),
        y_amp = 300,
        y_freq = 3,
        y_phase = 0,
        nr_steps = nr_steps,
      )

Note that in this example all the parameters are constant, except for the `x_phase`, which
is set to vary linearly from 0 to 1 over the entire duration of the animation. In general,
any curve parameter can be either a constant value or the result of evaluating a
time-dependent interpolator. The framework provides a range of basic interpolator types,
as well as functions for transforming and combining them in various ways, and it is easy
enough to define your own. So long as you put your custom interpolators through
`anim.common.interpolator` (which can be used as a decorator), the framework will treat
them exactly like built-in interpolators.

The result returned by the above `make_draw` call is a “draw procedure”, which is a
procedure that takes two arguments: a Cairo graphics context, and the current time. It
does whatever is necessary to render the image into that context at that time. The
framework provides functions for combining draw procedures, remapping them through time,
etc. And of course you can define your own.

And finally we can render the curve out to actual images:

    def anim_init(g) :
        g.translate(dimensions / 2)
        g.source_colour = qahirah.Colour.grey(1)
        g.paint()
        g.source_colour = qahirah.Colour.from_hsva((0.25, 0.9, 0.9))
        g.line_width = 4
    #end anim_init

    anim.common.render_anim \
      (
        dimensions = dimensions,
        start_time = 0.0,
        end_time = anim_duration,
        frame_rate = frame_rate,
        draw_frame = lissa,
        overall_presetup = anim_init,
        out_dir = "frames",
        start_frame_nr = 1
      )

which will run for a few seconds or so to generate 250 frames. This requires that a
subdirectory called “`frames`” be created, into which the frames will be written under the
names “`0001.png`”, “`0002.png`” etc. The FFmpeg package provides the “`ffplay`” command,
which can directly play the PNG frames using a command like

    ffplay -autoexit frames/%04d.png

or, if you want playback to loop:

    ffplay -loop 0 frames/%04d.png

Note the definition of `anim_init`, which is called by `render_anim` after it creates the
Cairo drawing context but before rendering any frames, in order to do any special setup
that you need. Here it sets the drawing origin to the centre of the frame (for positioning
the curve), clears the background to white, and sets a pen colour and size for the curve.
HSV tends to be a more convenient space for experimenting with colours than RGB, which is
why I prefer to specify colours in that space.

If you try playing back the resulting animation from the above, it won’t look right. What
happens is that the Lissajous curves from previous frames remain on succeeding frames
instead of being erased, until the result at the end of the animation is a solid green
square.

What you want is to start with a white background for each frame. This can be done with a
custom draw procedure as simple as

    def init_frame(g, t) :
        g.source_colour = qahirah.Colour.grey(1) # background colour
        g.paint()
        g.source_colour = qahirah.Colour.from_hsva((0.25, 0.9, 0.9)) # curve colour
    #end init_frame

(Note to be a draw procedure it has to take two arguments, even though it pays no
attention to the second one.) Now we need a way to ensure this is invoked immediately
prior to the lissa draw procedure for each frame. We can use `draw_compose` for this, to
construct a draw procedure which invokes its first argument followed by its second
argument:

    anim.common.render_anim \
      (
        dimensions = dimensions,
        start_time = 0.0,
        end_time = anim_duration,
        frame_rate = frame_rate,
        draw_frame = anim.common.draw_compose(init_frame, lissa),
        overall_presetup = anim_init,
        out_dir = "frames",
        start_frame_nr = 1
      )

(Note the erasure of the background and setting of the pen colour in `anim_init` can now
be removed, since it is just repeating what is being done in `init_frame`.) If you try
playing back the new frames, you should now see a nicely-animating clean Lissajous curve.

Now, supposing we want to animate the colour of the curve. The draw procedure returned by
`anim.lissa.make_draw` doesn’t have any option for adjusting Cairo settings like the pen
size, colour etc; it simply uses whatever settings are currently in effect. To control
these settings in an animated fashion, we can add a suitable `set_source_colour` call to
our `init_frame` procedure, and animate it based on the second argument (which we were
ignoring before). But since `init_frame` consists of nothing but Cairo calls, there is a
convenience routine to make it easier to define the whole procedure, complete with
animation:

    init_frame = anim.common.make_draw \
      (
        ("set_source_colour", (Colour.grey(1),)),
        ("paint", ()),
        (
            "set_source_colour",
            (
                anim.common.hsva_to_colour_interpolator
                  (
                    h = anim.common.linear_interpolator(0, anim_duration, 0, 1),
                    s = 0.9,
                    v = 0.9,
                    a = 1.0
                  ),
            )
        ),
      )

Now the exact same `render_anim` call as before will produce a Lissajous animation where
the curve goes through all the colours of the spectrum.

Hopefully that gives you the flavour of the framework, and how easy it is to do some quite
elaborate animations. By all means, delve further into it, try things out, and have fun.

Further code examples are available in my anim_framework_examples repo
([GitLab](https://gitlab.com/ldo/anim_framework_examples),
[GitHub](https://github.com/ldo/anim_framework_examples)).

Lawrence D'Oliveiro <ldo@geek-central.gen.nz>
2017 April 7