.. _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 `_. 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()