1# -*- coding: utf-8 -*-
2"""
3This module defines `Effects` which can be used for animations.  For more details see
4http://asciimatics.readthedocs.io/en/latest/animation.html
5"""
6from __future__ import division
7from __future__ import absolute_import
8from __future__ import print_function
9from __future__ import unicode_literals
10from builtins import chr
11from builtins import object
12from builtins import range
13from future.utils import with_metaclass
14from abc import ABCMeta, abstractmethod, abstractproperty
15from random import randint, random, choice
16from math import sin, cos, pi
17from asciimatics.paths import DynamicPath
18from asciimatics.screen import Screen
19import datetime
20
21
22class Effect(with_metaclass(ABCMeta, object)):
23    """
24    Abstract class to handle a special effect on the screen.  An Effect can
25    cover anything from a static image at the start of the Scene through to
26    dynamic animations that need to be redrawn for every frame.
27
28    The basic interaction with a :py:obj:`.Scene` is as follows:
29
30    1.  The Scene will register with the Effect when it as added using
31        :py:meth:`.register_scene`.
32    2.  The Scene will call :py:meth:`.Effect.reset` for all Effects when it
33        starts.
34    3.  The Scene will determine the number of frames required (either through
35        explicit configuration or querying :py:obj:`.stop_frame` for every
36        Effect).
37    4.  It will then run the scene, calling :py:meth:`.Effect.update` for
38        each effect that is in the scene.  The base Effect will then call the
39        abstract method _update() if the effect should be visible.
40    5.  If any keys are pressed or the mouse moved/clicked, the scene will call
41        :py:meth:`.Effect.process_event` for each event, allowing the effect to
42        act on it if needed.
43
44    New Effects, therefore need to implement the abstract methods on this
45    class to satisfy the contract with Scene.  Since most effects don't require
46    user interaction, the default process_event() implementation will ignore the
47    event (and so effects don't need to implement this method unless needed).
48    """
49
50    def __init__(self, screen, start_frame=0, stop_frame=0, delete_count=None):
51        """
52        :param screen: The Screen that will render this Effect.
53        :param start_frame: Start index for the effect.
54        :param stop_frame: Stop index for the effect.
55        :param delete_count: Number of frames before this effect is deleted.
56        """
57        self._screen = screen
58        self._start_frame = start_frame
59        self._stop_frame = stop_frame
60        self._delete_count = delete_count
61        self._scene = None
62
63    def update(self, frame_no):
64        """
65        Process the animation effect for the specified frame number.
66
67        :param frame_no: The index of the frame being generated.
68        """
69        if (frame_no >= self._start_frame and
70                (self._stop_frame == 0 or frame_no < self._stop_frame)):
71            self._update(frame_no)
72
73    def register_scene(self, scene):
74        """
75        Register the Scene that owns this Effect.
76
77        :param scene: The Scene to be registered
78        """
79        self._scene = scene
80
81    @abstractmethod
82    def reset(self):
83        """
84        Function to reset the effect when replaying the scene.
85        """
86
87    @abstractmethod
88    def _update(self, frame_no):
89        """
90        This effect will be called every time the mainline animator
91        creates a new frame to display on the screen.
92
93        :param frame_no: The index of the frame being generated.
94        """
95
96    @abstractproperty
97    def stop_frame(self):
98        """
99        Last frame for this effect.  A value of zero means no specific end.
100        """
101
102    @property
103    def delete_count(self):
104        """
105        The number of frames before this Effect should be deleted.
106        """
107        return self._delete_count
108
109    @property
110    def screen(self):
111        """
112        The Screen that will render this Effect.
113        """
114        return self._screen
115
116    @delete_count.setter
117    def delete_count(self, value):
118        self._delete_count = value
119
120    @property
121    def frame_update_count(self):
122        """
123        The number of frames before this Effect should be updated.
124
125        Increasing this number potentially reduces the CPU load of a Scene (if
126        no other Effect needs to be scheduled sooner), but can affect perceived
127        responsiveness of the Scene if it is too long.  Handle with care!
128
129        A value of 0 means refreshes are not required beyond a response to an
130        input event.  It defaults to 1 for all Effects.
131        """
132        return 1
133
134    @property
135    def safe_to_default_unhandled_input(self):
136        """
137        Whether it is safe to use the default handler for any unhandled input
138        from this Effect.
139
140        A value of False means that asciimatics should not use the default
141        handler.  This is typically the case for Frames.
142        """
143        return True
144
145    @property
146    def scene(self):
147        """
148        The Scene that owns this Effect.
149        """
150        return self._scene
151
152    # pylint: disable=no-self-use
153    def process_event(self, event):
154        """
155        Process any input event.
156
157        :param event: The event that was triggered.
158        :returns: None if the Effect processed the event, else the original
159                  event.
160        """
161        return event
162
163
164class Scroll(Effect):
165    """
166    Special effect to scroll the screen up at a required rate.  Since the Screen
167    has a limited size and will not wrap, ensure that it is large enough to
168    Scroll for the desired time.
169    """
170
171    def __init__(self, screen, rate, **kwargs):
172        """
173        :param screen: The Screen being used for the Scene.
174        :param rate: How many frames to wait between scrolling the screen.
175
176        Also see the common keyword arguments in :py:obj:`.Effect`.
177        """
178        super(Scroll, self).__init__(screen, **kwargs)
179        self._rate = rate
180        self._last_frame = None
181
182    def reset(self):
183        self._last_frame = 0
184
185    def _update(self, frame_no):
186        if (frame_no - self._last_frame) >= self._rate:
187            self._screen.scroll()
188            self._last_frame = frame_no
189
190    @property
191    def stop_frame(self):
192        return 0
193
194
195class Cycle(Effect):
196    """
197    Special effect to cycle the colours on some specified text from a
198    Renderer.  The text is automatically centred to the width of the Screen.
199    This effect is not compatible with multi-colour rendered text.
200    """
201
202    def __init__(self, screen, renderer, y, **kwargs):
203        """
204        :param screen: The Screen being used for the Scene.
205        :param renderer: The Renderer which is to be cycled.
206        :param y: The line (y coordinate) for the start of the text.
207
208        Also see the common keyword arguments in :py:obj:`.Effect`.
209        """
210        super(Cycle, self).__init__(screen, **kwargs)
211        self._renderer = renderer
212        self._y = y
213        self._colour = 0
214
215    def reset(self):
216        pass
217
218    def _update(self, frame_no):
219        if frame_no % 2 == 0:
220            return
221
222        y = self._y
223        image, _ = self._renderer.rendered_text
224        for line in image:
225            if self._screen.is_visible(0, y):
226                self._screen.centre(line, y, self._colour)
227            y += 1
228        self._colour = (self._colour + 1) % 8
229
230    @property
231    def stop_frame(self):
232        return 0
233
234
235class BannerText(Effect):
236    """
237    Special effect to scroll some text (from a Renderer) horizontally like a
238    banner.
239    """
240
241    def __init__(self, screen, renderer, y, colour, bg=Screen.COLOUR_BLACK,
242                 **kwargs):
243        """
244        :param screen: The Screen being used for the Scene.
245        :param renderer: The renderer to be scrolled
246        :param y: The line (y coordinate) for the start of the text.
247        :param colour: The default foreground colour to use for the text.
248        :param bg: The default background colour to use for the text.
249
250        Also see the common keyword arguments in :py:obj:`.Effect`.
251        """
252        super(BannerText, self).__init__(screen, **kwargs)
253        self._renderer = renderer
254        self._y = y
255        self._colour = colour
256        self._bg = bg
257        self._text_pos = None
258        self._scr_pos = None
259
260    def reset(self):
261        self._text_pos = 0
262        self._scr_pos = self._screen.width
263
264    def _update(self, frame_no):
265        if self._scr_pos == 0 and self._text_pos < self._renderer.max_width:
266            self._text_pos += 1
267
268        if self._scr_pos > 0:
269            self._scr_pos -= 1
270
271        image, colours = self._renderer.rendered_text
272        for (i, line) in enumerate(image):
273            line += " "
274            colours[i].append((self._colour, 2, self._bg))
275            end_pos = min(
276                len(line),
277                self._text_pos + self._screen.width - self._scr_pos)
278            self._screen.paint(line[self._text_pos:end_pos],
279                               self._scr_pos,
280                               self._y + i,
281                               self._colour,
282                               bg=self._bg,
283                               colour_map=colours[i][self._text_pos:end_pos])
284
285    @property
286    def stop_frame(self):
287        return self._start_frame + self._renderer.max_width + self._screen.width
288
289
290class Print(Effect):
291    """
292    Special effect that simply prints the specified text (from a Renderer) at
293    the required location.
294    """
295
296    def __init__(self, screen, renderer, y, x=None, colour=7, attr=0, bg=0,
297                 clear=False, transparent=True, speed=4, **kwargs):
298        """
299        :param screen: The Screen being used for the Scene.
300        :param renderer: The renderer to be printed.
301        :param x: The column (x coordinate) for the start of the text.
302            If not specified, defaults to centring the text on screen.
303        :param y: The line (y coordinate) for the start of the text.
304        :param colour: The foreground colour to use for the text.
305        :param attr: The colour attribute to use for the text.
306        :param bg: The background colour to use for the text.
307        :param clear: Whether to clear the text before stopping.
308        :param transparent: Whether to print spaces (and so be able to overlay other Effects).
309            If False, this will redraw all characters and so replace any Effect underneath it.
310        :param speed: The refresh rate in frames between refreshes.
311
312        Note that a speed of 1 will force the Screen to redraw the Effect every frame update, while a value
313        of 0 will redraw on demand - i.e. will redraw every time that an update is required by another Effect.
314
315        Also see the common keyword arguments in :py:obj:`.Effect`.
316        """
317        super(Print, self).__init__(screen, **kwargs)
318        self._renderer = renderer
319        self._transparent = transparent
320        self._y = y
321        self._x = ((self._screen.width - renderer.max_width) // 2 if x is None
322                   else x)
323        self._colour = colour
324        self._attr = attr
325        self._bg = bg
326        self._clear = clear
327        self._speed = speed
328        self._frame_no = 0
329
330    def reset(self):
331        pass  # Nothing required
332
333    def _update(self, frame_no):
334        self._frame_no = frame_no
335        if self._clear and \
336                (frame_no == self._stop_frame - 1) or (self._delete_count == 1):
337            for i in range(0, self._renderer.max_height):
338                self._screen.print_at(" " * self._renderer.max_width,
339                                      self._x,
340                                      self._y + i,
341                                      bg=self._bg)
342        elif self._speed == 0 or frame_no % self._speed == 0:
343            image, colours = self._renderer.rendered_text
344            for (i, line) in enumerate(image):
345                self._screen.paint(line, self._x, self._y + i, self._colour,
346                                   attr=self._attr,
347                                   bg=self._bg,
348                                   transparent=self._transparent,
349                                   colour_map=colours[i])
350
351    @property
352    def stop_frame(self):
353        return self._stop_frame
354
355    @property
356    def frame_update_count(self):
357        # Only demand update for next update frame.
358        return self._speed - (self._frame_no % self._speed) if self._speed > 0 else 1000000
359
360
361class Mirage(Effect):
362    """
363    Special effect to make bits of the specified text appear over time.  This
364    text is automatically centred on the screen.
365    """
366
367    def __init__(self, screen, renderer, y, colour, **kwargs):
368        """
369        :param screen: The Screen being used for the Scene.
370        :param renderer: The renderer to be displayed.
371        :param y: The line (y coordinate) for the start of the text.
372        :param colour: The colour attribute to use for the text.
373
374        Also see the common keyword arguments in :py:obj:`.Effect`.
375        """
376        super(Mirage, self).__init__(screen, **kwargs)
377        self._renderer = renderer
378        self._y = y
379        self._colour = colour
380        self._count = 0
381
382    def reset(self):
383        self._count = 0
384
385    def _update(self, frame_no):
386        if frame_no % 2 == 0:
387            return
388
389        y = self._y
390        image, colours = self._renderer.rendered_text
391        for i, line in enumerate(image):
392            if self._screen.is_visible(0, y):
393                x = (self._screen.width - len(line)) // 2
394                for j, c in enumerate(line):
395                    if c != " " and random() > 0.85:
396                        if colours[i][j][0] is not None:
397                            self._screen.print_at(c, x, y,
398                                                  colours[i][j][0],
399                                                  colours[i][j][1])
400                        else:
401                            self._screen.print_at(c, x, y, self._colour)
402                    x += 1
403            y += 1
404
405    @property
406    def stop_frame(self):
407        return self._stop_frame
408
409
410class _Star(object):
411    """
412    Simple class to represent a single star for the Stars special effect.
413    """
414
415    def __init__(self, screen, pattern):
416        """
417        :param screen: The Screen being used for the Scene.
418        :param pattern: The pattern to loop through
419        """
420        self._screen = screen
421        self._star_chars = pattern
422        self._cycle = None
423        self._old_char = None
424        self._respawn()
425
426    def _respawn(self):
427        """
428        Pick a random location for the star making sure it does
429        not overwrite an existing piece of text.
430        """
431        self._cycle = randint(0, len(self._star_chars))
432        (height, width) = self._screen.dimensions
433        while True:
434            self._x = randint(0, width - 1)
435            self._y = self._screen.start_line + randint(0, height - 1)
436            if self._screen.get_from(self._x, self._y)[0] == 32:
437                break
438        self._old_char = " "
439
440    def update(self):
441        """
442        Draw the star.
443        """
444        if not self._screen.is_visible(self._x, self._y):
445            self._respawn()
446
447        cur_char, _, _, _ = self._screen.get_from(self._x, self._y)
448        if cur_char not in (ord(self._old_char), 32):
449            self._respawn()
450
451        self._cycle += 1
452        if self._cycle >= len(self._star_chars):
453            self._cycle = 0
454
455        new_char = self._star_chars[self._cycle]
456        if new_char == self._old_char:
457            return
458
459        self._screen.print_at(new_char, self._x, self._y)
460        self._old_char = new_char
461
462
463class Stars(Effect):
464    """
465    Add random stars to the screen and make them twinkle.
466    """
467
468    def __init__(self, screen, count, pattern="..+..   ...x...  ...*...         ", **kwargs):
469        """
470        :param screen: The Screen being used for the Scene.
471        :param count: The number of starts to create.
472        :param pattern: The string pattern for the stars to loop through
473
474        Also see the common keyword arguments in :py:obj:`.Effect`.
475        """
476        super(Stars, self).__init__(screen, **kwargs)
477        self._pattern = pattern
478        self._max = count
479        self._stars = []
480
481    def reset(self):
482        self._stars = [_Star(self._screen, self._pattern) for _ in range(self._max)]
483
484    def _update(self, frame_no):
485        for star in self._stars:
486            star.update()
487
488    @property
489    def stop_frame(self):
490        return 0
491
492
493class _Trail(object):
494    """
495    Track a single trail  for a falling character effect (a la Matrix).
496    """
497
498    def __init__(self, screen, x):
499        """
500        :param screen: The Screen being used for the Scene.
501        :param x: The column (y coordinate) for this trail to use.
502        """
503        self._screen = screen
504        self._x = x
505        self._y = 0
506        self._life = 0
507        self._rate = 0
508        self._clear = True
509        self._maybe_reseed(True)
510
511    def _maybe_reseed(self, normal):
512        """
513        Randomly create a new column once this one is finished.
514        """
515        self._y += self._rate
516        self._life -= 1
517        if self._life <= 0:
518            self._clear = not self._clear if normal else True
519            self._rate = randint(1, 2)
520            if self._clear:
521                self._y = 0
522                self._life = self._screen.height // self._rate
523            else:
524                self._y = randint(0, self._screen.height // 2) - \
525                    self._screen.height // 4
526                self._life = \
527                    randint(1, self._screen.height - self._y) // self._rate
528
529    def update(self, reseed):
530        """
531        Update that trail!
532
533        :param reseed: Whether we are in the normal reseed cycle or not.
534        """
535        if self._clear:
536            for i in range(0, 3):
537                self._screen.print_at(" ",
538                                      self._x,
539                                      self._screen.start_line + self._y + i)
540            self._maybe_reseed(reseed)
541        else:
542            for i in range(0, 3):
543                self._screen.print_at(chr(randint(32, 126)),
544                                      self._x,
545                                      self._screen.start_line + self._y + i,
546                                      Screen.COLOUR_GREEN)
547            for i in range(4, 6):
548                self._screen.print_at(chr(randint(32, 126)),
549                                      self._x,
550                                      self._screen.start_line + self._y + i,
551                                      Screen.COLOUR_GREEN,
552                                      Screen.A_BOLD)
553            self._maybe_reseed(reseed)
554
555
556class Matrix(Effect):
557    """
558    Matrix-like falling green letters.
559    """
560
561    def __init__(self, screen, **kwargs):
562        """
563        :param screen: The Screen being used for the Scene.
564
565        Also see the common keyword arguments in :py:obj:`.Effect`.
566        """
567        super(Matrix, self).__init__(screen, **kwargs)
568        self._chars = []
569
570    def reset(self):
571        self._chars = [_Trail(self._screen, x) for x in
572                       range(self._screen.width)]
573
574    def _update(self, frame_no):
575        if frame_no % 2 == 0:
576            for char in self._chars:
577                char.update((self._stop_frame == 0) or (
578                    self._stop_frame - frame_no > 100))
579
580    @property
581    def stop_frame(self):
582        return self._stop_frame
583
584
585class Wipe(Effect):
586    """
587    Wipe the screen down from top to bottom.
588    """
589
590    def __init__(self, screen, bg=0, **kwargs):
591        """
592        :param screen: The Screen being used for the Scene.
593        :param bg: Optional background colour to use for the wipe.
594
595        Also see the common keyword arguments in :py:obj:`.Effect`.
596        """
597        super(Wipe, self).__init__(screen, **kwargs)
598        self._bg = bg
599        self._y = None
600
601    def reset(self):
602        self._y = 0
603
604    def _update(self, frame_no):
605        if frame_no % 2 == 0:
606            if self._screen.is_visible(0, self._y):
607                self._screen.print_at(
608                    " " * self._screen.width, 0, self._y, bg=self._bg)
609            self._y += 1
610
611    @property
612    def stop_frame(self):
613        return self._stop_frame
614
615
616class Sprite(Effect):
617    """
618    An animated character capable of following a path around the screen.
619    """
620
621    def __init__(self, screen, renderer_dict, path, colour=Screen.COLOUR_WHITE,
622                 clear=True, **kwargs):
623        """
624        :param screen: The Screen being used for the Scene.
625        :param renderer_dict: A dictionary of Renderers to use for displaying
626                              the Sprite.
627        :param path: The Path for the Sprite to follow.
628        :param colour: The colour to use to render the Sprite.
629        :param clear: Whether to clear out old images or leave a trail.
630
631        Also see the common keyword arguments in :py:obj:`.Effect`.
632        """
633        super(Sprite, self).__init__(screen, **kwargs)
634        self._renderer_dict = renderer_dict
635        self._path = path
636        self._index = None
637        self._colour = colour
638        self._clear = clear
639        self._old_height = None
640        self._old_width = None
641        self._old_x = None
642        self._old_y = None
643        self._dir_count = 0
644        self._dir_x = None
645        self._dir_y = None
646        self._old_direction = None
647        self.reset()
648
649    def reset(self):
650        self._dir_count = 0
651        self._dir_x = None
652        self._dir_y = None
653        self._old_x = None
654        self._old_y = None
655        self._old_direction = None
656        self._path.reset()
657
658    def last_position(self):
659        """
660        Returns the last position of this Sprite as a tuple
661        (x, y, width, height).
662        """
663        return self._old_x, self._old_y, self._old_width, self._old_height
664
665    def overlaps(self, other, use_new_pos=False):
666        """
667        Check whether this Sprite overlaps another.
668
669        :param other: The other Sprite to check for an overlap.
670        :param use_new_pos: Whether to use latest position (due to recent
671            update).  Defaults to False.
672        :returns: True if the two Sprites overlap.
673        """
674        (x, y) = self._path.next_pos() if use_new_pos else (self._old_x,
675                                                            self._old_y)
676        w = self._old_width
677        h = self._old_height
678
679        x2, y2, w2, h2 = other.last_position()
680
681        if ((x > x2 + w2 - 1) or (x2 > x + w - 1) or
682                (y > y2 + h2 - 1) or (y2 > y + h - 1)):
683            return False
684        else:
685            return True
686
687    def _update(self, frame_no):
688        if frame_no % 2 == 0:
689            # Blank out the old sprite if moved.
690            if (self._clear and
691                    self._old_x is not None and self._old_y is not None):
692                for i in range(0, self._old_height):
693                    self._screen.print_at(
694                        " " * self._old_width, self._old_x, self._old_y + i, 0)
695
696            # Don't draw a new one if we're about to stop the Sprite.
697            if self._delete_count is not None and self._delete_count <= 2:
698                return
699
700            # Figure out the direction of the sprite, if enough time has
701            # elapsed.
702            (x, y) = self._path.next_pos()
703            if self._dir_count % 3 == 0:
704                direction = None
705                if self._dir_x is not None:
706                    dx = (x - self._dir_x) // 2
707                    dy = y - self._dir_y
708                    if dx * dx > dy * dy:
709                        direction = "left" if dx < 0 else "right"
710                    elif dx == 0 and dy == 0:
711                        direction = "default"
712                    else:
713                        direction = "up" if dy < 0 else "down"
714                self._dir_x = x
715                self._dir_y = y
716            else:
717                direction = self._old_direction
718            self._dir_count += 1
719
720            # If no data - pick the default
721            if direction not in self._renderer_dict:
722                direction = "default"
723
724            # Now we've done the directions, centre the sprite on the path.
725            x -= self._renderer_dict[direction].max_width // 2
726            y -= self._renderer_dict[direction].max_height // 2
727
728            # Update the path index for the sprite if needed.
729            if self._path.is_finished():
730                self._path.reset()
731
732            # Draw the new sprite.
733            # self._screen.print_at(str(x)+","+str(y)+" ", 0, 0)
734            image, colours = self._renderer_dict[direction].rendered_text
735            for (i, line) in enumerate(image):
736                self._screen.paint(line, x, y + i, self._colour,
737                                   colour_map=colours[i])
738
739            # Remember what we need to clear up next frame.
740            self._old_width = self._renderer_dict[direction].max_width
741            self._old_height = self._renderer_dict[direction].max_height
742            self._old_direction = direction
743            self._old_x = x
744            self._old_y = y
745
746    @property
747    def stop_frame(self):
748        return self._stop_frame
749
750    def process_event(self, event):
751        if isinstance(self._path, DynamicPath):
752            return self._path.process_event(event)
753        else:
754            return event
755
756
757class _Flake(object):
758    """
759    Track a single snow flake.
760    """
761
762    _snow_chars = ".+*"
763    _drift_chars = " ,;#@"
764
765    def __init__(self, screen):
766        """
767        :param screen: The Screen being used for the Scene.
768        """
769        self._screen = screen
770        self._x = 0
771        self._y = 0
772        self._rate = 0
773        self._char = None
774        self._reseed()
775
776    def _reseed(self):
777        """
778        Randomly create a new snowflake once this one is finished.
779        """
780        self._char = choice(self._snow_chars)
781        self._rate = randint(1, 3)
782        self._x = randint(0, self._screen.width - 1)
783        self._y = self._screen.start_line + randint(0, self._rate)
784
785    def update(self, reseed):
786        """
787        Update that snowflake!
788
789        :param reseed: Whether we are in the normal reseed cycle or not.
790        """
791        self._screen.print_at(" ", self._x, self._y)
792        cell = None
793        for _ in range(self._rate):
794            self._y += 1
795            cell = self._screen.get_from(self._x, self._y)
796            if cell is None or cell[0] != 32:
797                break
798
799        if ((cell is not None and cell[0] in [ord(x) for x in self._snow_chars + " "]) and
800                (self._y < self._screen.start_line + self._screen.height)):
801            self._screen.print_at(self._char,
802                                  self._x,
803                                  self._y)
804        else:
805            if self._y > self._screen.start_line + self._screen.height:
806                self._y = self._screen.start_line + self._screen.height
807
808            drift_index = -1
809            if cell:
810                drift_index = self._drift_chars.find(chr(cell[0]))
811            if 0 <= drift_index < len(self._drift_chars) - 1:
812                drift_char = self._drift_chars[drift_index + 1]
813                self._screen.print_at(drift_char, self._x, self._y)
814            else:
815                self._screen.print_at(",", self._x, self._y - 1)
816            if reseed:
817                self._reseed()
818
819
820class Snow(Effect):
821    """
822    Settling snow effect.
823    """
824
825    def __init__(self, screen, **kwargs):
826        """
827        :param screen: The Screen being used for the Scene.
828
829        Also see the common keyword arguments in :py:obj:`.Effect`.
830        """
831        super(Snow, self).__init__(screen, **kwargs)
832        self._chars = []
833
834    def reset(self):
835        # Make the snow start falling one flake at a time.
836        self._chars = []
837
838    def _update(self, frame_no):
839        if frame_no % 3 == 0:
840            if len(self._chars) < self._screen.width // 3:
841                self._chars.append(_Flake(self._screen))
842
843            for char in self._chars:
844                char.update((self._stop_frame == 0) or (
845                    self._stop_frame - frame_no > 100))
846
847    @property
848    def stop_frame(self):
849        return self._stop_frame
850
851
852class Clock(Effect):
853    """
854    An ASCII ticking clock (telling the correct local time).
855    """
856
857    def __init__(self, screen, x, y, r, bg=Screen.COLOUR_BLACK, **kwargs):
858        """
859        :param screen: The Screen being used for the Scene.
860        :param x: X coordinate for the centre of the clock.
861        :param y: Y coordinate for the centre of the clock.
862        :param r: Radius of the clock.
863        :param bg: Background colour for the clock.
864
865        Also see the common keyword arguments in :py:obj:`.Effect`.
866        """
867        super(Clock, self).__init__(screen, **kwargs)
868        self._x = x
869        self._y = y
870        self._r = r
871        self._bg = bg
872        self._old_time = None
873
874    def reset(self):
875        pass
876
877    def _update(self, frame_no):
878        # Helper functions to map various time elements
879        def _hour_pos(t):
880            return (t.tm_hour + t.tm_min / 60) * pi / 6
881
882        def _min_pos(t):
883            return t.tm_min * pi / 30
884
885        def _sec_pos(t):
886            return t.tm_sec * pi / 30
887
888        # Clear old hands
889        if self._old_time is not None:
890            ot = self._old_time
891            self._screen.move(self._x, self._y)
892            self._screen.draw(self._x + (self._r * sin(_hour_pos(ot))),
893                              self._y - (self._r * cos(_hour_pos(ot)) / 2),
894                              char=" ", bg=self._bg)
895            self._screen.move(self._x, self._y)
896            self._screen.draw(self._x + (self._r * sin(_min_pos(ot)) * 2),
897                              self._y - (self._r * cos(_min_pos(ot))),
898                              char=" ", bg=self._bg)
899            self._screen.move(self._x, self._y)
900            self._screen.draw(self._x + (self._r * sin(_sec_pos(ot)) * 2),
901                              self._y - (self._r * cos(_sec_pos(ot))),
902                              char=" ", bg=self._bg)
903
904        # Draw new ones
905        new_time = datetime.datetime.now().timetuple()
906        self._screen.move(self._x, self._y)
907        self._screen.draw(self._x + (self._r * sin(_hour_pos(new_time))),
908                          self._y - (self._r * cos(_hour_pos(new_time)) / 2),
909                          colour=Screen.COLOUR_WHITE, bg=self._bg)
910        self._screen.move(self._x, self._y)
911        self._screen.draw(self._x + (self._r * sin(_min_pos(new_time)) * 2),
912                          self._y - (self._r * cos(_min_pos(new_time))),
913                          colour=Screen.COLOUR_WHITE, bg=self._bg)
914        self._screen.move(self._x, self._y)
915        self._screen.draw(self._x + (self._r * sin(_sec_pos(new_time)) * 2),
916                          self._y - (self._r * cos(_sec_pos(new_time))),
917                          colour=Screen.COLOUR_CYAN, bg=self._bg, thin=True)
918        self._screen.print_at("o", self._x, self._y, Screen.COLOUR_YELLOW,
919                              Screen.A_BOLD, bg=self._bg)
920        self._old_time = new_time
921
922    @property
923    def stop_frame(self):
924        return self._stop_frame
925
926    @property
927    def frame_update_count(self):
928        # Only need to update once a second
929        return 20
930
931
932class Cog(Effect):
933    """
934    A rotating cog.
935    """
936
937    def __init__(self, screen, x, y, radius, direction=1, colour=7, **kwargs):
938        """
939        :param screen: The Screen being used for the Scene.
940        :param x: X coordinate of the centre of the cog.
941        :param y: Y coordinate of the centre of the cog.
942        :param radius: The radius of the cog.
943        :param direction: The direction of rotation. Positive numbers are
944            anti-clockwise, negative numbers clockwise.
945        :param colour: The colour of the cog.
946
947        Also see the common keyword arguments in :py:obj:`.Effect`.
948        """
949        super(Cog, self).__init__(screen, **kwargs)
950        self._x = x
951        self._y = y
952        self._radius = radius
953        self._old_frame = 0
954        self._rate = 2
955        self._direction = direction
956        self._colour = colour
957
958    def reset(self):
959        pass
960
961    def _update(self, frame_no):
962        # Rate limit the animation
963        if frame_no % self._rate != 0:
964            return
965
966        # Function to plot.
967        def f(p):
968            return self._x + (self._radius * 2 - (6 * (p // 4 % 2))) * sin(
969                (self._old_frame + p) * pi / 40)
970
971        def g(p):
972            return self._y + (self._radius - (3 * (p // 4 % 2))) * cos(
973                (self._old_frame + p) * pi / 40)
974
975        # Clear old wave.
976        if self._old_frame != 0:
977            self._screen.move(f(0), g(0))
978            for x in range(81):
979                self._screen.draw(f(x), g(x), char=" ")
980
981        # Draw new one
982        self._old_frame += self._direction
983        self._screen.move(f(0), g(0))
984        for x in range(81):
985            self._screen.draw(f(x), g(x), colour=self._colour)
986
987    @property
988    def stop_frame(self):
989        return self._stop_frame
990
991
992class RandomNoise(Effect):
993    """
994    White noise effect - like an old analogue TV set that isn't quite tuned
995    right.  If desired, a signal image (from a renderer) can be specified that
996    will appear from the noise.
997    """
998
999    def __init__(self, screen, signal=None, jitter=6, **kwargs):
1000        """
1001        :param screen: The Screen being used for the Scene.
1002        :param signal: The renderer to use as the 'signal' in the white noise.
1003        :param jitter: The amount that the signal will jump when there is noise.
1004
1005        Also see the common keyword arguments in :py:obj:`.Effect`.
1006        """
1007        super(RandomNoise, self).__init__(screen, **kwargs)
1008        self._signal = signal
1009        self._strength = 0.0
1010        self._step = 0.0
1011        self._jitter = jitter
1012
1013    def reset(self):
1014        self._strength = 0.0
1015        self._step = -0.01
1016
1017    def _update(self, frame_no):
1018        if self._signal:
1019            start_x = int((self._screen.width - self._signal.max_width) // 2)
1020            start_y = int((self._screen.height - self._signal.max_height) // 2)
1021            text, colours = self._signal.rendered_text
1022        else:
1023            start_x = start_y = 0
1024            text, colours = "", []
1025
1026        for y in range(self._screen.height):
1027            if self._strength < 1.0:
1028                jitter = int(self._jitter - self._jitter * self._strength)
1029                offset = jitter - 2 * randint(0, jitter)
1030            else:
1031                offset = 0
1032            for x in range(self._screen.width):
1033                ix = x - start_x
1034                iy = y - start_y
1035                if (self._signal and random() <= self._strength and
1036                        x >= start_x and y >= start_y and
1037                        iy < len(text) and 0 <= ix < len(text[iy])):
1038                    self._screen.paint(text[iy][ix],
1039                                       x + offset, y,
1040                                       colour_map=[colours[iy][ix]])
1041                else:
1042                    if random() < 0.2:
1043                        self._screen.print_at(chr(randint(33, 126)), x, y)
1044
1045        # Tune the signal
1046        self._strength += self._step
1047        if self._strength >= 1.25 or self._strength <= -0.5:
1048            self._step = -self._step
1049
1050    @property
1051    def stop_frame(self):
1052        return self._stop_frame
1053
1054
1055class Julia(Effect):
1056    """
1057    Julia Set generator.  See http://en.wikipedia.org/wiki/Julia_set for more
1058    information on this fractal.
1059    """
1060
1061    # Character set to use so we still get a grey scale for low-colour systems.
1062    _greyscale = '@@&&99##GGHHhh3322AAss;;::.. '
1063
1064    # Colour palette for 256 colour xterm mode.
1065    _256_palette = [196, 202, 208, 214, 220, 226,
1066                    154, 118, 82, 46,
1067                    47, 48, 49, 50, 51,
1068                    45, 39, 33, 27, 21,
1069                    57, 93, 129, 201,
1070                    200, 199, 198, 197, 0]
1071
1072    def __init__(self, screen, c=None, **kwargs):
1073        """
1074        :param screen: The Screen being used for the Scene.
1075        :param c: The starting value of 'c' for the Julia Set.
1076
1077        Also see the common keyword arguments in :py:obj:`.Effect`.
1078        """
1079        super(Julia, self).__init__(screen, **kwargs)
1080        self._width = screen.width
1081        self._height = screen.height
1082        self._centre = [0.0, 0.0]
1083        self._size = [4.0, 4.0]
1084        self._min_x = self._min_y = -2.0
1085        self._max_x = self._max_y = 2.0
1086        self._c = c if c is not None else [-0.8, 0.156]
1087        self._scale = 0.995
1088
1089    def reset(self):
1090        pass
1091
1092    def _update(self, frame_no):
1093        # Draw the new image to the required block.
1094        c = complex(self._c[0], self._c[1])
1095        sx = self._centre[0] - (self._size[0] / 2.0)
1096        sy = self._centre[1] - (self._size[1] / 2.0)
1097        for y in range(self._height):
1098            for x in range(self._width):
1099                z = complex(sx + self._size[0] * (x / self._width),
1100                            sy + self._size[1] * (y / self._height))
1101                n = len(self._256_palette)
1102                while abs(z) < 10 and n >= 1:
1103                    z = z ** 2 + c
1104                    n -= 1
1105                colour = \
1106                    self._256_palette[
1107                        n - 1] if self._screen.colours >= 256 else 7
1108                self._screen.print_at(self._greyscale[n - 1], x, y, colour)
1109
1110        # Zoom
1111        self._size = [i * self._scale for i in self._size]
1112        area = self._size[0] * self._size[1]
1113        if area <= 4.0 or area >= 16:
1114            self._scale = 1.0 / self._scale
1115
1116        # Rotate
1117        self._c = [self._c[0] * cos(pi / 180) - self._c[1] * sin(pi / 180),
1118                   self._c[0] * sin(pi / 180) + self._c[1] * cos(pi / 180)]
1119
1120    @property
1121    def stop_frame(self):
1122        return self._stop_frame
1123
1124
1125class Background(Effect):
1126    """
1127    Effect to be used as a Desktop background.  This sets the background to the specified
1128    colour.
1129    """
1130
1131    def __init__(self, screen, bg=0, **kwargs):
1132        """
1133        :param screen: The Screen being used for the Scene.
1134        :param bg: Optional colour for the background.
1135
1136        Also see the common keyword arguments in :py:obj:`.Effect`.
1137        """
1138        super(Background, self).__init__(screen, **kwargs)
1139        self._bg = bg
1140
1141    def reset(self):
1142        pass
1143
1144    def _update(self, frame_no):
1145        self._screen.clear_buffer(7, 0, self._bg)
1146
1147    @property
1148    def frame_update_count(self):
1149        return 1000000
1150
1151    @property
1152    def stop_frame(self):
1153        return self._stop_frame
1154