Animation

Scenes and Effects

The asciimatics package gets its name from a storyboard technique in films (‘animatics’) where simple animations and mock-ups are used to get a better feel for the planned film. Much like these storyboards, you need two key elements for your animation.

  1. One or more Scene objects that encompass the key stages of your animation.
  2. One or more Effect objects in each Scene that actually display something on the Screen.

An Effect is basically an object that encodes something to be displayed on the Screen. It can be anything from Print that just displays some rendered text at a specific location for a certain time to Snow that adds dynamically generated falling snow to the Scene. These are the building blocks of your animation and will be rendered in the strict order that they appear in the Scene, so most of the time you want to put foreground Effects last to ensure they overwrite anything else.

There is no hard and fast rule of how to divide up your Scenes, though there is normally a natural cut where you want to move between effects or clear the Screen, much like you’d need to move to a different cell in a comic strip. These cuts are where you should consider creating a new Scene.

Once you have built up a set of Effects into a list of one or more Scenes, you can pass this list to play() which will run through the Scenes in order, or stop playing if the user exits by pressing ‘q’ (assuming you use the default key handling).

Timing Effects

When playing animations, asciimatics will try to redraw the Screen 20 times a second. Each iteration of the loop produces a new frame (no relation to the widget class Frame) and increments the frame counter.

This counter is passed as the frame_no parameter on update() to every Effect amd so an be used to time the animation. For example, if you only want the Effect to do something every half a second, you could wait for frame_no to increase by 10 before doing the next update.

This is also the counter that determines when to start/stop an Effect based on the start_frame and stop_frame properties on each Effect. Specifying non-zero values will delay the start of the Effect until, or stop drawing it at, the specified frame count in the Scene.

See the credits sample for an example of how to use these properties.

Sprites and Paths

A Sprite is a special Effect designed to move some rendered text around the Screen, thus creating an animated character. As such, they work like any other Effect, needing to be placed in a Scene and passed to the Screen (through the play() method) to be displayed. They typically take:

  • a set of Renderers to animate the motion of the character when moving in any direction
  • a default Renderer (to be used when standing still)
  • a path to define where the Sprite moves.

Much like Renderers, the paths come in 2 flavours:

  1. A Path is a pre-defined path that can be fully determined at the start of the program. This provides 4 methods - jump_to(), wait(), move_straight_to() and move_round_to() - to define the path. Just decide on the path and script it by chaining these methods together.
  2. A DynamicPath which depends on the program state and so can only be calculated when needed - e.g. because it depends on what key the user is pressing. These provide an abstract method - process_event() - that must be overridden to handle any keys and Update the current coordinates of the Path, to be returned the next time the Sprite asks for an update.

The full declaration of a Sprite is therefore something like this.

# Sample Sprite that plots an "X" for each step along an elliptical path.
centre = (screen.width // 2, screen.height // 2)
curve_path = []
for i in range(0, 11):
    curve_path.append(
        (centre[0] + (screen.width / 4 * math.sin(i * math.pi / 5)),
         centre[1] - (screen.height / 4 * math.cos(i * math.pi / 5))))
path = Path()
path.jump_to(centre[0], centre[1] - screen.height // 4),
path.move_round_to(curve_path, 60)
sprite = Sprite(
    screen,
    renderer_dict={
        "default": StaticRenderer(images=["X"])
    },
    path=path,
    colour=Screen.COLOUR_RED,
    clear=False)

For more examples of using Sprites, including dynamic Paths, see the samples directory.

Particle Systems

A ParticleEffect is a special Effect designed to draw a particle system. It consists of one or more ParticleEmitter objects which in turn contains one or more Particle objects.

The ParticleEffect defines a chain of ParticleEmitters that spawn one or more Particles, each with a unique set of attributes - e.g. location, direction, colour, etc. The ParticleEffect renders a frame by rendering each of these Particles and then updating them following the rules defined by the ParticleEmitter.

It all sounds a bit convoluted, doesn’t it? Let’s try a concrete example to clarify it… Consider the StarFirework effect. This is constructed as follows.

  1. The StarFirework constructs a Rocket. This is a ParticleEmitter that has just one Particle that shoots vertically up the Screen to hit a pre-defined end point.
  2. When this Particle hits its end-point, it expires and spawns a StarExplosion. This is a ParticleEmitter that spawns many Particles in such a way that they are explode outwards radially from where the Rocket expired.
  3. In turn, each of these Particles from the StarExplosion spawns a StarTrail on each new frame. These are ParticleSystems that spawn a single Particle which just hovers for a few frames and fades away.

Putting this all together (by playing the Effect) you have a classic exploding firework. For more examples, see the other Effects in the particles and fireworks samples.

CPU Considerations

Many people run asciimatics on low-power systems and so care about CPU. However there is a trade-off between CPU usage and responsiveness of any User Interface or the slickness of any animation. Asciimatics tries to handle this for you by looking at when each Effect next wants to be redrawn and only refreshing the Screen when needed.

For most use-cases, this default should be enough for your needs. However, there are a couple of cases where you might need more. The first is very low-power (e.g. SOC) systems where you need to keep CPU usage to a minimum for a widget-based UI. In this case, you can use the reduce_cpu parameter when constructing your Frame.

The other case, is actually the opposite problem - you may find that asciimatics is being too conservative and you need to refresh the Screen before it thinks you need to do so. In this case, you can simply force its hand by calling force_update(), which will force a full refresh of the Screen next time that draw_next_frame() is called.

Using async frameworks

If you cannot allow asciimatics to schedule each frame itself, e.g. because you are using an asynchronous framework like gevent, asyncio or twisted, that’s fine. Asciimatics is designed to run in tiny time slices that are ideal for such a framework. All you need to do is call set_scenes() to set up your scenes and draw_next_frame() (every 1/20 of a second) to draw the next frame.

For example, here is how you can run inside an asyncio event loop.

import asyncio
from asciimatics.effects import Cycle, Stars
from asciimatics.renderers import FigletText
from asciimatics.scene import Scene
from asciimatics.screen import Screen


def update_screen(end_time, loop, screen):
    screen.draw_next_frame()
    if loop.time() < end_time:
        loop.call_later(0.05, update_screen, end_time, loop, screen)
    else:
        loop.stop()


# Define the scene that you'd like to play.
screen = Screen.open()
effects = [
    Cycle(
        screen,
        FigletText("ASCIIMATICS", font='big'),
        screen.height // 2 - 8),
    Cycle(
        screen,
        FigletText("ROCKS!", font='big'),
        screen.height // 2 + 3),
    Stars(screen, (screen.width + screen.height) // 2)
]
screen.set_scenes([Scene(effects, 500)])

# Schedule the first call to display_date()
loop = asyncio.new_event_loop()
end_time = loop.time() + 5.0
loop.call_soon(update_screen, end_time, loop, screen)

# Blocking call interrupted by loop.stop()
loop.run_forever()
loop.close()
screen.close()