1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
|
.. _animation-ref:
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 :py:obj:`.Scene` objects that encompass the key stages of your
animation.
2. One or more :py:obj:`.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 :py:obj:`.Print` that just displays some
rendered text at a specific location for a certain time to :py:obj:`.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 :py:meth:`.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
:py:meth:`~.Effect.update` to every `Effect` and so can 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 :py:obj:`.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 :py:obj:`.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 :py:obj:`.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.
.. code-block:: python
# 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 :py:obj:`.ParticleEffect` is a special Effect designed to draw a `particle
system <https://en.m.wikipedia.org/wiki/Particle_system>`_. It consists of one
or more :py:obj:`.ParticleEmitter` objects which in turn contains one or
more :py:obj:`.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 :py:obj:`.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
``Particle``\ s in such a way that they are explode outwards radially from
where the ``Rocket`` expired.
3. In turn, each of these ``Particle``\ s from the ``StarExplosion`` spawns a
``StarTrail`` on each new frame. These are ``ParticleSystem``\ s 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 :py:obj:`.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 :py:meth:`.force_update`, which will force a full refresh of the
``Screen`` next time that :py:meth:`.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 :py:meth:`.set_scenes` to set up
your scenes and :py:meth:`.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.
.. code-block:: python
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()
|