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.
- One or more
Scene
objects that encompass the key stages of your animation. - 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:
- 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()
andmove_round_to()
- to define the path. Just decide on the path and script it by chaining these methods together. - 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 ParticleEmitter
s that
spawn one or more Particle
s, each with a unique set of attributes - e.g.
location, direction, colour, etc. The ParticleEffect
renders a frame by
rendering each of these Particle
s 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.
- The
StarFirework
constructs aRocket
. This is aParticleEmitter
that has just oneParticle
that shoots vertically up the Screen to hit a pre-defined end point. - When this
Particle
hits its end-point, it expires and spawns aStarExplosion
. This is aParticleEmitter
that spawns manyParticle
s in such a way that they are explode outwards radially from where theRocket
expired. - In turn, each of these
Particle
s from theStarExplosion
spawns aStarTrail
on each new frame. These areParticleSystem
s that spawn a singleParticle
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.get_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()