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