pax_global_header00006660000000000000000000000064150720067710014517gustar00rootroot0000000000000052 comment=d56b092e2a81bc151a4fd71880a71a97a97665cc terminaltexteffects-release-0.12.1/000077500000000000000000000000001507200677100172765ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/.gitattributes000066400000000000000000000000301507200677100221620ustar00rootroot00000000000000docs/img/* export-ignoreterminaltexteffects-release-0.12.1/.gitignore000066400000000000000000000063071507200677100212740ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ .tte_venv # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ notes.txt terminaltexteffects/test_tte.py */*.stats *.prof github_resources/* *.png *.gv effect_test.py effect_dev.py effect_canvas_test.py .vscode/* lib_tests/* dev_notes/* 38_venv/*terminaltexteffects-release-0.12.1/CHANGELOG.md000066400000000000000000001041561507200677100211160ustar00rootroot00000000000000# Change Log --- ## 0.12.1 --- ### Bug Fixes (0.12.1) --- * Fixed bug in ArgField caused by Field init signature change in Python 3.14. This class and parent module will be removed in 0.13.0. ## 0.12.0 --- ### New Features (0.12.0) --- #### New Effects (0.12.0) * Highlight - Run a specular highlight across the text. Highlight direction, brightness, and width can be specified. * Laseretch - A laser travels across the terminal, etching characters and emitting sparks. * Sweep - Sweep across the canvas to reveal uncolored text, reverse sweep to color the text. #### New Engine Features (0.12.0) * Background color specification is supported throughout the engine. Methods which accept Color arguments expect a `ColorPair` object to specify both the foreground and background color. * New `EventHandler.Action`: `Action.RESET_APPEARANCE` will reset the character appearance to the input character with no modifications. This eliminates the need to make a `Scene` for this purpose. * Existing 8/24 bit color sequences in the input data are parsed and handled by the engine. A new `TerminalConfig` option `--existing-color-handling` is used to control how these sequences are handled. * `easing.eased_step_function()` allows easing functions to be used generically by returning a closure that produces an eased value based on the easing function and step size provided when called. * A new easing function has been added which returns a custom easing fuction based on cubic bezier controls. * Added custom exceptions. ### Changes (0.12.0) --- #### Effects Changes (0.12.0) * Spotlights - The maximum size of the beam is limited to the smaller of the two canvas dimensions and the minimum size is limited to 1. * Spray - Argument spray_volume is limited to 0 < n <= 1. * Colorshift - `--loop` has been renamed `--no-loop`. Looping the gradient is now default. * All effects which apply a gradient across the text build the gradient mapping based on the text dimensions regardless of the canvas size. This fixes truncated gradients where parts of the gradient map were assigned to empty coordinates. * Some effects support dynamic handling of color sequences in the input data. * Blackhole - Star characters changed to ASCII only to improve supported fonts. #### Engine Changes (0.12.0) * Frame rate timing is enforced within the `BaseEffectIterator` when accessing the `frame` property, rather than within the `Terminal` on calls to `print()`. This enables frame timing when iterating without requiring the use of the `terminal_output()` context manager. * The frame rate can be set to `0` to run without a limit. * Removed unused method Segment.get_coord_on_segment(). * Activating a Path with no segments will raise a ValueError. * `base_effect.active_characters` was refactored from a list to a set. * Bezier curves are no longer limited to two control points. Any number of control points can be specified in calls to `Path.new_waypoint()`, however, performance may suffer with large numbers of control points along unique paths. * Caching has been implemented for all geometry functions significantly improving performance in cases where many characters are traveling along the same Path. * Reorganized the most common API class imports up to the package level. * Moved the SyncMetric Enum from the Animation module top level into the Scene class. * `Scene.apply_gradient_symbols()` accepts two gradients, one for the foreground and one for the background. ### Bug Fixes (0.12.0) --- #### Effects Fixes (0.12.0) * VHSTape - Fixed glitch wave lines not appearing for some canvas/input_text size ratios. * Fireworks - Fixed launch_delay set to 0 causing an infinite loop. * Spotlights - Fixed infinite loop caused by very small beam_width_ratio values. * Overflow - Fixed effect ignoring `--final-gradient-direction` argument. #### Engine Fixes (0.12.0) * Fixed Color() objects not treating rgb colors initialized with/without the hash as equal. Ex: Color('#ffffff') and Color('ffffff') * Gradients initialized with a tuple of steps including the value 0 will raise a ValueError as expected. Ex: Gradient(Color('ff0000'), Color('00ff00'), Color('0000ff'), steps=(4,0)) * Fixed infinite loop when a new scene is created without an id and a scene has been deleted resuling in the length of the scenes dict corresponding to an existing scene id. * Fixed `Canvas` center calculations being off by one for odd widths/heights due to floor division. * Fixed `Gradient.get_color_at_fraction` rounding resulting in over-representing colors in the middle of the spectrum. * `Gradient.build_coordinate_color_mapping` signature changed to required full bounding box specification. This allows the effect to selectively build based on the text/canvas/terminal dimensions and reduces build time by by reducing the map size when possible. * Adds a call to `ansitools.dec_save_cursor_position` after each call to `ansitools.dec_restore_cursor_position` to address some terminals clearing the saved data after the restore. #### Other (0.12.0) * Fixed Canvas width/height docstrings and help output to correctly indicate 0/-1 matching terminal device/input text. --- ## 0.11.0 --- ### New Features (0.11.0) --- #### New Effects (0.11.0) * Matrix effect. Matrix digital rain effect with that ends with a final curtain and character resolve phase. #### New Engine Features (0.11.0) * Canvas is now arbitrarily sizeable. (`-1` matches the input text dimension, `0` matches the terminal dimension) * Canvas can be anchored around the terminal. * Text can be anchored around the Canvas. * Canvas new attributes `text_[left/right/top/bottom]` and `text_width/height` and `text_center_row/center_column`. * Version switch (--version, -v) ### Changes (0.11.0) --- #### Effects Changes (0.11.0) * Slice effect calculates the center of the text absolutely rather than by average line length. * Print effect no longer moves the print head to the start of each line, only back to the first character on the next line. * Many effects were updated to support anchoring within the Canvas. #### Engine Changes (0.11.0) * Performance improvements to geometry functions related to circles. (10.0.1) * Gradient's support indexing and slicing. * EffectCharacter objects no longer have a `symbol` attribute. Instead, the `Animation` class has a new attribute `current_character_visual` which provides access to a `symbol` and `color` attribute reflecting the character's current symbol and color. The prior `EffectCharacter.symbol` attribute was unreliable and represented both a formatted and unformatted symbol depending on when it was accessed. In addition, the `color` attribute is now a `Color` object and the color code has been moved into the `_color_code` attribute. * EffectCharacter objects have a new attribute `is_fill_character: bool`. --- #### Effects Fixes (0.11.0) * Fixed swarm effect not handling the first swarm (bottom right characters) resulting in missing characters in the output. (10.0.1) #### Other (0.11.0) * Keyboard Interrupts are handled gracefully while effects are animating. --- ## 0.10.1 --- ### Changes (0.10.1) --- #### Engine Changes (0.10.1) * Performance improvements to geometry functions related to circles. ### Bug Fixes (0.10.1) * Fixed swarm effect not handling the first swarm (bottom right characters) resulting in missing characters in the output. --- ## 0.10.0 --- ### New Features (0.10.0) --- #### New Effects (0.10.0) * ColorShift: Display a gradient that shifts colors across the terminal. Supports standing and traveling gradients in the following directions: vertical, horizontal, diagonal, radial. The final gradient appearance is optional using the --skips-final-gradient argument. This effect supports infinite looping when imported by setting ColorShiftConfig.cycles to 0. This functionality is not available when run from the TTE application. #### New Engine Features (0.10.0) * File input: Use the `--input-file` or `-i` option to pass a file as input. ### Changes (0.10.0) --- #### Effects Changes (0.10.0) * Added `--wave-direction` config to Waves effect. * Added additional directions to `--wipe-direction` config in Wipe effect. * VerticalSlice is now Slice and supports vertical, horizontal, and diagonal slice directions. #### Engine Changes (0.10.0) * Increased compatibility with Python versions from >=3.10 to >=3.8 * Updated type information for gradient step variables to accept a single int as well as tuple[int, ...]. * Color TypeAlias replaced with Color class. Color objects are used throughout the engine. * Renamed OutputArea to Canvas. * Changed center gradient direction to radial. ### Bug Fixes (0.10.0) --- #### Engine Fixes (0.10.0) * Characters created as `fill_characters` now adhere to `--no-color` and `--xterm-colors`. #### Other (0.10.0) * Added cookbook to the documentation and animated prompt example. * Added printing `Color` and `Gradient` objects examples to docs. --- ## 0.9.3 --- ### New Features (0.9.3) --- #### New Engine Features (0.9.3) * Added argument to the `BaseEffect.terminal_output()` context manager. `end_symbol` (default `\n`) is used to specify the symbol that will be printed after the effect completes. Set to `''` or `' '` to enable animated prompts. ### Changes (0.9.3) --- #### Engine Changes (0.9.3) * Removed unnecessary write calls for cursor positioning on every frame. * Separated functionality related to cursor positioning and frame timing out of `Terminal.print()` and into `Terminal.enforce_framerate()`, `Terminal.prep_canvas()` and `Terminal.move_cursor_to_top()`. ### Bug Fixes (0.9.3) --- #### Engine Fixes (0.9.3) * Fixed the canvas of an effect being 1 row less than specified via the `Terminal.terminal_height` attribute. This was caused by mixing use of `print()` and `sys.stdout.write()`. --- ## 0.9.1 --- ### New Features (0.9.1) --- #### New Engine Features (0.9.1) * Terminal dimension auto-detection supports automatically detecting a single dimensions. ### Changes (0.9.1) --- #### Effects Changes (0.9.1) * All effects have been updated to use the new `update()` method and `frame` property of `base_effect.BaseEffectIterator`. See Engine Changes for more info. #### Engine Changes (0.9.1) * `base_effect.BaseEffectIterator` now has an `update()` method which calls the `tick()` method of all active characters and manages the `active_characters` list. * `base_effect.BaseEffectIterator` has a `frame` property which calls `Terminal.get_formatted_output_string()` and returns the string. * `TerminalConfig.terminal_dimensions` has been split into `TerminalConfig.terminal_width` and `TerminalConfig.terminal_height` to simplify the command line argument for dimensions and make it more obvious which dimension is being specified when interacting with `effect.terminal_config`. #### Other Changes (0.9.1) ### Bug Fixes (0.9.1) --- #### Engine Fixes (0.9.1) * Fixed division by zero error when the terminal height was set to 1. ## 0.9.0 --- ### New Features (0.9.0) --- #### New Engine Features (0.9.0) * Linear easing function added. ### Changes (0.9.0) --- #### Other Changes (0.9.0) * Major re-organization of the codebase and significant documentation changes and additions. ## 0.8.0 --- ### New Features (0.8.0) --- #### New Engine Features (0.8.0) * Library support: TTE effects are now importable. All effects are iterators that return strings for each frame of the output. See README for more information. * Terminal: New terminal argument (--terminal-dimensions) allows specification of the terminal dimensions without relying on auto-detection. Especially useful in cases where TTE is being used as a library in non-terminal or TUI contexts. * Terminal: New terminal argument (--ignore-terminal-dimensions) causes the canvas dimensions to match the input data dimensions without regard to the terminal. ### Changes (0.8.0) --- #### Effects Changes (0.8.0) * Scattered. Holds scrambled text at the start for a few frames. * Scattered. Lowered default movement-speed from 0.5 to 0.3. #### Engine Changes (0.8.0) * graphics.Gradient ```__iter___()``` refactored to return a generator. No longer improperly implements the iterator protocol by resetting index in ```___iter__()```. * Terminal: Argument --animation-rate is now --frame-rate and is specified as a target frames per second. * Terminal: Argument --no-wrap is now --wrap-text and defaults to False. * Terminal: If a terminal object is instantiated without a TerminalConfig passed, it will instantiate a new TerminalConfig. * Terminal: Terminal.get_formatted_output_string() will return a string representing the current frame. * Terminal: Terminal.print() will print the frame to the terminal and handle cursor position. The optional argument (enforce_frame_rate: bool = True) determines if the frame rate set at Terminal.config.frame_rate is enforced. If set to False, the print will occur without delay. * New argument validator for terminal dimensions (argvalidators.TerminalDeminsions). * New module base_effect.py: * base_effect.BaseEffect: * This is an abstract class which forms the base iterable for all effects and provides the terminal_output() context manager. * base_effect.BaseEffectIterator: * This is an abstract class which provides the functionality to enable iteration over effects. ### Bug Fixes (0.8.0) --- #### Engine Fixes (0.8.0) * Fixed Argfield nargs type from str to str | int. * Implemented custom formatter into argsdataclass.py argument parsing. ## 0.7.0 --- ### New Features #### New Effects (0.7.0) * Beams. Light beams travel across the canvas and illuminate the characters behind them. * Overflow. The input text is scrambled by row and repeated randomly, scrolling up the terminal, before eventually displaying in the correct order. * OrbitingVolley. Characters fire from launcher which orbit canvas. * Spotlights. Spotlights search the text area, illuminating characters, before converging in the center and expanding. #### New Engine Features (0.7.0) * Gradients now support multiple step specification to control the distance between each stop pair. For example: graphics.Gradient(RED, BLUE, YELLOW, steps=(2,5)) results in a spectrum of RED -> (1 step) -> BLUE -> (4 steps) -> YELLOW * graphics.Gradient.get_color_at_fraction(fraction: float) will return a color at the given fraction of the spectrum when provided a float between 0 and 1, inclusive. This can be used to match the color to a ratio/ For example, the character height in the terminal. * graphics.Gradient.build_coordinate_color_mapping() will map gradient colors to coordinates in the terminal and supports a Gradient.Direction argument to enable gradients in the following directions: horizontal, vertical, diagonal, center * graphics.Gradient, if printed, will show a colored spectrum and the description of its stops and steps. * The Scene class has a new method: apply_gradient_to_symbols(). This method will iterate over a list of symbols and apply the colors from a gradient to the symbols. A frame with the symbol will be added for each color starting from the last color used in the previous symbol, up to the the index determined by the ratio of the current symbol's index in the symbols list to the total length of the list. This method allows scenes to automatically create frames from a list of symbols and gradient of arbitrary length while ensuring every symbol and color is displayed. * On instatiation, Terminal creates EffectCharacters for every coordinate in the canvas that does not have an input character. These EffectCharacters have the symbol " " and are stored in Terminal._fill_characters as well as added to Terminal.character_by_input_coord. * argvalidators.IntRange will validate a range specified as "int-int" and return a tuple[int,int]. * argvalidators.FloatRange will validate a range of floats specified as "float-float" and return a tuple[float, float]. * character.animation.set_appearance(symbol, color) will set the character symbol and color directly. If a Scene is active, the appearance will be overwritten with the Scene frame on the next call to step_animation(). This method is intended for the occasion where a full scene isn't needed, or the appearance needs to be set based on conditions not compatible with Scenes or the EventHandler. For example, setting the color based on the terminal row. * Terminal.CharacterSort enums moved to Terminal.CharacterGroup, Terminal.CharacterSort is now used for sorting and return a flat list of characters. * Terminal.CharacterSort has new sort methods, TOP_TO_BOTTOM_LEFT_TO_RIGHT, TOP_TO_BOTTOM_RIGHT_TO_LEFT, BOTTOM_TO_TOP_LEFT_TO_RIGHT, BOTTOM_TO_TOP_RIGHT_TO_LEFT, OUTSIDE_ROW_TO_MIDDLE, MIDDLE_ROW_TO_OUTSIDE * New Terminal.CharacterGroup options, CENTER_TO_OUTSIDE_DIAMONDS and OUTSIDE_TO_CENTER_DIAMONS * graphics.Animation.adjust_color_brightness(color: graphics.Color, brightness: float) will convert the color to HSL, adjust the brightness to the given level, and return an RGB hex string. * CTRL-C keyboard interrupt during a running effect will exit gracefully. * geometry.find_coords_in_circle() has been rewritten to find all coords which fall in an ellipse. The result is a circle due to the height/width ratio of terminal cells. This function now finds all terminal coordinates within the 'circle' rather than an arbitrary subset. * All command line arguments are typed allowing for more easily defined and tested effect args. ### Changes (0.7.0) #### Effects Changes (0.7.0) * All effects have been updated to use the latest API calls for improved performance. * All effects support gradients for the final appearance. * All effects support gradient direction. * All effects have had their default colors refreshed. * ErrorCorrect swap-delay lowered and error-pairs specification changed to percent float. * Rain effect supports character specification for rain drops and movement speed range for the rain drop falling speed. * Print effect uses the row final gradient color for the print head color. * RandomSequence effect accepts a starting color and a speed. * Rings effect prepares faster. Ring colors are set in order of appearance in the ring-colors argument. Ring spin speed is configurable. Rings with less than 25% visible characters based on radius are no longer generated. Ring gap is set as a percent of the smallest canvas dimension. * Scattered effect gradient progresses from the first color to the row color. * Spray effect spray-volume is specified as a percent of the total number of characters and movement speed is a range. * Swarm effect swarm focus points algorithm changed to reduce long distances between points. * Decrypt effect supports gradient specification for plaintext and multiple color specification for ciphertext. * Decrypt effect has a --typing-speed arg to increase the speed of the initial text typing effect. * Decrypt effect has had the decrypting speed increased. * Beams effect uses Animation.adjust_color_brightness() to lower the background character brightness and shows the lighter color when the beam passes by. * Crumble effect uses Animation.adjust_color_brightness() to set the weak and dust colors based on the final gradient. * Fireworks effect launch_delay argument has a +/- 0-50% randomness applied. * Bubbles effect --no-rainbow changed to --rainbow and default set to False. * Bubbles effect --bubble-color changed to --bubble-colors. Bubble color is randomly chosen from the colors unless --rainbow is used. * Burn effect burns faster with some randomness in speed. * Burn effect final color fades in from the burned color. * Burn effect characters are shown prior to burning using a starting_color arg. * Pour effect has a --pour-speed argument. #### Engine Changes (0.7.0) * Geometry related methods have been removed from the motion class. They are now located at terminaltexteffects.utils.geometry as separate functions. * The Coord() object definition has been moved from the motion module to the geometry module. * Terminal.add_character() takes a geometry.Coord() argument to set the character's input_coordinate. * EffectCharacters have a unique ID set by the Terminal on instatiation. As a result, all EffectCharacters should be created using Terminal.add_character(). * EffectCharacters added by the effect are stored in Terminal._added_characters. * Retrieving EffectCharacters from the terminal should no longer be done via accessing the lists of characters [_added_characters, _fill_characters, _input_characters], but should be retrieved via Terminal.get_characters() and Terminal.get_characters_sorted(). * Setting EffectCharacter visibility is now done via Terminal.set_character_visibility(). This enables the terminal to keep track of all visible characters without needing to iterate over all characters on every call to _update_terminal_state(). * EventHandler.Action.SET_CHARACTER_VISIBILITY_STATE has been removed as visibilty state is handled by the Terminal. To enable visibility state changes through the event system, use a CALLBACK action with target EventHandler.Callback(terminal.set_character_visibility, True/False). * geometry.find_coords_on_circle() num_points arg renamed to points_limit and new arg unique: bool, added to remove any duplicate Coords. * The animation rate argument (-a, --animation-rate) has been removed from all effects and is handled as a terminal argument specified prior to the effect name. * argtypes.py has been renamed argvalidators.py and all functions have been refactored into classes with a METAVAR class member and a type_parser method. * easing.EasingFunction type alias used anywhere an easing function is accepted. * Exceptions raised are no longer caught in a except clause. Only a finally clause is used to restore the cursor. Tracebacks are useful. #### Other Changes (0.7.0) * More tests have been added. ### Bug Fixes (0.7.0) #### Effects Fixes (0.7.0) * All effects with command line options that accept variable length arguments which require at least 1 argument will present an error message when the option is called with 0 arguments. #### Engine Fixes (0.7.0) * Fixed division by zero error in geometry.find_coord_at_distance() when the origin coord and the target coord are the same. * Fixed gradient generating an extra color in the spectrum when the initial color pair was repeated. Ex: Gradient('ffffff','000000','ffffff','000000, steps=5) would result in the third color 'ffffff' being added to the spectrum when it was already present as the end of the generation from '000000'->'ffffff'. ## 0.6.0 ### New Features (0.6.0) #### New Effects (0.6.0) * Print. Lines are printed one at a time following a print head. Print head performs line feed, carriage return. * BinaryPath. Characters are converted into their binary representation. These binary groups travel to their input coordinate and collapse into the original character symbol. * Wipe. Performs directional wipes with an optional trailing gradient. * Slide. Slides characters into position from outside the terminal view. Characters can be grouped by column, row, or diagonal. Groups can be merged from opposite directions or slide from the same direction. * SynthGrid. Creates a gradient colored grid in which blocks of characters dissolve into the input text. #### New Engine Features (0.6.0) * Terminal.get_character() method accepts a Terminal.CharacterSort argument to easily retrieve the input characters in groups sorted by various directions, ex: Terminal.CharacterSort.COLUMN_LEFT_TO_RIGHT * Terminal.add_character() method allows adding characters to the effect that are not part of the input text. These characters are added to a separate list (Terminal.non_input_characters) in terminal to allow for iteration over Terminal.characters and adding new characters based on the input characters without modifying the Terminal.characters list during iteration. The added characters are handled the same as input characters by the Terminal. * New EventHandler Action, Callback. The Action target can be any callable and will pass the character as the first argument, followed by any additional arguments provided. Uses new EventHandler.Callback type with signature EventHandler.Callback(typing.Callable, *args) * graphics.Gradient() objects specified with a single color will create a list of the single color with length *steps*. This enables gradients to be specified via command line arguments while supporting an arbitrary number of colors > 0, without needing to perform any checking in the effect logic. ### Changes (0.6.0) ### Effects Changes (0.6.0) * Rowslide, Columnslide, and Rowmerge have been replaced with a single effect, Slide. * Many classic effects now support gradient specification which includes stops, steps, and frames to enable greater customization. * Randomsequence effect supports gradient specification. * Scattered effect supports gradient specification. * Expand effect supports gradient specification. * Pour effect now has a back and forth pouring animation and supports gradient specification. #### Engine Changes (0.6.0) * Terminal._update_terminal_state() refactored for improved performance. * EffectCharacter.tick() will progress motion and animation by one step. This solves the problem of running Animation.step_animation() before Motion.move() and desyncing Path synced animations. * EffectCharacter.is_active has been renamed to EffectCharacter.is_visible. * EffectCharacter.is_active() can be used to check if motion/animation is in progress. * graphics.Animation.new_scene(), motion.Motion.new_path(), and Path.new_waypoint() all support automatic IDs. If no ID is provided a unique ID is automatically generated. ### Bug Fixes (0.6.0) * Fixed rare division by zero error in Path.step() when the final segment has a distance of zero and the distance to travel exceeds the total distance of the Path. * Fixed effects not respecting --no-color argument. ## 0.5.0 ### New Features (0.5.0) * New effect, Vhstape. Lines of characters glitch left and right and lose detail like an old VHS tape. * New effect, Crumble. Characters lose color and fall as dust before being vacuumed up and rebuilt. * New effect, Rings. Characters are dispersed throughout the canvas and form into spinning rings. * motion.Motion.chain_paths(list[Paths]) will automatically register Paths with the EventHandler to create a chain of paths. Looping is supported. * motion.Motion.find_coords_in_rect() will return a random selection of coordinates within a rectangular area. This is faster than using find_coords_in_circle() and should be used when the shape of the search area isn't important. * Terminal.Canvas.coord_in_canvas() can be used to determine if a Coord is in the canvas. * Paths have replaced Waypoints as the motion target specification object. Paths group Waypoints together and allow for easing motion and animations across an arbitrary number of Waypoints. Single Waypoint Paths are supported and function the same as Waypoints did previously. Paths can be looped with the loop argument. * Quadratic and Cubic bezier curves are supported. Control points are specified in the Waypoint object signature. When a control point is specified, motion will be curved from the prior Waypoint to the Waypoint with the control point, using the control point to determine the curve. Curves are supported within Paths. * New EventHandler.Event PATH_HOLDING is triggered when a Path enters the holding state. * New EventHandler.Action SET_CHARACTER_ACTIVATION_STATE can be used to modify the character activation state based on events. * New EventHandler.Action SET_COORDINATE can be used to set the character's current_coordinate attribute. * Paths have a layer attribute that can be used to automatically adjust the character's layer when the Path is activated. Has no effect when Path.layer is None, defaults to None. * New EventHandler.Events SEGMENT_ENTERED and SEGMENT_EXITED. These events are triggered when a character enters or exits a segment in a Path. The segment is specified using the end Waypoint of the segment. These events will only be called one time for each run through the Path. Looping Paths will reset these events to be called again. ### Changes (0.5.0) * graphics.Animation.random_color() is now a static method. * motion.Motion.find_coords_in_circle() now generates 7*radius coords in each inner-circle. * BlackholeEffect uses chain_paths() and benefits from better circle support for a much improved blackhole animation. * BlackholeEffect singularity Paths are curved towards center lines. * EventHandler.Event.WAYPOINT_REACHED removed and split into two events, PATH_HOLDING and PATH_COMPLETE. * EventHandler.Event.PATH_COMPLETE is triggered when the final Path Waypoint is reached AND holding time reaches 0. * Fireworks effect uses Paths and curves to create a more realistic firework explosion. * Crumble effect uses control points to create a curved vacuuming phase. * graphics.Gradient accepts an arbitrary number of color stops. The number of steps applies between each color stop. * motion.find_coords_in_circle() and motion.find_coords_in_rect() no longer take a num_points argument. All points in the area are returned. ### Bug Fixes (0.5.0) * Fixed looping animations when synced to Path not resetting properly. ## 0.4.3 ### Changes (0.4.3) * blackhole radius is based on the canvas size, not the input text size. ## 0.4.2 ### Changes (0.4.2) * motion.Motion.find_points_on_circle and motion.Motion.find_points_in_circle now account for the terminal character height/width ratio to return points that more closely approximate a circle. All effects which use these functions have been updated to account for this change. ## 0.4.1 ### Changes (0.4.1) * Updated documentation ## 0.4.0 ### New Features (0.4.0) * Waves effect. A wave animation is played over the characters. Wave colors and final colors are configurable. * Blackhole effect. Characters spawn scattered as a field of stars. A blackhole forms and consumes the stars then explodes the characters across the screen. Characters then 'cool' and ease into position. * Swarm effect. Characters a separated into swarms and fly around the canvas before landing in position. * Animations support easing functions. Easing functions are applied to Scenes using Scene.ease = easing_function. * Canvas has a center attribute that is the center Coord of the canvas. * Terminal has a random_coord() method which returns a random coordinate. Can specify outside the canvas. ### Changes (0.4.0) * Animation and Motion have been refactored to use direct Scene and Waypoint object references instead of string IDs. * base_character.EventHandler uses Scene and Waypoint objects instead of string IDs. * graphics.GraphicalEffect renamed to CharacterVisual * graphics.Sequence renamed to Frame * Animation methods for created Scenes and adding frames to scenes have been refactored to return Scene objects and expose terminal modes, respectively. * Easing function api has been simplified. Easing function callables are used directly rather than Enums and function maps. * Layer is set on the EffectCharacter object instead of the motion object. The layer is modified through the EventHandler to allow finer control over the layer. * Animations not longer sync to specific waypoints, rather, they sync to the progress of the character towards the active waypoint. * Animations synced to waypoint progress can now sync to either the distance progression or the step progression. * Motion methods which utilize coordinates now use Coord objects rather than tuples. * Motion has methods for finding coordinates on a circle and in a circle. ### Bug Fixes (0.4.0) * Fixed Gradient creating two more steps than specified. * Fixed waypoint synced animation index out of range error. ## 0.3.1 ### New Features (0.3.1) * Bouncyballs effect. Balls drop from the top of the canvas and bounce before settling into position. A gradient is used to transition to the final color after the ball has landed. Random colors are used for balls unless specified. * Unstable effect. Spawn characters jumbled, explode to the edge of the canvas, then reassemble them in the correct layout. * Bubble effect. Characters are formed into bubbles and fall down the screen before popping. * Middleout effect. Characters start as a single character in the center of the canvas. A row or column is expanded in the center of the screen, then the entire output is expanded from this row/column. Expansion from row/column is determined by the --expand-direction argument. * Errorcorrect effect. Some characters spawn with their location swapped with another character. The characters then move, in pairs, to their correct location following an animation. * --no-wrap argument prevents line wrapping. * --tab-width argument can be used to specify the number of spaces used in place of tab characters. Defaults to 4. * New Events for WaypointActivated and SceneActivated. * New Event Actions for DeactivateWaypoint and DeactivateScene. * Scenes can be synced to Waypoint progress. The scene will progress in-line with the character's steps towards the waypoint. * Waypoints now have a layer attribute. Characters are drawin in ascending layer order. While a character has a waypoint active, that waypoint's layer is used. Otherwise, the character is drawn in layer 0. ### Changes (0.3.1) * Added Easing Functions help output for fireworks effect. * Updated spray effect help output. * Removed shootingstar effect. It was not particularly interesting. * Coord type is now hashable and frozen. * Waypoints are hashable. Can be compared for equality based on row, col pair. * Scenes can be compared for equality based on id. * Terminal maintains an input_coord tuple[row, col] -> EffectCharacter map called character_by_input_coord. * The terminal cursor is now hidden during the effect. * The find_points_on_circle method in the motion module is now a static method. * Terminal.Canvas has center_row and center_column attributes. * Added layers to effects. ### Bug Fixes (0.3.1) * Fixed animating_chars filter in effect_template to properly remove completed characters. * Initial symbol assignment when activating a scene no longer increases played_frames count. * Waypoints and Animations completed are deactivated to prevent repeated event triggering. * Fixed step_animation in graphics module handling of looping animations. It will no longer deactivate the animation. ## 0.2.1 ### New Features (0.2.1) * Added explode distance argument to fireworks effect * Added random_color function to graphics module ### Changes (0.2.1) ### Bug Fixes (0.2.1) * Fixed inactive characters in expand effect. terminaltexteffects-release-0.12.1/LICENSE000066400000000000000000000020601507200677100203010ustar00rootroot00000000000000MIT License Copyright (c) [2023] [ChrisBuilds] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. terminaltexteffects-release-0.12.1/README.md000066400000000000000000000464771507200677100205770ustar00rootroot00000000000000

TTE

Terminal Text Effects

Inline Visual Effects in the Terminal

[![PyPI - Version](https://img.shields.io/pypi/v/terminaltexteffects?style=flat&color=green)](http://pypi.org/project/terminaltexteffects/ "![PyPI - Version](https://img.shields.io/pypi/v/terminaltexteffects?style=flat&color=green)") ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/terminaltexteffects) [![Python Bytes](https://img.shields.io/badge/Python_Bytes-377-D7F9FF?logo=applepodcasts&labelColor=blue)](https://youtu.be/eWnYlxOREu4?t=1549) ![License](https://img.shields.io/github/license/ChrisBuilds/terminaltexteffects) ## Table Of Contents * [About](#tte) * [Requirements](#requirements) * [Installation](#installation) * [Usage (Application)](#application-quickstart) * [Usage (Library)](#library-quickstart) * [Effect Showcase](#effect-showcase) * [In-Development Preview](#in-development-preview) * [Latest Release Notes](#latest-release-notes) * [License](#license) ## TTE ![synthgrid_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/6d1bab16-0520-44fa-a508-8f92d7d3be9e) TerminalTextEffects (TTE) is a terminal visual effects engine. TTE can be installed as a system application to produce effects in your terminal, or as a Python library to enable effects within your Python scripts/applications. TTE includes a growing library of built-in effects which showcase the engine's features. These features include: * Xterm 256 / RGB hex color support * Complex character movement via Paths, Waypoints, and motion easing, with support for bezier curves. * Complex animations via Scenes with symbol/color changes, layers, easing, and Path synced progression. * Variable stop/step color gradient generation. * Event handling for Path/Scene state changes with custom callback support and many pre-defined actions. * Effect customization exposed through a typed effect configuration dataclass that is automatically handled as CLI arguments. * Runs inline, preserving terminal state and workflow. ## Requirements TerminalTextEffects is written in Python and does not require any 3rd party modules. Terminal interactions use standard ANSI terminal sequences and should work in most modern terminals. Note: Windows Terminal performance is slow for some effects. ## Installation ```pip install terminaltexteffects``` OR ```pipx install terminaltexteffects``` ### Nix (flakes) Add it as an input to a flake: ```nix inputs = { terminaltexteffects.url = "github:ChrisBuilds/terminaltexteffects/" } ```` Create a shell with it: ```nix nix shell github:ChrisBuilds/terminaltexteffects/ ``` Or run it directly: ```nix echo 'terminaltexteffects is awesome' | nix run github:ChrisBuilds/terminaltexteffects/ -- beams ``` ### Nix (classic) Fetch the source and add it to, e.g. your shell: ```nix let pkgs = import {}; tte = pkgs.callPackage (pkgs.fetchFromGitHub { owner = "ChrisBuilds"; repo = "terminaltexteffects"; rev = ""; hash = ""; # Build first, put proper hash in place }) {}; in pkgs.mkShell { packages = [tte]; } ``` ## Usage View the [Documentation](https://chrisbuilds.github.io/terminaltexteffects/) for a full installation and usage guide. ### Application Quickstart #### Options
TTE Command Line Options ```markdown options: -h, --help show this help message and exit --input-file INPUT_FILE, -i INPUT_FILE File to read input from (default: None) --version, -v show program's version number and exit --tab-width (int > 0) Number of spaces to use for a tab character. (default: 4) --xterm-colors Convert any colors specified in 24-bit RBG hex to the closest 8-bit XTerm-256 color. (default: False) --no-color Disable all colors in the effect. (default: False) --existing-color-handling {always,dynamic,ignore} Specify handling of existing 8-bit and 24-bit ANSI color sequences in the input data. 3-bit and 4-bit sequences are not supported. 'always' will always use the input colors, ignoring any effect specific colors. 'dynamic' will leave it to the effect implementation to apply input colors. 'ignore' will ignore the colors in the input data. Default is 'ignore'. (default: ignore) --wrap-text Wrap text wider than the canvas width. (default: False) --frame-rate FRAME_RATE Target frame rate for the animation in frames per second. Set to 0 to disable frame rate limiting. (default: 100) --canvas-width int >= -1 Canvas width, set to an integer > 0 to use a specific dimension, use 0 to match the terminal width, or use -1 to match the input text width. (default: -1) --canvas-height int >= -1 Canvas height, set to an integer > 0 to use a specific dimension, use 0 to match the terminal height, or use -1 to match the input text height. (default: -1) --anchor-canvas {sw,s,se,e,ne,n,nw,w,c} Anchor point for the canvas. The canvas will be anchored in the terminal to the location corresponding to the cardinal/diagonal direction. (default: sw) --anchor-text {n,ne,e,se,s,sw,w,nw,c} Anchor point for the text within the Canvas. Input text will anchored in the Canvas to the location corresponding to the cardinal/diagonal direction. (default: sw) --ignore-terminal-dimensions Ignore the terminal dimensions and utilize the full Canvas beyond the extents of the terminal. Useful for sending frames to another output handler. (default: False) Effect: Name of the effect to apply. Use -h for effect specific help. {beams,binarypath,blackhole,bouncyballs,bubbles,burn,canvas_test,colorshift,crumble,decrypt,dev,errorcorrect,expand,fireworks,highlight,laseretch,matrix,middleout,orbittingvolley,overflow,pour,print,rain,randomsequence,rings,scattered,slice,slide,spotlights,spray,swarm,sweep,synthgrid,test,unstable,vhstape,waves,wipe} Available Effects beams Create beams which travel over the canvas illuminating the characters behind them. binarypath Binary representations of each character move towards the home coordinate of the character. blackhole Characters are consumed by a black hole and explode outwards. bouncyballs Characters are bouncy balls falling from the top of the canvas. bubbles Characters are formed into bubbles that float down and pop. burn Burns vertically in the canvas. colorshift Display a gradient that shifts colors across the terminal. crumble Characters lose color and crumble into dust, vacuumed up, and reformed. decrypt Display a movie style decryption effect. errorcorrect Some characters start in the wrong position and are corrected in sequence. expand Expands the text from a single point. fireworks Characters launch and explode like fireworks and fall into place. highlight Run a specular highlight across the text. laseretch A laser etches characters onto the terminal. matrix Matrix digital rain effect. middleout Text expands in a single row or column in the middle of the canvas then out. orbittingvolley Four launchers orbit the canvas firing volleys of characters inward to build the input text from the center out. overflow Input text overflows and scrolls the terminal in a random order until eventually appearing ordered. pour Pours the characters into position from the given direction. print Lines are printed one at a time following a print head. Print head performs line feed, carriage return. rain Rain characters from the top of the canvas. randomsequence Prints the input data in a random sequence. rings Characters are dispersed and form into spinning rings. scattered Text is scattered across the canvas and moves into position. slice Slices the input in half and slides it into place from opposite directions. slide Slide characters into view from outside the terminal. spotlights Spotlights search the text area, illuminating characters, before converging in the center and expanding. spray Draws the characters spawning at varying rates from a single point. swarm Characters are grouped into swarms and move around the terminal before settling into position. sweep Sweep across the canvas to reveal uncolored text, reverse sweep to color the text. synthgrid Create a grid which fills with characters dissolving into the final text. unstable Spawn characters jumbled, explode them to the edge of the canvas, then reassemble them in the correct layout. vhstape Lines of characters glitch left and right and lose detail like an old VHS tape. waves Waves travel across the terminal leaving behind the characters. wipe Wipes the text across the terminal to reveal characters. Ex: ls -a | tte decrypt --typing-speed 2 --ciphertext-colors 008000 00cb00 00ff00 --final-gradient-stops eda000 --final-gradient-steps 12 --final-gradient-direction vertical ```
```cat your_text | tte [options]``` OR ```cat your_text | python -m terminaltexteffects [options]``` * Use ``` -h``` to view options for a specific effect, such as color or movement direction. * Ex: ```tte decrypt -h``` For more information, view the [Application Usage Guide](https://chrisbuilds.github.io/terminaltexteffects/appguide/). ### Library Quickstart All effects are iterators which return a string representing the current frame. Basic usage is as simple as importing the effect, instantiating it with the input text, and iterating over the effect. ```python from terminaltexteffects.effects import effect_rain effect = effect_rain.Rain("your text here") for frame in effect: # do something with the string ... ``` In the event you want to allow TTE to handle the terminal setup/teardown, cursor positioning, and animation frame rate, a terminal_output() context manager is available. ```python from terminaltexteffects.effects import effect_rain effect = effect_rain.Rain("your text here") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` For more information, view the [Library Usage Guide](https://chrisbuilds.github.io/terminaltexteffects/libguide/). ### Effect Showcase Note: Below you'll find a subset of the built-in effects. View all of the effects and related information in the [Effects Showroom](https://chrisbuilds.github.io/terminaltexteffects/showroom/). #### Beams ![beams_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/6bb98dac-688e-43c9-96aa-1a45f451d4cb) #### Binarypath ![binarypath_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/99ad3946-c475-4743-93e2-cdfb2a7f558f) #### Blackhole ![blackhole_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/877579d3-d353-4bed-9a95-d3ea7a53200a) #### Bubbles ![bubbles_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/5a616538-7936-4f55-b2ff-28e6c4179fce) #### Burn ![burn_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/9770711a-ea68-48cc-947f-fb13c6613a2e) #### Decrypt ![decrypt_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/36c23e70-065d-4316-a09e-c2761882cbb3) #### Fireworks ![fireworks_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/da6a97b1-c4fd-4370-9852-9ddb8a494b55) #### Matrix ![matrix_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/0f6ddfd9-5e78-4de2-a187-7950b1e5b9d0) #### Orbittingvolley ![orbittingvolley_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/084038e5-9d49-4c7d-bf15-e989f541b15c) #### Pour ![pour_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/145c2a4e-6b30-48c6-80a3-afb03edf7c22) #### Print ![print_demo](/docs/img/effects_demos/print_demo.gif) #### Rain ![rain_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/7b8cf447-67b6-41e9-b354-07b3e5161d10) #### Rings ![rings_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/cb7f6388-0f46-42f1-a2b3-6a267e9451f0) #### Slide ![slide_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/218e7218-e9ef-44de-b43b-5e824623a957) #### Spotlights ![spotlights_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/4ab93725-0c8a-4bdf-af91-057338f4e007) #### VHSTape ![vhstape_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/720abbf4-f97d-4ce9-96ee-15ef973488d2) #### Waves ![waves_demo](https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/ea9b04ca-e526-4c7e-b98d-a98a42f7137f) ## In-Development Preview Any effects shown below are in development and will be available in the next release. ## Latest Release Notes Visit the [ChangeBlog](https://chrisbuilds.github.io/terminaltexteffects/changeblog/changeblog/) for release write-ups. ## 0.12.1 --- ### Bug Fixes (0.12.1) --- * Fixed bug in ArgField caused by Field init signature change in Python 3.14. This class and parent module will be removed in 0.13.0. ### New Features (0.12.0) --- #### New Effects (0.12.0) * Highlight - Run a specular highlight across the text. Highlight direction, brightness, and width can be specified. * Laseretch - A laser travels across the terminal, etching characters and emitting sparks. * Sweep - Sweep across the canvas to reveal uncolored text, reverse sweep to color the text. #### New Engine Features (0.12.0) * Background color specification is supported throughout the engine. Methods which accept Color arguments expect a `ColorPair` object to specify both the foreground and background color. * New `EventHandler.Action`: `Action.RESET_APPEARANCE` will reset the character appearance to the input character with no modifications. This eliminates the need to make a `Scene` for this purpose. * Existing 8/24 bit color sequences in the input data are parsed and handled by the engine. A new `TerminalConfig` option `--existing-color-handling` is used to control how these sequences are handled. * `easing.eased_step_function()` allows easing functions to be used generically by returning a closure that produces an eased value based on the easing function and step size provided when called. * A new easing function has been added which returns a custom easing fuction based on cubic bezier controls. * Added custom exceptions. ### Changes (0.12.0) --- #### Effects Changes (0.12.0) * Spotlights - The maximum size of the beam is limited to the smaller of the two canvas dimensions and the minimum size is limited to 1. * Spray - Argument spray_volume is limited to 0 < n <= 1. * Colorshift - `--loop` has been renamed `--no-loop`. Looping the gradient is now default. * All effects which apply a gradient across the text build the gradient mapping based on the text dimensions regardless of the canvas size. This fixes truncated gradients where parts of the gradient map were assigned to empty coordinates. * Some effects support dynamic handling of color sequences in the input data. * Blackhole - Star characters changed to ASCII only to improve supported fonts. #### Engine Changes (0.12.0) * Frame rate timing is enforced within the `BaseEffectIterator` when accessing the `frame` property, rather than within the `Terminal` on calls to `print()`. This enables frame timing when iterating without requiring the use of the `terminal_output()` context manager. * The frame rate can be set to `0` to run without a limit. * Removed unused method Segment.get_coord_on_segment(). * Activating a Path with no segments will raise a ValueError. * `base_effect.active_characters` was refactored from a list to a set. * Bezier curves are no longer limited to two control points. Any number of control points can be specified in calls to `Path.new_waypoint()`, however, performance may suffer with large numbers of control points along unique paths. * Caching has been implemented for all geometry functions significantly improving performance in cases where many characters are traveling along the same Path. * Reorganized the most common API class imports up to the package level. * Moved the SyncMetric Enum from the Animation module top level into the Scene class. * `Scene.apply_gradient_symbols()` accepts two gradients, one for the foreground and one for the background. ### Bug Fixes (0.12.0) --- #### Effects Fixes (0.12.0) * VHSTape - Fixed glitch wave lines not appearing for some canvas/input_text size ratios. * Fireworks - Fixed launch_delay set to 0 causing an infinite loop. * Spotlights - Fixed infinite loop caused by very small beam_width_ratio values. * Overflow - Fixed effect ignoring `--final-gradient-direction` argument. #### Engine Fixes (0.12.0) * Fixed Color() objects not treating rgb colors initialized with/without the hash as equal. Ex: Color('#ffffff') and Color('ffffff') * Gradients initialized with a tuple of steps including the value 0 will raise a ValueError as expected. Ex: Gradient(Color('ff0000'), Color('00ff00'), Color('0000ff'), steps=(4,0)) * Fixed infinite loop when a new scene is created without an id and a scene has been deleted resuling in the length of the scenes dict corresponding to an existing scene id. * Fixed `Canvas` center calculations being off by one for odd widths/heights due to floor division. * Fixed `Gradient.get_color_at_fraction` rounding resulting in over-representing colors in the middle of the spectrum. * `Gradient.build_coordinate_color_mapping` signature changed to required full bounding box specification. This allows the effect to selectively build based on the text/canvas/terminal dimensions and reduces build time by by reducing the map size when possible. * Adds a call to `ansitools.dec_save_cursor_position` after each call to `ansitools.dec_restore_cursor_position` to address some terminals clearing the saved data after the restore. #### Other (0.12.0) * Fixed Canvas width/height docstrings and help output to correctly indicate 0/-1 matching terminal device/input text. --- ## License Distributed under the MIT License. See [LICENSE](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/LICENSE.md) for more information. terminaltexteffects-release-0.12.1/default.nix000066400000000000000000000011661507200677100214460ustar00rootroot00000000000000{ lib, python312Packages, }: let poetryDef = with builtins; (fromTOML (readFile ./pyproject.toml)).tool.poetry; name = poetryDef.name; in python312Packages.buildPythonApplication { pname = name; inherit (poetryDef) version; src = builtins.path { path = ./.; name = name; }; pyproject = true; nativeBuildInputs = [ python312Packages.poetry-core ]; meta = { inherit (poetryDef) description; maintainers = poetryDef.authors; homepage = "https://github.com/ChrisBuilds/${name}"; license = lib.licenses.mit; mainProgram = "tte"; }; } terminaltexteffects-release-0.12.1/docs/000077500000000000000000000000001507200677100202265ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/docs/appguide.md000066400000000000000000000036431507200677100223540ustar00rootroot00000000000000# Application Guide When used as a system application, TerminalTextEffects will produce animations on text passed to stdin or through the `-i` argument. Passing data via STDIN to TTE occurs via pipes or redirection. ## Invocation Examples === "Piping" ```bash title="Piping directory listing output through TTE" ls -latr | tte slide ``` === "Redirection" ```bash title="Redirecting a file through TTE" tte slide < your_file ``` === "File Input" ```bash title="Passing a file argument to TTE" tte -i path/to/file slide ``` ## Configuration TTE has many global terminal configuration options as well as effect-specific configuration options available via command-line arguments. Terminal configuration options should be specified prior to providing the effect name. The basic format is as follows: ```bash title="TTE usage syntax" tte [global_options] [effect_options] ``` Using the `-h` argument in place of the global_options or effect_options will produce either the global or effect help output, respectively. The example below will pass the output of the `ls` command to TTE with the following options: * *Global* options: - Text will be wrapped if wider than the terminal. - Tabs will be replaced with 4 spaces. * *Effect* options: - Use the [slide](./effects/slide.md) effect. - Merge the groups. - Set movement-speed to 2. - Group by column. ```bash title="TTE argument specification example" ls | tte --wrap-text --tab-width 4 slide --merge --movement-speed 2 --grouping column ``` ## Example Usage Animate fetch output on shell launch using screenfetch: ```bash title="Shell Fetch" screenfetch -N | tte slide --merge ``` ![fetch_demo](./img/application_demos/fetch_example.gif) !!! note Fetch applications which utilize terminal sequences for color/formatting will not work with TTE. Check if your fetch application has a raw output switch. terminaltexteffects-release-0.12.1/docs/changeblog/000077500000000000000000000000001507200677100223175ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/docs/changeblog/changeblog.md000066400000000000000000000007551507200677100247410ustar00rootroot00000000000000# ChangeBlog Home The ChangeBlog features explanatory documentation for each release. Blog entries will explain the major features of a given release and provide in-depth explainations of TTE Engine features. ## Release Entries * [0.12.0 - Color Parsing, Background Color, more Easing](./changeblog_0.12.0.md) * [0.11.0 - Matrix Effect and Canvas+Text Anchoring](./changeblog_0.11.0.md) * [0.10.0 - ColorShift, Canvas, Better Compatibililty, and more Customization](./changeblog_0.10.0.md) terminaltexteffects-release-0.12.1/docs/changeblog/changeblog_0.10.0.md000066400000000000000000000131401507200677100255250ustar00rootroot00000000000000# 0.10.0 (ColorShift) ## Release 0.10.0 ### ColorShift, Canvas, Better Compatibililty, and more Customization This release comes with a shiny new effect [ColorShift](../showroom.md#colorshift), increased customization for [Waves](../showroom.md#waves) and [Wipe](../showroom.md#wipe) as well as renaming the VerticalSlice effect to [Slice](../showroom.md#slice) with added slice directions. Engine changes includes renaming the OutputArea class to [Canvas](../engine/terminal/canvas.md) and building out the [Color](../engine/utils/color.md) class from what was previously a TypeAlias. In addition, to increase compatibility with older version of Python (back to 3.8), a few minor changes were implemented including using annotations and removing the occasional use of modern syntax where unnecessary. Additionally, input can be passed to TTE using the new `-i` argument. Finally, a few new pages were added to the docs. Check out the [Cookbook](../cookbook.md) for interesting examples using the TTE library and the [ChangeBlog](changeblog.md) (you're already here) for a more friendly breakdown of each release. ### ColorShift The new [ColorShift](../showroom.md#colorshift) effect is largely in response to a [Feature Request issue](https://github.com/ChrisBuilds/terminaltexteffects/issues/9) involving looping effects and the desire for a simple RGB gradient effect. TTE supports [Gradients](../engine/utils/gradient.md) with an arbitrary number of stops and steps. This means you can create gradients that transition through as many colors as you want, with as many steps between colors. Fewer steps results in a more abrupt transition. As a part of the 0.10.0 update, I've added the `loop` argument which causes the final color stop to blend back to the first. Here's an example: #### Not Looped ```python from terminaltexteffects.utils.graphics import Color, Gradient g = Gradient(Color("ff0000"), Color("00ff00"), Color("0000ff"), steps=10, loop=False) print(g) ``` ![not_looped_gradient](../img/changeblog_media/0.10.0/not_looped_gradient_printed.png) #### Looped ```python from terminaltexteffects.utils.graphics import Color, Gradient g = Gradient(Color("ff0000"), Color("00ff00"), Color("0000ff"), steps=10, loop=True) print(g) ``` ![looped_gradient](../img/changeblog_media/0.10.0/looped_gradient_printed.png) Notice how the looped [Gradient](../engine/utils/gradient.md) transitions from blue, the final color stop, back to red, the first color stop. That allows the smooth looped gradient animations seen in the [ColorShift](../showroom.md#colorshift) effect. #### Travelling [ColorShift](../showroom.md#colorshift) supports standing Gradients and travelling Gradients in the following directions: * vertical * horizontal * diagonal * radial The travel direction can be reversed (think, center -> outside and outside -> center) using the `--reverse-travel-direction` argument. #### Never-Ending Effects [ColorShift](../showroom.md#colorshift) marks the first effect in the TTE library to support infinite looping. This feature is set by setting the [ColorShiftConfig](../effects/colorshift.md#terminaltexteffects.effects.effect_colorshift.ColorShiftConfig) `cycles` attribute to `0`. This will only work when using the effect in your code. The command line argument validator for `cycles` will not allow values below `1`. This may change in the future, however at this time I'm not sold on having effects that have to be killed when run as an application. [ColorShiftConfig](../effects/colorshift.md#terminaltexteffects.effects.effect_colorshift.ColorShiftConfig) `skip-final-gradient` will cause the effect to end when the last gradient cycle concludes. Otherwise, the `final_gradient_`* configuration will be used to transition to a final state. ### OutputArea is now Canvas On the slow but steady journey to documenting the engine from the perspective of writing effects, I realized `OutputArea` is not a particularly good class name to describe the area of the terminal in which the effect is being drawn. In addition, setting the terminal dimensions in the [TerminalConfig](../engine/terminal/terminalconfig.md) was not intuitive when you imagine the terminal dimensions are referring to your actual terminal device. To remedy this, `OutputArea` was renamed [Canvas](../engine/terminal/canvas.md) and the `TerminalConfig.terminal_height`/`TerminalConfig.terminal_width` are now `TerminalConfig.canvas_height`/`TerminalConfig.canvas_width`. So what is the [Canvas](../engine/terminal/canvas.md)? It's the space in the terminal where the effect is actually being rendered. When set automatically, it is determined by the bounding box that contains all of your text when it is passed to TTE. So if your input text is 5 lines high and 30 characters wide, the [Canvas](../engine/terminal/canvas.md) is 5x30. This is independent of your terminal device dimensions. Of course, if your text extends beyond the terminal device dimensions, it may be wrapped (if `TerminalConfig.wrap_text` is `True`) which will result in different [Canvas](../engine/terminal/canvas.md) dimensions. ### 0.10.0 Demos Speaking of [ColorShift](../showroom.md#colorshift), check it out. ![ColorShift Demo](../img/effects_demos/colorshift_demo.gif) Here's one of the new directions for [Waves](../showroom.md#waves), `center_to_outside`. ![Waves Demo](../img/changeblog_media/0.10.0/waves_center_out_changeblog_0_10_0.gif) Here's [Wipe](../showroom.md#wipe) showing one of the new wipe directions, `outside_to_center`. ![Wipe Demo](../img/changeblog_media/0.10.0/wipe_changeblog_0_10_0.gif) --- ### Plain Old Changelog [0.10.0](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/CHANGELOG.md) terminaltexteffects-release-0.12.1/docs/changeblog/changeblog_0.11.0.md000066400000000000000000000432421507200677100255340ustar00rootroot00000000000000# 0.11.0 (Enter the Matrix) ## Release 0.11.0 ### Release Summary (Matrix Effect, Anchoring, and Canvas Sizing) This release features the oft requested [Matrix](../showroom.md#matrix) effect and improvements to [Slice](../showroom.md#slice) and [Print](../showroom.md#print) as well as updates to many effects to support the new [Canvas Overhaul](#canvas-overhaul). The Engine underwent major changes to the [Canvas](../engine/terminal/canvas.md) class to enable arbitrary resizing as requested [in an issue](https://github.com/ChrisBuilds/terminaltexteffects/issues/14) and support for anchoring the Canvas within the terminal, and anchoring the text within the Canvas. Minor changes include the addition of a `--version` switch, better handling of `ctrl-c` interrupts during effect animations and a new [BaseCharacter](../engine/basecharacter.md) attribute `is_fill_character`. ### Housekeeping (We'll do it live.) In a surprise turn of events, TTE was shared on [HackerNews](https://news.ycombinator.com/item?id=40503202) a few weeks ago. I hadn't intended to share the project widely as, up to that point, it had only been used and tested by a small number of people and I assumed the result would be a deluge of Issues that would be better handled slow and steady. I only noticed it had been shared after seeing the repo star count jump from ~200 to ~500 in the span of an hour. A quick google query for "TerminalTextEffects" presented the source of this attention. Not only was TTE on the front page of HN, it was in the number one spot, where it stayed for the entire day. [Hacker News 2024-05-28](https://news.ycombinator.com/front?day=2024-05-28) Even more surprising, the comments were overwhelmingly positive. And yet more surprising still, the number of issues reported was quite low and easily managed. By the time it was all over, TTE made the top spot in the HN best-of and held it until it aged off three days later. Overall, an excellent, if unplanned, "launch". Since then, TTE has been featured in various blogs, Python news articles, the [VSCode YouTube Channel](https://www.youtube.com/shorts/E3VP5g3oXX0), and many other place around the web. I'm glad to have more people checking out this ridiculous project. Now, on to the release info. ## Matrix Effect --- When people initially encounter TTE their first thought is something like, "Neat.", and their second thought is probably, "Why?". Shortly after that, the obvious next thought is, "There must be a Matrix effect.". This led to disappointment and bewilderment. Who would make a terminal visual effects library and not include the most famous terminal visual effect in cinema history? Nobody. That would be crazy. ![Matrix Demo](../img/effects_demos/matrix_demo.gif) *I don't even see the code...* *All I see is set_appearance(), set_coordinate(), set_visibility()...* When I set out to replicate the Matrix digital rain effect, I thought, "surely this will only take a few hours". I mean, it's basically characters dropping from the top of the terminal and occasionally changing colors between shades of green, and swapping symbols. Easy. I was incorrect. After reviewing clips from the first film, and frame stepping through to learn all the secrets, I have discovered that The Matrix digital rain effect has a surprising amount of complexity. Here is the result of my analysis. ???+ Abstract "What Is The Matrix?" `Characters are not falling, they're activating in sequence, at varying speeds` : Yep, the perception that the characters are falling down the screen is a false memory (with one exception). The reality is that the cells are being activated in sequence from top to bottom in a given column. The delay between the activations is consistent within a column for the duration of a streak, but changes for each new streak. `Symbols consist of numbers, punctuation, Katakana, and a few others` : You'll need a good font to represent all of these characters, and the effect config will need an option to override and use a custom collection of characters. `Symbols and Colors in a given cell change randomly and separately` : The changes to symbol and color for a given character cell are not linked and must be calculated separately. `The number of visible characters in a column varies from just a few, to the entire column` : Sometimes a streak is only three or four characters, other times it's the entire column. `Streak lengths are consistent during a streak, but vary between streaks` : The number of visible characters remains the same during a streak, once the number has been reached. As new character cells are activated, old cells are deactivated. `Only a single streak is active in a column` : A new streak will not start until the previous streak has completely fallen out of view. `Full column streaks are held in place for a random amount of time` : On the occasion that a streak is long enough to cover the entire column, it is held (no characters are removed) for a random duration. `Characters are a brighter color when added and the last few characters fade off` : The 'front' character in a streak is a near-white color and loses that color when it is no longer the front. The end of the streak fades toward black, but not evenly so. `Once a streak reaches the bottom of the terminal, the entire column may begin dropping` : Here's the exception to the "characters aren't falling" thing. When the front of a column reaches the bottom of the terminal, the entire column will sometimes drop together. Every character will actually shift down a single row. This happens at random intervals and is separate from the tail fade off that happens for every streak. After discovering all of the above, I realized building this effect was going to take a little longer than I had imagined. I wanted the TTE implementation of the Matrix effect to be as accurate as possible within the limits of the medium. The actual movie effect has sub-character color variation and glow, which isn't a thing in a real terminal. But pretty much everything else was doable. ### Matrix Effect Implementation, In Stages To demonstrate the iterative process of building an effect, I recorded the effect output at each stage of completion. Check out each section below to see the effect come together. ??? example "0 - Falling" Characters are activated in sequence with a random delay between activations, consistent within the streak life. ![rain](../img/changeblog_media/0.11.0/matrix_effect_dev_snapshot_0_character_rain.gif) ??? example "1 - Kata Symbols" Symbols are changed to katakana, punctuation, etc. and empty spaces in the canvas are filled with characters. ![symbols](../img/changeblog_media/0.11.0/matrix_effect_dev_snapshot_1_kata_symbols.gif) ??? example "2 - Color/Symbol Shifting" Colors and Symbols are changed separately and randomly. In addition, the leading character is a brighter color. ![shifting](../img/changeblog_media/0.11.0/matrix_effect_dev_snapshot_2_color_symbol_changes.gif) ??? example "3 - Column Lengths" Streak columns have varying lengths. ![column lengths](../img/changeblog_media/0.11.0/matrix_effect_dev_snapshot_3_column_length.gif) ??? example "4 - Columns Fall Off and Recycle" Once reaching the bottom of the terminal, columns continue to drop characters off the back until completely gone. After which, it begins again with a new configuration. ![fall off](../img/changeblog_media/0.11.0/matrix_effect_dev_snapshot_4_columns_fall_off_recycle.gif) ??? example "5 - Head/Tail Colors" The characters at the head of the streak are bright and the characters near the tail fade off. ![head tail colors](../img/changeblog_media/0.11.0/matrix_effect_dev_snapshot_5_colors_front_back.gif) ??? example "6 - Columns Drop" Once reaching the bottom of the terminal, sometimes the column will drop all characters by a row at random intervals. In this example, the columns shift to red when dropping for demonstration purposes. ![column drop](../img/changeblog_media/0.11.0/matrix_effect_dev_snapshot_6_column_drop.gif) ??? example "7 - Filling" The effect conclusion in the Matrix film title sequence doesn't practically work on multi-line text as it would require a unique column for every character in every row. This would take a long time to complete. So the TTE implementation concludes by filling the screen with rain, then dissolving into the input text. ![filling](../img/changeblog_media/0.11.0/matrix_effect_dev_snapshot_7_filling.gif) ??? example "8 - Dissolving" The final dissolve into the input text. ![dissolve](../img/changeblog_media/0.11.0/matrix_effect_dev_snapshot_8_resolving.gif) ## Canvas Overhaul *18x24, pre-stretched, double primed...* --- The [Canvas](../engine/terminal/canvas.md) has received a lot of love in this release. An issue was [raised](https://github.com/ChrisBuilds/terminaltexteffects/issues/14) which asked for an obvious capability. Allow the Canvas to exceed the size of the input text. This made sense, so here you go. ### Canvas Dimensions? Terminal Dimensions? Input Dimensions? #### TTE Output 101 To make the most of the possibilities for *where* you output an effect, you need to understand the difference between the Canvas, the Terminal, and the Input Text dimensions. ![canvas_centered_with_terminal](../img/changeblog_media/0.11.0/canvas_centered_terminal_edges.png) The image above helps to explain each component. To achieve this, the following settings were used: - Canvas height set to match terminal height. - Canvas width set to an exact value which is smaller than the terminal width. - Canvas anchored to the center of the terminal. - Text anchored to the center of the canvas. Let's break this down. ##### Terminal The [Terminal](../engine/terminal/terminal.md) class performs many functions, but as it pertains to this discussion, we are only interested in the terminal dimensions. `Terminal Dimensions` : The terminal dimensions are based on your terminal emulator. TTE discovers the dimensions of your terminal emulator by calling `shutil.get_terminal_size()`. Should there an issue occur that prevents terminal dimension discovery, a fallback of `(80, 24)` is used. : In the image above, you can see the terminal as the black area on the left and right edges. This indicates the Canvas width is less than the terminal width, and the Canvas was anchored to the center. ##### Canvas The [Canvas](../engine/terminal/canvas.md) is the space within the terminal where the effect takes place. From the perspective of an effect, the Canvas is the entire world. Effects *should* be designed to reference the Canvas for all requests for Coordinates to ensure Coordinates are anchored appropriately and within the correct space within the terminal. `Canvas Dimensions` : The Canvas dimensions are determined in one of three ways. : - Specified exactly via the `--canvas-width` and `--canvas-height` options (or related TerminalConfig attributes). : - Set to match the discovered Terminal dimensions by setting `--canvas-height` or `--canvas-width` to `0`. : - Set to match the dimensions of the input text by setting `--canvas-height` or `--canvas-width` to `-1`. : Note that the separate dimensions, width and height, can be specified through any of these methods independently. : In the image above, you can see the canvas as the grey area surrounding the inner-text, with solid borders. In the image, the Canvas is set to match the Terminal height (so there is no exposed terminal above or below) but the width is less than the Terminal width. By anchoring the Canvas to the center, there is visible unused terminal space on either side. ##### Input Text The input text is the data passed to TTE on which the effect operates. TTE attempts to preserve this text in it's original form unless the `--wrap-text` option has been passed. If the Canvas is set to match the input text dimensions, the Canvas will be sized to the minimum bounding box that would contain all of the input text. ??? "How Input Text is Processed" - Whitespace characters to the right of the last non-whitespace character in a given line are stripped. - Empty lines are preserved. - Whitespace to the left of a non-whitespace character is preserved. - Tabs are converted to four spaces, and the tab width can be specified using the `--tab-width` option. `Input Text Dimensions` : The input text dimensions are determined by the input text provided to TTE and will be modified based on the following: : - If the `--wrap-text` option is passed, the text will be wrapped based on the Canvas dimensions, which will alter the original text dimensions. : - If the Canvas is set match the dimensions of the text and the `--wrap-text` option is passed, the text will be wrapped based on the Terminal dimensions. : - If the `--ignore-terminal-dimensions` option is passed, wrapping will occur based on the Canvas size or not at all (if the Canvas is set to match the text), however the output will exceed the dimensions of the terminal and will be wrapped by the terminal emulator (resulting in unexpected effect behavior) unless the output is directed somewhere else. Now that you understand how TTE handles the space where the effect is drawn, you can appreciate the latest updates to the Canvas. #### Arbitrary Canvas Sizing As mentioned above, the Canvas size can be specified with the `--canvas-width` and `--canvas-height` options, as well as the corresponding [TerminalConfig](../engine/terminal/terminalconfig.md) attributes. The following options are supported: - `-1` = Match the specified Canvas dimension to the corresponding input text dimension. - `0` = Match the specified Canvas dimension to the corresponding terminal dimension. - `n > 0` = Use the exact specified dimension. Either dimension can be specified with any of the three options. By sizing the Canvas greater than the input text, you can expand the total effect area and give the effects more room the breathe. ### Anchoring *...my adventure in off-by-one errors...* --- #### Anchoring the Canvas and/or Input Text The Canvas and/or Input Text can be anchored around the respective container using the `--anchor-[canvas/text]` option. Acceptable values are any of the Cardinal/Diagonal directions, or centered. - `sw` = South West (bottom left corner) (**default**) - `w` = West (centered on left edge) - `nw` = North West (top left corner) - `n` = North (centered on top edge) - `ne` = North East (top right corner) - `e` = East (centered on right edge) - `se` = South East (bottom right corner) - `s` = South (centered on bottom edge) - `c` = Center (centered within the Canvas/Terminal) Here is an example of the Canvas anchored to the North East of the terminal, and the text anchored to the center of the Canvas. ![canvas_anchored_ne](../img/changeblog_media/0.11.0/canvas_anchored_ne.png) Here is an example of the Canvas centered in the terminal and the text anchored to the East. ![canvas_centered_text_east](../img/changeblog_media/0.11.0/text_anchored_e.png) Here is an example of the Canvas sized to the text and centered in the terminal. Notice how there is no gray space outside the text. In this case, anchoring the text has no effect as there is no additional space in the Canvas. ![canvas_matched_to_text](../img/changeblog_media/0.11.0/canvas_sized_to_text.png) ### Effects are Canvas Aware Effects often base many of their attributes on the Canvas. For example, the [Beams](../effects/beams.md) effect features beams which travel across the entire Canvas. In the example below, the Canvas has been made wider and taller than the input text, and the text has been anchored to the center. Here's the invocation line and result: `cat testinput/demo.txt | tte --canvas-width 100 --canvas-height 40 --anchor-text c beams` ![beams_big_canvas](../img/changeblog_media/0.11.0/beams_centered_large_canvas.gif) Here's an example using the [Spray](../effects/spray.md) effect with a wide Canvas and the text anchored to the default `sw` along with the height set to match the input text height. The spray origin is much further away than usual and the resulting effect looks better. `cat testinput/demo.txt | tte --canvas-width 150 --canvas-height -1 --anchor-text sw spray` ![spray_wide_canvas](../img/changeblog_media/0.11.0/spray_wide_canvas_anchored.gif) All these changes with the Canvas has led to some additional complexity when ensuring all effects operate in reference to the Canvas dimensions properly in addition to the complexity handling the interactions between anchoring, text wrapping, and ignoring the terminal dimensions when appropriate. I'm sure there are edge cases somebody will find and report. Looking forward to fixing those. ## Other Changes --- - The `--version` switch will show the TTE version. High-tech stuff. - Inputting a keyboard interrupt (`ctrl-c`) during an animation will gracefully interrupt the effect, restore the terminal state, and exit. - A bug was fixed in the [Swarm](../effects/swarm.md) effect which was causing the first character group to be discarded. This was noticed by a user who submitted a [great issue](https://github.com/ChrisBuilds/terminaltexteffects/issues/13) documenting the unfortunate amputation of the *cowsay* cow. ``` bash $ echo "The cow misses legs!" | cowsay | tte --no-color swarm ______________________ < The cow misses legs! > ---------------------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || ``` I'm happy to report that the bug was found and fixed, returning the cow to its intended state. --- That's all for this release. Thanks for stopping by. [0.11.0](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/CHANGELOG.md) terminaltexteffects-release-0.12.1/docs/changeblog/changeblog_0.12.0.md000066400000000000000000000372551507200677100255440ustar00rootroot00000000000000# 0.12.0 (Color Parsing) ## Release 0.12.0 ### Release Summary (Color Sequence Parsing, Background Colors, and Tests) This release features three new effects, [Highlight](../showroom.md#highlight), [LaserEtch](../showroom.md#laseretch), and [Sweep](../showroom.md#sweep) as well as support for parsing existing color sequences from the input data. Support for background colors has been added throughout the engine. There are many smaller changes such as improved bezier curves, custom easing functions, and various optimizations, some of which will be detailed below. ### It's Been A While --- It has been nearly 8 months since the last release. That's entirely due to life changes on my end that kept me away from TTE development. I have not been able to work on TTE consistently for about six of those months. Things have settled and I am now able to dedicate time each week to this project (and new projects). Expect more frequent updates from now on. ### New Effects (Highlight, LaserEtch, Sweep) --- First up, there are three new effects. #### Highlight The [Highlight](../effects/highlight.md) effect runs a specular highlight across the text. To best demonstrate this effect, I will use a solid block of text. ![highlight_block_demo](../img/changeblog_media/0.12.0/highlight_block_demo.gif) The highlight brightness, width, and direction are all customizable. #### LaserEtch The [LaserEtch](../effects/laseretch.md) effect burns the text into the terminal, sending sparks flying. As the sparks fall, they cool and disappear. If the sparks reach the bottom of the canvas before burning out, they will land on the bottom. ![laseretch_demo](../img/effects_demos/laseretch_demo.gif) The etch speed, laser colors, spark colors and all gradients are customizable. #### Sweep The [Sweep](../effects/sweep.md) effect makes two passes over the canvas. On the first pass, the text is revealed, dimmed, and without color. On the second pass, the text is colored. ![sweep_demo](../img/effects_demos/sweep_demo.gif) The sweep directions and sweep noise symbols are customizable. On the second sweep, the noise takes on colors from the final gradient. ### Color Sequence Parsing --- TTE can now parse 8/24-bit color sequences from the input data, and associate them with the characters to which they should be applied. This includes both foreground and background sequences. The following ANSI escape sequence formats are supported: : 8-bit Sequences : - `ESC[38:5:⟨n⟩m Select foreground color` : - `ESC[48:5:⟨n⟩m Select background color` : 24-bit Sequences : - `ESC[38;2;⟨r⟩;⟨g⟩;⟨b⟩ m Select RGB foreground color` : - `ESC[48;2;⟨r⟩;⟨g⟩;⟨b⟩ m Select RGB background color` : [Wikipedia - ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) --- There is a new [TerminalConfig](../engine/terminal/terminalconfig.md#terminaltexteffects.engine.terminal.TerminalConfig.existing_color_handling) command line argument `--existing-color-handling` that determines how these sequences are used within effects. There are three options for the `--existing-color-handling` argument: `dynamic` : When set to 'dynamic', each effect will determine how the input sequences are handled. `always` : When set to 'always', no other color will be applied to the characters. They will always reflect the colors as provided in the input text. In most cases, this setting will result in an effect that loses much of its design. `ignore` : When set to 'ignore', the input colors will be ignored. This is the default. #### Color Handling Example I recommend using a tool such as [ASCII Silhouettify](https://meatfighter.com/ascii-silhouettify/) to turn your art into ASCII. In the example below, ASCII Silhouettify was used to process the image into ASCII with color sequences. The ASCII was piped directly into TTE. Each of the color handling options is demonstrated. === "Source Image" ![astro_planet_balloon](../img/changeblog_media/0.12.0/astro_holding_planet.png) === "Resulting ASCII" ```  ___-mmma___ _s"-_"~gggg~""= .<-_ _r_g@@@@@@@@@@D"r_g@p"q_a ,fg@@@@@@@@@D"o"o@@@@@@@P,"_o ________ /,@@@@@@B>_="g@@@@@@@@P,"o@@_ '(._-..q_ q_a jJ{@P".="<@@@@@@@@@P"=_g@@"~"g@' [ ].,[@,jW[ /_""~g@@@@@@@@@@P_="g@@P_+_@@@P_@.]F,_gP gPf /@ @@@@@@@@P"o"_@@@P_="g@@@>_P'_+.~_@"_wP; ..@,@BP".="_g@@D"->_g@@@>_*'_s",__@P _@"r ' >"_g@@@B="<>_g@@@B>-"~"o"_ "gB"~_@"o" ;D=_"s>"o@@@@B>'_->_="~ _g@P _gP"s"@ _>t"@@@_<4+"~_w>_="_^,_gB" _gD",>_D_,' -_u_Fo"gg@B>"o*"_'~_g@P"~_gM",>_d"o@B"F __1@1%mmP>"_gy@@@@@@.__~"""s"~8"<@@P"_'/ 0gW@@@@@@W@@@Q$@B@@@B>_s"_m>_g@@P"_g@Ps 1@@g5@@@@@g@@D>"-="_mD"_g@@D>__g@@@"+ '<=mmm==>"" "a_4@BP>"__g@@@BP"r" "<==mmy="" ] ] _~~B>"""->._ ] ,+_^~-"_@@B==4@g_g_ ] ,/gF^f_@P_g@BBBB@@_4@@, ] __@,/_@[_~g@@@@@@@g_8L0@L _~~1_ ,/@,//@@@@@@@@@@@@@@@@'gQ@\ jd/|g]h |@g'/@@@@@@@@@@@@@@@@@@'[@@//'.<>/[ @|.:@/gp\@@@@@@@@@@@@@@@@@ @ L=r_' !@h,,@'@@"@@@@@@@@@@@@@@@@@.@@']\ ]"==a<)@+"@@@@@@@@@@@@@B2#F@g, " ', F@@@_`qt\oW@BP@@@@0S$W@@'/AB"f| \ l4@@@_,!r0@@@@@@@@@@@P,^"g+@WJ ^'@@P/_<.<-_"""""_+"_"oFg@\s _ ">_"=_""""_<"_@"g@@@@`, !!|0@@@@BP"_g@@P"~":aV_ '_;@@@Fa[@@P+faB=4@\9p\[ @[@@@' [@B___`' `[,"~ , [@@@'[-mm=>"_''qB>J~"g[ [!@@;TR"""~__~~_g@@P'T[ |[@@g_"LG@@@@[g""~_-_@[ |[@@@8-"@@@@@@F@ "_@@8| {:"_r] 8"==*"~T'*8="- ;/@@@"_ ]|@@+_1 [@@@@g| [\@@@@, !`@@@F' \*"="' ".""_" """  ``` ??? example "Dynamic Color Sequence Handling" The beams effect supports dynamic color handling which results in the standard gradient being replaced with the colors parsed from the input data. The beams, however, keep their color. ![beams_astro_dynamic](../img/changeblog_media/0.12.0/beams_dynamic_astro_demo.gif) ??? example "Always Color Sequence Handling" The characters never deviate from the input colors. This results in the beams taking on the color of the character as they pass over. The dimming effect is not applied. ![beams_astro_always](../img/changeblog_media/0.12.0/beams_always_astro_demo.gif) ??? example "Ignore Color Sequence Handling" The color sequences are parsed and removed from the input. The result is the normal effect behavior. ![beams_astro_ignore](../img/changeblog_media/0.12.0/beams_ignore_astro_demo.gif) Support for dynamic color handling has not been added to all effects. To track the progress of this feature, see [this Issue](https://github.com/ChrisBuilds/terminaltexteffects/issues/37). ### Background Colors --- TTE now supports specifying background colors throughout the engine. In calls which expect color values, a [ColorPair](../engine/utils/colorpair.md) object, providing a foreground and/or background color is used. There are no effects currently which use background colors. --- ### Ease All The Things *easing is easy* #### Easing Closure A new [easing](../engine/utils/easing.md) function is available that provides a closure around an arbitrary ease. Example: ```python import terminaltexteffects as tte bounce_ease = tte.easing.eased_step_function(easing_func=tte.easing.out_bounce, step_size=0.01) print(bounce_ease()) print(bounce_ease()) print(bounce_ease()) print(bounce_ease()) ``` Output: ```text (0.0, 0.0) (0.01, 0.0007562500000000001) (0.02, 0.0030250000000000003) (0.03, 0.00680625) ``` Every call to `bounce_ease` above, outputs a tuple of (current step, eased value). Once the current step reaches 1, the function will stop increasing the step and the return value will never change. The [wipe](../effects/wipe.md) effect supports arbitrary easing via this new function. Here is an example of the `out_bounce` easing function applied to the progression of the wipe. ![bounce_wipe](../img/changeblog_media/0.12.0/wipe_bounce_demo.gif) #### Custom Cubic Bezier Ease In addition to the function above, a new easing function has been added which allows for the specification of completely custom easing function using bezier control points. An example follows. Here, I'll use [cubic-bezier.com](https://cubic-bezier.com/#0,.80,1,.20) to visually build a curve. The x-axis is time, in our case each call to the ease function will progress one unit across the x-axis. The y-axis is the progression of the function. The bottom is 0, the top is 1. ![cubic-bezier.com](../img/changeblog_media/0.12.0/cubic-bezier.com_demo.png) I will apply the control points seen in the image above, (0, .81, .98, .22) to the easing function and pass the function to the `ease` argument when creating a new [Path](../engine/motion/path.md). The target [Coord](../engine/utils/geometry.md) will be the right side of the canvas. ```python target_coord = tte.Coord( self.terminal.canvas.right, self.terminal.canvas.center_row, ) pth = test_char.motion.new_path(ease=tte.easing.make_easing(0, 0.81, 0.98, 0.22), speed=0.5) pth.new_waypoint(target_coord) ``` With the custom easing function applied to the motion of a single character traveling across the canvas, we would expect to see the character move quickly at first, slow down towards the center, and speed up again as it progresses to the right of the canvas. ![custom_eased_path](../img/changeblog_media/0.12.0/custom_ease_demo.gif) ### Misc Optimizations and Fixes This updates includes many fixes and optimizations, details can be found in the full changelog. ### Testing Now that TTE has grown in scale beyond that which I can meaningfully test manually, a full suite of unittests has been added. Pytest is fun, and parameterization has enabled the testing of all effects, with all of their arguments, and a representative sample of the acceptable ranges for those arguments. This was simply impossible manually. All together, there are approximately 30,000 tests run against the codebase, taking about seven minutes. --- ### Plain Old Changelog [0.12.0](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/CHANGELOG.md) terminaltexteffects-release-0.12.1/docs/cookbook.md000066400000000000000000000022511507200677100223560ustar00rootroot00000000000000# Library Cookbook Below you'll find examples of interesting ways to use the TTE library. ## Animated Prompts You can use any effect to create an animated prompt by setting the `end_symbol` parameter of the `terminal_output()` context manager to `" "`. Adjust the effect configuration to achieve a more responsive prompt. ```python from terminaltexteffects.effects.effect_beams import Beams from terminaltexteffects.effects.effect_slide import Slide def slide_animated_prompt(prompt_text: str) -> str: effect = Slide(prompt_text) effect.effect_config.final_gradient_frames = 1 with effect.terminal_output(end_symbol=" ") as terminal: for frame in effect: terminal.print(frame) return input() def beams_animated_prompt(prompt_text: str) -> str: effect = Beams(prompt_text) effect.effect_config.final_gradient_frames = 1 with effect.terminal_output(end_symbol=" ") as terminal: for frame in effect: terminal.print(frame) return input() resp = slide_animated_prompt("Here's a sliding prompt:") resp = beams_animated_prompt("Here's one with beams:") ``` ### Output ![t](./img/lib_demos/animated_prompts_demo.gif) terminaltexteffects-release-0.12.1/docs/effectguide/000077500000000000000000000000001507200677100225005ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/docs/effectguide/effectguide.md000066400000000000000000000004471507200677100253010ustar00rootroot00000000000000# Writing Custom Effects with TTE The following lessons will teach you how to write your own effects using the TerminalTextEffects engine. These lessons will cover: * TTE Engine Update Cycle * Anatomy of an Effect * Character Motion * Character Animation * Effect CLI Arguments and Validation terminaltexteffects-release-0.12.1/docs/effectguide/effectguide_lesson0.md000066400000000000000000000020311507200677100267330ustar00rootroot00000000000000# Lesson 0 ## Introduction to the TTE Engine The TTE engine architecture features a single Terminal object and many, mostly isolated, EffectCharacter objects. The Terminal is responsible for creating EffectCharacters, providing them to the effects, and printing them to the screen. EffectCharacters are responsible for progressing themselves through their animation/motion logic. You can think of every character on the screen as it's own object, moving itself around, and modifying it's own appearance. The Terminal simply gets the latest location and string representation of each character on every call to Terminal.print(). ### Terminal Let's look at the lifecycle of the Terminal. ``` mermaid --- title: Terminal --- get_dimensions make_effectcharacters update_terminal_state make_output_string print [*] --> get_dimensions get_dimensions --> make_effectcharacters make_effectcharacters --> update_terminal_state update_terminal_state --> make_output_string make_output_string --> print print --> update_terminal_state print --> [*] ``` terminaltexteffects-release-0.12.1/docs/effects/000077500000000000000000000000001507200677100216455ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/docs/effects/beams.md000066400000000000000000000005161507200677100232600ustar00rootroot00000000000000# Beams ![Demo](../img/effects_demos/beams_demo.gif) ## Quick Start ``` py title="beams.py" from terminaltexteffects.effects.effect_beams import Beams effect = Beams("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_beams terminaltexteffects-release-0.12.1/docs/effects/binarypath.md000066400000000000000000000005611507200677100243320ustar00rootroot00000000000000# Binarypath ![Demo](../img/effects_demos/binarypath_demo.gif) ## Quick Start ``` py title="binarypath.py" from terminaltexteffects.effects.effect_binarypath import BinaryPath effect = BinaryPath("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_binarypath terminaltexteffects-release-0.12.1/docs/effects/blackhole.md000066400000000000000000000005521507200677100241150ustar00rootroot00000000000000# Blackhole ![Demo](../img/effects_demos/blackhole_demo.gif) ## Quick Start ``` py title="blackhole.py" from terminaltexteffects.effects.effect_blackhole import Blackhole effect = Blackhole("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_blackhole terminaltexteffects-release-0.12.1/docs/effects/bouncyballs.md000066400000000000000000000005701507200677100245060ustar00rootroot00000000000000# BouncyBalls ![Demo](../img/effects_demos/bouncyballs_demo.gif) ## Quick Start ``` py title="bouncyballs.py" from terminaltexteffects.effects.effect_bouncyballs import BouncyBalls effect = BouncyBalls("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_bouncyballs terminaltexteffects-release-0.12.1/docs/effects/bubbles.md000066400000000000000000000005341507200677100236070ustar00rootroot00000000000000# Bubbles ![Demo](../img/effects_demos/bubbles_demo.gif) ## Quick Start ``` py title="bubbles.py" from terminaltexteffects.effects.effect_bubbles import Bubbles effect = Bubbles("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_bubbles terminaltexteffects-release-0.12.1/docs/effects/burn.md000066400000000000000000000005071507200677100231370ustar00rootroot00000000000000# Burn ![Demo](../img/effects_demos/burn_demo.gif) ## Quick Start ``` py title="burn.py" from terminaltexteffects.effects.effect_burn import Burn effect = Burn("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_burn terminaltexteffects-release-0.12.1/docs/effects/colorshift.md000066400000000000000000000007771507200677100243560ustar00rootroot00000000000000# ColorShift ![Demo](../img/effects_demos/colorshift_demo.gif) ## Quick Start ``` py title="colorshift.py" from terminaltexteffects import Gradient from terminaltexteffects.effects.effect_colorshift import ColorShift effect = ColorShift("YourTextHere") effect.effect_config.travel = True effect.effect_config.travel_direction = Gradient.Direction.RADIAL with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_colorshift terminaltexteffects-release-0.12.1/docs/effects/crumble.md000066400000000000000000000005341507200677100236220ustar00rootroot00000000000000# Crumble ![Demo](../img/effects_demos/crumble_demo.gif) ## Quick Start ``` py title="crumble.py" from terminaltexteffects.effects.effect_crumble import Crumble effect = Crumble("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_crumble terminaltexteffects-release-0.12.1/docs/effects/decrypt.md000066400000000000000000000005341507200677100236430ustar00rootroot00000000000000# Decrypt ![Demo](../img/effects_demos/decrypt_demo.gif) ## Quick Start ``` py title="decrypt.py" from terminaltexteffects.effects.effect_decrypt import Decrypt effect = Decrypt("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_decrypt terminaltexteffects-release-0.12.1/docs/effects/errorcorrect.md000066400000000000000000000005771507200677100247130ustar00rootroot00000000000000# ErrorCorrect ![Demo](../img/effects_demos/errorcorrect_demo.gif) ## Quick Start ``` py title="errorcorrect.py" from terminaltexteffects.effects.effect_errorcorrect import ErrorCorrect effect = ErrorCorrect("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_errorcorrect terminaltexteffects-release-0.12.1/docs/effects/expand.md000066400000000000000000000005251507200677100234500ustar00rootroot00000000000000# Expand ![Demo](../img/effects_demos/expand_demo.gif) ## Quick Start ``` py title="expand.py" from terminaltexteffects.effects.effect_expand import Expand effect = Expand("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_expand terminaltexteffects-release-0.12.1/docs/effects/fireworks.md000066400000000000000000000005521507200677100242040ustar00rootroot00000000000000# Fireworks ![Demo](../img/effects_demos/fireworks_demo.gif) ## Quick Start ``` py title="fireworks.py" from terminaltexteffects.effects.effect_fireworks import Fireworks effect = Fireworks("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_fireworks terminaltexteffects-release-0.12.1/docs/effects/highlight.md000066400000000000000000000007461507200677100241450ustar00rootroot00000000000000# Highlight ![Demo](../img/effects_demos/highlight_demo.gif) ## Quick Start ``` py title="highlight.py" from terminaltexteffects import Gradient from terminaltexteffects.effects.effect_highlight import Highlight effect = Highlight("YourTextHere") with effect.terminal_output() as terminal: effect.effect_config.final_gradient_direction = Gradient.Direction.HORIZONTAL for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_highlight terminaltexteffects-release-0.12.1/docs/effects/laseretch.md000066400000000000000000000007461507200677100241500ustar00rootroot00000000000000# LaserEtch ![Demo](../img/effects_demos/laseretch_demo.gif) ## Quick Start ``` py title="laseretch.py" from terminaltexteffects import Gradient from terminaltexteffects.effects.effect_laseretch import LaserEtch effect = LaserEtch("YourTextHere") with effect.terminal_output() as terminal: effect.effect_config.final_gradient_direction = Gradient.Direction.HORIZONTAL for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_laseretch terminaltexteffects-release-0.12.1/docs/effects/matrix.md000066400000000000000000000005341507200677100234750ustar00rootroot00000000000000# Matrix ![Demo](../img/effects_demos/matrix_demo.gif) ## Quick Start ``` py title="matrix.py" from terminaltexteffects.effects.effect_matrix import Matrix effect = Matrix("YourTextHere\n" * 10) with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_matrix terminaltexteffects-release-0.12.1/docs/effects/middleout.md000066400000000000000000000005521507200677100241570ustar00rootroot00000000000000# MiddleOut ![Demo](../img/effects_demos/middleout_demo.gif) ## Quick Start ``` py title="middleout.py" from terminaltexteffects.effects.effect_middleout import MiddleOut effect = MiddleOut("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_middleout terminaltexteffects-release-0.12.1/docs/effects/orbittingvolley.md000066400000000000000000000006241507200677100254250ustar00rootroot00000000000000# OrbittingVolley ![Demo](../img/effects_demos/orbittingvolley_demo.gif) ## Quick Start ``` py title="orbittingvolley.py" from terminaltexteffects.effects.effect_orbittingvolley import OrbittingVolley effect = OrbittingVolley("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_orbittingvolley terminaltexteffects-release-0.12.1/docs/effects/overflow.md000066400000000000000000000005431507200677100240340ustar00rootroot00000000000000# Overflow ![Demo](../img/effects_demos/overflow_demo.gif) ## Quick Start ``` py title="overflow.py" from terminaltexteffects.effects.effect_overflow import Overflow effect = Overflow("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_overflow terminaltexteffects-release-0.12.1/docs/effects/pour.md000066400000000000000000000005071507200677100231560ustar00rootroot00000000000000# Pour ![Demo](../img/effects_demos/pour_demo.gif) ## Quick Start ``` py title="pour.py" from terminaltexteffects.effects.effect_pour import Pour effect = Pour("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_pour terminaltexteffects-release-0.12.1/docs/effects/print.md000066400000000000000000000005161507200677100233250ustar00rootroot00000000000000# Print ![Demo](../img/effects_demos/print_demo.gif) ## Quick Start ``` py title="print.py" from terminaltexteffects.effects.effect_print import Print effect = Print("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_print terminaltexteffects-release-0.12.1/docs/effects/rain.md000066400000000000000000000005071507200677100231220ustar00rootroot00000000000000# Rain ![Demo](../img/effects_demos/rain_demo.gif) ## Quick Start ``` py title="rain.py" from terminaltexteffects.effects.effect_rain import Rain effect = Rain("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_rain terminaltexteffects-release-0.12.1/docs/effects/randomsequence.md000066400000000000000000000006171507200677100252040ustar00rootroot00000000000000# RandomSequence ![Demo](../img/effects_demos/randomsequence_demo.gif) ## Quick Start ``` py title="randomsequence.py" from terminaltexteffects.effects.effect_random_sequence import RandomSequence effect = RandomSequence("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_random_sequence terminaltexteffects-release-0.12.1/docs/effects/rings.md000066400000000000000000000005161507200677100233130ustar00rootroot00000000000000# Rings ![Demo](../img/effects_demos/rings_demo.gif) ## Quick Start ``` py title="rings.py" from terminaltexteffects.effects.effect_rings import Rings effect = Rings("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_rings terminaltexteffects-release-0.12.1/docs/effects/scattered.md000066400000000000000000000005521507200677100241470ustar00rootroot00000000000000# Scattered ![Demo](../img/effects_demos/scattered_demo.gif) ## Quick Start ``` py title="scattered.py" from terminaltexteffects.effects.effect_scattered import Scattered effect = Scattered("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_scattered terminaltexteffects-release-0.12.1/docs/effects/slice.md000066400000000000000000000006001507200677100232620ustar00rootroot00000000000000# Slice ![Demo](../img/effects_demos/slice_demo.gif) ## Quick Start ``` py title="slice.py" from terminaltexteffects.effects.effect_slice import Slice effect = Slice("YourTextHere") effect.effect_config.slice_direction = "diagonal" with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_slice terminaltexteffects-release-0.12.1/docs/effects/slide.md000066400000000000000000000005161507200677100232710ustar00rootroot00000000000000# Slide ![Demo](../img/effects_demos/slide_demo.gif) ## Quick Start ``` py title="slide.py" from terminaltexteffects.effects.effect_slide import Slide effect = Slide("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_slide terminaltexteffects-release-0.12.1/docs/effects/spotlights.md000066400000000000000000000005611507200677100243710ustar00rootroot00000000000000# Spotlights ![Demo](../img/effects_demos/spotlights_demo.gif) ## Quick Start ``` py title="spotlights.py" from terminaltexteffects.effects.effect_spotlights import Spotlights effect = Spotlights("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_spotlights terminaltexteffects-release-0.12.1/docs/effects/spray.md000066400000000000000000000005161507200677100233270ustar00rootroot00000000000000# Spray ![Demo](../img/effects_demos/spray_demo.gif) ## Quick Start ``` py title="spray.py" from terminaltexteffects.effects.effect_spray import Spray effect = Spray("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_spray terminaltexteffects-release-0.12.1/docs/effects/swarm.md000066400000000000000000000005161507200677100233220ustar00rootroot00000000000000# Swarm ![Demo](../img/effects_demos/swarm_demo.gif) ## Quick Start ``` py title="swarm.py" from terminaltexteffects.effects.effect_swarm import Swarm effect = Swarm("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_swarm terminaltexteffects-release-0.12.1/docs/effects/sweep.md000066400000000000000000000007021507200677100233110ustar00rootroot00000000000000# Sweep ![Demo](../img/effects_demos/sweep_demo.gif) ## Quick Start ``` py title="sweep.py" import terminaltexteffects as tte from terminaltexteffects.effects.effect_sweep import Sweep effect = Sweep("YourTextHere") effect.effect_config.final_gradient_direction = tte.Gradient.Direction.HORIZONTAL with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_sweep terminaltexteffects-release-0.12.1/docs/effects/synthgrid.md000066400000000000000000000005521507200677100242040ustar00rootroot00000000000000# SynthGrid ![Demo](../img/effects_demos/synthgrid_demo.gif) ## Quick Start ``` py title="synthgrid.py" from terminaltexteffects.effects.effect_synthgrid import SynthGrid effect = SynthGrid("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_synthgrid terminaltexteffects-release-0.12.1/docs/effects/unstable.md000066400000000000000000000005431507200677100240060ustar00rootroot00000000000000# Unstable ![Demo](../img/effects_demos/unstable_demo.gif) ## Quick Start ``` py title="unstable.py" from terminaltexteffects.effects.effect_unstable import Unstable effect = Unstable("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_unstable terminaltexteffects-release-0.12.1/docs/effects/vhstape.md000066400000000000000000000005341507200677100236430ustar00rootroot00000000000000# VHSTape ![Demo](../img/effects_demos/vhstape_demo.gif) ## Quick Start ``` py title="vhstape.py" from terminaltexteffects.effects.effect_vhstape import VHSTape effect = VHSTape("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_vhstape terminaltexteffects-release-0.12.1/docs/effects/waves.md000066400000000000000000000005161507200677100233160ustar00rootroot00000000000000# Waves ![Demo](../img/effects_demos/waves_demo.gif) ## Quick Start ``` py title="waves.py" from terminaltexteffects.effects.effect_waves import Waves effect = Waves("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_waves terminaltexteffects-release-0.12.1/docs/effects/wipe.md000066400000000000000000000005071507200677100231350ustar00rootroot00000000000000# Wipe ![Demo](../img/effects_demos/wipe_demo.gif) ## Quick Start ``` py title="wipe.py" from terminaltexteffects.effects.effect_wipe import Wipe effect = Wipe("YourTextHere") with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` ::: terminaltexteffects.effects.effect_wipe terminaltexteffects-release-0.12.1/docs/engine/000077500000000000000000000000001507200677100214735ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/docs/engine/animation/000077500000000000000000000000001507200677100234525ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/docs/engine/animation/animation.md000066400000000000000000000001621507200677100257520ustar00rootroot00000000000000# Animation *Module*: `terminaltexteffects.engine.animation` ::: terminaltexteffects.engine.animation.Animation terminaltexteffects-release-0.12.1/docs/engine/animation/charactervisual.md000066400000000000000000000001761507200677100271600ustar00rootroot00000000000000# CharacterVisual *Module*: `terminaltexteffects.engine.animation` ::: terminaltexteffects.engine.animation.CharacterVisual terminaltexteffects-release-0.12.1/docs/engine/animation/frame.md000066400000000000000000000001521507200677100250640ustar00rootroot00000000000000# Frame *Module*: `terminaltexteffects.engine.animation` ::: terminaltexteffects.engine.animation.Frame terminaltexteffects-release-0.12.1/docs/engine/animation/scene.md000066400000000000000000000001521507200677100250670ustar00rootroot00000000000000# Scene *Module*: `terminaltexteffects.engine.animation` ::: terminaltexteffects.engine.animation.Scene terminaltexteffects-release-0.12.1/docs/engine/basecharacter.md000066400000000000000000000002101507200677100245750ustar00rootroot00000000000000# EffectCharacter *Module*: `terminaltexteffects.engine.base_character` ::: terminaltexteffects.engine.base_character.EffectCharacter terminaltexteffects-release-0.12.1/docs/engine/baseeffect.md000066400000000000000000000001551507200677100241050ustar00rootroot00000000000000# BaseEffect *Module*: `terminaltexteffects.engine.base_effect` ::: terminaltexteffects.engine.base_effect terminaltexteffects-release-0.12.1/docs/engine/eventhandler.md000066400000000000000000000002021507200677100244660ustar00rootroot00000000000000# EventHandler *Module*: `terminaltexteffects.engine.base_character` ::: terminaltexteffects.engine.base_character.EventHandler terminaltexteffects-release-0.12.1/docs/engine/motion/000077500000000000000000000000001507200677100230005ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/docs/engine/motion/motion.md000066400000000000000000000001461507200677100246300ustar00rootroot00000000000000# Motion *Module*: `terminaltexteffects.engine.motion` ::: terminaltexteffects.engine.motion.Motion terminaltexteffects-release-0.12.1/docs/engine/motion/path.md000066400000000000000000000001421507200677100242530ustar00rootroot00000000000000# Path *Module*: `terminaltexteffects.engine.motion` ::: terminaltexteffects.engine.motion.Path terminaltexteffects-release-0.12.1/docs/engine/motion/segment.md000066400000000000000000000001501507200677100247600ustar00rootroot00000000000000# Segment *Module*: `terminaltexteffects.engine.motion` ::: terminaltexteffects.engine.motion.Segment terminaltexteffects-release-0.12.1/docs/engine/motion/waypoint.md000066400000000000000000000001521507200677100251720ustar00rootroot00000000000000# Waypoint *Module*: `terminaltexteffects.engine.motion` ::: terminaltexteffects.engine.motion.Waypoint terminaltexteffects-release-0.12.1/docs/engine/terminal/000077500000000000000000000000001507200677100233065ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/docs/engine/terminal/canvas.md000066400000000000000000000001521507200677100251010ustar00rootroot00000000000000# Canvas *Module*: `terminaltexteffects.engine.terminal` ::: terminaltexteffects.engine.terminal.Canvas terminaltexteffects-release-0.12.1/docs/engine/terminal/terminal.md000066400000000000000000000001561507200677100254450ustar00rootroot00000000000000# Terminal *Module*: `terminaltexteffects.engine.terminal` ::: terminaltexteffects.engine.terminal.Terminal terminaltexteffects-release-0.12.1/docs/engine/terminal/terminalconfig.md000066400000000000000000000001721507200677100266310ustar00rootroot00000000000000# TerminalConfig *Module*: `terminaltexteffects.engine.terminal` ::: terminaltexteffects.engine.terminal.TerminalConfig terminaltexteffects-release-0.12.1/docs/engine/utils/000077500000000000000000000000001507200677100226335ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/docs/engine/utils/ansitools.md000066400000000000000000000001461507200677100251710ustar00rootroot00000000000000# ANSItools *Module*: `terminaltexteffects.utils.ansitools` ::: terminaltexteffects.utils.ansitools terminaltexteffects-release-0.12.1/docs/engine/utils/argsdataclass.md000066400000000000000000000001621507200677100257700ustar00rootroot00000000000000# ArgsDataClass *Module*: `terminaltexteffects.utils.argsdataclass` ::: terminaltexteffects.utils.argsdataclass terminaltexteffects-release-0.12.1/docs/engine/utils/argvalidators.md000066400000000000000000000014001507200677100260120ustar00rootroot00000000000000# Argument Validation *Module*: `terminaltexteffects.utils.argvalidators` ::: terminaltexteffects.utils.argvalidators ## Example Usage The following example demonstrates using the `PositiveFloat` class to provide a `type_parser` and `metavar` to the `RandomSequenceConfig.speed` argument. This will validate that the argument passed as `--speed` is a float > 0. ```python class RandomSequenceConfig(ArgsDataClass): speed: float = ArgField( cmd_name=["--speed"], type_parser=argvalidators.PositiveFloat.type_parser, default=0.004, metavar=argvalidators.PositiveFloat.METAVAR, help="Speed of the animation as a percentage of the total number of characters to reveal in each tick.", ) # type: ignore[assignment] ``` terminaltexteffects-release-0.12.1/docs/engine/utils/color.md000066400000000000000000000024101507200677100242700ustar00rootroot00000000000000# Color *Module*: `terminaltexteffects.utils.graphics` ## Basic Usage Color objects are used to represent colors throughout TTE. However, they can be instantiated and printed directly. ### Supports Multiple Specification Formats ```python from terminaltexteffects.utils.graphics import Color red = Color('ff0000') xterm_red = Color(9) rgb_red_again = Color('#ff0000') ``` ### Printing Colors Colors can be printed to show the code and resulting color appearance. ```python from terminaltexteffects.utils.graphics import Color red = Color("ff0000") print(red) ``` ![t](../../img/lib_demos/printing_colors_demo.png) ### Using Colors to build a Gradient ```python from terminaltexteffects.utils.graphics import Gradient, Color rgb = Gradient(Color("ff0000"), Color("00ff00"), Color("0000ff"), steps=5) for color in rgb: # color is a hex string ... ``` ### Passing Colors to effect configurations ```python text = ("EXAMPLE" * 10 + "\n") * 10 red = Color("ff0000") green = Color("00ff00") blue = Color("0000ff") effect = ColorShift(text) effect.effect_config.gradient_stops = (red, green, blue) with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` --- ## Color Reference ::: terminaltexteffects.utils.graphics.Color terminaltexteffects-release-0.12.1/docs/engine/utils/colorpair.md000066400000000000000000000014321507200677100251470ustar00rootroot00000000000000# ColorPair *Module*: `terminaltexteffects.utils.graphics` ## Basic Usage ColorPair objects are used to represent a foreground and background color pair. ### Usage ```python import terminaltexteffects as tte color_pair = tte.ColorPair(fg=tte.Color("FF0000"), bg=tte.Color("00FF00")) ``` ### Alternate Signature Colors can be specified using strings or integers. Color objects will be created automatically. ```python import terminaltexteffects as tte color_pair = tte.ColorPair(fg="FF0000", bg="00FF00") ``` `fg` and/or `bg` are optional and default to `None`. ### Printing ColorPairs ColorPair objects can be printed to see the resulting colors. ![t](../../img/lib_demos/colorpair_print_example.png) --- ## ColorPair Reference ::: terminaltexteffects.utils.graphics.ColorPair terminaltexteffects-release-0.12.1/docs/engine/utils/colorterm.md000066400000000000000000000001461507200677100251640ustar00rootroot00000000000000# ColorTerm *Module*: `terminaltexteffects.utils.colorterm` ::: terminaltexteffects.utils.colorterm terminaltexteffects-release-0.12.1/docs/engine/utils/easing.md000066400000000000000000000001471507200677100244250ustar00rootroot00000000000000# Easing Functions *Module*: `terminaltexteffects.utils.easing` ::: terminaltexteffects.utils.easing terminaltexteffects-release-0.12.1/docs/engine/utils/exceptions.md000066400000000000000000000001511507200677100253330ustar00rootroot00000000000000# Exceptions *Module*: `terminaltexteffects.utils.exceptions` ::: terminaltexteffects.utils.exceptions terminaltexteffects-release-0.12.1/docs/engine/utils/geometry.md000066400000000000000000000001431507200677100250060ustar00rootroot00000000000000# Geometry *Module*: `terminaltexteffects.utils.geometry` ::: terminaltexteffects.utils.geometry terminaltexteffects-release-0.12.1/docs/engine/utils/gradient.md000066400000000000000000000010441507200677100247510ustar00rootroot00000000000000# Gradient *Module*: `terminaltexteffects.utils.graphics` ## Basic Usage ```python from terminaltexteffects.utils.graphics import Gradient, Color rgb = Gradient(Color("ff0000"), Color("00ff00"), Color("0000ff"), steps=5) for color in rgb: # color is a hex string ... ``` ## Printing Gradients Gradients can be printed to the terminal to show information about the stops, steps, and resulting spectrum. ![t](../../img/lib_demos/printing_gradients_demo.png) --- ## Gradient Reference ::: terminaltexteffects.utils.graphics.Gradient terminaltexteffects-release-0.12.1/docs/engine/utils/hexterm.md000066400000000000000000000001401507200677100246240ustar00rootroot00000000000000# HexTerm *Module*: `terminaltexteffects.utils.hexterm` ::: terminaltexteffects.utils.hexterm terminaltexteffects-release-0.12.1/docs/index.md000066400000000000000000000034711507200677100216640ustar00rootroot00000000000000# Intro to TTE ![title_blackhole](./img/application_demos/shadow_title_blackhole.gif) ## What is TTE? TerminalTextEffects (TTE) is a terminal visual effects engine. TTE can be installed as a system application to produce effects in your terminal, or as a Python library to enable effects within your Python scripts/applications. TTE includes a growing library of built-in effects which showcase the engine's features. These features include: * Xterm 256 / RGB hex color support * Complex character movement via Paths, Waypoints, and motion easing, with support for quadratic/cubic bezier curves. * Complex animations via Scenes with symbol/color changes, layers, easing, and Path synced progression. * Variable stop/step color gradient generation. * Event handling for Path/Scene state changes with custom callback support and many pre-defined actions. * Effect customization exposed through a typed effect configuration dataclass that is automatically handled as CLI arguments. * Runs inline, preserving terminal state and workflow. ## Getting Started TTE can be used as a system application or as a Python library. To get started, visit the installation and usage guides below. [Installation Guide](./installation.md){ .md-button } [Application Usage](./appguide.md){ .md-button } [Library Usage](./libguide.md){ .md-button } ## Effects Library TTE includes a growing library of built-in effects. Visit the showroom to see examples of each effect. [Effects Showroom](./showroom.md){ .md-button } ## Library Cookbook Check out the cookbook to see interesting examples using the TTE library. [Library Cookbook](./cookbook.md){ .md-button } ## Release Write-Ups Friendly release write-ups can be found in the [ChangeBlog](./changeblog/changeblog.md) [ChangeBlog](./changeblog/changeblog.md){ .md-button } terminaltexteffects-release-0.12.1/docs/installation.md000066400000000000000000000017471507200677100232620ustar00rootroot00000000000000# Installation TerminalTextEffects can be installed as a system application using pipx or as a library using pip. ## System Application using Pipx When installed as an application, TerminalTextEffects can be called from the shell to produce effects on any input piped to stdin. Usages include invocation on shell launch, aliasing commands to pass output through TTE, and SSH login animations. Pipx is the easiest way to make TTE available in your shell. `pipx install terminaltexteffects` !!! note If pipx is unavailable, you can install via `pip` and run TTE by calling the python binary with the module argument. ```bash title="ls redirection" ls -latr | python3 -m terminaltexteffects ``` [Application Usage](./appguide.md){ .md-button } ## Library installation using Pip When installed as a library, TerminalTextEffects can be imported to produce animations in your Python applications. `pip install terminaltexteffects` [Library Usage](./libguide.md){ .md-button } terminaltexteffects-release-0.12.1/docs/libguide.md000066400000000000000000000127471507200677100223470ustar00rootroot00000000000000# Library Guide ## Playing Effect Animations All effects are iterators which return a string representing the current frame. Basic usage is as simple as importing the effect, instantiating it with the input text, and iterating over the effect. Effects includes a helpful context manager ([effect.terminal_output()](./engine/baseeffect.md#terminaltexteffects.engine.base_effect.BaseEffect.terminal_output)) to handle terminal setup/teardown and cursor positioning. The following example plays the [Slide](./effects/slide.md) effect animation using the [effect.terminal_output()](./engine/baseeffect.md#terminaltexteffects.engine.base_effect.BaseEffect.terminal_output) context manager. === "Syntax" ```python from terminaltexteffects.effects.effect_slide import Slide text = ("EXAMPLE" * 10 + "\n") * 10 effect = Slide(text) effect.effect_config.merge = True # (1) with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` 1. Use the `effect_config` attribute to modify the effect configuration. Setting `merge` to `True` on the Slide effect causes the text to slide in from alternating sides of the terminal. === "Output" ![t](./img/lib_demos/libguide_onlyslide_output.gif) ## Effects are Iterable If you want to handle the output yourself, such as sending the frames to a TUI or GUI, simply iterate over the effect without the context manager. ```python from terminaltexteffects.effects.effect_slide import Slide text = ("EXAMPLE" * 10 + "\n") * 10 effect = Slide(text) effect.effect_config.merge = True # (1) for frame in effect: # frame is a string, do something with it ``` 1. Use the `effect_config` attribute to modify the effect configuration. Setting `merge` to `True` on the Slide effect causes the text to slide in from alternating sides of the terminal. ## Configuring Effects All effect configuration options are available within each effect via the `effect.effect_config` and `effect.terminal_config` attributes. === "Syntax" ```python from terminaltexteffects.effects.effect_slide import Slide from terminaltexteffects.utils.graphics import Color text = ("EXAMPLE" * 10 + "\n") * 10 effect = Slide(text) effect.effect_config.merge = True # (1) effect.effect_config.grouping = "column" # (2) effect.effect_config.final_gradient_stops = (Color("0ff000"), Color("000ff0"), Color("0f00f0")) # (3) effect.terminal_config.canvas_width = 30 # (4) with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) ``` 1. Use the `effect_config` attribute to modify the effect configuration. Setting `merge` to `True` on the Slide effect causes the text to slide in from alternating sides of the terminal. 2. Columns will slide in, rather than rows. 3. Change the gradient colors from the defaults. 4. Set the canvas width manually rather than automatically detect. Canvas height will be automatically set to the input text height. === "Output" ![t](./img/lib_demos/libguide_configuration_output.gif) ## Configuring the Terminal/Canvas ### Terminal/Canvas Dimensions TTE uses a [Terminal](./engine/terminal/terminal.md) class and a [Canvas](./engine/terminal/canvas.md) class to handle terminal/canvas dimensions, wrapping text, etc. Effects contain an attribute (`effect.terminal_config`) which allows access to the various terminal configuration options. The configuration should be modified prior to iterating over the effect. For example, to set the Canvas dimensions manually: ```python effect.terminal_config.canvas_width = 80 effect.terminal_config.canvas_height = 24 ``` If either `canvas_width` or `canvas_height` are set to `0`, that dimension will be automatically detected based on the terminal device dimensions. If either dimensions is set to `-1`, that dimensions will be set to match the input text dimensions. By default, if your Canavs dimensions exceed the visible area of the Terminal, the text outside of that area will not be output in the frame. If you want to output all characters regardless of they position relative to the visible terminal area, set `terminal_config.ignore_terminal_dimensions` to `True`. This should **only** be used if you are handing the frame yourself and outputting somewhere other than the terminal. The terminal emulator will wrap the text and produce unexpected results. ```python effect.terminal_config.ignore_terminal_dimensions = True ``` ### Frame Rate TTE sets a default target frame rate of 100FPS. To configure this within your scripts, access the `effect.terminal_config.frame_rate` attribute. Set this attribute to `0` to remove the frame limit. For more information on terminal configuration options, check out the [TerminalConfig](./engine/terminal/terminalconfig.md) reference. ## Infinitely Looping Effects Some effects support infinite looping. For example, [ColorShift](./effects/colorshift.md) via the [ColorShiftConfig.cycles](./effects/colorshift.md#terminaltexteffects.effects.effect_colorshift.ColorShiftConfig) config attribute. When set to 0 directly, the effect will cycle indefinitely. Explore the configuration options for a given effect to see if it supports infinite looping. !!! note Infinite looping is *NOT* supported when TTE is run as an application. The command line argument validators will not accept these values. This is by design, to prevent users inadvertently inputting a configuration that results in having to interrupt the process to end an effect. terminaltexteffects-release-0.12.1/docs/showroom.md000066400000000000000000002617231507200677100224400ustar00rootroot00000000000000# Effects Showroom The effects shown below represent the built-in library of effects and their default configuration. ## Beams Creates beams which travel over the canvas illuminating the characters. ![Demo](./img/effects_demos/beams_demo.gif) [Reference](./effects/beams.md){ .md-button } [Config](./effects/beams.md#terminaltexteffects.effects.effect_beams.BeamsConfig){ .md-button } ??? example "Beams Command Line Arguments" ``` --beam-row-symbols (ASCII/UTF-8 character) [(ASCII/UTF-8 character) ...] Symbols to use for the beam effect when moving along a row. Strings will be used in sequence to create an animation. (default: ('▂', '▁', '_')) --beam-column-symbols (ASCII/UTF-8 character) [(ASCII/UTF-8 character) ...] Symbols to use for the beam effect when moving along a column. Strings will be used in sequence to create an animation. (default: ('▌', '▍', '▎', '▏')) --beam-delay (int > 0) Number of frames to wait before adding the next group of beams. Beams are added in groups of size random(1, 5). (default: 10) --beam-row-speed-range (hyphen separated int range e.g. '1-10') Minimum speed of the beam when moving along a row. (default: (10, 40)) --beam-column-speed-range (hyphen separated int range e.g. '1-10') Minimum speed of the beam when moving along a column. (default: (6, 10)) --beam-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the beam, a gradient will be created between the colors. (default: ('ffffff', '00D1FF', '8A008A')) --beam-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, numbers for the of gradient steps to use. More steps will create a smoother and longer gradient animation. Steps are paired with the colors in final-gradient- stops. (default: (2, 8)) --beam-gradient-frames (int > 0) Number of frames to display each gradient step. Increase to slow down the gradient animation. (default: 2) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the wipe gradient. (default: ('8A008A', '00D1FF', 'ffffff')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, numbers for the of gradient steps to use. More steps will create a smoother and longer gradient animation. Steps are paired with the colors in final-gradient- stops. (default: (12,)) --final-gradient-frames (int > 0) Number of frames to display each gradient step. Increase to slow down the gradient animation. (default: 5) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --final-wipe-speed (int > 0) Speed of the final wipe as measured in diagonal groups activated per frame. (default: 1) Example: terminaltexteffects beams --beam-row-symbols ▂ ▁ _ --beam-column-symbols ▌ ▍ ▎ ▏ --beam-delay 10 --beam-row-speed-range 10-40 --beam-column-speed-range 6-10 --beam-gradient-stops ffffff 00D1FF 8A008A --beam-gradient-steps 2 8 --beam-gradient-frames 2 --final-gradient-stops 8A008A 00D1FF ffffff --final-gradient-steps 12 --final-gradient-frames 5 --final-gradient-direction vertical --final-wipe-speed 1 ``` --- ## Binarypath Decodes characters into their binary form. Characters travel from outside the canvas towards their input coordinate, moving at right angles. ![Demo](./img/effects_demos/binarypath_demo.gif) [Reference](./effects/binarypath.md){ .md-button } [Config](./effects/binarypath.md#terminaltexteffects.effects.effect_binarypath.BinaryPathConfig){ .md-button } ??? example "Binarypath Command Line Arguments" ``` --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('00d500', '007500')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.CENTER) --binary-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the binary characters. Character color is randomly assigned from this list. (default: ('044E29', '157e38', '45bf55', '95ed87')) --movement-speed (float > 0) Speed of the binary groups as they travel around the terminal. (default: 1.0) --active-binary-groups (0 <= float(n) <= 1) Maximum number of binary groups that are active at any given time. Lower this to improve performance. (default: 0.05) Example: terminaltexteffects binarypath --final-gradient-stops 00d500 007500 --final-gradient-steps 12 --final-gradient-direction vertical --binary-colors 044E29 157e38 45bf55 95ed87 --movement-speed 1.0 --active-binary-groups 0.05 ``` --- ## Blackhole Creates a blackhole in a starfield, consumes the stars, explodes the input data back into position. ![Demo](./img/effects_demos/blackhole_demo.gif) [Reference](./effects/blackhole.md){ .md-button } [Config](./effects/blackhole.md#terminaltexteffects.effects.effect_blackhole.BlackholeConfig){ .md-button } ??? example "Blackhole Command Line Arguments" ``` --blackhole-color (XTerm [0-255] OR RGB Hex [000000-ffffff]) Color for the stars that comprise the blackhole border. (default: ffffff) --star-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] List of colors from which character colors will be chosen and applied after the explosion, but before the cooldown to final color. (default: ('ffcc0d', 'ff7326', 'ff194d', 'bf2669', '702a8c', '049dbf')) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('8A008A', '00D1FF', 'ffffff')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.DIAGONAL) Example: terminaltexteffects blackhole --star-colors ffcc0d ff7326 ff194d bf2669 702a8c 049dbf --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --final-gradient-direction vertical ``` --- ## BouncyBalls Characters fall from the top of the canvas as bouncy balls before settling into place. ![Demo](./img/effects_demos/bouncyballs_demo.gif) [Reference](./effects/bouncyballs.md){ .md-button } [Config](./effects/bouncyballs.md#terminaltexteffects.effects.effect_bouncyballs.BouncyBallsConfig){ .md-button } ??? example "Bouncyballs Command Line Arguments" ``` --ball-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated list of colors from which ball colors will be randomly selected. If no colors are provided, the colors are random. (default: ('d1f4a5', '96e2a4', '5acda9')) --ball-symbols (ASCII/UTF-8 character) [(ASCII/UTF-8 character) ...] Space separated list of symbols to use for the balls. (default: ('*', 'o', 'O', '0', '.')) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('f8ffae', '43c6ac')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.DIAGONAL) --ball-delay (int >= 0) Number of frames between ball drops, increase to reduce ball drop rate. (default: 7) --movement-speed (float > 0) Movement speed of the characters. (default: 0.25) --easing EASING Easing function to use for character movement. (default: out_bounce) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects bouncyballs --ball-colors d1f4a5 96e2a4 5acda9 --ball-symbols o "*" O 0 . --final-gradient-stops f8ffae 43c6ac --final-gradient-steps 12 --final-gradient-direction diagonal --ball-delay 7 --movement-speed 0.25 --easing OUT_BOUNCE ``` --- ## Bubbles Forms bubbles with the characters. Bubbles float down and pop. ![Demo](./img/effects_demos/bubbles_demo.gif) [Reference](./effects/bubbles.md){ .md-button } [Config](./effects/bubbles.md#terminaltexteffects.effects.effect_bubbles.BubblesConfig){ .md-button } ??? example "Bubbles Command Line Arguments" ``` --rainbow If set, the bubbles will be colored with a rotating rainbow gradient. (default: False) --bubble-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the bubbles. Ignored if --no-rainbow is left as default False. (default: ('d33aff', '7395c4', '43c2a7', '02ff7f')) --pop-color (XTerm [0-255] OR RGB Hex [000000-ffffff]) Color for the spray emitted when a bubble pops. (default: ffffff) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('d33aff', '02ff7f')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.DIAGONAL) --bubble-speed (float > 0) Speed of the floating bubbles. (default: 0.1) --bubble-delay (int > 0) Number of frames between bubbles. (default: 50) --pop-condition {row,bottom,anywhere} Condition for a bubble to pop. 'row' will pop the bubble when it reaches the the lowest row for which a character in the bubble originates. 'bottom' will pop the bubble at the bottom row of the terminal. 'anywhere' will pop the bubble randomly, or at the bottom of the terminal. (default: row) --easing (Easing Function) Easing function to use for character movement after a bubble pops. (default: in_out_sine) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects bubbles --bubble-colors d33aff 7395c4 43c2a7 02ff7f --pop-color ffffff --final-gradient-stops d33aff 02ff7f --final-gradient-steps 12 --final-gradient-direction diagonal --bubble-speed 0.1 --bubble-delay 50 --pop-condition row --easing IN_OUT_SINE ``` --- ## Burn Characters are ignited and burn up the screen. ![Demo](./img/effects_demos/burn_demo.gif) [Reference](./effects/burn.md){ .md-button } [Config](./effects/burn.md#terminaltexteffects.effects.effect_burn.BurnConfig){ .md-button } ??? example "Burn Command Line Arguments" ``` --starting-color (XTerm [0-255] OR RGB Hex [000000-ffffff]) Color of the characters before they start to burn. (default: 837373) --burn-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Colors transitioned through as the characters burn. (default: ('ffffff', 'fff75d', 'fe650d', '8A003C', '510100')) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('00c3ff', 'ffff1c')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) Example: terminaltexteffects burn --starting-color 837373 --burn-colors ffffff fff75d fe650d 8a003c 510100 --final-gradient-stops 00c3ff ffff1c --final-gradient-steps 12 ``` --- ## ColorShift Display a gradient that shifts colors across the terminal. ![Demo](./img/effects_demos/colorshift_demo.gif) !!! note Demo GIF uses `--travel` and `--travel-direction radial` arguments. [Reference](./effects/colorshift.md){ .md-button } [Config](./effects/colorshift.md#terminaltexteffects.effects.effect_colorshift.ColorShiftConfig){ .md-button } ??? example "ColorShift Command Line Arguments" ``` --gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the gradient. (default: (Color('e81416'), Color('ffa500'), Color('faeb36'), Color('79c314'), Color('487de7'), Color('4b369d'), Color('70369d'))) --gradient-steps (int > 0) [(int > 0) ...] Number of gradient steps to use. More steps will create a smoother gradient animation. (default: 12) --gradient-frames (int > 0) Number of frames to display each gradient step. Increase to slow down the gradient animation. (default: 5) --travel Display the gradient as a traveling wave (default: False) --travel-direction (diagonal, horizontal, vertical, radial) Direction the gradient travels across the canvas. (default: Direction.HORIZONTAL) --reverse-travel-direction Reverse the gradient travel direction. (default: False) --no-loop Do not loop the gradient. If not set, the gradient generation will loop the final gradient color back to the first gradient color. (default: False) --cycles (int > 0) Number of times to cycle the gradient. (default: 3) --skip-final-gradient Skip the final gradient. (default: False) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied from bottom to top). If only one color is provided, the characters will be displayed in that color. (default: (Color('e81416'), Color('ffa500'), Color('faeb36'), Color('79c314'), Color('487de7'), Color('4b369d'), Color('70369d'))) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: 12) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) Example: terminaltexteffects colorshift --gradient-stops 0000ff ffffff 0000ff --gradient-steps 12 --gradient-frames 10 --cycles 3 --travel --travel-direction radial --final-gradient-stops 00c3ff ffff1c --final-gradient-steps 12 ``` --- ## Crumble Characters crumble into dust before being vacuumed up and reformed. ![Demo](./img/effects_demos/crumble_demo.gif) [Reference](./effects/crumble.md){ .md-button } [Config](./effects/crumble.md#terminaltexteffects.effects.effect_crumble.CrumbleConfig){ .md-button } ??? example "Crumble Command Line Arguments" ``` --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('5CE1FF', 'FF8C00')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.DIAGONAL) Example: terminaltexteffects crumble --final-gradient-stops 5CE1FF FF8C00 --final-gradient-steps 12 --final-gradient-direction diagonal ``` --- ## Decrypt Movie style text decryption effect. ![Demo](./img/effects_demos/decrypt_demo.gif) [Reference](./effects/decrypt.md){ .md-button } [Config](./effects/decrypt.md#terminaltexteffects.effects.effect_decrypt.DecryptConfig){ .md-button } ??? example "Decrypt Command Line Arguments" ``` --typing-speed (int > 0) Number of characters typed per keystroke. (default: 1) --ciphertext-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the ciphertext. Color will be randomly selected for each character. (default: ('008000', '00cb00', '00ff00')) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('eda000',)) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) Example: terminaltexteffects decrypt --typing-speed 2 --ciphertext-colors 008000 00cb00 00ff00 --final-gradient-stops eda000 --final-gradient-steps 12 --final-gradient-direction vertical ``` --- ## ErrorCorrect Swaps characters from an incorrect initial position to the correct position. ![Demo](./img/effects_demos/errorcorrect_demo.gif) [Reference](./effects/errorcorrect.md){ .md-button } [Config](./effects/errorcorrect.md#terminaltexteffects.effects.effect_errorcorrect.ErrorCorrectConfig){ .md-button } ??? example "ErrorCorrect Command Line Arguments" ``` --error-pairs (int > 0) Percent of characters that are in the wrong position. This is a float between 0 and 1.0. 0.2 means 20 percent of the characters will be in the wrong position. (default: 0.1) --swap-delay (int > 0) Number of frames between swaps. (default: 10) --error-color (XTerm [0-255] OR RGB Hex [000000-ffffff]) Color for the characters that are in the wrong position. (default: e74c3c) --correct-color (XTerm [0-255] OR RGB Hex [000000-ffffff]) Color for the characters once corrected, this is a gradient from error-color and fades to final-color. (default: 45bf55) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('8A008A', '00D1FF', 'FFFFFF')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --movement-speed (float > 0) Speed of the characters while moving to the correct position. Valid values are n > 0. Adjust speed and animation rate separately to fine tune the effect. (default: 0.5) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects errorcorrect --error-pairs 0.1 --swap-delay 10 --error-color e74c3c --correct-color 45bf55 --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --movement-speed 0.5 ``` --- ## Expand Characters expand from the center. ![Demo](./img/effects_demos/expand_demo.gif) [Reference](./effects/expand.md){ .md-button } [Config](./effects/expand.md#terminaltexteffects.effects.effect_expand.ExpandConfig){ .md-button } ??? example "Expand Command Line Arguments" ``` --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('8A008A', '00D1FF', 'FFFFFF')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-frames (int > 0) Number of frames to display each gradient step. Increase to slow down the gradient animation. (default: 5) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --movement-speed (float > 0) Movement speed of the characters. (default: 0.35) --expand-easing EXPAND_EASING Easing function to use for character movement. (default: in_out_quart) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects expand --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --final-gradient-frames 5 --movement-speed 0.35 --expand-easing IN_OUT_QUART ``` --- ## Fireworks Launches characters up the screen where they explode like fireworks and fall into place. ![Demo](./img/effects_demos/fireworks_demo.gif) [Reference](./effects/fireworks.md){ .md-button } [Config](./effects/fireworks.md#terminaltexteffects.effects.effect_fireworks.FireworksConfig){ .md-button } ??? example "Fireworks Command Line Arguments" ``` --explode-anywhere If set, fireworks explode anywhere in the canvas. Otherwise, fireworks explode above highest settled row of text. (default: False) --firework-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated list of colors from which firework colors will be randomly selected. (default: ('88F7E2', '44D492', 'F5EB67', 'FFA15C', 'FA233E')) --firework-symbol (ASCII/UTF-8 character) Symbol to use for the firework shell. (default: o) --firework-volume (0 <= float(n) <= 1) Percent of total characters in each firework shell. (default: 0.02) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('8A008A', '00D1FF', 'FFFFFF')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.HORIZONTAL) --launch-delay (int >= 0) Number of frames to wait between launching each firework shell. +/- 0-50 percent randomness is applied to this value. (default: 60) --explode-distance (0 <= float(n) <= 1) Maximum distance from the firework shell origin to the explode waypoint as a percentage of the total canvas width. (default: 0.1) Example: terminaltexteffects fireworks --firework-colors 88F7E2 44D492 F5EB67 FFA15C FA233E --firework-symbol o --firework-volume 0.02 --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --launch-delay 60 --explode-distance 0.1 --explode-anywhere ``` --- ## Highlight Run a specular highlight across the text. ![Demo](./img/effects_demos/highlight_demo.gif) [Reference](./effects/highlight.md){ .md-button } [Config](./effects/highlight.md#terminaltexteffects.effects.effect_highlight.HighlightConfig){ .md-button } ??? example "Highlight Command Line Arguments" ``` --highlight-brightness (float > 0) Brightness of the highlight color. Values less than 1 will darken the highlight color, while values greater than 1 will brighten the highlight color. (default: 1.75) --highlight-direction {column_left_to_right,column_right_to_left,row_top_to_bottom,row_bottom_to_top,diagonal_top_left_to_bottom_right,diagonal_bottom_left_to_top_right,diagonal_top_right_to_bottom_left,diagonal_bottom_right_to_top_left,outside_to_center,center_to_outside} Direction the highlight will travel. (default: diagonal_bottom_left_to_top_right) --highlight-width (int > 0) Width of the highlight. n >= 1 (default: 8) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied from bottom to top). If only one color is provided, the characters will be displayed in that color. (default: (Color('8A008A'), Color('00D1FF'), Color('FFFFFF'))) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: 12) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) Example: terminaltexteffects highlight --highlight-brightness 1.5 --highlight-direction diagonal_bottom_left_to_top_right --highlight-width 8 --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --final-gradient-direction vertical ``` --- ## LaserEtch A laser etches characters onto the terminal. ![Demo](./img/effects_demos/laseretch_demo.gif) [Reference](./effects/laseretch.md){ .md-button } [Config](./effects/laseretch.md#terminaltexteffects.effects.effect_laseretch.LaserEtchConfig){ .md-button } ??? example "LaserEtch Command Line Arguments" ``` --etch-direction {column_left_to_right,column_right_to_left,row_top_to_bottom,row_bottom_to_top,diagonal_top_left_to_bottom_right,diagonal_bottom_left_to_top_right,diagonal_top_right_to_bottom_left,diagonal_bottom_right_to_top_left,outside_to_center,center_to_outside} Pattern used to etch the text. (default: row_top_to_bottom) --etch-speed (int > 0) Along with etch_delay, determines the speed at which the characters are etched onto the terminal. This value specifies the number of characters to etch simultaneously. (default: 1) --etch-delay (int >= 0) Along with etch_speed, determines the speed at which the characters are etched onto the terminal. This values specifies the number of frames to wait before etching the next set of characters. (default: 3) --cool-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the gradient used to cool the characters after etching. If only one color is provided, the characters will be displayed in that color. (default: (Color('ffe680'), Color('ff7b00'))) --laser-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the laser gradient. If only one color is provided, the characters will be displayed in that color. (default: (Color('ffffff'), Color('376cff'))) --spark-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the spark cooling gradient. If only one color is provided, the characters will be displayed in that color. (default: (Color('ffffff'), Color('ffe680'), Color('ff7b00'), Color('1a0900'))) --spark-cooling-frames (int > 0) Number of frames to display each spark cooling gradient step. Increase to slow down the rate of cooling. (default: 10) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: (Color('8A008A'), Color('00D1FF'), Color('ffffff'))) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: 8) --final-gradient-frames (int > 0) Number of frames to display each gradient step. Increase to slow down the gradient animation. (default: 5) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) Example: terminaltexteffects laseretch --etch-speed 2 --etch-delay 5 --etch-direction row_top_to_bottom --cool-gradient-stops ffe680 ff7b00 --laser-gradient-stops ffffff 376cff --spark-gradient-stops ffffff ffe680 ff7b00 1a0900 --spark-cooling-frames 10 --final-gradient-stops 8A008A 00D1FF ffffff --final-gradient-steps 8 --final-gradient-frames 5 --final-gradient-direction vertical ``` --- ## Matrix Matrix digital rain effect. ![Demo](./img/effects_demos/matrix_demo.gif) [Reference](./effects/matrix.md){ .md-button } [Config](./effects/matrix.md#terminaltexteffects.effects.effect_matrix.MatrixConfig){ .md-button } ??? example "Matrix Command Line Arguments" ``` --highlight-color (XTerm [0-255] OR RGB Hex [000000-ffffff]) Color for the bottom of the rain column. (default: Color Code: dbffdb Color Appearance: █████) --rain-color-gradient (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the rain gradient. Colors are selected from the gradient randomly. If only one color is provided, the characters will be displayed in that color. (default: (Color(92be92), Color(185318))) --rain-symbols (ASCII/UTF-8 character) [(ASCII/UTF-8 character) ...] Space separated, unquoted, list of symbols to use for the rain. (default: ('2', '5', '9', '8', 'Z', '*', ')', ':', '.', '"', '=', '+', '-', '¦', '|', '_', 'ヲ', 'ア', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ケ', 'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', 'ツ', 'テ', 'ナ', 'ニ', 'ヌ', 'ネ', 'ハ', 'ヒ', 'ホ', 'マ', 'ミ', 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ラ', 'リ', 'ワ')) --rain-fall-delay-range (hyphen separated int range e.g. '1-10') Range for the speed of the falling rain as determined by the delay between rows. Actual delay is randomly selected from the range. (default: (8, 25)) --rain-column-delay-range (hyphen separated int range e.g. '1-10') Range of frames to wait between adding new rain columns. (default: (5, 15)) --rain-time (int > 0) Time, in seconds, to display the rain effect before transitioning to the input text. (default: 15) --symbol-swap-chance (float > 0) Chance of swapping a character's symbol on each tick. (default: 0.005) --color-swap-chance (float > 0) Chance of swapping a character's color on each tick. (default: 0.001) --resolve-delay (int > 0) Number of frames to wait between resolving the next group of characters. This is used to adjust the speed of the final resolve phase. (default: 5) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: Color Code: 389c38 Color Appearance: █████) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: 12) --final-gradient-frames (int > 0) Number of frames to display each gradient step. Increase to slow down the gradient animation. (default: 5) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) Example: tte matrix --rain-color-gradient 92be92 185318 --rain-symbols 2 5 9 8 Z : . = + - ¦ _ --rain-fall-delay-range 8-25 --rain-column-delay-range 5-15 --rain-time 15 --symbol-swap-chance 0.005 --color-swap-chance 0.001 --resolve-delay 5 --final-gradient-stops 389c38 --final-gradient-steps 12 --final-gradient-frames 5 --final-gradient-direction vertical --highlight-color dbffdb ``` --- ## MiddleOut Text expands in a single row or column in the middle of the canvas then out. ![Demo](./img/effects_demos/middleout_demo.gif) [Reference](./effects/middleout.md){ .md-button } [Config](./effects/middleout.md#terminaltexteffects.effects.effect_middleout.MiddleOutConfig){ .md-button } ??? example "MiddleOut Command Line Arguments" ``` --starting-color (XTerm [0-255] OR RGB Hex [000000-ffffff]) Color for the initial text in the center of the canvas. (default: ffffff) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('8A008A', '00D1FF', 'FFFFFF')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --expand-direction {vertical,horizontal} Direction the text will expand. (default: vertical) --center-movement-speed (float > 0) Speed of the characters during the initial expansion of the center vertical/horiztonal line. Note: Speed effects the number of steps in the easing function. Adjust speed and animation rate separately to fine tune the effect. (default: 0.35) --full-movement-speed (float > 0) Speed of the characters during the final full expansion. Note: Speed effects the number of steps in the easing function. Adjust speed and animation rate separately to fine tune the effect. (default: 0.35) --center-easing CENTER_EASING Easing function to use for initial expansion. (default: in_out_sine) --full-easing FULL_EASING Easing function to use for full expansion. (default: in_out_sine) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects middleout --starting-color 8A008A --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --expand-direction vertical --center-movement-speed 0.35 --full-movement-speed 0.35 --center-easing IN_OUT_SINE --full-easing IN_OUT_SINE ``` --- ## OrbittingVolley Four launchers orbit the canvas firing volleys of characters inward to build the input text from the center out. ![Demo](./img/effects_demos/orbittingvolley_demo.gif) [Reference](./effects/orbittingvolley.md){ .md-button } [Config](./effects/orbittingvolley.md#terminaltexteffects.effects.effect_orbittingvolley.OrbittingVolleyConfig){ .md-button } ??? example "OrbittingVolley Command Line Arguments" ``` --top-launcher-symbol (ASCII/UTF-8 character) Symbol for the top launcher. (default: █) --right-launcher-symbol (ASCII/UTF-8 character) Symbol for the right launcher. (default: █) --bottom-launcher-symbol (ASCII/UTF-8 character) Symbol for the bottom launcher. (default: █) --left-launcher-symbol (ASCII/UTF-8 character) Symbol for the left launcher. (default: █) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('FFA15C', '44D492')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.RADIAL) --launcher-movement-speed (float > 0) Orbitting speed of the launchers. (default: 0.5) --character-movement-speed (float > 0) Speed of the launched characters. (default: 1) --volley-size (0 <= float(n) <= 1) Percent of total input characters each launcher will fire per volley. Lower limit of one character. (default: 0.03) --launch-delay (int >= 0) Number of animation ticks to wait between volleys of characters. (default: 50) --character-easing (Easing Function) Easing function to use for launched character movement. (default: out_sine) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects orbittingvolley --top-launcher-symbol █ --right-launcher-symbol █ --bottom-launcher-symbol █ --left-launcher-symbol █ --final-gradient-stops FFA15C 44D492 --final-gradient-steps 12 --launcher-movement-speed 0.5 --character-movement-speed 1 --volley-size 0.03 --launch-delay 50 --character-easing OUT_SINE ``` --- ## Overflow Input text overflows and scrolls the terminal in a random order until eventually appearing ordered. ![Demo](./img/effects_demos/overflow_demo.gif) [Reference](./effects/overflow.md){ .md-button } [Config](./effects/overflow.md#terminaltexteffects.effects.effect_overflow.OverflowConfig){ .md-button } ??? example "Overflow Command Line Arguments" ``` --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('8A008A', '00D1FF', 'FFFFFF')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --overflow-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the overflow gradient. (default: ('f2ebc0', '8dbfb3', 'f2ebc0')) --overflow-cycles-range (hyphen separated int range e.g. '1-10') Number of cycles to overflow the text. (default: (2, 4)) --overflow-speed (int > 0) Speed of the overflow effect. (default: 3) Example: terminaltexteffects overflow --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --overflow-gradient-stops f2ebc0 8dbfb3 f2ebc0 --overflow-cycles-range 2-4 --overflow-speed 3 ``` --- ## Pour Pours the characters back and forth from the top, bottom, left, or right. ![Demo](./img/effects_demos/pour_demo.gif) [Reference](./effects/pour.md){ .md-button } [Config](./effects/pour.md#terminaltexteffects.effects.effect_pour.PourConfig){ .md-button } ??? example "Pour Command Line Arguments" ``` --pour-direction {up,down,left,right} Direction the text will pour. (default: down) --pour-speed (int > 0) Number of characters poured in per tick. Increase to speed up the effect. (default: 1) --movement-speed (float > 0) Movement speed of the characters. (default: 0.2) --gap (int >= 0) Number of frames to wait between each character in the pour effect. Increase to slow down effect and create a more defined back and forth motion. (default: 1) --starting-color (XTerm [0-255] OR RGB Hex [000000-ffffff]) Color of the characters before the gradient starts. (default: ffffff) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient. If only one color is provided, the characters will be displayed in that color. (default: ('8A008A', '00D1FF', 'FFFFFF')) --final-gradient-steps (int > 0) Number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-frames (int > 0) Number of frames to display each gradient step. Increase to slow down the gradient animation. (default: 10) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --easing EASING Easing function to use for character movement. (default: in_quad) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects pour --pour-direction down --movement-speed 0.2 --gap 1 --starting-color FFFFFF --final-gradient-stops 8A008A 00D1FF FFFFFF --easing IN_QUAD ``` --- ## Print Prints the input data one line at at time with a carriage return and line feed. ![Demo](./img/effects_demos/print_demo.gif) [Reference](./effects/print.md){ .md-button } [Config](./effects/print.md#terminaltexteffects.effects.effect_print.PrintConfig){ .md-button } ??? example "Print Command Line Arguments" ``` --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('02b8bd', 'c1f0e3', '00ffa0')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.DIAGONAL) --print-head-return-speed (float > 0) Speed of the print head when performing a carriage return. (default: 1.25) --print-speed (int > 0) Speed of the print head when printing characters. (default: 1) --print-head-easing PRINT_HEAD_EASING Easing function to use for print head movement. (default: in_out_quad) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects print --final-gradient-stops 02b8bd c1f0e3 00ffa0 --final-gradient-steps 12 --print-head-return-speed 1.25 --print-speed 1 --print-head-easing IN_OUT_QUAD ``` --- ## Rain Rain characters from the top of the canvas. ![Demo](./img/effects_demos/rain_demo.gif) [Reference](./effects/rain.md){ .md-button } [Config](./effects/rain.md#terminaltexteffects.effects.effect_rain.RainConfig){ .md-button } ??? example "Rain Command Line Arguments" ``` --rain-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] List of colors for the rain drops. Colors are randomly chosen from the list. (default: ('00315C', '004C8F', '0075DB', '3F91D9', '78B9F2', '9AC8F5', 'B8D8F8', 'E3EFFC')) --movement-speed (hyphen separated float range e.g. '0.25-0.5') Falling speed range of the rain drops. (default: (0.1, 0.2)) --rain-symbols (ASCII/UTF-8 character) [(ASCII/UTF-8 character) ...] Space separated list of symbols to use for the rain drops. Symbols are randomly chosen from the list. (default: ('o', '.', ',', '*', '|')) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('488bff', 'b2e7de', '57eaf7')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.DIAGONAL) --easing (Easing Function) Easing function to use for character movement. (default: in_quart) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects rain --rain-symbols o . , "*" "|" --rain-colors 00315C 004C8F 0075DB 3F91D9 78B9F2 9AC8F5 B8D8F8 E3EFFC --final-gradient-stops 488bff b2e7de 57eaf7 --final-gradient-steps 12 --movement-speed 0.1-0.2 --easing IN_QUART ``` --- ## RandomSequence Prints the input data in a random sequence, one character at a time. ![Demo](./img/effects_demos/randomsequence_demo.gif) [Reference](./effects/randomsequence.md){ .md-button } [Config](./effects/randomsequence.md#terminaltexteffects.effects.effect_random_sequence.RandomSequenceConfig){ .md-button } ??? example "RandomSequence Command Line Arguments" ``` --starting-color (XTerm [0-255] OR RGB Hex [000000-ffffff]) Color of the characters at spawn. (default: 000000) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('8A008A', '00D1FF', 'FFFFFF')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-frames (int > 0) Number of frames to display each gradient step. Increase to slow down the gradient animation. (default: 12) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --speed (float > 0) Speed of the animation as a percentage of the total number of characters. (default: 0.004) Example: terminaltexteffects randomsequence --starting-color 000000 --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --final-gradient-frames 12 --speed 0.004 ``` --- ## Rings Characters are dispersed and form into spinning rings. ![Demo](./img/effects_demos/rings_demo.gif) [Reference](./effects/rings.md){ .md-button } [Config](./effects/rings.md#terminaltexteffects.effects.effect_rings.RingsConfig){ .md-button } ??? example "Rings Command Line Arguments" ``` --ring-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the rings. (default: ('ab48ff', 'e7b2b2', 'fffebd')) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('ab48ff', 'e7b2b2', 'fffebd')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --ring-gap RING_GAP Distance between rings as a percent of the smallest canvas dimension. (default: 0.1) --spin-duration SPIN_DURATION Number of frames for each cycle of the spin phase. (default: 200) --spin-speed (hyphen separated float range e.g. '0.25-0.5') Range of speeds for the rotation of the rings. The speed is randomly selected from this range for each ring. (default: (0.25, 1.0)) --disperse-duration DISPERSE_DURATION Number of frames spent in the dispersed state between spinning cycles. (default: 200) --spin-disperse-cycles SPIN_DISPERSE_CYCLES Number of times the animation will cycles between spinning rings and dispersed characters. (default: 3) Example: terminaltexteffects rings --ring-colors ab48ff e7b2b2 fffebd --final-gradient-stops ab48ff e7b2b2 fffebd --final-gradient-steps 12 --ring-gap 0.1 --spin-duration 200 --spin-speed 0.25-1.0 --disperse-duration 200 --spin-disperse-cycles 3 ``` --- ## Scattered Text is scattered across the canvas and moves into position. ![Demo](./img/effects_demos/scattered_demo.gif) [Reference](./effects/scattered.md){ .md-button } [Config](./effects/scattered.md#terminaltexteffects.effects.effect_scattered.ScatteredConfig){ .md-button } ??? example "Scattered Command Line Arguments" ``` --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient. If only one color is provided, the characters will be displayed in that color. (default: ('ff9048', 'ab9dff', 'bdffea')) --final-gradient-steps (int > 0) Number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-frames (int > 0) Number of frames to display each gradient step. Increase to slow down the gradient animation. (default: 12) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --movement-speed (float > 0) Movement speed of the characters. (default: 0.5) --movement-easing MOVEMENT_EASING Easing function to use for character movement. (default: in_out_back) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects scattered --final-gradient-stops ff9048 ab9dff bdffea --final-gradient-steps 12 --final-gradient-frames 12 --movement-speed 0.5 --movement-easing IN_OUT_BACK ``` --- ## Slice Slices the input in half and slides it into place from opposite directions. ![Demo](./img/effects_demos/slice_demo.gif) [Reference](./effects/slice.md){ .md-button } [Config](./effects/slice.md#terminaltexteffects.effects.effect_slice.SliceConfig){ .md-button } ??? example "Slice Command Line Arguments" ``` --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: (Color(8A008A), Color(00D1FF), Color(FFFFFF))) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: 12) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.DIAGONAL) --slice-direction {vertical,horizontal,diagonal} Direction of the slice. (default: vertical) --movement-speed (float > 0) Movement speed of the characters. (default: 0.15) --movement-easing MOVEMENT_EASING Easing function to use for character movement. (default: in_out_expo) Easing ------ Note: A prefix must be added to the function name (except LINEAR). All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- LINEAR - Linear easing SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects slice --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --slice-direction vertical--movement-speed 0.15 --movement-easing IN_OUT_EXPO ``` --- ## Slide Slide characters into view from outside the terminal. ![Demo](./img/effects_demos/slide_demo.gif) [Reference](./effects/slide.md){ .md-button } [Config](./effects/slide.md#terminaltexteffects.effects.effect_slide.SlideConfig){ .md-button } ??? example "Slide Command Line Arguments" ``` --movement-speed (float > 0) Speed of the characters. (default: 0.5) --grouping {row,column,diagonal} Direction to group characters. (default: row) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient. If only one color is provided, the characters will be displayed in that color. (default: ('833ab4', 'fd1d1d', 'fcb045')) --final-gradient-steps (int > 0) Number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-frames (int > 0) Number of frames to display each gradient step. Increase to slow down the gradient animation. (default: 10) --final-gradient-direction FINAL_GRADIENT_DIRECTION Direction of the gradient (vertical, horizontal, diagonal, center). (default: Direction.VERTICAL) --gap (int >= 0) Number of frames to wait before adding the next group of characters. Increasing this value creates a more staggered effect. (default: 3) --reverse-direction Reverse the direction of the characters. (default: False) --merge Merge the character groups originating from either side of the terminal. (--reverse-direction is ignored when merging) (default: False) --movement-easing (Easing Function) Easing function to use for character movement. (default: in_out_quad) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects slide --movement-speed 0.5 --grouping row --final-gradient-stops 833ab4 fd1d1d fcb045 --final-gradient-steps 12 --final-gradient-frames 10 --final-gradient-direction vertical --gap 3 --reverse-direction --merge --movement-easing OUT_QUAD ``` --- ## Spotlights Spotlights search the text area, illuminating characters, before converging in the center and expanding. ![Demo](./img/effects_demos/spotlights_demo.gif) [Reference](./effects/spotlights.md){ .md-button } [Config](./effects/spotlights.md#terminaltexteffects.effects.effect_spotlights.SpotlightsConfig){ .md-button } ??? example "Spotlights Command Line Arguments" ``` --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('ab48ff', 'e7b2b2', 'fffebd')) --final-gradient-steps (int > 0) [(int > 0) ...] Number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --beam-width-ratio (float > 0) Width of the beam of light as min(width, height) // n of the input text. (default: 2.0) --beam-falloff (float >= 0) Distance from the edge of the beam where the brightness begins to fall off, as a percentage of total beam width. (default: 0.3) --search-duration (int > 0) Duration of the search phase, in frames, before the spotlights converge in the center. (default: 750) --search-speed-range (hyphen separated float range e.g. '0.25-0.5') Range of speeds for the spotlights during the search phase. The speed is a random value between the two provided values. (default: (0.25, 0.5)) --spotlight-count (int > 0) Number of spotlights to use. (default: 3) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects spotlights --final-gradient-stops ab48ff e7b2b2 fffebd --final-gradient-steps 12 --beam-width-ratio 2.0 --beam-falloff 0.3 --search-duration 750 --search-speed-range 0.25-0.5 --spotlight-count 3 ``` --- ## Spray Sprays the characters from a single point. ![Demo](./img/effects_demos/spray_demo.gif) [Reference](./effects/spray.md){ .md-button } [Config](./effects/spray.md#terminaltexteffects.effects.effect_spray.SprayConfig){ .md-button } ??? example "Spray Command Line Arguments" ``` --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('8A008A', '00D1FF', 'FFFFFF')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --spray-position {n,ne,e,se,s,sw,w,nw,center} Position for the spray origin. (default: e) --spray-volume (float > 0) Number of characters to spray per tick as a percent of the total number of characters. (default: 0.005) --movement-speed (hyphen separated float range e.g. '0.25-0.5') Movement speed of the characters. (default: (0.4, 1.0)) --movement-easing MOVEMENT_EASING Easing function to use for character movement. (default: out_expo) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects spray --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --spray-position e --spray-volume 0.005 --movement-speed 0.4-1.0 --movement-easing OUT_EXPO ``` --- ## Swarm Characters are grouped into swarms and move around the terminal before settling into position. ![Demo](./img/effects_demos/swarm_demo.gif) [Reference](./effects/swarm.md){ .md-button } [Config](./effects/swarm.md#terminaltexteffects.effects.effect_swarm.SwarmConfig){ .md-button } ??? example "Swarm Command Line Arguments" ``` --base-color (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the swarms (default: ('31a0d4',)) --flash-color (XTerm [0-255] OR RGB Hex [000000-ffffff]) Color for the character flash. Characters flash when moving. (default: f2ea79) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('31b900', 'f0ff65')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.HORIZONTAL) --swarm-size (0 <= float(n) <= 1) Percent of total characters in each swarm. (default: 0.1) --swarm-coordination (0 <= float(n) <= 1) Percent of characters in a swarm that move as a group. (default: 0.8) --swarm-area-count (hyphen separated int range e.g. '1-10') Range of the number of areas where characters will swarm. (default: (2, 4)) Example: terminaltexteffects swarm --base-color 31a0d4 --flash-color f2ea79 --final-gradient-stops 31b900 f0ff65 --final-gradient-steps 12 --swarm-size 0.1 --swarm-coordination 0.80 --swarm-area-count 2-4 ``` --- ## Sweep Sweep across the canvas to reveal uncolored text, reverse sweep to color the text. ![Demo](./img/effects_demos/sweep_demo.gif) [Reference](./effects/sweep.md){ .md-button } [Config](./effects/sweep.md#terminaltexteffects.effects.effect_sweep.SweepConfig){ .md-button } ??? example "Sweep Command Line Arguments" ``` --sweep-symbols (ASCII/UTF-8 character) [(ASCII/UTF-8 character) ...] Space separated list of symbols to use for the sweep shimmer. (default: ('█', '▓', '▒', '░')) --first-sweep-direction {column_left_to_right,column_right_to_left,row_top_to_bottom,row_bottom_to_top,diagonal_top_left_to_bottom_right,diagonal_bottom_left_to_top_right,diagonal_top_right_to_bottom_left,diagonal_bottom_right_to_top_left,outside_to_center,center_to_outside} Direction of the first sweep, revealing uncolored characters. (default: column_right_to_left) --second-sweep-direction {column_left_to_right,column_right_to_left,row_top_to_bottom,row_bottom_to_top,diagonal_top_left_to_bottom_right,diagonal_bottom_left_to_top_right,diagonal_top_right_to_bottom_left,diagonal_bottom_right_to_top_left,outside_to_center,center_to_outside} Direction of the second sweep, coloring the characters. (default: column_left_to_right) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied from bottom to top). If only one color is provided, the characters will be displayed in that color. (default: (Color('8A008A'), Color('00D1FF'), Color('ffffff'))) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: 8) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) Example: terminaltexteffects sweep --sweep-symbols '█' '▓' '▒' '░' --first-sweep-direction column_right_to_left --second-sweep-direction column_left_to_right --final-gradient-stops 8A008A 00D1FF ffffff --final-gradient-steps 8 8 8 --final-gradient-direction vertical ``` --- ## SynthGrid Create a grid which fills with characters dissolving into the final text. ![Demo](./img/effects_demos/synthgrid_demo.gif) [Reference](./effects/synthgrid.md){ .md-button } [Config](./effects/synthgrid.md#terminaltexteffects.effects.effect_synthgrid.SynthGridConfig){ .md-button } ??? example "SynthGrid Command Line Arguments" ``` --grid-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the grid gradient. (default: ('CC00CC', 'ffffff')) --grid-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --grid-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the gradient for the grid color. (default: Direction.DIAGONAL) --text-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the text gradient. (default: ('8A008A', '00D1FF', 'FFFFFF')) --text-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --text-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the gradient for the text color. (default: Direction.VERTICAL) --grid-row-symbol (ASCII/UTF-8 character) Symbol to use for grid row lines. (default: ─) --grid-column-symbol (ASCII/UTF-8 character) Symbol to use for grid column lines. (default: │) --text-generation-symbols (ASCII/UTF-8 character) [(ASCII/UTF-8 character) ...] Space separated, unquoted, list of characters for the text generation animation. (default: ('░', '▒', '▓')) --max-active-blocks (float > 0) Maximum percentage of blocks to have active at any given time. For example, if set to 0.1, 10 percent of the blocks will be active at any given time. (default: 0.1) Example: terminaltexteffects synthgrid --grid-gradient-stops CC00CC ffffff --grid-gradient-steps 12 --text-gradient-stops 8A008A 00D1FF FFFFFF --text-gradient-steps 12 --grid-row-symbol ─ --grid-column-symbol "│" --text-generation-symbols ░ ▒ ▓ --max-active-blocks 0.1 ``` --- ## Unstable Spawns characters jumbled, explodes them to the edge of the canvas, then reassembles them. ![Demo](./img/effects_demos/unstable_demo.gif) [Reference](./effects/unstable.md){ .md-button } [Config](./effects/unstable.md#terminaltexteffects.effects.effect_unstable.UnstableConfig){ .md-button } ??? example "Unstable Command Line Arguments" ``` --unstable-color (XTerm [0-255] OR RGB Hex [000000-ffffff]) Color transitioned to as the characters become unstable. (default: ff9200) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('8A008A', '00D1FF', 'FFFFFF')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --explosion-ease EXPLOSION_EASE Easing function to use for character movement during the explosion. (default: out_expo) --explosion-speed (float > 0) Speed of characters during explosion. (default: 0.75) --reassembly-ease REASSEMBLY_EASE Easing function to use for character reassembly. (default: out_expo) --reassembly-speed (float > 0) Speed of characters during reassembly. (default: 0.75) Easing ------ Note: A prefix must be added to the function name. All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects unstable --unstable-color ff9200 --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --explosion-ease OUT_EXPO --explosion-speed 0.75 --reassembly-ease OUT_EXPO --reassembly-speed 0.75 ``` --- ## VHSTape Lines of characters glitch left and right and lose detail like an old VHS tape. ![Demo](./img/effects_demos/vhstape_demo.gif) [Reference](./effects/vhstape.md){ .md-button } [Config](./effects/vhstape.md#terminaltexteffects.effects.effect_vhstape.VHSTapeConfig){ .md-button } ??? example "VHSTape Command Line Arguments" ``` --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: ('ab48ff', 'e7b2b2', 'fffebd')) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (12,)) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --glitch-line-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the characters when a single line is glitching. Colors are applied in order as an animation. (default: ('ffffff', 'ff0000', '00ff00', '0000ff', 'ffffff')) --glitch-wave-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the characters in lines that are part of the glitch wave. Colors are applied in order as an animation. (default: ('ffffff', 'ff0000', '00ff00', '0000ff', 'ffffff')) --noise-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the characters during the noise phase. (default: ('1e1e1f', '3c3b3d', '6d6c70', 'a2a1a6', 'cbc9cf', 'ffffff')) --glitch-line-chance (0 <= float(n) <= 1) Chance that a line will glitch on any given frame. (default: 0.05) --noise-chance (0 <= float(n) <= 1) Chance that all characters will experience noise on any given frame. (default: 0.004) --total-glitch-time (int > 0) Total time, frames, that the glitching phase will last. (default: 1000) Example: terminaltexteffects vhstape --final-gradient-stops ab48ff e7b2b2 fffebd --final-gradient-steps 12 --glitch-line-colors ffffff ff0000 00ff00 0000ff ffffff --glitch-wave-colors ffffff ff0000 00ff00 0000ff ffffff --noise-colors 1e1e1f 3c3b3d 6d6c70 a2a1a6 cbc9cf ffffff --glitch-line-chance 0.05 --noise-chance 0.004 --total-glitch-time 1000 ``` --- ## Waves Waves travel across the terminal leaving behind the characters. ![Demo](./img/effects_demos/waves_demo.gif) [Reference](./effects/waves.md){ .md-button } [Config](./effects/waves.md#terminaltexteffects.effects.effect_waves.WavesConfig){ .md-button } ??? example "Waves Command Line Arguments" ``` --wave-symbols (ASCII/UTF-8 character) [(ASCII/UTF-8 character) ...] Symbols to use for the wave animation. Multi-character strings will be used in sequence to create an animation. (default: ('▁', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▇', '▆', '▅', '▄', '▃', '▂', '▁')) --wave-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: (Color(#f0ff65), Color(#ffb102), Color(#31a0d4), Color(#ffb102), Color(#f0ff65))) --wave-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: (6,)) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. (default: (Color(#ffb102), Color(#31a0d4), Color(#f0ff65))) --final-gradient-steps (int > 0) [(int > 0) ...] Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: 12) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.DIAGONAL) --wave-count WAVE_COUNT Number of waves to generate. n > 0. (default: 7) --wave-length (int > 0) The number of frames for each step of the wave. Higher wave-lengths will create a slower wave. (default: 2) --wave-direction {column_left_to_right,column_right_to_left,row_top_to_bottom,row_bottom_to_top,center_to_outside,outside_to_center} Direction of the wave. (default: column_left_to_right) --wave-easing WAVE_EASING Easing function to use for wave travel. (default: in_out_sine) Easing ------ Note: A prefix must be added to the function name (except LINEAR). All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- LINEAR - Linear easing SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. Example: terminaltexteffects waves --wave-symbols ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▇ ▆ ▅ ▄ ▃ ▂ ▁ --wave-gradient-stops f0ff65 ffb102 31a0d4 ffb102 f0ff65 --wave-gradient-steps 6 --final-gradient-stops ffb102 31a0d4 f0ff65 --final-gradient-steps 12 --wave-count 7 --wave-length 2 --wave-easing IN_OUT_SINE ``` --- ## Wipe Performs a wipe across the terminal to reveal characters. ![Demo](./img/effects_demos/wipe_demo.gif) [Reference](./effects/wipe.md){ .md-button } [Config](./effects/wipe.md#terminaltexteffects.effects.effect_wipe.WipeConfig){ .md-button } ??? example "Wipe Command Line Arguments" ``` --wipe-direction {column_left_to_right,column_right_to_left,row_top_to_bottom,row_bottom_to_top,diagonal_top_left_to_bottom_right,diagonal_bottom_left_to_top_right,diagonal_top_right_to_bottom_left,diagonal_bottom_right_to_top_left,outside_to_center,center_to_outside} Direction the text will wipe. (default: diagonal_bottom_left_to_top_right) --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...] Space separated, unquoted, list of colors for the wipe gradient. (default: (Color(#833ab4), Color(#fd1d1d), Color(#fcb045))) --final-gradient-steps (int > 0) [(int > 0) ...] Number of gradient steps to use. More steps will create a smoother and longer gradient animation. (default: 12) --final-gradient-frames (int > 0) Number of frames to display each gradient step. Increase to slow down the gradient animation. (default: 5) --final-gradient-direction (diagonal, horizontal, vertical, radial) Direction of the final gradient. (default: Direction.VERTICAL) --wipe-delay (int >= 0) Number of frames to wait before adding the next character group. Increase, to slow down the effect. (default: 0) Example: terminaltexteffects wipe --wipe-direction diagonal_bottom_left_to_top_right --final-gradient-stops 833ab4 fd1d1d fcb045 --final-gradient-steps 12 --final-gradient-frames 5 --wipe-delay 0 ``` terminaltexteffects-release-0.12.1/flake.lock000066400000000000000000000017671507200677100212450ustar00rootroot00000000000000{ "nodes": { "nixpkgs": { "locked": { "lastModified": 1717112898, "narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=", "owner": "nixos", "repo": "nixpkgs", "rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0", "type": "github" }, "original": { "owner": "nixos", "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "nixpkgs": "nixpkgs", "systems": "systems" } }, "systems": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "owner": "nix-systems", "repo": "default", "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { "owner": "nix-systems", "repo": "default", "type": "github" } } }, "root": "root", "version": 7 } terminaltexteffects-release-0.12.1/flake.nix000066400000000000000000000010171507200677100210770ustar00rootroot00000000000000{ description = "Visual effects applied to text in the terminal. "; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; systems.url = "github:nix-systems/default"; }; outputs = { systems, nixpkgs, ... }: let forEachSystem = f: nixpkgs.lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system}); in { formatter = forEachSystem (pkgs: pkgs.alejandra); packages = forEachSystem (pkgs: { default = pkgs.callPackage ./default.nix {}; }); }; } terminaltexteffects-release-0.12.1/mkdocs.yml000066400000000000000000000064051507200677100213060ustar00rootroot00000000000000site_name: TerminalTextEffects Docs site_description: TerminalTextEffects Documentation site_author: ChrisBuilds repo_url: https://github.com/ChrisBuilds/terminaltexteffects docs_dir: docs theme: name: material palette: scheme: slate features: - content.code.copy - content.code.annotate plugins: - mkdocstrings markdown_extensions: - admonition - pymdownx.superfences - pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format - pymdownx.tabbed: alternate_style: true - pymdownx.highlight: anchor_linenums: true line_spans: __span pygments_lang_class: true - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.details - pymdownx.caret - pymdownx.tilde - attr_list - def_list nav: - Intro to TTE: index.md - Change[B]log: - changeblog/changeblog.md - changeblog/changeblog_0.12.0.md - changeblog/changeblog_0.11.0.md - changeblog/changeblog_0.10.0.md - How to install & use TTE: - Install: installation.md - Application Usage: appguide.md - Library Usage: libguide.md # - Effect Building Guide: # - effectguide/effectguide.md # - effectguide/effectguide_lesson0.md - Effects Showroom: showroom.md - Library Cookbook: cookbook.md - Reference: - Engine: - engine/baseeffect.md - engine/basecharacter.md - engine/eventhandler.md - Animation: - engine/animation/animation.md - engine/animation/charactervisual.md - engine/animation/frame.md - engine/animation/scene.md - Motion: - engine/motion/motion.md - engine/motion/waypoint.md - engine/motion/segment.md - engine/motion/path.md - Terminal: - engine/terminal/terminal.md - engine/terminal/terminalconfig.md - engine/terminal/canvas.md - Utils: - engine/utils/ansitools.md - engine/utils/argsdataclass.md - engine/utils/argvalidators.md - engine/utils/color.md - engine/utils/colorpair.md - engine/utils/colorterm.md - engine/utils/easing.md - engine/utils/exceptions.md - engine/utils/geometry.md - engine/utils/gradient.md - engine/utils/hexterm.md - Effects: - effects/beams.md - effects/binarypath.md - effects/blackhole.md - effects/bouncyballs.md - effects/bubbles.md - effects/burn.md - effects/colorshift.md - effects/crumble.md - effects/decrypt.md - effects/errorcorrect.md - effects/expand.md - effects/fireworks.md - effects/highlight.md - effects/laseretch.md - effects/matrix.md - effects/middleout.md - effects/orbittingvolley.md - effects/overflow.md - effects/pour.md - effects/print.md - effects/rain.md - effects/randomsequence.md - effects/rings.md - effects/scattered.md - effects/slice.md - effects/slide.md - effects/spotlights.md - effects/spray.md - effects/swarm.md - effects/sweep.md - effects/synthgrid.md - effects/unstable.md - effects/vhstape.md - effects/waves.md - effects/wipe.md terminaltexteffects-release-0.12.1/poetry.lock000066400000000000000000000003641507200677100214750ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. package = [] [metadata] lock-version = "2.0" python-versions = "^3.8" content-hash = "53f2eabc9c26446fbcc00d348c47878e118afc2054778c3c803a0a8028af27d9" terminaltexteffects-release-0.12.1/pyproject.toml000066400000000000000000000032661507200677100222210ustar00rootroot00000000000000[tool.poetry] name = "terminaltexteffects" version = "0.12.1" description = "TerminalTextEffects (TTE) is a terminal visual effects engine." authors = ["Chris <741258@pm.me>"] license = "MIT" readme = "README.md" repository = "https://github.com/ChrisBuilds/terminaltexteffects" documentation = "https://chrisbuilds.github.io/terminaltexteffects/" [tool.poetry.dependencies] python = "^3.8" [tool.poetry.scripts] tte = "terminaltexteffects.__main__:main" [tool.ruff] show-fixes = true line-length = 120 [tool.ruff.lint] ignore = [ "C90", "ANN401", "EXE", "PLR0912", # too many branches "PLR0913", # too many function args "PLR2004", # magic numbers "RUF009", # function-call-in-dataclass-default-argument "S311", # suspicious-non-cryptographic-random-usage "SLF001", # private member access "TRY003", # f-strings in exception message "T201", # printing ] select = ["ALL"] fixable = ["ALL"] extend-fixable = ["EM102"] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401", "D104"] "**/tests/*" = [ "S101", # assert ] [tool.pytest.ini_options] console_output_style = "progress" minversion = "6.0" addopts = ["--capture=sys", "--strict-markers", "--strict-config", "-ra"] markers = [ "manual: subset to run manually", "visual: visually inspect effects", "effects: quick effect tests", "engine: engine tests", "animation: animation tests", "motion: motion tests", "terminal: terminal tests", "base_character: base character tests", "utils: utility tests", "smoke: quick tests covering over 90% of code", ] testpaths = ["tests"] [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" terminaltexteffects-release-0.12.1/terminaltexteffects/000077500000000000000000000000001507200677100233565ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/terminaltexteffects/__init__.py000066400000000000000000000012601507200677100254660ustar00rootroot00000000000000"""Terminal Text Effects package. This package provides various text effects for terminal applications. """ from terminaltexteffects.engine.animation import Animation, Scene from terminaltexteffects.engine.base_character import EffectCharacter, EventHandler from terminaltexteffects.engine.motion import ( Motion, Path, Segment, Waypoint, ) from terminaltexteffects.engine.terminal import Terminal from terminaltexteffects.utils import easing, geometry, graphics from terminaltexteffects.utils.geometry import Coord from terminaltexteffects.utils.graphics import Color, ColorPair, Gradient Event = EventHandler.Event Action = EventHandler.Action __version__ = "0.12.1" terminaltexteffects-release-0.12.1/terminaltexteffects/__main__.py000066400000000000000000000055701507200677100254570ustar00rootroot00000000000000"""Provides the command line interface for the TerminalTextEffects application.""" from __future__ import annotations import argparse import importlib import pkgutil import sys from pathlib import Path import terminaltexteffects.effects import terminaltexteffects.engine.terminal as term from terminaltexteffects.engine.terminal import TerminalConfig from terminaltexteffects.utils.argsdataclass import ArgsDataClass def main() -> None: """Run the terminaltexteffects command line interface.""" parser = (argparse.ArgumentParser)( prog="tte", description="A terminal visual effects engine, application, and library", epilog="Ex: ls -a | tte decrypt --typing-speed 2 --ciphertext-colors 008000 00cb00 00ff00 " "--final-gradient-stops eda000 --final-gradient-steps 12 --final-gradient-direction vertical", ) parser.add_argument("--input-file", "-i", type=str, help="File to read input from") parser.add_argument( "--version", "-v", action="version", version="TerminalTextEffects " + terminaltexteffects.__version__, ) TerminalConfig._add_args_to_parser(parser) subparsers = parser.add_subparsers( title="Effect", description="Name of the effect to apply. Use -h for effect specific help.", help="Available Effects", required=True, ) for module_info in pkgutil.iter_modules( terminaltexteffects.effects.__path__, terminaltexteffects.effects.__name__ + ".", ): module = importlib.import_module(module_info.name) if hasattr(module, "get_effect_and_args"): effect_class, args_class = module.get_effect_and_args() args_class._add_to_args_subparsers(subparsers) args = parser.parse_args() if args.input_file: try: input_data = Path(args.input_file).read_text(encoding="UTF-8") except FileNotFoundError: print(f"File not found: {args.input_file}") return except Exception as e: # noqa: BLE001 print(f"Error reading file: {args.input_file} - {e}") return else: input_data = term.Terminal.get_piped_input() if not input_data.strip(): print("NO INPUT.") else: terminal_config = TerminalConfig._from_parsed_args_mapping(args, TerminalConfig) effect_config = ArgsDataClass._from_parsed_args_mapping(args) effect_class = effect_config.get_effect_class() terminal_config.use_terminal_dimensions = True effect = effect_class(input_data) effect.effect_config = effect_config effect.terminal_config = terminal_config try: with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) except KeyboardInterrupt: sys.exit(1) if __name__ == "__main__": main() terminaltexteffects-release-0.12.1/terminaltexteffects/effects/000077500000000000000000000000001507200677100247755ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/terminaltexteffects/effects/__init__.py000066400000000000000000000000001507200677100270740ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_beams.py000066400000000000000000000527011507200677100277570ustar00rootroot00000000000000"""Creates beams which travel over the canvas illuminating the characters. Classes: Beams: Creates beams which travel over the canvas illuminating the characters. BeamsConfig: Configuration for the Beams effect. BeamsIterator: Iterates over the Beams effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass import terminaltexteffects as tte from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Return the Beams effect class and its configuration class.""" return Beams, BeamsConfig @argclass( name="beams", help="Create beams which travel over the canvas illuminating the characters behind them.", description="beams | Create beams which travel over the canvas illuminating the characters behind them.", epilog=( "Example: terminaltexteffects beams --beam-row-symbols ▂ ▁ _ --beam-column-symbols ▌ ▍ ▎ ▏ --beam-delay " "10 --beam-row-speed-range 10-40 --beam-column-speed-range 6-10 --beam-gradient-stops ffffff 00D1FF " "8A008A --beam-gradient-steps 2 8 --beam-gradient-frames 2 --final-gradient-stops 8A008A 00D1FF " "ffffff --final-gradient-steps 12 --final-gradient-frames 5 --final-gradient-direction vertical " "--final-wipe-speed 1" ), ) @dataclass class BeamsConfig(ArgsDataClass): """Configuration for the Beams effect. Attributes: beam_row_symbols (tuple[str, ...] | str): Symbols to use for the beam effect when moving along a row. Strings will be used in sequence to create an animation. beam_column_symbols (tuple[str, ...] | str): Symbols to use for the beam effect when moving along a column. Strings will be used in sequence to create an animation. beam_delay (int): Number of frames to wait before adding the next group of beams. Beams are added in groups of size random(1, 5). Valid values are n > 0. beam_row_speed_range (tuple[int, int]): Speed range of the beam when moving along a row. Valid values are n > 0. beam_column_speed_range (tuple[int, int]): Speed range of the beam when moving along a column. Valid values are n > 0. beam_gradient_stops (tuple[tte.Color, ...]): Tuple of colors for the beam, a gradient will be created between the colors. beam_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Steps are paired with the colors in final-gradient-stops. Valid values are n > 0. beam_gradient_frames (int): Number of frames to display each gradient step. Increase to slow down the gradient animation. Valid values are n > 0. final_gradient_stops (tuple[tte.Color, ...]): Tuple of colors for the wipe gradient. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Steps are paired with the colors in final-gradient-stops. Valid values are n > 0. final_gradient_frames (int): Number of frames to display each gradient step. Increase to slow down the gradient animation. final_gradient_direction (tte.Gradient.Direction): Direction of the final gradient. final_wipe_speed (int): Speed of the final wipe as measured in diagonal groups activated per frame. Valid values are n > 0. """ beam_row_symbols: tuple[str, ...] | str = ArgField( cmd_name="--beam-row-symbols", type_parser=argvalidators.Symbol.type_parser, nargs="+", default=("▂", "▁", "_"), metavar=argvalidators.Symbol.METAVAR, help=( "Symbols to use for the beam effect when moving along a row. " "Strings will be used in sequence to create an animation." ), ) # type: ignore[assignment] ( "tuple[str, ...] | str : Symbols to use for the beam effect when moving along a row. " "Strings will be used in sequence to create an animation." ) beam_column_symbols: tuple[str, ...] | str = ArgField( cmd_name="--beam-column-symbols", type_parser=argvalidators.Symbol.type_parser, nargs="+", default=("▌", "▍", "▎", "▏"), metavar=argvalidators.Symbol.METAVAR, help=( "Symbols to use for the beam effect when moving along a column. " "Strings will be used in sequence to create an animation." ), ) # type: ignore[assignment] ( "tuple[str, ...] | str : Symbols to use for the beam effect when moving along a column. " "Strings will be used in sequence to create an animation." ) beam_delay: int = ArgField( cmd_name="--beam-delay", type_parser=argvalidators.PositiveInt.type_parser, default=10, metavar=argvalidators.PositiveInt.METAVAR, help=( "Number of frames to wait before adding the next group of beams. " "Beams are added in groups of size random(1, 5)." ), ) # type: ignore[assignment] ( "int : Number of frames to wait before adding the next group of beams. " "Beams are added in groups of size random(1, 5)." ) beam_row_speed_range: tuple[int, int] = ArgField( cmd_name="--beam-row-speed-range", type_parser=argvalidators.PositiveIntRange.type_parser, default=(10, 40), metavar=argvalidators.PositiveIntRange.METAVAR, help="Speed range of the beam when moving along a row.", ) # type: ignore[assignment] "tuple[int, int] : Speed range of the beam when moving along a row." beam_column_speed_range: tuple[int, int] = ArgField( cmd_name="--beam-column-speed-range", type_parser=argvalidators.PositiveIntRange.type_parser, default=(6, 10), metavar=argvalidators.PositiveIntRange.METAVAR, help="Speed range of the beam when moving along a column.", ) # type: ignore[assignment] "tuple[int, int] : Speed range of the beam when moving along a column." beam_gradient_stops: tuple[tte.Color, ...] = ArgField( cmd_name="--beam-gradient-stops", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(tte.Color("ffffff"), tte.Color("00D1FF"), tte.Color("8A008A")), metavar="(XTerm [0-255] OR RGB Hex [000000-ffffff])", help="Space separated, unquoted, list of colors for the beam, a gradient will be created between the colors.", ) # type: ignore[assignment] "tuple[tte.Color, ...] : Tuple of colors for the beam, a gradient will be created between the colors." beam_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--beam-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=(2, 8), metavar=argvalidators.PositiveInt.METAVAR, help=( "Space separated, unquoted, numbers for the of gradient steps to use. " "More steps will create a smoother and longer gradient animation. " "Steps are paired with the colors in final-gradient-stops." ), ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. " "More steps will create a smoother and longer gradient animation. " "Steps are paired with the colors in final-gradient-stops." ) beam_gradient_frames: int = ArgField( cmd_name="--beam-gradient-frames", type_parser=argvalidators.PositiveInt.type_parser, default=2, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to display each gradient step. Increase to slow down the gradient animation.", ) # type: ignore[assignment] "int : Number of frames to display each gradient step. Increase to slow down the gradient animation." final_gradient_stops: tuple[tte.Color, ...] = ArgField( cmd_name="--final-gradient-stops", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(tte.Color("8A008A"), tte.Color("00D1FF"), tte.Color("ffffff")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the wipe gradient.", ) # type: ignore[assignment] "tuple[tte.Color, ...] : Tuple of colors for the wipe gradient." final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help=( "Space separated, unquoted, numbers for the of gradient steps to use. " "More steps will create a smoother and longer gradient animation. " "Steps are paired with the colors in final-gradient-stops." ), ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. " "More steps will create a smoother and longer gradient animation. " "Steps are paired with the colors in final-gradient-stops." ) final_gradient_frames: int = ArgField( cmd_name="--final-gradient-frames", type_parser=argvalidators.PositiveInt.type_parser, default=5, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to display each gradient step. Increase to slow down the gradient animation.", ) # type: ignore[assignment] "int : Number of frames to display each gradient step. Increase to slow down the gradient animation." final_gradient_direction: tte.Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=tte.Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "tte.Gradient.Direction : Direction of the final gradient." final_wipe_speed: int = ArgField( cmd_name="--final-wipe-speed", type_parser=argvalidators.PositiveInt.type_parser, default=1, metavar=argvalidators.PositiveInt.METAVAR, help="Speed of the final wipe as measured in diagonal groups activated per frame.", ) # type: ignore[assignment] "int : Speed of the final wipe as measured in diagonal groups activated per frame." @classmethod def get_effect_class(cls) -> type[Beams]: """Return the effect class associated with this configuration.""" return Beams class BeamsIterator(BaseEffectIterator[BeamsConfig]): """Iterator for the Beams effect.""" class Group: """Represents a group of characters.""" def __init__( self, characters: list[tte.EffectCharacter], direction: str, terminal: tte.Terminal, args: BeamsConfig, ) -> None: """Initialize the Group.""" self.characters = characters self.direction: str = direction self.terminal = terminal direction_speed_range = { "row": (args.beam_row_speed_range[0], args.beam_row_speed_range[1]), "column": (args.beam_column_speed_range[0], args.beam_column_speed_range[1]), } self.speed = random.randint(direction_speed_range[direction][0], direction_speed_range[direction][1]) * 0.1 self.next_character_counter: float = 0 if self.direction == "row": self.characters.sort(key=lambda character: character.input_coord.column) elif self.direction == "column": self.characters.sort(key=lambda character: character.input_coord.row) if random.choice([True, False]): self.characters.reverse() def increment_next_character_counter(self) -> None: """Increment the counter for the next character.""" self.next_character_counter += self.speed def get_next_character(self) -> tte.EffectCharacter | None: """Get the next character in the group. If the next character is already active, determined by having an active scene, the active scene is reset and None is returned. Otherwise, the next character is returned and the character is made visible. Returns: tte.EffectCharacter | None: The next character if the character or None if the character is already active. """ self.next_character_counter -= 1 next_character = self.characters.pop(0) if next_character.animation.active_scene: next_character.animation.active_scene.reset_scene() return_value = None else: self.terminal.set_character_visibility(next_character, is_visible=True) return_value = next_character next_character.animation.activate_scene(next_character.animation.query_scene("beam_" + self.direction)) return return_value def complete(self) -> bool: """Check if the group is complete. Returns: bool: True if the group is complete, False otherwise. """ return not self.characters def __init__(self, effect: Beams) -> None: """Initialize the BeamsIterator. Args: effect (Beams): The Beams effect instance. """ super().__init__(effect) self.pending_groups: list[BeamsIterator.Group] = [] self.character_final_color_map: dict[tte.EffectCharacter, tte.ColorPair] = {} self.active_groups: list[BeamsIterator.Group] = [] self.delay = 0 self.phase = "beams" self.final_wipe_groups = self.terminal.get_characters_grouped( tte.Terminal.CharacterGroup.DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT, ) self.build() def build(self) -> None: """Build the initial state for the Beams effect.""" final_gradient = tte.Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(outer_fill_chars=True, inner_fill_chars=True): if character.is_fill_character: self.character_final_color_map[character] = tte.ColorPair(fg="000000") continue if self.terminal.config.existing_color_handling == "dynamic" and self.preexisting_colors_present: fg_color = tte.Color("ffffff") bg_color = None if character.animation.input_fg_color: fg_color = character.animation.input_fg_color if character.animation.input_bg_color: bg_color = character.animation.input_bg_color self.character_final_color_map[character] = tte.ColorPair(fg=fg_color, bg=bg_color) else: self.character_final_color_map[character] = tte.ColorPair( fg=final_gradient_mapping[character.input_coord], ) beam_gradient = tte.Gradient(*self.config.beam_gradient_stops, steps=self.config.beam_gradient_steps) groups: list[BeamsIterator.Group] = [] for row in self.terminal.get_characters_grouped( tte.Terminal.CharacterGroup.ROW_TOP_TO_BOTTOM, outer_fill_chars=True, inner_fill_chars=True, ): groups.append(BeamsIterator.Group(row, "row", self.terminal, self.config)) # noqa: PERF401 for column in self.terminal.get_characters_grouped( tte.Terminal.CharacterGroup.COLUMN_LEFT_TO_RIGHT, outer_fill_chars=True, inner_fill_chars=True, ): groups.append(BeamsIterator.Group(column, "column", self.terminal, self.config)) # noqa: PERF401 for group in groups: for character in group.characters: beam_row_scn = character.animation.new_scene(scene_id="beam_row") beam_column_scn = character.animation.new_scene(scene_id="beam_column") brigthen_scn = character.animation.new_scene(scene_id="brighten") beam_row_scn.apply_gradient_to_symbols( self.config.beam_row_symbols, self.config.beam_gradient_frames, fg_gradient=beam_gradient, ) beam_column_scn.apply_gradient_to_symbols( self.config.beam_column_symbols, self.config.beam_gradient_frames, fg_gradient=beam_gradient, ) fg_fade_gradient = bg_fade_gradient = fg_brighten_gradient = bg_brighten_gradient = None char_fg_color = self.character_final_color_map[character].fg_color char_bg_color = self.character_final_color_map[character].bg_color if char_fg_color: faded_fg_color = character.animation.adjust_color_brightness(char_fg_color, 0.3) fg_fade_gradient = tte.Gradient(char_fg_color, faded_fg_color, steps=10) fg_brighten_gradient = tte.Gradient(faded_fg_color, char_fg_color, steps=10) if char_bg_color: faded_bg_color = character.animation.adjust_color_brightness(char_bg_color, 0.3) bg_fade_gradient = tte.Gradient(char_bg_color, faded_bg_color, steps=10) bg_brighten_gradient = tte.Gradient(faded_bg_color, char_bg_color, steps=10) beam_row_scn.apply_gradient_to_symbols( character.input_symbol, 5, fg_gradient=fg_fade_gradient, bg_gradient=bg_fade_gradient, ) beam_column_scn.apply_gradient_to_symbols( character.input_symbol, 5, fg_gradient=fg_fade_gradient, bg_gradient=bg_fade_gradient, ) brigthen_scn.apply_gradient_to_symbols( character.input_symbol, self.config.final_gradient_frames, fg_gradient=fg_brighten_gradient, bg_gradient=bg_brighten_gradient, ) self.pending_groups = groups random.shuffle(self.pending_groups) def __next__(self) -> str: """Return the next frame in the effect.""" if self.phase != "complete" or self.active_characters: if self.phase == "beams": if not self.delay: if self.pending_groups: for _ in range(random.randint(1, 5)): if self.pending_groups: self.active_groups.append(self.pending_groups.pop(0)) self.delay = self.config.beam_delay else: self.delay -= 1 for group in self.active_groups: group.increment_next_character_counter() if int(group.next_character_counter) > 1: for _ in range(int(group.next_character_counter)): if not group.complete(): next_char = group.get_next_character() if next_char: self.active_characters.add(next_char) self.active_groups = [group for group in self.active_groups if not group.complete()] if not self.pending_groups and not self.active_groups and not self.active_characters: self.phase = "final_wipe" elif self.phase == "final_wipe": if self.final_wipe_groups: for _ in range(self.config.final_wipe_speed): if not self.final_wipe_groups: break next_group = self.final_wipe_groups.pop(0) for character in next_group: character.animation.activate_scene(character.animation.query_scene("brighten")) self.terminal.set_character_visibility(character, is_visible=True) self.active_characters.add(character) else: self.phase = "complete" self.update() return self.frame raise StopIteration class Beams(BaseEffect[BeamsConfig]): """Creates beams which travel over the canvas illuminating the characters. Attributes: effect_config (BeamsConfig): Configuration for the effect. terminal_config (tte.TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[BeamsConfig]: return BeamsConfig @property def _iterator_cls(self) -> type[BeamsIterator]: return BeamsIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_binarypath.py000066400000000000000000000415271507200677100310350ustar00rootroot00000000000000"""Decodes characters into their binary form. Characters travel towards their input coordinate, moving at right angles. Classes: BinaryPath: Decodes characters into their binary form. Characters travel from outside the canvas towards their " "input coordinate, moving at right angles. BinaryPathConfig: Configuration for the BinaryPath effect. BinaryPathIterator: Effect iterator for the BinaryPath effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass import terminaltexteffects as tte from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Return the BinaryPath effect class and its configuration class.""" return BinaryPath, BinaryPathConfig @argclass( name="binarypath", help="Binary representations of each character move towards the home coordinate of the character.", description="binarypath | Binary representations of each character move through the terminal towards the " "home coordinate of the character.", epilog="Example: terminaltexteffects binarypath --final-gradient-stops 00d500 007500 --final-gradient-steps 12 " "--final-gradient-direction vertical --binary-colors 044E29 157e38 45bf55 95ed87 --movement-speed 1.0 " "--active-binary-groups 0.05", ) @dataclass class BinaryPathConfig(ArgsDataClass): """Configuration for the BinaryPath effect. Attributes: final_gradient_stops (tuple[tte.Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (tte.Gradient.Direction): Direction of the final gradient. binary_colors (tuple[tte.Color, ...]): Tuple of colors for the binary characters. Character color is randomly assigned from this list. movement_speed (float): Speed of the binary groups as they travel around the terminal. Valid values are n > 0. active_binary_groups (float): Maximum number of binary groups that are active at any given time as a percentage of the total number of binary groups. Lower this to improve performance. Valid values are 0 < n <= 1. """ final_gradient_stops: tuple[tte.Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(tte.Color("00d500"), tte.Color("007500")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[tte.Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, " "the characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name=["--final-gradient-steps"], type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number (n > 0) of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: tte.Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=tte.Gradient.Direction.RADIAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "tte.Gradient.Direction : Direction of the final gradient." binary_colors: tuple[tte.Color, ...] = ArgField( cmd_name=["--binary-colors"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(tte.Color("044E29"), tte.Color("157e38"), tte.Color("45bf55"), tte.Color("95ed87")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the binary characters. Character color is randomly " "assigned from this list.", ) # type: ignore[assignment] ( "tuple[tte.Color, ...] : Tuple of colors for the binary characters. Character color is randomly assigned from " "this list." ) movement_speed: float = ArgField( cmd_name="--movement-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=1.0, metavar=argvalidators.PositiveFloat.METAVAR, help="Speed of the binary groups as they travel around the terminal.", ) # type: ignore[assignment] "float : Speed of the binary groups as they travel around the terminal." active_binary_groups: float = ArgField( cmd_name="--active-binary-groups", type_parser=argvalidators.NonNegativeRatio.type_parser, default=0.05, metavar=argvalidators.NonNegativeRatio.METAVAR, help="Maximum number of binary groups that are active at any given time as a percentage of the total number " "of binary groups. Lower this to improve performance.", ) # type: ignore[assignment] ( "float : Maximum number of binary groups that are active at any given time as a percentage of the total number " "of binary groups. Lower this to improve performance." ) @classmethod def get_effect_class(cls) -> type[BinaryPath]: """Return the effect class associated with this configuration.""" return BinaryPath class BinaryPathIterator(BaseEffectIterator[BinaryPathConfig]): """Iterator for the BinaryPath effect.""" class _BinaryRepresentation: """Binary representation of a character. Used to animate the characters moving towards the input coordinate.""" def __init__(self, character: tte.EffectCharacter, terminal: tte.Terminal) -> None: self.character = character self.terminal = terminal self.binary_string = format(ord(self.character.animation.current_character_visual.symbol), "08b") self.binary_characters: list[tte.EffectCharacter] = [] self.pending_binary_characters: list[tte.EffectCharacter] = [] self.input_coord = self.character.input_coord self.is_active = False def _travel_complete(self) -> bool: return all(bin_char.motion.current_coord == self.input_coord for bin_char in self.binary_characters) def _deactivate(self) -> None: for bin_char in self.binary_characters: self.terminal.set_character_visibility(bin_char, is_visible=False) self.is_active = False def _activate_source_character(self) -> None: self.terminal.set_character_visibility(self.character, is_visible=True) self.character.animation.activate_scene(self.character.animation.query_scene("collapse_scn")) def __init__(self, effect: BinaryPath) -> None: """Initialize the BinaryPath effect iterator. Args: effect (BinaryPath): The BinaryPath effect instance. """ super().__init__(effect) self.pending_chars: list[tte.EffectCharacter] = [] self.pending_binary_representations: list[BinaryPathIterator._BinaryRepresentation] = [] self.character_final_color_map: dict[tte.EffectCharacter, tte.ColorPair] = {} self.last_frame_provided = False self.active_binary_reps: list[BinaryPathIterator._BinaryRepresentation] = [] self.complete = False self.phase = "travel" self.final_wipe_chars = self.terminal.get_characters_grouped( grouping=self.terminal.CharacterGroup.DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT, ) self.max_active_binary_groups: int = 0 self.build() def build(self) -> None: # noqa: PLR0915 """Build the BinaryPath effect.""" final_gradient = tte.Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): if self.terminal.config.existing_color_handling == "dynamic" and character.animation.input_fg_color: self.character_final_color_map[character] = tte.ColorPair( fg=character.animation.input_fg_color, bg=character.animation.input_bg_color, ) else: self.character_final_color_map[character] = tte.ColorPair( fg=final_gradient_mapping[character.input_coord], ) for character in self.terminal.get_characters(): bin_rep = BinaryPathIterator._BinaryRepresentation(character, self.terminal) for binary_char in bin_rep.binary_string: bin_rep.binary_characters.append(self.terminal.add_character(binary_char, tte.Coord(0, 0))) bin_rep.pending_binary_characters.append(bin_rep.binary_characters[-1]) self.pending_binary_representations.append(bin_rep) for bin_rep in self.pending_binary_representations: path_coords: list[tte.Coord] = [] starting_coord = self.terminal.canvas.random_coord(outside_scope=True) path_coords.append(starting_coord) last_orientation = random.choice(("col", "row")) next_coord = starting_coord # will be rebound in the loop while path_coords[-1] != bin_rep.character.input_coord: last_coord = path_coords[-1] if last_coord.column > bin_rep.character.input_coord.column: column_direction = -1 elif last_coord.column == bin_rep.character.input_coord.column: column_direction = 0 else: column_direction = 1 if last_coord.row > bin_rep.character.input_coord.row: row_direction = -1 elif last_coord.row == bin_rep.character.input_coord.row: row_direction = 0 else: row_direction = 1 max_column_distance = abs(last_coord.column - bin_rep.character.input_coord.column) max_row_distance = abs(last_coord.row - bin_rep.character.input_coord.row) if last_orientation == "col" and max_row_distance > 0: next_coord = tte.Coord( last_coord.column, last_coord.row + ( random.randint(1, min(max_row_distance, max(10, int(self.terminal.canvas.right * 0.2)))) * row_direction ), ) last_orientation = "row" elif last_orientation == "row" and max_column_distance > 0: next_coord = tte.Coord( last_coord.column + (random.randint(1, min(max_column_distance, 4)) * column_direction), last_coord.row, ) last_orientation = "col" else: next_coord = bin_rep.character.input_coord path_coords.append(next_coord) path_coords.append(next_coord) final_coord = bin_rep.character.input_coord path_coords.append(final_coord) for bin_effectchar in bin_rep.binary_characters: bin_effectchar.motion.set_coordinate(path_coords[0]) digital_path = bin_effectchar.motion.new_path(speed=self.config.movement_speed) for coord in path_coords: digital_path.new_waypoint(coord) bin_effectchar.motion.activate_path(digital_path) bin_effectchar.layer = 1 color_scn = bin_effectchar.animation.new_scene() color_scn.add_frame( bin_effectchar.animation.current_character_visual.symbol, 1, colors=tte.ColorPair(fg=random.choice(self.config.binary_colors)), ) bin_effectchar.animation.activate_scene(color_scn) for character in self.terminal.get_characters(): collapse_scn = character.animation.new_scene(ease=tte.easing.in_quad, scene_id="collapse_scn") dim_color = character.animation.adjust_color_brightness( self.character_final_color_map[character].fg_color, # type: ignore[arg-type] 0.5, ) dim_gradient = tte.Gradient(tte.Color("ffffff"), dim_color, steps=10) collapse_scn.apply_gradient_to_symbols(character.input_symbol, 7, fg_gradient=dim_gradient) brighten_scn = character.animation.new_scene(scene_id="brighten_scn") brighten_gradient = tte.Gradient(dim_color, self.character_final_color_map[character].fg_color, steps=10) # type: ignore[arg-type] brighten_scn.apply_gradient_to_symbols(character.input_symbol, 2, fg_gradient=brighten_gradient) self.max_active_binary_groups = max( 1, int(self.config.active_binary_groups * len(self.pending_binary_representations)), ) def __next__(self) -> str: """Return the next frame in the effect.""" if not self.complete or self.active_characters: if self.phase == "travel": while ( len(self.active_binary_reps) < self.max_active_binary_groups and self.pending_binary_representations ): next_binary_rep = self.pending_binary_representations.pop( random.randrange(len(self.pending_binary_representations)), ) next_binary_rep.is_active = True self.active_binary_reps.append(next_binary_rep) if self.active_binary_reps: for active_rep in self.active_binary_reps: if active_rep.pending_binary_characters: next_char = active_rep.pending_binary_characters.pop(0) self.active_characters.add(next_char) self.terminal.set_character_visibility(next_char, is_visible=True) elif active_rep._travel_complete(): active_rep._deactivate() active_rep._activate_source_character() self.active_characters.add(active_rep.character) self.active_binary_reps = [ binary_rep for binary_rep in self.active_binary_reps if binary_rep.is_active ] if not self.active_characters: self.phase = "wipe" if self.phase == "wipe": if self.final_wipe_chars: next_group = self.final_wipe_chars.pop(0) for character in next_group: character.animation.activate_scene(character.animation.query_scene("brighten_scn")) self.terminal.set_character_visibility(character, is_visible=True) self.active_characters.add(character) else: self.complete = True self.update() return self.frame if not self.last_frame_provided: self.last_frame_provided = True return self.frame raise StopIteration class BinaryPath(BaseEffect): """Decode characters into their binary form. Characters travel to their input coordinate, moving at right angles. Attributes: effect_config (BinaryPathConfig): Configuration for the BinaryPath effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[BinaryPathConfig]: return BinaryPathConfig @property def _iterator_cls(self) -> type[BinaryPathIterator]: return BinaryPathIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_blackhole.py000066400000000000000000000507111507200677100306130ustar00rootroot00000000000000"""Creates a blackhole in a starfield, consumes the stars, explodes the input data back into position. Classes: BlackholeConfig: Configuration for the Blackhole effect. Blackhole: Creates a blackhole in a starfield, consumes the stars, explodes the input data back into position. BlackholeIterator: Iterator for the Blackhole effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, Coord, EffectCharacter, EventHandler, Gradient, Scene, easing, geometry from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass from terminaltexteffects.utils.graphics import ColorPair def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Return the Blackhole effect class and its configuration class.""" return Blackhole, BlackholeConfig @argclass( name="blackhole", help="Characters are consumed by a black hole and explode outwards.", description="blackhole | Characters are consumed by a black hole and explode outwards.", epilog="Example: terminaltexteffects blackhole --star-colors ffcc0d ff7326 ff194d bf2669 702a8c 049dbf " "--final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --final-gradient-direction vertical", ) @dataclass class BlackholeConfig(ArgsDataClass): """Configuration for the Blackhole effect. Attributes: blackhole_color (Color): Color for the stars that comprise the blackhole border. star_colors (tuple[Color, ...]): Tuple of colors from which character colors will be chosen and applied after the explosion, but before the cooldown to final color. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the character gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ blackhole_color: Color = ArgField( cmd_name=["--blackhole-color"], type_parser=argvalidators.ColorArg.type_parser, default=Color("ffffff"), metavar=argvalidators.ColorArg.METAVAR, help="Color for the stars that comprise the blackhole border.", ) # type: ignore[assignment] "Color : Color for the stars that comprise the blackhole border." star_colors: tuple[Color, ...] = ArgField( cmd_name=["--star-colors"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("ffcc0d"), Color("ff7326"), Color("ff194d"), Color("bf2669"), Color("702a8c"), Color("049dbf")), metavar=argvalidators.ColorArg.METAVAR, help="List of colors from which character colors will be chosen and applied after the explosion, but before " "the cooldown to final color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors from which character colors will be chosen and applied after the " "explosion, but before the cooldown to final color." ) final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("8A008A"), Color("00D1FF"), Color("ffffff")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If " "only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name=["--final-gradient-steps"], type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will create " "a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.DIAGONAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Blackhole]: """Return the effect class associated with this configuration.""" return Blackhole class BlackholeIterator(BaseEffectIterator[BlackholeConfig]): """Iterator for the Blackhole effect.""" def __init__(self, effect: Blackhole) -> None: """Initialize the Blackhole effect iterator. Args: effect (Blackhole): The Blackhole effect instance. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.blackhole_chars: list[EffectCharacter] = [] self.awaiting_consumption_chars: list[EffectCharacter] = [] self.blackhole_radius = max( min( round(self.terminal.canvas.width * 0.3), round(self.terminal.canvas.height * 0.3), ), 3, ) self.character_final_color_map: dict[EffectCharacter, Color] = {} self.preexisting_colors_present = any( any((character.animation.input_fg_color, character.animation.input_bg_color)) for character in self.terminal.get_characters() ) self.build() def prepare_blackhole(self) -> None: """Prepare the blackhole and starfield characters.""" star_symbols = ["*", "'", "`", "¤", "•", "°", "·"] starfield_colors = Gradient(Color("4a4a4d"), Color("ffffff"), steps=6).spectrum gradient_map = {} for color in starfield_colors: gradient_map[color] = Gradient(color, Color("000000"), steps=10) available_chars = list(self.terminal._input_characters) while len(self.blackhole_chars) < self.blackhole_radius * 3 and available_chars: self.blackhole_chars.append(available_chars.pop(random.randrange(0, len(available_chars)))) black_hole_ring_positions = geometry.find_coords_on_circle( self.terminal.canvas.center, self.blackhole_radius, len(self.blackhole_chars), ) for position_index, character in enumerate(self.blackhole_chars): starting_pos = black_hole_ring_positions[position_index] blackhole_path = character.motion.new_path(path_id="blackhole", speed=0.5, ease=easing.in_out_sine) blackhole_path.new_waypoint(starting_pos) blackhole_scn = character.animation.new_scene(scene_id="blackhole") blackhole_scn.add_frame("*", 1, colors=ColorPair(fg=self.config.blackhole_color)) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, blackhole_path, EventHandler.Action.SET_LAYER, 1, ) # make rotation waypoints blackhole_rotation_path = character.motion.new_path(path_id="blackhole_rotation", speed=0.2, loop=True) for coord in black_hole_ring_positions[position_index:] + black_hole_ring_positions[:position_index]: blackhole_rotation_path.new_waypoint(coord, waypoint_id=str(len(blackhole_rotation_path.waypoints))) for character in self.terminal.get_characters(): self.terminal.set_character_visibility(character, is_visible=True) starting_scn = character.animation.new_scene() star_symbol = random.choice(star_symbols) star_color = random.choice(starfield_colors) starting_scn.add_frame(star_symbol, 1, colors=ColorPair(fg=star_color)) character.animation.activate_scene(starting_scn) if character not in self.blackhole_chars: starfield_coord = self.terminal.canvas.random_coord() character.motion.set_coordinate(starfield_coord) if starfield_coord.row > self.terminal.canvas.center_row: if starfield_coord.column in range( round(self.terminal.canvas.right * 0.4), round(self.terminal.canvas.right * 0.7), ): # if within the top center 40% of the screen control_point = Coord(self.terminal.canvas.center.column, starfield_coord.row) else: control_point = Coord(starfield_coord.column, self.terminal.canvas.center_row) elif starfield_coord.row < self.terminal.canvas.center_row: if starfield_coord.column in range( round(self.terminal.canvas.right * 0.4), round(self.terminal.canvas.right * 0.7), ): # if within the bottom center 40% of the screen control_point = Coord(self.terminal.canvas.center.column, starfield_coord.row) else: control_point = Coord(starfield_coord.column, self.terminal.canvas.center_row) else: control_point = self.terminal.canvas.center singularity_path = character.motion.new_path(path_id="singularity", speed=0.3, ease=easing.in_expo) singularity_path.new_waypoint(self.terminal.canvas.center, bezier_control=control_point) consumed_scn = character.animation.new_scene() for color in gradient_map[star_color]: consumed_scn.add_frame(star_symbol, 1, colors=ColorPair(fg=color)) consumed_scn.add_frame(" ", 1) consumed_scn.sync = Scene.SyncMetric.DISTANCE character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, singularity_path, EventHandler.Action.SET_LAYER, 2, ) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, singularity_path, EventHandler.Action.ACTIVATE_SCENE, consumed_scn, ) self.awaiting_consumption_chars.append(character) random.shuffle(self.awaiting_consumption_chars) def rotate_blackhole(self) -> None: """Rotate the blackhole characters.""" for character in self.blackhole_chars: character.motion.activate_path(character.motion.query_path("blackhole_rotation")) self.active_characters.add(character) def collapse_blackhole(self) -> None: """Collapse the blackhole characters.""" black_hole_ring_positions = geometry.find_coords_on_circle( self.terminal.canvas.center, self.blackhole_radius + 3, len(self.blackhole_chars), ) unstable_symbols = ["◦", "◎", "◉", "●", "◉", "◎", "◦"] point_char_made = False for character in self.blackhole_chars: next_pos = black_hole_ring_positions.pop(0) expand_path = character.motion.new_path(speed=0.1, ease=easing.in_expo) expand_path.new_waypoint(next_pos) collapse_path = character.motion.new_path(speed=0.3, ease=easing.in_expo) collapse_path.new_waypoint(self.terminal.canvas.center) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, expand_path, EventHandler.Action.ACTIVATE_PATH, collapse_path, ) if not point_char_made: point_scn = character.animation.new_scene() for _ in range(3): for symbol in unstable_symbols: point_scn.add_frame( symbol, 6, colors=ColorPair(fg=random.choice(self.config.star_colors)), ) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, collapse_path, EventHandler.Action.ACTIVATE_SCENE, point_scn, ) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, collapse_path, EventHandler.Action.SET_LAYER, 3, ) point_char_made = True character.motion.activate_path(expand_path) self.active_characters.add(character) def explode_singularity(self) -> None: """Explode the singularity characters.""" star_colors = [ Color("ffcc0d"), Color("ff7326"), Color("ff194d"), Color("bf2669"), Color("702a8c"), Color("049dbf"), ] for character in self.terminal.get_characters(): nearby_coord = geometry.find_coords_on_circle(character.input_coord, 3, 5)[random.randrange(0, 5)] nearby_path = character.motion.new_path(speed=random.randint(2, 3) / 10, ease=easing.out_expo) nearby_path.new_waypoint(nearby_coord) input_path = character.motion.new_path(speed=random.randint(3, 5) / 100, ease=easing.in_cubic) input_path.new_waypoint(character.input_coord) explode_scn = character.animation.new_scene() explode_star_color = random.choice(star_colors) explode_scn.add_frame(character.input_symbol, 1, colors=ColorPair(fg=explode_star_color)) cooling_scn = character.animation.new_scene() if self.terminal.config.existing_color_handling == "dynamic" and self.preexisting_colors_present: if not any((character.animation.input_fg_color, character.animation.input_bg_color)): cooling_scn.add_frame(character.input_symbol, 1, colors=ColorPair()) else: cooling_gradient_fg = None cooling_gradient_bg = None if character.animation.input_fg_color: cooling_gradient_fg = Gradient( explode_star_color, character.animation.input_fg_color, steps=10, ) if character.animation.input_bg_color: cooling_gradient_bg = Gradient( explode_star_color, character.animation.input_bg_color, steps=10, ) cooling_scn.apply_gradient_to_symbols( character.input_symbol, 20, fg_gradient=cooling_gradient_fg, bg_gradient=cooling_gradient_bg, ) else: cooling_gradient = Gradient(explode_star_color, self.character_final_color_map[character], steps=10) cooling_scn.apply_gradient_to_symbols(character.input_symbol, 20, fg_gradient=cooling_gradient) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, nearby_path, EventHandler.Action.ACTIVATE_PATH, input_path, ) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, nearby_path, EventHandler.Action.ACTIVATE_SCENE, cooling_scn, ) character.animation.activate_scene(explode_scn) character.motion.activate_path(nearby_path) self.active_characters.add(character) def build(self) -> None: """Build the Blackhole effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] self.prepare_blackhole() self.formation_delay = max(100 // len(self.blackhole_chars), 10) self.f_delay = self.formation_delay self.next_char_consuming_delay = 0 self.max_consume = max(min(int(0.1 * len(self.terminal._input_characters)), 15), 2) self.phase = "forming" self.awaiting_blackhole_chars = list(self.blackhole_chars) def __next__(self) -> str: """Return the next frame in the Blackhole effect.""" if self.active_characters or self.phase != "complete": if self.phase == "forming": if self.awaiting_blackhole_chars: if not self.f_delay: next_char = self.awaiting_blackhole_chars.pop(0) next_char.motion.activate_path(next_char.motion.query_path("blackhole")) next_char.animation.activate_scene(next_char.animation.query_scene("blackhole")) self.active_characters.add(next_char) self.f_delay = self.formation_delay else: self.f_delay -= 1 elif not self.active_characters: self.rotate_blackhole() self.phase = "consuming" elif self.phase == "consuming": if self.awaiting_consumption_chars: if not self.next_char_consuming_delay: for _ in range(random.randrange(1, self.max_consume)): if self.awaiting_consumption_chars: next_char = self.awaiting_consumption_chars.pop(0) next_char.motion.activate_path(next_char.motion.query_path("singularity")) self.active_characters.add(next_char) else: break self.max_consume += 1 self.next_char_consuming_delay = random.randrange(0, 10) else: self.next_char_consuming_delay -= 1 elif all(character in self.blackhole_chars for character in self.active_characters): self.phase = "collapsing" elif self.phase == "collapsing": self.collapse_blackhole() self.phase = "exploding" elif self.phase == "exploding" and all( character.motion.active_path is None and character.animation.active_scene is None for character in self.blackhole_chars ): self.explode_singularity() self.phase = "complete" self.update() return self.frame raise StopIteration class Blackhole(BaseEffect[BlackholeConfig]): """Creates a blackhole in a starfield, consumes the stars, explodes the input data back into position. Attributes: effect_config (BlackholeConfig): Configuration for the Blackhole effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[BlackholeConfig]: return BlackholeConfig @property def _iterator_cls(self) -> type[BlackholeIterator]: return BlackholeIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_bouncyballs.py000066400000000000000000000260431507200677100312050ustar00rootroot00000000000000"""Characters fall from the top of the canvas as bouncy balls before settling into place. Classes: BouncyBalls: Characters fall from the top of the canvas as bouncy balls before settling into place. BouncyBallsConfig: Configuration for the BouncyBalls effect. BouncyBallsIterator: Iterator for the BouncyBalls effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, Coord, EffectCharacter, Gradient, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass from terminaltexteffects.utils.graphics import ColorPair def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return BouncyBalls, BouncyBallsConfig @argclass( name="bouncyballs", help="Characters are bouncy balls falling from the top of the canvas.", description="bouncyballs | Characters are bouncy balls falling from the top of the canvas.", epilog=f"{argvalidators.EASING_EPILOG}" "Example: terminaltexteffects bouncyballs --ball-colors d1f4a5 96e2a4 5acda9 --ball-symbols o '*' O 0 . " "--final-gradient-stops f8ffae 43c6ac --final-gradient-steps 12 --final-gradient-direction diagonal " "--ball-delay 7 --movement-speed 0.25 --easing OUT_BOUNCE", ) @dataclass class BouncyBallsConfig(ArgsDataClass): """Configuration for the BouncyBalls effect. Attributes: ball_colors (tuple[Color, ...]): Tuple of colors from which ball colors will be randomly selected. If no colors are provided, the colors are random. ball_symbols (tuple[str, ...] | str): Tuple of symbols to use for the balls. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. ball_delay (int): Number of frames between ball drops, increase to reduce ball drop rate. Valid values are n > 0. movement_speed (float): Movement speed of the characters. Valid values are n > 0. easing (easing.EasingFunction): Easing function to use for character movement. """ ball_colors: tuple[Color, ...] = ArgField( cmd_name=["--ball-colors"], type_parser=argvalidators.ColorArg.type_parser, metavar=argvalidators.ColorArg.METAVAR, nargs="+", default=(Color("d1f4a5"), Color("96e2a4"), Color("5acda9")), help="Space separated list of colors from which ball colors will be randomly selected. If no colors are " "provided, the colors are random.", ) # type: ignore[assignment] "tuple[Color, ...] : Tuple of colors from which ball colors will be randomly selected. If no colors are " "provided, the colors are random." ball_symbols: tuple[str, ...] = ArgField( cmd_name="--ball-symbols", type_parser=argvalidators.Symbol.type_parser, nargs="+", default=("*", "o", "O", "0", "."), metavar=argvalidators.Symbol.METAVAR, help="Space separated list of symbols to use for the balls.", ) # type: ignore[assignment] "tuple[str, ...] | str : Tuple of symbols to use for the balls." ball_delay: int = ArgField( cmd_name="--ball-delay", type_parser=argvalidators.NonNegativeInt.type_parser, default=7, metavar=argvalidators.NonNegativeInt.METAVAR, help="Number of frames between ball drops, increase to reduce ball drop rate.", ) # type: ignore[assignment] "int : Number of frames between ball drops, increase to reduce ball drop rate." movement_speed: float = ArgField( cmd_name="--movement-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=0.25, metavar=argvalidators.PositiveFloat.METAVAR, help="Movement speed of the characters. ", ) # type: ignore[assignment] "float : Movement speed of the characters. " movement_easing: easing.EasingFunction = ArgField( cmd_name="--movement-easing", type_parser=argvalidators.Ease.type_parser, default=easing.out_bounce, help="Easing function to use for character movement.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for character movement." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("f8ffae"), Color("43c6ac")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name=["--final-gradient-steps"], type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will create " "a smoother and longer gradient animation." final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.DIAGONAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[BouncyBalls]: """Get the effect class associated with this configuration.""" return BouncyBalls class BouncyBallsIterator(BaseEffectIterator[BouncyBallsConfig]): """Iterator for the BouncyBalls effect.""" def __init__(self, effect: BouncyBalls) -> None: """Initialize the effect iterator. Args: effect (BouncyBalls): The effect to use for the iterator. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.group_by_row: dict[int, list[EffectCharacter | None]] = {} self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the initial state of the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] color = random.choice(self.config.ball_colors) symbol = random.choice(self.config.ball_symbols) ball_scene = character.animation.new_scene() ball_scene.add_frame(symbol, 1, colors=ColorPair(fg=color)) final_scene = character.animation.new_scene() char_final_gradient = Gradient(color, self.character_final_color_map[character], steps=10) final_scene.apply_gradient_to_symbols(character.input_symbol, 10, fg_gradient=char_final_gradient) character.motion.set_coordinate( Coord(character.input_coord.column, int(self.terminal.canvas.top * random.uniform(1.0, 1.5))), ) input_coord_path = character.motion.new_path( speed=self.config.movement_speed, ease=self.config.movement_easing, ) input_coord_path.new_waypoint(character.input_coord) character.motion.activate_path(input_coord_path) character.animation.activate_scene(ball_scene) character.event_handler.register_event( character.event_handler.Event.PATH_COMPLETE, input_coord_path, character.event_handler.Action.ACTIVATE_SCENE, final_scene, ) self.pending_chars.append(character) for character in sorted(self.pending_chars, key=lambda c: c.input_coord.row): if character.input_coord.row not in self.group_by_row: self.group_by_row[character.input_coord.row] = [] self.group_by_row[character.input_coord.row].append(character) self.pending_chars.clear() self.ball_delay = 0 def __next__(self) -> str: """Return the next frame in the animation.""" if self.group_by_row or self.active_characters or self.pending_chars: if not self.pending_chars and self.group_by_row: self.pending_chars.extend(self.group_by_row.pop(min(self.group_by_row.keys()))) # type: ignore[arg-type] if self.pending_chars: if self.ball_delay == 0: for _ in range(random.randint(2, 6)): if self.pending_chars: next_character = self.pending_chars.pop(random.randint(0, len(self.pending_chars) - 1)) self.terminal.set_character_visibility(next_character, is_visible=True) self.active_characters.add(next_character) else: break self.ball_delay = self.config.ball_delay else: self.ball_delay -= 1 self.update() return self.frame raise StopIteration class BouncyBalls(BaseEffect[BouncyBallsConfig]): """Characters fall from the top of the canvas as bouncy balls before settling into place. Attributes: effect_config (BouncyBallsConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[BouncyBallsConfig]: return BouncyBallsConfig @property def _iterator_cls(self) -> type[BouncyBallsIterator]: return BouncyBallsIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_bubbles.py000066400000000000000000000443121507200677100303050ustar00rootroot00000000000000"""Forms bubbles with the characters. Bubbles float down and pop. Classes: Bubbles: Forms bubbles with the characters. Bubbles float down and pop. BubblesConfig: Configuration for the Bubbles effect. BubblesIterator: Iterates over the Bubbles effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, Coord, EffectCharacter, EventHandler, Gradient, Terminal, easing, geometry from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass from terminaltexteffects.utils.graphics import ColorPair def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Bubbles, BubblesConfig @argclass( name="bubbles", help="Characters are formed into bubbles that float down and pop.", description="bubbles | Characters are formed into bubbles that float down and pop.", epilog=f"{argvalidators.EASING_EPILOG}" "Example: terminaltexteffects bubbles --bubble-colors d33aff 7395c4 43c2a7 02ff7f --pop-color ffffff " "--final-gradient-stops d33aff 02ff7f --final-gradient-steps 12 --final-gradient-direction diagonal " "--bubble-speed 0.1 --bubble-delay 50 --pop-condition row --easing IN_OUT_SINE", ) @dataclass class BubblesConfig(ArgsDataClass): """Configuration for the Bubbles effect. Attributes: rainbow (bool): If set, the bubbles will be colored with a rotating rainbow gradient. bubble_colors (tuple[Color, ...]): Tuple of colors for the bubbles. Ignored if --no-rainbow is left as default False. pop_color (Color): Color for the spray emitted when a bubble pops. bubble_speed (float): Speed of the floating bubbles. Valid values are n > 0. bubble_delay (int): Number of frames between bubbles. Valid values are n >= 0. pop_condition (typing.Literal["row", "bottom", "anywhere"]): Condition for a bubble to pop. 'row' will pop the bubble when it reaches the the lowest row for which a character in the bubble originates. 'bottom' will pop the bubble at the bottom row of the terminal. 'anywhere' will pop the bubble randomly, or at the bottom of the terminal. movement_easing (easing.EasingFunction): Easing function to use for character movement after a bubble pops. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ rainbow: bool = ArgField( cmd_name="--rainbow", action="store_true", default=False, help="If set, the bubbles will be colored with a rotating rainbow gradient.", ) # type: ignore[assignment] "bool : If set, the bubbles will be colored with a rotating rainbow gradient." bubble_colors: tuple[Color, ...] = ArgField( cmd_name="--bubble-colors", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("d33aff"), Color("7395c4"), Color("43c2a7"), Color("02ff7f")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the bubbles. Ignored if --no-rainbow is left as " "default False.", ) # type: ignore[assignment] "tuple[Color, ...] : Tuple of colors for the bubbles. Ignored if --no-rainbow is left as default False." pop_color: Color = ArgField( cmd_name="--pop-color", type_parser=argvalidators.ColorArg.type_parser, default=Color("ffffff"), metavar=argvalidators.ColorArg.METAVAR, help="Color for the spray emitted when a bubble pops.", ) # type: ignore[assignment] "Color : Color for the spray emitted when a bubble pops." bubble_speed: float = ArgField( cmd_name="--bubble-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=0.1, metavar=argvalidators.PositiveFloat.METAVAR, help="Speed of the floating bubbles. ", ) # type: ignore[assignment] "float : Speed of the floating bubbles. " bubble_delay: int = ArgField( cmd_name="--bubble-delay", type_parser=argvalidators.PositiveInt.type_parser, default=50, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames between bubbles.", ) # type: ignore[assignment] "int : Number of frames between bubbles." pop_condition: typing.Literal["row", "bottom", "anywhere"] = ArgField( cmd_name="--pop-condition", default="row", choices=["row", "bottom", "anywhere"], help="Condition for a bubble to pop. 'row' will pop the bubble when it reaches the the lowest row for which " "a character in the bubble originates. 'bottom' will pop the bubble at the bottom row of the terminal. " "'anywhere' will pop the bubble randomly, or at the bottom of the terminal.", ) # type: ignore[assignment] ( "typing.Literal['row', 'bottom', 'anywhere'] : Condition for a bubble to pop. 'row' will pop the bubble when " "it reaches the the lowest row for which a character in the bubble originates. 'bottom' will pop the bubble at " "the bottom row of the terminal. 'anywhere' will pop the bubble randomly, or at the bottom of the terminal." ) movement_easing: easing.EasingFunction = ArgField( cmd_name=["--movement-easing"], default=easing.in_out_sine, type_parser=argvalidators.Ease.type_parser, metavar=argvalidators.Ease.METAVAR, help="Easing function to use for character movement after a bubble pops.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for character movement after a bubble pops." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("d33aff"), Color("02ff7f")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.DIAGONAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Bubbles]: """Get the effect class associated with this configuration.""" return Bubbles class BubblesIterator(BaseEffectIterator[BubblesConfig]): """Iterator for the Bubbles effect.""" class Bubble: """A bubble of characters that float down and pop.""" def __init__( self, effect: BubblesIterator, origin: Coord, characters: list[EffectCharacter], terminal: Terminal, ) -> None: """Initialize the bubble.""" self.effect = effect self.characters = characters self.terminal = terminal self.radius = max(len(self.characters) // 5, 1) self.origin = origin self.anchor_char = self.terminal.add_character(" ", self.origin) if self.effect.config.pop_condition == "row": self.lowest_row = min([char.input_coord.row for char in self.characters]) else: self.lowest_row = self.effect.terminal.canvas.bottom self.set_character_coordinates() self.landed = False self.make_waypoints() self.make_gradients() def set_character_coordinates(self) -> None: """Set the coordinates of the characters in the bubble.""" for i, char in enumerate(self.characters): point = geometry.find_coords_on_circle( self.anchor_char.motion.current_coord, self.radius, len(self.characters), unique=False, )[i] char.motion.set_coordinate(point) if point.row == self.lowest_row: self.landed = True if self.effect.config.pop_condition == "anywhere" and random.random() < 0.002: self.landed = True def make_waypoints(self) -> None: """Make the waypoints for the bubble.""" waypoint_column = random.randint(self.effect.terminal.canvas.left, self.effect.terminal.canvas.right) floor_path = self.anchor_char.motion.new_path(speed=self.effect.config.bubble_speed) floor_path.new_waypoint(Coord(waypoint_column, self.lowest_row)) self.anchor_char.motion.activate_path(floor_path) def make_gradients(self) -> None: """Make the gradients for the bubble.""" if self.effect.config.rainbow: rainbow_gradient = list(self.effect.rainbow_gradient.spectrum) gradient_offset = 0 for character in self.characters: sheen_scene = character.animation.new_scene() for step in rainbow_gradient: sheen_scene.add_frame(character.input_symbol, 5, colors=ColorPair(fg=step)) gradient_offset += 2 gradient_offset %= len(rainbow_gradient) rainbow_gradient = rainbow_gradient[gradient_offset:] + rainbow_gradient[:gradient_offset] character.animation.activate_scene(sheen_scene) if character.animation.active_scene: character.animation.active_scene.is_looping = True else: bubble_color = random.choice(self.effect.config.bubble_colors) for character in self.characters: sheen_scene = character.animation.new_scene() sheen_scene.add_frame(character.input_symbol, 1, colors=ColorPair(fg=bubble_color)) character.animation.activate_scene(sheen_scene) def pop(self) -> None: """Pop the bubble.""" char: EffectCharacter point: Coord for char, point in zip( self.characters, geometry.find_coords_on_circle( self.anchor_char.motion.current_coord, self.radius + 3, len(self.characters), ), ): pop_out_path = char.motion.new_path(path_id="pop_out", speed=0.2, ease=easing.out_expo) pop_out_path.new_waypoint(point) char.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, pop_out_path, EventHandler.Action.ACTIVATE_PATH, char.motion.paths["final"], ) for character in self.characters: character.animation.activate_scene(character.animation.query_scene("pop_1")) character.motion.activate_path(character.motion.query_path("pop_out")) def activate(self) -> None: """Activate the bubble.""" for char in self.characters: self.terminal.set_character_visibility(char, is_visible=True) def move(self) -> None: """Move the bubble.""" self.anchor_char.motion.move() self.set_character_coordinates() for character in self.characters: character.animation.step_animation() def __init__(self, effect: Bubbles) -> None: """Initialize the effect iterator.""" super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.bubbles: list[BubblesIterator.Bubble] = [] red = Color("#e81416") orange = Color("#ffa500") yellow = Color("#faeb36") green = Color("#79c314") blue = Color("#487de7") indigo = Color("#4b369d") violet = Color("#70369d") self.rainbow_gradient = Gradient(red, orange, yellow, green, blue, indigo, violet, steps=5) self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the initial state of the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] character.layer = 1 pop_1_scene = character.animation.new_scene(scene_id="pop_1") pop_2_scene = character.animation.new_scene() pop_1_scene.add_frame("*", 20, colors=ColorPair(fg=self.config.pop_color)) pop_2_scene.add_frame("'", 20, colors=ColorPair(fg=self.config.pop_color)) final_scene = character.animation.new_scene() char_final_gradient = Gradient(self.config.pop_color, self.character_final_color_map[character], steps=10) final_scene.apply_gradient_to_symbols(character.input_symbol, 10, fg_gradient=char_final_gradient) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, pop_1_scene, EventHandler.Action.ACTIVATE_SCENE, pop_2_scene, ) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, pop_2_scene, EventHandler.Action.ACTIVATE_SCENE, final_scene, ) final_path = character.motion.new_path( path_id="final", speed=0.3, ease=easing.in_out_expo, ) final_path.new_waypoint(character.input_coord) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, final_path, EventHandler.Action.SET_LAYER, 0, ) unbubbled_chars = [] for char_list in self.terminal.get_characters_grouped(grouping=self.terminal.CharacterGroup.ROW_BOTTOM_TO_TOP): unbubbled_chars.extend(char_list) self.bubbles = [] while unbubbled_chars: bubble_group = [] if len(unbubbled_chars) < 5: bubble_group.extend(unbubbled_chars) unbubbled_chars.clear() else: for _ in range(random.randint(5, min(len(unbubbled_chars), 20))): bubble_group.append(unbubbled_chars.pop(0)) # noqa: PERF401 bubble_origin = Coord( random.randint(self.terminal.canvas.left, self.terminal.canvas.right), self.terminal.canvas.top, ) new_bubble = BubblesIterator.Bubble(self, bubble_origin, bubble_group, self.terminal) self.bubbles.append(new_bubble) self.animating_bubbles: list[BubblesIterator.Bubble] = [] self.steps_since_last_bubble = 0 def __next__(self) -> str: """Return the next frame in the animation.""" if self.animating_bubbles or self.active_characters or self.bubbles: if self.bubbles and self.steps_since_last_bubble >= self.config.bubble_delay: next_bubble = self.bubbles.pop(0) next_bubble.activate() self.animating_bubbles.append(next_bubble) self.steps_since_last_bubble = 0 self.steps_since_last_bubble += 1 for bubble in self.animating_bubbles: if bubble.landed: bubble.pop() self.active_characters = self.active_characters.union(bubble.characters) self.animating_bubbles = [bubble for bubble in self.animating_bubbles if not bubble.landed] for bubble in self.animating_bubbles: bubble.move() self.update() return self.frame raise StopIteration class Bubbles(BaseEffect[BubblesConfig]): """Forms bubbles with the characters. Bubbles float down and pop. Attributes: effect_config (BubblesConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[BubblesConfig]: return BubblesConfig @property def _iterator_cls(self) -> type[BubblesIterator]: return BubblesIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_burn.py000066400000000000000000000206531507200677100276370ustar00rootroot00000000000000"""Characters are ignited and burn up the screen. Classes: Burn: Characters are ignited and burn up the screen. BurnConfig: Configuration for the Burn effect. BurnIterator: Iterates over the Burn effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, EffectCharacter, EventHandler, Gradient from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass from terminaltexteffects.utils.graphics import ColorPair def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Burn, BurnConfig @argclass( name="burn", help="Burns vertically in the canvas.", description="burn | Burn the canvas.", epilog=( "Example: terminaltexteffects burn --starting-color 837373 --burn-colors ffffff fff75d fe650d 8a003c " "510100 --final-gradient-stops 00c3ff ffff1c --final-gradient-steps 12" ), ) @dataclass class BurnConfig(ArgsDataClass): """Configuration for the Burn effect. Attributes: starting_color (Color): Color of the characters before they start to burn. burn_colors (tuple[Color, ...]): Colors transitioned through as the characters burn. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ starting_color: Color = ArgField( cmd_name="--starting-color", type_parser=argvalidators.ColorArg.type_parser, default=Color("837373"), metavar=argvalidators.ColorArg.METAVAR, help="Color of the characters before they start to burn.", ) # type: ignore[assignment] "Color : Color of the characters before they start to burn." burn_colors: tuple[Color, ...] = ArgField( cmd_name=["--burn-colors"], type_parser=argvalidators.ColorArg.type_parser, default=(Color("ffffff"), Color("fff75d"), Color("fe650d"), Color("8A003C"), Color("510100")), nargs="+", metavar=argvalidators.ColorArg.METAVAR, help="Colors transitioned through as the characters burn.", ) # type: ignore[assignment] "tuple[Color, ...] : Colors transitioned through as the characters burn." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("00c3ff"), Color("ffff1c")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name=["--final-gradient-steps"], type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Burn]: """Get the effect class associated with this configuration.""" return Burn class BurnIterator(BaseEffectIterator[BurnConfig]): """Iterator for the Burn effect.""" def __init__(self, effect: Burn) -> None: """Initialize the Burn effect iterator. Args: effect (Burn): The effect to use for the iterator. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the Burn effect.""" vertical_build_order = [ "'", ".", "▖", "▙", "█", "▜", "▀", "▝", ".", ] final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] fire_gradient = Gradient(*self.config.burn_colors, steps=10) groups = dict( enumerate( self.terminal.get_characters_grouped(grouping=self.terminal.CharacterGroup.COLUMN_LEFT_TO_RIGHT), ), ) def groups_remaining(rows: dict[int, list[EffectCharacter]]) -> bool: """Check if there are any groups remaining.""" return any(row for row in rows.values()) while groups_remaining(groups): keys = [key for key in groups if groups[key]] next_char = groups[random.choice(keys)].pop(0) self.terminal.set_character_visibility(next_char, is_visible=True) next_char.animation.set_appearance( next_char.input_symbol, colors=ColorPair(fg=self.config.starting_color), ) burn_scn = next_char.animation.new_scene(scene_id="burn") burn_scn.apply_gradient_to_symbols(vertical_build_order, 12, fg_gradient=fire_gradient) final_color_scn = next_char.animation.new_scene() for color in Gradient(fire_gradient.spectrum[-1], self.character_final_color_map[next_char], steps=8): final_color_scn.add_frame(next_char.input_symbol, 4, colors=ColorPair(fg=color)) next_char.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, burn_scn, EventHandler.Action.ACTIVATE_SCENE, final_color_scn, ) self.pending_chars.append(next_char) def __next__(self) -> str: """Return the next frame in the animation.""" if self.pending_chars or self.active_characters: for _ in range(random.randint(2, 4)): if self.pending_chars: next_char = self.pending_chars.pop(0) next_char.animation.activate_scene(next_char.animation.query_scene("burn")) self.active_characters.add(next_char) self.update() return self.frame raise StopIteration class Burn(BaseEffect[BurnConfig]): """Characters are ignited and burn up the screen. Attributes: effect_config (BurnConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[BurnConfig]: return BurnConfig @property def _iterator_cls(self) -> type[BurnIterator]: return BurnIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_colorshift.py000066400000000000000000000333761507200677100310530ustar00rootroot00000000000000"""Display a gradient that shifts colors across the terminal. Classes: ColorShift: Display a gradient that shifts colors across the terminal. ColorShiftConfig: Configuration for the ColorShift effect. ColorShiftIterator: Iterator for the ColorShift effect. Does not normally need to be called directly. """ from __future__ import annotations import typing from dataclasses import dataclass from terminaltexteffects import Color, EffectCharacter, EventHandler, Gradient, geometry from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass from terminaltexteffects.utils.graphics import ColorPair def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return ColorShift, ColorShiftConfig @argclass( name="colorshift", help="Display a gradient that shifts colors across the terminal.", description="Display a gradient that shifts colors across the terminal.", epilog=( "Example: terminaltexteffects colorshift --gradient-stops 0000ff ffffff 0000ff " "--gradient-steps 12 --gradient-frames 10 --cycles 3 --travel --travel-direction radial --final-gradient-stops " "00c3ff ffff1c --final-gradient-steps 12" ), ) @dataclass class ColorShiftConfig(ArgsDataClass): """Configuration for the ColorShift effect. Attributes: gradient_stops (tuple[Color, ...]): Tuple of colors for the gradient. If only one color is provided, the characters will be displayed in that color. gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. gradient_frames (int): Number of frames to display each gradient step. Increase to slow down the gradient animation. no_loop (bool): Do not loop the gradient. If not set, the gradient generation will loop the final gradient color back to the first gradient color. travel (bool): Display the gradient as a traveling wave. travel_direction (Gradient.Direction): Direction the gradient travels across the canvas. reverse_travel_direction (bool): Reverse the gradient travel direction. cycles (int): Number of times to cycle the gradient. Use 0 for infinite. Valid values are n >= 0. skip_final_gradient (bool): Skip the final gradient. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use for the final gradient. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient across the canvas. """ gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=( Color("e81416"), Color("ffa500"), Color("faeb36"), Color("79c314"), Color("487de7"), Color("4b369d"), Color("70369d"), ), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the gradient.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the gradient. If only one color is provided, the characters will " "be displayed in that color." ) gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Number of gradient steps to use. More steps will create a smoother gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) gradient_frames: int = ArgField( cmd_name="--gradient-frames", type_parser=argvalidators.PositiveInt.type_parser, default=5, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to display each gradient step. Increase to slow down the gradient animation.", ) # type: ignore[assignment] "int : Number of frames to display each gradient step. Increase to slow down the gradient animation." travel: bool = ArgField( cmd_name="--travel", action="store_true", help="Display the gradient as a traveling wave", ) # type: ignore[assignment] "bool : Display the gradient as a traveling wave." travel_direction: Gradient.Direction = ArgField( cmd_name="--travel-direction", default=Gradient.Direction.HORIZONTAL, type_parser=argvalidators.GradientDirection.type_parser, metavar=argvalidators.GradientDirection.METAVAR, help="Direction the gradient travels across the canvas.", ) # type: ignore[assignment] "Gradient.Direction : Direction the gradient travels across the canvas." reverse_travel_direction: bool = ArgField( cmd_name="--reverse-travel-direction", action="store_true", help="Reverse the gradient travel direction.", ) # type: ignore[assignment] "bool : Reverse the gradient travel direction." no_loop: bool = ArgField( cmd_name="--no-loop", action="store_true", help="Do not loop the gradient. If not set, the gradient generation will loop the final gradient " "color back to the first gradient color.", ) # type: ignore[assignment] ( "bool : Do not loop the gradient. If not set, the gradient generation will loop the final gradient color " "back to the first gradient color." ) cycles: int = ArgField( cmd_name="--cycles", type_parser=argvalidators.PositiveInt.type_parser, default=3, metavar=argvalidators.PositiveInt.METAVAR, help="Number of times to cycle the gradient.", ) # type: ignore[assignment] "int : Number of times to cycle the gradient. Use 0 for infinite." skip_final_gradient: bool = ArgField( cmd_name="--skip-final-gradient", action="store_true", help="Skip the final gradient.", ) # type: ignore[assignment] "bool : Skip the final gradient." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=( Color("e81416"), Color("ffa500"), Color("faeb36"), Color("79c314"), Color("487de7"), Color("4b369d"), Color("70369d"), ), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name=["--final-gradient-steps"], type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[ColorShift]: """Get the effect class associated with this configuration.""" return ColorShift class ColorShiftIterator(BaseEffectIterator[ColorShiftConfig]): """Iterator for the ColorShift effect.""" def __init__(self, effect: ColorShift) -> None: """Initialize the iterator with the provided effect. Args: effect (ColorShift): The effect to use for the iterator. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.loop_tracker_map: dict[EffectCharacter, int] = {} self.build() def loop_tracker(self, character: EffectCharacter) -> None: """Track the number of times a character has looped through the gradient.""" self.loop_tracker_map[character] = self.loop_tracker_map.get(character, 0) + 1 if self.config.cycles == 0 or (self.loop_tracker_map[character] < self.config.cycles): character.animation.activate_scene(character.animation.query_scene("gradient")) elif not self.config.skip_final_gradient: character.animation.activate_scene(character.animation.query_scene("final_gradient")) def build(self) -> None: """Build the initial state of the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] gradient = Gradient(*self.config.gradient_stops, steps=self.config.gradient_steps, loop=not self.config.no_loop) for character in self.terminal.get_characters(): self.terminal.set_character_visibility(character, is_visible=True) gradient_scn = character.animation.new_scene(scene_id="gradient") if self.config.travel: if self.config.travel_direction == Gradient.Direction.HORIZONTAL: direction_index = character.input_coord.column / self.terminal.canvas.right elif self.config.travel_direction == Gradient.Direction.VERTICAL: direction_index = character.input_coord.row / self.terminal.canvas.top elif self.config.travel_direction == Gradient.Direction.DIAGONAL: direction_index = (character.input_coord.row + character.input_coord.column) / ( self.terminal.canvas.right + self.terminal.canvas.top ) else: # radial direction_index = geometry.find_normalized_distance_from_center( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, character.input_coord, ) shift_distance = int(len(gradient.spectrum) * direction_index) if self.config.reverse_travel_direction: shift_distance = shift_distance * -1 colors = gradient.spectrum[shift_distance:] + gradient.spectrum[:shift_distance] else: colors = gradient.spectrum for color in colors: gradient_scn.add_frame( character.input_symbol, self.config.gradient_frames, colors=ColorPair(fg=color), ) final_color_scn = character.animation.new_scene(scene_id="final_gradient") for color in Gradient(colors[-1], self.character_final_color_map[character], steps=8): final_color_scn.add_frame( character.input_symbol, self.config.gradient_frames, colors=ColorPair(fg=color), ) character.animation.activate_scene(gradient_scn) self.active_characters.add(character) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, gradient_scn, EventHandler.Action.CALLBACK, EventHandler.Callback(self.loop_tracker), ) def __next__(self) -> str: """Return the next frame in the animation.""" if self.pending_chars or self.active_characters: # perform effect logic self.update() return self.frame raise StopIteration class ColorShift(BaseEffect[ColorShiftConfig]): """Display a gradient that shifts colors across the terminal.""" @property def _config_cls(self) -> type[ColorShiftConfig]: return ColorShiftConfig @property def _iterator_cls(self) -> type[ColorShiftIterator]: return ColorShiftIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_crumble.py000066400000000000000000000275051507200677100303250ustar00rootroot00000000000000"""Characters crumble into dust before being vacuumed up and reformed. Classes: Crumble: Characters crumble into dust before being vacuumed up and reformed. CrumbleConfig: Configuration for the Crumble effect. CrumbleIterator: Iterates over the Crumble effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, Coord, EffectCharacter, EventHandler, Gradient, Scene, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass from terminaltexteffects.utils.graphics import ColorPair def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Crumble, CrumbleConfig @argclass( name="crumble", help="Characters lose color and crumble into dust, vacuumed up, and reformed.", description="crumble | Characters lose color and crumble into dust, vacuumed up, and reformed.", epilog=( "Example: terminaltexteffects crumble --final-gradient-stops 5CE1FF FF8C00 --final-gradient-steps 12 " "--final-gradient-direction diagonal" ), ) @dataclass class CrumbleConfig(ArgsDataClass): """Configuration for the Crumble effect. Attributes: final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("#5CE1FF"), Color("#FF8C00")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If " "only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.DIAGONAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Crumble]: """Get the effect class associated with this configuration.""" return Crumble class CrumbleIterator(BaseEffectIterator[CrumbleConfig]): """Iterator for the Crumble effect.""" def __init__(self, effect: Crumble) -> None: """Initialize the iterator with the provided effect. Args: effect (Crumble): The effect to iterate over. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the initial state of the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] strengthen_flash_gradient = Gradient(self.character_final_color_map[character], Color("ffffff"), steps=6) strengthen_gradient = Gradient(Color("ffffff"), self.character_final_color_map[character], steps=9) weak_color = character.animation.adjust_color_brightness(self.character_final_color_map[character], 0.65) dust_color = character.animation.adjust_color_brightness(self.character_final_color_map[character], 0.55) weaken_gradient = Gradient(weak_color, dust_color, steps=9) self.terminal.set_character_visibility(character, is_visible=True) # set up initial and falling stage initial_scn = character.animation.new_scene() initial_scn.add_frame(character.input_symbol, 1, colors=ColorPair(fg=weak_color)) character.animation.activate_scene(initial_scn) fall_path = character.motion.new_path( speed=0.2, ease=easing.out_bounce, ) fall_path.new_waypoint(Coord(character.input_coord.column, self.terminal.canvas.bottom)) weaken_scn = character.animation.new_scene(scene_id="weaken") weaken_scn.apply_gradient_to_symbols(character.input_symbol, 6, fg_gradient=weaken_gradient) top_path = character.motion.new_path(path_id="top", speed=0.5, ease=easing.out_quint) top_path.new_waypoint( Coord(character.input_coord.column, self.terminal.canvas.top), bezier_control=Coord(self.terminal.canvas.center_column, self.terminal.canvas.center_row), ) # set up reset stage input_path = character.motion.new_path(path_id="input", speed=0.3) input_path.new_waypoint(character.input_coord) strengthen_flash_scn = character.animation.new_scene() strengthen_flash_scn.apply_gradient_to_symbols( character.input_symbol, 4, fg_gradient=strengthen_flash_gradient, ) strengthen_scn = character.animation.new_scene() strengthen_scn.apply_gradient_to_symbols(character.input_symbol, 6, fg_gradient=strengthen_gradient) dust_scn = character.animation.new_scene(sync=Scene.SyncMetric.DISTANCE) for _ in range(5): dust_scn.add_frame(random.choice(["*", ".", ","]), 1, colors=ColorPair(fg=dust_color)) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, weaken_scn, EventHandler.Action.ACTIVATE_PATH, fall_path, ) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, weaken_scn, EventHandler.Action.SET_LAYER, 1, ) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, weaken_scn, EventHandler.Action.ACTIVATE_SCENE, dust_scn, ) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, input_path, EventHandler.Action.ACTIVATE_SCENE, strengthen_flash_scn, ) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, strengthen_flash_scn, EventHandler.Action.ACTIVATE_SCENE, strengthen_scn, ) self.pending_chars.append(character) random.shuffle(self.pending_chars) self.fall_delay = 20 self.max_fall_delay = 20 self.min_fall_delay = 15 self.reset = False self.fall_group_maxsize = 1 self.stage = "falling" self.unvacuumed_chars = list(self.terminal._input_characters) random.shuffle(self.unvacuumed_chars) def __next__(self) -> str: """Return the next frame in the animation.""" if self.stage != "complete": if self.stage == "falling": if self.pending_chars: if self.fall_delay == 0: # Determine the size of the next group of falling characters fall_group_size = random.randint(1, self.fall_group_maxsize) # Add the next group of falling characters to the animating characters list for _ in range(fall_group_size): if self.pending_chars: next_char = self.pending_chars.pop(0) next_char.animation.activate_scene(next_char.animation.query_scene("weaken")) self.active_characters.add(next_char) # Reset the fall delay and adjust the fall group size and delay range self.fall_delay = random.randint(self.min_fall_delay, self.max_fall_delay) if random.randint(1, 10) > 4: # 60% chance to modify the fall delay and group size self.fall_group_maxsize += 1 self.min_fall_delay = max(0, self.min_fall_delay - 1) self.max_fall_delay = max(0, self.max_fall_delay - 1) else: self.fall_delay -= 1 if not self.pending_chars and not self.active_characters: self.stage = "vacuuming" elif self.stage == "vacuuming": if self.unvacuumed_chars: for _ in range(random.randint(3, 10)): if self.unvacuumed_chars: next_char = self.unvacuumed_chars.pop(0) next_char.motion.activate_path(next_char.motion.query_path("top")) self.active_characters.add(next_char) if not self.active_characters: self.stage = "resetting" elif self.stage == "resetting": if not self.reset: for character in self.terminal.get_characters(): character.motion.activate_path(character.motion.query_path("input")) self.active_characters.add(character) self.reset = True if not self.active_characters: self.stage = "complete" self.update() return self.frame raise StopIteration class Crumble(BaseEffect[CrumbleConfig]): """Characters crumble into dust before being vacuumed up and reformed. Attributes: effect_config (CrumbleConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[CrumbleConfig]: return CrumbleConfig @property def _iterator_cls(self) -> type[CrumbleIterator]: return CrumbleIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_decrypt.py000066400000000000000000000272561507200677100303510ustar00rootroot00000000000000"""Movie style text decryption effect. Classes: Decrypt: Movie style text decryption effect. DecryptConfig: Configuration for the Decrypt effect. DecryptIterator: Iterates over the Decrypt effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, ColorPair, EffectCharacter, EventHandler, Gradient, Scene from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Decrypt, DecryptConfig @argclass( name="decrypt", help="Display a movie style decryption effect.", description="decrypt | Movie style decryption effect.", epilog=( "Example: terminaltexteffects decrypt --typing-speed 2 --ciphertext-colors 008000 00cb00 00ff00 " "--final-gradient-stops eda000 --final-gradient-steps 12 --final-gradient-direction vertical" ), ) @dataclass class DecryptConfig(ArgsDataClass): """Configuration for the Decrypt effect. Attributes: typing_speed (int): Number of characters typed per keystroke. ciphertext_colors (tuple[Color, ...]): Colors for the ciphertext. Color will be randomly selected for each character. final_gradient_stops (tuple[Color, ...]): Colors for the character gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Number of gradient steps to use. More steps will create a smoother and longer gradient animation. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ typing_speed: int = ArgField( cmd_name="--typing-speed", type_parser=argvalidators.PositiveInt.type_parser, default=1, metavar=argvalidators.PositiveInt.METAVAR, help="Number of characters typed per keystroke.", ) # type: ignore[assignment] "int : Number of characters typed per keystroke." ciphertext_colors: tuple[Color, ...] = ArgField( cmd_name=["--ciphertext-colors"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("008000"), Color("00cb00"), Color("00ff00")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the ciphertext. Color will be randomly selected for " "each character.", ) # type: ignore[assignment] "tuple[Color, ...] : Colors for the ciphertext. Color will be randomly selected for each character." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("eda000"),), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Colors for the character gradient. If only one color is provided, the characters " "will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Number of gradient steps to use. More steps will create a smoother and " "longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Decrypt]: """Get the effect class associated with this configuration.""" return Decrypt class DecryptIterator(BaseEffectIterator[DecryptConfig]): """Iterator for the Decrypt effect.""" @dataclass class _DecryptChars: keyboard: typing.ClassVar[list[int]] = list(range(33, 127)) blocks: typing.ClassVar[list[int]] = list(range(9608, 9632)) box_drawing: typing.ClassVar[list[int]] = list(range(9472, 9599)) misc: typing.ClassVar[list[int]] = list(range(174, 452)) def __init__(self, effect: Decrypt) -> None: """Initialize the iterator with the provided effect. Args: effect (Decrypt): The effect to use for the iterator. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.typing_pending_chars: list[EffectCharacter] = [] self.decrypting_pending_chars: set[EffectCharacter] = set() self.phase = "typing" self.encrypted_symbols: list[str] = [] self.scenes: dict[str, Scene] = {} self.character_final_color_map: dict[EffectCharacter, Color] = {} self.make_encrypted_symbols() self.build() def make_encrypted_symbols(self) -> None: """Create a list of encrypted symbols.""" for n in DecryptIterator._DecryptChars.keyboard: self.encrypted_symbols.append(chr(n)) for n in DecryptIterator._DecryptChars.blocks: self.encrypted_symbols.append(chr(n)) for n in DecryptIterator._DecryptChars.box_drawing: self.encrypted_symbols.append(chr(n)) for n in DecryptIterator._DecryptChars.misc: self.encrypted_symbols.append(chr(n)) def make_decrypting_animation_scenes(self, character: EffectCharacter) -> None: """Create the animation scenes for decrypting the text.""" fast_decrypt_scene = character.animation.new_scene(scene_id="fast_decrypt") color = random.choice(self.config.ciphertext_colors) for _ in range(80): symbol = random.choice(self.encrypted_symbols) fast_decrypt_scene.add_frame(symbol, 3, colors=ColorPair(fg=color)) duration = 3 slow_decrypt_scene = character.animation.new_scene(scene_id="slow_decrypt") for _ in range(random.randint(1, 15)): # 1-15 longer duration units symbol = random.choice(self.encrypted_symbols) # 30% chance of extra long duration # wide duration range reduces 'waves' in the animation # shorter duration creates flipping effect duration = random.randrange(50, 125) if random.randint(0, 100) <= 30 else random.randrange(5, 10) slow_decrypt_scene.add_frame(symbol, duration, colors=ColorPair(fg=color)) discovered_scene = character.animation.new_scene(scene_id="discovered") discovered_gradient = Gradient(Color("ffffff"), self.character_final_color_map[character], steps=10) discovered_scene.apply_gradient_to_symbols(character.input_symbol, 8, fg_gradient=discovered_gradient) def prepare_data_for_type_effect(self) -> None: """Prepare the data for the typing effect.""" for character in self.terminal.get_characters(): typing_scene = character.animation.new_scene(scene_id="typing") for block_char in ["▉", "▓", "▒", "░"]: typing_scene.add_frame( block_char, 2, colors=ColorPair(fg=random.choice(self.config.ciphertext_colors)), ) typing_scene.add_frame( random.choice(self.encrypted_symbols), 2, colors=ColorPair(fg=random.choice(self.config.ciphertext_colors)), ) self.typing_pending_chars.append(character) def prepare_data_for_decrypt_effect(self) -> None: """Prepare the data for the decrypting effect.""" for character in self.terminal.get_characters(): self.make_decrypting_animation_scenes(character) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, character.animation.query_scene("fast_decrypt"), EventHandler.Action.ACTIVATE_SCENE, character.animation.query_scene("slow_decrypt"), ) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, character.animation.query_scene("slow_decrypt"), EventHandler.Action.ACTIVATE_SCENE, character.animation.query_scene("discovered"), ) character.animation.activate_scene(character.animation.query_scene("fast_decrypt")) self.decrypting_pending_chars.add(character) def build(self) -> None: """Build the initial state of the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] self.prepare_data_for_type_effect() self.prepare_data_for_decrypt_effect() def __next__(self) -> str: """Return the next frame in the animation.""" if self.phase == "typing": if self.typing_pending_chars or self.active_characters: if self.typing_pending_chars and random.randint(0, 100) <= 75: for _ in range(self.config.typing_speed): if self.typing_pending_chars: next_character = self.typing_pending_chars.pop(0) self.terminal.set_character_visibility(next_character, is_visible=True) next_character.animation.activate_scene(next_character.animation.query_scene("typing")) self.active_characters.add(next_character) self.update() return self.frame self.active_characters = self.decrypting_pending_chars for char in self.active_characters: char.animation.activate_scene(char.animation.query_scene("fast_decrypt")) self.phase = "decrypting" if self.phase == "decrypting": if self.active_characters: self.update() return self.frame raise StopIteration raise StopIteration class Decrypt(BaseEffect[DecryptConfig]): """Movie style text decryption effect. Attributes: effect_config (DecryptConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[DecryptConfig]: return DecryptConfig @property def _iterator_cls(self) -> type[DecryptIterator]: return DecryptIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_errorcorrect.py000066400000000000000000000341111507200677100313760ustar00rootroot00000000000000"""Swaps characters from an incorrect initial position to the correct position. Classes: ErrorCorrect: Swaps characters from an incorrect initial position to the correct position. ErrorCorrectConfig: Configuration for the ErrorCorrect effect. ErrorCorrectIterator: Iterates over the effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, EffectCharacter, EventHandler, Gradient, Scene from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass from terminaltexteffects.utils.graphics import ColorPair def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return ErrorCorrect, ErrorCorrectConfig @argclass( name="errorcorrect", help="Some characters start in the wrong position and are corrected in sequence.", description="errorcorrect | Some characters start in the wrong position and are corrected in sequence.", epilog=( f"{argvalidators.EASING_EPILOG}" "Example: terminaltexteffects errorcorrect --error-pairs 0.1 --swap-delay 10 --error-color e74c3c " "--correct-color 45bf55 --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 " "--movement-speed 0.5" ), ) @dataclass class ErrorCorrectConfig(ArgsDataClass): """Configuration for the ErrorCorrect effect. Attributes: error_pairs (float): Percent of characters that are in the wrong position. This is a float between 0 and 1.0. 0.2 means 20 percent of the characters will be in the wrong position. Valid values are 0 < n <= 1.0. swap_delay (int): Number of frames between swaps. Valid values are n >= 0. error_color (Color): Color for the characters that are in the wrong position. correct_color (Color): Color for the characters once corrected, this is a gradient from error-color and fades to final-color. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. movement_speed (float): Speed of the characters while moving to the correct position. Valid values are n > 0. """ error_pairs: float = ArgField( cmd_name="--error-pairs", type_parser=argvalidators.PositiveFloat.type_parser, default=0.1, metavar="(int > 0)", help="Percent of characters that are in the wrong position. This is a float between 0 and 1.0. 0.2 means " "20 percent of the characters will be in the wrong position.", ) # type: ignore[assignment] ( "float : Percent of characters that are in the wrong position. This is a float between 0 and 1.0. 0.2 " "means 20 percent of the characters will be in the wrong position." ) swap_delay: int = ArgField( cmd_name="--swap-delay", type_parser=argvalidators.PositiveInt.type_parser, default=10, metavar="(int > 0)", help="Number of frames between swaps.", ) # type: ignore[assignment] "int : Number of frames between swaps." error_color: Color = ArgField( cmd_name=["--error-color"], type_parser=argvalidators.ColorArg.type_parser, default=Color("e74c3c"), metavar="(XTerm [0-255] OR RGB Hex [000000-ffffff])", help="Color for the characters that are in the wrong position.", ) # type: ignore[assignment] "Color : Color for the characters that are in the wrong position." correct_color: Color = ArgField( cmd_name=["--correct-color"], type_parser=argvalidators.ColorArg.type_parser, default=Color("45bf55"), metavar="(XTerm [0-255] OR RGB Hex [000000-ffffff])", help="Color for the characters once corrected, this is a gradient from error-color and fades to final-color.", ) # type: ignore[assignment] "Color : Color for the characters once corrected, this is a gradient from error-color and fades to final-color." movement_speed: float = ArgField( cmd_name="--movement-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=0.5, metavar="(float > 0)", help="Speed of the characters while moving to the correct position. ", ) # type: ignore[assignment] "float : Speed of the characters while moving to the correct position. " final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("8A008A"), Color("00D1FF"), Color("FFFFFF")), metavar="(XTerm [0-255] OR RGB Hex [000000-ffffff])", help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar="(int > 0)", help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[ErrorCorrect]: """Get the effect class associated with this configuration.""" return ErrorCorrect class ErrorCorrectIterator(BaseEffectIterator[ErrorCorrectConfig]): """Iterates over the ErrorCorrect effect.""" def __init__(self, effect: ErrorCorrect) -> None: """Initialize the iterator. Args: effect (ErrorCorrect): The effect to use for the iterator. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.swapped: list[tuple[EffectCharacter, EffectCharacter]] = [] self.swap_delay = 0 self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the initial state of the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] for character in self.terminal.get_characters(): spawn_scene = character.animation.new_scene() spawn_scene.add_frame( character.input_symbol, 1, colors=ColorPair(fg=self.character_final_color_map[character]), ) character.animation.activate_scene(spawn_scene) self.terminal.set_character_visibility(character, is_visible=True) all_characters: list[EffectCharacter] = list(self.terminal._input_characters) correcting_gradient = Gradient(self.config.error_color, self.config.correct_color, steps=10) block_symbol = "▓" block_wipe_start = ("▁", "▂", "▃", "▄", "▅", "▆", "▇", "█") block_wipe_end = ("▇", "▆", "▅", "▄", "▃", "▂", "▁") for _ in range(int(self.config.error_pairs * len(self.terminal.get_characters()))): if len(all_characters) < 2: break char1 = all_characters.pop(random.randrange(len(all_characters))) char2 = all_characters.pop(random.randrange(len(all_characters))) char1.motion.set_coordinate(char2.input_coord) char1_input_coord_path = char1.motion.new_path(path_id="input_coord", speed=self.config.movement_speed) char1_input_coord_path.new_waypoint(char1.input_coord) char2.motion.set_coordinate(char1.input_coord) char2_input_coord_path = char2.motion.new_path(path_id="input_coord", speed=self.config.movement_speed) char2_input_coord_path.new_waypoint(char2.input_coord) self.swapped.append((char1, char2)) for character in (char1, char2): first_block_wipe = character.animation.new_scene() last_block_wipe = character.animation.new_scene() for block in block_wipe_start: first_block_wipe.add_frame(block, 3, colors=ColorPair(fg=self.config.error_color)) for block in block_wipe_end: last_block_wipe.add_frame(block, 3, colors=ColorPair(fg=self.config.correct_color)) initial_scene = character.animation.new_scene() initial_scene.add_frame(character.input_symbol, 1, colors=ColorPair(fg=self.config.error_color)) character.animation.activate_scene(initial_scene) error_scene = character.animation.new_scene(scene_id="error") for _ in range(10): error_scene.add_frame(block_symbol, 3, colors=ColorPair(fg=self.config.error_color)) error_scene.add_frame(character.input_symbol, 3, colors=ColorPair(fg="ffffff")) correcting_scene = character.animation.new_scene(sync=Scene.SyncMetric.DISTANCE) correcting_scene.apply_gradient_to_symbols("█", 3, fg_gradient=correcting_gradient) final_scene = character.animation.new_scene() char_final_gradient = Gradient( self.config.correct_color, self.character_final_color_map[character], steps=10, ) final_scene.apply_gradient_to_symbols(character.input_symbol, 3, fg_gradient=char_final_gradient) input_coord_path = character.motion.query_path("input_coord") character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, error_scene, EventHandler.Action.ACTIVATE_SCENE, first_block_wipe, ) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, first_block_wipe, EventHandler.Action.ACTIVATE_SCENE, correcting_scene, ) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, first_block_wipe, EventHandler.Action.ACTIVATE_PATH, input_coord_path, ) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, input_coord_path, EventHandler.Action.SET_LAYER, 1, ) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, input_coord_path, EventHandler.Action.SET_LAYER, 0, ) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, input_coord_path, EventHandler.Action.ACTIVATE_SCENE, last_block_wipe, ) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, last_block_wipe, EventHandler.Action.ACTIVATE_SCENE, final_scene, ) def __next__(self) -> str: """Return the next frame in the animation.""" if self.swapped and not self.swap_delay: next_pair = self.swapped.pop(0) for char in next_pair: char.animation.activate_scene(char.animation.query_scene("error")) self.active_characters.add(char) self.swap_delay = self.config.swap_delay elif self.swap_delay: self.swap_delay -= 1 if self.active_characters: self.update() return self.frame raise StopIteration class ErrorCorrect(BaseEffect[ErrorCorrectConfig]): """Swaps characters from an incorrect initial position to the correct position. Attributes: effect_config (ErrorCorrectConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[ErrorCorrectConfig]: return ErrorCorrectConfig @property def _iterator_cls(self) -> type[ErrorCorrectIterator]: return ErrorCorrectIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_expand.py000066400000000000000000000202711507200677100301440ustar00rootroot00000000000000"""Characters expand from the center. Classes: Expand: Characters expand from the center. ExpandConfig: Configuration for the Expand effect. ExpandIterator: Iterates over the effect. """ from __future__ import annotations import typing from dataclasses import dataclass from terminaltexteffects import Color, EffectCharacter, EventHandler, Gradient, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Expand, ExpandConfig @argclass( name="expand", help="Expands the text from a single point.", description="expand | Expands the text from a single point.", epilog=( f"{argvalidators.EASING_EPILOG}" "Example: terminaltexteffects expand --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 " "--final-gradient-frames 5 --movement-speed 0.35 --expand-easing IN_OUT_QUART" ), ) @dataclass class ExpandConfig(ArgsDataClass): """Configuration for the Expand effect. Attributes: movement_speed (float): Movement speed of the characters. expand_easing (easing.EasingFunction): Easing function to use for character movement. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_frames (int): Number of frames to display each gradient step. Increase to slow down the gradient animation. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ expand_easing: easing.EasingFunction = ArgField( cmd_name="--expand-easing", default=easing.in_out_quart, type_parser=argvalidators.Ease.type_parser, help="Easing function to use for character movement.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for character movement." movement_speed: float = ArgField( cmd_name="--movement-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=0.35, metavar=argvalidators.PositiveFloat.METAVAR, help="Movement speed of the characters. ", ) # type: ignore[assignment] "float : Movement speed of the characters. " final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("8A008A"), Color("00D1FF"), Color("FFFFFF")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If " "only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_frames: int = ArgField( cmd_name="--final-gradient-frames", type_parser=argvalidators.PositiveInt.type_parser, default=5, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to display each gradient step. Increase to slow down the gradient animation.", ) # type: ignore[assignment] "int : Number of frames to display each gradient step. Increase to slow down the gradient animation." final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Expand]: """Get the effect class associated with this configuration.""" return Expand class ExpandIterator(BaseEffectIterator[ExpandConfig]): """Iterates over the Expand effect.""" def __init__( self, effect: Expand, ) -> None: """Initialize the Expand effect iterator. Args: effect (Expand): The effect to use for the iterator. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the Expand effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] for character in self.terminal.get_characters(): character.motion.set_coordinate(self.terminal.canvas.center) input_coord_path = character.motion.new_path( speed=self.config.movement_speed, ease=self.config.expand_easing, ) input_coord_path.new_waypoint(character.input_coord) self.terminal.set_character_visibility(character, is_visible=True) self.active_characters.add(character) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, input_coord_path, EventHandler.Action.SET_LAYER, 1, ) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, input_coord_path, EventHandler.Action.SET_LAYER, 0, ) character.motion.activate_path(input_coord_path) gradient_scn = character.animation.new_scene() gradient = Gradient(final_gradient.spectrum[0], self.character_final_color_map[character], steps=10) gradient_scn.apply_gradient_to_symbols( character.input_symbol, self.config.final_gradient_frames, fg_gradient=gradient, ) character.animation.activate_scene(gradient_scn) def __next__(self) -> str: """Return the next frame in the animation.""" if self.active_characters: self.update() return self.frame raise StopIteration class Expand(BaseEffect[ExpandConfig]): """Characters expand from the center. Attributes: effect_config (ExpandConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[ExpandConfig]: return ExpandConfig @property def _iterator_cls(self) -> type[ExpandIterator]: return ExpandIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_fireworks.py000066400000000000000000000354141507200677100307050ustar00rootroot00000000000000"""Launches characters up the screen where they explode like fireworks and fall into place. Classes: Fireworks: Characters explode like fireworks and fall into place. FireworksConfig: Configuration for the Fireworks effect. FireworksIterator: Iterates over the effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import ( Color, ColorPair, Coord, EffectCharacter, EventHandler, Gradient, Scene, easing, geometry, ) from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Fireworks, FireworksConfig @argclass( name="fireworks", help="Characters launch and explode like fireworks and fall into place.", description="fireworks | Characters explode like fireworks and fall into place.", epilog=( "Example: terminaltexteffects fireworks --firework-colors 88F7E2 44D492 F5EB67 FFA15C FA233E " "--firework-symbol o --firework-volume 0.02 --final-gradient-stops 8A008A 00D1FF FFFFFF " "--final-gradient-steps 12 --launch-delay 60 --explode-distance 0.1 --explode-anywhere" ), ) @dataclass class FireworksConfig(ArgsDataClass): """Configuration for the Fireworks effect. Attributes: explode_anywhere (bool): If set, fireworks explode anywhere in the canvas. Otherwise, fireworks explode above highest settled row of text. firework_colors (tuple[Color, ...]): Tuple of colors from which firework colors will be randomly selected. firework_symbol (str): Symbol to use for the firework shell. firework_volume (float): Percent of total characters in each firework shell. Valid values are 0 < n <= 1. launch_delay (int): Number of frames to wait between launching each firework shell. +/- 0-50 percent randomness is applied to this value. Valid values are n >= 0. explode_distance (float): Maximum distance from the firework shell origin to the explode waypoint as a percentage of the total canvas width. Valid values are 0 < n <= 1. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ explode_anywhere: bool = ArgField( cmd_name="--explode-anywhere", action="store_true", default=False, help="If set, fireworks explode anywhere in the canvas. Otherwise, fireworks explode above highest settled " "row of text.", ) # type: ignore[assignment] ( "bool : If set, fireworks explode anywhere in the canvas. Otherwise, fireworks explode above highest " "settled row of text." ) firework_colors: tuple[Color, ...] = ArgField( cmd_name="--firework-colors", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("88F7E2"), Color("44D492"), Color("F5EB67"), Color("FFA15C"), Color("FA233E")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated list of colors from which firework colors will be randomly selected.", ) # type: ignore[assignment] "tuple[Color, ...] : Tuple of colors from which firework colors will be randomly selected." firework_symbol: str = ArgField( cmd_name="--firework-symbol", type_parser=argvalidators.Symbol.type_parser, default="o", metavar=argvalidators.Symbol.METAVAR, help="Symbol to use for the firework shell.", ) # type: ignore[assignment] "str : Symbol to use for the firework shell." firework_volume: float = ArgField( cmd_name="--firework-volume", type_parser=argvalidators.NonNegativeRatio.type_parser, default=0.02, metavar=argvalidators.NonNegativeRatio.METAVAR, help="Percent of total characters in each firework shell.", ) # type: ignore[assignment] "float : Percent of total characters in each firework shell." launch_delay: int = ArgField( cmd_name="--launch-delay", type_parser=argvalidators.NonNegativeInt.type_parser, default=60, metavar=argvalidators.NonNegativeInt.METAVAR, help="Number of frames to wait between launching each firework shell. +/- 0-50 percent randomness is " "applied to this value.", ) # type: ignore[assignment] ( "int : Number of frames to wait between launching each firework shell. +/- 0-50 percent randomness is " "applied to this value." ) explode_distance: float = ArgField( cmd_name="--explode-distance", default=0.1, type_parser=argvalidators.NonNegativeRatio.type_parser, metavar=argvalidators.NonNegativeRatio.METAVAR, help="Maximum distance from the firework shell origin to the explode waypoint as a percentage of the " "total canvas width.", ) # type: ignore[assignment] ( "float : Maximum distance from the firework shell origin to the explode waypoint as a percentage of " "the total canvas width." ) final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name="--final-gradient-stops", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("8A008A"), Color("00D1FF"), Color("FFFFFF")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.HORIZONTAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Fireworks]: """Get the effect class associated with this configuration.""" return Fireworks class FireworksIterator(BaseEffectIterator[FireworksConfig]): """Iterator for the Fireworks effect.""" def __init__(self, effect: Fireworks) -> None: """Initialize the Fireworks effect iterator. Args: effect (Fireworks): The Fireworks effect to iterate over. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.shells: list[list[EffectCharacter]] = [] self.firework_volume = max(1, round(self.config.firework_volume * len(self.terminal._input_characters))) self.explode_distance = max(1, round(self.terminal.canvas.right * self.config.explode_distance)) self.character_final_color_map: dict[EffectCharacter, Color] = {} self.launch_delay: int = 0 self.build() def prepare_waypoints(self) -> None: """Prepare the waypoints for the characters.""" firework_shell: list[EffectCharacter] = [] for character in self.terminal.get_characters(): if len(firework_shell) == self.firework_volume or not firework_shell: origin_x = random.randrange(0, self.terminal.canvas.right) self.shells.append(firework_shell) firework_shell = [] min_row = character.input_coord.row if not self.config.explode_anywhere else self.terminal.canvas.bottom origin_y = random.randrange(min_row, self.terminal.canvas.top + 1) origin_coord = Coord(origin_x, origin_y) explode_waypoint_coords = geometry.find_coords_in_circle(origin_coord, self.explode_distance) character.motion.set_coordinate(Coord(origin_x, self.terminal.canvas.bottom)) # type: ignore[attr-defined] apex_path = character.motion.new_path(path_id="apex_pth", speed=0.2, ease=easing.out_expo, layer=2) apex_wpt = apex_path.new_waypoint(origin_coord) # type: ignore[attr-defined] explode_path = character.motion.new_path(speed=random.uniform(0.09, 0.2), ease=easing.out_circ, layer=2) explode_wpt = explode_path.new_waypoint(random.choice(explode_waypoint_coords)) # type: ignore[attr-defined] bloom_control_point = geometry.find_coord_at_distance( apex_wpt.coord, explode_wpt.coord, self.explode_distance // 2, ) bloom_wpt = explode_path.new_waypoint( Coord(bloom_control_point.column, max(1, bloom_control_point.row - 7)), bezier_control=bloom_control_point, ) input_path = character.motion.new_path(path_id="input_pth", speed=0.3, ease=easing.in_out_quart, layer=2) input_control_point = Coord(bloom_wpt.coord.column, 1) input_path.new_waypoint(character.input_coord, bezier_control=input_control_point) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, apex_path, EventHandler.Action.ACTIVATE_PATH, explode_path, ) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, explode_path, EventHandler.Action.ACTIVATE_PATH, input_path, ) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, input_path, EventHandler.Action.SET_LAYER, 0, ) character.motion.activate_path(apex_path) firework_shell.append(character) if firework_shell: self.shells.append(firework_shell) def prepare_scenes(self) -> None: """Prepare the scenes for the characters.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] for firework_shell in self.shells: shell_color = random.choice(self.config.firework_colors) shell_gradient = Gradient(shell_color, Color("FFFFFF"), shell_color, steps=5) for character in firework_shell: # launch scene launch_scn = character.animation.new_scene() launch_scn.add_frame(self.config.firework_symbol, 2, colors=ColorPair(fg=shell_color)) launch_scn.add_frame(self.config.firework_symbol, 1, colors=ColorPair(fg="FFFFFF")) launch_scn.is_looping = True # bloom scene bloom_scn = character.animation.new_scene(sync=Scene.SyncMetric.STEP) for color in shell_gradient: bloom_scn.add_frame(character.input_symbol, 3, colors=ColorPair(fg=color)) # fall scene fall_scn = character.animation.new_scene() fall_gradient = Gradient(shell_color, self.character_final_color_map[character], steps=15) fall_scn.apply_gradient_to_symbols(character.input_symbol, 15, fg_gradient=fall_gradient) character.animation.activate_scene(launch_scn) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, character.motion.query_path("apex_pth"), EventHandler.Action.ACTIVATE_SCENE, bloom_scn, ) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, character.motion.query_path("input_pth"), EventHandler.Action.ACTIVATE_SCENE, fall_scn, ) def build(self) -> None: """Build the Fireworks effect.""" self.prepare_waypoints() self.prepare_scenes() def __next__(self) -> str: """Return the next frame in the animation.""" if self.shells or self.active_characters: if self.shells and self.launch_delay <= 0: next_group = self.shells.pop() for character in next_group: self.terminal.set_character_visibility(character, is_visible=True) self.active_characters.add(character) self.launch_delay = int(self.config.launch_delay * random.uniform(0.5, 1.5)) self.launch_delay -= 1 self.update() return self.frame raise StopIteration class Fireworks(BaseEffect[FireworksConfig]): """Launches characters up the screen where they explode like fireworks and fall into place. Attributes: effect_config (FireworksConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[FireworksConfig]: return FireworksConfig @property def _iterator_cls(self) -> type[FireworksIterator]: return FireworksIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_highlight.py000066400000000000000000000255511507200677100306420ustar00rootroot00000000000000"""Runs a specular highlight across the text. Classes: Highlight: Runs a specular highlight across the text. HighlightConfig: Configuration for the Highlight effect. HighlightIterator: Effect iterator for the Highlight effect. """ from __future__ import annotations import typing from dataclasses import dataclass from terminaltexteffects import Animation, Color, ColorPair, EffectCharacter, Gradient, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Highlight, HighlightConfig @argclass( name="highlight", help="Run a specular highlight across the text.", description="highlight | Run a specular highlight across the text.", epilog=( "Example: terminaltexteffects highlight --highlight-brightness 1.5 --highlight-direction " "diagonal_bottom_left_to_top_right --highlight-width 8 --final-gradient-stops 8A008A 00D1FF FFFFFF " "--final-gradient-steps 12 --final-gradient-direction vertical" ), ) @dataclass class HighlightConfig(ArgsDataClass): """Configuration for the Highlight effect. Attributes: highlight_brightness (float): Brightness of the highlight color. Values less than 1 will darken the highlight color, while values greater than 1 will brighten the highlight color. highlight_direction (typing.Literal['column_left_to_right','row_top_to_bottom','row_bottom_to_top', diagonal_top_left_to_bottom_right', 'diagonal_bottom_left_to_top_right', 'diagonal_top_right_to_bottom_left', 'diagonal_bottom_right_to_top_left',]): Direction the highlight will travel. highlight_width (int): Width of the highlight. n >= 1 final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Int or Tuple of ints for the number of gradient steps to use. More steps will create a smoother and longer gradient animation. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ # noqa: E501 # long type hint for highlight_direction required for mkdocs to associate the name: description pair highlight_brightness: float = ArgField( cmd_name="--highlight-brightness", type_parser=argvalidators.PositiveFloat.type_parser, default=1.75, metavar=argvalidators.PositiveFloat.METAVAR, help="Brightness of the highlight color. Values less than 1 will darken the highlight color, while values " "greater than 1 will brighten the highlight color.", ) # type: ignore[assignment] ( "float : Brightness of the highlight color. Values less than 1 will darken the highlight color, while " "values greater than 1 will brighten the highlight color." ) highlight_direction: typing.Literal[ "column_left_to_right", "row_top_to_bottom", "row_bottom_to_top", "diagonal_top_left_to_bottom_right", "diagonal_bottom_left_to_top_right", "diagonal_top_right_to_bottom_left", "diagonal_bottom_right_to_top_left", "outside_to_center", "center_to_outside", ] = ArgField( cmd_name="--highlight-direction", default="diagonal_bottom_left_to_top_right", choices=[ "column_left_to_right", "column_right_to_left", "row_top_to_bottom", "row_bottom_to_top", "diagonal_top_left_to_bottom_right", "diagonal_bottom_left_to_top_right", "diagonal_top_right_to_bottom_left", "diagonal_bottom_right_to_top_left", "outside_to_center", "center_to_outside", ], help="Direction the highlight will travel.", ) # type: ignore[assignment] ( "typing.Literal['column_left_to_right','row_top_to_bottom','row_bottom_to_top'," "'diagonal_top_left_to_bottom_right','diagonal_bottom_left_to_top_right'," "'diagonal_top_right_to_bottom_left','diagonal_bottom_right_to_top_left',] : Direction the " "highlight will travel." ) highlight_width: int = ArgField( cmd_name="--highlight-width", type_parser=argvalidators.PositiveInt.type_parser, default=8, metavar=argvalidators.PositiveInt.METAVAR, help="Width of the highlight. n >= 1", ) # type: ignore[assignment] "int : Width of the highlight. n >= 1" final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("8A008A"), Color("00D1FF"), Color("FFFFFF")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If " "only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Highlight]: """Get the effect class associated with this configuration.""" return Highlight class HighlightIterator(BaseEffectIterator[HighlightConfig]): """Effect iterator for the Highlight effect.""" def __init__(self, effect: Highlight) -> None: """Initialize the Highlight effect iterator. Args: effect (Highlight): The Highlight effect to iterate over. """ super().__init__(effect) self.character_final_color_map: dict[EffectCharacter, Color] = {} self.pending_characters: list[list[EffectCharacter]] = [] self.easer = easing.eased_step_function(easing.in_out_circ, 0.01) self.groups_activated = 0 self.sort_map = { "column_left_to_right": self.terminal.CharacterGroup.COLUMN_LEFT_TO_RIGHT, "column_right_to_left": self.terminal.CharacterGroup.COLUMN_RIGHT_TO_LEFT, "row_top_to_bottom": self.terminal.CharacterGroup.ROW_TOP_TO_BOTTOM, "row_bottom_to_top": self.terminal.CharacterGroup.ROW_BOTTOM_TO_TOP, "diagonal_top_left_to_bottom_right": self.terminal.CharacterGroup.DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT, "diagonal_bottom_left_to_top_right": self.terminal.CharacterGroup.DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT, "diagonal_top_right_to_bottom_left": self.terminal.CharacterGroup.DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT, "diagonal_bottom_right_to_top_left": self.terminal.CharacterGroup.DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT, "center_to_outside": self.terminal.CharacterGroup.CENTER_TO_OUTSIDE_DIAMONDS, "outside_to_center": self.terminal.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS, } self.pending_characters = self.terminal.get_characters_grouped(self.sort_map[self.config.highlight_direction]) self.total_groups = len(self.pending_characters) self.build() def build(self) -> None: """Build the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): base_color = final_gradient_mapping[character.input_coord] self.character_final_color_map[character] = base_color highlight_color = Animation.adjust_color_brightness(base_color, self.config.highlight_brightness) highlight_gradient = Gradient( base_color, highlight_color, highlight_color, base_color, steps=(3, self.config.highlight_width, 3), ) character.animation.set_appearance(character.input_symbol, ColorPair(fg=base_color)) specular_highlight_scn = character.animation.new_scene(scene_id="highlight") for color in highlight_gradient: specular_highlight_scn.add_frame(character.input_symbol, 2, colors=ColorPair(fg=color)) self.terminal.set_character_visibility(character, is_visible=True) self.active_characters.add(character) def __next__(self) -> str: """Return the next frame in the animation.""" if self.active_characters or self.pending_characters: _, eased_percentage = self.easer() while (self.groups_activated / self.total_groups) < eased_percentage: if self.pending_characters: next_group = self.pending_characters.pop(0) for character in next_group: scn = character.animation.query_scene("highlight") character.animation.activate_scene(scn) self.active_characters.add(character) self.groups_activated += 1 self.update() return self.frame raise StopIteration class Highlight(BaseEffect[HighlightConfig]): """Run a specular highlight across the text.""" @property def _config_cls(self) -> type[HighlightConfig]: return HighlightConfig @property def _iterator_cls(self) -> type[HighlightIterator]: return HighlightIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_laseretch.py000066400000000000000000000527171507200677100306510ustar00rootroot00000000000000"""A laser etches characters onto the terminal. Classes: LaserEtch: A laser etches characters onto the terminal. LaserEtchConfig: Configuration for the LaserEtch effect. LaserEtchIterator: Iterator for the LaserEtch effect. """ from __future__ import annotations import random import typing from collections import deque from dataclasses import dataclass import terminaltexteffects as tte from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Return the effect class and the effect configuration dataclass. Returns: tuple[type[typing.Any], type[ArgsDataClass]]: The effect class and the effect configuration dataclass. """ return LaserEtch, LaserEtchConfig @argclass( name="laseretch", help="A laser etches characters onto the terminal.", description="A laser etches characters onto the terminal.", epilog=( "Example: terminaltexteffects laseretch --etch-speed 2 --etch-delay 5 --etch-direction " "row_top_to_bottom --cool-gradient-stops ffe680 ff7b00 --laser-gradient-stops ffffff 376cff " "--spark-gradient-stops ffffff ffe680 ff7b00 1a0900 --spark-cooling-frames 10 --final-gradient-stops " "8A008A 00D1FF ffffff --final-gradient-steps 8 --final-gradient-frames 5 " "--final-gradient-direction vertical" ), ) @dataclass class LaserEtchConfig(ArgsDataClass): """LaserEtch effect configuration dataclass. Attributes: etch_direction (typing.Literal['column_left_to_right','row_top_to_bottom','row_bottom_to_top',diagonal_top_left_to_bottom_right','diagonal_bottom_left_to_top_right','diagonal_top_right_to_bottom_left','diagonal_bottom_right_to_top_left']): Pattern used to etch the text. etch_speed (int): Along with etch_delay, determines the speed at which the characters are etched onto the terminal. This value specifies the number of characters to etch simultaneously. etch_delay (int): Along with etch_speed, determines the speed at which the characters are etched onto the terminal. This values specifies the number of frames to wait before etching the next group of characters. cool_gradient_stops (tuple[tte.Color, ...]): Space separated, unquoted, list of colors for the gradient used to cool the characters after etching. If only one color is provided, the characters will be displayed in that color. laser_gradient_stops (tuple[tte.Color, ...]): Space separated, unquoted, list of colors for the laser gradient. If only one color is provided, the characters will be displayed in that color. spark_gradient_stops (tuple[tte.Color, ...]): Space separated, unquoted, list of colors for the spark cooling gradient. If only one color is provided, the characters will be displayed in that color. spark_cooling_frames (int): Number of frames to display each spark cooling gradient step. Increase to slow down the rate of cooling. final_gradient_stops (tuple[tte.Color, ...]): Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. final_gradient_frames (int): Number of frames to display each gradient step. Increase to slow down the gradient animation. final_gradient_direction (tte.Gradient.Direction): Direction of the final gradient. """ # noqa: E501 etch_direction: typing.Literal[ "column_left_to_right", "row_top_to_bottom", "row_bottom_to_top", "diagonal_top_left_to_bottom_right", "diagonal_bottom_left_to_top_right", "diagonal_top_right_to_bottom_left", "diagonal_bottom_right_to_top_left", "outside_to_center", "center_to_outside", ] = ArgField( cmd_name="--etch-direction", default="row_top_to_bottom", choices=[ "column_left_to_right", "column_right_to_left", "row_top_to_bottom", "row_bottom_to_top", "diagonal_top_left_to_bottom_right", "diagonal_bottom_left_to_top_right", "diagonal_top_right_to_bottom_left", "diagonal_bottom_right_to_top_left", "outside_to_center", "center_to_outside", ], help="Pattern used to etch the text.", ) # type: ignore[assignment] "typing.Literal['column_left_to_right','row_top_to_bottom','row_bottom_to_top','diagonal_top_left_to_bottom_right','diagonal_bottom_left_to_top_right','diagonal_top_right_to_bottom_left','diagonal_bottom_right_to_top_left',]: Pattern used to etch the text." # noqa: E501 etch_speed: int = ArgField( cmd_name="--etch-speed", type_parser=argvalidators.PositiveInt.type_parser, default=1, metavar=argvalidators.PositiveInt.METAVAR, help="Along with etch_delay, determines the speed at which the characters are etched onto the terminal. " "This value specifies the number of characters to etch simultaneously.", ) # type: ignore[assignment] ( "int: Along with etch_delay, determines the speed at which the characters are etched onto the terminal. " "This value specifies the number of characters to etch simultaneously." ) etch_delay: int = ArgField( cmd_name="--etch-delay", type_parser=argvalidators.NonNegativeInt.type_parser, default=3, metavar=argvalidators.NonNegativeInt.METAVAR, help="Along with etch_speed, determines the speed at which the characters are etched onto the terminal. " "This values specifies the number of frames to wait before etching the next set of characters.", ) # type: ignore[assignment] ( "int: Along with etch_speed, determines the speed at which the characters are etched onto the terminal. " "This values specifies the number of frames to wait before etching the next set of characters." ) cool_gradient_stops: tuple[tte.Color, ...] = ArgField( cmd_name=["--cool-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(tte.Color("ffe680"), tte.Color("ff7b00")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the gradient used to cool the characters after etching. " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] "tuple[Color, ...]: Space separated, unquoted, list of colors for the cooling gradient " "If only one color is provided, the characters will be displayed in that color." laser_gradient_stops: tuple[tte.Color, ...] = ArgField( cmd_name=["--laser-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(tte.Color("ffffff"), tte.Color("376cff")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the laser gradient. " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] "tuple[Color, ...]: Space separated, unquoted, list of colors for the laser gradient. " "If only one color is provided, the characters will be displayed in that color." spark_gradient_stops: tuple[tte.Color, ...] = ArgField( cmd_name=["--spark-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(tte.Color("ffffff"), tte.Color("ffe680"), tte.Color("ff7b00"), tte.Color("1a0900")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the spark cooling gradient. " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] "tuple[Color, ...]: Space separated, unquoted, list of colors for the spark cooling gradient. " "If only one color is provided, the characters will be displayed in that color." spark_cooling_frames: int = ArgField( cmd_name="--spark-cooling-frames", type_parser=argvalidators.PositiveInt.type_parser, default=10, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to display each spark cooling gradient step. Increase to slow down the rate of cooling.", ) # type: ignore[assignment] "int: Number of frames to display each spark cooling gradient step. Increase to slow down the rate of cooling." final_gradient_stops: tuple[tte.Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(tte.Color("8A008A"), tte.Color("00D1FF"), tte.Color("ffffff")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] "tuple[Color, ...]: Space separated, unquoted, list of colors for the character gradient " "(applied across the canvas). If only one color is provided, the characters will be displayed in that color." final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=8, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] "tuple[int, ...] | int: Space separated, unquoted, list of the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." final_gradient_frames: int = ArgField( cmd_name="--final-gradient-frames", type_parser=argvalidators.PositiveInt.type_parser, default=5, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to display each gradient step. Increase to slow down the gradient animation.", ) # type: ignore[assignment] "int: Number of frames to display each gradient step. Increase to slow down the gradient animation." final_gradient_direction: tte.Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=tte.Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[BaseEffect]: """Return the effect class associated with this configuration dataclass.""" return LaserEtch class LaserEtchIterator(BaseEffectIterator[LaserEtchConfig]): """Iterator for the LaserEtch effect.""" class Laser: """A class to represent a laser beam effect in a terminal. The Laser class is responsible for creating and managing a laser beam effect in a terminal. It handles the initialization of the laser beam, the creation of spark effects, repositioning of the laser beam, emitting sparks, and disabling the laser beam. Methods: reposition(target: Coord) -> None: Repositions the laser beam to the target coordinate. emit(spark_count: int = 1) -> None: Emits a specified number of sparks from the laser beam. disable() -> None: Disables the laser beam by setting the visibility of the beam characters to False. """ def __init__( self, terminal: tte.Terminal, config: LaserEtchConfig, active_chars: set[tte.EffectCharacter], ) -> None: """Initialize the laser beam. Args: terminal (Terminal): The effect terminal. config (LaserEtchConfig): The effect configuration. active_chars (set[EffectCharacter]): The set of active characters in the effect. """ self.terminal = terminal self.config = config self.active_chars = active_chars self.position: tte.Coord = tte.Coord(0, 0) row = 0 col = 0 self.beam_chars: list[tte.EffectCharacter] = [] laser_gradient = deque(tte.Gradient(*config.laser_gradient_stops, steps=6, loop=True)) self.spark_gradient = tte.Gradient( *config.spark_gradient_stops, steps=(3, 8), ) self.sparks = self._make_sparks() while row <= self.terminal.canvas.top: symbol = "*" if not self.beam_chars else "/" char = self.terminal.add_character(symbol, tte.Coord(col, row)) char.layer = 2 self.terminal.set_character_visibility(char, is_visible=True) row += 1 col += 1 self.beam_chars.append(char) laser_scn = char.animation.new_scene(scene_id="laser", is_looping=True) for color in laser_gradient: laser_scn.add_frame(char.input_symbol, 3, colors=tte.ColorPair(fg=color)) laser_gradient.rotate(-1) char.animation.activate_scene(laser_scn) def _make_sparks(self) -> deque[tte.EffectCharacter]: sparks: deque[tte.EffectCharacter] = deque() for _ in range(2000): new_char = self.terminal.add_character(random.choice((".", ",", "*")), self.position) spark_scn = new_char.animation.new_scene(scene_id="spark") for color in self.spark_gradient: spark_scn.add_frame( new_char.input_symbol, self.config.spark_cooling_frames, colors=tte.ColorPair(fg=color), ) new_char.event_handler.register_event( tte.EventHandler.Event.SCENE_COMPLETE, spark_scn, tte.EventHandler.Action.CALLBACK, tte.EventHandler.Callback(lambda c: self.terminal.set_character_visibility(c, is_visible=False)), ) new_char.layer = 2 sparks.append(new_char) return sparks def reposition(self, target: tte.Coord) -> None: """Reposition the laser beam to the target coordinate. Set the coordinate of the laser beam characters based on the target coordinate to create the appearance of a laser beam. Args: target (Coord): The target coordinate for the laser beam. """ self.position = target row = target.row col = target.column for char in self.beam_chars: char.motion.set_coordinate(tte.Coord(col, row)) row += 1 col += 1 self.emit_sparks() def emit_sparks(self, spark_count: int = 1) -> None: """Emit sparks from the laser beam. Sets up the spark character Path and activates the Path and Scene for each spark character. The spark characters are added to the effect active_characters set. Args: spark_count (int, optional): Number of spark characters to emit. Defaults to 1. """ for _ in range(spark_count): next_spark = self.sparks[-1] self.sparks.rotate(1) next_spark.motion.set_coordinate(self.position) if next_spark.animation.active_scene: next_spark.animation.active_scene.reset_scene() self.terminal.set_character_visibility(next_spark, is_visible=True) spark_path = next_spark.motion.new_path(ease=tte.easing.out_sine, speed=0.15) fall_target_coord = tte.Coord( random.randint(self.position.column - 20, self.position.column + 20), self.terminal.canvas.bottom, ) spark_path.new_waypoint( fall_target_coord, bezier_control=tte.Coord(fall_target_coord.column, self.position.row + random.randint(-10, 20)), ) next_spark.motion.activate_path(spark_path) next_spark.animation.activate_scene(next_spark.animation.query_scene("spark")) self.active_chars.add(next_spark) def disable(self) -> None: """Disable the laser beam by setting the visibility of the beam characters to False.""" for char in self.beam_chars: self.terminal.set_character_visibility(char, is_visible=False) def __init__(self, effect: LaserEtch) -> None: """Initialize the effect iterator.""" super().__init__(effect) self.character_final_color_map: dict[tte.EffectCharacter, tte.ColorPair] = {} self.pending_chars: list[tte.EffectCharacter] = [] self.build() self.char_delay = 0 self.laser = LaserEtchIterator.Laser(self.terminal, self.config, self.active_characters) self.active_characters.update(self.laser.beam_chars) self.color_shifted_chars: set[tte.EffectCharacter] = set() def build(self) -> None: """Build the effect.""" sort_map = { "column_left_to_right": self.terminal.CharacterGroup.COLUMN_LEFT_TO_RIGHT, "column_right_to_left": self.terminal.CharacterGroup.COLUMN_RIGHT_TO_LEFT, "row_top_to_bottom": self.terminal.CharacterGroup.ROW_TOP_TO_BOTTOM, "row_bottom_to_top": self.terminal.CharacterGroup.ROW_BOTTOM_TO_TOP, "diagonal_top_left_to_bottom_right": self.terminal.CharacterGroup.DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT, "diagonal_bottom_left_to_top_right": self.terminal.CharacterGroup.DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT, "diagonal_top_right_to_bottom_left": self.terminal.CharacterGroup.DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT, "diagonal_bottom_right_to_top_left": self.terminal.CharacterGroup.DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT, "center_to_outside": self.terminal.CharacterGroup.CENTER_TO_OUTSIDE_DIAMONDS, "outside_to_center": self.terminal.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS, } final_fg_gradient = tte.Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_fg_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = tte.ColorPair( fg=final_gradient_mapping[character.input_coord], ) cool_gradient = tte.Gradient( *self.config.cool_gradient_stops, final_gradient_mapping[character.input_coord], steps=8, ) spawn_scn = character.animation.new_scene(scene_id="spawn") spawn_scn.add_frame("^", duration=3, colors=tte.ColorPair(fg="ffe680")) for color in cool_gradient: spawn_scn.add_frame(character.input_symbol, 4, colors=tte.ColorPair(fg=color)) character.animation.activate_scene(spawn_scn) for n, char_list in enumerate( self.terminal.get_characters_grouped(sort_map[self.config.etch_direction]), ): if n % 2: self.pending_chars.extend(char_list[::-1]) else: self.pending_chars.extend(char_list) def __next__(self) -> str: """Return the next frame in the effect.""" while self.pending_chars or self.active_characters: if not self.char_delay: for _ in range(self.config.etch_speed): if not self.pending_chars: break next_char = self.pending_chars.pop(0) self.terminal.set_character_visibility(next_char, is_visible=True) self.active_characters.add(next_char) self.laser.reposition(next_char.input_coord) self.char_delay = self.config.etch_delay else: self.char_delay -= 1 if self.pending_chars: self.active_characters.update(self.laser.beam_chars) else: self.laser.disable() self.update() return self.frame raise StopIteration class LaserEtch(BaseEffect[LaserEtchConfig]): """A laser etches characters onto the terminal.""" @property def _config_cls(self) -> type: return LaserEtchConfig @property def _iterator_cls(self) -> type: return LaserEtchIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_matrix.py000066400000000000000000000603421507200677100301740ustar00rootroot00000000000000"""Matrix digital rain effect. Classes: Matrix: Matrix digital rain effect. MatrixConfig: Configuration for the Matrix effect. MatrixIterator: Iterator for the Matrix effect. Does not normally need to be called directly. """ from __future__ import annotations import random import time import typing from dataclasses import dataclass from terminaltexteffects import Animation, Color, ColorPair, Coord, EffectCharacter, Gradient, Terminal from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass MATRIX_SYMBOLS_COMMON = ( "2", "5", "9", "8", "Z", "*", ")", ":", ".", '"', "=", "+", "-", "¦", "|", "_", ) MATRIX_SYMBOLS_KATA = ( "ヲ", "ア", "ウ", "エ", "オ", "カ", "キ", "ケ", "コ", "サ", "シ", "ス", "セ", "ソ", "タ", "ツ", "テ", "ナ", "ニ", "ヌ", "ネ", "ハ", "ヒ", "ホ", "マ", "ミ", "ム", "メ", "モ", "ヤ", "ユ", "ラ", "リ", "ワ", ) def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Matrix, MatrixConfig @argclass( name="matrix", help="Matrix digital rain effect.", description="matrix | Matrix digital rain effect.", epilog=( "Example: tte matrix --rain-color-gradient 92be92 185318 --rain-symbols 2 5 9 8 Z : . = + - ¦ _ " "--rain-fall-delay-range 8-25 --rain-column-delay-range 5-15 --rain-time 15 --symbol-swap-chance 0.005 " "--color-swap-chance 0.001 --resolve-delay 5 --final-gradient-stops 389c38 --final-gradient-steps 12 " "--final-gradient-frames 5 --final-gradient-direction vertical --highlight-color dbffdb" ), ) @dataclass class MatrixConfig(ArgsDataClass): """Configuration for the Matrix effect. Attributes: highlight_color (Color): Color for the bottom of the rain column. rain_color_gradient (tuple[Color, ...]): Tuple of colors for the rain gradient. If only one color is " "provided, the characters will be displayed in that color. rain_symbols (tuple[str, ...]): Tuple of symbols to use for the rain. rain_fall_delay_range (tuple[int, int]): Speed of the falling rain as determined by the delay between rows. " "Actual delay is randomly selected from the range. rain_column_delay_range (tuple[int, int]): Range of frames to wait between adding new rain columns. rain_time (int): Time, in seconds, to display the rain effect before transitioning to the input text. symbol_swap_chance (float): Chance of swapping a character's symbol on each tick. color_swap_chance (float): Chance of swapping a character's color on each tick. resolve_delay (int): Number of frames to wait between resolving the next group of characters. This is used " "to adjust the speed of the final resolve phase. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color " "is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Int or Tuple of ints for the number of gradient steps to use. " "More steps will create a smoother and longer gradient animation. final_gradient_frames (int): Number of frames to display each gradient step. Increase to slow down the " "gradient animation. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ highlight_color: Color = ArgField( cmd_name=["--highlight-color"], type_parser=argvalidators.ColorArg.type_parser, default=Color("dbffdb"), metavar=argvalidators.ColorArg.METAVAR, help="Color for the bottom of the rain column.", ) # type: ignore[assignment] "Color : Color for the bottom of the rain column." rain_color_gradient: tuple[Color, ...] = ArgField( cmd_name=["--rain-color-gradient"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("92be92"), Color("185318")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the rain gradient. Colors are selected from the " "gradient randomly. If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the rain gradient. If only one color is provided, the characters " "will be displayed in that color." ) rain_symbols: tuple[str, ...] = ArgField( cmd_name=["--rain-symbols"], nargs="+", type_parser=argvalidators.Symbol.type_parser, default=MATRIX_SYMBOLS_COMMON + MATRIX_SYMBOLS_KATA, metavar=argvalidators.Symbol.METAVAR, help="Space separated, unquoted, list of symbols to use for the rain.", ) # type: ignore[assignment] "tuple[str, ...] : Tuple of symbols to use for the rain." rain_fall_delay_range: tuple[int, int] = ArgField( cmd_name=["--rain-fall-delay-range"], type_parser=argvalidators.PositiveIntRange.type_parser, default=(3, 25), metavar=argvalidators.PositiveIntRange.METAVAR, help="Range for the speed of the falling rain as determined by the delay between rows. Actual delay is " "randomly selected from the range.", ) # type: ignore[assignment] ( "tuple[int, int] : Speed of the falling rain as determined by the delay between rows. Actual delay is " "randomly selected from the range." ) rain_column_delay_range: tuple[int, int] = ArgField( cmd_name=["--rain-column-delay-range"], type_parser=argvalidators.PositiveIntRange.type_parser, default=(5, 15), metavar=argvalidators.PositiveIntRange.METAVAR, help="Range of frames to wait between adding new rain columns.", ) # type: ignore[assignment] "tuple[int, int] : Range of frames to wait between adding new rain columns." rain_time: int = ArgField( cmd_name="--rain-time", type_parser=argvalidators.PositiveInt.type_parser, default=15, metavar=argvalidators.PositiveInt.METAVAR, help="Time, in seconds, to display the rain effect before transitioning to the input text.", ) # type: ignore[assignment] "int : Time, in seconds, to display the rain effect before transitioning to the input text." symbol_swap_chance: float = ArgField( cmd_name="--symbol-swap-chance", type_parser=argvalidators.PositiveFloat.type_parser, default=0.005, metavar=argvalidators.PositiveFloat.METAVAR, help="Chance of swapping a character's symbol on each tick.", ) # type: ignore[assignment] "float : Chance of swapping a character's symbol on each tick." color_swap_chance: float = ArgField( cmd_name="--color-swap-chance", type_parser=argvalidators.PositiveFloat.type_parser, default=0.001, metavar=argvalidators.PositiveFloat.METAVAR, help="Chance of swapping a character's color on each tick.", ) # type: ignore[assignment] "float : Chance of swapping a character's color on each tick." resolve_delay: int = ArgField( cmd_name="--resolve-delay", type_parser=argvalidators.PositiveInt.type_parser, default=5, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to wait between resolving the next group of characters. " "This is used to adjust the speed of the final resolve phase.", ) # type: ignore[assignment] ( "int : Number of frames to wait between resolving the next group of characters. This is used to " "adjust the speed of the final resolve phase." ) final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("92be92"), Color("336b33")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If " "only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_frames: int = ArgField( cmd_name="--final-gradient-frames", type_parser=argvalidators.PositiveInt.type_parser, default=5, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to display each gradient step. Increase to slow down the gradient animation.", ) # type: ignore[assignment] "int : Number of frames to display each gradient step. Increase to slow down the gradient animation." final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.RADIAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Matrix]: """Get the effect class associated with this configuration.""" return Matrix class MatrixIterator(BaseEffectIterator[MatrixConfig]): """Iterator for the Matrix effect.""" class RainColumn: """Rain column for the Matrix effect.""" def __init__( self, characters: list[EffectCharacter], terminal: Terminal, config: MatrixConfig, rain_colors: Gradient, ) -> None: """Initialize the rain column.""" self.terminal = terminal self.config = config self.characters: list[EffectCharacter] = characters self.pending_characters: list[EffectCharacter] = [] self.matrix_symbols: tuple[str, ...] = config.rain_symbols self.rain_colors = rain_colors self.column_drop_chance = 0.08 self.setup_column("rain") def setup_column(self, phase: str) -> None: """Set up the rain column for the specified phase.""" self.pending_characters.clear() self.phase = phase for character in self.characters: self.terminal.set_character_visibility(character, is_visible=False) self.pending_characters.append(character) character.motion.current_coord = character.input_coord self.visible_characters: list[EffectCharacter] = [] if self.phase == "fill": self.base_rain_fall_delay = random.randint( max(self.config.rain_fall_delay_range[0] // 3, 1), max(self.config.rain_fall_delay_range[1] // 3, 1), ) else: self.base_rain_fall_delay = random.randint( self.config.rain_fall_delay_range[0], self.config.rain_fall_delay_range[1], ) self.active_rain_fall_delay = 0 if self.phase == "rain": self.length = random.randint(max(1, int(len(self.characters) * 0.1)), len(self.characters)) else: self.length = len(self.characters) self.hold_time = 0 if self.length == len(self.characters): self.hold_time = random.randint(20, 45) def trim_column(self) -> None: """Trim the rain column.""" if not self.visible_characters: return popped_char = self.visible_characters.pop(0) self.terminal.set_character_visibility(popped_char, is_visible=False) if len(self.visible_characters) > 1: self.fade_last_character() def drop_column(self) -> None: """Drop the rain column.""" out_of_canvas = [] for character in self.visible_characters: character.motion.current_coord = Coord( character.motion.current_coord.column, character.motion.current_coord.row - 1, ) if character.motion.current_coord.row < self.terminal.canvas.bottom: self.terminal.set_character_visibility(character, is_visible=False) out_of_canvas.append(character) self.visible_characters = [char for char in self.visible_characters if char not in out_of_canvas] def fade_last_character(self) -> None: """Fade the last character in the rain column.""" darker_color = Animation.adjust_color_brightness(random.choice(self.rain_colors[-3:]), 0.65) # type: ignore[arg-type] self.visible_characters[0].animation.set_appearance( self.visible_characters[0].animation.current_character_visual.symbol, colors=ColorPair(fg=darker_color), ) def resolve_char(self) -> EffectCharacter: """Resolve a character in the rain column. Returns: EffectCharacter: The resolved character. """ return self.visible_characters.pop(random.randint(0, len(self.visible_characters) - 1)) def tick(self) -> None: """Advance the rain column by one tick.""" if not self.active_rain_fall_delay: if self.pending_characters: next_char = self.pending_characters.pop(0) next_char.animation.set_appearance( random.choice(self.matrix_symbols), colors=ColorPair(fg=self.config.highlight_color), ) previous_character = self.visible_characters[-1] if self.visible_characters else None # if there is a previous character, remove the highlight if previous_character: previous_character.animation.set_appearance( previous_character.animation.current_character_visual.symbol, colors=ColorPair( fg=random.choice(self.rain_colors), ), ) self.terminal.set_character_visibility(next_char, is_visible=True) self.visible_characters.append(next_char) # if no pending characters, but still visible characters, trim the column # unless the column is the full height of the canvas, then respect the hold # time before trimming elif self.visible_characters: # adjust the bottom character color to remove the lightlight. # always do this on the first hold frame, then # randomly adjust the bottom character's color # this is separately handled from the rest to prevent the # highlight color from being replaced before appropriate if ( self.visible_characters[-1].animation.current_character_visual.colors and self.visible_characters[-1].animation.current_character_visual.colors.fg_color == self.config.highlight_color ): self.visible_characters[-1].animation.set_appearance( self.visible_characters[-1].animation.current_character_visual.symbol, colors=ColorPair(fg=random.choice(self.rain_colors)), ) if self.hold_time: self.hold_time -= 1 elif self.phase == "rain": if random.random() < self.column_drop_chance: self.drop_column() self.trim_column() # if the column is longer than the preset length while still adding characters, trim it if len(self.visible_characters) > self.length: self.trim_column() self.active_rain_fall_delay = self.base_rain_fall_delay else: self.active_rain_fall_delay -= 1 # randomly change the symbol and/or color of the characters next_color: Color | None for character in self.visible_characters: if random.random() < self.config.symbol_swap_chance: next_symbol = random.choice(self.matrix_symbols) else: next_symbol = character.animation.current_character_visual.symbol if random.random() < self.config.color_swap_chance: next_color = random.choice(self.rain_colors) elif character.animation.current_character_visual.colors: next_color = character.animation.current_character_visual.colors.fg_color else: next_color = None character.animation.set_appearance(next_symbol, colors=ColorPair(fg=next_color)) def __init__(self, effect: Matrix) -> None: """Initialize the Matrix effect iterator.""" super().__init__(effect) self.pending_columns: list[MatrixIterator.RainColumn] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.active_columns: list[MatrixIterator.RainColumn] = [] self.full_columns: list[MatrixIterator.RainColumn] = [] self.rain_colors = Gradient(*self.config.rain_color_gradient, steps=6) self.column_delay = 0 self.resolve_delay = self.config.resolve_delay self.final_frame_shown = False self.rain_complete = False self.phase = "rain" self.build() self.rain_start = time.time() def build(self) -> None: """Build the initial state of the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): resolve_scn = character.animation.new_scene(scene_id="resolve") for color in Gradient(self.config.highlight_color, final_gradient_mapping[character.input_coord], steps=8): resolve_scn.add_frame( character.input_symbol, self.config.final_gradient_frames, colors=ColorPair(fg=color), ) for column_chars in self.terminal.get_characters_grouped( self.terminal.CharacterGroup.COLUMN_LEFT_TO_RIGHT, outer_fill_chars=True, inner_fill_chars=True, ): column_chars.reverse() self.pending_columns.append( MatrixIterator.RainColumn(column_chars, self.terminal, self.config, self.rain_colors), ) random.shuffle(self.pending_columns) def __next__(self) -> str: # noqa: PLR0915 """Return the next frame in the animation.""" if self.phase in ("rain", "fill"): if not self.column_delay: if self.phase == "rain": for _ in range(random.randint(1, 3)): if self.pending_columns: self.active_columns.append(self.pending_columns.pop(0)) else: while self.pending_columns: self.active_columns.append(self.pending_columns.pop(0)) if self.phase == "rain": self.column_delay = random.randint( self.config.rain_column_delay_range[0], self.config.rain_column_delay_range[1], ) else: self.column_delay = 1 else: self.column_delay -= 1 for column in self.active_columns: column.tick() if not column.pending_characters: if column.phase == "fill" and column not in self.full_columns: self.full_columns.append(column) elif not column.visible_characters: column.setup_column(self.phase) self.pending_columns.append(column) self.active_columns = [column for column in self.active_columns if column.visible_characters] if ( self.phase == "fill" and not self.pending_columns and all((not column.pending_characters and column.phase == "fill") for column in self.active_columns) ): self.phase = "resolve" self.active_columns.clear() if ( self.phase == "rain" and self.config.rain_time > 0 and time.time() - self.rain_start > self.config.rain_time ): self.rain_complete = True self.phase = "fill" for column in self.active_columns: column.hold_time = 0 column.column_drop_chance = 1 for column in self.pending_columns: column.setup_column(self.phase) elif self.phase == "resolve": for column in self.full_columns: column.tick() if column.visible_characters: if not self.resolve_delay: for _ in range(random.randint(1, 4)): if column.visible_characters: next_char = column.resolve_char() if next_char.input_symbol != " ": next_char.animation.activate_scene(next_char.animation.query_scene("resolve")) self.active_characters.add(next_char) else: self.terminal.set_character_visibility(next_char, is_visible=False) self.resolve_delay = self.config.resolve_delay else: self.resolve_delay -= 1 self.full_columns = [column for column in self.full_columns if column.visible_characters] if ( self.full_columns or self.active_columns or self.active_characters or self.pending_columns or not self.rain_complete ): self.update() return self.frame if not self.final_frame_shown: self.final_frame_shown = True self.update() return self.frame raise StopIteration class Matrix(BaseEffect[MatrixConfig]): """Matrix digital rain effect. Attributes: effect_config (MatrixConfig): Configuration for the Matrix effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[MatrixConfig]: return MatrixConfig @property def _iterator_cls(self) -> type[MatrixIterator]: return MatrixIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_middleout.py000066400000000000000000000252671507200677100306650ustar00rootroot00000000000000"""Text expands in a single row or column in the middle of the canvas then out. Classes: MiddleOut: Text expands in a single row or column in the middle of the canvas then out. MiddleOutConfig: Configuration for the Middleout effect. MiddleOutIterator: Iterates over the effect's frames. Does not normally need to be called directly. """ from __future__ import annotations import typing from dataclasses import dataclass from terminaltexteffects import Color, ColorPair, Coord, EffectCharacter, Gradient, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return MiddleOut, MiddleOutConfig @argclass( name="middleout", help="Text expands in a single row or column in the middle of the canvas then out.", description="middleout | Text expands in a single row or column in the middle of the canvas then out.", epilog=( f"{argvalidators.EASING_EPILOG} Example: terminaltexteffects middleout --starting-color 8A008A " "--final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --expand-direction vertical " "--center-movement-speed 0.35 --full-movement-speed 0.35 --center-easing IN_OUT_SINE " "--full-easing IN_OUT_SINE" ), ) @dataclass class MiddleOutConfig(ArgsDataClass): """Configuration for the Middleout effect. Attributes: starting_color (Color): Color for the initial text in the center of the canvas. expand_direction (typing.Literal["vertical", "horizontal"]): Direction the text will expand. Choices: " "vertical, horizontal. center_movement_speed (float): Speed of the characters during the initial expansion of the center " "vertical/horiztonal. Valid values are n > 0. full_movement_speed (float): Speed of the characters during the final full expansion. Valid values are n > 0. center_easing (easing.EasingFunction): Easing function to use for initial expansion. full_easing (easing.EasingFunction): Easing function to use for full expansion. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color " "is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps " "will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ starting_color: Color = ArgField( cmd_name="--starting-color", type_parser=argvalidators.ColorArg.type_parser, default=Color("ffffff"), metavar=argvalidators.ColorArg.METAVAR, help="Color for the initial text in the center of the canvas.", ) # type: ignore[assignment] """Color : Color for the initial text in the center of the canvas.""" expand_direction: typing.Literal["vertical", "horizontal"] = ArgField( cmd_name="--expand-direction", default="vertical", choices=["vertical", "horizontal"], help="Direction the text will expand.", ) # type: ignore[assignment] """str : Direction the text will expand.""" center_movement_speed: float = ArgField( cmd_name="--center-movement-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=0.35, metavar=argvalidators.PositiveFloat.METAVAR, help="Speed of the characters during the initial expansion of the center vertical/horiztonal line. ", ) # type: ignore[assignment] """float : Speed of the characters during the initial expansion of the center vertical/horiztonal line. """ full_movement_speed: float = ArgField( cmd_name="--full-movement-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=0.35, metavar=argvalidators.PositiveFloat.METAVAR, help="Speed of the characters during the final full expansion. ", ) # type: ignore[assignment] """float : Speed of the characters during the final full expansion. """ center_easing: easing.EasingFunction = ArgField( cmd_name="--center-easing", default=easing.in_out_sine, type_parser=argvalidators.Ease.type_parser, help="Easing function to use for initial expansion.", ) # type: ignore[assignment] """easing.EasingFunction : Easing function to use for initial expansion.""" full_easing: easing.EasingFunction = ArgField( cmd_name="--full-easing", default=easing.in_out_sine, type_parser=argvalidators.Ease.type_parser, help="Easing function to use for full expansion.", ) # type: ignore[assignment] """easing.EasingFunction : Easing function to use for full expansion.""" final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name="--final-gradient-stops", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("8A008A"), Color("00D1FF"), Color("FFFFFF")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] """Gradient.Direction : Direction of the final gradient.""" @classmethod def get_effect_class(cls) -> type[MiddleOut]: """Get the effect class associated with this configuration.""" return MiddleOut class MiddleOutIterator(BaseEffectIterator[MiddleOutConfig]): """Iterates over the frames of the MiddleOut effect.""" def __init__(self, effect: MiddleOut) -> None: """Initialize the MiddleOut effect iterator. Args: effect (MiddleOut): The MiddleOut effect to iterate over. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.phase = "center" self.build() def build(self) -> None: """Build the initial state of the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] character.motion.set_coordinate(self.terminal.canvas.center) # setup waypoints if self.config.expand_direction == "vertical": column = character.input_coord.column row = self.terminal.canvas.center_row else: column = self.terminal.canvas.center_column row = character.input_coord.row center_path = character.motion.new_path( speed=self.config.center_movement_speed, ease=self.config.center_easing, ) center_path.new_waypoint(Coord(column, row)) full_path = character.motion.new_path( path_id="full", speed=self.config.full_movement_speed, ease=self.config.full_easing, ) full_path.new_waypoint(character.input_coord, waypoint_id="full") # setup scenes full_scene = character.animation.new_scene(scene_id="full") full_gradient = Gradient(self.config.starting_color, self.character_final_color_map[character], steps=10) full_scene.apply_gradient_to_symbols(character.input_symbol, 10, fg_gradient=full_gradient) # initialize character state character.motion.activate_path(center_path) character.animation.set_appearance(character.input_symbol, ColorPair(fg=self.config.starting_color)) self.terminal.set_character_visibility(character, is_visible=True) self.active_characters.add(character) def __next__(self) -> str: """Return the next frame in the animation.""" if self.phase == "center" and not self.active_characters: self.phase = "full" self.active_characters = set(self.terminal.get_characters()) for character in self.active_characters: character.motion.activate_path(character.motion.query_path("full")) character.animation.activate_scene(character.animation.query_scene("full")) if self.active_characters: self.update() return self.frame raise StopIteration class MiddleOut(BaseEffect[MiddleOutConfig]): """Text expands in a single row or column in the middle of the canvas then out. Attributes: effect_config (MiddleOutConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[MiddleOutConfig]: return MiddleOutConfig @property def _iterator_cls(self) -> type[MiddleOutIterator]: return MiddleOutIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_orbittingvolley.py000066400000000000000000000444441507200677100321310ustar00rootroot00000000000000"""Four launchers orbit the canvas firing volleys of characters inward to build the input text from the center out. Classes: OrbittingVolley: Four launchers orbit the canvas firing volleys of characters inward to build the input text from the center out. OrbittingVolleyConfig: Configuration for the OrbittingVolley effect. OrbittingVolleyIterator: Effect iterator for OrbittingVolley. Does not normally need to be called directly. """ from __future__ import annotations import typing from dataclasses import dataclass from itertools import cycle from terminaltexteffects import Color, ColorPair, Coord, EffectCharacter, EventHandler, Gradient, Terminal, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return OrbittingVolley, OrbittingVolleyConfig @argclass( name="orbittingvolley", help=( "Four launchers orbit the canvas firing volleys of characters inward to build the input text " "from the center out." ), description=( "orbittingvolley | Four launchers orbit the canvas firing volleys of characters inward to build " "the input text from the center out." ), epilog=( f"{argvalidators.EASING_EPILOG} Example: terminaltexteffects orbittingvolley --top-launcher-symbol █ " "--right-launcher-symbol █ --bottom-launcher-symbol █ --left-launcher-symbol █ " "--final-gradient-stops FFA15C 44D492 --final-gradient-steps 12 --launcher-movement-speed 0.5 " "--character-movement-speed 1 --volley-size 0.03 --launch-delay 50 --character-easing OUT_SINE" ), ) @dataclass class OrbittingVolleyConfig(ArgsDataClass): """Configuration for the OrbittingVolley effect. Attributes: top_launcher_symbol (str): Symbol for the top launcher. right_launcher_symbol (str): Symbol for the right launcher. bottom_launcher_symbol (str): Symbol for the bottom launcher. left_launcher_symbol (str): Symbol for the left launcher. launcher_movement_speed (float): Orbitting speed of the launchers. Valid values are n > 0. character_movement_speed (float): Speed of the launched characters. Valid values are n > 0. volley_size (float): Percent of total input characters each launcher will fire per volley. Lower limit of " "one character. Valid values are 0 < n <= 1. launch_delay (int): Number of animation ticks to wait between volleys of characters. Valid values are n >= 0. character_easing (easing.EasingFunction): Easing function to use for launched character movement. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one " "color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps " "will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ top_launcher_symbol: str = ArgField( cmd_name="--top-launcher-symbol", type_parser=argvalidators.Symbol.type_parser, default="█", metavar=argvalidators.Symbol.METAVAR, help="Symbol for the top launcher.", ) # type: ignore[assignment] "str : Symbol for the top launcher." right_launcher_symbol: str = ArgField( cmd_name="--right-launcher-symbol", type_parser=argvalidators.Symbol.type_parser, default="█", metavar=argvalidators.Symbol.METAVAR, help="Symbol for the right launcher.", ) # type: ignore[assignment] "str : Symbol for the right launcher." bottom_launcher_symbol: str = ArgField( cmd_name="--bottom-launcher-symbol", type_parser=argvalidators.Symbol.type_parser, default="█", metavar=argvalidators.Symbol.METAVAR, help="Symbol for the bottom launcher.", ) # type: ignore[assignment] "str : Symbol for the bottom launcher." left_launcher_symbol: str = ArgField( cmd_name="--left-launcher-symbol", type_parser=argvalidators.Symbol.type_parser, default="█", metavar=argvalidators.Symbol.METAVAR, help="Symbol for the left launcher.", ) # type: ignore[assignment] "str : Symbol for the left launcher." launcher_movement_speed: float = ArgField( cmd_name="--launcher-movement-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=0.5, metavar=argvalidators.PositiveFloat.METAVAR, help="Orbitting speed of the launchers.", ) # type: ignore[assignment] "float : Orbitting speed of the launchers." character_movement_speed: float = ArgField( cmd_name="--character-movement-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=1, metavar=argvalidators.PositiveFloat.METAVAR, help="Speed of the launched characters.", ) # type: ignore[assignment] "float : Speed of the launched characters." volley_size: float = ArgField( cmd_name="--volley-size", type_parser=argvalidators.NonNegativeRatio.type_parser, default=0.03, metavar=argvalidators.NonNegativeRatio.METAVAR, help="Percent of total input characters each launcher will fire per volley. Lower limit of one character.", ) # type: ignore[assignment] "float : Percent of total input characters each launcher will fire per volley. Lower limit of one character." launch_delay: int = ArgField( cmd_name="--launch-delay", type_parser=argvalidators.NonNegativeInt.type_parser, default=50, metavar=argvalidators.NonNegativeInt.METAVAR, help="Number of animation ticks to wait between volleys of characters.", ) # type: ignore[assignment] "int : Number of animation ticks to wait between volleys of characters." character_easing: easing.EasingFunction = ArgField( cmd_name=["--character-easing"], default=easing.out_sine, type_parser=argvalidators.Ease.type_parser, metavar=argvalidators.Ease.METAVAR, help="Easing function to use for launched character movement.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for launched character movement." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name="--final-gradient-stops", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("FFA15C"), Color("44D492")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create " "a smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.RADIAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[OrbittingVolley]: """Get the effect class associated with this configuration.""" return OrbittingVolley class OrbittingVolleyIterator(BaseEffectIterator[OrbittingVolleyConfig]): """Effect iterator for OrbittingVolley.""" class Launcher: """A launcher that fires characters inward to build the input text from the center out.""" def __init__( self, terminal: Terminal, args: OrbittingVolleyConfig, starting_edge_coord: Coord, symbol: str, ) -> None: """Initialize the launcher. Args: terminal (Terminal): The effect Terminal. args (OrbittingVolleyConfig): Configuration for the effect. starting_edge_coord (Coord): The starting coordinate for the launcher. symbol (str): The symbol to use for the launcher. """ self.terminal = terminal self.args = args self.character = self.terminal.add_character(symbol, starting_edge_coord) self.magazine: list[EffectCharacter] = [] def build_paths(self) -> None: """Build the paths for the launcher.""" waypoints = [ Coord(self.terminal.canvas.left, self.terminal.canvas.top), Coord(self.terminal.canvas.right, self.terminal.canvas.top), ] waypoint_start_index = waypoints.index(self.character.input_coord) perimeter_path = self.character.motion.new_path( speed=self.args.launcher_movement_speed, path_id="perimeter", layer=2, ) for waypoint in waypoints[waypoint_start_index:] + waypoints[:waypoint_start_index]: perimeter_path.new_waypoint(waypoint) def launch(self) -> EffectCharacter | None: """Launch a character from the magazine.""" if self.magazine: next_char = self.magazine.pop(0) next_char.motion.set_coordinate(self.character.motion.current_coord) input_path = next_char.motion.query_path("input_path") next_char.motion.activate_path(input_path) self.terminal.set_character_visibility(next_char, is_visible=True) else: next_char = None return next_char def __init__(self, effect: OrbittingVolley) -> None: """Initialize the effect iterator.""" super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) self.character_final_color_map: dict[EffectCharacter, Color] = {} self.final_gradient_coordinate_map: dict[Coord, Color] = self.final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) self.launcher_gradient_coordinate_map: dict[Coord, Color] = self.final_gradient.build_coordinate_color_mapping( self.terminal.canvas.bottom, self.terminal.canvas.top, self.terminal.canvas.left, self.terminal.canvas.right, self.config.final_gradient_direction, ) self.complete = False self.build() def build(self) -> None: """Build the initial state of the effect.""" for character in self.terminal.get_characters(): self.character_final_color_map[character] = self.final_gradient_coordinate_map[character.input_coord] input_path = character.motion.new_path( speed=self.config.character_movement_speed, ease=self.config.character_easing, path_id="input_path", layer=1, ) input_path.new_waypoint(character.input_coord) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, input_path, EventHandler.Action.SET_LAYER, 0, ) character.animation.set_appearance( character.input_symbol, ColorPair(fg=self.character_final_color_map[character]), ) self._launchers: list[OrbittingVolleyIterator.Launcher] = [] for coord, symbol in ( ( Coord(self.terminal.canvas.left, self.terminal.canvas.top), self.config.top_launcher_symbol, ), ( Coord(self.terminal.canvas.right, self.terminal.canvas.top), self.config.right_launcher_symbol, ), ( Coord(self.terminal.canvas.right, self.terminal.canvas.bottom), self.config.bottom_launcher_symbol, ), ( Coord(self.terminal.canvas.left, self.terminal.canvas.bottom), self.config.left_launcher_symbol, ), ): launcher = OrbittingVolleyIterator.Launcher(self.terminal, self.config, coord, symbol) launcher.character.layer = 2 self.terminal.set_character_visibility(launcher.character, is_visible=True) self.active_characters.add(launcher.character) self._launchers.append(launcher) self._main_launcher = self._launchers[0] self._main_launcher.character.animation.set_appearance( self._main_launcher.character.input_symbol, ColorPair(fg=self.final_gradient.spectrum[-1]), ) self._main_launcher.build_paths() self._main_launcher.character.motion.activate_path(self._main_launcher.character.motion.query_path("perimeter")) self._sorted_chars = [] for char_list in self.terminal.get_characters_grouped(Terminal.CharacterGroup.CENTER_TO_OUTSIDE_DIAMONDS): self._sorted_chars.extend(char_list) for launcher, character in zip(cycle(self._launchers), self._sorted_chars): launcher.magazine.append(character) self._delay = 0 def _set_launcher_coordinates(self, parent: Launcher, child: Launcher) -> None: parent_progress = parent.character.motion.current_coord.column / self.terminal.canvas.right if child.character.input_coord == Coord(self.terminal.canvas.right, self.terminal.canvas.top): child_row = self.terminal.canvas.top - int(self.terminal.canvas.top * parent_progress) child.character.motion.set_coordinate(Coord(self.terminal.canvas.right, max(1, child_row))) elif child.character.input_coord == Coord(self.terminal.canvas.right, self.terminal.canvas.bottom): child_column = self.terminal.canvas.right - int(self.terminal.canvas.right * parent_progress) child.character.motion.set_coordinate(Coord(max(1, child_column), self.terminal.canvas.bottom)) elif child.character.input_coord == Coord(self.terminal.canvas.left, self.terminal.canvas.bottom): child_row = self.terminal.canvas.bottom + int(self.terminal.canvas.top * parent_progress) child.character.motion.set_coordinate( Coord(self.terminal.canvas.left, min(self.terminal.canvas.top, child_row)), ) color = self.launcher_gradient_coordinate_map[child.character.motion.current_coord] child.character.animation.set_appearance(child.character.input_symbol, ColorPair(fg=color)) def __next__(self) -> str: """Return the next frame in the animation.""" if any(launcher.magazine for launcher in self._launchers) or len(self.active_characters) > 1: if self._main_launcher.character.motion.active_path is None: perimeter_path = self._main_launcher.character.motion.query_path("perimeter") self._main_launcher.character.motion.set_coordinate(perimeter_path.waypoints[0].coord) self._main_launcher.character.motion.activate_path(perimeter_path) self.active_characters.add(self._main_launcher.character) self._main_launcher.character.animation.set_appearance( self.config.top_launcher_symbol, ColorPair( fg=self.launcher_gradient_coordinate_map[self._main_launcher.character.motion.current_coord], ), ) for launcher in self._launchers[1:]: self._set_launcher_coordinates(self._main_launcher, launcher) if not self._delay: for launcher in self._launchers: characters_to_launch = max( int((self.config.volley_size * len(self.terminal._input_characters)) / 4), 1, ) for _ in range(characters_to_launch): next_char = launcher.launch() if next_char: self.active_characters.add(next_char) self._delay = self.config.launch_delay else: self._delay -= 1 self.update() return self.frame if not self.complete: self.complete = True for launcher in self._launchers: self.terminal.set_character_visibility(launcher.character, is_visible=False) return self.frame raise StopIteration class OrbittingVolley(BaseEffect[OrbittingVolleyConfig]): """Four launchers orbit the canvas firing volleys of characters inward to build the input text from the center out. Attributes: effect_config (OrbittingVolleyConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[OrbittingVolleyConfig]: return OrbittingVolleyConfig @property def _iterator_cls(self) -> type[OrbittingVolleyIterator]: return OrbittingVolleyIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_overflow.py000066400000000000000000000321461507200677100305340ustar00rootroot00000000000000"""Input text overflows and scrolls the terminal in a random order until eventually appearing ordered. Classes: Overflow: Input text overflows and scrolls the terminal in a random order until eventually appearing ordered. OverflowConfig: Configuration for the Overflow effect. OverflowIterator: Iterates over the effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, ColorPair, Coord, EffectCharacter, Gradient, Terminal from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Overflow, OverflowConfig @argclass( name="overflow", help="Input text overflows and scrolls the terminal in a random order until eventually appearing ordered.", description="overflow | Input text overflows and scrolls the terminal in a random order until eventually " "appearing ordered.", epilog=( "Example: terminaltexteffects overflow --final-gradient-stops 8A008A 00D1FF FFFFFF " "--final-gradient-steps 12 --overflow-gradient-stops f2ebc0 8dbfb3 f2ebc0 --overflow-cycles-range 2-4 " "--overflow-speed 3" ), ) @dataclass class OverflowConfig(ArgsDataClass): """Configuration for the Overflow effect. Attributes: overflow_gradient_stops (tuple[Color, ...]): Tuple of colors for the overflow gradient. overflow_cycles_range (tuple[int, int]): Lower and upper range of the number of cycles to overflow the text. " "Valid values are n >= 0. overflow_speed (int): Speed of the overflow effect. Valid values are n > 0. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color " "is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps " "will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ overflow_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--overflow-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("f2ebc0"), Color("8dbfb3"), Color("f2ebc0")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the overflow gradient.", ) # type: ignore[assignment] "tuple[Color, ...] : Tuple of colors for the overflow gradient." overflow_cycles_range: tuple[int, int] = ArgField( cmd_name=["--overflow-cycles-range"], type_parser=argvalidators.PositiveIntRange.type_parser, default=(2, 4), metavar=argvalidators.PositiveIntRange.METAVAR, help="Number of cycles to overflow the text.", ) # type: ignore[assignment] "tuple[int, int] : Lower and upper range of the number of cycles to overflow the text." overflow_speed: int = ArgField( cmd_name=["--overflow-speed"], type_parser=argvalidators.PositiveInt.type_parser, default=3, metavar=argvalidators.PositiveInt.METAVAR, help="Speed of the overflow effect.", ) # type: ignore[assignment] "int : Speed of the overflow effect." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("8A008A"), Color("00D1FF"), Color("FFFFFF")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If " "only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name=["--final-gradient-steps"], type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Overflow]: """Get the effect class associated with this configuration.""" return Overflow class OverflowIterator(BaseEffectIterator[OverflowConfig]): """Iterates over the effect.""" class Row: """Represents a row of characters in the overflow effect.""" def __init__(self, characters: list[EffectCharacter], *, final: bool = False) -> None: """Initialize the row. Args: characters (list[EffectCharacter]): The characters in the row. final (bool, optional): This is the final state of the row. Defaults to False. """ self.characters = characters self.current_index = 0 self.final = final def move_up(self) -> None: """Move the row up by one row.""" for character in self.characters: current_row = character.motion.current_coord.row character.motion.set_coordinate(Coord(character.motion.current_coord.column, current_row + 1)) def setup(self) -> None: """Set up the row for display.""" for character in self.characters: character.motion.set_coordinate(Coord(character.input_coord.column, 0)) def set_color(self, fg_color: Color | None = None, bg_color: Color | None = None) -> None: """Set the color of the row.""" for character in self.characters: character.animation.set_appearance( character.input_symbol, ColorPair(fg=fg_color, bg=bg_color), ) def __init__(self, effect: Overflow) -> None: """Initialize the effect iterator.""" super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.pending_rows: list[OverflowIterator.Row] = [] self.active_rows: list[OverflowIterator.Row] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the initial state of the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(outer_fill_chars=True, inner_fill_chars=True): self.character_final_color_map[character] = final_gradient_mapping.get( character.input_coord, Color("000000"), ) lower_range, upper_range = self.config.overflow_cycles_range rows = self.terminal.get_characters_grouped(Terminal.CharacterGroup.ROW_TOP_TO_BOTTOM) if upper_range > 0: for _ in range(random.randint(lower_range, upper_range)): random.shuffle(rows) for row in rows: copied_characters = [] # copy the character attributes to new characters for character in row: character_copy = self.terminal.add_character(character.input_symbol, character.input_coord) character_copy.animation.existing_color_handling = self.terminal.config.existing_color_handling character_copy._input_ansi_sequences = character._input_ansi_sequences character_copy.animation.no_color = character.animation.no_color character_copy.animation.use_xterm_colors = character.animation.use_xterm_colors character_copy.animation.input_fg_color = character.animation.input_fg_color character_copy.animation.input_bg_color = character.animation.input_bg_color copied_characters.append(character_copy) self.pending_rows.append(OverflowIterator.Row(copied_characters)) # add rows in correct order to the end of self.pending_rows for row in self.terminal.get_characters_grouped( Terminal.CharacterGroup.ROW_TOP_TO_BOTTOM, outer_fill_chars=True, inner_fill_chars=True, ): next_row = OverflowIterator.Row(row) for character in next_row.characters: if self.terminal.config.existing_color_handling == "dynamic" and any( (character.animation.input_fg_color, character.animation.input_bg_color), ): character.animation.set_appearance( character.animation.current_character_visual.symbol, ColorPair( fg=character.animation.input_fg_color, bg=character.animation.input_bg_color, ), ) else: character.animation.set_appearance( character.animation.current_character_visual.symbol, ColorPair(fg=self.character_final_color_map[character]), ) self.pending_rows.append(OverflowIterator.Row(row, final=True)) self._delay = 0 self._overflow_gradient = Gradient( *self.config.overflow_gradient_stops, steps=max((self.terminal.canvas.top // max(1, len(self.config.overflow_gradient_stops) - 1)), 1), ) def __next__(self) -> str: """Return the next frame in the animation.""" if self.pending_rows: if not self._delay: for _ in range(random.randint(1, self.config.overflow_speed)): if self.pending_rows: for row in self.active_rows: row.move_up() if not row.final: row.set_color( self._overflow_gradient.spectrum[ min( row.characters[0].motion.current_coord.row, len(self._overflow_gradient.spectrum) - 1, ) ], ) next_row = self.pending_rows.pop(0) next_row.setup() next_row.move_up() if not next_row.final: next_row.set_color(self._overflow_gradient.spectrum[0]) for character in next_row.characters: self.terminal.set_character_visibility(character, is_visible=True) self.active_rows.append(next_row) self._delay = random.randint(0, 3) else: self._delay -= 1 self.active_rows = [ row for row in self.active_rows if row.characters[0].motion.current_coord.row <= self.terminal.canvas.top ] self.update() return self.frame raise StopIteration class Overflow(BaseEffect[OverflowConfig]): """Input text overflows and scrolls the terminal in a random order until eventually appearing ordered. Attributes: effect_config (OverflowConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[OverflowConfig]: return OverflowConfig @property def _iterator_cls(self) -> type[OverflowIterator]: return OverflowIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_pour.py000066400000000000000000000306621507200677100276570ustar00rootroot00000000000000"""Pours the characters back and forth from the top, bottom, left, or right. Classes: Pour: Pours the characters back and forth from the top, bottom, left, or right. PourConfig: Configuration for the Pour effect. PourIterator: Iterates over the frames of the Pour effect. Does not normally need to be called directly. """ from __future__ import annotations import typing from dataclasses import dataclass from enum import Enum, auto from terminaltexteffects import Color, Coord, EffectCharacter, Gradient, Terminal, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Pour, PourConfig @argclass( name="pour", help="Pours the characters into position from the given direction.", description="pour | Pours the characters into position from the given direction.", epilog=( f"{argvalidators.EASING_EPILOG} Example: terminaltexteffects pour --pour-direction down " "--movement-speed 0.2 --gap 1 --starting-color FFFFFF --final-gradient-stops 8A008A 00D1FF FFFFFF " "--easing IN_QUAD" ), ) @dataclass class PourConfig(ArgsDataClass): """Configuration for the Pour effect. Attributes: pour_direction (str): Direction the text will pour. Valid values are "up", "down", "left", and "right". pour_speed (int): Number of characters poured in per tick. Increase to speed up the effect. " "Valid values are n > 0. movement_speed (float): Movement speed of the characters. Valid values are n > 0. gap (int): Number of frames to wait between each character in the pour effect. Increase to slow down effect " "and create a more defined back and forth motion. Valid values are n >= 0. starting_color (Color): Color of the characters before the gradient starts. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the character gradient. If only one color " "is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Number of gradient steps to use. More steps will create a " "smoother and longer gradient animation. final_gradient_frames (int): Number of frames to display each gradient step. Increase to slow down the " "gradient animation. final_gradient_direction (Gradient.Direction): Direction of the final gradient. easing (easing.EasingFunction): Easing function to use for character movement. """ pour_direction: typing.Literal["up", "down", "left", "right"] = ArgField( cmd_name=["--pour-direction"], default="down", choices=["up", "down", "left", "right"], help="Direction the text will pour.", ) # type: ignore[assignment] "typing.Literal['up', 'down', 'left', 'right'] : Direction the text will pour." pour_speed: int = ArgField( cmd_name="--pour-speed", type_parser=argvalidators.PositiveInt.type_parser, default=1, metavar=argvalidators.PositiveInt.METAVAR, help="Number of characters poured in per tick. Increase to speed up the effect.", ) # type: ignore[assignment] "int : Number of characters poured in per tick. Increase to speed up the effect." movement_speed: float = ArgField( cmd_name="--movement-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=0.2, metavar=argvalidators.PositiveFloat.METAVAR, help="Movement speed of the characters. ", ) # type: ignore[assignment] "float : Movement speed of the characters." gap: int = ArgField( cmd_name="--gap", type_parser=argvalidators.NonNegativeInt.type_parser, default=1, metavar=argvalidators.NonNegativeInt.METAVAR, help="Number of frames to wait between each character in the pour effect. Increase to slow down effect " "and create a more defined back and forth motion.", ) # type: ignore[assignment] "int : Number of frames to wait between each character in the pour effect." starting_color: Color = ArgField( cmd_name=["--starting-color"], type_parser=argvalidators.ColorArg.type_parser, default=Color("ffffff"), metavar=argvalidators.ColorArg.METAVAR, help="Color of the characters before the gradient starts.", ) # type: ignore[assignment] "Color : Color of the characters before the gradient starts." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("8A008A"), Color("00D1FF"), Color("FFFFFF")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient. If only one color is provided, " "the characters will be displayed in that color.", ) # type: ignore[assignment] "tuple[Color, ...] : Tuple of colors for the character gradient." final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name=["--final-gradient-steps"], type_parser=argvalidators.PositiveInt.type_parser, default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Number of gradient steps to use. More steps will create a smoother and longer gradient animation.", ) # type: ignore[assignment] "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use." final_gradient_frames: int = ArgField( cmd_name=["--final-gradient-frames"], type_parser=argvalidators.PositiveInt.type_parser, default=10, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to display each gradient step. Increase to slow down the gradient animation.", ) # type: ignore[assignment] "int : Number of frames to display each gradient step. Increase to slow down the gradient animation." final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." movement_easing: easing.EasingFunction = ArgField( cmd_name="--movement-easing", default=easing.in_quad, type_parser=argvalidators.Ease.type_parser, help="Easing function to use for character movement.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for character movement." @classmethod def get_effect_class(cls) -> type[Pour]: """Get the effect class associated with this configuration.""" return Pour class PourIterator(BaseEffectIterator[PourConfig]): """Iterator for the Pour effect.""" class PourDirection(Enum): """Pour direction enumeration.""" UP = auto() DOWN = auto() LEFT = auto() RIGHT = auto() def __init__(self, effect: Pour) -> None: """Initialize the iterator with the provided effect. Args: effect (Pour): The effect to use for the iterator. """ super().__init__(effect) self.pending_groups: list[list[EffectCharacter]] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the initial state of the effect.""" self._pour_direction = { "down": PourIterator.PourDirection.DOWN, "up": PourIterator.PourDirection.UP, "left": PourIterator.PourDirection.LEFT, "right": PourIterator.PourDirection.RIGHT, }.get(self.config.pour_direction, PourIterator.PourDirection.DOWN) final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] sort_map = { PourIterator.PourDirection.DOWN: Terminal.CharacterGroup.ROW_BOTTOM_TO_TOP, PourIterator.PourDirection.UP: Terminal.CharacterGroup.ROW_TOP_TO_BOTTOM, PourIterator.PourDirection.LEFT: Terminal.CharacterGroup.COLUMN_LEFT_TO_RIGHT, PourIterator.PourDirection.RIGHT: Terminal.CharacterGroup.COLUMN_RIGHT_TO_LEFT, } groups = self.terminal.get_characters_grouped(grouping=sort_map[self._pour_direction]) for i, group in enumerate(groups): for character in group: self.terminal.set_character_visibility(character, is_visible=False) if self._pour_direction == PourIterator.PourDirection.DOWN: character.motion.set_coordinate(Coord(character.input_coord.column, self.terminal.canvas.top)) elif self._pour_direction == PourIterator.PourDirection.UP: character.motion.set_coordinate(Coord(character.input_coord.column, self.terminal.canvas.bottom)) elif self._pour_direction == PourIterator.PourDirection.LEFT: character.motion.set_coordinate(Coord(self.terminal.canvas.right, character.input_coord.row)) elif self._pour_direction == PourIterator.PourDirection.RIGHT: character.motion.set_coordinate(Coord(self.terminal.canvas.left, character.input_coord.row)) input_coord_path = character.motion.new_path( speed=self.config.movement_speed, ease=self.config.movement_easing, ) input_coord_path.new_waypoint(character.input_coord) character.motion.activate_path(input_coord_path) pour_gradient = Gradient( self.config.starting_color, self.character_final_color_map[character], steps=self.config.final_gradient_steps, ) pour_scn = character.animation.new_scene() pour_scn.apply_gradient_to_symbols( character.input_symbol, self.config.final_gradient_frames, fg_gradient=pour_gradient, ) character.animation.activate_scene(pour_scn) if i % 2 == 0: self.pending_groups.append(group) else: self.pending_groups.append(group[::-1]) self.gap = 0 self.current_group = self.pending_groups.pop(0) def __next__(self) -> str: """Return the next frame in the animation.""" if self.pending_groups or self.active_characters or self.current_group: if not self.current_group and self.pending_groups: self.current_group = self.pending_groups.pop(0) if self.current_group: if not self.gap: for _ in range(self.config.pour_speed): if self.current_group: next_character = self.current_group.pop(0) self.terminal.set_character_visibility(next_character, is_visible=True) self.active_characters.add(next_character) self.gap = self.config.gap else: self.gap -= 1 self.update() return self.frame raise StopIteration class Pour(BaseEffect[PourConfig]): """Pours the characters back and forth from the top, bottom, left, or right. Attributes: effect_config (PourConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[PourConfig]: return PourConfig @property def _iterator_cls(self) -> type[PourIterator]: return PourIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_print.py000066400000000000000000000323451507200677100300260ustar00rootroot00000000000000"""Prints the input data one line at at time with a carriage return and line feed. Classes: Print: Prints the input data one line at at time with a carriage return and line feed. PrintConfig: Configuration for the Print effect. PrintIterator: Effect iterator for the Print effect. Does not normally need to be called directly. """ from __future__ import annotations import typing from dataclasses import dataclass from terminaltexteffects import Color, Coord, EffectCharacter, EventHandler, Gradient, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Print, PrintConfig @argclass( name="print", help="Lines are printed one at a time following a print head. Print head performs line feed, carriage return.", description="print | Lines are printed one at a time following a print head. Print head performs line feed, " "carriage return.", epilog=( f"{argvalidators.EASING_EPILOG} Example: terminaltexteffects print --final-gradient-stops 02b8bd " "c1f0e3 00ffa0 --final-gradient-steps 12 --print-head-return-speed 1.25 --print-speed 1 " "--print-head-easing IN_OUT_QUAD" ), ) @dataclass class PrintConfig(ArgsDataClass): """Configuration for the Print effect. Attributes: print_head_return_speed (float): Speed of the print head when performing a carriage return. print_speed (int): Speed of the print head when printing characters. print_head_easing (easing.EasingFunction): Easing function to use for print head movement. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one " "color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps " "will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ print_head_return_speed: float = ArgField( cmd_name=["--print-head-return-speed"], type_parser=argvalidators.PositiveFloat.type_parser, default=1.25, metavar=argvalidators.PositiveFloat.METAVAR, help="Speed of the print head when performing a carriage return.", ) # type: ignore[assignment] "float : Speed of the print head when performing a carriage return." print_speed: int = ArgField( cmd_name=["--print-speed"], type_parser=argvalidators.PositiveInt.type_parser, default=1, metavar=argvalidators.PositiveInt.METAVAR, help="Speed of the print head when printing characters.", ) # type: ignore[assignment] "int : Speed of the print head when printing characters." print_head_easing: easing.EasingFunction = ArgField( cmd_name=["--print-head-easing"], default=easing.in_out_quad, type_parser=argvalidators.Ease.type_parser, help="Easing function to use for print head movement.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for print head movement." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("02b8bd"), Color("c1f0e3"), Color("00ffa0")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name=["--final-gradient-steps"], type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.DIAGONAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Print]: """Get the effect class associated with this configuration.""" return Print class PrintIterator(BaseEffectIterator[PrintConfig]): """Effect iterator for the Print effect.""" class Row: """Row of characters to print.""" def __init__( self, characters: list[EffectCharacter], character_final_color_map: dict[EffectCharacter, Color], typing_head_color: Color, ) -> None: """Initialize the row of characters to print. Args: characters (list[EffectCharacter]): List of characters to print. character_final_color_map (dict[EffectCharacter, Color]): Mapping of characters to their final colors. typing_head_color (Color): Color of the typing head. """ self.untyped_chars: list[EffectCharacter] = [] self.typed_chars: list[EffectCharacter] = [] if all(character.input_symbol == " " for character in characters): characters = characters[:1] else: right_extent = max( character.input_coord.column for character in characters if not character.is_fill_character ) characters = [char for char in characters if char.input_coord.column <= right_extent] for character in characters: character.motion.set_coordinate(Coord(character.input_coord.column, 1)) color_gradient = Gradient(typing_head_color, character_final_color_map[character], steps=5) typed_animation = character.animation.new_scene() typed_animation.apply_gradient_to_symbols( ("█", "▓", "▒", "░", character.input_symbol), 5, fg_gradient=color_gradient, ) character.animation.activate_scene(typed_animation) self.untyped_chars.append(character) def move_up(self) -> None: """Move the row up one row.""" for character in self.typed_chars: current_row = character.motion.current_coord.row character.motion.set_coordinate(Coord(character.motion.current_coord.column, current_row + 1)) def type_char(self) -> EffectCharacter | None: """Type the next character in the row.""" if self.untyped_chars: next_char = self.untyped_chars.pop(0) self.typed_chars.append(next_char) return next_char return None def __init__(self, effect: Print) -> None: """Initialize the iterator with the Print effect. Args: effect (Print): Print effect to apply to the input data. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.pending_rows: list[PrintIterator.Row] = [] self.processed_rows: list[PrintIterator.Row] = [] self.typing_head = self.terminal.add_character("█", Coord(1, 1)) self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the initial state of the effect.""" self.final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = self.final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(outer_fill_chars=True, inner_fill_chars=True): self.character_final_color_map[character] = final_gradient_mapping.get( character.input_coord, Color("ffffff"), ) input_rows = self.terminal.get_characters_grouped( grouping=self.terminal.CharacterGroup.ROW_TOP_TO_BOTTOM, outer_fill_chars=True, inner_fill_chars=True, ) for input_row in input_rows: self.pending_rows.append( PrintIterator.Row( input_row, self.character_final_color_map, Color("ffffff"), ), ) self._current_row: PrintIterator.Row = self.pending_rows.pop(0) self._typing = True self._last_column = 0 def __next__(self) -> str: """Return the next frame in the animation.""" if self.active_characters or self._typing: if self.typing_head.motion.active_path: pass elif self._current_row.untyped_chars: for _ in range(min(len(self._current_row.untyped_chars), self.config.print_speed)): next_char = self._current_row.type_char() if next_char: self.terminal.set_character_visibility(next_char, is_visible=True) self.active_characters.add(next_char) self._last_column = next_char.input_coord.column else: self.processed_rows.append(self._current_row) if self.pending_rows: for row in self.processed_rows: row.move_up() self._current_row = self.pending_rows.pop(0) if not all( character.is_fill_character for character in self.processed_rows[-1].typed_chars ) and not all(character.is_fill_character for character in self._current_row.untyped_chars): left_extent = min( [ character.input_coord.column for character in self._current_row.untyped_chars if not character.is_fill_character ], ) self._current_row.untyped_chars = [ char for char in self._current_row.untyped_chars if left_extent <= char.input_coord.column <= self.terminal.canvas.text_right ] self.typing_head.motion.set_coordinate(Coord(self._last_column, 1)) self.terminal.set_character_visibility(self.typing_head, is_visible=True) self.typing_head.motion.paths.clear() carriage_return_path = self.typing_head.motion.new_path( speed=self.config.print_head_return_speed, ease=self.config.print_head_easing, path_id="carriage_return_path", ) carriage_return_path.new_waypoint( Coord(self._current_row.untyped_chars[0].input_coord.column, 1), ) self.typing_head.motion.activate_path(carriage_return_path) self.typing_head.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, carriage_return_path, EventHandler.Action.CALLBACK, EventHandler.Callback(self.terminal.set_character_visibility, False), # noqa: FBT003 ) self.active_characters.add(self.typing_head) else: self._typing = False self.update() return self.frame raise StopIteration class Print(BaseEffect[PrintConfig]): """Prints the input data one line at at time with a carriage return and line feed. Attributes: effect_config (PrintConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal """ @property def _config_cls(self) -> type[PrintConfig]: return PrintConfig @property def _iterator_cls(self) -> type[PrintIterator]: return PrintIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_rain.py000066400000000000000000000244351507200677100276240ustar00rootroot00000000000000"""Rain characters from the top of the canvas. Classes: Rain: Rain characters from the top of the canvas. RainConfig: Configuration for the Rain effect. RainIterator: Iterator for the Rain effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, ColorPair, Coord, EffectCharacter, Gradient, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Rain, RainConfig @argclass( name="rain", help="Rain characters from the top of the canvas.", description="rain | Rain characters from the top of the canvas.", epilog=( f"{argvalidators.EASING_EPILOG} Example: terminaltexteffects rain --rain-symbols o . , '*' '|' " "--rain-colors 00315C 004C8F 0075DB 3F91D9 78B9F2 9AC8F5 B8D8F8 E3EFFC --final-gradient-stops " "488bff b2e7de 57eaf7 --final-gradient-steps 12 --movement-speed 0.1-0.2 --easing IN_QUART" ), ) @dataclass class RainConfig(ArgsDataClass): """Configuration for the Rain effect. Attributes: rain_colors (tuple[Color, ...]): Tuple of colors for the rain drops. Colors are randomly chosen from the tuple. movement_speed (tuple[float, float]): Falling speed range of the rain drops. Valid values are n > 0. rain_symbols (tuple[str, ...] | str): Tuple of symbols to use for the rain drops. Symbols are randomly chosen " "from the tuple. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is " "provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. movement_easing (easing.EasingFunction): Easing function to use for character movement. """ rain_colors: tuple[Color, ...] = ArgField( cmd_name=["--rain-colors"], type_parser=argvalidators.ColorArg.type_parser, metavar=argvalidators.ColorArg.METAVAR, nargs="+", default=( Color("00315C"), Color("004C8F"), Color("0075DB"), Color("3F91D9"), Color("78B9F2"), Color("9AC8F5"), Color("B8D8F8"), Color("E3EFFC"), ), help="List of colors for the rain drops. Colors are randomly chosen from the list.", ) # type: ignore[assignment] "tuple[Color, ...] : Tuple of colors for the rain drops. Colors are randomly chosen from the tuple." movement_speed: tuple[float, float] = ArgField( cmd_name="--movement-speed", type_parser=argvalidators.PositiveFloatRange.type_parser, default=(0.1, 0.2), metavar=argvalidators.PositiveFloatRange.METAVAR, help="Falling speed range of the rain drops.", ) # type: ignore[assignment] "tuple[float, float] : Falling speed range of the rain drops." rain_symbols: tuple[str, ...] = ArgField( cmd_name="--rain-symbols", type_parser=argvalidators.Symbol.type_parser, nargs="+", default=("o", ".", ",", "*", "|"), metavar=argvalidators.Symbol.METAVAR, help="Space separated list of symbols to use for the rain drops. Symbols are randomly chosen from the list.", ) # type: ignore[assignment] "tuple[str, ...] : Tuple of symbols to use for the rain drops. Symbols are randomly chosen from the tuple." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name="--final-gradient-stops", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("488bff"), Color("b2e7de"), Color("57eaf7")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If " "only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.DIAGONAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." movement_easing: easing.EasingFunction = ArgField( cmd_name=["--movement-easing"], default=easing.in_quart, type_parser=argvalidators.Ease.type_parser, metavar=argvalidators.Ease.METAVAR, help="Easing function to use for character movement.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for character movement." @classmethod def get_effect_class(cls) -> type[Rain]: """Get the effect class associated with this configuration.""" return Rain class RainIterator(BaseEffectIterator[RainConfig]): """Iterator for the Rain effect.""" def __init__(self, effect: Rain) -> None: """Initialize the iterator with the provided effect. Args: effect (Rain): The effect to use for the iterator. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.group_by_row: dict[int, list[EffectCharacter | None]] = {} self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the rain effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] for character in self.terminal.get_characters(): raindrop_color = random.choice(self.config.rain_colors) rain_scn = character.animation.new_scene() rain_scn.add_frame(random.choice(self.config.rain_symbols), 1, colors=ColorPair(fg=raindrop_color)) raindrop_gradient = Gradient(raindrop_color, self.character_final_color_map[character], steps=7) fade_scn = character.animation.new_scene() fade_scn.apply_gradient_to_symbols(character.input_symbol, 5, fg_gradient=raindrop_gradient) character.animation.activate_scene(rain_scn) character.motion.set_coordinate(Coord(character.input_coord.column, self.terminal.canvas.top)) input_path = character.motion.new_path( speed=random.uniform(self.config.movement_speed[0], self.config.movement_speed[1]), ease=self.config.movement_easing, ) input_path.new_waypoint(character.input_coord) character.event_handler.register_event( character.event_handler.Event.PATH_COMPLETE, input_path, character.event_handler.Action.ACTIVATE_SCENE, fade_scn, ) character.motion.activate_path(input_path) self.pending_chars.append(character) for character in sorted(self.pending_chars, key=lambda c: c.input_coord.row): if character.input_coord.row not in self.group_by_row: self.group_by_row[character.input_coord.row] = [] self.group_by_row[character.input_coord.row].append(character) self.pending_chars.clear() def __next__(self) -> str: """Return the next frame in the animation.""" if self.group_by_row or self.active_characters or self.pending_chars: if not self.pending_chars and self.group_by_row: self.pending_chars.extend(self.group_by_row.pop(min(self.group_by_row.keys()))) # type: ignore[arg-type] if self.pending_chars: for _ in range(random.randint(1, 3)): if self.pending_chars: next_character = self.pending_chars.pop(random.randint(0, len(self.pending_chars) - 1)) self.terminal.set_character_visibility(next_character, is_visible=True) self.active_characters.add(next_character) else: break self.update() return self.frame raise StopIteration class Rain(BaseEffect[RainConfig]): """Rain characters from the top of the canvas. Attributes: effect_config (PourConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[RainConfig]: return RainConfig @property def _iterator_cls(self) -> type[RainIterator]: return RainIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_random_sequence.py000066400000000000000000000204321507200677100320340ustar00rootroot00000000000000"""Prints the input data in a random sequence, one character at a time. Classes: RandomSequence: Prints the input data in a random sequence. RandomSequenceConfig: Configuration for the RandomSequence effect. RandomSequenceIterator: Iterator for the RandomSequence effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, EffectCharacter, Gradient from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return RandomSequence, RandomSequenceConfig @argclass( name="randomsequence", help="Prints the input data in a random sequence.", description="randomsequence | Prints the input data in a random sequence.", epilog=( "Example: terminaltexteffects randomsequence --starting-color 000000 --final-gradient-stops 8A008A 00D1FF " "FFFFFF --final-gradient-steps 12 --final-gradient-frames 12 --speed 0.004" ), ) @dataclass class RandomSequenceConfig(ArgsDataClass): """Configuration for the RandomSequence effect. Attributes: starting_color (Color): Color of the characters at spawn. speed (float): Speed of the animation as a percentage of the total number of characters to reveal in each tick. Valid values are 0 < n <= 1. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_frames (int): Number of frames to display each gradient step. Increase to slow down the gradient animation. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ starting_color: Color = ArgField( cmd_name=["--starting-color"], type_parser=argvalidators.ColorArg.type_parser, default=Color("000000"), metavar=argvalidators.ColorArg.METAVAR, help="Color of the characters at spawn.", ) # type: ignore[assignment] "Color : Color of the characters at spawn." speed: float = ArgField( cmd_name=["--speed"], type_parser=argvalidators.PositiveFloat.type_parser, default=0.004, metavar=argvalidators.PositiveFloat.METAVAR, help="Speed of the animation as a percentage of the total number of characters to reveal in each tick.", ) # type: ignore[assignment] "float : Speed of the animation as a percentage of the total number of characters to reveal in each tick." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("8A008A"), Color("00D1FF"), Color("FFFFFF")), metavar=argvalidators.ColorArg.METAVAR, help=( "Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color." ), ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. " "If only one color is provided, the characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name=["--final-gradient-steps"], type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help=( "Space separated, unquoted, list of the number of gradient steps to use. " "More steps will create a smoother and longer gradient animation." ), ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. " "More steps will create a smoother and longer gradient animation." ) final_gradient_frames: int = ArgField( cmd_name=["--final-gradient-frames"], type_parser=argvalidators.PositiveInt.type_parser, default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to display each gradient step. Increase to slow down the gradient animation.", ) # type: ignore[assignment] "int : Number of frames to display each gradient step. Increase to slow down the gradient animation." final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[RandomSequence]: """Get the effect class associated with this configuration.""" return RandomSequence class RandomSequenceIterator(BaseEffectIterator[RandomSequenceConfig]): """Iterator for the RandomSequence effect.""" def __init__(self, effect: RandomSequence) -> None: """Initialize the effect iterator. Args: effect (RandomSequence): The effect to use for the iterator. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.characters_per_tick = max(int(self.config.speed * len(self.terminal._input_characters)), 1) self.build() def build(self) -> None: """Build the initial state of the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] self.terminal.set_character_visibility(character, is_visible=False) gradient_scn = character.animation.new_scene() gradient = Gradient(self.config.starting_color, self.character_final_color_map[character], steps=7) gradient_scn.apply_gradient_to_symbols( character.input_symbol, self.config.final_gradient_frames, fg_gradient=gradient, ) character.animation.activate_scene(gradient_scn) self.pending_chars.append(character) random.shuffle(self.pending_chars) def __next__(self) -> str: """Return the next frame in the animation.""" if self.pending_chars or self.active_characters: for _ in range(self.characters_per_tick): if self.pending_chars: next_char = self.pending_chars.pop() self.terminal.set_character_visibility(next_char, is_visible=True) self.active_characters.add(next_char) self.update() return self.frame raise StopIteration class RandomSequence(BaseEffect[RandomSequenceConfig]): """Prints the input data in a random sequence, one character at a time. Attributes: effect_config (PourConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[RandomSequenceConfig]: return RandomSequenceConfig @property def _iterator_cls(self) -> type[RandomSequenceIterator]: return RandomSequenceIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_rings.py000066400000000000000000000507371507200677100300210ustar00rootroot00000000000000"""Characters are dispersed and form into spinning rings. Classes: Rings: Characters are dispersed and form into spinning rings. RingsConfig: Configuration for the Rings effect. RingsIterator: Iterates over the effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, ColorPair, Coord, EffectCharacter, EventHandler, Gradient, easing, geometry from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass if typing.TYPE_CHECKING: from terminaltexteffects.engine import motion def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Rings, RingsConfig @argclass( name="rings", help="Characters are dispersed and form into spinning rings.", description="rings | Characters are dispersed and form into spinning rings.", epilog=( "Example: terminaltexteffects rings --ring-colors ab48ff e7b2b2 fffebd --final-gradient-stops ab48ff " "e7b2b2 fffebd --final-gradient-steps 12 --ring-gap 0.1 --spin-duration 200 --spin-speed 0.25-1.0 " "--disperse-duration 200 --spin-disperse-cycles 3" ), ) @dataclass class RingsConfig(ArgsDataClass): """Configurations for the RingsEffect. Attributes: ring_colors (tuple[Color, ...]): Tuple of colors for the rings. ring_gap (float): Distance between rings as a percent of the smallest canvas dimension. " "Valid values are 0 < n <= 1. spin_duration (int): Number of frames for each cycle of the spin phase. Valid values are n >= 0. spin_speed (tuple[float, float]): Range of speeds for the rotation of the rings. The speed is randomly " "selected from this range for each ring. Valid values are n > 0. disperse_duration (int): Number of frames spent in the dispersed state between spinning cycles. " "Valid values are n >= 0. spin_disperse_cycles (int): Number of times the animation will cycle between spinning rings and " "dispersed characters. Valid values are n > 0. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color " "is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Number of gradient steps to use. More steps will create a " "smoother and longer gradient animation. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ ring_colors: tuple[Color, ...] = ArgField( cmd_name=["--ring-colors"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("ab48ff"), Color("e7b2b2"), Color("fffebd")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the rings.", ) # type: ignore[assignment] "tuple[Color] : Tuple of colors for the rings." ring_gap: float = ArgField( cmd_name=["--ring-gap"], type_parser=argvalidators.PositiveFloat.type_parser, default=0.1, help="Distance between rings as a percent of the smallest canvas dimension.", ) # type: ignore[assignment] "float : Distance between rings as a percent of the smallest canvas dimension." spin_duration: int = ArgField( cmd_name=["--spin-duration"], type_parser=argvalidators.PositiveInt.type_parser, default=200, help="Number of frames for each cycle of the spin phase.", ) # type: ignore[assignment] "int : Number of frames for each cycle of the spin phase." spin_speed: tuple[float, float] = ArgField( cmd_name=["--spin-speed"], type_parser=argvalidators.PositiveFloatRange.type_parser, default=(0.25, 1.0), metavar=argvalidators.PositiveFloatRange.METAVAR, help="Range of speeds for the rotation of the rings. The speed is randomly selected from this " "range for each ring.", ) # type: ignore[assignment] ( "tuple[float, float] : Range of speeds for the rotation of the rings. The speed is randomly selected " "from this range for each ring." ) disperse_duration: int = ArgField( cmd_name=["--disperse-duration"], type_parser=argvalidators.PositiveInt.type_parser, default=200, help="Number of frames spent in the dispersed state between spinning cycles.", ) # type: ignore[assignment] "int : Number of frames spent in the dispersed state between spinning cycles." spin_disperse_cycles: int = ArgField( cmd_name=["--spin-disperse-cycles"], type_parser=argvalidators.PositiveInt.type_parser, default=3, help="Number of times the animation will cycles between spinning rings and dispersed characters.", ) # type: ignore[assignment] "int : Number of times the animation will cycles between spinning rings and dispersed characters." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("ab48ff"), Color("e7b2b2"), Color("fffebd")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color] : Tuple of colors for the final color gradient. If only one color is provided, the characters " "will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name=["--final-gradient-steps"], type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Number of gradient steps to use. More steps will create a smoother and longer " "gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Rings]: """Get the effect class associated with this configuration.""" return Rings class RingsIterator(BaseEffectIterator[RingsConfig]): """Iterator for the Rings effect.""" class Ring: """A ring of characters that spin around a central point.""" def __init__( self, config: RingsConfig, radius: int, origin: Coord, ring_coords: list[Coord], ring_gap: int, ring_color: Color, character_color_map: dict[EffectCharacter, Color], ) -> None: """Initialize the ring. Args: config (RingsConfig): Configuration for the effect. radius (int): Radius of the ring. origin (Coord): Center of the ring. ring_coords (list[Coord]): Coordinates of the ring. ring_gap (int): Distance between rings. ring_color (Color): Color of the ring. character_color_map (dict[EffectCharacter, Color]): Mapping of characters to colors. """ self.config = config self._built = False self.radius = radius self.origin: Coord = origin self.counter_clockwise_coords = ring_coords self.clockwise_coords = ring_coords[::-1] self.ring_gap = ring_gap self.ring_color = ring_color self.characters: list[EffectCharacter] = [] self.character_last_ring_path: dict[EffectCharacter, motion.Path] = {} self.rotations = 0 self.rotation_speed = random.uniform(self.config.spin_speed[0], self.config.spin_speed[1]) self.character_color_map = character_color_map def add_character(self, character: EffectCharacter, clockwise: int) -> None: """Add a character to the ring.""" # make gradient scene gradient_scn = character.animation.new_scene(scene_id="gradient") char_gradient = Gradient(self.character_color_map[character], self.ring_color, steps=8) gradient_scn.apply_gradient_to_symbols(character.input_symbol, 5, fg_gradient=char_gradient) # make rotation waypoints ring_paths: list[motion.Path] = [] character_starting_index = len(self.characters) coords = self.clockwise_coords if clockwise else self.counter_clockwise_coords for coord in coords[character_starting_index:] + coords[:character_starting_index]: ring_path = character.motion.new_path(path_id=str(len(ring_paths)), speed=self.rotation_speed) ring_path.new_waypoint(coord, waypoint_id=str(len(ring_path.waypoints))) ring_paths.append(ring_path) self.character_last_ring_path[character] = ring_paths[0] # make disperse scene disperse_scn = character.animation.new_scene(is_looping=False, scene_id="disperse") disperse_gradient = Gradient(self.ring_color, self.character_color_map[character], steps=8) disperse_scn.apply_gradient_to_symbols(character.input_symbol, 16, fg_gradient=disperse_gradient) character.motion.chain_paths(ring_paths, loop=True) self.characters.append(character) def make_disperse_waypoints(self, character: EffectCharacter, origin: Coord) -> motion.Path: """Make waypoints for the disperse path. Args: character (EffectCharacter): Character to disperse. origin (Coord): Origin of the disperse path. Returns: motion.Path: Disperse path. """ disperse_coords = geometry.find_coords_in_rect(origin, self.ring_gap) character.motion.paths.pop("disperse", None) disperse_path = character.motion.new_path(speed=0.1, loop=True, path_id="disperse") for _ in range(5): disperse_path.new_waypoint(disperse_coords[random.randrange(0, len(disperse_coords))]) return disperse_path def disperse(self) -> None: """Disperse the characters.""" for character in self.characters: if character.motion.active_path is not None: self.character_last_ring_path[character] = character.motion.active_path else: self.character_last_ring_path[character] = character.motion.paths["0"] character.motion.activate_path(self.make_disperse_waypoints(character, character.motion.current_coord)) character.animation.activate_scene(character.animation.query_scene("disperse")) def spin(self) -> None: """Spin the ring.""" for character in self.characters: condense_path = character.motion.new_path(speed=0.1) condense_path.new_waypoint(self.character_last_ring_path[character].waypoints[0].coord) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, condense_path, EventHandler.Action.ACTIVATE_PATH, self.character_last_ring_path[character], ) character.motion.activate_path(condense_path) character.animation.activate_scene(character.animation.query_scene("gradient")) def __init__(self, effect: Rings) -> None: """Initialize the effect iterator.""" super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.ring_chars: list[EffectCharacter] = [] self.non_ring_chars: list[EffectCharacter] = [] self.rings: dict[int, RingsIterator.Ring] = {} self.ring_gap = int( max(round(min(self.terminal.canvas.top, self.terminal.canvas.right) * self.config.ring_gap), 1), ) self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the initial state of the effect.""" self.ring_gap = int( max(round(min(self.terminal.canvas.top, self.terminal.canvas.right) * self.config.ring_gap), 1), ) final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] start_scn = character.animation.new_scene() start_scn.add_frame( character.input_symbol, 1, colors=ColorPair(fg=self.character_final_color_map[character]), ) home_path = character.motion.new_path(speed=0.8, ease=easing.out_quad, path_id="home") home_path.new_waypoint(character.input_coord) character.animation.activate_scene(start_scn) self.terminal.set_character_visibility(character, is_visible=True) self.pending_chars.append(character) random.shuffle(self.pending_chars) # make rings for radius in range(1, max(self.terminal.canvas.right, self.terminal.canvas.top), self.ring_gap): ring_coords = geometry.find_coords_on_circle(self.terminal.canvas.center, radius, 7 * radius, unique=True) # check if any part of the ring is in the canvas, if not, stop creating rings if ( len([coord for coord in ring_coords if self.terminal.canvas.coord_is_in_canvas(coord)]) / len(ring_coords) < 0.25 ): break self.rings[radius] = RingsIterator.Ring( self.config, radius, self.terminal.canvas.center, ring_coords, self.ring_gap, self.config.ring_colors[len(self.rings) % len(self.config.ring_colors)], self.character_final_color_map, ) # assign characters to rings for ring_count, ring in enumerate(self.rings.values()): for _ in ring.counter_clockwise_coords: if self.pending_chars: next_character = self.pending_chars.pop(0) # set rings to rotate in opposite directions ring.add_character(next_character, clockwise=ring_count % 2) self.ring_chars.append(next_character) # make external waypoints for characters not in rings for character in self.terminal.get_characters(): if character not in self.ring_chars: external_path = character.motion.new_path(path_id="external", speed=0.8, ease=easing.out_sine) external_path.new_waypoint(self.terminal.canvas.random_coord(outside_scope=True)) self.non_ring_chars.append(character) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, external_path, EventHandler.Action.CALLBACK, EventHandler.Callback(self.terminal.set_character_visibility, False), # noqa: FBT003 ) self._rings_list = list(self.rings.values()) self._phase = "start" self._initial_disperse_complete = False self._spin_time_remaining = self.config.spin_duration self._disperse_time_remaining = self.config.disperse_duration self._cycles_remaining = self.config.spin_disperse_cycles self._initial_phase_time_remaining = 100 def __next__(self) -> str: """Return the next frame in the animation.""" if self._phase != "complete": if self._phase == "start": if not self._initial_phase_time_remaining: self._phase = "disperse" else: self._initial_phase_time_remaining -= 1 elif self._phase == "disperse": if not self._initial_disperse_complete: self._initial_disperse_complete = True for ring in self._rings_list: for character in ring.characters: disperse_path = ring.make_disperse_waypoints( character, character.motion.paths["0"].waypoints[0].coord, ) initial_path = character.motion.new_path(speed=0.3, ease=easing.out_cubic) initial_path.new_waypoint(disperse_path.waypoints[0].coord) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, initial_path, EventHandler.Action.ACTIVATE_PATH, disperse_path, ) character.animation.activate_scene(character.animation.query_scene("disperse")) character.motion.activate_path(initial_path) self.active_characters.add(character) for character in self.non_ring_chars: character.motion.activate_path(character.motion.query_path("external")) self.active_characters.add(character) elif not self._disperse_time_remaining: self._phase = "spin" self._cycles_remaining -= 1 self._spin_time_remaining = self.config.spin_duration for ring in self._rings_list: ring.spin() else: self._disperse_time_remaining -= 1 elif self._phase == "spin": if not self._spin_time_remaining: if not self._cycles_remaining: self._phase = "final" for character in self.terminal.get_characters(): self.terminal.set_character_visibility(character, is_visible=True) character.motion.activate_path(character.motion.query_path("home")) self.active_characters.add(character) if "external" in character.motion.paths: continue character.animation.activate_scene(character.animation.query_scene("disperse")) else: self._disperse_time_remaining = self.config.disperse_duration for ring in self._rings_list: ring.disperse() self._phase = "disperse" else: self._spin_time_remaining -= 1 elif self._phase == "final" and not self.active_characters: self._phase = "complete" self.update() return self.frame raise StopIteration class Rings(BaseEffect[RingsConfig]): """Characters are dispersed and form into spinning rings. Attributes: effect_config (RingsConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[RingsConfig]: return RingsConfig @property def _iterator_cls(self) -> type[RingsIterator]: return RingsIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_scattered.py000066400000000000000000000211121507200677100306360ustar00rootroot00000000000000"""Text is scattered across the canvas and moves into position. Classes: Scattered: Move the characters into place from random starting locations. ScatteredConfig: Configuration for the Scattered effect. ScatteredIterator: Effect iterator for the effect. Does not normally need to be called directly. """ from __future__ import annotations import typing from dataclasses import dataclass from terminaltexteffects import Color, Coord, EffectCharacter, EventHandler, Gradient, Scene, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Scattered, ScatteredConfig @argclass( name="scattered", help="Text is scattered across the canvas and moves into position.", description="scattered | Text is scattered across the canvas and moves into position.", epilog=( f"{argvalidators.EASING_EPILOG} Example: terminaltexteffects scattered --final-gradient-stops ff9048 " "ab9dff bdffea --final-gradient-steps 12 --final-gradient-frames 12 --movement-speed 0.5 " "--movement-easing IN_OUT_BACK" ), ) @dataclass class ScatteredConfig(ArgsDataClass): """Configuration for the effect. Attributes: movement_speed (float): Movement speed of the characters. Valid values are n > 0. movement_easing (easing.EasingFunction): Easing function to use for character movement. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the character gradient. If only one color is " "provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_frames (int): Number of frames to display each gradient step. Increase to slow down the " "gradient animation. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ movement_speed: float = ArgField( cmd_name="--movement-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=0.3, metavar=argvalidators.PositiveFloat.METAVAR, help="Movement speed of the characters. ", ) # type: ignore[assignment] "float : Movement speed of the characters. " movement_easing: easing.EasingFunction = ArgField( cmd_name="--movement-easing", default=easing.in_out_back, type_parser=argvalidators.Ease.type_parser, help="Easing function to use for character movement.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for character movement." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("ff9048"), Color("ab9dff"), Color("bdffea")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient. If only one color is provided, " "the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the character gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Number of gradient steps to use. More steps will create a smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will create " "a smoother and longer gradient animation." ) final_gradient_frames: int = ArgField( cmd_name="--final-gradient-frames", type_parser=argvalidators.PositiveInt.type_parser, default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to display each gradient step. Increase to slow down the gradient animation.", ) # type: ignore[assignment] "int : Number of frames to display each gradient step. Increase to slow down the gradient animation." final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Scattered]: """Get the effect class associated with this configuration.""" return Scattered class ScatteredIterator(BaseEffectIterator[ScatteredConfig]): """Effect iterator for the effect.""" def __init__(self, effect: Scattered) -> None: """Initialize the effect iterator.""" super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] if self.terminal.canvas.right < 2 or self.terminal.canvas.top < 2: character.motion.set_coordinate(Coord(1, 1)) else: character.motion.set_coordinate(self.terminal.canvas.random_coord()) input_coord_path = character.motion.new_path( speed=self.config.movement_speed, ease=self.config.movement_easing, ) input_coord_path.new_waypoint(character.input_coord) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, input_coord_path, EventHandler.Action.SET_LAYER, 1, ) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, input_coord_path, EventHandler.Action.SET_LAYER, 0, ) character.motion.activate_path(input_coord_path) self.terminal.set_character_visibility(character, is_visible=True) gradient_scn = character.animation.new_scene(sync=Scene.SyncMetric.DISTANCE) char_gradient = Gradient(final_gradient.spectrum[0], self.character_final_color_map[character], steps=10) gradient_scn.apply_gradient_to_symbols( character.input_symbol, self.config.final_gradient_frames, fg_gradient=char_gradient, ) character.animation.activate_scene(gradient_scn) self.active_characters.add(character) self._initial_hold_frames = 25 def __next__(self) -> str: """Return the next frame in the animation.""" if self.pending_chars or self.active_characters: if self._initial_hold_frames: self._initial_hold_frames -= 1 return self.frame self.update() return self.frame raise StopIteration class Scattered(BaseEffect[ScatteredConfig]): """Text is scattered across the canvas and moves into position. Attributes: effect_config (ScatteredConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[ScatteredConfig]: return ScatteredConfig @property def _iterator_cls(self) -> type[ScatteredIterator]: return ScatteredIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_slice.py000066400000000000000000000332761507200677100277750ustar00rootroot00000000000000"""Slices the input in half and slides it into place from opposite directions. Classes: Slice: Slices the input in half and slides it into place from opposite directions. SliceConfig: Configuration for the Slice effect. SliceIterator: Effect iterator for the effect. Does not normally need to be called directly. """ from __future__ import annotations import typing from dataclasses import dataclass from terminaltexteffects import Color, ColorPair, Coord, EffectCharacter, Gradient, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Slice, SliceConfig @argclass( name="slice", help="Slices the input in half and slides it into place from opposite directions.", description="slice | Slices the input in half and slides it into place from opposite directions.", epilog=( f"{argvalidators.EASING_EPILOG} Example: terminaltexteffects slice --final-gradient-stops 8A008A 00D1FF " "FFFFFF --final-gradient-steps 12 --slice-direction vertical--movement-speed 0.15 " "--movement-easing IN_OUT_EXPO" ), ) @dataclass class SliceConfig(ArgsDataClass): """Configuration for the Slice effect. Attributes: slice_direction (typing.Literal["vertical", "horizontal", "diagonal"]): Direction of the slice. movement_speed (float): Movement speed of the characters. Valid values are n > 0. movement_easing (easing.EasingFunction): Easing function to use for character movement. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ slice_direction: typing.Literal["vertical", "horizontal", "diagonal"] = ArgField( cmd_name="--slice-direction", default="vertical", choices=["vertical", "horizontal", "diagonal"], help="Direction of the slice.", ) # type: ignore[assignment] "typing.Literal['vertical', 'horizontal', 'diagonal'] : Direction of the slice." movement_speed: float = ArgField( cmd_name="--movement-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=0.15, metavar=argvalidators.PositiveFloat.METAVAR, help="Movement speed of the characters. ", ) # type: ignore[assignment] "float : Movement speed of the characters. Doubled for horizontal slices." movement_easing: easing.EasingFunction = ArgField( cmd_name="--movement-easing", type_parser=argvalidators.Ease.type_parser, default=easing.in_out_expo, help="Easing function to use for character movement.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for character movement." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("8A008A"), Color("00D1FF"), Color("FFFFFF")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If " "only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.DIAGONAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Slice]: """Get the effect class associated with this configuration.""" return Slice class SliceIterator(BaseEffectIterator[SliceConfig]): """Effect iterator for the Slice effect.""" def __init__(self, effect: Slice) -> None: """Initialize the effect iterator.""" super().__init__(effect) self.pending_groups: list[list[EffectCharacter]] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: # noqa: PLR0915 """Build the effect.""" slice_direction_map = { "vertical": self.terminal.CharacterGroup.ROW_BOTTOM_TO_TOP, "horizontal": self.terminal.CharacterGroup.COLUMN_RIGHT_TO_LEFT, "diagonal": self.terminal.CharacterGroup.DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT, } final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] character.animation.set_appearance( character.input_symbol, ColorPair(fg=self.character_final_color_map[character]), ) if self.config.slice_direction == "vertical": self.rows = self.terminal.get_characters_grouped(grouping=slice_direction_map[self.config.slice_direction]) for row_index, row in enumerate(self.rows): new_row = [] left_half = [ character for character in row if character.input_coord.column <= self.terminal.canvas.text_center_column ] for character in left_half: character.motion.set_coordinate(Coord(character.input_coord.column, self.terminal.canvas.top + 1)) input_coord_path = character.motion.new_path( speed=self.config.movement_speed, ease=self.config.movement_easing, ) input_coord_path.new_waypoint(character.input_coord) character.motion.activate_path(input_coord_path) opposite_row = self.rows[-(row_index + 1)] right_half = [c for c in opposite_row if c.input_coord.column > self.terminal.canvas.text_center_column] for character in right_half: character.motion.set_coordinate( Coord(character.input_coord.column, self.terminal.canvas.bottom - 1), ) input_coord_path = character.motion.new_path( speed=self.config.movement_speed, ease=self.config.movement_easing, ) input_coord_path.new_waypoint(character.input_coord) character.motion.activate_path(input_coord_path) new_row.extend(left_half) new_row.extend(right_half) self.active_characters = self.active_characters.union(new_row) elif self.config.slice_direction == "horizontal": self.config.movement_speed *= 2 self.columns = self.terminal.get_characters_grouped( grouping=slice_direction_map[self.config.slice_direction], outer_fill_chars=True, inner_fill_chars=True, ) trimmed_columns = [] for column in self.columns: new_column = [ character for character in column if ( self.terminal.canvas.text_left <= character.input_coord.column <= self.terminal.canvas.text_right ) and (self.terminal.canvas.text_bottom <= character.input_coord.row <= self.terminal.canvas.text_top) ] if new_column: trimmed_columns.append(new_column) self.columns = trimmed_columns mid_point = self.terminal.canvas.text_center_row for column_index, column in enumerate(self.columns): new_column = [] bottom_half = [character for character in column if character.input_coord.row <= mid_point] for character in bottom_half: character.motion.set_coordinate(Coord(self.terminal.canvas.left - 1, character.input_coord.row)) input_coord_path = character.motion.new_path( speed=self.config.movement_speed, ease=self.config.movement_easing, ) input_coord_path.new_waypoint(character.input_coord) character.motion.activate_path(input_coord_path) opposite_column = self.columns[-(column_index + 1)] top_half = [c for c in opposite_column if c.input_coord.row > mid_point] for character in top_half: character.motion.set_coordinate(Coord(self.terminal.canvas.right + 1, character.input_coord.row)) input_coord_path = character.motion.new_path( speed=self.config.movement_speed, ease=self.config.movement_easing, ) input_coord_path.new_waypoint(character.input_coord) character.motion.activate_path(input_coord_path) new_column.extend(bottom_half) new_column.extend(top_half) self.active_characters = self.active_characters.union(new_column) elif self.config.slice_direction == "diagonal": self.diagonals = self.terminal.get_characters_grouped( grouping=slice_direction_map[self.config.slice_direction], ) left = self.diagonals[: len(self.diagonals) // 2] right = self.diagonals[len(self.diagonals) // 2 :] while left or right: new_group = [] if left: left_group = left.pop(0) origin_coord = Coord(left_group[0].input_coord.column, self.terminal.canvas.bottom - 1) for character in left_group: character.motion.set_coordinate(origin_coord) input_coord_path = character.motion.new_path( speed=self.config.movement_speed, ease=self.config.movement_easing, ) input_coord_path.new_waypoint(character.input_coord) character.motion.activate_path(input_coord_path) new_group.extend(left_group) if right: right_group = right.pop(0) origin_coord = Coord(right_group[-1].input_coord.column, self.terminal.canvas.top + 1) for character in right_group: character.motion.set_coordinate(origin_coord) input_coord_path = character.motion.new_path( speed=self.config.movement_speed, ease=self.config.movement_easing, ) input_coord_path.new_waypoint(character.input_coord) character.motion.activate_path(input_coord_path) new_group.extend(right_group) self.active_characters = self.active_characters.union(new_group) for character in self.active_characters: self.terminal.set_character_visibility(character, is_visible=True) def __next__(self) -> str: """Return the next frame in the animation.""" if self.active_characters: self.update() return self.frame raise StopIteration class Slice(BaseEffect[SliceConfig]): """Slices the input in half and slides it into place from opposite directions. Attributes: effect_config (SliceConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[SliceConfig]: return SliceConfig @property def _iterator_cls(self) -> type[SliceIterator]: return SliceIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_slide.py000066400000000000000000000341111507200677100277630ustar00rootroot00000000000000"""Slide characters into view from outside the terminal. Classes: Slide: Slide characters into view from outside the terminal. SlideConfig: Configuration for the Slide effect. SlideIterator: Effect iterator for the Slide effect. Does not normally need to be called directly. """ from __future__ import annotations import typing from dataclasses import dataclass from terminaltexteffects import Color, EffectCharacter, Gradient, easing, geometry from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Slide, SlideConfig @argclass( name="slide", help="Slide characters into view from outside the terminal.", description="slide | Slide characters into view from outside the terminal, grouped by row, column, or diagonal.", epilog=( f"{argvalidators.EASING_EPILOG} Example: terminaltexteffects slide --movement-speed 0.5 --grouping row " "--final-gradient-stops 833ab4 fd1d1d fcb045 --final-gradient-steps 12 --final-gradient-frames 10 " "--final-gradient-direction vertical --gap 3 --reverse-direction --merge --movement-easing OUT_QUAD" ), ) @dataclass class SlideConfig(ArgsDataClass): """Configuration for the Slide effect. Attributes: movement_speed (float): Speed of the characters. Valid values are n > 0. grouping (typing.Literal["row", "column", "diagonal"]): Direction to group characters. Valid values are 'row', 'column', 'diagonal'. gap (int): Number of frames to wait before adding the next group of characters. Increasing this value creates a more staggered effect. Valid values are n >= 0. reverse_direction (bool): Reverse the direction of the characters. merge (bool): Merge the character groups originating. movement_easing (easing.EasingFunction): Easing function to use for character movement. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the character gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_frames (int): Number of frames to display each gradient step. Increase to slow down the gradient animation. final_gradient_direction (Gradient.Direction): Direction of the gradient. """ movement_speed: float = ArgField( cmd_name="--movement-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=0.5, metavar=argvalidators.PositiveFloat.METAVAR, help="Speed of the characters.", ) # type: ignore[assignment] "float : Speed of the characters." grouping: typing.Literal["row", "column", "diagonal"] = ArgField( cmd_name="--grouping", default="row", choices=["row", "column", "diagonal"], help="Direction to group characters.", ) # type: ignore[assignment] ( "typing.Literal['row', 'column', 'diagonal'] : Direction to group characters. Valid values are " "Literal['row', 'column', 'diagonal']." ) gap: int = ArgField( cmd_name="--gap", type_parser=argvalidators.NonNegativeInt.type_parser, default=3, metavar=argvalidators.NonNegativeInt.METAVAR, help="Number of frames to wait before adding the next group of characters. Increasing this value creates a " "more staggered effect.", ) # type: ignore[assignment] ( "int : Number of frames to wait before adding the next group of characters. Increasing this value creates a " "more staggered effect." ) reverse_direction: bool = ArgField( cmd_name="--reverse-direction", action="store_true", help="Reverse the direction of the characters.", ) # type: ignore[assignment] "bool : Reverse the direction of the characters." merge: bool = ArgField( cmd_name="--merge", action="store_true", help="Merge the character groups originating from either side of the terminal. (--reverse-direction is " "ignored when merging)", ) # type: ignore[assignment] "bool : Merge the character groups originating from either side of the terminal." movement_easing: easing.EasingFunction = ArgField( cmd_name=["--movement-easing"], default=easing.in_out_quad, type_parser=argvalidators.Ease.type_parser, metavar=argvalidators.Ease.METAVAR, help="Easing function to use for character movement.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for character movement." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("#833ab4"), Color("#fd1d1d"), Color("#fcb045")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient. If only one color is provided, " "the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the character gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Number of gradient steps to use. More steps will create a smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_frames: int = ArgField( cmd_name="--final-gradient-frames", type_parser=argvalidators.PositiveInt.type_parser, default=10, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to display each gradient step. Increase to slow down the gradient animation.", ) # type: ignore[assignment] "int : Number of frames to display each gradient step. Increase to slow down the gradient animation." final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", default=Gradient.Direction.VERTICAL, type_parser=argvalidators.GradientDirection.type_parser, help="Direction of the gradient (vertical, horizontal, diagonal, center).", ) # type: ignore[assignment] "Gradient.Direction : Direction of the gradient." @classmethod def get_effect_class(cls) -> type[Slide]: """Get the effect class associated with this configuration.""" return Slide class SlideIterator(BaseEffectIterator[SlideConfig]): """Effect iterator for the Slide effect.""" def __init__(self, effect: Slide) -> None: """Initialize the Slide effect iterator.""" super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.pending_groups: list[list[EffectCharacter]] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: # noqa: PLR0915 """Build the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] groups: list[list[EffectCharacter]] = [] if self.config.grouping == "row": groups = self.terminal.get_characters_grouped(self.terminal.CharacterGroup.ROW_TOP_TO_BOTTOM) elif self.config.grouping == "column": groups = self.terminal.get_characters_grouped(self.terminal.CharacterGroup.COLUMN_LEFT_TO_RIGHT) elif self.config.grouping == "diagonal": groups = self.terminal.get_characters_grouped( self.terminal.CharacterGroup.DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT, ) for group in groups: for character in group: input_path = character.motion.new_path( path_id="input_path", speed=self.config.movement_speed, ease=self.config.movement_easing, ) input_path.new_waypoint(character.input_coord) for group_index, group in enumerate(groups): if self.config.grouping == "row": if self.config.merge and group_index % 2 == 0: starting_column = self.terminal.canvas.right + 1 else: groups[group_index] = groups[group_index][::-1] starting_column = self.terminal.canvas.left - 1 if self.config.reverse_direction and not self.config.merge: groups[group_index] = groups[group_index][::-1] starting_column = self.terminal.canvas.right + 1 for character in groups[group_index]: character.motion.set_coordinate(geometry.Coord(starting_column, character.input_coord.row)) elif self.config.grouping == "column": if self.config.merge and group_index % 2 == 0: starting_row = self.terminal.canvas.bottom - 1 else: groups[group_index] = groups[group_index][::-1] starting_row = self.terminal.canvas.top + 1 if self.config.reverse_direction and not self.config.merge: groups[group_index] = groups[group_index][::-1] starting_row = self.terminal.canvas.bottom - 1 for character in groups[group_index]: character.motion.set_coordinate(geometry.Coord(character.input_coord.column, starting_row)) if self.config.grouping == "diagonal": distance_from_outside_bottom = group[-1].input_coord.row - (self.terminal.canvas.bottom - 1) starting_coord = geometry.Coord( group[-1].input_coord.column - distance_from_outside_bottom, group[-1].input_coord.row - distance_from_outside_bottom, ) if self.config.merge and group_index % 2 == 0: groups[group_index] = groups[group_index][::-1] distance_from_outside = (self.terminal.canvas.top + 1) - group[0].input_coord.row starting_coord = geometry.Coord( group[0].input_coord.column + distance_from_outside, group[0].input_coord.row + distance_from_outside, ) if self.config.reverse_direction and not self.config.merge: groups[group_index] = groups[group_index][::-1] distance_from_outside = (self.terminal.canvas.top + 1) - group[0].input_coord.row starting_coord = geometry.Coord( group[0].input_coord.column + distance_from_outside, group[0].input_coord.row + distance_from_outside, ) for character in groups[group_index]: character.motion.set_coordinate(starting_coord) for character in group: gradient_scn = character.animation.new_scene() char_gradient = Gradient( self.config.final_gradient_stops[0], self.character_final_color_map[character], steps=10, ) gradient_scn.apply_gradient_to_symbols( character.input_symbol, self.config.final_gradient_frames, fg_gradient=char_gradient, ) character.animation.activate_scene(gradient_scn) self.pending_groups = groups self._active_groups: list[list[EffectCharacter]] = [] self._current_gap = 0 def __next__(self) -> str: """Return the next frame in the animation.""" if self.pending_groups or self.active_characters or self._active_groups: if self._current_gap == self.config.gap and self.pending_groups: self._active_groups.append(self.pending_groups.pop(0)) self._current_gap = 0 elif self.pending_groups: self._current_gap += 1 for group in self._active_groups: if group: next_char = group.pop(0) self.terminal.set_character_visibility(next_char, is_visible=True) next_char.motion.activate_path(next_char.motion.paths["input_path"]) self.active_characters.add(next_char) self._active_groups = [group for group in self._active_groups if group] self.update() return self.frame raise StopIteration class Slide(BaseEffect[SlideConfig]): """Slides characters into view from outside the terminal. Attributes: effect_config (SlideConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[SlideConfig]: return SlideConfig @property def _iterator_cls(self) -> type[SlideIterator]: return SlideIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_spotlights.py000066400000000000000000000373771507200677100311040ustar00rootroot00000000000000"""Spotlights search the text area, illuminating characters, before converging in the center and expanding. Classes: Spotlights: Spotlights search the text area, illuminating characters, before converging in the center and expanding. SpotlightsConfig: Configuration for the Spotlights effect. SpotlightsIterator: Effect iterator for the Spotlights effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, ColorPair, Coord, EffectCharacter, Gradient, easing, geometry from terminaltexteffects.engine import animation, motion from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Spotlights, SpotlightsConfig @argclass( name="spotlights", help="Spotlights search the text area, illuminating characters, before converging in the center and expanding.", description="spotlights | Spotlights search the text area, illuminating characters, before converging in the " "center and expanding.", epilog=( f"{argvalidators.EASING_EPILOG} Example: terminaltexteffects spotlights --final-gradient-stops ab48ff " "e7b2b2 fffebd --final-gradient-steps 12 --beam-width-ratio 2.0 --beam-falloff 0.3 --search-duration " "750 --search-speed-range 0.25-0.5 --spotlight-count 3" ), ) @dataclass class SpotlightsConfig(ArgsDataClass): """Configuration for the Spotlights effect. Attributes: beam_width_ratio (float): Width of the beam of light as min(width, height) // n of the input text. Valid values are n > 0. Values where n < 1 are raised to 1. beam_falloff (float): Distance from the edge of the beam where the brightness begins to fall off, as a percentage of total beam width. Valid values are 0 <= n <= 1. search_duration (int): Duration of the search phase, in frames, before the spotlights converge in the center. Valid values are n > 0. search_speed_range (tuple[float, float]): Range of speeds for the spotlights during the search phase. The speed is a random value between the two provided values. Valid values are n > 0. spotlight_count (int): Number of spotlights to use. Valid values are n > 0. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ beam_width_ratio: float = ArgField( cmd_name="--beam-width-ratio", type_parser=argvalidators.PositiveFloat.type_parser, default=2.0, metavar=argvalidators.PositiveFloat.METAVAR, help="Width of the beam of light as min(width, height) // n of the input text. Values less than 1 " "are raised to 1.", ) # type: ignore[assignment] ( "float : Width of the beam of light as min(width, height) // n of the input text. Values less than 1 " "are raised to 1." ) beam_falloff: float = ArgField( cmd_name="--beam-falloff", type_parser=argvalidators.NonNegativeFloat.type_parser, default=0.3, metavar=argvalidators.NonNegativeFloat.METAVAR, help="Distance from the edge of the beam where the brightness begins to fall off, as a percentage of " "total beam width.", ) # type: ignore[assignment] ( "float : Distance from the edge of the beam where the brightness begins to fall off, as a percentage " "of total beam width." ) search_duration: int = ArgField( cmd_name="--search-duration", type_parser=argvalidators.PositiveInt.type_parser, default=750, metavar=argvalidators.PositiveInt.METAVAR, help="Duration of the search phase, in frames, before the spotlights converge in the center.", ) # type: ignore[assignment] "int : Duration of the search phase, in frames, before the spotlights converge in the center." search_speed_range: tuple[float, float] = ArgField( cmd_name="--search-speed-range", type_parser=argvalidators.PositiveFloatRange.type_parser, default=(0.25, 0.5), metavar=argvalidators.PositiveFloatRange.METAVAR, help="Range of speeds for the spotlights during the search phase. The speed is a random value between the " "two provided values.", ) # type: ignore[assignment] ( "tuple[float, float] : Range of speeds for the spotlights during the search phase. The speed is a random " "value between the two provided values." ) spotlight_count: int = ArgField( cmd_name="--spotlight-count", type_parser=argvalidators.PositiveInt.type_parser, default=3, metavar=argvalidators.PositiveInt.METAVAR, help="Number of spotlights to use.", ) # type: ignore[assignment] "int : Number of spotlights to use." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("ab48ff"), Color("e7b2b2"), Color("fffebd")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Number of gradient steps to use. More steps will create a smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Spotlights]: """Get the effect class associated with this configuration.""" return Spotlights class SpotlightsIterator(BaseEffectIterator[SpotlightsConfig]): """Effect iterator for the Spotlights effect.""" def __init__(self, effect: Spotlights) -> None: """Initialize the effect iterator. Args: effect (Spotlights): The effect to use for the iterator. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.illuminated_chars: set[EffectCharacter] = set() self.character_color_map: dict[EffectCharacter, tuple[Color, Color]] = {} # (bright, dark) self.build() def make_spotlights(self, num_spotlights: int) -> list[EffectCharacter]: """Create the spotlights. Args: num_spotlights (int): The number of spotlights to create. Returns: list[EffectCharacter]: The spotlights as a list of EffectCharacter instances. """ spotlights: list[EffectCharacter] = [] minimum_distance = self.terminal.canvas.right // 4 for _ in range(num_spotlights): spotlight = self.terminal.add_character("O", self.terminal.canvas.random_coord(outside_scope=True)) spotlights.append(spotlight) spotlight_target_coords: list[Coord] = [] last_coord = self.terminal.canvas.random_coord() spotlight_target_coords.append(last_coord) for _ in range(10): next_coord = self.find_coord_at_minimum_distance(last_coord, minimum_distance) spotlight_target_coords.append(next_coord) last_coord = next_coord paths: list[motion.Path] = [] for coord in spotlight_target_coords: path = spotlight.motion.new_path( speed=random.uniform(self.config.search_speed_range[0], self.config.search_speed_range[1]), ease=easing.in_out_quad, path_id=str(len(paths)), ) path.new_waypoint(coord, bezier_control=self.terminal.canvas.random_coord(outside_scope=True)) paths.append(path) spotlight.motion.chain_paths(paths, loop=True) path = spotlight.motion.new_path(speed=0.5, ease=easing.in_out_sine, path_id="center") path.new_waypoint(self.terminal.canvas.center) return spotlights def find_coord_at_minimum_distance(self, origin_coord: Coord, minimum_distance: int) -> Coord: """Find a coordinate at a minimum distance from the origin. Args: origin_coord (Coord): Origin coordinate. minimum_distance (int): Minimum distance from the origin. Returns: Coord: The coordinate found. """ coord_found = False while not coord_found: coord = self.terminal.canvas.random_coord() distance = geometry.find_length_of_line(origin_coord, coord) if distance >= minimum_distance: coord_found = True return coord # type: ignore[arg-type] def illuminate_chars(self, range_: int) -> None: """Illuminate characters within a range of the spotlights. Args: range_ (int): The range of the spotlights. """ coords_in_range: list[Coord] = [] for spotlight in self.spotlights: coords_in_range.extend(geometry.find_coords_in_circle(spotlight.motion.current_coord, range_)) chars_in_range: set[EffectCharacter] = set() for coord in coords_in_range: character = self.terminal.get_character_by_input_coord(coord) if character and character.input_symbol != " ": chars_in_range.add(character) chars_no_longer_in_range = self.illuminated_chars - chars_in_range for character in chars_no_longer_in_range: character.animation.set_appearance( character.input_symbol, ColorPair(fg=self.character_color_map[character][1]), ) for character in chars_in_range: distance = min( [ geometry.find_length_of_line( spotlight.motion.current_coord, character.input_coord, double_row_diff=True, ) for spotlight in self.spotlights ], ) if distance > range_ * (1 - self.config.beam_falloff): brightness_factor = max( 1 - (distance - range_ * (1 - self.config.beam_falloff)) / (range_ * self.config.beam_falloff), 0.2, ) adjusted_color = animation.Animation.adjust_color_brightness( self.character_color_map[character][0], brightness_factor, ) else: adjusted_color = self.character_color_map[character][0] character.animation.set_appearance(character.input_symbol, ColorPair(fg=adjusted_color)) self.illuminated_chars = chars_in_range def build(self) -> None: """Build the initial state of the effect.""" self.spotlights: list[EffectCharacter] = self.make_spotlights(self.config.spotlight_count) final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): if self.terminal.config.existing_color_handling == "dynamic" and character.animation.input_fg_color: color_bright = character.animation.input_fg_color color_dark = animation.Animation.adjust_color_brightness(color_bright, 0.2) else: color_bright = final_gradient_mapping[character.input_coord] color_dark = animation.Animation.adjust_color_brightness(color_bright, 0.2) self.terminal.set_character_visibility(character, is_visible=True) self.character_color_map[character] = (color_bright, color_dark) character.animation.set_appearance(character.input_symbol, ColorPair(fg=color_dark)) smallest_dimension = min(self.terminal.canvas.right, self.terminal.canvas.top) self.illuminate_range = max( int( min( smallest_dimension // self.config.beam_width_ratio, smallest_dimension, ), ), 1, ) self.search_duration = self.config.search_duration self.searching = True self.complete = False for spotlight in self.spotlights: spotlight_path_start = spotlight.motion.query_path("0") spotlight.motion.activate_path(spotlight_path_start) self.active_characters.add(spotlight) def __next__(self) -> str: """Return the next frame in the animation.""" if not self.complete: self.illuminate_chars(self.illuminate_range) if self.searching: self.search_duration -= 1 if not self.search_duration: for spotlight in self.spotlights: spotlight_path_center = spotlight.motion.query_path("center") spotlight.motion.activate_path(spotlight_path_center) self.searching = False if not any(spotlight.motion.active_path for spotlight in self.spotlights): while len(self.spotlights) > 1: self.spotlights.pop() self.illuminate_range += 1 if self.illuminate_range > max(self.terminal.canvas.right, self.terminal.canvas.top) // 1.5: self.complete = True self.update() return self.frame raise StopIteration class Spotlights(BaseEffect[SpotlightsConfig]): """Spotlights search the text area, illuminating characters, before converging in the center and expanding. Attributes: effect_config (SpotlightsConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[SpotlightsConfig]: return SpotlightsConfig @property def _iterator_cls(self) -> type[SpotlightsIterator]: return SpotlightsIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_spray.py000066400000000000000000000264121507200677100300260ustar00rootroot00000000000000"""Sprays the characters from a single point. Classes: Spray: Sprays the characters from a single point. SprayConfig: Configuration for the Spray effect. SprayIterator: Iterates over the effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from enum import Enum, auto from terminaltexteffects import Color, Coord, EffectCharacter, EventHandler, Gradient, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Spray, SprayConfig @argclass( name="spray", help="Draws the characters spawning at varying rates from a single point.", description="spray | Draws the characters spawning at varying rates from a single point.", epilog=( f"{argvalidators.EASING_EPILOG} Example: terminaltexteffects spray --final-gradient-stops 8A008A 00D1FF " "FFFFFF --final-gradient-steps 12 --spray-position e --spray-volume 0.005 --movement-speed 0.4-1.0 " "--movement-easing OUT_EXPO" ), ) @dataclass class SprayConfig(ArgsDataClass): """Configuration for the Spray effect. Attributes: spray_position (typing.Literal["n", "ne", "e", "se", "s", "sw", "w", "nw", "center"]): Position for the spray origin. Valid values are n, ne, e, se, s, sw, w, nw, center. spray_volume (float): Number of characters to spray per tick as a percent of the total number of characters. Valid values are 0 < n <= 1. movement_speed (tuple[float, float]): Movement speed of the characters. Valid values are n > 0. movement_easing (easing.EasingFunction): Easing function to use for character movement. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ spray_position: typing.Literal["n", "ne", "e", "se", "s", "sw", "w", "nw", "center"] = ArgField( cmd_name="--spray-position", choices=["n", "ne", "e", "se", "s", "sw", "w", "nw", "center"], default="e", help="Position for the spray origin.", ) # type: ignore[assignment] "typing.Literal['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center'] : Position for the spray origin." spray_volume: float = ArgField( cmd_name="--spray-volume", type_parser=argvalidators.PositiveRatio.type_parser, default=0.005, metavar=argvalidators.PositiveRatio.METAVAR, help="Number of characters to spray per tick as a percent of the total number of characters.", ) # type: ignore[assignment] "float : Number of characters to spray per tick as a percent of the total number of characters." movement_speed_range: tuple[float, float] = ArgField( cmd_name="--movement-speed-range", type_parser=argvalidators.PositiveFloatRange.type_parser, default=(0.4, 1.0), metavar=argvalidators.PositiveFloatRange.METAVAR, help="Movement speed range of the characters.", ) # type: ignore[assignment] "tuple[float, float] : Movement speed range of the characters." movement_easing: easing.EasingFunction = ArgField( cmd_name="--movement-easing", type_parser=argvalidators.Ease.type_parser, default=easing.out_expo, help="Easing function to use for character movement.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for character movement." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("8A008A"), Color("00D1FF"), Color("FFFFFF")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If " "only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name=["--final-gradient-steps"], type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Spray]: """Get the effect class associated with this configuration.""" return Spray class SprayIterator(BaseEffectIterator[SprayConfig]): """Iterator for the Spray effect.""" class SprayPosition(Enum): """Enum for the spray position.""" N = auto() NE = auto() E = auto() SE = auto() S = auto() SW = auto() W = auto() NW = auto() CENTER = auto() def __init__(self, effect: Spray) -> None: """Initialize the effect iterator. Args: effect (Spray): The effect to iterate over. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the effect.""" self._spray_position = { "n": SprayIterator.SprayPosition.N, "ne": SprayIterator.SprayPosition.NE, "e": SprayIterator.SprayPosition.E, "se": SprayIterator.SprayPosition.SE, "s": SprayIterator.SprayPosition.S, "sw": SprayIterator.SprayPosition.SW, "w": SprayIterator.SprayPosition.W, "nw": SprayIterator.SprayPosition.NW, "center": SprayIterator.SprayPosition.CENTER, }.get(self.config.spray_position, SprayIterator.SprayPosition.E) final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] spray_origin_map = { SprayIterator.SprayPosition.CENTER: (self.terminal.canvas.center), SprayIterator.SprayPosition.N: Coord(self.terminal.canvas.right // 2, self.terminal.canvas.top), SprayIterator.SprayPosition.NW: Coord(self.terminal.canvas.left, self.terminal.canvas.top), SprayIterator.SprayPosition.W: Coord(self.terminal.canvas.left, self.terminal.canvas.top // 2), SprayIterator.SprayPosition.SW: Coord(self.terminal.canvas.left, self.terminal.canvas.bottom), SprayIterator.SprayPosition.S: Coord(self.terminal.canvas.right // 2, self.terminal.canvas.bottom), SprayIterator.SprayPosition.SE: Coord(self.terminal.canvas.right - 1, self.terminal.canvas.bottom), SprayIterator.SprayPosition.E: Coord(self.terminal.canvas.right - 1, self.terminal.canvas.top // 2), SprayIterator.SprayPosition.NE: Coord(self.terminal.canvas.right - 1, self.terminal.canvas.top), } for character in self.terminal.get_characters(): character.motion.set_coordinate(spray_origin_map[self._spray_position]) input_coord_path = character.motion.new_path( speed=random.uniform(self.config.movement_speed_range[0], self.config.movement_speed_range[1]), ease=self.config.movement_easing, ) input_coord_path.new_waypoint(character.input_coord) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, input_coord_path, EventHandler.Action.SET_LAYER, 1, ) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, input_coord_path, EventHandler.Action.SET_LAYER, 0, ) droplet_scn = character.animation.new_scene() spray_gradient = Gradient( random.choice(final_gradient.spectrum), self.character_final_color_map[character], steps=7, ) droplet_scn.apply_gradient_to_symbols(character.input_symbol, 20, fg_gradient=spray_gradient) character.animation.activate_scene(droplet_scn) character.motion.activate_path(input_coord_path) self.pending_chars.append(character) random.shuffle(self.pending_chars) self._volume = max(int(len(self.pending_chars) * self.config.spray_volume), 1) def __next__(self) -> str: """Return the next frame in the animation.""" if self.pending_chars or self.active_characters: if self.pending_chars: for _ in range(random.randint(1, self._volume)): if self.pending_chars: next_character = self.pending_chars.pop() self.terminal.set_character_visibility(next_character, is_visible=True) self.active_characters.add(next_character) self.update() return self.frame raise StopIteration class Spray(BaseEffect[SprayConfig]): """Sprays the characters from a single point. Attributes: effect_config (SprayConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[SprayConfig]: return SprayConfig @property def _iterator_cls(self) -> type[SprayIterator]: return SprayIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_swarm.py000066400000000000000000000377771507200677100300410ustar00rootroot00000000000000"""Characters are grouped into swarms and move around the terminal before settling into position. Classes: Swarm: Characters are grouped into swarms and move around the terminal before settling into position. SwarmConfig: Configuration for the Swarm effect. SwarmIterator: Effect iterator for the Swarm effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import ( Color, ColorPair, Coord, EffectCharacter, EventHandler, Gradient, Scene, easing, geometry, ) from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Swarm, SwarmConfig @argclass( name="swarm", help="Characters are grouped into swarms and move around the terminal before settling into position.", description="swarm | Characters are grouped into swarms and move around the terminal before settling " "into position.", epilog=( "Example: terminaltexteffects swarm --base-color 31a0d4 --flash-color f2ea79 --final-gradient-stops " "31b900 f0ff65 --final-gradient-steps 12 --swarm-size 0.1 --swarm-coordination 0.80 " "--swarm-area-count 2-4" ), ) @dataclass class SwarmConfig(ArgsDataClass): """Configuration for the Swarm effect. Attributes: base_color (tuple[Color, ...]): Tuple of colors for the swarms. flash_color (Color): Color for the character flash. Characters flash when moving. swarm_size (float): Percent of total characters in each swarm. Valid values are 0 < n <= 1. swarm_coordination (float): Percent of characters in a swarm that move as a group. Valid values are 0 < n <= 1. swarm_area_count_range (tuple[int, int]): Range of the number of areas where characters will swarm. Valid values are n > 0. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ base_color: tuple[Color, ...] = ArgField( cmd_name=["--base-color"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("31a0d4"),), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the swarms", ) # type: ignore[assignment] """tuple[Color, ...] : Tuple of colors for the swarms""" flash_color: Color = ArgField( cmd_name=["--flash-color"], type_parser=argvalidators.ColorArg.type_parser, default=Color("f2ea79"), metavar=argvalidators.ColorArg.METAVAR, help="Color for the character flash. Characters flash when moving.", ) # type: ignore[assignment] """Color : Color for the character flash. Characters flash when moving.""" swarm_size: float = ArgField( cmd_name="--swarm-size", type_parser=argvalidators.NonNegativeRatio.type_parser, metavar=argvalidators.NonNegativeRatio.METAVAR, default=0.1, help="Percent of total characters in each swarm.", ) # type: ignore[assignment] "float : Percent of total characters in each swarm." swarm_coordination: float = ArgField( cmd_name="--swarm-coordination", type_parser=argvalidators.NonNegativeRatio.type_parser, metavar=argvalidators.NonNegativeRatio.METAVAR, default=0.80, help="Percent of characters in a swarm that move as a group.", ) # type: ignore[assignment] "float : Percent of characters in a swarm that move as a group." swarm_area_count_range: tuple[int, int] = ArgField( cmd_name="--swarm-area-count-range", type_parser=argvalidators.PositiveIntRange.type_parser, metavar=argvalidators.PositiveIntRange.METAVAR, default=(2, 4), help="Range of the number of areas where characters will swarm.", ) # type: ignore[assignment] "tuple[int, int] : Range of the number of areas where characters will swarm." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("31b900"), Color("f0ff65")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If " "only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name=["--final-gradient-steps"], type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.HORIZONTAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Swarm]: """Get the effect class associated with this configuration.""" return Swarm class SwarmIterator(BaseEffectIterator[SwarmConfig]): """Effect iterator for the Swarm effect.""" def __init__( self, effect: Swarm, ) -> None: """Initialize the Swarm effect iterator. Args: effect (Swarm): The effect to use for the iterator. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.swarms: list[list[EffectCharacter]] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def make_swarms(self, swarm_size: int) -> None: """Create swarms of characters. Args: swarm_size (int): The size of each swarm. """ unswarmed_characters = self.terminal.get_characters( sort=self.terminal.CharacterSort.BOTTOM_TO_TOP_RIGHT_TO_LEFT, ) while unswarmed_characters: new_swarm: list[EffectCharacter] = [] for _ in range(swarm_size): if unswarmed_characters: new_swarm.append(unswarmed_characters.pop()) else: break self.swarms.append(new_swarm) final_swarm = self.swarms.pop() if len(final_swarm) < swarm_size // 2: self.swarms[-1].extend(final_swarm) else: self.swarms.append(final_swarm) def build(self) -> None: # noqa: PLR0915 """Build the initial state of the effect.""" swarm_size: int = max(round(len(self.terminal.get_characters()) * self.config.swarm_size), 1) self.make_swarms(swarm_size) final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] flash_list = [self.config.flash_color for _ in range(10)] for swarm in self.swarms: swarm_gradient = Gradient(random.choice(self.config.base_color), self.config.flash_color, steps=7) swarm_gradient_mirror = list(swarm_gradient) + flash_list + list(swarm_gradient)[::-1] swarm_area_coordinate_map: dict[Coord, list[Coord]] = {} swarm_spawn = self.terminal.canvas.random_coord(outside_scope=True) swarm_areas: list[Coord] = [] swarm_area_count = random.randint( self.config.swarm_area_count_range[0], self.config.swarm_area_count_range[1], ) # create areas where characters will swarm last_focus_coord = swarm_spawn radius = max(min(self.terminal.canvas.right, self.terminal.canvas.top) // 2, 1) while len(swarm_areas) < swarm_area_count: potential_focus_coords = geometry.find_coords_on_circle(last_focus_coord, radius) random.shuffle(potential_focus_coords) for coord in potential_focus_coords: if self.terminal.canvas.coord_is_in_canvas(coord): next_focus_coord = coord break else: next_focus_coord = self.terminal.canvas.random_coord() swarm_areas.append(next_focus_coord) swarm_area_coordinate_map[last_focus_coord] = geometry.find_coords_in_circle( last_focus_coord, max(min(self.terminal.canvas.right, self.terminal.canvas.top) // 6, 1) * 2, ) last_focus_coord = next_focus_coord # assign characters waypoints for swarm areas and inner waypoints within the swarm areas for character in swarm: swarm_area_count = 0 character.motion.set_coordinate(swarm_spawn) flash_scn = character.animation.new_scene(sync=Scene.SyncMetric.DISTANCE) for step in swarm_gradient_mirror: flash_scn.add_frame(character.input_symbol, 1, colors=ColorPair(fg=step)) for swarm_area_coords in swarm_area_coordinate_map.values(): swarm_area_name = f"{swarm_area_count}_swarm_area" swarm_area_count += 1 origin_path = character.motion.new_path(path_id=swarm_area_name, speed=0.25, ease=easing.out_sine) origin_path.new_waypoint(random.choice(swarm_area_coords), waypoint_id=swarm_area_name) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, origin_path, EventHandler.Action.ACTIVATE_SCENE, flash_scn, ) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, origin_path, EventHandler.Action.SET_LAYER, 1, ) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, origin_path, EventHandler.Action.DEACTIVATE_SCENE, flash_scn, ) inner_paths = 0 total_inner_paths = 2 while inner_paths < total_inner_paths: next_coord = random.choice(swarm_area_coords) inner_paths += 1 inner_path = character.motion.new_path( path_id=str(len(character.motion.paths)), speed=0.1, ease=easing.in_out_sine, ) inner_path.new_waypoint(next_coord, waypoint_id=str(len(character.motion.paths))) # create landing waypoint and scene input_path = character.motion.new_path(speed=0.3, ease=easing.in_out_quad) input_path.new_waypoint(character.input_coord) input_scn = character.animation.new_scene() for step in Gradient(self.config.flash_color, self.character_final_color_map[character], steps=10): input_scn.add_frame(character.input_symbol, 3, colors=ColorPair(fg=step)) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, input_path, EventHandler.Action.ACTIVATE_SCENE, input_scn, ) character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, input_path, EventHandler.Action.SET_LAYER, 0, ) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, input_path, EventHandler.Action.ACTIVATE_SCENE, flash_scn, ) character.motion.chain_paths(list(character.motion.paths.values())) self.call_next = True self.active_swarm_area = "0_swarm_area" def __next__(self) -> str: """Return the next frame in the animation.""" if self.swarms or self.active_characters: if self.swarms and self.call_next: self.call_next = False self.current_swarm = self.swarms.pop() self.active_swarm_area = "0_swarm_area" for character in self.current_swarm: character.motion.activate_path(character.motion.query_path("0_swarm_area")) self.terminal.set_character_visibility(character, is_visible=True) self.active_characters.add(character) if len(self.active_characters) < len(self.current_swarm): # some of the characters have landed self.call_next = True if self.current_swarm: for character in self.current_swarm: if ( character.motion.active_path and character.motion.active_path.path_id != self.active_swarm_area and "swarm_area" in character.motion.active_path.path_id and int(character.motion.active_path.path_id[0]) > int(self.active_swarm_area[0]) ): self.active_swarm_area = character.motion.active_path.path_id for other in self.current_swarm: if other is not character and random.random() < self.config.swarm_coordination: other.motion.activate_path(other.motion.paths[self.active_swarm_area]) break self.update() return self.frame raise StopIteration class Swarm(BaseEffect[SwarmConfig]): """Characters are grouped into swarms and move around the terminal before settling into position. Attributes: effect_config (SwarmConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[SwarmConfig]: return SwarmConfig @property def _iterator_cls(self) -> type[SwarmIterator]: return SwarmIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_sweep.py000066400000000000000000000313171507200677100300130ustar00rootroot00000000000000"""Sweep across the canvas to reveal uncolored text, reverse sweep to color the text. Classes: Sweep: Sweep across the canvas to reveal uncolored text, reverse sweep to color the text. SweepConfig: Configuration for the Sweep effect. SweepIterator: Iterator for the Sweep effect. """ from __future__ import annotations import random import typing from dataclasses import dataclass import terminaltexteffects as tte from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Return the effect class and the effect configuration dataclass. Returns: tuple[type[typing.Any], type[ArgsDataClass]]: The effect class and the effect configuration dataclass. """ return Sweep, SweepConfig @argclass( name="sweep", help="Sweep across the canvas to reveal uncolored text, reverse sweep to color the text.", description="sweep | Sweep across the canvas to reveal uncolored text, reverse sweep to color the text.", epilog=( "Example: terminaltexteffects sweep --sweep-symbols '█' '▓' '▒' '░' --first-sweep-direction " "column_right_to_left --second-sweep-direction column_left_to_right --final-gradient-stops 8A008A " "00D1FF ffffff --final-gradient-steps 8 8 8 --final-gradient-direction vertical" ), ) @dataclass class SweepConfig(ArgsDataClass): """Sweep effect configuration dataclass.""" sweep_symbols: tuple[str, ...] = ArgField( cmd_name="--sweep-symbols", type_parser=argvalidators.Symbol.type_parser, nargs="+", default=("█", "▓", "▒", "░"), metavar=argvalidators.Symbol.METAVAR, help="Space separated list of symbols to use for the sweep shimmer.", ) # type: ignore[assignment] "tuple[str, ...] | str : Tuple of symbols to use for the sweep shimmer." first_sweep_direction: typing.Literal[ "column_left_to_right", "row_top_to_bottom", "row_bottom_to_top", "diagonal_top_left_to_bottom_right", "diagonal_bottom_left_to_top_right", "diagonal_top_right_to_bottom_left", "diagonal_bottom_right_to_top_left", "outside_to_center", "center_to_outside", ] = ArgField( cmd_name="--first-sweep-direction", default="column_right_to_left", choices=[ "column_left_to_right", "column_right_to_left", "row_top_to_bottom", "row_bottom_to_top", "diagonal_top_left_to_bottom_right", "diagonal_bottom_left_to_top_right", "diagonal_top_right_to_bottom_left", "diagonal_bottom_right_to_top_left", "outside_to_center", "center_to_outside", ], help="Direction of the first sweep, revealing uncolored characters.", ) # type: ignore[assignment] "typing.Literal['column_left_to_right','row_top_to_bottom','row_bottom_to_top','diagonal_top_left_to_bottom_right','diagonal_bottom_left_to_top_right','diagonal_top_right_to_bottom_left','diagonal_bottom_right_to_top_left',]" second_sweep_direction: typing.Literal[ "column_left_to_right", "row_top_to_bottom", "row_bottom_to_top", "diagonal_top_left_to_bottom_right", "diagonal_bottom_left_to_top_right", "diagonal_top_right_to_bottom_left", "diagonal_bottom_right_to_top_left", "outside_to_center", "center_to_outside", ] = ArgField( cmd_name="--second-sweep-direction", default="column_left_to_right", choices=[ "column_left_to_right", "column_right_to_left", "row_top_to_bottom", "row_bottom_to_top", "diagonal_top_left_to_bottom_right", "diagonal_bottom_left_to_top_right", "diagonal_top_right_to_bottom_left", "diagonal_bottom_right_to_top_left", "outside_to_center", "center_to_outside", ], help="Direction of the second sweep, coloring the characters.", ) # type: ignore[assignment] "typing.Literal['column_left_to_right','row_top_to_bottom','row_bottom_to_top','diagonal_top_left_to_bottom_right','diagonal_bottom_left_to_top_right','diagonal_top_right_to_bottom_left','diagonal_bottom_right_to_top_left',]" final_gradient_stops: tuple[tte.Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(tte.Color("8A008A"), tte.Color("00D1FF"), tte.Color("ffffff")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied from bottom to top). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] "tuple[Color, ...]: Space separated, unquoted, list of colors for the character gradient " "(applied from bottom to top). If only one color is provided, the characters will be displayed in that color." final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=8, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] "tuple[int, ...] | int: Space separated, unquoted, list of the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." final_gradient_direction: tte.Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=tte.Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[BaseEffect]: """Return the effect class associated with this configuration dataclass.""" return Sweep class SweepIterator(BaseEffectIterator[SweepConfig]): """Iterator for the sweep effect.""" def __init__(self, effect: Sweep) -> None: """Initialize the effect iterator.""" super().__init__(effect) self.character_final_color_map: dict[tte.EffectCharacter, tte.ColorPair] = {} self.pending_chars: list[tte.EffectCharacter] = [] self.pending_groups_initial_sweep: list[list[tte.EffectCharacter]] = [] self.pending_groups_second_sweep: list[list[tte.EffectCharacter]] = [] self.initial_sweep_complete = False self.second_sweep_complete = False self.easer = tte.easing.eased_step_function(tte.easing.in_out_circ, 0.01) self.total_groups = 0 self.groups_activated = 0 self.build() def build(self) -> None: """Build the effect.""" final_fg_gradient = tte.Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_fg_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) shades_of_gray = [ tte.Color("A0A0A0"), tte.Color("808080"), tte.Color("404040"), tte.Color("202020"), tte.Color("101010"), ] grouping_map = { "column_left_to_right": self.terminal.CharacterGroup.COLUMN_LEFT_TO_RIGHT, "column_right_to_left": self.terminal.CharacterGroup.COLUMN_RIGHT_TO_LEFT, "row_top_to_bottom": self.terminal.CharacterGroup.ROW_TOP_TO_BOTTOM, "row_bottom_to_top": self.terminal.CharacterGroup.ROW_BOTTOM_TO_TOP, "diagonal_top_left_to_bottom_right": self.terminal.CharacterGroup.DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT, "diagonal_bottom_left_to_top_right": self.terminal.CharacterGroup.DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT, "diagonal_top_right_to_bottom_left": self.terminal.CharacterGroup.DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT, "diagonal_bottom_right_to_top_left": self.terminal.CharacterGroup.DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT, "center_to_outside": self.terminal.CharacterGroup.CENTER_TO_OUTSIDE_DIAMONDS, "outside_to_center": self.terminal.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS, } for character in self.terminal.get_characters(inner_fill_chars=True, outer_fill_chars=True): if not character.is_fill_character: self.character_final_color_map[character] = tte.ColorPair( fg=final_gradient_mapping[character.input_coord], ) initial_sweep_scn = character.animation.new_scene(scene_id="initial_sweep") for char in self.config.sweep_symbols: initial_sweep_scn.add_frame(char, 5, colors=tte.ColorPair(fg=random.choice(shades_of_gray))) initial_sweep_scn.add_frame(character.input_symbol, 1, colors=tte.ColorPair(fg="808080")) second_sweep_scn = character.animation.new_scene(scene_id="second_sweep") for char in self.config.sweep_symbols: second_sweep_scn.add_frame( char, 5, colors=tte.ColorPair(fg=random.choice(final_fg_gradient.spectrum)), ) second_sweep_scn.add_frame( character.input_symbol, 1, colors=tte.ColorPair( fg=final_gradient_mapping[character.input_coord] if not character.is_fill_character else "000000", ), ) self.pending_groups_initial_sweep = self.terminal.get_characters_grouped( grouping_map[self.config.first_sweep_direction], inner_fill_chars=True, outer_fill_chars=True, ) self.pending_groups_second_sweep = self.terminal.get_characters_grouped( grouping_map[self.config.second_sweep_direction], inner_fill_chars=True, outer_fill_chars=True, ) self.total_groups = len(self.pending_groups_initial_sweep) def __next__(self) -> str: """Return the next frame in the effect.""" while self.pending_groups_initial_sweep or self.pending_groups_second_sweep or self.active_characters: _, eased_percentage = self.easer() if not self.initial_sweep_complete: while (self.groups_activated / self.total_groups) < eased_percentage: if self.pending_groups_initial_sweep: group = self.pending_groups_initial_sweep.pop(0) for character in group: self.terminal.set_character_visibility(character, is_visible=True) character.animation.activate_scene(character.animation.query_scene("initial_sweep")) self.active_characters.update(group) self.groups_activated += 1 if self.groups_activated == self.total_groups: self.initial_sweep_complete = True self.easer = tte.easing.eased_step_function(tte.easing.in_out_circ, 0.01) self.groups_activated = 0 elif not self.second_sweep_complete: while (self.groups_activated / self.total_groups) < eased_percentage: if self.pending_groups_second_sweep: group = self.pending_groups_second_sweep.pop(0) for character in group: character.animation.activate_scene(character.animation.query_scene("second_sweep")) self.active_characters.update(group) self.groups_activated += 1 if self.groups_activated == self.total_groups: self.second_sweep_complete = True self.update() return self.frame raise StopIteration class Sweep(BaseEffect[SweepConfig]): """Sweep across the canvas to reveal uncolored text, reverse sweep to color the text.""" @property def _config_cls(self) -> type[SweepConfig]: return SweepConfig @property def _iterator_cls(self) -> type[SweepIterator]: return SweepIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_synthgrid.py000066400000000000000000000524101507200677100307000ustar00rootroot00000000000000"""Create a grid which fills with characters dissolving into the final text. Classes: SynthGrid: Create a grid which fills with characters dissolving into the final text. SynthGridConfig: Configuration for the SynthGrid effect. SynthGridIterator: Iterates over the effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, ColorPair, Coord, EffectCharacter, EventHandler, Gradient, Terminal, geometry from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return SynthGrid, SynthGridConfig @argclass( name="synthgrid", help="Create a grid which fills with characters dissolving into the final text.", description="synthgrid | Create a grid which fills with characters dissolving into the final text.", epilog=( "Example: terminaltexteffects synthgrid --grid-gradient-stops CC00CC ffffff --grid-gradient-steps 12 " "--text-gradient-stops 8A008A 00D1FF FFFFFF --text-gradient-steps 12 --grid-row-symbol ─ " "--grid-column-symbol '|' --text-generation-symbols ░ ▒ ▓ --max-active-blocks 0.1" ), ) @dataclass class SynthGridConfig(ArgsDataClass): """Configuration for the SynthGrid effect. Attributes: grid_gradient_stops (tuple[Color, ...]): Tuple of colors for the grid gradient. grid_gradient_steps (tuple[int, ...] | int ): Int or Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. grid_gradient_direction (Gradient.Direction): Direction of the gradient for the grid color. text_gradient_stops (tuple[Color, ...]): Tuple of colors for the text gradient. text_gradient_steps (tuple[int, ...] | int ): Int or Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. text_gradient_direction (Gradient.Direction): Direction of the gradient for the text color. grid_row_symbol (str): Symbol to use for grid row lines. grid_column_symbol (str): Symbol to use for grid column lines. text_generation_symbols (tuple[str, ...] | str): Tuple of characters for the text generation animation. max_active_blocks (float): Maximum percentage of blocks to have active at any given time. For example, if set to 0.1, 10 percent of the blocks will be active at any given time. Valid values are 0 < n <= 1. """ grid_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--grid-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("CC00CC"), Color("ffffff")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the grid gradient.", ) # type: ignore[assignment] "tuple[Color, ...] : Tuple of colors for the grid gradient." grid_gradient_steps: tuple[int, ...] = ArgField( cmd_name="--grid-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) grid_gradient_direction: Gradient.Direction = ArgField( cmd_name="--grid-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.DIAGONAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the gradient for the grid color.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the gradient for the grid color." text_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--text-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("8A008A"), Color("00D1FF"), Color("FFFFFF")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the text gradient.", ) # type: ignore[assignment] "tuple[Color, ...] : Tuple of colors for the text gradient." text_gradient_steps: tuple[int, ...] = ArgField( cmd_name="--text-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) text_gradient_direction: Gradient.Direction = ArgField( cmd_name="--text-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the gradient for the text color.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the gradient for the text color." grid_row_symbol: str = ArgField( cmd_name="--grid-row-symbol", type_parser=argvalidators.Symbol.type_parser, default="─", metavar=argvalidators.Symbol.METAVAR, help="Symbol to use for grid row lines.", ) # type: ignore[assignment] "str : Symbol to use for grid row lines." grid_column_symbol: str = ArgField( cmd_name="--grid-column-symbol", type_parser=argvalidators.Symbol.type_parser, default="│", metavar=argvalidators.Symbol.METAVAR, help="Symbol to use for grid column lines.", ) # type: ignore[assignment] "str : Symbol to use for grid column lines." text_generation_symbols: tuple[str, ...] = ArgField( cmd_name="--text-generation-symbols", type_parser=argvalidators.Symbol.type_parser, nargs="+", default=("░", "▒", "▓"), metavar=argvalidators.Symbol.METAVAR, help="Space separated, unquoted, list of characters for the text generation animation.", ) # type: ignore[assignment] "tuple[str, ...] : Tuple of characters for the text generation animation." max_active_blocks: float = ArgField( cmd_name="--max-active-blocks", type_parser=argvalidators.PositiveRatio.type_parser, default=0.1, metavar=argvalidators.PositiveRatio.METAVAR, help="Maximum percentage of blocks to have active at any given time. For example, if set to 0.1, 10 percent " "of the blocks will be active at any given time.", ) # type: ignore[assignment] "float : Maximum percentage of blocks to have active at any given time." @classmethod def get_effect_class(cls) -> type[SynthGrid]: """Get the effect class associated with this configuration.""" return SynthGrid class GridLine: """A line in the grid.""" def __init__( self, terminal: Terminal, args: SynthGridConfig, origin: Coord, direction: str, grid_gradient_mapping: dict[geometry.Coord, Color], ) -> None: """Initialize the grid line. Args: terminal (Terminal): Terminal from the effect. args (SynthGridConfig): Configuration for the effect. origin (Coord): Origin coordinate. direction (str): Direction of the line. grid_gradient_mapping (dict[geometry.Coord, Color]): Mapping of coordinates to colors. """ self.terminal = terminal self.args = args self.origin = origin self.direction = direction if self.direction == "horizontal": self.grid_symbol = self.args.grid_row_symbol elif self.direction == "vertical": self.grid_symbol = self.args.grid_column_symbol self.characters: list[EffectCharacter] = [] if direction == "horizontal": for column_index in range(self.terminal.canvas.left, self.terminal.canvas.right + 1): effect_char = self.terminal.add_character(self.grid_symbol, Coord(0, 0)) grid_scn = effect_char.animation.new_scene() grid_scn.add_frame( self.grid_symbol, 1, colors=ColorPair(fg=grid_gradient_mapping[geometry.Coord(column_index, origin.row)]), ) effect_char.animation.activate_scene(grid_scn) effect_char.layer = 2 effect_char.motion.set_coordinate(Coord(column_index, origin.row)) self.characters.append(effect_char) elif direction == "vertical": for row_index in range(self.terminal.canvas.bottom, self.terminal.canvas.top): effect_char = self.terminal.add_character(self.grid_symbol, Coord(0, 0)) grid_scn = effect_char.animation.new_scene() grid_scn.add_frame( self.grid_symbol, 1, colors=ColorPair(fg=grid_gradient_mapping[geometry.Coord(origin.column, row_index)]), ) effect_char.animation.activate_scene(grid_scn) effect_char.layer = 2 effect_char.motion.set_coordinate(Coord(origin.column, row_index)) self.characters.append(effect_char) self.collapsed_characters = list(self.characters) self.extended_characters: list[EffectCharacter] = [] def extend(self) -> None: """Extend the line.""" count = 3 if self.direction == "horizontal" else 1 for _ in range(count): if self.collapsed_characters: next_char = self.collapsed_characters.pop(0) self.terminal.set_character_visibility(next_char, is_visible=True) self.extended_characters.append(next_char) def collapse(self) -> None: """Collapse the line.""" count = 3 if self.direction == "horizontal" else 1 if not self.collapsed_characters: self.extended_characters = self.extended_characters[::-1] for _ in range(count): if self.extended_characters: next_char = self.extended_characters.pop(0) self.terminal.set_character_visibility(next_char, is_visible=False) self.collapsed_characters.append(next_char) def is_extended(self) -> bool: """Check if the line is extended. Returns: bool: True if the line is extended, False otherwise. """ return not self.collapsed_characters def is_collapsed(self) -> bool: """Check if the line is collapsed. Returns: bool: True if the line is collapsed, False otherwise. """ return not self.extended_characters class SynthGridIterator(BaseEffectIterator[SynthGridConfig]): """Iterator for the SynthGrid effect.""" def __init__(self, effect: SynthGrid) -> None: """Initialize the effect iterator. Args: effect (SynthGrid): The effect to use for the iterator. """ super().__init__(effect) self.pending_groups: list[tuple[int, list[EffectCharacter]]] = [] self.grid_lines: list[GridLine] = [] self.group_tracker: dict[int, int] = {} self.build() def find_even_gap(self, dimension: int) -> int: """Find the closest even gap to 20% of the longest dimension. Args: dimension (int): The longest dimension. Returns: int: The gap that is closest to 20% of the dimension length. """ dimension = dimension - 2 if dimension <= 0: return 0 potential_gaps: list[int] = [i for i in range(dimension, 4, -1) if dimension % i <= 1] if not potential_gaps: return 4 return min(potential_gaps, key=lambda x: abs(x - dimension // 5)) def build(self) -> None: # noqa: PLR0915 """Build the initial state of the effect.""" grid_gradient = Gradient(*self.config.grid_gradient_stops, steps=self.config.grid_gradient_steps) grid_gradient_mapping = grid_gradient.build_coordinate_color_mapping( 1, self.terminal.canvas.top, 1, self.terminal.canvas.right, self.config.grid_gradient_direction, ) text_gradient = Gradient(*self.config.text_gradient_stops, steps=self.config.text_gradient_steps) text_gradient_mapping = text_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.text_gradient_direction, ) self.grid_lines.append( GridLine( self.terminal, self.config, Coord(self.terminal.canvas.left, self.terminal.canvas.bottom), "horizontal", grid_gradient_mapping, ), ) self.grid_lines.append( GridLine( self.terminal, self.config, Coord(self.terminal.canvas.left, self.terminal.canvas.top), "horizontal", grid_gradient_mapping, ), ) self.grid_lines.append( GridLine( self.terminal, self.config, Coord(self.terminal.canvas.left, self.terminal.canvas.bottom), "vertical", grid_gradient_mapping, ), ) self.grid_lines.append( GridLine( self.terminal, self.config, Coord(self.terminal.canvas.right, self.terminal.canvas.bottom), "vertical", grid_gradient_mapping, ), ) column_indexes: list[int] = [] row_indexes: list[int] = [] if self.terminal.canvas.top > 2 * self.terminal.canvas.right: row_gap = self.find_even_gap(self.terminal.canvas.top) + 1 column_gap = row_gap * 2 else: column_gap = self.find_even_gap(self.terminal.canvas.right) + 1 row_gap = column_gap // 2 for row_index in range(self.terminal.canvas.bottom + row_gap, self.terminal.canvas.top, max(row_gap, 1)): if self.terminal.canvas.top - row_index < 2: continue row_indexes.append(row_index) self.grid_lines.append( GridLine( self.terminal, self.config, Coord(self.terminal.canvas.left, row_index), "horizontal", grid_gradient_mapping, ), ) for column_index in range( self.terminal.canvas.left + column_gap, self.terminal.canvas.right, max(column_gap, 1), ): if self.terminal.canvas.right - column_index < 2: continue column_indexes.append(column_index) self.grid_lines.append( GridLine( self.terminal, self.config, Coord(column_index, self.terminal.canvas.bottom), "vertical", grid_gradient_mapping, ), ) row_indexes.append(self.terminal.canvas.top + 1) column_indexes.append(self.terminal.canvas.right + 1) prev_row_index = 1 for row_index in row_indexes: prev_column_index = 1 for column_index in column_indexes: coords_in_block: list[Coord] = [] if row_index == self.terminal.canvas.top: # make sure the top row is included row_index += 1 # noqa: PLW2901 for row in range(prev_row_index, row_index): for column in range(prev_column_index, column_index): coords_in_block.append(Coord(column, row)) # noqa: PERF401 characters_in_block: list[EffectCharacter] = [ self.terminal.character_by_input_coord[coord] for coord in coords_in_block if coord in self.terminal.character_by_input_coord ] if characters_in_block: self.pending_groups.append((len(self.pending_groups), characters_in_block)) prev_column_index = column_index prev_row_index = row_index for group_number, group in self.pending_groups: self.group_tracker[group_number] = 0 for character in group: dissolve_scn = character.animation.new_scene() for _ in range(random.randint(15, 30)): dissolve_scn.add_frame( random.choice(self.config.text_generation_symbols), 3, colors=ColorPair(fg=random.choice(text_gradient.spectrum)), ) if character.input_symbol == " ": dissolve_scn.add_frame(character.input_symbol, 1) else: dissolve_scn.add_frame( character.input_symbol, 1, colors=ColorPair(fg=text_gradient_mapping[character.input_coord]), ) character.animation.activate_scene(dissolve_scn) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, dissolve_scn, EventHandler.Action.CALLBACK, EventHandler.Callback(self.update_group_tracker, group_number), ) random.shuffle(self.pending_groups) self._phase = "grid_expand" self._total_group_count = len(self.pending_groups) if not self._total_group_count: for character in self.terminal.get_characters(): self.terminal.set_character_visibility(character, is_visible=True) self.active_characters.add(character) self._active_groups: int = 0 def update_group_tracker(self, _: EffectCharacter, *args) -> None: # noqa: ANN002 """Update the group tracker.""" self.group_tracker[args[0]] -= 1 def __next__(self) -> str: """Return the next frame in the animation.""" if self.pending_groups or self.active_characters or self._phase != "complete": if self._phase == "grid_expand": if not all(grid_line.is_extended() for grid_line in self.grid_lines): for grid_line in self.grid_lines: if not grid_line.is_extended(): grid_line.extend() else: self._phase = "add_chars" elif self._phase == "add_chars": if ( self.pending_groups and self._active_groups < self._total_group_count * self.config.max_active_blocks ): group_number, next_group = self.pending_groups.pop(0) for char in next_group: self.terminal.set_character_visibility(char, is_visible=True) self.active_characters.add(char) self.group_tracker[group_number] += 1 if not self.pending_groups and not self.active_characters and not self._active_groups: self._phase = "collapse" elif self._phase == "collapse": if not all(grid_line.is_collapsed() for grid_line in self.grid_lines): for grid_line in self.grid_lines: if not grid_line.is_collapsed(): grid_line.collapse() else: self._phase = "complete" self.update() self._active_groups = 0 for active_count in self.group_tracker.values(): if active_count: self._active_groups += 1 return self.frame raise StopIteration class SynthGrid(BaseEffect[SynthGridConfig]): """Create a grid which fills with characters dissolving into the final text. Attributes: effect_config (SynthGridConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[SynthGridConfig]: return SynthGridConfig @property def _iterator_cls(self) -> type[SynthGridIterator]: return SynthGridIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_unstable.py000066400000000000000000000330521507200677100305030ustar00rootroot00000000000000"""Spawns characters jumbled, explodes them to the edge of the canvas, then reassembles them. Classes: Unstable: Spawns characters jumbled, explodes them to the edge of the canvas, then reassembles them. UnstableConfig: Configuration for the Unstable effect. UnstableIterator: Effect iterator for the Unstable effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, Coord, EffectCharacter, Gradient, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Unstable, UnstableConfig @argclass( name="unstable", help="Spawn characters jumbled, explode them to the edge of the canvas, then reassemble them in the " "correct layout.", description="unstable | Spawn characters jumbled, explode them to the edge of the canvas, then reassemble them " "in the correct layout.", epilog=( f"{argvalidators.EASING_EPILOG} Example: terminaltexteffects unstable --unstable-color ff9200 " "--final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --explosion-ease OUT_EXPO " "--explosion-speed 0.75 --reassembly-ease OUT_EXPO --reassembly-speed 0.75" ), ) @dataclass class UnstableConfig(ArgsDataClass): """Configuration for the Unstable effect. Attributes: unstable_color (Color): Color transitioned to as the characters become unstable. explosion_ease (easing.EasingFunction): Easing function to use for character movement during the explosion. explosion_speed (float): Speed of characters during explosion. Valid values are n > 0. reassembly_ease (easing.EasingFunction): Easing function to use for character reassembly. reassembly_speed (float): Speed of characters during reassembly. Valid values are n > 0. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ unstable_color: Color = ArgField( cmd_name=["--unstable-color"], type_parser=argvalidators.ColorArg.type_parser, default=Color("ff9200"), metavar=argvalidators.ColorArg.METAVAR, help="Color transitioned to as the characters become unstable.", ) # type: ignore[assignment] "Color : Color transitioned to as the characters become unstable." explosion_ease: easing.EasingFunction = ArgField( cmd_name=["--explosion-ease"], type_parser=argvalidators.Ease.type_parser, default=easing.out_expo, help="Easing function to use for character movement during the explosion.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for character movement during the explosion." explosion_speed: float = ArgField( cmd_name=["--explosion-speed"], type_parser=argvalidators.PositiveFloat.type_parser, default=0.75, metavar=argvalidators.PositiveFloat.METAVAR, help="Speed of characters during explosion. ", ) # type: ignore[assignment] "float : Speed of characters during explosion. " reassembly_ease: easing.EasingFunction = ArgField( cmd_name=["--reassembly-ease"], type_parser=argvalidators.Ease.type_parser, default=easing.out_expo, help="Easing function to use for character reassembly.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for character reassembly." reassembly_speed: float = ArgField( cmd_name=["--reassembly-speed"], type_parser=argvalidators.PositiveFloat.type_parser, default=0.75, metavar=argvalidators.PositiveFloat.METAVAR, help="Speed of characters during reassembly. ", ) # type: ignore[assignment] "float : Speed of characters during reassembly." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("8A008A"), Color("00D1FF"), Color("FFFFFF")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If " "only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name=["--final-gradient-steps"], type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Unstable]: """Get the effect class associated with this configuration.""" return Unstable class UnstableIterator(BaseEffectIterator[UnstableConfig]): """Effect iterator for the Unstable effect.""" def __init__(self, effect: Unstable) -> None: """Initialize the effect iterator.""" super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.jumbled_coords: dict[EffectCharacter, Coord] = {} self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the initial effect state.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] character_coords = [character.input_coord for character in self.terminal.get_characters()] for character in self.terminal.get_characters(): pos = random.randint(0, 3) if pos == 0: col = self.terminal.canvas.left row = self.terminal.canvas.random_row() elif pos == 1: col = self.terminal.canvas.right row = self.terminal.canvas.random_row() elif pos == 2: col = self.terminal.canvas.random_column() row = self.terminal.canvas.bottom else: col = self.terminal.canvas.random_column() row = self.terminal.canvas.top jumbled_coord = character_coords.pop(random.randint(0, len(character_coords) - 1)) self.jumbled_coords[character] = jumbled_coord character.motion.set_coordinate(jumbled_coord) explosion_path = character.motion.new_path(path_id="explosion", speed=1.25, ease=self.config.explosion_ease) explosion_path.new_waypoint(Coord(col, row)) reassembly_path = character.motion.new_path( path_id="reassembly", speed=0.75, ease=self.config.reassembly_ease, ) reassembly_path.new_waypoint(character.input_coord) unstable_gradient = Gradient( self.character_final_color_map[character], self.config.unstable_color, steps=25, ) rumble_scn = character.animation.new_scene(scene_id="rumble") rumble_scn.apply_gradient_to_symbols(character.input_symbol, 10, fg_gradient=unstable_gradient) final_color = Gradient(self.config.unstable_color, self.character_final_color_map[character], steps=12) final_scn = character.animation.new_scene(scene_id="final") final_scn.apply_gradient_to_symbols(character.input_symbol, 5, fg_gradient=final_color) character.animation.activate_scene(rumble_scn) self.terminal.set_character_visibility(character, is_visible=True) self._explosion_hold_time = 50 self.phase = "rumble" self._max_rumble_steps = 250 self._current_rumble_steps = 0 self._rumble_mod_delay = 20 def __next__(self) -> str: """Return the next from in the effect.""" next_frame = None if self.phase == "rumble": if self._current_rumble_steps < self._max_rumble_steps: if self._current_rumble_steps > 30 and self._current_rumble_steps % self._rumble_mod_delay == 0: row_offset = random.choice([-1, 0, 1]) column_offset = random.choice([-1, 0, 1]) for character in self.terminal.get_characters(): character.motion.set_coordinate( Coord( character.motion.current_coord.column + column_offset, character.motion.current_coord.row + row_offset, ), ) character.animation.step_animation() next_frame = self.frame for character in self.terminal.get_characters(): character.motion.set_coordinate(self.jumbled_coords[character]) self._rumble_mod_delay -= 1 self._rumble_mod_delay = max(self._rumble_mod_delay, 1) else: for character in self.terminal.get_characters(): character.animation.step_animation() next_frame = self.frame self._current_rumble_steps += 1 else: self.phase = "explosion" for character in self.terminal.get_characters(): character.motion.activate_path(character.motion.query_path("explosion")) self.active_characters = set(self.terminal.get_characters()) if self.phase == "explosion": if self.active_characters: for character in self.active_characters: character.tick() self.active_characters = { character for character in self.active_characters if character.motion.current_coord != character.motion.query_path("explosion").waypoints[0].coord } next_frame = self.frame elif self._explosion_hold_time: for character in self.active_characters: character.tick() self._explosion_hold_time -= 1 next_frame = self.frame else: self.phase = "reassembly" for character in self.terminal.get_characters(): character.animation.activate_scene(character.animation.query_scene("final")) self.active_characters.add(character) character.motion.activate_path(character.motion.query_path("reassembly")) if self.phase == "reassembly" and self.active_characters: for character in self.active_characters: character.tick() self.active_characters = { character for character in self.active_characters if character.motion.current_coord != character.motion.query_path("reassembly").waypoints[0].coord or not character.animation.active_scene_is_complete() } next_frame = self.frame if next_frame is not None: return next_frame raise StopIteration class Unstable(BaseEffect[UnstableConfig]): """Spawns characters jumbled, explodes them to the edge of the canvas, then reassembles them. Attributes: effect_config (UnstableConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[UnstableConfig]: return UnstableConfig @property def _iterator_cls(self) -> type[UnstableIterator]: return UnstableIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_vhstape.py000066400000000000000000000615411507200677100303440ustar00rootroot00000000000000"""Lines of characters glitch left and right and lose detail like an old VHS tape. Classes: VHSTape: Lines of characters glitch left and right and lose detail like an old VHS tape. VHSTapeConfig: Configuration for the VHSTape effect. VHSTapeIterator: Effect iterator for the VHSTape effect. Does not normally need to be called directly. """ from __future__ import annotations import random import typing from dataclasses import dataclass from terminaltexteffects import Color, ColorPair, Coord, EffectCharacter, EventHandler, Gradient, Scene from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return VHSTape, VHSTapeConfig @argclass( name="vhstape", help="Lines of characters glitch left and right and lose detail like an old VHS tape.", description="vhstape | Lines of characters glitch left and right and lose detail like an old VHS tape.", epilog=( "Example: terminaltexteffects vhstape --final-gradient-stops ab48ff e7b2b2 fffebd " "--final-gradient-steps 12 --glitch-line-colors ffffff ff0000 00ff00 0000ff ffffff --glitch-wave-colors " "ffffff ff0000 00ff00 0000ff ffffff --noise-colors 1e1e1f 3c3b3d 6d6c70 a2a1a6 cbc9cf ffffff " "--glitch-line-chance 0.05 --noise-chance 0.004 --total-glitch-time 1000" ), ) @dataclass class VHSTapeConfig(ArgsDataClass): """Configuration for the VHSTape effect. Attributes: glitch_line_colors (tuple[Color, ...]): Tuple of colors for the characters when a single line is glitching. Colors are applied in order as an animation. glitch_wave_colors (tuple[Color, ...]): Tuple of colors for the characters in lines that are part of the glitch wave. Colors are applied in order as an animation. noise_colors (tuple[Color, ...]): Tuple of colors for the characters during the noise phase. glitch_line_chance (float): Chance that a line will glitch on any given frame. noise_chance (float): Chance that all characters will experience noise on any given frame. Valid values are 0 <= n <= 1. total_glitch_time (int): Total time, in frames, that the glitching phase will last. Valid values are n > 0. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ glitch_line_colors: tuple[Color, ...] = ArgField( cmd_name="--glitch-line-colors", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("ffffff"), Color("ff0000"), Color("00ff00"), Color("0000ff"), Color("ffffff")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the characters when a single line is glitching. Colors " "are applied in order as an animation.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the characters when a single line is glitching. Colors are " "applied in order as an animation." ) glitch_wave_colors: tuple[Color, ...] = ArgField( cmd_name="--glitch-wave-colors", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("ffffff"), Color("ff0000"), Color("00ff00"), Color("0000ff"), Color("ffffff")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the characters in lines that are part of the glitch wave. " "Colors are applied in order as an animation.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the characters in lines that are part of the glitch wave. Colors " "are applied in order as an animation." ) noise_colors: tuple[Color, ...] = ArgField( cmd_name="--noise-colors", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("1e1e1f"), Color("3c3b3d"), Color("6d6c70"), Color("a2a1a6"), Color("cbc9cf"), Color("ffffff")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the characters during the noise phase.", ) # type: ignore[assignment] "tuple[Color, ...] : Tuple of colors for the characters during the noise phase." glitch_line_chance: float = ArgField( cmd_name="--glitch-line-chance", type_parser=argvalidators.NonNegativeRatio.type_parser, default=0.05, metavar=argvalidators.NonNegativeRatio.METAVAR, help="Chance that a line will glitch on any given frame.", ) # type: ignore[assignment] "float : Chance that a line will glitch on any given frame." noise_chance: float = ArgField( cmd_name="--noise-chance", type_parser=argvalidators.NonNegativeRatio.type_parser, default=0.004, metavar=argvalidators.NonNegativeRatio.METAVAR, help="Chance that all characters will experience noise on any given frame.", ) # type: ignore[assignment] "float : Chance that all characters will experience noise on any given frame." total_glitch_time: int = ArgField( cmd_name="--total-glitch-time", type_parser=argvalidators.PositiveInt.type_parser, default=1000, metavar=argvalidators.PositiveInt.METAVAR, help="Total time, frames, that the glitching phase will last.", ) # type: ignore[assignment] "int : Total time, frames, that the glitching phase will last." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("ab48ff"), Color("e7b2b2"), Color("fffebd")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If " "only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create " "a smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[VHSTape]: """Get the effect class associated with this configuration.""" return VHSTape class VHSTapeIterator(BaseEffectIterator[VHSTapeConfig]): """Effect iterator for the VHSTape effect.""" class Line: """Line of characters for the VHSTape effect.""" def __init__( self, characters: list[EffectCharacter], args: VHSTapeConfig, character_final_color_map: dict[EffectCharacter, Color], ) -> None: """Initialize the line of characters. Args: characters (list[EffectCharacter]): The characters in the line. args (VHSTapeConfig): Configuration for the effect. character_final_color_map (dict[EffectCharacter, Color]): Mapping of characters to their final colors. """ self.characters = characters self.args = args self.character_final_color_map = character_final_color_map self.build_line_effects() def build_line_effects(self) -> None: """Build the effects for the line of characters.""" glitch_line_colors = self.args.glitch_line_colors snow_chars = ["#", "*", ".", ":"] noise_colors = self.args.noise_colors offset = random.randint(4, 25) direction = random.choice((-1, 1)) hold_time = random.randint(1, 50) for character in self.characters: # make glitch and restore waypoints glitch_path = character.motion.new_path(path_id="glitch", speed=2, hold_time=hold_time) glitch_path.new_waypoint( Coord(character.input_coord.column + (offset * direction), character.input_coord.row), waypoint_id="glitch", ) restore_path = character.motion.new_path(path_id="restore", speed=2) restore_path.new_waypoint(character.input_coord, waypoint_id="restore") # make glitch wave waypoints glitch_wave_mid_path = character.motion.new_path(path_id="glitch_wave_mid", speed=2) glitch_wave_mid_path.new_waypoint( Coord(character.input_coord.column + 8, character.input_coord.row), waypoint_id="glitch_wave_mid", ) glitch_wave_end_path = character.motion.new_path(path_id="glitch_wave_end", speed=2) glitch_wave_end_path.new_waypoint( Coord(character.input_coord.column + 14, character.input_coord.row), waypoint_id="glitch_wave_end", ) # make glitch scenes base_scn = character.animation.new_scene(scene_id="base") base_scn.add_frame( character.input_symbol, duration=1, colors=ColorPair(fg=self.character_final_color_map[character]), ) glitch_scn_forward = character.animation.new_scene( scene_id="rgb_glitch_fwd", sync=Scene.SyncMetric.STEP, ) for color in glitch_line_colors: glitch_scn_forward.add_frame(character.input_symbol, duration=1, colors=ColorPair(fg=color)) glitch_scn_backward = character.animation.new_scene( scene_id="rgb_glitch_bwd", sync=Scene.SyncMetric.STEP, ) for color in glitch_line_colors[::-1]: glitch_scn_backward.add_frame(character.input_symbol, duration=1, colors=ColorPair(fg=color)) snow_scn = character.animation.new_scene(scene_id="snow") for _ in range(25): snow_scn.add_frame( random.choice(snow_chars), duration=2, colors=ColorPair(fg=random.choice(noise_colors)), ) snow_scn.add_frame( character.input_symbol, duration=1, colors=ColorPair(fg=self.character_final_color_map[character]), ) final_snow_scn = character.animation.new_scene(scene_id="final_snow") final_redraw_scn = character.animation.new_scene(scene_id="final_redraw") final_redraw_scn.add_frame("█", duration=10, colors=ColorPair(fg="ffffff")) final_redraw_scn.add_frame( character.input_symbol, duration=1, colors=ColorPair(fg=self.character_final_color_map[character]), ) for _ in range(50): final_snow_scn.add_frame( random.choice(snow_chars), duration=2, colors=ColorPair(fg=random.choice(noise_colors)), ) # register events character.event_handler.register_event( EventHandler.Event.PATH_COMPLETE, glitch_path, EventHandler.Action.ACTIVATE_PATH, restore_path, ) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, glitch_path, EventHandler.Action.ACTIVATE_SCENE, glitch_scn_forward, ) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, restore_path, EventHandler.Action.ACTIVATE_SCENE, glitch_scn_backward, ) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, glitch_wave_mid_path, EventHandler.Action.ACTIVATE_SCENE, glitch_scn_forward, ) character.event_handler.register_event( EventHandler.Event.PATH_ACTIVATED, glitch_wave_end_path, EventHandler.Action.ACTIVATE_SCENE, glitch_scn_forward, ) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, glitch_scn_backward, EventHandler.Action.ACTIVATE_SCENE, base_scn, ) def snow(self) -> None: """Activate the snow animation for the line.""" for character in self.characters: character.animation.activate_scene(character.animation.query_scene("snow")) def set_hold_time(self, hold_time: int) -> None: """Set the hold time for the glitch and restore paths.""" for character in self.characters: character.motion.paths["glitch"].hold_time = hold_time def glitch(self, *, final: bool = False) -> None: """Activate the glitch animation for the line. Args: final (bool, optional): If final, set hold times to 0. Defaults to False. """ for character in self.characters: glitch_path = character.motion.query_path("glitch") restore_path = character.motion.query_path("restore") if final: glitch_path.hold_time = 0 restore_path.hold_time = 0 glitch_path.speed = 40 / random.randint(20, 40) restore_path.speed = 40 / random.randint(20, 40) character.motion.activate_path(glitch_path) def restore(self) -> None: """Activate the restore animation for the line.""" for character in self.characters: restore_path = character.motion.query_path("restore") restore_path.speed = 40 / random.randint(20, 40) character.motion.activate_path(restore_path) def activate_path(self, path_id: str) -> None: """Activate the specified path for the line. Args: path_id (str): The ID of the path to activate. """ for character in self.characters: character.motion.activate_path(character.motion.query_path(path_id)) def line_movement_complete(self) -> bool: """Check if the movement of the line is complete. Returns: bool: True if the movement of the line is complete, False otherwise. """ return all(character.motion.movement_is_complete() for character in self.characters) def __init__(self, effect: VHSTape) -> None: """Initialize the effect iterator. Args: effect (VHSTape): The effect to use for the iterator. """ super().__init__(effect) self.pending_chars: list[EffectCharacter] = [] self.lines: dict[int, VHSTapeIterator.Line] = {} self.active_glitch_wave_top: int | None = None self.active_glitch_wave_lines: list[VHSTapeIterator.Line] = [] self.active_glitch_lines: list[VHSTapeIterator.Line] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the initial state of the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] for row_index, characters in enumerate( self.terminal.get_characters_grouped(grouping=self.terminal.CharacterGroup.ROW_BOTTOM_TO_TOP), ): self.lines[row_index] = VHSTapeIterator.Line(characters, self.config, self.character_final_color_map) for character in self.terminal.get_characters(): self.terminal.set_character_visibility(character, is_visible=True) character.animation.activate_scene(character.animation.query_scene("base")) self._glitching_steps_elapsed = 0 self._phase = "glitching" self._to_redraw = list(self.lines.values()) self._redrawing = False def glitch_wave(self) -> None: """Move the glitch wave.""" if not self.active_glitch_wave_top: if self.terminal.canvas.text_height >= 3: # choose a wave top index in the top half of the canvas or at least 3 rows up self.active_glitch_wave_top = self.terminal.canvas.text_bottom + random.randint( max((3, round(self.terminal.canvas.text_height * 0.5))), self.terminal.canvas.text_height, ) else: # not enough room for a wave return # if all lines have completed movement, proceed to move/restore wave if all(line.line_movement_complete() for line in self.active_glitch_wave_lines): if self.active_glitch_wave_lines: # only move 30% of the time wave_top_delta = (1 if random.random() < 0.3 else -1) if random.random() < 0.3 else 0 self.active_glitch_wave_top += wave_top_delta # clamp wave top to canvas self.active_glitch_wave_top = max(2, min(self.active_glitch_wave_top, self.terminal.canvas.text_top)) # get the lines for the wave new_wave_lines: list[VHSTapeIterator.Line] = [] for line_index in range(self.active_glitch_wave_top - 2, self.active_glitch_wave_top + 1): adjusted_line_index = line_index - (self.terminal.canvas.text_bottom - 1) if adjusted_line_index in self.lines: new_wave_lines.append(self.lines[adjusted_line_index]) # restore any lines that are no longer part of the wave for line in self.active_glitch_wave_lines: if line not in new_wave_lines: line.restore() self.active_characters = self.active_characters.union(line.characters) self.active_glitch_wave_lines = new_wave_lines if self.active_glitch_wave_top < self.terminal.canvas.text_bottom + 2: # wave at bottom, restore lines for line in self.active_glitch_wave_lines: line.restore() self.active_characters = self.active_characters.union(line.characters) self.active_glitch_wave_top = None self.active_glitch_wave_lines = [] else: for line, path_id in zip( self.active_glitch_wave_lines, ("glitch_wave_mid", "glitch_wave_end", "glitch_wave_mid"), ): line.activate_path(path_id) self.active_characters = self.active_characters.union(line.characters) def __next__(self) -> str: """Return the next frame in the animation.""" if self._phase != "complete" or self.active_characters: if self._phase == "glitching": # Check if all active glitch wave lines have completed their movement, if so move the wave if not self.active_glitch_wave_lines or all( line.line_movement_complete() for line in self.active_glitch_wave_lines ): self.glitch_wave() # Remove completed glitch lines from active glitch lines self.active_glitch_lines = [ line for line in self.active_glitch_lines if not line.line_movement_complete() ] # Randomly add new glitch lines if random.random() < self.config.glitch_line_chance and len(self.active_glitch_lines) < 3: glitch_line: VHSTapeIterator.Line = random.choice(list(self.lines.values())) if glitch_line not in self.active_glitch_wave_lines and glitch_line not in self.active_glitch_lines: glitch_line.set_hold_time(random.randint(30, 120)) self.active_glitch_lines.append(glitch_line) glitch_line.glitch() self.active_characters = self.active_characters.union(glitch_line.characters) # Randomly add noise to all lines if random.random() < self.config.noise_chance: for line in self.lines.values(): line.snow() if line not in self.active_glitch_wave_lines and line not in self.active_glitch_lines: self.active_characters = self.active_characters.union(line.characters) self._glitching_steps_elapsed += 1 # Check if glitching time has reached the total glitch time if self._glitching_steps_elapsed >= self.config.total_glitch_time: # Restore glitch wave lines for line in self.active_glitch_wave_lines: line.restore() # Restore glitch lines for line in self.active_glitch_lines: line.restore() self._phase = "noise" elif self._phase == "noise": # Activate final snow animation for all characters if not self.active_characters: for character in self.terminal.get_characters(): character.animation.activate_scene(character.animation.query_scene("final_snow")) self.active_characters.add(character) self._phase = "redraw" elif self._phase == "redraw": # Redraw lines one by one if self._redrawing or not self.active_characters: self._redrawing = True if self._to_redraw: next_line = self._to_redraw.pop() for character in next_line.characters: character.animation.activate_scene(character.animation.query_scene("final_redraw")) self.active_characters.add(character) else: self._phase = "complete" self.update() return self.frame raise StopIteration class VHSTape(BaseEffect[VHSTapeConfig]): """Lines of characters glitch left and right and lose detail like an old VHS tape. Attributes: effect_config (VHSTapeConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[VHSTapeConfig]: return VHSTapeConfig @property def _iterator_cls(self) -> type[VHSTapeIterator]: return VHSTapeIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_waves.py000066400000000000000000000311121507200677100300060ustar00rootroot00000000000000"""Waves travel across the terminal leaving behind the characters. Classes: Waves: Creates waves that travel across the terminal, leaving behind the characters. WavesConfig: Configuration for the Waves effect. WavesIterator: Iterates over the effect. Does not normally need to be called directly. """ from __future__ import annotations import typing from dataclasses import dataclass from terminaltexteffects import Color, ColorPair, EffectCharacter, EventHandler, Gradient, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Waves, WavesConfig @argclass( name="waves", help="Waves travel across the terminal leaving behind the characters.", description="waves | Waves travel across the terminal leaving behind the characters.", epilog=( f"{argvalidators.EASING_EPILOG} Example: terminaltexteffects waves --wave-symbols ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ " "▇ ▆ ▅ ▄ ▃ ▂ ▁ --wave-gradient-stops f0ff65 ffb102 31a0d4 ffb102 f0ff65 --wave-gradient-steps 6 " "--final-gradient-stops ffb102 31a0d4 f0ff65 --final-gradient-steps 12 --wave-count 7 --wave-length 2 " "--wave-easing IN_OUT_SINE" ), ) @dataclass class WavesConfig(ArgsDataClass): """Configuration for the Waves effect. Attributes: wave_symbols (tuple[str, ...] | str): Symbols to use for the wave animation. Multi-character strings will be used in sequence to create an animation. wave_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. wave_gradient_steps (tuple[int, ...]): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. wave_count (int): Number of waves to generate. Valid values are n > 0. wave_length (int): The number of frames for each step of the wave. Higher wave-lengths will create a slower wave. Valid values are n > 0. wave_direction (typing.Literal['column_left_to_right','column_right_to_left','row_top_to_bottom','row_bottom_to_top','center_to_outside','outside_to_center']): Direction of the wave. wave_easing (easing.EasingFunction): Easing function to use for wave travel. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ # noqa: E501 wave_symbols: tuple[str, ...] = ArgField( cmd_name="--wave-symbols", type_parser=argvalidators.Symbol.type_parser, default=("▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▂", "▁"), nargs="+", metavar=argvalidators.Symbol.METAVAR, help="Symbols to use for the wave animation. Multi-character strings will be used in sequence to create an " "animation.", ) # type: ignore[assignment] ( "tuple[str, ...] : Symbols to use for the wave animation. Multi-character strings will be used in sequence to " "create an animation." ) wave_gradient_stops: tuple[Color, ...] = ArgField( cmd_name="--wave-gradient-stops", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("#f0ff65"), Color("#ffb102"), Color("#31a0d4"), Color("#ffb102"), Color("#f0ff65")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If " "only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) wave_gradient_steps: tuple[int, ...] = ArgField( cmd_name="--wave-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=(6,), metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create a " "smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] : Tuple of the number of gradient steps to use. More steps will create a smoother and " "longer gradient animation." ) wave_count: int = ArgField( cmd_name="--wave-count", type_parser=argvalidators.PositiveInt.type_parser, default=7, help="Number of waves to generate. n > 0.", ) # type: ignore[assignment] "int : Number of waves to generate. n > 0." wave_length: int = ArgField( cmd_name="--wave-length", type_parser=argvalidators.PositiveInt.type_parser, default=2, metavar=argvalidators.PositiveInt.METAVAR, help="The number of frames for each step of the wave. Higher wave-lengths will create a slower wave.", ) # type: ignore[assignment] "int : The number of frames for each step of the wave. Higher wave-lengths will create a slower wave." wave_direction: typing.Literal[ "column_left_to_right", "column_right_to_left", "row_top_to_bottom", "row_bottom_to_top", "center_to_outside", "outside_to_center", ] = ArgField( cmd_name="--wave-direction", default="column_left_to_right", help="Direction of the wave.", choices=[ "column_left_to_right", "column_right_to_left", "row_top_to_bottom", "row_bottom_to_top", "center_to_outside", "outside_to_center", ], ) # type: ignore[assignment] "typing.Literal['column_left_to_right','column_right_to_left','row_top_to_bottom','row_bottom_to_top','center_to_outside','outside_to_center']" wave_easing: easing.EasingFunction = ArgField( cmd_name="--wave-easing", type_parser=argvalidators.Ease.type_parser, default=easing.in_out_sine, help="Easing function to use for wave travel.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for wave travel." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name="--final-gradient-stops", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("#ffb102"), Color("#31a0d4"), Color("#f0ff65")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color.", ) # type: ignore[assignment] ( "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the " "characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Space separated, unquoted, list of the number of gradient steps to use. More steps will create " "a smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps " "will create a smoother and longer gradient animation." ) final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.DIAGONAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Waves]: """Get the effect class associated with this configuration.""" return Waves class WavesIterator(BaseEffectIterator[WavesConfig]): """Iterator for the Waves effect.""" def __init__(self, effect: Waves) -> None: """Initialize the iterator with the provided effect. Args: effect (Waves): The effect to iterate over. """ super().__init__(effect) self.pending_columns: list[list[EffectCharacter]] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.build() def build(self) -> None: """Build the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) wave_gradient = Gradient(*self.config.wave_gradient_stops, steps=self.config.wave_gradient_steps) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] wave_scn = character.animation.new_scene() wave_scn.ease = self.config.wave_easing for _ in range(self.config.wave_count): wave_scn.apply_gradient_to_symbols( self.config.wave_symbols, duration=self.config.wave_length, fg_gradient=wave_gradient, ) final_scn = character.animation.new_scene() for step in Gradient( wave_gradient.spectrum[-1], self.character_final_color_map[character], steps=self.config.final_gradient_steps, ): final_scn.add_frame(character.input_symbol, 10, colors=ColorPair(fg=step)) character.event_handler.register_event( EventHandler.Event.SCENE_COMPLETE, wave_scn, EventHandler.Action.ACTIVATE_SCENE, final_scn, ) character.animation.activate_scene(wave_scn) grouping_map = { "column_left_to_right": self.terminal.CharacterGroup.COLUMN_LEFT_TO_RIGHT, "column_right_to_left": self.terminal.CharacterGroup.COLUMN_RIGHT_TO_LEFT, "row_top_to_bottom": self.terminal.CharacterGroup.ROW_TOP_TO_BOTTOM, "row_bottom_to_top": self.terminal.CharacterGroup.ROW_BOTTOM_TO_TOP, "center_to_outside": self.terminal.CharacterGroup.CENTER_TO_OUTSIDE_DIAMONDS, "outside_to_center": self.terminal.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS, } for column in self.terminal.get_characters_grouped(grouping=grouping_map[self.config.wave_direction]): self.pending_columns.append(column) def __next__(self) -> str: """Return the next frame in the animation.""" if self.pending_columns or self.active_characters: if self.pending_columns: next_column = self.pending_columns.pop(0) for character in next_column: self.terminal.set_character_visibility(character, is_visible=True) self.active_characters.add(character) self.update() return self.frame raise StopIteration class Waves(BaseEffect[WavesConfig]): """Creates waves that travel across the terminal, leaving behind the characters. Attributes: effect_config (ExpandConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[WavesConfig]: return WavesConfig @property def _iterator_cls(self) -> type[WavesIterator]: return WavesIterator terminaltexteffects-release-0.12.1/terminaltexteffects/effects/effect_wipe.py000066400000000000000000000303741507200677100276360ustar00rootroot00000000000000"""Performs a wipe across the terminal to reveal characters. Classes: Wipe: Performs a wipe across the terminal to reveal characters. WipeConfig: Configuration for the Wipe effect. WipeIterator: Effect iterator for the Wipe effect. Does not normally need to be called directly. """ from __future__ import annotations import typing from dataclasses import dataclass from terminaltexteffects import Color, EffectCharacter, Gradient, easing from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Get the effect class and its configuration class.""" return Wipe, WipeConfig @argclass( name="wipe", help="Wipes the text across the terminal to reveal characters.", description="wipe | Wipes the text across the terminal to reveal characters.", epilog=( "Example: terminaltexteffects wipe --wipe-direction diagonal_bottom_left_to_top_right " "--final-gradient-stops 833ab4 fd1d1d fcb045 --final-gradient-steps 12 " "--final-gradient-frames 5 --wipe-delay 0" ), ) @dataclass class WipeConfig(ArgsDataClass): """Configuration for the Wipe effect. Attributes: wipe_direction (typing.Literal["column_left_to_right","row_top_to_bottom","row_bottom_to_top","diagonal_top_left_to_bottom_right","diagonal_bottom_left_to_top_right","diagonal_top_right_to_bottom_left","diagonal_bottom_right_to_top_left","center_to_outside","outside_to_center"]): Direction the text will wipe. wipe_delay (int): Number of frames to wait before adding the next character group. Increase, to slow down the effect. Valid values are n >= 0. final_gradient_stops (tuple[Color, ...]): Tuple of colors for the wipe gradient. final_gradient_steps (tuple[int, ...] | int): Tuple of the number of gradient steps to use. More steps will create a smoother and longer gradient animation. Valid values are n > 0. final_gradient_frames (int): Number of frames to display each gradient step. Increase to slow down the gradient animation. final_gradient_direction (Gradient.Direction): Direction of the final gradient. """ # noqa: E501 wipe_direction: typing.Literal[ "column_left_to_right", "row_top_to_bottom", "row_bottom_to_top", "diagonal_top_left_to_bottom_right", "diagonal_bottom_left_to_top_right", "diagonal_top_right_to_bottom_left", "diagonal_bottom_right_to_top_left", "outside_to_center", "center_to_outside", ] = ArgField( cmd_name="--wipe-direction", default="diagonal_bottom_left_to_top_right", choices=[ "column_left_to_right", "column_right_to_left", "row_top_to_bottom", "row_bottom_to_top", "diagonal_top_left_to_bottom_right", "diagonal_bottom_left_to_top_right", "diagonal_top_right_to_bottom_left", "diagonal_bottom_right_to_top_left", "outside_to_center", "center_to_outside", ], help="Direction the text will wipe.", ) # type: ignore[assignment] "typing.Literal['column_left_to_right','row_top_to_bottom','row_bottom_to_top','diagonal_top_left_to_bottom_right','diagonal_bottom_left_to_top_right','diagonal_top_right_to_bottom_left','diagonal_bottom_right_to_top_left',]" wipe_delay: int = ArgField( cmd_name="--wipe-delay", type_parser=argvalidators.NonNegativeInt.type_parser, default=0, metavar=argvalidators.NonNegativeInt.METAVAR, help="Number of frames to wait before adding the next character group. Increase, to slow down the effect.", ) # type: ignore[assignment] "int : Number of frames to wait before adding the next character group. Increase, to slow down the effect." wipe_ease: easing.EasingFunction = ArgField( cmd_name="--wipe-ease", type_parser=argvalidators.Ease.type_parser, default=easing.linear, help="Easing function to use for the wipe effect.", ) # type: ignore[assignment] "easing.EasingFunction : Easing function to use for the wipe effect." wipe_ease_stepsize: float = ArgField( cmd_name="--wipe-ease-stepsize", type_parser=argvalidators.EasingStep.type_parser, default=0.01, metavar=argvalidators.EasingStep.METAVAR, help="Step size to use for the easing function.", ) # type: ignore[assignment] "float : Step size to use for the easing function." final_gradient_stops: tuple[Color, ...] = ArgField( cmd_name="--final-gradient-stops", type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(Color("#833ab4"), Color("#fd1d1d"), Color("#fcb045")), metavar=argvalidators.ColorArg.METAVAR, help="Space separated, unquoted, list of colors for the wipe gradient.", ) # type: ignore[assignment] "tuple[Color, ...] : Tuple of colors for the wipe gradient." final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help="Number of gradient steps to use. More steps will create a smoother and longer gradient animation.", ) # type: ignore[assignment] ( "tuple[int, ...] | int : Int or Tuple of ints for the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ) final_gradient_frames: int = ArgField( cmd_name="--final-gradient-frames", type_parser=argvalidators.PositiveInt.type_parser, default=5, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to display each gradient step. Increase to slow down the gradient animation.", ) # type: ignore[assignment] "int : Number of frames to display each gradient step. Increase to slow down the gradient animation." final_gradient_direction: Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." @classmethod def get_effect_class(cls) -> type[Wipe]: """Get the effect class associated with this configuration.""" return Wipe class WipeIterator(BaseEffectIterator[WipeConfig]): """Effect iterator for the Wipe effect.""" def __init__(self, effect: Wipe) -> None: """Initialize the effect iterator. Args: effect (Wipe): The effect to use for the iterator. """ super().__init__(effect) self.groups: list[list[EffectCharacter]] = [] self.active_groups: list[list[EffectCharacter]] = [] self.character_final_color_map: dict[EffectCharacter, Color] = {} self.wipe_ease = easing.eased_step_function(self.config.wipe_ease, self.config.wipe_ease_stepsize) self.complete = False self.build() def build(self) -> None: """Build the effect.""" final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] sort_map = { "column_left_to_right": self.terminal.CharacterGroup.COLUMN_LEFT_TO_RIGHT, "column_right_to_left": self.terminal.CharacterGroup.COLUMN_RIGHT_TO_LEFT, "row_top_to_bottom": self.terminal.CharacterGroup.ROW_TOP_TO_BOTTOM, "row_bottom_to_top": self.terminal.CharacterGroup.ROW_BOTTOM_TO_TOP, "diagonal_top_left_to_bottom_right": self.terminal.CharacterGroup.DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT, "diagonal_bottom_left_to_top_right": self.terminal.CharacterGroup.DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT, "diagonal_top_right_to_bottom_left": self.terminal.CharacterGroup.DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT, "diagonal_bottom_right_to_top_left": self.terminal.CharacterGroup.DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT, "center_to_outside": self.terminal.CharacterGroup.CENTER_TO_OUTSIDE_DIAMONDS, "outside_to_center": self.terminal.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS, } character_groups = self.terminal.get_characters_grouped(sort_map[self.config.wipe_direction]) for group in character_groups: for character in group: wipe_scn = character.animation.new_scene(scene_id="wipe") wipe_gradient = Gradient( final_gradient.spectrum[0], self.character_final_color_map[character], steps=self.config.final_gradient_steps, ) wipe_scn.apply_gradient_to_symbols( character.input_symbol, self.config.final_gradient_frames, fg_gradient=wipe_gradient, ) character.animation.activate_scene(wipe_scn) self.groups.append(group) self._wipe_delay = self.config.wipe_delay def __next__(self) -> str: """Return the next frame in the animation.""" if not self.complete or self.active_characters: if not self._wipe_delay: current_step, progress_ratio = self.wipe_ease() target_active_group_count = min(int(len(self.groups) * progress_ratio), len(self.groups)) # if the easing function results in a decreased progress ratio, deactivate groups if target_active_group_count < len(self.active_groups): for group in self.active_groups[target_active_group_count:]: for character in group: self.terminal.set_character_visibility(character, is_visible=False) scn = character.animation.active_scene if scn: scn.reset_scene() character.animation.deactivate_scene(scn) self.active_groups = self.active_groups[:target_active_group_count] if len(self.active_groups) < target_active_group_count: for i in range(target_active_group_count): group = self.groups[i] if group in self.active_groups: continue for character in group: self.terminal.set_character_visibility(character, is_visible=True) scn = character.animation.query_scene("wipe") if scn: character.animation.activate_scene(scn) self.active_characters.add(character) self.active_groups.append(group) self._wipe_delay = self.config.wipe_delay if current_step == 1: self.complete = True else: self._wipe_delay -= 1 self.update() return self.frame raise StopIteration class Wipe(BaseEffect[WipeConfig]): """Performs a wipe across the terminal to reveal characters. Attributes: effect_config (WipeConfig): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property def _config_cls(self) -> type[WipeConfig]: return WipeConfig @property def _iterator_cls(self) -> type[WipeIterator]: return WipeIterator terminaltexteffects-release-0.12.1/terminaltexteffects/engine/000077500000000000000000000000001507200677100246235ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/terminaltexteffects/engine/__init__.py000066400000000000000000000000001507200677100267220ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/terminaltexteffects/engine/animation.py000066400000000000000000001037101507200677100271560ustar00rootroot00000000000000"""Classes for handling animations in terminal text effects. Classes: CharacterVisual: A class for storing symbol, color, and terminal graphical modes for the character. Frame: A class representing a frame in an animation. Scene: A class representing a sequence of frames that can be played in an animation. Animation: A class for handling animations for an EffectCharacter. """ from __future__ import annotations import typing from dataclasses import dataclass from enum import Enum, auto from terminaltexteffects.utils import ansitools, colorterm, easing, graphics, hexterm from terminaltexteffects.utils.exceptions import ( ActivateEmptySceneError, ApplyGradientToSymbolsEmptyGradientsError, ApplyGradientToSymbolsInvalidSymbolError, ApplyGradientToSymbolsNoGradientsError, FrameDurationError, SceneNotFoundError, ) if typing.TYPE_CHECKING: from terminaltexteffects.engine import base_character # pragma: no cover @dataclass class CharacterVisual: """A class for storing symbol, color, and terminal graphical modes for the character. Args: symbol (str): The unformatted symbol. bold (bool): Bold mode. dim (bool): Dim mode. italic (bool): Italic mode. underline (bool): Underline mode. blink (bool): Blink mode. reverse (bool): Reverse mode. hidden (bool): Hidden mode. strike (bool): Strike mode. colors (graphics.ColorPair | None): The symbol's colors. _fg_color_code (str | int | None): The symbol's foreground color code. _bg_color_code (str | int | None): The symbol's background color code. Attributes: formatted_symbol (str): The current symbol with all ANSI sequences applied. Methods: format_symbol: Formats the symbol for printing by applying ANSI sequences for any active modes and color. """ symbol: str bold: bool = False dim: bool = False italic: bool = False underline: bool = False blink: bool = False reverse: bool = False hidden: bool = False strike: bool = False colors: graphics.ColorPair | None = None # the Color object provided during initialization # the _*_color_code attributes are used to store the actual 8-bit int or 24-bit hex str after applying terminal # config args these are used by colorterm to produce the ansi sequences _fg_color_code: str | int | None = None _bg_color_code: str | int | None = None def __post_init__(self) -> None: """Create the formatted symbol by applying ANSI sequences for any active modes and color.""" self.formatted_symbol = self.format_symbol() def format_symbol(self) -> str: """Format the symbol for printing by applying ANSI sequences for any active modes and color.""" formatting_string = "" if self.bold: formatting_string += ansitools.apply_bold() if self.italic: formatting_string += ansitools.apply_italic() if self.underline: formatting_string += ansitools.apply_underline() if self.blink: formatting_string += ansitools.apply_blink() if self.reverse: formatting_string += ansitools.apply_reverse() if self.hidden: formatting_string += ansitools.apply_hidden() if self.strike: formatting_string += ansitools.apply_strikethrough() if self._fg_color_code is not None: formatting_string += colorterm.fg(self._fg_color_code) if self._bg_color_code is not None: formatting_string += colorterm.bg(self._bg_color_code) return f"{formatting_string}{self.symbol}{ansitools.reset_all() if formatting_string else ''}" @dataclass class Frame: """A Frame is a CharacterVisual with a duration. Args: character_visual (CharacterVisual): a CharacterVisual object duration (int): the number of ticks to display the Frame Attributes: character_visual (CharacterVisual): the CharacterVisual object for the Frame duration (int): the number of ticks to display the Frame ticks_elapsed (int): the number of ticks that have elapsed displaying this frame """ character_visual: CharacterVisual duration: int def __post_init__(self) -> None: """Initialize the ticks_elapsed attribute to 0.""" self.ticks_elapsed = 0 class Scene: """A Scene is a collection of Frames that can be played in sequence. Scenes can be looped and synced to movement. Methods: add_frame: Adds a Frame to the Scene. activate: Activates the Scene. get_next_visual: Gets the next CharacterVisual in the Scene. apply_gradient_to_symbols: Applies a gradient effect to a sequence of symbols. reset_scene: Resets the Scene. Attributes: scene_id (str): the ID of the Scene is_looping (bool): Whether the Scene should loop sync (Scene.SyncMetric | None): The type of sync to use for the Scene ease (easing.EasingFunction | None): The easing function to use for the Scene no_color (bool): Whether to ignore colors use_xterm_colors (bool): Whether to convert all colors to XTerm-256 colors frames (list[Frame]): The list of Frames in the Scene played_frames (list[Frame]): The list of Frames that have been played frame_index_map (dict[int, Frame]): A mapping of frame index to Frame easing_total_steps (int): The total number of steps in the easing function easing_current_step (int): The current step in the easing function preexisting_colors (graphics.ColorPair | None): The preexisting colors parsed from the input. """ xterm_color_map: typing.ClassVar[dict[str, int]] = {} class SyncMetric(Enum): """Enum for specifying the type of sync to use for a Scene. Attributes: DISTANCE (int): Sync to a Waypoint based on distance from the Waypoint STEP (int): Sync to a Waypoint based on the number of steps taken towards the Waypoint """ DISTANCE = auto() STEP = auto() def __init__( self, scene_id: str, *, is_looping: bool = False, sync: SyncMetric | None = None, ease: easing.EasingFunction | None = None, no_color: bool = False, use_xterm_colors: bool = False, ) -> None: """Initialize a Scene. Args: scene_id (str): the ID of the Scene is_looping (bool, optional): Whether the Scene should loop. Defaults to False. sync (Scene.SyncMetric | None, optional): The type of sync to use for the Scene. Defaults to None. ease (easing.EasingFunction | None, optional): The easing function to use for the Scene. Defaults to None. no_color (bool, optional): Whether to colors should be ignored. Defaults to False. use_xterm_colors (bool, optional): Whether to convert all colors to XTerm-256 colors. Defaults to False. """ self.scene_id = scene_id self.is_looping = is_looping self.sync: Scene.SyncMetric | None = sync self.ease: easing.EasingFunction | None = ease self.no_color = no_color self.use_xterm_colors = use_xterm_colors self.frames: list[Frame] = [] self.played_frames: list[Frame] = [] self.frame_index_map: dict[int, Frame] = {} self.easing_total_steps: int = 0 self.easing_current_step: int = 0 self.preexisting_colors: graphics.ColorPair | None = None def _get_color_code(self, color: graphics.Color | None) -> str | int | None: """Get the color code for the given color. RGB colors are converted to XTerm-256 colors if use_xterm_colors is True. If no_color is True, returns None. Otherwise, returns the RGB color. Args: color (graphics.Color | None): the color to get the code for Returns: str | int | None: the color code """ if color: if self.no_color: return None if self.use_xterm_colors: if color.xterm_color is not None: return color.xterm_color if color.rgb_color in self.xterm_color_map: return self.xterm_color_map[color.rgb_color] xterm_color = hexterm.hex_to_xterm(color.rgb_color) self.xterm_color_map[color.rgb_color] = xterm_color return xterm_color return color.rgb_color return None def add_frame( self, symbol: str, duration: int, *, colors: graphics.ColorPair | None = None, bold: bool = False, dim: bool = False, italic: bool = False, underline: bool = False, blink: bool = False, reverse: bool = False, hidden: bool = False, strike: bool = False, ) -> None: """Add a Frame to the Scene with the given symbol, duration, color, and graphical modes. Args: symbol (str): the symbol to show duration (int): the number of frames to use the Frame colors (graphics.ColorPair | None, optional): the colors to use. Defaults to None. bold (bool, optional): bold mode. Defaults to False. dim (bool, optional): dim mode. Defaults to False. italic (bool, optional): italic mode. Defaults to False. underline (bool, optional): underline mode. Defaults to False. blink (bool, optional): blink mode. Defaults to False. reverse (bool, optional): reverse mode. Defaults to False. hidden (bool, optional): hidden mode. Defaults to False. strike (bool, optional): strike mode. Defaults to False. Raises: FrameDurationError: if the frame duration is less than 1 """ # override fg and bg colors if they are set in the Scene due to existing color handling = always if self.preexisting_colors: colors = self.preexisting_colors # get the color code for the fg and bg colors if colors: char_vis_fg_color = self._get_color_code(colors.fg_color) char_vis_bg_color = self._get_color_code(colors.bg_color) else: char_vis_fg_color = None char_vis_bg_color = None if duration < 1: raise FrameDurationError(duration) char_vis = CharacterVisual( symbol, bold=bold, dim=dim, italic=italic, underline=underline, blink=blink, reverse=reverse, hidden=hidden, strike=strike, colors=colors, _fg_color_code=char_vis_fg_color, _bg_color_code=char_vis_bg_color, ) frame = Frame(char_vis, duration) self.frames.append(frame) for _ in range(frame.duration): self.frame_index_map[self.easing_total_steps] = frame self.easing_total_steps += 1 def activate(self) -> CharacterVisual: """Activate the Scene by returning the first frame `CharacterVisual`. Called by the `Animation` object when the Scene is activated. Raises: ActivateEmptySceneError: if the Scene has no frames Returns: CharacterVisual: the next CharacterVisual in the Scene """ if self.frames: return self.frames[0].character_visual raise ActivateEmptySceneError(self) def get_next_visual(self) -> CharacterVisual: """Get the next CharacterVisual in the Scene. Retrieve the current frame from `frames`, then increment the `ticks_elapsed` attribute of the Frame. If the `ticks_elapsed` equals the duration of the current frame, reset `ticks_elapsed` to `0` and move the current frame from `frames` to `played_frames`. If the `Scene` is set to `loop` and all frames have been played, refill `frames` with the frames from `played_frames` and clear `played_frames`. Return the `CharacterVisual` of the current frame. Returns: CharacterVisual: The visual of the current frame in the Scene. """ current_frame = self.frames[0] next_visual = current_frame.character_visual current_frame.ticks_elapsed += 1 if current_frame.ticks_elapsed == current_frame.duration: current_frame.ticks_elapsed = 0 self.played_frames.append(self.frames.pop(0)) if self.is_looping and not self.frames: self.frames.extend(self.played_frames) self.played_frames.clear() return next_visual def apply_gradient_to_symbols( self, symbols: typing.Sequence[str], duration: int, *, fg_gradient: graphics.Gradient | None = None, bg_gradient: graphics.Gradient | None = None, ) -> None: """Apply a gradient effect to a sequence of symbols and add each symbol as a frame to the Scene. Args: symbols (Sequence[str]): The sequence of symbols to apply the gradient to. duration (int): The duration to show each frame. fg_gradient (graphics.Gradient | None): The foreground gradient to apply. Defaults to None. bg_gradient (graphics.Gradient | None): The background gradient to apply. Defaults to None. Returns: None Raises: ApplyGradientToSymbolsNoGradientsError: if both fg_gradient and bg_gradient are None ApplyGradientToSymbolsEmptyGradientsError: if both fg_gradient and bg_gradient are empty """ T = typing.TypeVar("T") R = typing.TypeVar("R") def cyclic_distribution( larger_seq: typing.Sequence[T], smaller_seq: typing.Sequence[R], ) -> typing.Generator[tuple[T, R], None, None]: """Distributes the elements of a smaller sequence cyclically across a larger sequence with overflow. Example: cyclic_distribution([1, 2, 3, 4, 5], [a, b]) -> [(1, a), (2, a), (3, a), (4, b), (5, b)] Args: larger_seq (typing.Sequence[T]): the larger sequence smaller_seq (typing.Sequence[R]): the smaller sequence Yields: typing.Generator[tuple[T, R], None, None]: a generator yielding tuples of elements from the larger and smaller sequences """ repeat_factor = len(larger_seq) // len(smaller_seq) overflow_count = len(larger_seq) % len(smaller_seq) overflow_used = False smaller_index = 0 current_repeat_factor = 0 for larger_seq_element in larger_seq: if current_repeat_factor >= repeat_factor: if overflow_count: if overflow_used: smaller_index += 1 current_repeat_factor = 0 overflow_used = False else: overflow_used = True overflow_count -= 1 else: smaller_index += 1 current_repeat_factor = 0 current_repeat_factor += 1 yield larger_seq_element, smaller_seq[smaller_index] if fg_gradient is None and bg_gradient is None: raise ApplyGradientToSymbolsNoGradientsError if not ((fg_gradient and fg_gradient.spectrum) or (bg_gradient and bg_gradient.spectrum)): raise ApplyGradientToSymbolsEmptyGradientsError for symbol in symbols: if len(symbol) > 1: raise ApplyGradientToSymbolsInvalidSymbolError(symbol) color_pairs: list[graphics.ColorPair] = [] if fg_gradient and fg_gradient.spectrum and bg_gradient and bg_gradient.spectrum: if len(fg_gradient.spectrum) >= len(bg_gradient.spectrum): color_pairs = [ graphics.ColorPair(fg=fg_color, bg=bg_color) for fg_color, bg_color in cyclic_distribution(fg_gradient.spectrum, bg_gradient.spectrum) ] else: color_pairs = [ graphics.ColorPair(fg=fg_color, bg=bg_color) for bg_color, fg_color in cyclic_distribution(bg_gradient.spectrum, fg_gradient.spectrum) ] elif fg_gradient and fg_gradient.spectrum: color_pairs = [graphics.ColorPair(fg=color, bg=None) for color in fg_gradient.spectrum] elif bg_gradient and bg_gradient.spectrum: color_pairs = [graphics.ColorPair(fg=None, bg=color) for color in bg_gradient.spectrum] if len(symbols) >= len(color_pairs): for symbol, colors in cyclic_distribution(symbols, color_pairs): self.add_frame(symbol, duration, colors=colors) else: for colors, symbol in cyclic_distribution(color_pairs, symbols): self.add_frame(symbol, duration, colors=colors) def reset_scene(self) -> None: """Reset the Scene.""" for sequence in self.frames: sequence.ticks_elapsed = 0 self.played_frames.append(sequence) self.frames.clear() self.frames.extend(self.played_frames) self.played_frames.clear() def __eq__(self, other: object) -> bool: """Check if two Scene objects are equal based on their scene_id.""" if not isinstance(other, Scene): return NotImplemented return self.scene_id == other.scene_id def __hash__(self) -> int: """Return the hash value of the Scene based on its scene_id.""" return hash(self.scene_id) class Animation: """Animation handler for an EffectCharacter. It contains a scene_name -> Scene mapping and the active Scene. Calls to step_animation() progress the Scene and apply the next visual to the character. Attributes: scenes (dict[str, Scene]): a mapping of scene IDs to Scene objects character (base_character.EffectCharacter): the EffectCharacter object to animate active_scene (Scene | None): the active Scene use_xterm_colors (bool): whether to convert all colors to XTerm-256 colors no_color (bool): whether to ignore colors existing_color_handling (str): how to handle color ANSI sequences from the input data input_fg_color (graphics.Color | None): the input foreground Color input_bg_color (graphics.Color | None): the input background Color xterm_color_map (dict[str, int]): a mapping of RGB color codes to XTerm-256 color codes active_scene_current_step (int): the current step in the active Scene current_character_visual (CharacterVisual): the current visual of the character Methods: new_scene: Creates a new Scene and adds it to the Animation. query_scene: Returns a Scene from the Animation. active_scene_is_complete: Returns whether the active scene is complete. set_appearance: Applies a symbol and color to the character. adjust_color_brightness: Adjusts the brightness of a given color. _ease_animation: Returns the percentage of total distance that should be moved based on the easing function. step_animation: Apply the next symbol in the scene to the character. activate_scene: Activates a Scene. """ def __init__(self, character: base_character.EffectCharacter) -> None: """Initialize the Animation object. Args: character (base_character.EffectCharacter): the EffectCharacter object to animate """ self.scenes: dict[str, Scene] = {} self.character = character self.active_scene: Scene | None = None self.use_xterm_colors: bool = False self.no_color: bool = False self.existing_color_handling: typing.Literal["always", "dynamic", "ignore"] = "ignore" self.input_fg_color: graphics.Color | None = None self.input_bg_color: graphics.Color | None = None self.xterm_color_map: dict[str, int] = {} self.active_scene_current_step: int = 0 self.current_character_visual: CharacterVisual = CharacterVisual(character.input_symbol) def _get_color_code(self, color: graphics.Color | None) -> str | int | None: """Get the color code for the given color. RGB colors are converted to XTerm-256 colors if use_xterm_colors is True. If no_color is True, returns None. Otherwise, returns the RGB color. Args: color (graphics.Color | None): the color to get the code for Returns: str | int | None: the color code """ if color: if self.no_color: return None if self.use_xterm_colors: if color.xterm_color is not None: return color.xterm_color if color.rgb_color in self.xterm_color_map: return self.xterm_color_map[color.rgb_color] xterm_color = hexterm.hex_to_xterm(color.rgb_color) self.xterm_color_map[color.rgb_color] = xterm_color return xterm_color return color.rgb_color return None def new_scene( self, *, is_looping: bool = False, sync: Scene.SyncMetric | None = None, ease: easing.EasingFunction | None = None, scene_id: str = "", ) -> Scene: """Create a new Scene and adds it to the Animation. If no ID is provided, a unique ID is generated. Args: scene_id (str): Unique name for the scene. Used to query for the scene. is_looping (bool): Whether the scene should loop. sync (Scene.SyncMetric): The type of sync to use for the scene. ease (easing.EasingFunction): The easing function to use for the scene. Returns: Scene: the new Scene """ if not scene_id: found_unique = False current_id = len(self.scenes) while not found_unique: scene_id = f"{current_id}" if scene_id not in self.scenes: found_unique = True else: current_id += 1 if self.existing_color_handling == "always": preexisting_colors = graphics.ColorPair(fg=self.input_fg_color, bg=self.input_bg_color) else: preexisting_colors = None new_scene = Scene( scene_id=scene_id, is_looping=is_looping, sync=sync, ease=ease, no_color=self.no_color, use_xterm_colors=self.use_xterm_colors, ) new_scene.preexisting_colors = preexisting_colors self.scenes[scene_id] = new_scene return new_scene def query_scene(self, scene_id: str) -> Scene: """Return a Scene from the Animation. If the scene doesn't exist, raises a ValueError. Args: scene_id (str): the ID of the Scene Raises: ValueError: if the Scene does not exist Returns: Scene: the Scene """ scene = self.scenes.get(scene_id, None) if not scene: raise SceneNotFoundError(scene_id) return scene def active_scene_is_complete(self) -> bool: """Return whether the active scene is complete. A scene is complete if all sequences have been played. Looping scenes are always complete. Returns: bool: True if complete, False otherwise """ return bool(not self.active_scene or not self.active_scene.frames or self.active_scene.is_looping) def set_appearance(self, symbol: str, colors: graphics.ColorPair | None = None) -> None: """Update the current character visual with the symbol and colors provided. If the character has an active scene, any appearance set with this method will be overwritten when the scene is stepped to the next frame. Args: symbol (str): The symbol to apply. colors (graphics.ColorPair | None): The colors to apply. """ if colors is None: colors = graphics.ColorPair(fg=None, bg=None) # override fg and bg colors if they are set in the Scene due to existing color handling = always if self.existing_color_handling == "always": if self.input_fg_color: colors = graphics.ColorPair(fg=self.input_fg_color, bg=colors.bg_color) if self.input_bg_color: colors = graphics.ColorPair(fg=colors.fg_color, bg=self.input_bg_color) char_vis_fg_color: str | int | None = self._get_color_code(colors.fg_color) char_vis_bg_color: str | int | None = self._get_color_code(colors.bg_color) self.current_character_visual = CharacterVisual( symbol, colors=colors, _fg_color_code=char_vis_fg_color, _bg_color_code=char_vis_bg_color, ) @staticmethod def adjust_color_brightness(color: graphics.Color, brightness: float) -> graphics.Color: """Adjust the brightness of a given color. Args: color (Color): The color code to adjust. brightness (float): The brightness adjustment factor. Returns: Color: The adjusted color code. """ def hue_to_rgb(lightness_scaled: float, color_intensity: float, hue_value: float) -> float: """Convert a hue value to an RGB value component. This function is a helper function used in the conversion from HSL (Hue, Saturation, Lightness) color space to RGB (Red, Green, Blue) color space. It takes in three parameters: lightness_scaled, color_intensity, and hue_value. These parameters are derived from the HSL color space and are used to calculate the corresponding RGB value. Args: lightness_scaled (float): The lightness value from the HSL color space, scaled and shifted to be used in the RGB conversion. color_intensity (float): The intensity of the color, used to adjust the RGB values. hue_value (float): The hue value from the HSL color space, used to calculate the RGB values. Returns: float: The calculated RGB component. """ if hue_value < 0: hue_value += 1 if hue_value > 1: hue_value -= 1 if hue_value < 1 / 6: return lightness_scaled + (color_intensity - lightness_scaled) * 6 * hue_value if hue_value < 1 / 2: return color_intensity if hue_value < 2 / 3: return lightness_scaled + (color_intensity - lightness_scaled) * (2 / 3 - hue_value) * 6 return lightness_scaled normalized_red = int(color.rgb_color[0:2], 16) / 255 normalized_green = int(color.rgb_color[2:4], 16) / 255 normalized_blue = int(color.rgb_color[4:6], 16) / 255 # Convert RGB to HSL max_val = max(normalized_red, normalized_green, normalized_blue) min_val = min(normalized_red, normalized_green, normalized_blue) lightness = (max_val + min_val) / 2 if max_val == min_val: hue_value = saturation = 0.0 # achromatic else: diff = max_val - min_val lightness_threshold = 0.5 saturation = ( diff / (2 - max_val - min_val) if lightness > lightness_threshold else diff / (max_val + min_val) ) if max_val == normalized_red: hue_value = (normalized_green - normalized_blue) / diff + ( 6 if normalized_green < normalized_blue else 0 ) elif max_val == normalized_green: hue_value = (normalized_blue - normalized_red) / diff + 2 else: hue_value = (normalized_red - normalized_green) / diff + 4 hue_value /= 6 # Adjust lightness lightness = max(min(lightness * brightness, 1), 0) # Convert back to RGB if saturation == 0: red = green = blue = lightness # achromatic else: color_intensity = ( lightness * (1 + saturation) if lightness < lightness_threshold # type: ignore[unbound] else lightness + saturation - lightness * saturation ) lightness_scaled = 2 * lightness - color_intensity red = hue_to_rgb(lightness_scaled, color_intensity, hue_value + 1 / 3) green = hue_to_rgb(lightness_scaled, color_intensity, hue_value) blue = hue_to_rgb(lightness_scaled, color_intensity, hue_value - 1 / 3) # Convert to hex adjusted_color = f"{int(red * 255):02x}{int(green * 255):02x}{int(blue * 255):02x}" return graphics.Color(adjusted_color) def _ease_animation(self, easing_func: easing.EasingFunction) -> float: """Return the percentage of total distance that should be moved based on the easing function. Args: easing_func (easing.EasingFunction): The easing function to use. Returns: float: The percentage of total distance to move. """ if self.active_scene is None: return 0 elapsed_step_ratio = self.active_scene.easing_current_step / self.active_scene.easing_total_steps return easing_func(elapsed_step_ratio) def step_animation(self) -> None: """Progress the Scene and apply the next visual to the character. If the active scene is complete, a SCENE_COMPLETE event is triggered. """ if self.active_scene and self.active_scene.frames: # if the active scene is synced to movement, calculate the sequence index based on the # current waypoint progress if self.active_scene.sync: if self.character.motion.active_path: if self.active_scene.sync == Scene.SyncMetric.STEP: sequence_index = round( (len(self.active_scene.frames) - 1) * ( max(self.character.motion.active_path.current_step, 1) / max(self.character.motion.active_path.max_steps, 1) ), ) elif self.active_scene.sync == Scene.SyncMetric.DISTANCE: sequence_index = round( (len(self.active_scene.frames) - 1) * ( max( max(self.character.motion.active_path.total_distance, 1) - max( self.character.motion.active_path.total_distance - self.character.motion.active_path.last_distance_reached, 1, ), 1, ) / max(self.character.motion.active_path.total_distance, 1) ), ) try: self.current_character_visual = self.active_scene.frames[sequence_index].character_visual # type: ignore[unbound] except IndexError: self.current_character_visual = self.active_scene.frames[-1].character_visual # when the active waypoint has been deactivated, use the final symbol in the scene and finish the scene else: self.current_character_visual = self.active_scene.frames[-1].character_visual self.active_scene.played_frames.extend(self.active_scene.frames) self.active_scene.frames.clear() elif self.active_scene and self.active_scene.ease: easing_factor = self._ease_animation(self.active_scene.ease) frame_index = round(easing_factor * max(self.active_scene.easing_total_steps - 1, 0)) frame_index = max(min(frame_index, self.active_scene.easing_total_steps - 1), 0) frame = self.active_scene.frame_index_map[frame_index] self.current_character_visual = frame.character_visual self.active_scene.easing_current_step += 1 if self.active_scene.easing_current_step == self.active_scene.easing_total_steps: if self.active_scene.is_looping: self.active_scene.easing_current_step = 0 else: self.active_scene.played_frames.extend(self.active_scene.frames) self.active_scene.frames.clear() else: self.current_character_visual = self.active_scene.get_next_visual() if self.active_scene_is_complete(): completed_scene = self.active_scene if not self.active_scene.is_looping: self.active_scene.reset_scene() self.active_scene = None self.character.event_handler._handle_event( self.character.event_handler.Event.SCENE_COMPLETE, completed_scene, ) def activate_scene(self, scene: Scene) -> None: """Set the active scene and updates the current character visual. A SCENE_ACTIVATED event is triggered. Args: scene (Scene): the Scene to set as active """ self.active_scene = scene self.active_scene_current_step = 0 self.current_character_visual = self.active_scene.activate() self.character.event_handler._handle_event(self.character.event_handler.Event.SCENE_ACTIVATED, scene) def deactivate_scene(self, scene: Scene) -> None: """Deactivates a scene. Args: scene (Scene): the Scene to deactivate """ if self.active_scene is scene: self.active_scene = None terminaltexteffects-release-0.12.1/terminaltexteffects/engine/base_character.py000066400000000000000000000374141507200677100301340ustar00rootroot00000000000000"""EffectCharacter class and EventHandler class used to manage the state of a single character from the input data.""" from __future__ import annotations import typing from dataclasses import dataclass from enum import Enum, auto from terminaltexteffects.engine import animation, motion from terminaltexteffects.utils.exceptions import ( EventRegistrationCallerError, EventRegistrationTargetError, ) from terminaltexteffects.utils.geometry import Coord class EventHandler: """Register and handle events related to a character. Events related to character state changes (e.g. scene complete) can be registered with the EventHandler. When an event is triggered, the EventHandler will take the specified action (e.g. activate a Path). The EventHandler is used by the EffectCharacter class to handle events related to the character. Attributes: character (EffectCharacter): The character that the EventHandler is handling events for. registered_events (dict[tuple[Event, str], list[tuple[Action, str]]]): A dictionary of registered events. The key is a tuple of the event and the caller ID (waypoint id/scene id). The value is a list of tuples of the action and the action target (path id/scene id). layer (int): The layer of the character. The layer determines the order in which characters are printed. Note: SEGMENT_ENTERED/EXITED events will trigger the first time the character enters or exits a segment. If looping, each loop will trigger the event, but not backwards motion as is possible with the bounce easing functions. """ def __init__(self, character: EffectCharacter) -> None: """Initialize the instance with the EffectCharacter object. Args: character (EffectCharacter): The character for which the EventHandler is handling events. """ self.character = character self.registered_events: dict[ tuple[EventHandler.Event, animation.Scene | motion.Waypoint | motion.Path], list[ tuple[ EventHandler.Action, animation.Scene | motion.Waypoint | motion.Path | int | Coord | EventHandler.Callback | None, ] ], ] = {} class Event(Enum): """An Event that can be registered with the EventHandler. Register Events with the EventHandler using the register_event method of the EventHandler class. Attributes: SEGMENT_ENTERED (Event): A path segment has been entered. SEGMENT_EXITED (Event): A path segment has been exited. PATH_ACTIVATED (Event): A path has been activated. PATH_COMPLETE (Event): A path has been completed. PATH_HOLDING (Event): A path has entered the holding state. SCENE_ACTIVATED (Event): An animation scene has been activated. SCENE_COMPLETE (Event): An animation scene has completed. """ SEGMENT_ENTERED = auto() SEGMENT_EXITED = auto() PATH_ACTIVATED = auto() PATH_COMPLETE = auto() PATH_HOLDING = auto() SCENE_ACTIVATED = auto() SCENE_COMPLETE = auto() class Action(Enum): """Actions that can be taken when an event is triggered. An Action is taken when an Event is triggered. Register Actions with the EventHandler using the register_event method of the EventHandler class. Attributes: ACTIVATE_PATH (Action): Activates a path. The action target is the path ID. ACTIVATE_SCENE (Action): Activates an animation scene. The action target is the scene ID. DEACTIVATE_PATH (Action): Deactivates a path. The action target is the path ID. DEACTIVATE_SCENE (Action): Deactivates an animation scene. The action target is the scene ID. RESET_APPEARANCE (Action): Resets the appearance of the character to the input symbol and color. SET_LAYER (Action): Sets the layer of the character. The action target is the layer number. SET_COORDINATE (Action): Sets the coordinate of the character. The action target is the coordinate. CALLBACK (Action): Calls a callback function. The action target is an EventHandler.Callback object. """ ACTIVATE_PATH = auto() ACTIVATE_SCENE = auto() DEACTIVATE_PATH = auto() DEACTIVATE_SCENE = auto() RESET_APPEARANCE = auto() SET_LAYER = auto() SET_COORDINATE = auto() CALLBACK = auto() @dataclass(init=False) class Callback: """A callback action target that can be taken when an event is triggered. Register callback actions with the EventHandler using the register_event method of the EventHandler class. The callback function will be called with the character and any additional arguments when the event is triggered. The character will be the first argument passed to the callback function followed by any additional arguments in the order they were passed to the Callback object. Example: Create a callback to set the character's visibility to False. The following code would be used within an effect where 'self' is the EffectIterator instance: cb = EventHandler.Callback(lambda c: self.terminal.set_character_visibility(c, is_visible=False)) """ callback: typing.Callable args: tuple[typing.Any, ...] def __init__(self, callback: typing.Callable, *args: typing.Any) -> None: """Initialize the instance with the callback function and arguments. Args: callback (typing.Callable): The callback function to call. args (tuple[typing.Any,...]): A tuple of arguments to pass to the callback function. The first argument will be the character, followed by any additional arguments. """ self.callback = callback self.args = args @typing.overload def register_event( self, event: Event, caller: animation.Scene | motion.Waypoint | motion.Path, action: typing.Literal[Action.ACTIVATE_SCENE, Action.DEACTIVATE_SCENE], target: animation.Scene, ) -> None: ... @typing.overload def register_event( self, event: Event, caller: animation.Scene | motion.Waypoint | motion.Path, action: typing.Literal[Action.ACTIVATE_PATH, Action.DEACTIVATE_PATH], target: motion.Path, ) -> None: ... @typing.overload def register_event( self, event: Event, caller: animation.Scene | motion.Waypoint | motion.Path, action: typing.Literal[Action.SET_COORDINATE], target: Coord, ) -> None: ... @typing.overload def register_event( self, event: Event, caller: animation.Scene | motion.Waypoint | motion.Path, action: typing.Literal[Action.SET_LAYER], target: int, ) -> None: ... @typing.overload def register_event( self, event: Event, caller: animation.Scene | motion.Waypoint | motion.Path, action: typing.Literal[Action.CALLBACK], target: Callback, ) -> None: ... @typing.overload def register_event( self, event: Event, caller: animation.Scene | motion.Waypoint | motion.Path, action: typing.Literal[Action.RESET_APPEARANCE], ) -> None: ... def register_event( self, event: Event, caller: animation.Scene | motion.Waypoint | motion.Path, action: Action, target: animation.Scene | motion.Path | int | Coord | Callback | None = None, ) -> None: """Register an event to be handled by the EventHandler. Args: event (Event): The event to register. caller (animation.Scene | motion.Waypoint | motion.Path): The object that triggers the event. action (Action): The action to take when the event is triggered. target (animation.Scene | motion.Path | int | Coord | Callback): The target of the action. Raises: ValueError: If the caller or target object is not the correct type for the event or action. Example: Register an event to activate a scene when a Path is complete: `event_handler.register_event(EventHandler.Event.PATH_COMPLETE, some_path, EventHandler.Action.ACTIVATE_SCENE, some_scene)` """ event_caller_map = { EventHandler.Event.SEGMENT_ENTERED: motion.Waypoint, EventHandler.Event.SEGMENT_EXITED: motion.Waypoint, EventHandler.Event.PATH_ACTIVATED: motion.Path, EventHandler.Event.PATH_COMPLETE: motion.Path, EventHandler.Event.PATH_HOLDING: motion.Path, EventHandler.Event.SCENE_ACTIVATED: animation.Scene, EventHandler.Event.SCENE_COMPLETE: animation.Scene, } action_target_map = { EventHandler.Action.ACTIVATE_PATH: motion.Path, EventHandler.Action.ACTIVATE_SCENE: animation.Scene, EventHandler.Action.DEACTIVATE_PATH: motion.Path, EventHandler.Action.DEACTIVATE_SCENE: animation.Scene, EventHandler.Action.RESET_APPEARANCE: type(None), EventHandler.Action.SET_LAYER: int, EventHandler.Action.SET_COORDINATE: Coord, EventHandler.Action.CALLBACK: EventHandler.Callback, } if event_caller_map[event] != caller.__class__: raise EventRegistrationCallerError(event, caller, event_caller_map[event]) if (action is EventHandler.Action.RESET_APPEARANCE and target is not None) or ( action_target_map[action] != target.__class__ ): raise EventRegistrationTargetError(action, target, action_target_map[action]) new_event = (event, caller) new_action = (action, target) if new_event not in self.registered_events: self.registered_events[new_event] = [] self.registered_events[new_event].append(new_action) def _handle_event(self, event: Event, caller: animation.Scene | motion.Waypoint | motion.Path) -> None: """Handle an event by taking the specified action. Args: event (Event): An event to handle. If the event is not registered, nothing happens. caller (animation.Scene | motion.Waypoint | motion.Path): The object triggering the call. """ action_map = { EventHandler.Action.ACTIVATE_PATH: self.character.motion.activate_path, EventHandler.Action.ACTIVATE_SCENE: self.character.animation.activate_scene, EventHandler.Action.DEACTIVATE_PATH: self.character.motion.deactivate_path, EventHandler.Action.DEACTIVATE_SCENE: self.character.animation.deactivate_scene, EventHandler.Action.RESET_APPEARANCE: lambda _: self.character.animation.set_appearance( self.character.input_symbol, ), EventHandler.Action.SET_LAYER: lambda layer: setattr(self.character, "layer", layer), EventHandler.Action.SET_COORDINATE: lambda coord: setattr(self.character.motion, "current_coord", coord), EventHandler.Action.CALLBACK: lambda callback: callback.callback(self.character, *callback.args), } if (event, caller) not in self.registered_events: return for event_action in self.registered_events[(event, caller)]: action, target = event_action action_map[action](target) # type: ignore[operator] class EffectCharacter: """A class representing a single character from the input data. EffectCharacters are managed by the Terminal and are used to apply animations and effects to individual characters. Add an EffectCharacter to the Terminal using the add_character method of the Terminal class. Methods: tick: Progress the character's animation and motion by one step. Attributes: character_id (int): The unique ID of the character, generated by the Terminal. input_symbol (str): The symbol for the character in the input data. input_coord (Coord): The coordinate of the character in the input data. is_visible (bool): Whether the character is currently visible and should be printed to the terminal. animation (graphics.Animation): The animation object that controls the character's appearance. motion (motion.Motion): The motion object that controls the character's movement. event_handler (EventHandler): The event handler object that handles events related to the character. layer (int): The layer of the character. The layer determines the order in which characters are printed. is_fill_character (bool): Whether the character is a fill character. Fill characters are used to fill the empty cells of the Canvas. """ def __init__(self, character_id: int, symbol: str, input_column: int, input_row: int) -> None: """Initialize the character instance with the character ID, symbol, and input coordinates. Args: character_id (int): The unique ID of the character, generated by the Terminal. symbol (str): The symbol for the character in the input data. input_column (int): The column of the character in the input data. input_row (int): The row of the character in the input data. """ self._character_id: int = character_id self._input_symbol: str = symbol self._input_coord: Coord = Coord(input_column, input_row) self._input_ansi_sequences: dict[str, str | None] = {"fg_color": None, "bg_color": None} self._is_visible: bool = False self.animation: animation.Animation = animation.Animation(self) self.motion: motion.Motion = motion.Motion(self) self.event_handler: EventHandler = EventHandler(self) self.layer: int = 0 self.is_fill_character = False @property def input_symbol(self) -> str: """The symbol for the character in the input data.""" return self._input_symbol @property def input_coord(self) -> Coord: """The coordinate of the character in the input data.""" return self._input_coord @property def is_visible(self) -> bool: """Whether the character is currently visible and should be printed to the terminal.""" return self._is_visible @property def character_id(self) -> int: """The unique ID of the character, generated by the Terminal.""" return self._character_id @property def is_active(self) -> bool: """Returns whether the character is currently active. A character is active if its animation or motion is not complete. Returns: bool: True if the character is active, False if not. """ return bool(not self.animation.active_scene_is_complete() or not self.motion.movement_is_complete()) def tick(self) -> None: """Progress the character's animation and motion by one step.""" self.motion.move() self.animation.step_animation() def __hash__(self) -> int: """Return the hash value of the character.""" return hash(self.character_id) def __eq__(self, other: object) -> bool: """Check if two EffectCharacter instances are equal based on their character_id.""" if not isinstance(other, EffectCharacter): return NotImplemented return self.character_id == other.character_id def __repr__(self) -> str: """Return a string representation of the EffectCharacter instance.""" return ( f"EffectCharacter(character_id={self.character_id}, symbol='{self.input_symbol}', " f"input_column={self.input_coord.column}, input_row={self.input_coord.row})" ) terminaltexteffects-release-0.12.1/terminaltexteffects/engine/base_effect.py000066400000000000000000000140441507200677100274260ustar00rootroot00000000000000"""Base classes for all effects. Base classes from which all effects should inherit. These classes define the basic structure for an effect and establish the effect iterator interface as well as the effect configuration and terminal configuration. Classes: BaseEffectIterator(Generic[T]): An abstract base class that defines the basic structure for an iterator that applies a certain effect to the input data. Provides initilization for the effect configuration and terminal as well as the `__iter__` method. BaseEffect(Generic[T]): An abstract base class that defines the basic structure for an effect. Provides the `__iter__` method and a context manager for terminal output. """ from __future__ import annotations from abc import ABC, abstractmethod from contextlib import contextmanager from copy import deepcopy from typing import TYPE_CHECKING, Generic, TypeVar from terminaltexteffects.engine.terminal import Terminal, TerminalConfig from terminaltexteffects.utils.argsdataclass import ArgsDataClass if TYPE_CHECKING: from collections.abc import Generator from terminaltexteffects.engine.base_character import EffectCharacter T = TypeVar("T", bound=ArgsDataClass) class BaseEffectIterator(ABC, Generic[T]): """Base iterator class for all effects. Args: effect (BaseEffect): Effect to apply to the input data. Attributes: config (T): Configuration for the effect. terminal (Terminal): Terminal to use for output. active_characters (set[EffectCharacter]): Set of active characters in the effect. preexisting_colors_present (bool): Whether any characters in the input data have preexisting colors. Properties: frame (str): Current frame of the effect. Methods: update: Run the tick method for all active characters and remove inactive characters from the active list. __iter__: Return the iterator object. __next__: Return the next frame of the effect. """ def __init__(self, effect: BaseEffect) -> None: """Initialize the iterator with the Effect. Args: effect (BaseEffect): Effect to apply to the input data. """ self.config: T = deepcopy(effect.effect_config) self.terminal = Terminal(effect.input_data, deepcopy(effect.terminal_config)) self.active_characters: set[EffectCharacter] = set() self.preexisting_colors_present: bool = any( any((character.animation.input_fg_color, character.animation.input_bg_color)) for character in self.terminal.get_characters() ) @property def frame(self) -> str: """Return the current frame by getting the formatted output string from the terminal. If the frame rate is set >0 in the terminal configuration, enforce the frame rate. Returns: str: Current frame of the effect. """ if self.terminal._frame_rate: self.terminal.enforce_framerate() return self.terminal.get_formatted_output_string() def update(self) -> None: """Run the tick method for all active characters. Remove inactive characters from the active_characters set. """ for character in self.active_characters: character.tick() self.active_characters -= {character for character in self.active_characters if not character.is_active} def __iter__(self) -> BaseEffectIterator: """Return the iterator object. Returns: BaseEffectIterator: Iterator object. """ return self @abstractmethod def __next__(self) -> str: """Return the next frame of the effect. Perform any necessary updates to the effect to progress the effect logic and return the next frame. Raises: NotImplementedError: This method must be implemented by the subclass. Returns: str: Next frame of the effect. """ raise NotImplementedError class BaseEffect(ABC, Generic[T]): """Base iterable class for all effects. Base class for all effects. Provides the `__iter__` method and a context manager for terminal output. Attributes: input_data (str): Text to which the effect will be applied. effect_config (T): Configuration for the effect. terminal_config (TerminalConfig): Configuration for the terminal. """ @property @abstractmethod def _config_cls(self) -> type[T]: """Effect configuration class as a subclass of ArgsDataClass.""" raise NotImplementedError @property @abstractmethod def _iterator_cls(self) -> type[BaseEffectIterator]: """Effect iterator class as a subclass of BaseEffectIterator.""" raise NotImplementedError def __init__(self, input_data: str) -> None: """Initialize the effect with the input data. Args: input_data (str): Text to which the effect will be applied. """ self.input_data = input_data self.effect_config = self._config_cls() self.terminal_config = TerminalConfig() def __iter__(self) -> BaseEffectIterator: """Return the iterator object. Returns: BaseEffectIterator: Iterator object. """ return self._iterator_cls(self) @contextmanager def terminal_output(self, end_symbol: str = "\n") -> Generator[Terminal, None, None]: """Context manager for terminal output. Prepares the terminal for output and restores it after. Args: end_symbol (str, optional): Symbol to print after the effect has completed. Defaults to newline. Yields: Terminal: Terminal object for handling output. Raises: Exception: Any exception that occurs within the context manager will be raised before restoring the terminal state. """ terminal = Terminal(self.input_data, self.terminal_config) try: terminal.prep_canvas() yield terminal finally: terminal.restore_cursor(end_symbol) terminaltexteffects-release-0.12.1/terminaltexteffects/engine/motion.py000066400000000000000000000527251507200677100265150ustar00rootroot00000000000000"""Classes and methods for managing and manipulating character motion. Classes: Waypoint: A Waypoint comprises a coordinate, speed, and, optionally, bezier control point(s). Segment: A segment of a path consisting of two waypoints and the distance between them. Path: Represents a path consisting of multiple waypoints for motion. Motion: Motion class for managing the movement of a character. """ from __future__ import annotations import typing from dataclasses import dataclass from terminaltexteffects.utils import easing, geometry from terminaltexteffects.utils.exceptions import ( ActivateEmptyPathError, DuplicatePathIDError, DuplicateWaypointIDError, PathInvalidSpeedError, PathNotFoundError, WaypointNotFoundError, ) from terminaltexteffects.utils.geometry import Coord if typing.TYPE_CHECKING: from terminaltexteffects.engine import base_character # pragma: no cover @dataclass(frozen=True) class Waypoint: """A Waypoint comprises a coordinate, speed, and, optionally, bezier control point(s). Attributes: waypoint_id (str): unique identifier for the waypoint coord (Coord): coordinate bezier_control (tuple[Coord, ...] | None): coordinate of the control point for a bezier curve. Defaults to None. """ waypoint_id: str coord: Coord bezier_control: tuple[Coord, ...] | None = None @dataclass class Segment: """A segment of a path consisting of two waypoints and the distance between them. Segments are created by the Path class. The start waypoint is the end waypoint of the previous segment or the origin waypoint. Attributes: start (Waypoint): start waypoint end (Waypoint): end waypoint distance (float): distance between the start and end waypoints """ start: Waypoint end: Waypoint distance: float def __post_init__(self) -> None: """Initialize additional attributes for the Segment class.""" self.enter_event_triggered: bool = False self.exit_event_triggered: bool = False def __eq__(self, other: object) -> bool: """Check if two Segment objects are equal. Segments are equal if their start and end waypoints are equal. """ if not isinstance(other, Segment): return NotImplemented return self.start == other.start and self.end == other.end def __hash__(self) -> int: """Return the hash value of the Segment. Hash is calculated using a tuple of the start and end waypoints. """ return hash((self.start, self.end)) @dataclass class Path: """Represents a path consisting of multiple waypoints for motion. Attributes: path_id (str): The unique identifier for the path. speed (float): speed > 0 ease (easing.EasingFunction | None): easing function for character movement. Defaults to None. layer (int | None): layer to move the character to, if None, layer is unchanged. Defaults to None. hold_time (int): number of frames to hold the character at the end of the path. Defaults to 0. loop (bool): Whether the path should loop back to the beginning. Default is False. Methods: new_waypoint: Creates a new Waypoint and appends adds it to the Path. query_waypoint: Returns the waypoint with the given waypoint_id. step: Progresses to the next step along the path and returns the coordinate at that step. """ path_id: str speed: float = 1.0 ease: easing.EasingFunction | None = None layer: int | None = None hold_time: int = 0 loop: bool = False def __post_init__(self) -> None: """Initialize the Path object and calculates the total distance and maximum steps.""" self.segments: list[Segment] = [] self.waypoints: list[Waypoint] = [] self.waypoint_lookup: dict[str, Waypoint] = {} self.total_distance: float = 0 self.current_step: int = 0 self.max_steps: int = 0 self.hold_time_remaining = self.hold_time self.last_distance_reached: float = 0 # used for animation syncing to distance self.origin_segment: Segment | None = None if self.speed <= 0: raise PathInvalidSpeedError(self.speed) def new_waypoint( self, coord: Coord, *, bezier_control: tuple[Coord, ...] | Coord | None = None, waypoint_id: str = "", ) -> Waypoint: """Create a new Waypoint and appends adds it to the Path. Args: waypoint_id (str): Unique identifier for the waypoint. Used to query for the waypoint. coord (Coord): coordinate bezier_control (tuple[Coord, ...] | Coord | None): coordinate of the control point for a bezier curve. Defaults to None. Returns: Waypoint: The new waypoint. """ if not waypoint_id: found_unique = False current_id = len(self.waypoints) while not found_unique: waypoint_id = f"{current_id}" if waypoint_id not in self.waypoint_lookup: found_unique = True else: current_id += 1 if waypoint_id in self.waypoint_lookup: raise DuplicateWaypointIDError(waypoint_id) bezier_control_tuple: tuple[Coord, ...] | None if bezier_control and isinstance(bezier_control, Coord): bezier_control_tuple = (bezier_control,) elif bezier_control and isinstance(bezier_control, tuple): bezier_control_tuple = bezier_control else: bezier_control_tuple = None new_waypoint = Waypoint(waypoint_id, coord, bezier_control=bezier_control_tuple) self._add_waypoint_to_path(new_waypoint) return new_waypoint def _add_waypoint_to_path(self, waypoint: Waypoint) -> None: """Add a waypoint to the path and update the total distance and maximum steps. Args: waypoint (Waypoint): waypoint to add """ self.waypoint_lookup[waypoint.waypoint_id] = waypoint self.waypoints.append(waypoint) if len(self.waypoints) < 2: return if waypoint.bezier_control: distance_from_previous = geometry.find_length_of_bezier_curve( self.waypoints[-2].coord, waypoint.bezier_control, waypoint.coord, ) else: distance_from_previous = geometry.find_length_of_line( self.waypoints[-2].coord, waypoint.coord, ) self.total_distance += distance_from_previous self.segments.append(Segment(self.waypoints[-2], waypoint, distance_from_previous)) self.max_steps = round(self.total_distance / self.speed) def query_waypoint(self, waypoint_id: str) -> Waypoint: """Return the waypoint with the given waypoint_id. Args: waypoint_id (str): waypoint_id Returns: Waypoint: The waypoint with the given waypoint_id. """ waypoint = self.waypoint_lookup.get(waypoint_id, None) if not waypoint: raise WaypointNotFoundError(waypoint_id) return waypoint def step(self, event_handler: base_character.EventHandler) -> Coord: """Progresses to the next step along the path and returns the coordinate at that step. This method is called by the Motion.move() method. It calculates the next coordinate based on the current step, total distance, bezier control points, and the easing function if provided. It also handles the triggering of segment enter and exit events. Args: event_handler (base_character.EventHandler): The EventHandler for the character. Returns: Coord: The next coordinate on the path. """ if not self.max_steps or self.current_step >= self.max_steps or not self.total_distance: # if the path has zero distance or there are no more steps, return the final waypoint coordinate return self.segments[-1].end.coord self.current_step += 1 if self.ease: distance_factor = self.ease(self.current_step / self.max_steps) else: distance_factor = self.current_step / self.max_steps distance_to_travel = distance_factor * self.total_distance self.last_distance_reached = distance_to_travel for segment in self.segments: if distance_to_travel <= segment.distance: active_segment = segment if not segment.enter_event_triggered: segment.enter_event_triggered = True event_handler._handle_event(event_handler.Event.SEGMENT_ENTERED, segment.end) break distance_to_travel -= segment.distance if not segment.exit_event_triggered: segment.exit_event_triggered = True event_handler._handle_event(event_handler.Event.SEGMENT_EXITED, segment.end) # if the distance_to_travel is further than the last waypoint, # preserve the distance from the start of the final segment else: active_segment = self.segments[-1] distance_to_travel += active_segment.distance if active_segment.distance == 0: segment_distance_to_travel_factor = 0.0 elif self.ease: segment_distance_to_travel_factor = distance_to_travel / active_segment.distance else: segment_distance_to_travel_factor = min((distance_to_travel / active_segment.distance, 1)) if active_segment.end.bezier_control: next_coord = geometry.find_coord_on_bezier_curve( active_segment.start.coord, active_segment.end.bezier_control, active_segment.end.coord, segment_distance_to_travel_factor, ) else: next_coord = geometry.find_coord_on_line( active_segment.start.coord, active_segment.end.coord, segment_distance_to_travel_factor, ) return next_coord def __eq__(self, other: object) -> bool: """Check if two Path objects are equal. Args: other (object): object to compare Returns: bool: True if the two Path objects are equal, False otherwise. """ if not isinstance(other, Path): return NotImplemented return self.path_id == other.path_id def __hash__(self) -> int: """Return the hash value of the Path. Hash is calculated using the path_id. """ return hash(self.path_id) class Motion: """Motion class for managing the movement of a character. Attributes: paths (dict[str, Path]): dictionary of paths character (base_character.EffectCharacter): The EffectCharacter to move. current_coord (Coord): current coordinate previous_coord (Coord): previous coordinate active_path (Path | None): active path Methods: set_coordinate: Sets the current coordinate to the given coordinate. new_path: Creates a new Path and adds it to the Motion.paths dictionary with the path_id as key. query_path: Returns the path with the given path_id. movement_is_complete: Returns whether the character has an active path. chain_paths: Creates a chain of paths by registering activation events for each path such that paths[n] activates paths[n+1] when reached. If loop is True, paths[-1] activates paths[0] when reached. activate_path: Activates the first waypoint in the path. deactivate_path: Unsets the current path if the current path is path. move: Moves the character one step closer to the target position based on an easing function if present, otherwise linearly. """ def __init__(self, character: base_character.EffectCharacter) -> None: """Initialize the Motion object with the given EffectCharacter. Args: character (base_character.EffectCharacter): The EffectCharacter to move. """ self.paths: dict[str, Path] = {} self.character = character self.current_coord: Coord = Coord(character.input_coord.column, character.input_coord.row) self.previous_coord: Coord = Coord(-1, -1) self.active_path: Path | None = None def set_coordinate(self, coord: Coord) -> None: """Set the current coordinate to the given coordinate. Args: coord (Coord): coordinate """ self.current_coord = coord def new_path( self, *, speed: float = 1, ease: easing.EasingFunction | None = None, layer: int | None = None, hold_time: int = 0, loop: bool = False, path_id: str = "", ) -> Path: """Create a new Path and add it to the Motion.paths dictionary with the path_id as key. Args: speed (float, optional): speed > 0. Defaults to 1. ease (easing.EasingFunction | None, optional): easing function for character movement. Defaults to None. layer (int | None, optional): layer to move the character to, if None, layer is unchanged. Defaults to None. hold_time (int, optional): number of frames to hold the character at the end of the path. Defaults to 0. loop (bool, optional): Whether the path should loop back to the beginning. Default is False. path_id (str, optional): Unique identifier for the path. Used to query for the path. Defaults to "". Raises: ValueError: If a path with the provided id already exists. Returns: Path: The new path. """ if not path_id: found_unique = False current_id = len(self.paths) while not found_unique: path_id = f"{current_id}" if path_id not in self.paths: found_unique = True else: current_id += 1 if path_id in self.paths: raise DuplicatePathIDError(path_id) new_path = Path(path_id, speed, ease, layer, hold_time, loop) self.paths[path_id] = new_path return new_path def query_path(self, path_id: str) -> Path: """Return the path with the given path_id. Args: path_id (str): path_id Returns: Path: The path with the given path_id. """ path = self.paths.get(path_id, None) if not path: raise PathNotFoundError(path_id) return path def movement_is_complete(self) -> bool: """Return whether the character has an active path. Returns: bool: True if the character has no active path, False otherwise. """ return self.active_path is None def chain_paths(self, paths: list[Path], *, loop: bool = False) -> None: """Create a chain of paths. Paths are chained by registering activation events for each path such that paths[n] activates paths[n+1] when reached. If loop is True, paths[-1] activates paths[0] when reached. Args: paths (list[Path]): list of paths to chain loop (bool, optional): Whether the chain should loop. Defaults to False. """ if len(paths) < 2: return for i, path in enumerate(paths): if i == 0: continue self.character.event_handler.register_event( self.character.event_handler.Event.PATH_COMPLETE, paths[i - 1], self.character.event_handler.Action.ACTIVATE_PATH, path, ) if loop: self.character.event_handler.register_event( self.character.event_handler.Event.PATH_COMPLETE, paths[-1], self.character.event_handler.Action.ACTIVATE_PATH, paths[0], ) def activate_path(self, path: Path) -> None: """Activates the first waypoint in the given path and updates the path's properties accordingly. This method sets the active path to the given path, calculates the distance to the first waypoint, and updates the total distance of the path. If the path has an origin segment, it removes it from the segments list and subtracts its distance from the total distance. Then, it creates a new origin segment from the current coordinate to the first waypoint and inserts it at the beginning of the segments list. The method also resets the current step, hold time remaining, and max steps of the path based on the total distance and speed. It ensures that the enter and exit events for each segment are not triggered. If the path has a layer, it sets the character's layer to it. Finally, it triggers the PATH_ACTIVATED event for the character. Args: path (Path): The path to activate. """ if not path.waypoints: raise ActivateEmptyPathError(path.path_id) self.active_path = path first_waypoint = self.active_path.waypoints[0] if first_waypoint.bezier_control: distance_to_first_waypoint = geometry.find_length_of_bezier_curve( self.current_coord, first_waypoint.bezier_control, first_waypoint.coord, ) else: distance_to_first_waypoint = geometry.find_length_of_line( self.current_coord, first_waypoint.coord, ) self.active_path.total_distance += distance_to_first_waypoint if self.active_path.origin_segment: self.active_path.segments.pop(0) self.active_path.total_distance -= self.active_path.origin_segment.distance self.active_path.origin_segment = Segment( Waypoint("origin", self.current_coord), first_waypoint, distance_to_first_waypoint, ) self.active_path.segments.insert(0, self.active_path.origin_segment) self.active_path.current_step = 0 self.active_path.hold_time_remaining = self.active_path.hold_time self.active_path.max_steps = round(self.active_path.total_distance / self.active_path.speed) for segment in self.active_path.segments: segment.enter_event_triggered = False segment.exit_event_triggered = False if self.active_path.layer is not None: self.character.layer = self.active_path.layer self.character.event_handler._handle_event(self.character.event_handler.Event.PATH_ACTIVATED, self.active_path) def deactivate_path(self, path: Path) -> None: """Set the active path to None if the active path is the given path. Args: path (Path): the Path to deactivate """ if self.active_path and self.active_path is path: self.active_path = None def move(self) -> None: """Move the character along the active path. The character's current coordinate is updated to the next step in the active path. If the active path is completed, an event is triggered based on whether the path is set to loop or not. If the path is set to loop, the path is deactivated and then reactivated to start from the beginning. If not, the path is simply deactivated and a PATH_COMPLETE event is triggered. If the path has a hold time, the character will pause at the end of the path for the specified duration. During this hold time, a PATH_HOLDING event is triggered on the first frame, and the hold time is decremented on each subsequent frame until it reaches zero. If there is no active path or if the active path has no segments, the character does not move. The character's previous coordinate is preserved before moving to allow for clearing the location in the terminal. """ # preserve previous coordinate to allow for clearing the location in the terminal self.previous_coord = Coord(self.current_coord.column, self.current_coord.row) if not self.active_path or not self.active_path.segments: return self.current_coord = self.active_path.step(self.character.event_handler) if self.active_path.current_step == self.active_path.max_steps: if self.active_path.hold_time and self.active_path.hold_time_remaining == self.active_path.hold_time: self.character.event_handler._handle_event( self.character.event_handler.Event.PATH_HOLDING, self.active_path, ) self.active_path.hold_time_remaining -= 1 return if self.active_path.hold_time_remaining: self.active_path.hold_time_remaining -= 1 return if self.active_path.loop and len(self.active_path.segments) > 1: looping_path = self.active_path self.deactivate_path(self.active_path) self.activate_path(looping_path) else: self.completed_path = self.active_path self.deactivate_path(self.active_path) self.character.event_handler._handle_event( self.character.event_handler.Event.PATH_COMPLETE, self.completed_path, ) terminaltexteffects-release-0.12.1/terminaltexteffects/engine/terminal.py000066400000000000000000001442171507200677100270210ustar00rootroot00000000000000"""A module for managing the terminal state and output. Classes: TerminalConfig: Configuration for the terminal. Canvas: Represents the canvas in the terminal. The canvas is the area defined by the dimensions of the input data, unless specified otherwise in the TerminalConfig. Terminal: A class for managing the terminal state and output. """ from __future__ import annotations import random import re import shutil import sys import time import typing from dataclasses import dataclass from enum import Enum, auto from typing import Literal from terminaltexteffects.engine.base_character import EffectCharacter from terminaltexteffects.utils import ansitools, argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass from terminaltexteffects.utils.exceptions import ( InvalidCharacterGroupError, InvalidCharacterSortError, InvalidColorSortError, ) from terminaltexteffects.utils.geometry import Coord from terminaltexteffects.utils.graphics import Color @dataclass class TerminalConfig(ArgsDataClass): """Configuration for the terminal. Attributes: tab_width (int): Number of spaces to use for a tab character. xterm_colors (bool): Convert any colors specified in RBG hex to the closest XTerm-256 color. no_color (bool): Disable all colors in the effect. wrap_text (bool): Wrap text wider than the canvas width. frame_rate (float): Target frame rate for the animation in frames per second. Set to 0 to disable frame rate limiting. canvas_width (int): Canvas width, set to an integer > 0 to use a specific dimension, if set to 0 the canvas width is detected automatically based on the terminal device, if set to -1 the canvas width is based on the input data width. canvas_height (int): Canvas height, set to an integer > 0 to use a specific dimension, if set to 0 the canvas height is is detected automatically based on the terminal device, if set to -1 the canvas width is based on the input data height. anchor_canvas (Literal['sw','s','se','e','ne','n','nw','w','c']): Anchor point for the Canvas. The Canvas will be anchored in the terminal to the location corresponding to the cardinal/diagonal direction. Defaults to 'sw'. anchor_effect (Literal['sw','s','se','e','ne','n','nw','w','c']): Anchor point for the effect within the Canvas. Effect text will anchored in the Canvas to the location corresponding to the cardinal/diagonal direction. Defaults to 'sw'. ignore_terminal_dimensions (bool): Ignore the terminal dimensions and utilize the full Canvas beyond the extents of the terminal. Useful for sending frames to another output handler. existing_color_handling (Literal['always','dynamic','ignore']): Specify handling of existing ANSI color sequences in the input data. 'always' will always use the input colors, ignoring any effect specific colors. 'dynamic' will leave it to the effect implementation to apply input colors. 'ignore' will ignore the colors in the input data. Default is 'ignore'. """ tab_width: int = ArgField( cmd_name=["--tab-width"], type_parser=argvalidators.PositiveInt.type_parser, metavar=argvalidators.PositiveInt.METAVAR, default=4, help="Number of spaces to use for a tab character.", ) # type: ignore[assignment] "int : Number of spaces to use for a tab character." xterm_colors: bool = ArgField( cmd_name=["--xterm-colors"], default=False, action="store_true", help="Convert any colors specified in 24-bit RBG hex to the closest 8-bit XTerm-256 color.", ) # type: ignore[assignment] "bool : Convert any colors specified in 24-bit RBG hex to the closest 8-bit XTerm-256 color." no_color: bool = ArgField( cmd_name=["--no-color"], default=False, action="store_true", help="Disable all colors in the effect.", ) # type: ignore[assignment] "bool : Disable all colors in the effect." existing_color_handling: Literal["always", "dynamic", "ignore"] = ArgField( cmd_name=["--existing-color-handling"], default="ignore", choices=["always", "dynamic", "ignore"], help=( "Specify handling of existing 8-bit and 24-bit ANSI color sequences in the input data. 3-bit and 4-bit " "sequences are not supported. 'always' will always use the input colors, ignoring any effect specific " "colors. 'dynamic' will leave it to the effect implementation to apply input colors. 'ignore' will " "ignore the colors in the input data. Default is 'ignore'." ), ) # type: ignore[assignment] ( "Literal['always','dynamic','ignore'] : Specify handling of existing ANSI color sequences in the input data. " "'always' will always use the input colors, ignoring any effect specific colors. 'dynamic' will leave it to " "the effect implementation to apply input colors. 'ignore' will ignore the colors in the input data. " "Default is 'ignore'." ) wrap_text: int = ArgField( cmd_name="--wrap-text", default=False, action="store_true", help="Wrap text wider than the canvas width.", ) # type: ignore[assignment] "bool : Wrap text wider than the canvas width." frame_rate: int = ArgField( cmd_name="--frame-rate", type_parser=argvalidators.NonNegativeInt.type_parser, default=100, help="""Target frame rate for the animation in frames per second. Set to 0 to disable frame rate limiting.""", ) # type: ignore[assignment] "int : Target frame rate for the animation in frames per second. Set to 0 to disable frame rate limiting." canvas_width: int = ArgField( cmd_name=["--canvas-width"], metavar=argvalidators.CanvasDimension.METAVAR, type_parser=argvalidators.CanvasDimension.type_parser, default=-1, help=( "Canvas width, set to an integer > 0 to use a specific dimension, use 0 to match the terminal width, " "or use -1 to match the input text width." ), ) # type: ignore[assignment] ( "int : Canvas width, set to an integer > 0 to use a specific dimension, if set to 0 the canvas width is " "detected automatically based on the terminal device, if set to -1 the canvas width is based on " "the input data width." ) canvas_height: int = ArgField( cmd_name=["--canvas-height"], metavar=argvalidators.CanvasDimension.METAVAR, type_parser=argvalidators.CanvasDimension.type_parser, default=-1, help=( "Canvas height, set to an integer > 0 to use a specific dimension, use 0 to match the terminal " "height, or use -1 to match the input text height." ), ) # type: ignore[assignment] ( "int : Canvas height, set to an integer > 0 to use a specific dimension, if set to 0 the canvas height " "is is detected automatically based on the terminal device, if set to -1 the canvas width is " "based on the input data height." ) anchor_canvas: Literal["sw", "s", "se", "e", "ne", "n", "nw", "w", "c"] = ArgField( cmd_name=["--anchor-canvas"], choices=["sw", "s", "se", "e", "ne", "n", "nw", "w", "c"], default="sw", help=( "Anchor point for the canvas. The canvas will be anchored in the terminal to the location " "corresponding to the cardinal/diagonal direction." ), ) # type: ignore[assignment] ( "Literal['sw','s','se','e','ne','n','nw','w','c'] : Anchor point for the canvas. The canvas will be " "anchored in the terminal to the location corresponding to the cardinal/diagonal direction." ) anchor_text: Literal["n", "ne", "e", "se", "s", "sw", "w", "nw", "c"] = ArgField( cmd_name=["--anchor-text"], choices=["n", "ne", "e", "se", "s", "sw", "w", "nw", "c"], default="sw", help=( "Anchor point for the text within the Canvas. Input text will anchored in the Canvas to " "the location corresponding to the cardinal/diagonal direction." ), ) # type: ignore[assignment] ( "Literal['n','ne','e','se','s','sw','w','nw','c'] : Anchor point for the text within the Canvas. " "Input text will anchored in the Canvas to the location corresponding to the cardinal/diagonal direction." ) ignore_terminal_dimensions: bool = ArgField( cmd_name=["--ignore-terminal-dimensions"], default=False, action="store_true", help=( "Ignore the terminal dimensions and utilize the full Canvas beyond the extents of the terminal. " "Useful for sending frames to another output handler." ), ) # type: ignore[assignment] ( "bool : Ignore the terminal dimensions and utilize the full Canvas beyond the extents of the terminal. " "Useful for sending frames to another output handler." ) @dataclass class Canvas: """Represent the canvas in the terminal. The canvas is the area defined by the dimensions of the input data, unless specified otherwise in the TerminalConfig. This class provides methods for working with the canvas, such as checking if a coordinate is within the canvas, getting random coordinates within the canvas, and getting a random coordinate outside the canvas. This class also provides attributes for the dimensions of the canvas, the extents of the text within the canvas, and the center of the canvas. Args: top (int): top row of the canvas right (int): right column of the canvas bottom (int): bottom row of the canvas. Defaults to 1. left (int): left column of the canvas. Defaults to 1. Attributes: top (int): top row of the canvas right (int): right column of the canvas bottom (int): bottom row of the canvas left (int): left column of the canvas center_row (int): row of the center of the canvas center_column (int): column of the center of the canvas center (Coord): coordinate of the center of the canvas width (int): width of the canvas height (int): height of the canvas text_left (int): left column of the text within the canvas text_right (int): right column of the text within the canvas text_top (int): top row of the text within the canvas text_bottom (int): bottom row of the text within the canvas text_width (int): width of the text within the canvas text_height (int): height of the text within the canvas text_center_row (int): row of the center of the text within the canvas text_center_column (int): column of the center of the text within the canvas Methods: coord_is_in_canvas: Checks whether a coordinate is within the canvas. random_column: Get a random column position within the canvas. random_row: Get a random row position within the canvas. random_coord: Get a random coordinate within or outside the canvas. """ top: int """int: top row of the canvas""" right: int """int: right column of the canvas""" bottom: int = 1 """int: bottom row of the canvas""" left: int = 1 """int: left column of the canvas""" def __post_init__(self) -> None: """After initialization, calculate the center, width, height, and text dimensions of the canvas.""" self.center_row = max(self.top // 2, self.bottom) """int: row of the center of the canvas""" if self.top % 2 and self.top > 1: self.center_row += 1 self.center_column = max(self.right // 2, self.left) """int: column of the center of the canvas""" if self.right % 2 and self.right > 1: self.center_column += 1 self.center = Coord(self.center_column, self.center_row) """Coord: coordinate of the center of the canvas""" self.width = self.right """int: width of the canvas""" self.height = self.top """int: height of the canvas""" self.text_left = 0 """int: left column of the text within the canvas""" self.text_right = 0 """int: right column of the text within the canvas""" self.text_top = 0 """int: top row of the text within the canvas""" self.text_bottom = 0 """int: bottom row of the text within the canvas""" self.text_width = 0 """int: width of the text within the canvas""" self.text_height = 0 """int: height of the text within the canvas""" self.text_center_row = 0 """int: row of the center of the text within the canvas""" self.text_center_column = 0 """int: column of the center of the text within the canvas""" def _anchor_text( self, characters: list[EffectCharacter], anchor: Literal["n", "ne", "e", "se", "s", "sw", "w", "nw", "c"], ) -> list[EffectCharacter]: """Anchors the text within the canvas based on the specified anchor point. Args: characters (list[EffectCharacter]): _description_ anchor (Literal["n", "ne", "e", "se", "s", "sw", "w", "nw", "c"]): Anchor point for the text within the Canvas. Returns: list[EffectCharacter]: List of characters anchored within the canvas. Only returns characters with coordinates within the canvas after anchoring. """ # translate coordinate based on anchor within the canvas input_width = max([character._input_coord.column for character in characters]) input_height = max([character._input_coord.row for character in characters]) column_delta = row_delta = 0 if input_width != self.width: if anchor in ("s", "n", "c"): column_delta = self.center_column - (input_width // 2) elif anchor in ("se", "e", "ne"): column_delta = self.right - input_width elif anchor in ("sw", "w", "nw"): column_delta = self.left - 1 if input_height != self.height: if anchor in ("w", "e", "c"): row_delta = self.center_row - (input_height // 2) elif anchor in ("nw", "n", "ne"): row_delta = self.top - input_height elif anchor in ("sw", "s", "se"): row_delta = self.bottom - 1 for character in characters: current_coord = character.input_coord anchored_coord = Coord(current_coord.column + column_delta, current_coord.row + row_delta) character._input_coord = anchored_coord character.motion.set_coordinate(anchored_coord) characters = [character for character in characters if self.coord_is_in_canvas(character.input_coord)] # get text dimensions, centers, and extents self.text_left = min([character.input_coord.column for character in characters]) self.text_right = max([character.input_coord.column for character in characters]) self.text_top = max([character.input_coord.row for character in characters]) self.text_bottom = min([character.input_coord.row for character in characters]) self.text_width = max(self.text_right - self.text_left + 1, 1) self.text_height = max(self.text_top - self.text_bottom + 1, 1) self.text_center_row = self.text_bottom + ((self.text_top - self.text_bottom) // 2) self.text_center_column = self.text_left + ((self.text_right - self.text_left) // 2) return characters def coord_is_in_canvas(self, coord: Coord) -> bool: """Check whether a coordinate is within the canvas. Args: coord (Coord): coordinate to check Returns: bool: whether the coordinate is within the canvas """ return self.left <= coord.column <= self.right and self.bottom <= coord.row <= self.top def random_column(self) -> int: """Get a random column position. Position is within the canvas. Returns: int: a random column position (1 <= x <= canvas.right) """ return random.randint(self.left, self.right) def random_row(self) -> int: """Get a random row position. Position is within the canvas. Returns: int: a random row position (1 <= x <= terminal.canvas.top) """ return random.randint(self.bottom, self.top) def random_coord(self, *, outside_scope: bool = False) -> Coord: """Get a random coordinate. Coordinate is within the canvas unless outside_scope is True. Args: outside_scope (bool, optional): whether the coordinate should fall outside the canvas. Defaults to False. Returns: Coord: a random coordinate . Coordinate is within the canvas unless outside_scope is True. """ if outside_scope is True: random_coord_above = Coord(self.random_column(), self.top + 1) random_coord_below = Coord(self.random_column(), self.bottom - 1) random_coord_left = Coord(self.left - 1, self.random_row()) random_coord_right = Coord(self.right + 1, self.random_row()) return random.choice([random_coord_above, random_coord_below, random_coord_left, random_coord_right]) return Coord(self.random_column(), self.random_row()) class Terminal: """A class for managing the terminal state and output. Attributes: config (TerminalConfig): Configuration for the terminal. canvas (Canvas): The canvas in the terminal. character_by_input_coord (dict[Coord, EffectCharacter]): A dictionary of characters by their input coordinates. Methods: get_piped_input: Gets the piped input from stdin. prep_canvas: Prepares the terminal for the effect by adding empty lines and hiding the cursor. restore_cursor: Restores the cursor visibility. get_characters: Get a list of all EffectCharacters in the terminal with an optional sort. get_characters_grouped: Get a list of all EffectCharacters grouped by the specified CharacterGroup grouping. get_character_by_input_coord: Get an EffectCharacter by its input coordinates. set_character_visibility: Set the visibility of a character. get_formatted_output_string: Get the formatted output string based on the current terminal state. print: Prints the current terminal state to stdout while preserving the cursor position. """ ansi_sequence_color_map: typing.ClassVar[dict[str, Color]] = {} class CharacterGroup(Enum): """An enum specifying character groupings. Attributes: COLUMN_LEFT_TO_RIGHT: Group characters by column from left to right. COLUMN_RIGHT_TO_LEFT: Group characters by column from right to left. ROW_TOP_TO_BOTTOM: Group characters by row from top to bottom. ROW_BOTTOM_TO_TOP: Group characters by row from bottom to top. DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT: Group characters by diagonal from top left to bottom right. DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT: Group characters by diagonal from bottom left to top right. DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT: Group characters by diagonal from top right to bottom left. DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT: Group characters by diagonal from bottom right to top left. CENTER_TO_OUTSIDE_DIAMONDS: Group characters by distance from the center to the outside in diamond shapes. OUTSIDE_TO_CENTER_DIAMONDS: Group characters by distance from the outside to the center in diamond shapes. """ COLUMN_LEFT_TO_RIGHT = auto() COLUMN_RIGHT_TO_LEFT = auto() ROW_TOP_TO_BOTTOM = auto() ROW_BOTTOM_TO_TOP = auto() DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT = auto() DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT = auto() DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT = auto() DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT = auto() CENTER_TO_OUTSIDE_DIAMONDS = auto() OUTSIDE_TO_CENTER_DIAMONDS = auto() class CharacterSort(Enum): """An enum for specifying character sorts. Attributes: RANDOM: Random order. TOP_TO_BOTTOM_LEFT_TO_RIGHT: Top to bottom, left to right. TOP_TO_BOTTOM_RIGHT_TO_LEFT: Top to bottom, right to left. BOTTOM_TO_TOP_LEFT_TO_RIGHT: Bottom to top, left to right. BOTTOM_TO_TOP_RIGHT_TO_LEFT: Bottom to top, right to left. OUTSIDE_ROW_TO_MIDDLE: Outside row to middle. MIDDLE_ROW_TO_OUTSIDE: Middle row to outside. """ RANDOM = auto() TOP_TO_BOTTOM_LEFT_TO_RIGHT = auto() TOP_TO_BOTTOM_RIGHT_TO_LEFT = auto() BOTTOM_TO_TOP_LEFT_TO_RIGHT = auto() BOTTOM_TO_TOP_RIGHT_TO_LEFT = auto() OUTSIDE_ROW_TO_MIDDLE = auto() MIDDLE_ROW_TO_OUTSIDE = auto() class ColorSort(Enum): """An enum for specifying color sorts for the colors derived from the input text ansi sequences. Attributes: LEAST_TO_MOST: Sort colors from least to most frequent. MOST_TO_LEAST: Sort colors from most to least frequent. RANDOM: Random order. """ LEAST_TO_MOST = auto() MOST_TO_LEAST = auto() RANDOM = auto() def __init__(self, input_data: str, config: TerminalConfig | None = None) -> None: """Initialize the Terminal. Args: input_data (str): The input data to be displayed in the terminal. config (TerminalConfig, optional): Configuration for the terminal. Defaults to None. """ if config is None: self.config = TerminalConfig() else: self.config = config if not input_data: input_data = "No Input." self._next_character_id = 0 self._input_colors_frequency: dict[Color, int] = {} self._preprocessed_character_lines = self._preprocess_input_data(input_data) self._terminal_width, self._terminal_height = self._get_terminal_dimensions() self.canvas = Canvas(*self._get_canvas_dimensions()) if not self.config.ignore_terminal_dimensions: self.canvas_column_offset, self.canvas_row_offset = self._calc_canvas_offsets() else: self.canvas_column_offset = self.canvas_row_offset = 0 self._terminal_width = self.canvas.right self._terminal_height = self.canvas.top # the visible_* attributes are used to determine which characters are visible on the terminal self.visible_top = min(self.canvas.top + self.canvas_row_offset, self._terminal_height) self.visible_bottom = max(self.canvas.bottom + self.canvas_row_offset, 1) self.visible_right = min(self.canvas.right + self.canvas_column_offset, self._terminal_width) self.visible_left = max(self.canvas.left + self.canvas_column_offset, 1) self._input_characters = [ character for character in self._setup_input_characters() if character.input_coord.row <= self.canvas.top and character.input_coord.column <= self.canvas.right ] self._added_characters: list[EffectCharacter] = [] self.character_by_input_coord: dict[Coord, EffectCharacter] = { (character.input_coord): character for character in self._input_characters } self._inner_fill_characters, self._outer_fill_characters = self._make_fill_characters() self._visible_characters: set[EffectCharacter] = set() self._frame_rate = self.config.frame_rate self._last_time_printed = time.time() self._update_terminal_state() def _preprocess_input_data(self, input_data: str) -> list[list[EffectCharacter]]: # noqa: PLR0915 """Preprocess the input data. Preprocess the input data by replacing tabs with spaces and decomposing the input data into a list of characters while applying any active SGR foreground/background ANSI escape sequences discovered in the data. Args: input_data (str): The input data to be displayed in the terminal. Returns: list[EffectCharacter]: A list of characters decomposed from the input data. """ def find_ansi_sequences_with_positions(text: str) -> list[tuple[int, int]]: # [(start,end), ...] """Find SGR foreground and background ANSI escape sequences in the input text and return their positions. Args: text (str): The input text. Returns: list[tuple[int, int]]: A list of tuples containing the start and end positions of the ANSI escape sequences. """ # match all SGR sequences, though only 8bit and 24bit color sequences will be used, the others are ignored ansi_escape_pattern = r"(\x1b|\x1B|\033)\[[\d;]*m" matches = re.finditer(ansi_escape_pattern, text) return [(match.start(), match.end() - 1) for match in matches] characters: list[list[EffectCharacter]] = [] # replace tabs with spaces input_data = input_data.replace("\t", " " * self.config.tab_width) # remove trailing whitespace from each line input_data_lines = input_data.splitlines() input_data = "" for line in input_data_lines: input_data += line.rstrip() + "\n" # find ansi sequences sequence_list = find_ansi_sequences_with_positions(input_data) active_sequences = {"fg_color": "", "bg_color": ""} char_index = 0 current_character_line: list[EffectCharacter] = [] while char_index < len(input_data): if input_data[char_index] == "\n": characters.append(current_character_line) current_character_line = [] char_index += 1 elif sequence_list and char_index == sequence_list[0][0]: active_sequence = input_data[sequence_list[0][0] : sequence_list[0][1] + 1] # only apply sequences that are 8bit or 24bit color (only support RGB colorspace, though 38;3 and 38;4 # exist and indicate CMY and CMYK colorspaces, respectively) # match foreground colors if re.match(r"(\x1b|\x1B|\033)\[38;(2|5)", active_sequence): active_sequences["fg_color"] = active_sequence # match background colors elif re.match(r"(\x1b|\x1B|\033)\[48;(2|5)", active_sequence): active_sequences["bg_color"] = active_sequence # match reset sequence and clear active sequences elif re.match(r"(\x1b|\x1B|\033)\[0?m", active_sequence): active_sequences["fg_color"] = active_sequences["bg_color"] = "" char_index = sequence_list[0][1] + 1 sequence_list.pop(0) else: character = EffectCharacter(self._next_character_id, input_data[char_index], 0, 0) for sequence_type, sequence in active_sequences.items(): if sequence: character._input_ansi_sequences[sequence_type] = sequence if sequence in Terminal.ansi_sequence_color_map: color = Terminal.ansi_sequence_color_map[sequence] else: color = Color(ansitools.parse_ansi_color_sequence(sequence)) Terminal.ansi_sequence_color_map[sequence] = color if color in self._input_colors_frequency: self._input_colors_frequency[color] += 1 else: self._input_colors_frequency[color] = 1 if sequence_type == "fg_color": character.animation.input_fg_color = color else: character.animation.input_bg_color = color character.animation.no_color = self.config.no_color character.animation.use_xterm_colors = self.config.xterm_colors character.animation.existing_color_handling = self.config.existing_color_handling # if existing_color_handling is set to 'always', set the appearance to the input symbol with # any existing color sequences if character.animation.existing_color_handling == "always": character.animation.set_appearance(character.input_symbol) current_character_line.append(character) self._next_character_id += 1 char_index += 1 return characters def _calc_canvas_offsets(self) -> tuple[int, int]: """Calculate the canvas offsets based on the anchor point. Returns: tuple[int, int]: Canvas column offset, row offset """ canvas_column_offset = canvas_row_offset = 0 if self.config.anchor_canvas in ("s", "n", "c"): canvas_column_offset = (self._terminal_width // 2) - (self.canvas.width // 2) elif self.config.anchor_canvas in ("se", "e", "ne"): canvas_column_offset = self._terminal_width - self.canvas.width if self.config.anchor_canvas in ("w", "e", "c"): canvas_row_offset = self._terminal_height // 2 - self.canvas.height // 2 elif self.config.anchor_canvas in ("nw", "n", "ne"): canvas_row_offset = self._terminal_height - self.canvas.height return canvas_column_offset, canvas_row_offset def _get_canvas_dimensions(self) -> tuple[int, int]: """Determine the canvas dimensions using the input data dimensions, terminal dimensions, and text wrapping. Returns: tuple[int, int]: Canvas height, width. """ if self.config.canvas_width > 0: canvas_width = self.config.canvas_width elif self.config.canvas_width == 0: canvas_width = self._terminal_width else: input_width = max([len(line) for line in self._preprocessed_character_lines]) if self.config.ignore_terminal_dimensions: canvas_width = input_width else: canvas_width = min(self._terminal_width, input_width) if self.config.canvas_height > 0: canvas_height = self.config.canvas_height elif self.config.canvas_height == 0: canvas_height = self._terminal_height else: input_height = len(self._preprocessed_character_lines) if self.config.ignore_terminal_dimensions: canvas_height = input_height elif self.config.wrap_text: canvas_height = min( len(self._wrap_lines(self._preprocessed_character_lines, canvas_width)), self._terminal_height, ) else: canvas_height = min(self._terminal_height, input_height) return canvas_height, canvas_width def _get_terminal_dimensions(self) -> tuple[int, int]: """Get the terminal dimensions. Use shutil.get_terminal_size() to get the terminal dimensions. If the terminal size cannot be determined, default values of 80 columns and 24 rows are returned. Returns: tuple[int, int]: terminal width and height """ try: terminal_width, terminal_height = shutil.get_terminal_size() except OSError: # If the terminal size cannot be determined, return default values return 80, 24 return terminal_width, terminal_height @staticmethod def get_piped_input() -> str: """Get the piped input from stdin. This method checks if there is any piped input from the standard input (stdin). If there is no piped input, it returns an empty string. If there is piped input, it reads the input data from stdin and returns it as a string. The `sys.stdin.isatty()` check is used to determine if the program is being run interactively or if there is piped input. When the program is run interactively, `sys.stdin.isatty()` returns True, indicating that there is no piped input. In this case, the method returns an empty string. Returns: str: The piped input from stdin as a string, or an empty string if there is no piped input. """ if sys.stdin.isatty(): return "" return sys.stdin.read() def _wrap_lines(self, lines: list[list[EffectCharacter]], width: int) -> list[list[EffectCharacter]]: """Wrap the given lines of text to fit within the width of the canvas. Args: lines (list[list[EffectCharacter]]): The lines of text to be wrapped. width (int): The maximum length of a line. Returns: list: The wrapped lines of text. """ wrapped_lines = [] for line in lines: current_line = line while len(current_line) > width: wrapped_lines.append(current_line[:width]) current_line = current_line[width:] wrapped_lines.append(current_line) return wrapped_lines def _setup_input_characters(self) -> list[EffectCharacter]: """Set up the input characters discovered during preprocessing. Characters are positioned based on row/column coordinates relative to the anchor point in the Canvas. Coordinates are relative to the cursor row position at the time of execution. 1,1 is the bottom left corner of the row above the cursor. Returns: list[Character]: list of EffectCharacter objects """ formatted_lines = [] formatted_lines = ( self._wrap_lines(self._preprocessed_character_lines, self.canvas.right) if self.config.wrap_text else self._preprocessed_character_lines ) input_height = len(formatted_lines) input_characters: list[EffectCharacter] = [] for row, line in enumerate(formatted_lines): for column, character in enumerate(line, start=1): character._input_coord = Coord(column, input_height - row) if character._input_symbol != " ": input_characters.append(character) anchored_characters = self.canvas._anchor_text(input_characters, self.config.anchor_text) return [char for char in anchored_characters if self.canvas.coord_is_in_canvas(char._input_coord)] def _make_fill_characters(self) -> tuple[list[EffectCharacter], list[EffectCharacter]]: """Create lists of characters to fill the empty spaces in the canvas. The characters input_symbol is a space. The characters are added to the character_by_input_coord dictionary. Returns: tuple[list[EffectCharacter], list[EffectCharacter]]: lists of inner and outer fill characters """ inner_fill_characters = [] outer_fill_characters = [] for row in range(1, self.canvas.top + 1): for column in range(1, self.canvas.right + 1): coord = Coord(column, row) if coord not in self.character_by_input_coord: fill_char = EffectCharacter(self._next_character_id, " ", column, row) fill_char.is_fill_character = True fill_char.animation.no_color = self.config.no_color fill_char.animation.use_xterm_colors = self.config.xterm_colors fill_char.animation.existing_color_handling = self.config.existing_color_handling self.character_by_input_coord[coord] = fill_char self._next_character_id += 1 if ( self.canvas.text_left <= column <= self.canvas.text_right and self.canvas.text_bottom <= row <= self.canvas.text_top ): inner_fill_characters.append(fill_char) else: outer_fill_characters.append(fill_char) return inner_fill_characters, outer_fill_characters def add_character(self, symbol: str, coord: Coord) -> EffectCharacter: """Add a character to the terminal for printing. Used to create characters that are not in the input data. Args: symbol (str): symbol to add coord: (Coord): set character's input coordinates Returns: EffectCharacter: the character that was added """ character = EffectCharacter(self._next_character_id, symbol, coord.column, coord.row) character.animation.no_color = self.config.no_color character.animation.use_xterm_colors = self.config.xterm_colors character.animation.existing_color_handling = self.config.existing_color_handling self._added_characters.append(character) self._next_character_id += 1 return character def get_input_colors(self, sort: ColorSort = ColorSort.MOST_TO_LEAST) -> list[Color]: """Get a list of colors derived from the input text ansi sequences with an optional sort. Args: sort (ColorSort, optional): Sort the colors. Defaults to ColorSort.MOST_TO_LEAST. Raises: ValueError: If an invalid sort option is provided. Returns: list[Color]: list of Colors """ if sort == self.ColorSort.MOST_TO_LEAST: return sorted( self._input_colors_frequency.keys(), key=lambda color: self._input_colors_frequency[color], reverse=True, ) if sort == self.ColorSort.RANDOM: colors = list(self._input_colors_frequency.keys()) random.shuffle(colors) return colors if sort == self.ColorSort.LEAST_TO_MOST: return sorted(self._input_colors_frequency.keys(), key=lambda color: self._input_colors_frequency[color]) raise InvalidColorSortError(sort) def get_characters( self, *, input_chars: bool = True, inner_fill_chars: bool = False, outer_fill_chars: bool = False, added_chars: bool = False, sort: CharacterSort = CharacterSort.TOP_TO_BOTTOM_LEFT_TO_RIGHT, ) -> list[EffectCharacter]: """Get a list of all EffectCharacters in the terminal with an optional sort. Args: input_chars (bool, optional): whether to include input characters. Defaults to True. inner_fill_chars (bool, optional): whether to include inner fill characters. Defaults to False. outer_fill_chars (bool, optional): whether to include outer fill characters. Defaults to False. added_chars (bool, optional): whether to include added characters. Defaults to False. sort (CharacterSort, optional): order to sort the characters. Defaults to CharacterSort.TOP_TO_BOTTOM_LEFT_TO_RIGHT. Returns: list[EffectCharacter]: list of EffectCharacters in the terminal """ all_characters: list[EffectCharacter] = [] if input_chars: all_characters.extend(self._input_characters) if inner_fill_chars: all_characters.extend(self._inner_fill_characters) if outer_fill_chars: all_characters.extend(self._outer_fill_characters) if added_chars: all_characters.extend(self._added_characters) # default sort TOP_TO_BOTTOM_LEFT_TO_RIGHT all_characters.sort(key=lambda character: (-character.input_coord.row, character.input_coord.column)) if sort is self.CharacterSort.RANDOM: random.shuffle(all_characters) elif sort in (self.CharacterSort.TOP_TO_BOTTOM_LEFT_TO_RIGHT, self.CharacterSort.BOTTOM_TO_TOP_RIGHT_TO_LEFT): if sort is self.CharacterSort.BOTTOM_TO_TOP_RIGHT_TO_LEFT: all_characters.reverse() elif sort in (self.CharacterSort.BOTTOM_TO_TOP_LEFT_TO_RIGHT, self.CharacterSort.TOP_TO_BOTTOM_RIGHT_TO_LEFT): all_characters.sort(key=lambda character: (character.input_coord.row, character.input_coord.column)) if sort is self.CharacterSort.TOP_TO_BOTTOM_RIGHT_TO_LEFT: all_characters.reverse() elif sort in (self.CharacterSort.OUTSIDE_ROW_TO_MIDDLE, self.CharacterSort.MIDDLE_ROW_TO_OUTSIDE): all_characters = [ all_characters.pop(0) if i % 2 == 0 else all_characters.pop(-1) for i in range(len(all_characters)) ] if sort is self.CharacterSort.MIDDLE_ROW_TO_OUTSIDE: all_characters.reverse() else: raise InvalidCharacterSortError(sort) return all_characters def get_characters_grouped( self, grouping: CharacterGroup = CharacterGroup.ROW_TOP_TO_BOTTOM, *, input_chars: bool = True, inner_fill_chars: bool = False, outer_fill_chars: bool = False, added_chars: bool = False, ) -> list[list[EffectCharacter]]: """Get a list of all EffectCharacters grouped by the specified CharacterGroup grouping. Args: grouping (CharacterGroup, optional): order to group the characters. Defaults to ROW_TOP_TO_BOTTOM. input_chars (bool, optional): whether to include input characters. Defaults to True. inner_fill_chars (bool, optional): whether to include inner fill characters. Defaults to False. outer_fill_chars (bool, optional): whether to include outer fill characters. Defaults to False. added_chars (bool, optional): whether to include added characters. Defaults to False. Returns: list[list[EffectCharacter]]: list of lists of EffectCharacters in the terminal. Inner lists correspond to groups as specified in the grouping. """ all_characters: list[EffectCharacter] = [] if input_chars: all_characters.extend(self._input_characters) if inner_fill_chars: all_characters.extend(self._inner_fill_characters) if outer_fill_chars: all_characters.extend(self._outer_fill_characters) if added_chars: all_characters.extend(self._added_characters) all_characters.sort(key=lambda character: (character.input_coord.row, character.input_coord.column)) if grouping in (self.CharacterGroup.COLUMN_LEFT_TO_RIGHT, self.CharacterGroup.COLUMN_RIGHT_TO_LEFT): columns = [] for column_index in range(self.canvas.right + 1): characters_in_column = [ character for character in all_characters if character.input_coord.column == column_index ] if characters_in_column: columns.append(characters_in_column) if grouping == self.CharacterGroup.COLUMN_RIGHT_TO_LEFT: columns.reverse() return columns if grouping in (self.CharacterGroup.ROW_BOTTOM_TO_TOP, self.CharacterGroup.ROW_TOP_TO_BOTTOM): rows = [] for row_index in range(self.canvas.top + 1): characters_in_row = [ character for character in all_characters if character.input_coord.row == row_index ] if characters_in_row: rows.append(characters_in_row) if grouping == self.CharacterGroup.ROW_TOP_TO_BOTTOM: rows.reverse() return rows if grouping in ( self.CharacterGroup.DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT, self.CharacterGroup.DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT, ): diagonals = [] for diagonal_index in range(self.canvas.top + self.canvas.right + 1): characters_in_diagonal = [ character for character in all_characters if character.input_coord.row + character.input_coord.column == diagonal_index ] if characters_in_diagonal: diagonals.append(characters_in_diagonal) if grouping == self.CharacterGroup.DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT: diagonals.reverse() return diagonals if grouping in ( self.CharacterGroup.DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT, self.CharacterGroup.DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT, ): diagonals = [] for diagonal_index in range(self.canvas.left - self.canvas.top, self.canvas.right - self.canvas.bottom + 1): characters_in_diagonal = [ character for character in all_characters if character.input_coord.column - character.input_coord.row == diagonal_index ] if characters_in_diagonal: diagonals.append(characters_in_diagonal) if grouping == self.CharacterGroup.DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT: diagonals.reverse() return diagonals if grouping in ( self.CharacterGroup.CENTER_TO_OUTSIDE_DIAMONDS, self.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS, ): distance_map: dict[int, list[EffectCharacter]] = {} center_out = [] for character in all_characters: distance = abs(character.input_coord.column - self.canvas.center.column) + abs( character.input_coord.row - self.canvas.center.row, ) if distance not in distance_map: distance_map[distance] = [] distance_map[distance].append(character) for distance in sorted( distance_map.keys(), reverse=grouping is self.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS, ): center_out = [ distance_map[distance] for distance in sorted( distance_map.keys(), reverse=grouping is self.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS, ) ] return center_out raise InvalidCharacterGroupError(grouping) def get_character_by_input_coord(self, coord: Coord) -> EffectCharacter | None: """Get an EffectCharacter by its input coordinates. Args: coord (Coord): input coordinates of the character Returns: EffectCharacter | None: the character at the specified coordinates, or None if no character is found """ return self.character_by_input_coord.get(coord, None) def set_character_visibility(self, character: EffectCharacter, is_visible: bool) -> None: # noqa: FBT001 """Set the visibility of a character. Args: character (EffectCharacter): the character to set visibility for is_visible (bool): whether the character should be visible """ character._is_visible = is_visible if is_visible: self._visible_characters.add(character) else: self._visible_characters.discard(character) def get_formatted_output_string(self) -> str: """Get the formatted output string based on the current terminal state. This method updates the internal terminal representation state before returning the formatted output string. Returns: str: The formatted output string. """ self._update_terminal_state() return "\n".join(self.terminal_state[::-1]) def _update_terminal_state(self) -> None: """Update the internal representation of the terminal state. The terminal state is updated with the current position of all visible characters. """ rows = [[" " for _ in range(self.visible_right)] for _ in range(self.visible_top)] for character in sorted(self._visible_characters, key=lambda c: c.layer): row = character.motion.current_coord.row + self.canvas_row_offset column = character.motion.current_coord.column + self.canvas_column_offset if self.visible_bottom <= row <= self.visible_top and self.visible_left <= column <= self.visible_right: rows[row - 1][column - 1] = character.animation.current_character_visual.formatted_symbol terminal_state = ["".join(row) for row in rows] self.terminal_state = terminal_state def prep_canvas(self) -> None: """Prepare the terminal for the effect by adding empty lines and hiding the cursor.""" sys.stdout.write(ansitools.hide_cursor()) sys.stdout.write("\n" * (self.visible_top)) sys.stdout.write(ansitools.dec_save_cursor_position()) def restore_cursor(self, end_symbol: str = "\n") -> None: """Restores the cursor visibility and prints the end_symbol. Args: end_symbol (str, optional): The symbol to print after the effect has completed. Defaults to newline. """ sys.stdout.write(ansitools.show_cursor()) sys.stdout.write(end_symbol) def print(self, output_string: str) -> None: """Print the current terminal state to stdout while preserving the cursor position. Args: output_string (str): The string to be printed. """ self.move_cursor_to_top() sys.stdout.write(output_string) sys.stdout.flush() def enforce_framerate(self) -> None: """Enforce the frame rate set in the terminal config. Frame rate is enforced by sleeping if the time since the last frame is shorter than the expected frame delay. """ frame_delay = 1 / self._frame_rate time_since_last_print = time.time() - self._last_time_printed if time_since_last_print < frame_delay: time.sleep(frame_delay - time_since_last_print) self._last_time_printed = time.time() def move_cursor_to_top(self) -> None: """Restores the cursor position to the top of the canvas.""" sys.stdout.write(ansitools.dec_restore_cursor_position()) sys.stdout.write(ansitools.dec_save_cursor_position()) sys.stdout.write(ansitools.move_cursor_up(self.visible_top)) terminaltexteffects-release-0.12.1/terminaltexteffects/py.typed000066400000000000000000000000001507200677100250430ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/terminaltexteffects/template/000077500000000000000000000000001507200677100251715ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/terminaltexteffects/template/effect_template.py000066400000000000000000000136741507200677100307050ustar00rootroot00000000000000"""Effect Description. Classes: """ # noqa: INP001 from __future__ import annotations import typing from dataclasses import dataclass import terminaltexteffects as tte from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator from terminaltexteffects.utils import argvalidators from terminaltexteffects.utils.argsdataclass import ArgField, ArgsDataClass, argclass def get_effect_and_args() -> tuple[type[typing.Any], type[ArgsDataClass]]: """Return the effect class and the effect configuration dataclass.""" return Effect, EffectConfig @argclass( name="namedeffect", help="effect_description", description="effect_description", epilog=f"""{argvalidators.EASING_EPILOG} """, ) @dataclass class EffectConfig(ArgsDataClass): """Effect configuration dataclass.""" color_single: tte.Color = ArgField( cmd_name=["--color-single"], type_parser=argvalidators.ColorArg.type_parser, default=tte.Color(0), metavar=argvalidators.ColorArg.METAVAR, help="Color for the ___.", ) # type: ignore[assignment] "Color: Color for the ___." final_gradient_stops: tuple[tte.Color, ...] = ArgField( cmd_name=["--final-gradient-stops"], type_parser=argvalidators.ColorArg.type_parser, nargs="+", default=(tte.Color("8A008A"), tte.Color("00D1FF"), tte.Color("FFFFFF")), metavar=argvalidators.ColorArg.METAVAR, help=( "Space separated, unquoted, list of colors for the character gradient (applied across the canvas). " "If only one color is provided, the characters will be displayed in that color." ), ) # type: ignore[assignment] ( "tuple[Color, ...]: Space separated, unquoted, list of colors for the character gradient " "(applied across the canvas). If only one color is provided, the characters will be displayed in that color." ) final_gradient_steps: tuple[int, ...] | int = ArgField( cmd_name="--final-gradient-steps", type_parser=argvalidators.PositiveInt.type_parser, nargs="+", default=12, metavar=argvalidators.PositiveInt.METAVAR, help=( "Space separated, unquoted, list of the number of gradient steps to use. More steps will " "create a smoother and longer gradient animation." ), ) # type: ignore[assignment] ( "tuple[int, ...] | int: Space separated, unquoted, list of the number of gradient steps to use. More " "steps will create a smoother and longer gradient animation." ) final_gradient_frames: int = ArgField( cmd_name="--final-gradient-frames", type_parser=argvalidators.PositiveInt.type_parser, default=5, metavar=argvalidators.PositiveInt.METAVAR, help="Number of frames to display each gradient step. Increase to slow down the gradient animation.", ) # type: ignore[assignment] "int: Number of frames to display each gradient step. Increase to slow down the gradient animation." final_gradient_direction: tte.Gradient.Direction = ArgField( cmd_name="--final-gradient-direction", type_parser=argvalidators.GradientDirection.type_parser, default=tte.Gradient.Direction.VERTICAL, metavar=argvalidators.GradientDirection.METAVAR, help="Direction of the final gradient.", ) # type: ignore[assignment] "Gradient.Direction : Direction of the final gradient." movement_speed: float = ArgField( cmd_name="--movement-speed", type_parser=argvalidators.PositiveFloat.type_parser, default=1, metavar=argvalidators.PositiveFloat.METAVAR, help="Speed of the ___.", ) # type: ignore[assignment] "float: Speed of the ___." easing: tte.easing.EasingFunction = ArgField( cmd_name=["--easing"], default=tte.easing.in_out_sine, type_parser=argvalidators.Ease.type_parser, help="Easing function to use for character movement.", ) # type: ignore[assignment] "easing.EasingFunction: Easing function to use for character movement." @classmethod def get_effect_class(cls) -> type[Effect]: """Return the effect class associated with this configuration class.""" return Effect class EffectIterator(BaseEffectIterator[EffectConfig]): """Effect iterator for the NamedEffect effect.""" def __init__(self, effect: Effect) -> None: """Initialize the effect iterator. Args: effect (NamedEffect): The effect to iterate over. """ super().__init__(effect) self.pending_chars: list[tte.EffectCharacter] = [] self.character_final_color_map: dict[tte.EffectCharacter, tte.Color] = {} self.build() def build(self) -> None: """Build the effect.""" final_gradient = tte.Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps) final_gradient_mapping = final_gradient.build_coordinate_color_mapping( self.terminal.canvas.text_bottom, self.terminal.canvas.text_top, self.terminal.canvas.text_left, self.terminal.canvas.text_right, self.config.final_gradient_direction, ) for character in self.terminal.get_characters(): self.character_final_color_map[character] = final_gradient_mapping[character.input_coord] # do something with the data if needed (sort, adjust positions, etc) def __next__(self) -> str: """Return the next frame of the effect.""" if self.pending_chars or self.active_characters: # perform effect logic self.update() return self.frame raise StopIteration class Effect(BaseEffect[EffectConfig]): """Effect description.""" @property def _config_cls(self) -> type[EffectConfig]: return EffectConfig @property def _iterator_cls(self) -> type[EffectIterator]: return EffectIterator terminaltexteffects-release-0.12.1/terminaltexteffects/utils/000077500000000000000000000000001507200677100245165ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/terminaltexteffects/utils/__init__.py000066400000000000000000000000001507200677100266150ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/terminaltexteffects/utils/ansitools.py000066400000000000000000000107001507200677100271010ustar00rootroot00000000000000"""Collection of functions that generate ANSI escape codes for various terminal formatting effects. These escape codes can be used to modify the appearance of text in a terminal. Functions: parse_ansi_color_sequence(sequence: str) -> int | str: Parse an 8-bit or 24-bit ANSI color sequence. dec_save_cursor_position() -> str: Save the cursor position using DEC sequence. dec_restore_cursor_position() -> str: Restore the cursor position using DEC sequence. hide_cursor() -> str: Hide the cursor. show_cursor() -> str: Show the cursor. move_cursor_up(y: int) -> str: Move the cursor up y lines. move_cursor_to_column(x: int) -> str: Move the cursor to the specified column. reset_all() -> str: Reset all formatting. apply_bold() -> str: Apply bold formatting. apply_dim() -> str: Apply dim formatting. apply_italic() -> str: Apply italic formatting. apply_underline() -> str: Apply underline formatting. apply_blink() -> str: Apply blink formatting. apply_reverse() -> str: Apply reverse formatting. apply_hidden() -> str: Apply hidden formatting. apply_strikethrough() -> str: Apply strikethrough formatting. """ from __future__ import annotations import re def parse_ansi_color_sequence(sequence: str) -> int | str: """Parse an 8-bit or 24-bit ANSI color sequence. Returns the color code as an integer, in the case of 8-bit, or a hex string in the case of 24-bit. Args: sequence (str): ANSI color sequence Returns: int | str: 8-bit color int or 24-bit color str """ # Remove escape characters sequence = re.sub(r"(\033\[|\x1b\[)", "", sequence).strip("m") # detect 24-bit colors if re.match(r"^(38;2|48;2)", sequence): sequence = re.sub(r"^(38;2;|48;2;)", "", sequence) colors = [] for color in sequence.split(";"): if color: colors.append(int(color)) else: colors.append(0) # default to 0 if no value in field (e.g. 38;2;;0m) return "".join(f"{color:02X}" for color in colors) # detect 8-bit colors if re.match(r"^(38;5|48;5)", sequence): sequence = re.sub(r"^(38;5;|48;5;)", "", sequence) return int(sequence) msg = "Invalid ANSI color sequence" raise ValueError(msg) def dec_save_cursor_position() -> str: """Save the cursor position using DEC sequence. Returns: str: ANSI escape code """ return "\0337" def dec_restore_cursor_position() -> str: """Restore the cursor position using DEC sequence. Returns: str: ANSI escape code """ return "\0338" def hide_cursor() -> str: """Hide the cursor. Returns: str: ANSI escape code """ return "\033[?25l" def show_cursor() -> str: """Show the cursor. Returns: str: ANSI escape code """ return "\033[?25h" def move_cursor_up(y: int) -> str: """Move the cursor up y lines. Args: y (int): number of lines to move up Returns: str: ANSI escape code """ return f"\033[{y}A" def move_cursor_to_column(x: int) -> str: """Move the cursor to the specified column. Args: x (int): column number Returns: str: ANSI escape code """ return f"\033[{x}G" def reset_all() -> str: """Reset all formatting. Returns: str: ANSI escape code """ return "\033[0m" def apply_bold() -> str: """Apply bold formatting. Returns: str: ANSI escape code """ return "\033[1m" def apply_dim() -> str: """Apply dim formatting. Returns: str: ANSI escape code """ return "\033[2m" def apply_italic() -> str: """Apply italic formatting. Returns: str: ANSI escape code """ return "\033[3m" def apply_underline() -> str: """Apply underline formatting. Returns: str: ANSI escape code """ return "\033[4m" def apply_blink() -> str: """Apply blink formatting. Returns: str: ANSI escape code """ return "\033[5m" def apply_reverse() -> str: """Apply reverse formatting. Returns: str: ANSI escape code """ return "\033[7m" def apply_hidden() -> str: """Apply hidden formatting. Returns: str: ANSI escape code """ return "\033[8m" def apply_strikethrough() -> str: """Apply strikethrough formatting. Returns: str: ANSI escape code """ return "\033[9m" terminaltexteffects-release-0.12.1/terminaltexteffects/utils/argsdataclass.py000066400000000000000000000326261507200677100277150ustar00rootroot00000000000000"""Classes and functions designed to work with the argparse library and enable typed arguments. Support interacting with terminaltexteffects as both a command line tool and a library by providing types arguments. The ArgField class extends the built-in Field class to include additional metadata specific to command-line arguments. This metadata includes the command-line argument name, help text, type parser, metavar, nargs, action, required status, and choices. The ArgParserDescriptor dataclass contains required attributes to call the "add_parser()" method of the _argparse._SubParsersAction class. The ArgsDataClass dataclass represents command-line arguments and provides methods for handling them. It does not define any fields itself but is meant to be subclassed, with subclasses defining their own fields to represent the command-line arguments they expect. Classes: ArgField: A subclass of the dataclasses.Field class that represents a command-line argument. ArgParserDescriptor: A dataclass that contains required attributes to call "add_parser()" method of the _argparse._SubParsersAction" class. ArgsDataClass: A dataclass that represents command-line arguments and provides methods for handling them. """ from __future__ import annotations import inspect import sys import typing from dataclasses import MISSING, Field, dataclass, fields from terminaltexteffects.utils.argvalidators import CustomFormatter if typing.TYPE_CHECKING: import argparse class ArgField(Field): """A subclass of the dataclasses.Field class that represents a command-line argument. This class extends the built-in Field class to include additional metadata specific to command-line arguments. This metadata includes the command-line argument name, help text, type parser, metavar, nargs, action, required status, and choices. The class also overrides the __init__ method to handle the additional metadata and to set default values based on the 'action' attribute. If 'action' is "store_true", the default value is set to False. If 'action' is "store_false", the default value is set to True. Args: cmd_name (str | list[str]): The name or names of the command-line argument. help (str): The help text to display for the argument. type_parser (typing.Any, optional): The validator to use to validate the argument. Defaults to None. metavar (str | None, optional): A name for the argument in usage messages. Defaults to None. nargs (str | None, optional): The number of command-line arguments that should be consumed. Defaults to None. action (str | None, optional): The basic type of action to be taken when this argument is encountered at the command line. Defaults to None. required (bool, optional): Whether or not the command-line option is required. Defaults to False. choices (list[str | int] | None, optional): A container of the allowable values for the argument. Defaults to None. default (any, optional): The value produced if the argument is absent from the command line. Defaults to MISSING. default_factory (any, optional): A function that is called to provide the default value. Defaults to MISSING. init (bool, optional): If true (the default), this field is included as a parameter to the generated __init__ method. Defaults to True. repr (bool, optional): If true (the default), this field is included in the string returned by the generated __repr__ method. Defaults to True. hash (bool | None, optional): This can be a bool or None. If true, this field is included in the generated __hash__ method. Defaults to None. compare (bool, optional): If true (the default), this field is included in the generated equality and comparison methods (__eq__, __gt__, etc.). Defaults to True. kw_only (bool, optional): If true, this field must be passed as a keyword argument. Defaults to MISSING. """ def __init__( self, # custom metadata cmd_name: str | list[str], help: str, type_parser: typing.Callable | None = None, metavar: str | None = None, nargs: str | int | None = None, action: str | None = None, required: bool = False, choices: list[str | int] | None = None, # python internal attrs default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, kw_only=MISSING, doc="" ) -> None: """Initialize the ArgField with the provided metadata and default values.""" additional_metadata = ArgField.FieldAdditionalMetaData( cmd_name=cmd_name, type_parser=type_parser, metavar=metavar, nargs=nargs, help=help, action=action, required=required, choices=choices, ) if action == "store_true": default = False elif action == "store_false": default = True if sys.version_info >= (3, 14): # Field.__init__ signature changed in Python 3.14 super().__init__( default, default_factory, init, repr, hash, compare, vars(additional_metadata), kw_only=kw_only, doc="") elif sys.version_info >= (3, 10): # Field.__init__ signature changed in Python 3.10 super().__init__( default, default_factory, init, repr, hash, compare, vars(additional_metadata), kw_only=kw_only, ) else: super().__init__(default, default_factory, init, repr, hash, compare, vars(additional_metadata)) @dataclass class FieldAdditionalMetaData: """A dataclass that contains additional metadata for an ArgField.""" cmd_name: str | list[str] type_parser: typing.Any | None = None metavar: str | None = None nargs: str | int | None = None help: str | None = None action: str | None = None required: bool = False choices: list[str | int] | None = None @dataclass class ArgParserDescriptor: """Required attributes to call "add_parser()" method of _argparse._SubParsersAction" class.""" name: str help: str description: str epilog: str @dataclass class ArgsDataClass: """A dataclass that represents command-line arguments and provides methods for handling them. This class provides a structured way to define and work with command-line arguments. It uses Python's built-in dataclasses and the argparse module to parse command-line arguments and create an instance of the class from them. Note: This class does not define any fields itself. Instead, it is meant to be subclassed, with subclasses defining their own fields to represent the command-line arguments they expect. """ @classmethod def _from_parsed_args_mapping(cls, parsed_args: argparse.Namespace, arg_class=None): """Create an instance of the ArgsDataClass from parsed command-line arguments. This method takes a Namespace object, which is the result of parsing command-line arguments with argparse, and an optional class to instantiate. If no class is provided, it uses the 'arg_class' attribute from the parsed_args. It retrieves the signature of the __init__ method of the target class and iterates over its parameters. For each parameter, it gets the corresponding value from the parsed_args. If the value is a list (which can happen if the argument was defined with nargs="+" or nargs="*"), it converts the list to a tuple. It then creates a new instance of the target class, passing the collected parameters as keyword arguments. Args: parsed_args (argparse.Namespace): The parsed command-line arguments. arg_class (Optional[Type]): The class to instantiate. If None, uses 'arg_class' attribute from parsed_args. Returns: An instance of the target class, initialized with values from the parsed_args. Note: This method assumes that the names of the command-line arguments match the parameter names of the target class's __init__ method. If this is not the case, it may not work as expected. """ if arg_class is None: arg_class = parsed_args.arg_class signature = inspect.signature(arg_class.__init__) parameters = signature.parameters params_dict = {} for param in parameters: if param == "self": continue param_value = getattr(parsed_args, param) if isinstance(param_value, list): # argparser returns list for nargs="+" or nargs="*" param_value = tuple(param_value) params_dict[param] = param_value return arg_class(**params_dict) @classmethod def _get_all_fields(cls) -> dict[str, Field]: """Retrieve all fields defined in the ArgsDataClass and returns them as a dictionary. This method uses the `fields` function from the `dataclasses` module to get a list of all fields defined in the ArgsDataClass. It then iterates over these fields, adding each one to a dictionary with the field's name as the key and the field itself as the value. Returns: dict[str, Field]: A dictionary mapping field names to their corresponding Field objects. Each Field object contains information about the field, such as its name, type, and any default values or metadata it may have. """ fields_dict = {} for f in fields(cls): fields_dict[f.name] = f return fields_dict @classmethod def _add_args_to_parser(cls, parser: argparse.ArgumentParser) -> None: """Add arguments to the provided parser based on the fields defined in the ArgsDataClass. This method iterates over all fields in the ArgsDataClass. For each field, it checks if it has metadata. If metadata is present, it creates an instance of FieldAdditionalMetaData using the metadata. It then prepares a dictionary of argument descriptors, mapping field names to their corresponding values. These descriptors are used to add an argument to the parser with the `add_argument` method. Args: parser (argparse.ArgumentParser): The parser to which arguments will be added. Each argument corresponds to a field in the ArgsDataClass, and the argument's properties are determined by the field's metadata. Note: The 'type_parser' field name is specially handled and mapped to 'type' in the argument descriptors. The 'cmd_name' field is used as the name of the argument added to the parser. If 'cmd_name' is a string, it is wrapped in a list before being passed to `add_argument`. If a field has no metadata, it is skipped and no corresponding argument is added to the parser. """ arg_fields = cls._get_all_fields() for arg in arg_fields.values(): if not arg.metadata: continue additional_metadata = ArgField.FieldAdditionalMetaData(**arg.metadata) if isinstance(additional_metadata.cmd_name, str): additional_metadata.cmd_name = [additional_metadata.cmd_name] field_names_mapping = {"type_parser": "type"} arg_descriptor = {} for attr_name in vars(additional_metadata): value = getattr(additional_metadata, attr_name) if value is None: continue if attr_name == "cmd_name": continue if attr_name in field_names_mapping: attr_name = field_names_mapping[attr_name] arg_descriptor[attr_name] = value parser.add_argument(*additional_metadata.cmd_name, **arg_descriptor, default=arg.default) parser.formatter_class = CustomFormatter @classmethod def _add_to_args_subparsers(cls, subparsers: argparse._SubParsersAction) -> None: """Add arguments to the subparser. Args: cls (ArgsDataClass): ArgDataClass that required args defined in it subparsers (argparse._SubParsersAction): subparser to add arguments to """ sub_parser_descriptor = cls.arg_class_metadata # type: ignore[attr-defined] new_parser = subparsers.add_parser(**vars(sub_parser_descriptor)) new_parser.set_defaults(arg_class=cls) cls._add_args_to_parser(new_parser) def argclass(name: str, help: str, description: str, epilog: str): """Decorator for providing required metadata to an "ArgDataClass" Args: name (str): name for parser or subparser help (str): help string for parser or subparser description (str): description string for parser or subparser epilog (str): epilog string for parser or subparser """ def decorator(cls): cls.arg_class_metadata = ArgParserDescriptor(name=name, help=help, description=description, epilog=epilog) return cls return decorator terminaltexteffects-release-0.12.1/terminaltexteffects/utils/argvalidators.py000066400000000000000000000453451507200677100277450ustar00rootroot00000000000000"""Command line argument validators and METAVARs for consistent type parsing and help output. This module includes a custom formatter for argparse, which combines the features of `argparse.ArgumentDefaultsHelpFormatter` and `argparse.RawDescriptionHelpFormatter`. Classes: CustomFormatter: A custom formatter for argparse that combines the features of `argparse.ArgumentDefaultsHelpFormatter` and `argparse.RawDescriptionHelpFormatter`. GradientDirection: Argument type for gradient directions. ColorArg: Argument type for color values. Symbol: Argument type for single ASCII/UTF-8 characters. Ease: Argument type for easing functions. PositiveInt: Argument type for positive integers. NonNegativeInt: Argument type for nonnegative integers. PositiveIntRange: Argument type for integer ranges. PositiveFloat: Argument type for positive floats. NonNegativeFloat: Argument type for nonnegative floats. PositiveFloatRange: Argument type for float ranges. TerminalDimension: Argument type for terminal dimensions. CanvasDimension: Argument type for canvas dimensions. NonNegativeRatio: Argument type for float values between zero and one. PositiveRatio: Argument type for positive float values greater than zero and less than or equal to one. EasingStep: Argument type for easing step size values. Functions: is_ascii_or_utf8: Tests if the given string is either ASCII or UTF-8. Constants: EASING_EPILOG (str): A detailed description of the easing functions supported. """ from __future__ import annotations import argparse import typing from terminaltexteffects.utils import easing from terminaltexteffects.utils.graphics import Color, Gradient EASING_EPILOG = """\ Easing ------ Note: A prefix must be added to the function name (except LINEAR). All easing functions support the following prefixes: IN_ - Ease in OUT_ - Ease out IN_OUT_ - Ease in and out Easing Functions ---------------- LINEAR - Linear easing SINE - Sine easing QUAD - Quadratic easing CUBIC - Cubic easing QUART - Quartic easing QUINT - Quintic easing EXPO - Exponential easing CIRC - Circular easing BACK - Back easing ELASTIC - Elastic easing BOUNCE - Bounce easing Visit: https://easings.net/ for visualizations of the easing functions. """ class PositiveInt: """Validate argument is a positive integer. n > 0. int(n) > 0 Raises: argparse.ArgumentTypeError: Value is not a positive integer. """ METAVAR = "(int > 0)" @staticmethod def type_parser(arg: str) -> int: """Validate argument is a positive integer. n > 0. Args: arg (str): argument to validate Returns: int: validated positive integer """ try: arg_int = int(arg) except ValueError: msg = f"invalid value: '{arg}' is not a valid integer." raise argparse.ArgumentTypeError(msg) from None if arg_int > 0: return arg_int msg = f"invalid value: '{arg}' is not a valid value. Argument must be an integer > 0." raise argparse.ArgumentTypeError(msg) class NonNegativeInt: """Validate argument is a nonnegative integer. n >= 0. Raises: argparse.ArgumentTypeError: Value is not in range. """ METAVAR = "(int >= 0)" @staticmethod def type_parser(arg: str) -> int: """Validate argument is a nonnegative integer. n >= 0. Args: arg (str): argument to validate Raises: argparse.ArgumentTypeError: Value is not in range. Returns: int: validated gap value """ try: arg_int = int(arg) except ValueError: msg = f"invalid value: '{arg}' is not a valid integer." raise argparse.ArgumentTypeError(msg) from None if arg_int < 0: msg = f"invalid value: '{arg}' Argument must be int >= 0." raise argparse.ArgumentTypeError(msg) from None return arg_int class PositiveIntRange: """Validate argument is a valid range of integers n > 0. Positive integer ranges are a pair of integers separated by a hyphen. Ex: 1-10 Example: '1-10' is a valid input. Raises: argparse.ArgumentTypeError: Value is not a valid range of positive integers. """ METAVAR = "(hyphen separated positive int range e.g. '1-10')" @staticmethod def type_parser(arg: str) -> tuple[int, int]: """Validate argument is a valid range of integers n > 0. Args: arg (str): argument to validate Returns: tuple[int,int]: validated range """ try: start, end = map(int, arg.split("-")) if start <= 0: msg = f"invalid range: '{arg}' is not a valid range of positive ints. Must be start > 0. Ex: 1-10" raise argparse.ArgumentTypeError( msg, ) if start > end: msg = f"invalid range: '{arg}' is not a valid range of positive ints. Must be start <= end. Ex: 1-10" raise argparse.ArgumentTypeError( msg, ) except ValueError: msg = f"invalid range: '{arg}' is not a valid range of positive ints. Must be start-end. Ex: 1-10" raise argparse.ArgumentTypeError( msg, ) from None else: return start, end class PositiveFloat: """Validate argument is a positive float. n > 0. Raises: argparse.ArgumentTypeError: Value is not in range. """ METAVAR = "(float > 0)" @staticmethod def type_parser(arg: str) -> float: """Validate argument is a positive float. n > 0. Args: arg (str): argument to validate Raises: argparse.ArgumentTypeError: value is not in range. Returns: float: validated positive float """ try: float(arg) except ValueError: msg = f"invalid value: '{arg}' is not a valid float." raise argparse.ArgumentTypeError(msg) from None if float(arg) > 0: return float(arg) msg = f"invalid value: '{arg}' is not a valid value. Argument must be a float > 0." raise argparse.ArgumentTypeError(msg) class NonNegativeFloat: """Validate argument is a nonnegative float. n >= 0. Raises: argparse.ArgumentTypeError: Argument value is not in range. """ METAVAR = "(float >= 0)" @staticmethod def type_parser(arg: str) -> float: """Validate argument is a nonnegative float. n >= 0. Args: arg (str): argument to validate Raises: argparse.ArgumentTypeError: Argument value is not in range. Returns: float: validated value """ try: float(arg) except ValueError: msg = f"invalid argument value: '{arg}' is not a valid float." raise argparse.ArgumentTypeError(msg) from None if float(arg) < 0: msg = f"invalid argument value: '{arg}' is out of range. Must be float >= 0." raise argparse.ArgumentTypeError(msg) return float(arg) class PositiveFloatRange: """Validate argument is a valid range of positive floats. Float ranges are a pair of positive floats separated by a hyphen. Ex: 0.1-1.0 Raises: argparse.ArgumentTypeError: Value is not a valid range of floats. """ METAVAR = "(hyphen separated float range e.g. '0.25-0.5')" @staticmethod def type_parser(arg: str) -> tuple[float, float]: """Validate argument is a valid range of positive floats. Args: arg (str): argument to validate Returns: tuple[float,float]: validated range """ try: start, end = map(float, arg.split("-")) if start > end: msg = f"invalid range: '{arg}' is not a valid range of floats. Must be start <= end. Ex: 0.1-1.0" raise argparse.ArgumentTypeError( msg, ) if start == 0 or end == 0: msg = f"invalid range: '{arg}' is not a valid range of floats. Must be start > 0. Ex: 0.1-1.0" raise argparse.ArgumentTypeError( msg, ) except ValueError: msg = f"invalid range: '{arg}' is not a valid range. Must be start-end. Ex: 0.1-1.0" raise argparse.ArgumentTypeError(msg) from None else: return start, end class NonNegativeRatio: """Validate argument is a float value between zero and one. 0 <= float(n) <= 1 Raises: argparse.ArgumentTypeError: Value is not in range. """ METAVAR = "(0 <= float(n) <= 1)" @staticmethod def type_parser(arg: str) -> float: """Validate argument is a float value between zero and one. Args: arg (str): argument to validate Raises: argparse.ArgumentTypeError: Value is not in range. Returns: float: validated float value """ try: float(arg) except ValueError: msg = f"invalid value: '{arg}' is not a float or int." raise argparse.ArgumentTypeError(msg) from None if 0 <= float(arg) <= 1: return float(arg) msg = f"invalid value: '{arg}' is not a float >= 0 and <= 1. Example: 0.5" raise argparse.ArgumentTypeError(msg) class PositiveRatio: """Validate argument is a positive float. 0 < float(n) <= 1 Raises: argparse.ArgumentTypeError: Value is not in range. """ METAVAR = "(0 < float(n) <= 1)" @staticmethod def type_parser(arg: str) -> float: """Validate argument is a positive float. Args: arg (str): argument to validate Raises: argparse.ArgumentTypeError: Value is not in range. Returns: float: validated float value """ try: float(arg) except ValueError: msg = f"invalid value: '{arg}' is not a float or int." raise argparse.ArgumentTypeError(msg) from None if 0 < float(arg) <= 1: return float(arg) msg = f"invalid value: '{arg}' must be 0 < n <=1. Example: 0.5" raise argparse.ArgumentTypeError(msg) class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): """Combine ArgumentDefaultsHelpFormatter and RawDescriptionHelpFormatter for argparse.""" class GradientDirection: """Validate argument is a valid gradient direction. Raises: argparse.ArgumentTypeError: Argument value is not a valid gradient direction. """ METAVAR = "(diagonal, horizontal, vertical, radial)" @staticmethod def type_parser(arg: str) -> Gradient.Direction: """Validate argument is a valid gradient direction. Args: arg (str): argument to validate Returns: Gradient.Direction: validated gradient direction Raises: argparse.ArgumentTypeError: Argument value is not a valid gradient direction. """ direction_map = { "horizontal": Gradient.Direction.HORIZONTAL, "vertical": Gradient.Direction.VERTICAL, "diagonal": Gradient.Direction.DIAGONAL, "radial": Gradient.Direction.RADIAL, } if arg.lower() in direction_map: return direction_map[arg.lower()] msg = ( f"invalid gradient direction: '{arg}' is not a valid gradient direction. Choices are diagonal," " horizontal, vertical, or radial." ) raise argparse.ArgumentTypeError(msg) class ColorArg: """Validate argument is a valid color value. Color values can be either an XTerm color value (0-255) or an RGB hex value (000000-ffffff). Raises: argparse.ArgumentTypeError: Value is not in range of valid XTerm colors or RGB hex colors. """ METAVAR = "(XTerm [0-255] OR RGB Hex [000000-ffffff])" @staticmethod def type_parser(arg: str) -> Color: """Validate argument is a valid color value. Args: arg (str): argument to validate Raises: argparse.ArgumentTypeError: Color value is not in range. Returns: Color : validated color value """ xterm_min = 0 xterm_max = 255 if len(arg) <= 3: try: return Color(int(arg)) except ValueError: msg = ( f"invalid color value: '{arg}' is not a valid XTerm or RGB color." f" Must be in range {xterm_min}-{xterm_max} or 000000-FFFFFF." ) raise argparse.ArgumentTypeError(msg) from None else: try: return Color(arg) except ValueError: msg = ( f"invalid color value: '{arg}' is not a valid XTerm or RGB color." f" Must be in range {xterm_min}-{xterm_max} or 000000-FFFFFF." ) raise argparse.ArgumentTypeError(msg) from None class Symbol: """Validate argument is a single ASCII/UTF-8 character. Raises: argparse.ArgumentTypeError: Value is not a valid symbol. """ METAVAR = "(ASCII/UTF-8 character)" @staticmethod def type_parser(arg: str) -> str: """Validate argument is a valid symbol. Args: arg (str): argument to validate Returns: str: validated symbol """ if len(arg) == 1 and arg.isprintable(): return arg msg = f"invalid symbol: '{arg}' is not a valid symbol. Must be a single ASCII/UTF-8 character." raise argparse.ArgumentTypeError(msg) class CanvasDimension: """Validate argument is a valid canvas dimension. Raises: argparse.ArgumentTypeError: Value is not a valid canvas dimension. """ METAVAR = "int >= -1" @staticmethod def type_parser(arg: str) -> int: """Validate argument is a valid canvas dimension. Args: arg (str): argument to validate Raises: argparse.ArgumentTypeError: Value is not a valid canvas dimension. Returns: int: validated canvas dimension """ if arg.isdigit() or arg == "-1": return int(arg) msg = f"invalid value '{arg}' is not a valid integer. Must be >= -1." raise argparse.ArgumentTypeError(msg) class TerminalDimension: """Validate argument is a valid terminal dimension. A Terminal Dimension is an integer >= 0. Raises: argparse.ArgumentTypeError: Value is not a valid terminal dimension. """ METAVAR = "int >= 0" @staticmethod def type_parser(arg: str) -> int: """Validate argument is a valid terminal dimension. Args: arg (str): argument to validate Returns: int: validated terminal dimension """ try: dimension = int(arg) if dimension < 0: msg = f"invalid terminal dimensions: '{arg}' is not a valid terminal dimension. Must be >= 0." raise argparse.ArgumentTypeError(msg) except ValueError: msg = f"invalid terminal dimensions: '{arg}' is not a valid terminal dimension. Must be >= 0." raise argparse.ArgumentTypeError(msg) from None else: return dimension class Ease: """Validate argument is a valid easing function. Easing functions are prefixed by "in", "out", or "in_out" and suffixed by a valid easing function. Raises: argparse.ArgumentTypeError: Value is not a valid easing function. """ METAVAR = "(Easing Function)" @staticmethod def type_parser(arg: str) -> typing.Callable: """Validate argument is a valid easing function. Args: arg (str): argument to validate Raises: argparse.ArgumentTypeError: Ease value is not a valid easing function. Returns: Ease: validated ease value """ easing_func_map = { "linear": easing.linear, "in_sine": easing.in_sine, "out_sine": easing.out_sine, "in_out_sine": easing.in_out_sine, "in_quad": easing.in_quad, "out_quad": easing.out_quad, "in_out_quad": easing.in_out_quad, "in_cubic": easing.in_cubic, "out_cubic": easing.out_cubic, "in_out_cubic": easing.in_out_cubic, "in_quart": easing.in_quart, "out_quart": easing.out_quart, "in_out_quart": easing.in_out_quart, "in_quint": easing.in_quint, "out_quint": easing.out_quint, "in_out_quint": easing.in_out_quint, "in_expo": easing.in_expo, "out_expo": easing.out_expo, "in_out_expo": easing.in_out_expo, "in_circ": easing.in_circ, "out_circ": easing.out_circ, "in_out_circ": easing.in_out_circ, "in_back": easing.in_back, "out_back": easing.out_back, "in_out_back": easing.in_out_back, "in_elastic": easing.in_elastic, "out_elastic": easing.out_elastic, "in_out_elastic": easing.in_out_elastic, "in_bounce": easing.in_bounce, "out_bounce": easing.out_bounce, "in_out_bounce": easing.in_out_bounce, } try: return easing_func_map[arg.lower()] except KeyError: msg = f"invalid ease value: '{arg}' is not a valid ease." raise argparse.ArgumentTypeError(msg) from None class EasingStep: """Validate argument is a valid easing step size value. Raises: argparse.ArgumentTypeError: Value is not a valid easing step size. """ METAVAR = "0 < float(n) <= 1" @staticmethod def type_parser(arg: str) -> float: """Validate argument is a valid easing step size value. Args: arg (str): argument to validate Raises: argparse.ArgumentTypeError: Value is not a valid easing step size. Returns: float: validated easing step size value """ try: f = float(arg) except ValueError: msg = f"invalid value: '{arg}' is not a valid float." raise argparse.ArgumentTypeError(msg) from None if 0 < f <= 1: return f msg = f"invalid value: '{arg}' is not a float > 0 and <= 1. Example: 0.5" raise argparse.ArgumentTypeError(msg) terminaltexteffects-release-0.12.1/terminaltexteffects/utils/colorterm.py000066400000000000000000000051541507200677100271030ustar00rootroot00000000000000"""Convert xterm color codes and hex colors into ANSI escape sequences. Functions: fg(color_code: str | int) -> str: Set the foreground color of the terminal text. bg(color_code: str | int) -> str: Set the background color of the terminal text. """ from __future__ import annotations def _hex_to_int(hex_color: str) -> tuple[int, int, int]: """Convert a hex color string into a list of integers. Args: hex_color (str): Hex color string in the range 000000 -> FFFFFF. '#' is optional. Returns: tuple[int, int, int]: A tuple of integers [RED, GREEN, BLUE] representing the color in RGB format. """ hex_color = hex_color.strip("#") ints = [int(hex_color[i : i + 2], 16) for i in range(0, 6, 2)] return ints[0], ints[1], ints[2] def _color(color_code: str | int, location: int) -> str: """Return an ANSI escape sequence to color the foreground/background of text. This is a helper function for fg() and bg(). Args: color_code (str | int): The color code to be converted. location (int): The location to apply the color. Returns: str: The ANSI escape sequence for the color. Raises: ValueError: If the color code is not in the range 000000 -> FFFFFF or 0 -> 255. """ if isinstance(color_code, str): color_ints = _hex_to_int(color_code) sequence = f"\x1b[{location};2;{color_ints[0]};{color_ints[1]};{color_ints[2]}m" elif isinstance(color_code, int): if color_code not in range(256): msg = f"Got color code ({color_code}): xterm color codes must be an integer: 0 <= n <= 255" raise ValueError(msg) sequence = f"\x1b[{location};5;{color_code}m" else: msg = ( f"Got color code ({color_code}): Color must be either hex string #000000 -> #FFFFFF or" f" int xterm color code 0 <= n <= 255" ) raise TypeError( msg, ) return sequence def fg(color_code: str | int) -> str: """Set the foreground color of the terminal text. Args: color_code (str | int): The value to set the foreground color, as a hex string or X-Term 256 color code. Returns: str: The ANSI escape sequence to set the foreground color. """ return _color(color_code, 38) def bg(color_code: str | int) -> str: """Set the background color of the terminal text. Args: color_code (str | int): The value to set the background color, as a hex string or X-Term 256 color code. Returns: str: The ANSI escape sequence to set the background color. """ return _color(color_code, 48) terminaltexteffects-release-0.12.1/terminaltexteffects/utils/easing.py000066400000000000000000000421631507200677100263440ustar00rootroot00000000000000"""Functions for easing calculations. Functions: linear: Linear easing function. in_sine: Ease in using a sine function. out_sine: Ease out using a sine function. in_out_sine: Ease in/out using a sine function. in_quad: Ease in using a quadratic function. out_quad: Ease out using a quadratic function. in_out_quad: Ease in/out using a quadratic function. in_cubic: Ease in using a cubic function. out_cubic: Ease out using a cubic function. in_out_cubic: Ease in/out using a cubic function. in_quart: Ease in using a quartic function. out_quart: Ease out using a quartic function. in_out_quart: Ease in/out using a quartic function. in_quint: Ease in using a quintic function. out_quint: Ease out using a quintic function. in_out_quint: Ease in/out using a quintic function. in_expo: Ease in using an exponential function. out_expo: Ease out using an exponential function. in_out_expo: Ease in/out using an exponential function. in_circ: Ease in using a circular function. out_circ: Ease out using a circular function. in_out_circ: Ease in/out using a circular function. in_back: Ease in using a back function. out_back: Ease out using a back function. in_out_back: Ease in/out using a back function. in_elastic: Ease in using an elastic function. out_elastic: Ease out using an elastic function. in_out_elastic: Ease in/out using an elastic function. in_bounce: Ease in using a bounce function. out_bounce: Ease out using a bounce function. in_out_bounce: Ease in/out using a bounce function. eased_step_function: Create a closure that returns the eased value of each step from 0 to 1 increasing by the step_size. """ from __future__ import annotations import functools import math import typing # EasingFunction is a type alias for a function that takes a float between 0 and 1 and returns a float between 0 and 1. EasingFunction = typing.Callable[[float], float] "EasingFunctions take a float between 0 and 1 and return a float between 0 and 1." def linear(progress_ratio: float) -> float: """Linear easing function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ return progress_ratio def in_sine(progress_ratio: float) -> float: """Ease in using a sine function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ return 1 - math.cos((progress_ratio * math.pi) / 2) def out_sine(progress_ratio: float) -> float: """Ease out using a sine function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ return math.sin((progress_ratio * math.pi) / 2) def in_out_sine(progress_ratio: float) -> float: """Ease in/out using a sine function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ return -(math.cos(math.pi * progress_ratio) - 1) / 2 def in_quad(progress_ratio: float) -> float: """Ease in using a quadratic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ return progress_ratio**2 def out_quad(progress_ratio: float) -> float: """Ease out using a quadratic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ return 1 - (1 - progress_ratio) * (1 - progress_ratio) def in_out_quad(progress_ratio: float) -> float: """Ease in/out using a quadratic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ if progress_ratio < 0.5: return 2 * progress_ratio**2 return 1 - (-2 * progress_ratio + 2) ** 2 / 2 def in_cubic(progress_ratio: float) -> float: """Ease in using a cubic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ return progress_ratio**3 def out_cubic(progress_ratio: float) -> float: """Ease out using a cubic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ return 1 - (1 - progress_ratio) ** 3 def in_out_cubic(progress_ratio: float) -> float: """Ease in/out using a cubic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 representing the percentage of the current waypoint speed to apply to the character """ if progress_ratio < 0.5: return 4 * progress_ratio**3 return 1 - (-2 * progress_ratio + 2) ** 3 / 2 def in_quart(progress_ratio: float) -> float: """Ease in using a quartic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 representing the percentage of the current waypoint speed to apply to the character """ return progress_ratio**4 def out_quart(progress_ratio: float) -> float: """Ease out using a quartic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ return 1 - (1 - progress_ratio) ** 4 def in_out_quart(progress_ratio: float) -> float: """Ease in/out using a quartic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ if progress_ratio < 0.5: return 8 * progress_ratio**4 return 1 - (-2 * progress_ratio + 2) ** 4 / 2 def in_quint(progress_ratio: float) -> float: """Ease in using a quintic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ return progress_ratio**5 def out_quint(progress_ratio: float) -> float: """Ease out using a quintic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ return 1 - (1 - progress_ratio) ** 5 def in_out_quint(progress_ratio: float) -> float: """Ease in/out using a quintic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ if progress_ratio < 0.5: return 16 * progress_ratio**5 return 1 - (-2 * progress_ratio + 2) ** 5 / 2 def in_expo(progress_ratio: float) -> float: """Ease in using an exponential function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ if progress_ratio == 0: return 0 return 2 ** (10 * progress_ratio - 10) def out_expo(progress_ratio: float) -> float: """Ease out using an exponential function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ if progress_ratio == 1: return 1 return 1 - 2 ** (-10 * progress_ratio) def in_out_expo(progress_ratio: float) -> float: """Ease in/out using an exponential function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ if progress_ratio == 0: return 0 if progress_ratio == 1: return 1 if progress_ratio < 0.5: return 2 ** (20 * progress_ratio - 10) / 2 return (2 - 2 ** (-20 * progress_ratio + 10)) / 2 def in_circ(progress_ratio: float) -> float: """Ease in using a circular function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ return 1 - math.sqrt(1 - progress_ratio**2) def out_circ(progress_ratio: float) -> float: """Ease out using a circular function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ return math.sqrt(1 - (progress_ratio - 1) ** 2) def in_out_circ(progress_ratio: float) -> float: """Ease in/out using a circular function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ if progress_ratio < 0.5: return (1 - math.sqrt(1 - (2 * progress_ratio) ** 2)) / 2 return (math.sqrt(1 - (-2 * progress_ratio + 2) ** 2) + 1) / 2 def in_back(progress_ratio: float) -> float: """Ease in using a back function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ c1 = 1.70158 c3 = c1 + 1 return c3 * progress_ratio**3 - c1 * progress_ratio**2 def out_back(progress_ratio: float) -> float: """Ease out using a back function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ c1 = 1.70158 c3 = c1 + 1 return 1 + c3 * (progress_ratio - 1) ** 3 + c1 * (progress_ratio - 1) ** 2 def in_out_back(progress_ratio: float) -> float: """Ease in/out using a back function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ c1 = 1.70158 c2 = c1 * 1.525 if progress_ratio < 0.5: return ((2 * progress_ratio) ** 2 * ((c2 + 1) * 2 * progress_ratio - c2)) / 2 return ((2 * progress_ratio - 2) ** 2 * ((c2 + 1) * (progress_ratio * 2 - 2) + c2) + 2) / 2 def in_elastic(progress_ratio: float) -> float: """Ease in using an elastic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ c4 = (2 * math.pi) / 3 if progress_ratio == 0: return 0 if progress_ratio == 1: return 1 return -(2 ** (10 * progress_ratio - 10)) * math.sin((progress_ratio * 10 - 10.75) * c4) def out_elastic(progress_ratio: float) -> float: """Ease out using an elastic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 representing the percentage of the current waypoint speed to apply to the character """ c4 = (2 * math.pi) / 3 if progress_ratio == 0: return 0 if progress_ratio == 1: return 1 return 2 ** (-10 * progress_ratio) * math.sin((progress_ratio * 10 - 0.75) * c4) + 1 def in_out_elastic(progress_ratio: float) -> float: """Ease in/out using an elastic function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 representing the percentage of the current waypoint speed to apply to the character """ c5 = (2 * math.pi) / 4.5 if progress_ratio == 0: return 0 if progress_ratio == 1: return 1 if progress_ratio < 0.5: return -(2 ** (20 * progress_ratio - 10) * math.sin((20 * progress_ratio - 11.125) * c5)) / 2 return (2 ** (-20 * progress_ratio + 10) * math.sin((20 * progress_ratio - 11.125) * c5)) / 2 + 1 def in_bounce(progress_ratio: float) -> float: """Ease in using a bounce function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 representing the percentage of the current waypoint speed to apply to the character """ return 1 - out_bounce(1 - progress_ratio) def out_bounce(progress_ratio: float) -> float: """Ease out using a bounce function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ n1 = 7.5625 d1 = 2.75 if progress_ratio < 1 / d1: return n1 * progress_ratio**2 if progress_ratio < 2 / d1: return n1 * (progress_ratio - 1.5 / d1) ** 2 + 0.75 if progress_ratio < 2.5 / d1: return n1 * (progress_ratio - 2.25 / d1) ** 2 + 0.9375 return n1 * (progress_ratio - 2.625 / d1) ** 2 + 0.984375 def in_out_bounce(progress_ratio: float) -> float: """Ease in/out using a bounce function. Args: progress_ratio (float): the ratio of the current step to the maximum steps Returns: float: 0 <= n <= 1 eased value """ if progress_ratio < 0.5: return (1 - out_bounce(1 - 2 * progress_ratio)) / 2 return (1 + out_bounce(2 * progress_ratio - 1)) / 2 def make_easing(x1: float, y1: float, x2: float, y2: float) -> EasingFunction: """Create a cubic Bezier easing function using the provided control points. The easing function maps an input progress ratio (0 to 1) to an output value (0 to 1) according to a cubic Bezier curve defined by four points: - Start point: (0, 0) - First control point: (x1, y1) - Second control point: (x2, y2) - End point: (1, 1) Args: x1 (float): Determines the horizontal position of the first control point. Smaller values make the curve start off steeper, while larger values delay the initial acceleration. y1 (float): Determines the vertical position of the first control point. Smaller values create a gentler ease-in effect; larger values increase the initial acceleration. x2 (float): Determines the horizontal position of the second control point. Larger values extend the period of change, affecting how late the acceleration or deceleration begins. y2 (float): Determines the vertical position of the second control point. Larger values can create a more abrupt ease-out effect; smaller values result in a smoother finish. Note: Use a resource such as cubic-bezier.com to design an appropriate easing curve for your needs. Returns: EasingFunction: A function that takes a progress_ratio (0 <= progress_ratio <= 1) and returns the eased value computed from the cubic Bezier curve. """ # Compute Bezier curve x for a given parameter t. def sample_curve_x(t: float) -> float: return 3 * x1 * (1 - t) ** 2 * t + 3 * x2 * (1 - t) * t**2 + t**3 # Compute Bezier curve y for a given parameter t. def sample_curve_y(t: float) -> float: return 3 * y1 * (1 - t) ** 2 * t + 3 * y2 * (1 - t) * t**2 + t**3 # Compute derivative of curve x with respect to t. def sample_curve_derivative_x(t: float) -> float: return 3 * (1 - t) ** 2 * x1 + 6 * (1 - t) * t * (x2 - x1) + 3 * t**2 * (1 - x2) def bezier_easing(progress: float) -> float: # Clamp progress between 0 and 1. if progress <= 0: return 0 if progress >= 1: return 1 # Find t such that sample_curve_x(t) is close to progress. t = progress # initial guess for _ in range(20): x_est = sample_curve_x(t) dx = x_est - progress if abs(dx) < 1e-5: break d = sample_curve_derivative_x(t) if abs(d) < 1e-6: break t -= dx / d return sample_curve_y(t) return functools.wraps(bezier_easing)(functools.lru_cache(maxsize=8192)(bezier_easing)) make_easing = functools.wraps(make_easing)(functools.lru_cache(maxsize=8192)(make_easing)) def eased_step_function( easing_func: EasingFunction, step_size: float, *, clamp: bool = False, ) -> typing.Callable[[], tuple[float, float]]: """Create a closure that returns the eased value of each step from 0 to 1 increasing by the step_size. Args: easing_func (EasingFunction): The easing function to use. step_size (float): The step size. clamp (bool): If True, the easing function will be limited to 0 <= n <= 1. Defaults to False. Returns: callable[[],tuple[float,float]]: A closure that returns a tuple of the current input step and eased value of the current input step. """ if not 0 < step_size <= 1: msg = "Step size must be 0 < n <= 1." raise ValueError(msg) current_step = 0.0 def ease() -> tuple[float, float]: nonlocal current_step eased_value = easing_func(current_step) used_step = current_step if current_step < 1: current_step = min((current_step + step_size, 1.0)) if clamp: eased_value = max(0, min(eased_value, 1)) return used_step, eased_value return ease terminaltexteffects-release-0.12.1/terminaltexteffects/utils/exceptions/000077500000000000000000000000001507200677100266775ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/terminaltexteffects/utils/exceptions/__init__.py000066400000000000000000000015541507200677100310150ustar00rootroot00000000000000"""TerminalTextEffects exceptions module.""" from terminaltexteffects.utils.exceptions.animation_exceptions import ( ActivateEmptySceneError, ApplyGradientToSymbolsEmptyGradientsError, ApplyGradientToSymbolsInvalidSymbolError, ApplyGradientToSymbolsNoGradientsError, FrameDurationError, SceneNotFoundError, ) from terminaltexteffects.utils.exceptions.base_character_exceptions import ( EventRegistrationCallerError, EventRegistrationTargetError, ) from terminaltexteffects.utils.exceptions.motion_exceptions import ( ActivateEmptyPathError, DuplicatePathIDError, DuplicateWaypointIDError, PathInvalidSpeedError, PathNotFoundError, WaypointNotFoundError, ) from terminaltexteffects.utils.exceptions.terminal_exceptions import ( InvalidCharacterGroupError, InvalidCharacterSortError, InvalidColorSortError, ) terminaltexteffects-release-0.12.1/terminaltexteffects/utils/exceptions/animation_exceptions.py000066400000000000000000000103241507200677100334710ustar00rootroot00000000000000"""Custom exceptions for handling errors related to animations in the terminaltexteffects package. Classes: FrameDurationError: Raised when a frame is added to a Scene with an invalid duration. ActivateEmptySceneError: Raised when a Scene without any frames is activated. ApplyGradientToSymbolsNoGradientsError: Raised when calling `apply_gradient_to_symbols` without gradients. ApplyGradientToSymbolsEmptyGradientsError: Raised when calling `apply_gradient_to_symbols` with empty gradients. """ from __future__ import annotations from typing import TYPE_CHECKING from terminaltexteffects.utils.exceptions.base_terminaltexteffects_exception import TerminalTextEffectsError if TYPE_CHECKING: from terminaltexteffects import Scene class FrameDurationError(TerminalTextEffectsError): """Raised when a frame is added to a Scene with an invalid duration. A frame duration must be a positive integer. This error is raised when a frame is added to a Scene with a duration that is not a positive integer. """ def __init__(self, duration: int) -> None: """Initialize a FrameDurationError. Args: duration (int): The duration provided to the frame. """ self.duration = duration self.message = f"Frame duration must be a positive integer. Received: `{duration}`." super().__init__(self.message) class ActivateEmptySceneError(TerminalTextEffectsError): """Raised when a Scene is without any frames is activated. A Scene must have at least one frame to be activated. """ def __init__(self, scene: Scene) -> None: """Initialize an ActivateEmptySceneError. Args: scene (Scene): The Scene that was activated. """ self.scene = scene self.message = f"Scene `{scene.scene_id}` has no frames. A Scene must have at least one frame to be activated." super().__init__(self.message) class ApplyGradientToSymbolsNoGradientsError(TerminalTextEffectsError): """Raised when calling `apply_gradient_to_symbols` without gradients. At least one gradient must be provided, either a foreground gradient or a background gradient when calling `apply_gradient_to_symbols`. """ def __init__(self) -> None: """Initialize an ApplyGradientToSymbolsNoGradientsError.""" self.message = "Foreground and background gradient are None. At least one gradient must be provided." super().__init__(self.message) class ApplyGradientToSymbolsEmptyGradientsError(TerminalTextEffectsError): """Raised when calling `apply_gradient_to_symbols` with empty gradients. At least one gradient must be provided, either a foreground gradient or a background gradient when calling `apply_gradient_to_symbols`. In addition, at least one of the gradients must have at least one color. """ def __init__(self) -> None: """Initialize an ApplyGradientToSymbolsEmptyGradientsError.""" self.message = ( "Foreground and background gradient are empty. At least one gradient must have at least " "one color in the spectrum." ) super().__init__(self.message) class ApplyGradientToSymbolsInvalidSymbolError(TerminalTextEffectsError): """Raised when calling `apply_gradient_to_symbols` with an invalid symbol. The symbol provided to `apply_gradient_to_symbols` must be a string with a length of 1. """ def __init__(self, symbol: str) -> None: """Initialize an ApplyGradientToSymbolsInvalidSymbolError. Args: symbol (str): The symbol provided to `apply_gradient_to_symbols`. """ self.symbol = symbol self.message = f"Symbol must be a string with a length of 1. Received: `{symbol}`." super().__init__(self.message) class SceneNotFoundError(TerminalTextEffectsError): """Raised when `query_scene` is called with a scene_id that does not exist.""" def __init__(self, scene_id: str) -> None: """Initialize a SceneNotFoundError. Args: scene_id (str): The scene_id that was not found. """ self.scene_id = scene_id self.message = f"Scene with scene_id `{scene_id}` not found." super().__init__(self.message) terminaltexteffects-release-0.12.1/terminaltexteffects/utils/exceptions/base_character_exceptions.py000066400000000000000000000067341507200677100344520ustar00rootroot00000000000000"""Custom exceptions for handling errors related to EffectCharacters in the terminaltexteffects package.""" from __future__ import annotations from typing import TYPE_CHECKING from terminaltexteffects.utils.exceptions.base_terminaltexteffects_exception import TerminalTextEffectsError if TYPE_CHECKING: from terminaltexteffects import Coord, EventHandler, Path, Scene, Waypoint class EventRegistrationCallerError(TerminalTextEffectsError): """Raised when an event is registered with an invalid event -> caller relationship. Each event can only be registered with a related caller type. This error is raised when an event is registered with a caller that is not of the required type. For example, a Scene will never trigger a Path related event, and vice versa. The following are the valid caller types for each event: Event -> Caller - SEGMENT_* -> Path - PATH_* -> Path - SCENE_* -> Scene """ def __init__( self, event: EventHandler.Event, caller: Scene | Waypoint | Path, required: type[Scene | Waypoint | Path], ) -> None: """Initialize an EventRegistrationCallerError. Args: event (EventHandler.Event): The event that was registered. caller (Scene | Waypoint | Path): The object provided to trigger the event. required (Scene | Waypoint | Path): The valid caller types for the event. """ self.event = event self.caller = caller self.required = required self.message = ( f"Event `{event.name}` registered with caller type `{caller.__class__.__name__}`. Event `{event.name}` " f"requires caller type `{required.__name__}`." ) super().__init__(self.message) class EventRegistrationTargetError(TerminalTextEffectsError): """Raised when an event is registered with an invalid action -> target relationship. Each event action can only be registered with a related target type. This error is raised when an event action is registered with a target that is not of the required type. For example, an ACTIVATE_SCENE action will can not activate on a Path target. The following are the valid target types for each action: Action -> Target - *_SCENE -> Scene - *_PATH -> Path - SET_LAYER -> Int - SET_COORDINATE -> Coord - CALLBACK -> EventHandler.Callback - RESET_APPEARANCE -> None """ def __init__( self, action: EventHandler.Action, target: Scene | Path | int | Coord | EventHandler.Callback | None, required: type[Scene | Path | int | Coord | EventHandler.Callback | None], ) -> None: """Initialize an EventRegistrationTargetError. Args: action (EventHandler.Action): The action that was registered. target (Scene | Path | int | Coord | EventHandler.Callback | None): The target provided to the action. required (type[Scene | Path | int | Coord | EventHandler.Callback | None]): The valid target types. """ self.action = action self.target = target self.required = required self.message = ( f"Event action `{action.name}` registered with target type `{target.__class__.__name__}`. " f"Action `{action.name}` requires target type `{required.__name__}`." ) super().__init__(self.message) base_terminaltexteffects_exception.py000066400000000000000000000002711507200677100363220ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/terminaltexteffects/utils/exceptions"""Base class for exceptions in the terminaltexteffects package.""" class TerminalTextEffectsError(Exception): """Base class for exceptions in the terminaltexteffects package.""" terminaltexteffects-release-0.12.1/terminaltexteffects/utils/exceptions/motion_exceptions.py000066400000000000000000000070121507200677100330170ustar00rootroot00000000000000"""Custom exceptions for handling errors related to motion in the terminaltexteffects package.""" from __future__ import annotations from terminaltexteffects.utils.exceptions.base_terminaltexteffects_exception import TerminalTextEffectsError class PathInvalidSpeedError(TerminalTextEffectsError): """Raised when a Path is initialized with an invalid speed. A Path must be initialized with a speed that is a positive float. This error is raised when a Path is initialized with a speed that is not a positive float. """ def __init__(self, speed: float) -> None: """Initialize a PathInvalidSpeedError. Args: speed (float): The speed provided to the Path. """ self.speed = speed self.message = f"Path speed must be a positive float. Received: `{speed}`." super().__init__(self.message) class WaypointNotFoundError(TerminalTextEffectsError): """Raised when a Waypoint is not found in a Path. A WaypointNotFoundError is raised when a Waypoint with the given ID is not found in a Path. """ def __init__(self, waypoint_id: str) -> None: """Initialize a WaypointNotFoundError. Args: waypoint_id (str): The waypoint ID queried. """ self.waypoint_id = waypoint_id self.message = f"Waypoint `{waypoint_id}` not found in Path." super().__init__(self.message) class PathNotFoundError(TerminalTextEffectsError): """Raised when a Path is not found. A PathNotFoundError is raised when a Path with the given ID is not found. """ def __init__(self, path_id: str) -> None: """Initialize a PathNotFoundError. Args: path_id (str): The path ID queried. """ self.path_id = path_id self.message = f"Path `{path_id}` not found." super().__init__(self.message) class ActivateEmptyPathError(TerminalTextEffectsError): """Raised when attempting to activate an empty Path. An ActivateEmptyPathError is raised when attempting to activate a Path that has no Waypoints. """ def __init__(self, path_id: str) -> None: """Initialize an ActivateEmptyPathError. Args: path_id (str): The ID of the Path that is empty. """ self.path_id = path_id self.message = f"Cannot activate an empty Path `{path_id}`." super().__init__(self.message) class DuplicatePathIDError(TerminalTextEffectsError): """Raised when a Path is initialized with a duplicate ID. A DuplicatePathIDError is raised when a Path is initialized with an ID that has already been used. """ def __init__(self, path_id: str) -> None: """Initialize a DuplicatePathIDError. Args: path_id (str): The ID provided to the Path. """ self.path_id = path_id self.message = f"Path ID `{path_id}` has already been used." super().__init__(self.message) class DuplicateWaypointIDError(TerminalTextEffectsError): """Raised when a Waypoint is initialized with a duplicate ID. A DuplicateWaypointIDError is raised when a Waypoint is initialized with an ID that has already been used in the Path. """ def __init__(self, waypoint_id: str) -> None: """Initialize a DuplicateWaypointIDError. Args: waypoint_id (str): The ID provided to the Waypoint. """ self.waypoint_id = waypoint_id self.message = f"Waypoint ID `{waypoint_id}` has already been used." super().__init__(self.message) terminaltexteffects-release-0.12.1/terminaltexteffects/utils/exceptions/terminal_exceptions.py000066400000000000000000000051271507200677100333320ustar00rootroot00000000000000"""Custom exceptions for handling errors related to the Terminal in the terminaltexteffects package.""" from __future__ import annotations from typing import TYPE_CHECKING from terminaltexteffects.utils.exceptions.base_terminaltexteffects_exception import TerminalTextEffectsError if TYPE_CHECKING: from terminaltexteffects.engine.terminal import Terminal class InvalidCharacterGroupError(TerminalTextEffectsError): """Raised when an invalid character group is provided to a Terminal method. An InvalidCharacterGroupError is raised when a character group is provided to a Terminal method that is not a valid character group. Ref Terminal.CharacterGroup. """ def __init__(self, character_group: Terminal.CharacterGroup | str) -> None: """Initialize an InvalidCharacterGroupError. Args: character_group (Terminal.CharacterGroup | str): The character group provided to the Terminal method. """ self.character_group = character_group self.message = f"Invalid character group provided: `{character_group}`. Ref Terminal.CharacterGroup." super().__init__(self.message) class InvalidCharacterSortError(TerminalTextEffectsError): """Raised when an invalid character sort is provided to a Terminal method. An InvalidCharacterSortError is raised when a character sort is provided to a Terminal method that is not a valid character sort. Ref Terminal.CharacterSort. """ def __init__(self, character_sort: Terminal.CharacterSort | str) -> None: """Initialize an InvalidCharacterSortError. Args: character_sort (Terminal.CharacterSort | str): The character sort provided to the Terminal method. """ self.character_sort = character_sort self.message = f"Invalid character sort provided: `{character_sort}`. Ref Terminal.CharacterSort." super().__init__(self.message) class InvalidColorSortError(TerminalTextEffectsError): """Raised when an invalid color sort is provided to a Terminal method. An InvalidColorSortError is raised when a color sort is provided to a Terminal method that is not a valid color sort. Ref Terminal.ColorSort. """ def __init__(self, color_sort: Terminal.ColorSort) -> None: """Initialize an InvalidColorSortError. Args: color_sort (Terminal.ColorSort): The color sort provided to the Terminal method. """ self.color_sort = color_sort self.message = f"Invalid color sort provided: `{color_sort}`. Ref Terminal.ColorSort." super().__init__(self.message) terminaltexteffects-release-0.12.1/terminaltexteffects/utils/geometry.py000066400000000000000000000266441507200677100267370ustar00rootroot00000000000000"""Utility functions for geometric calculations and operations. The purpose of these functions is to find terminal coordinates that fall within certain regions or along certain paths. These functions are used by effects to enable more complex animations and movement paths. Functions: find_coords_on_circle: Finds points on a circle given the origin, radius, and number of points. find_coords_in_circle: Finds coordinates within an ellipse given the center and major axis length. find_coords_in_rect: Finds coordinates within a rectangle given the origin and distance. find_coord_at_distance: Finds the coordinate at a given distance along a line defined by two coordinates. find_coord_on_bezier_curve: Finds points on a bezier curve. find_coord_on_line: Finds points on a line. find_length_of_bezier_curve: Finds the length of a quadratic or cubic bezier curve. find_length_of_line: Finds the length of a line intersecting two coordinates. find_normalized_distance_from_center: Returns the normalized distance from the center of the Canvas. """ from __future__ import annotations import functools import math from dataclasses import dataclass @dataclass(eq=True, frozen=True) class Coord: """A coordinate with row and column values. Args: column (int): column value row (int): row value """ column: int row: int def find_coords_on_circle(origin: Coord, radius: int, coords_limit: int = 0, *, unique: bool = True) -> list[Coord]: """Find points on a circle. Args: origin (Coord): origin of the circle radius (int): radius of the circle coords_limit (int): limit the number of coords returned, if 0, the number of points is calculated based on the circumference of the circle unique (bool): whether to remove duplicate points. Defaults to True. Returns: list (Coord): list of Coord points on the circle """ points: list[Coord] = [] if not radius: return points seen_points = set() if not coords_limit: coords_limit = round(2 * math.pi * radius) angle_step = 2 * math.pi / coords_limit for i in range(coords_limit): angle = angle_step * i x = origin.column + radius * math.cos(angle) # correct for terminal character height/width ratio by doubling the x distance from origin x_diff = x - origin.column x += x_diff y = origin.row + radius * math.sin(angle) point_coord = Coord(round(x), round(y)) if unique: if point_coord not in seen_points: points.append(point_coord) else: points.append(point_coord) seen_points.add(point_coord) return points find_coords_on_circle = functools.wraps(find_coords_on_circle)(functools.lru_cache(maxsize=8192)(find_coords_on_circle)) def find_coords_in_circle(center: Coord, diameter: int) -> list[Coord]: """Find the coordinates within a circle with the given center and diameter. The actual shape calculated is an ellipse with a major axis of length diameter, however the terminal cell height/width ratio creates a circle visually. Args: center (Coord): The center coordinate of the circle. diameter (int): The length of the major axis of the circle. Returns: list[Coord]: A list of coordinates within the circle. """ h, k = center.column, center.row coords_in_ellipse: list[Coord] = [] if not diameter: return coords_in_ellipse a_squared = diameter**2 b_squared = (diameter / 2) ** 2 for x in range(h - diameter, h + diameter + 1): x_component = ((x - h) ** 2) / a_squared max_y_offset = int((b_squared * (1 - x_component)) ** 0.5) for y in range(k - max_y_offset, k + max_y_offset + 1): coords_in_ellipse.append(Coord(x, y)) # noqa: PERF401 return coords_in_ellipse find_coords_in_circle = functools.wraps(find_coords_in_circle)(functools.lru_cache(maxsize=8192)(find_coords_in_circle)) def find_coords_in_rect(origin: Coord, distance: int) -> list[Coord]: """Find coords that fall within a rectangle. Distance specifies the number of units in each direction from the origin. Final width = 2 * distance + 1, final height = 2 * distance + 1. Args: origin (Coord): center of the rectangle distance (int): distance from the origin Returns: list[Coord]: list of Coord points in the rectangle """ left_boundary = origin.column - distance right_boundary = origin.column + distance top_boundary = origin.row - distance bottom_boundary = origin.row + distance coords: list[Coord] = [] if not distance: return coords for column in range(left_boundary, right_boundary + 1): for row in range(top_boundary, bottom_boundary + 1): coords.append(Coord(column, row)) # noqa: PERF401 return coords find_coords_in_rect = functools.wraps(find_coords_in_rect)(functools.lru_cache(maxsize=8192)(find_coords_in_rect)) def find_coord_at_distance(origin: Coord, target: Coord, distance: float) -> Coord: """Find the coordinate at the given distance along the line defined by the origin and target coordinates. The coordinate returned is approximately [distance] units away from the target coordinate, away from the origin coordinate. Args: origin (Coord): origin coordinate (a) target (Coord): target coordinate (b) distance (float): distance from the target coordinate (b), away from the origin coordinate (a) Returns: Coord: Coordinate at the given distance (c). """ total_distance = find_length_of_line(origin, target) + distance if total_distance == 0 or origin == target: return target t = total_distance / find_length_of_line(origin, target) next_column, next_row = ( ((1 - t) * origin.column + t * target.column), ((1 - t) * origin.row + t * target.row), ) return Coord(round(next_column), round(next_row)) find_coord_at_distance = functools.wraps(find_coord_at_distance)( functools.lru_cache(maxsize=8192)(find_coord_at_distance), ) def find_coord_on_bezier_curve(start: Coord, control: tuple[Coord, ...], end: Coord, t: float) -> Coord: """Find points on a bezier curve of any degree. Args: start (Coord): The starting coordinate of the curve. control (tuple[Coord, ...]): The control points of the curve. end (Coord): The ending coordinate of the curve. t (float): The distance factor between the start and end coordinates. Returns: Coord: The coordinate on the bezier curve corresponding to the given parameter value. """ points = [start, *list(control), end] def de_casteljau(points: list[Coord], t: float): # noqa: ANN202 if len(points) == 1: return points[0] new_points = [] for i in range(len(points) - 1): x = (1 - t) * points[i].column + t * points[i + 1].column y = (1 - t) * points[i].row + t * points[i + 1].row new_points.append(Coord(x, y)) # type: ignore[arg-type] return de_casteljau(new_points, t) result = de_casteljau(points, t) return Coord(round(result.column), round(result.row)) find_coord_on_bezier_curve = functools.wraps(find_coord_on_bezier_curve)( functools.lru_cache(maxsize=16384)(find_coord_on_bezier_curve), ) def find_coord_on_line(start: Coord, end: Coord, t: float) -> Coord: """Find points on a line. Args: start (Coord): The starting coordinate of the line. end (Coord): The ending coordinate of the line. t (float): The distance factor between the start and end coordinates. Returns: Coord: The coordinate on the line corresponding to the given parameter value. """ x = (1 - t) * start.column + t * end.column y = (1 - t) * start.row + t * end.row return Coord(round(x), round(y)) find_coord_on_line = functools.wraps(find_coord_on_line)(functools.lru_cache(maxsize=16384)(find_coord_on_line)) def find_length_of_bezier_curve(start: Coord, control: tuple[Coord, ...] | Coord, end: Coord) -> float: """Find the length of a bezier curve. Args: start (Coord): The starting coordinate of the curve. control (tuple[Coord, ...] | Coord): The control point(s) of the curve. end (Coord): The ending coordinate of the curve. Returns: float: The length of the bezier curve. """ if isinstance(control, Coord): control = (control,) length = 0.0 prev_coord = start for t in range(1, 10): coord = find_coord_on_bezier_curve(start, control, end, t / 10) length += find_length_of_line(prev_coord, coord) prev_coord = coord prev_coord = coord return length find_length_of_bezier_curve = functools.wraps(find_length_of_bezier_curve)( functools.lru_cache(maxsize=4096)(find_length_of_bezier_curve), ) def find_length_of_line(coord1: Coord, coord2: Coord, *, double_row_diff: bool = False) -> float: """Return the length of the line intersecting coord1 and coord2. If double_row_diff is True, the distance is doubled to account for the terminal character height/width ratio. Args: coord1 (Coord): first coordinate. coord2 (Coord): second coordinate. double_row_diff (bool, optional): whether to double the row difference to account for terminal character height/width ratio. Defaults to False. Returns: float: length of the line """ column_diff = coord2.column - coord1.column row_diff = coord2.row - coord1.row if double_row_diff: return math.hypot(column_diff, 2 * row_diff) return math.hypot(column_diff, row_diff) find_length_of_line = functools.wraps(find_length_of_line)(functools.lru_cache(maxsize=8192)(find_length_of_line)) def find_normalized_distance_from_center(bottom: int, top: int, left: int, right: int, other_coord: Coord) -> float: """Return the normalized distance from the center of a rectangle on the Canvas as a float between 0 and 1. The distance is calculated using the Pythagorean theorem and accounts for the aspect ratio of the terminal. Args: bottom (int): Bottom row of the rectangle on the Canvas. top (int): Top row of the rectangle on the Canvas. left (int): Left column of the rectangle on the Canvas. right (int): Right column of the rectangle on the Canvas. other_coord (Coord): Other coordinate from which to calculate the distance to the center of the rectangle. Returns: float: Normalized distance from the center of the rectangle on the Canvas, float between 0 and 1. """ y_offset = bottom - 1 x_offset = left - 1 right = right - x_offset top = top - y_offset center_x = right / 2 center_y = top / 2 if (other_coord.column - x_offset) not in range(left - x_offset, right + 1) or ( other_coord.row - y_offset ) not in range(bottom - y_offset, top + 1): msg = "Coordinate is not within the rectangle." raise ValueError(msg) max_distance = ((right**2) + ((top * 2) ** 2)) ** 0.5 distance = ( ((other_coord.column - x_offset) - center_x) ** 2 + (((other_coord.row - y_offset) - center_y) * 2) ** 2 ) ** 0.5 return distance / (max_distance / 2) find_normalized_distance_from_center = functools.wraps(find_normalized_distance_from_center)( functools.lru_cache(maxsize=8192)(find_normalized_distance_from_center), ) terminaltexteffects-release-0.12.1/terminaltexteffects/utils/graphics.py000066400000000000000000000464741507200677100267070ustar00rootroot00000000000000"""Classes for storing and manipulating character graphics. Classes: Color: Represents a color in the RGB color space. Can be initialized with an XTerm-256 color code or an RGB hex color string. ColorPair: Represents a pair of colors to specify a character's foreground and background colors. Gradient: A list of RGB hex color strings transitioning from one color to another. Supports various gradient directions. """ from __future__ import annotations import itertools import random import typing from dataclasses import InitVar, dataclass, field from enum import Enum, auto from terminaltexteffects.utils import ansitools, colorterm, geometry, hexterm if typing.TYPE_CHECKING: from collections.abc import Iterator class Color: """Represents a color in the RGB color space. The color can be initialized with an XTerm-256 color code or an RGB hex color string. Can be printed to display the color code and appearance as a color block. Attributes: color_arg (int | str): The color value as an XTerm-256 color code or an RGB hex color string. xterm_color (int | None): The XTerm-256 color code. None if the color is an RGB hex color string. rgb_color (str): The RGB hex color string. Properties: rgb_ints (tuple[int, int, int]): Returns the RGB values as a tuple of integers. Raises: ValueError: If the color value is not a valid XTerm-256 color code or an RGB hex color string. """ def __init__(self, color_value: int | str) -> None: """Initialize a Color object. Args: color_value (int | str): The color value as an XTerm-256 color code or an RGB hex color string. Example: 255 or 'ffffff' or '#ffffff' Raises: ValueError: If the color value is not a valid XTerm-256 color code or an RGB hex color string. """ if isinstance(color_value, str): color_value = color_value.strip("#") self.color_arg = color_value self.xterm_color: int | None = None if hexterm.is_valid_color(color_value): if isinstance(color_value, int): self.xterm_color = color_value self.rgb_color = hexterm.xterm_to_hex(color_value) else: self.rgb_color = color_value self.xterm_color = None else: msg = ( "Invalid color value. Color must be an XTerm-256 color code or an RGB hex color string. " "Example: 255 or 'ffffff' or '#ffffff'" ) raise ValueError( msg, ) @property def rgb_ints(self) -> tuple[int, int, int]: """Returns the RGB values as a tuple of integers. Returns: tuple[int, int, int]: The RGB values as a tuple of integers. """ return colorterm._hex_to_int(self.rgb_color) def __repr__(self) -> str: """Return a string representation of the Color object.""" return f"Color('{self.color_arg}')" def __str__(self) -> str: """Return a string representation of the Color object.""" color_block = f"{colorterm.fg(self.rgb_color)}█████{ansitools.reset_all()}" return ( f"Color Code: {self.rgb_color}{f' | XTerm Color: {self.xterm_color}' if self.xterm_color else ''}" f"\nColor Appearance: {color_block}" ) def __eq__(self, other: object) -> bool: """Check if two Color objects are equal.""" if not isinstance(other, Color): return NotImplemented return self.color_arg == other.color_arg def __ne__(self, other: object) -> bool: """Check if two Color objects are not equal.""" if not isinstance(other, Color): return NotImplemented return self.color_arg != other.color_arg def __hash__(self) -> int: """Return the hash value of the Color object.""" return hash(self.color_arg) def __iter__(self) -> Iterator[Color]: """Return an iterator over the Color object.""" return iter((self,)) @dataclass() class ColorPair: """Represents a pair of colors to specify a character's foreground and background colors. On init, if fg or bg is not a Color object, create a Color object with the value. Attributes: fg_color (Color | None): The foreground color. None if no foreground color is specified. bg_color (Color | None): The background color. None if no background color is specified. fg (InitVar[Color | str | int | None]): The initial foreground color value. bg (InitVar[Color | str | int | None]): The initial background color value. """ fg_color: Color | None = field(init=False, default=None) bg_color: Color | None = field(init=False, default=None) fg: InitVar[Color | str | int | None] = None bg: InitVar[Color | str | int | None] = None def __post_init__(self, init_fg_color: Color | str | int | None, init_bg_color: Color | str | int | None) -> None: """If either fg or bg is not a Color object, create a Color object with the value.""" if init_fg_color is not None and not isinstance(init_fg_color, Color): self.fg_color = Color(init_fg_color) else: self.fg_color = init_fg_color if init_bg_color is not None and not isinstance(init_bg_color, Color): self.bg_color = Color(init_bg_color) else: self.bg_color = init_bg_color def __str__(self) -> str: """Return a string representation of the ColorPair object.""" color_block = ( f"{colorterm.fg(self.fg_color.rgb_color) if self.fg_color else ''}" f"{colorterm.bg(self.bg_color.rgb_color) if self.bg_color else ''}####{ansitools.reset_all()}" ) return ( f"Foreground Color Code: {self.fg_color.rgb_color if self.fg_color else ''}" f"{f' | Foreground XTerm Color: {self.fg_color.xterm_color}' if self.fg_color and self.fg_color.xterm_color else ''}\n" f"Background Color Code: {self.bg_color.rgb_color if self.bg_color else ''}" f"{f' | Background XTerm Color: {self.bg_color.xterm_color}' if self.bg_color and self.bg_color.xterm_color else ''}" f"\nColor Appearance: {color_block}" ) class Gradient: """A Gradient is a list of RGB hex color strings transitioning from one color to another. The gradient color list is calculated using linear interpolation based on the provided start and end colors and the number of steps. Gradients can be iterated over to get the next color in the gradient color list. If there is only one color in the stops list, the gradient will be a list of the same color. If multiple steps are given, the gradient between pairs of colors will be equal to the number of steps for the pair based on the order of stops and steps. Ex: stops = ("ffffff", "aaaaaa", "000000"), steps = (6, 3) "fffffff" -> (6 steps) -> "aaaaaa" -> (3 steps) -> "000000" The step count includes the stop for each pair. Total number of colors in the resulting gradient spectrum is the sum of the steps between each pair of stops plus 1. Attributes: spectrum (list[str]): List (length=sum(steps) + 1) of RGB hex color strings """ class Direction(Enum): """Enum for specifying the direction of the gradient.""" VERTICAL = auto() HORIZONTAL = auto() RADIAL = auto() DIAGONAL = auto() def __init__(self, *stops: Color, steps: tuple[int, ...] | int = 1, loop: bool = False) -> None: """Initialize a Gradient object. Args: stops (Color): One ore more variables of type Color representing the color stops. steps (int | tuple[int, ...], optional): Number of steps or a tuple of step values for generating the spectrum. Defaults to 1. loop (bool, optional): Loop the gradient. This causes the final gradient color to transition back to the first gradient color. Defaults to False. Raises: ValueError: If no color stops are provided. Attributes: _stops (tuple[Color]): Tuple of Color objects representing the color stops. _steps (int | tuple[int, ...]): Number of steps or a tuple of step values for generating the spectrum. _loop (bool): Loop the gradient. This causes the final gradient color to transition back to the first gradient color. spectrum (list[str]): List of strings representing the generated spectrum. _index (int): Current index of the spectrum. Returns: None """ self._stops = stops if len(self._stops) < 1: msg = "At least one stop must be provided." raise ValueError(msg) self._steps = steps self._loop = loop self.spectrum: list[Color] = self._generate(self._steps) self._index: int = 0 def get_color_at_fraction(self, fraction: float) -> Color: """Return the color at a fraction of the gradient. Args: fraction (float): The fraction of the gradient to get the color for. Returns: Color: The color at the fraction of the gradient. """ if fraction < 0 or fraction > 1: msg = "Fraction must be 0 <= fraction <= 1." raise ValueError(msg) for i in range(1, len(self.spectrum) + 1): if fraction <= i / len(self.spectrum): return self.spectrum[i - 1] return self.spectrum[-1] def _generate(self, steps: int | tuple[int, ...]) -> list[Color]: """Calculate a gradient of colors between two colors using linear interpolation. If there is only one color in the stops tuple, the gradient will be a list of the same color. If multiple steps are given, the gradient between pairs of colors will be equal to the number of steps for the pair based on the order of stops and steps. Ex: stops = ("ffffff", "aaaaaa", "000000"), steps = (6, 3) Distance from "ffffff" to "aaaaaa" = 6 steps (7 colors including start and end) Distance from "aaaaaa" to "000000" = 3 steps (4 colors including start and end) Total colors in the gradient spectrum = 10 ("aaaaaa" is not repeated when transitioning from "ffffff" to "aaaaaa" and from "aaaaaa" to "000000") The step count includes the stop for each pair. Total number of colors in the resulting gradient spectrum: sum(steps) + 1 Returns: list[str]: List (length=sum(steps) + 1) of RGB hex color strings. The first and last colors are the start and end stops, respectively. """ if isinstance(steps, int): steps = (steps,) for step in steps: if step < 1: msg = "Steps must be greater than 0." raise ValueError(msg) spectrum: list[Color] = [] if len(self._stops) == 1: color = self._stops[0] spectrum.extend(color for _ in range(steps[0])) return spectrum if self._loop: self._stops = (*self._stops, self._stops[0]) a, b = itertools.tee(self._stops) next(b, None) color_pairs = list(zip(a, b)) steps = steps[: len(color_pairs)] if len(steps) < len(color_pairs): steps = steps + (steps[-1],) * (len(color_pairs) - len(steps)) color_pair: tuple[Color, Color] for color_pair, step_count in zip(color_pairs, steps): if step_count < 1: msg = f"Invalid steps: {step_count} | Steps must be greater than 0." raise ValueError(msg) start, end = color_pair start_color_ints = start.rgb_ints end_color_ints = end.rgb_ints # Initialize an empty list to store the gradient colors gradient_colors: list[Color] = [] # Calculate the color deltas for each RGB value red_delta = (end_color_ints[0] - start_color_ints[0]) // step_count green_delta = (end_color_ints[1] - start_color_ints[1]) // step_count blue_delta = (end_color_ints[2] - start_color_ints[2]) // step_count # Calculate the intermediate colors and add them to the gradient colors list range_start = int(len(spectrum) > 0) # if this is the first pair, add the start color to the spectrum for i in range(range_start, max(step_count, 0)): red = start_color_ints[0] + (red_delta * i) green = start_color_ints[1] + (green_delta * i) blue = start_color_ints[2] + (blue_delta * i) # Ensure that the RGB values are within the valid range of 0-255 red = max(0, min(red, 255)) green = max(0, min(green, 255)) blue = max(0, min(blue, 255)) # Convert the RGB values to a hex color string and add it to the gradient colors list gradient_colors.append(Color(f"{red:02x}{green:02x}{blue:02x}")) # Add the end color to the gradient colors list gradient_colors.append(end) spectrum.extend(gradient_colors) return spectrum def build_coordinate_color_mapping( self, min_row: int, max_row: int, min_column: int, max_column: int, direction: Gradient.Direction, ) -> dict[geometry.Coord, Color]: """Build a mapping of coordinates to colors based on the gradient and a direction. For example, a vertical gradient will have the same color for each column in a row. When applied across all characters in the canvas, the gradient will be visible as a vertical gradient. Args: min_row (int): The minimum row value. Must be greater than 0 and less than or equal to max_row. max_row (int): The maximum row value. Must be greater than 0 and greater than or equal to min_row. min_column (int): The minimum column value. Must be greater than 0 and less than or equal to max_column. max_column (int): The maximum column value. Must be greater than 0 and greater than or equal to min_column. direction (Gradient.Direction): The direction of the gradient. Returns: dict[Coord, str]: A mapping of coordinates to colors. """ if any(value < 1 for value in (max_row, max_column, min_row, min_column)): msg = "max_row and max_column must be greater than 0." raise ValueError(msg) if min_row > max_row or min_column > max_column: msg = "min_row and min_column must be less than or equal to max_row and max_column." raise ValueError(msg) row_offset = min_row - 1 column_offset = min_column - 1 gradient_mapping: dict[geometry.Coord, Color] = {} if direction == Gradient.Direction.VERTICAL: for row_value in range(min_row, max_row + 1): fraction = (row_value - row_offset) / (max_row - row_offset) color = self.get_color_at_fraction(fraction) for column_value in range(min_column, max_column + 1): gradient_mapping[geometry.Coord(column_value, row_value)] = color elif direction == Gradient.Direction.HORIZONTAL: for column_value in range(min_column, max_column + 1): fraction = (column_value - column_offset) / (max_column - column_offset) color = self.get_color_at_fraction(fraction) for row_value in range(1, max_row + 1): gradient_mapping[geometry.Coord(column_value, row_value)] = color elif direction == Gradient.Direction.RADIAL: for row_value in range(min_row, max_row + 1): for column_value in range(min_column, max_column + 1): distance_from_center = geometry.find_normalized_distance_from_center( min_row, max_row, min_column, max_column, geometry.Coord(column_value, row_value), ) color = self.get_color_at_fraction(distance_from_center) gradient_mapping[geometry.Coord(column_value, row_value)] = color elif direction == Gradient.Direction.DIAGONAL: for row_value in range(min_row, max_row + 1): for column_value in range(min_column, max_column + 1): fraction = (((row_value - row_offset) * 2) + (column_value - column_offset)) / ( ((max_row - row_offset) * 2) + (max_column - column_offset) ) color = self.get_color_at_fraction(fraction) gradient_mapping[geometry.Coord(column_value, row_value)] = color return gradient_mapping def __iter__(self) -> Iterator[Color]: """Return an iterator over the Gradient object.""" yield from self.spectrum def __len__(self) -> int: """Return the length of the Gradient object.""" return len(self.spectrum) @typing.overload def __getitem__(self, index: int) -> Color: ... @typing.overload def __getitem__(self, index: slice) -> list[Color]: ... def __getitem__(self, index: int | slice) -> Color | list[Color]: """Return the color at the given index or a list of colors based on the slice.""" return self.spectrum[index] def __str__(self) -> str: """Return a string representation of the Gradient object.""" color_blocks = [f"{colorterm.fg(color.rgb_color)}█{ansitools.reset_all()}" for color in self.spectrum] return f"Gradient: Stops({', '.join(c.rgb_color for c in self._stops)}), Steps({self._steps})\n" + "".join( color_blocks, ) def random_color() -> Color: """Return a random color in the range 000000 -> ffffff. Returns: Color: A random color in the range 000000 -> ffffff. """ return Color(hex(random.randint(0, 0xFFFFFF))[2:].zfill(6)) def shift_color_towards(color: Color, target_color: Color, factor: float) -> Color: """Shift one color towards another by a given factor. Args: color (Color): The original color. target_color (Color): The target color to shift towards. factor (float): The factor by which to shift the color (0.0 to 1.0). Returns: Color: The resulting color after shifting. """ def interpolate(start: float, end: float, factor: float) -> float: """Interpolate between two values by a given factor.""" return start + (end - start) * factor # Normalize RGB values color_red = int(color.rgb_color[0:2], 16) / 255 color_green = int(color.rgb_color[2:4], 16) / 255 color_blue = int(color.rgb_color[4:6], 16) / 255 target_red = int(target_color.rgb_color[0:2], 16) / 255 target_green = int(target_color.rgb_color[2:4], 16) / 255 target_blue = int(target_color.rgb_color[4:6], 16) / 255 # Interpolate RGB values new_red = interpolate(color_red, target_red, factor) new_green = interpolate(color_green, target_green, factor) new_blue = interpolate(color_blue, target_blue, factor) # Convert back to hex shifted_color = f"{int(new_red * 255):02x}{int(new_green * 255):02x}{int(new_blue * 255):02x}" return Color(shifted_color) terminaltexteffects-release-0.12.1/terminaltexteffects/utils/hexterm.py000066400000000000000000000163111507200677100265460ustar00rootroot00000000000000"""List of all XTerm-256 color codes and functions to convert between RGB Hex color strings and XTerm-256 color codes. Functions: hex_to_xterm: Convert RGB Hex colors to their closest XTerm-256 color. xterm_to_hex: Convert XTerm-256 color codes to RGB Hex colors. is_valid_color: Check if the input is a valid RGB Hex color code. """ from __future__ import annotations xterm_to_hex_map = { 0: "#000000", 1: "#800000", 2: "#008000", 3: "#808000", 4: "#000080", 5: "#800080", 6: "#008080", 7: "#c0c0c0", 8: "#808080", 9: "#ff0000", 10: "#00ff00", 11: "#ffff00", 12: "#0000ff", 13: "#ff00ff", 14: "#00ffff", 15: "#ffffff", 16: "#000000", 17: "#00005f", 18: "#000087", 19: "#0000af", 20: "#0000d7", 21: "#0000ff", 22: "#005f00", 23: "#005f5f", 24: "#005f87", 25: "#005faf", 26: "#005fd7", 27: "#005fff", 28: "#008700", 29: "#00875f", 30: "#008787", 31: "#0087af", 32: "#0087d7", 33: "#0087ff", 34: "#00af00", 35: "#00af5f", 36: "#00af87", 37: "#00afaf", 38: "#00afd7", 39: "#00afff", 40: "#00d700", 41: "#00d75f", 42: "#00d787", 43: "#00d7af", 44: "#00d7d7", 45: "#00d7ff", 46: "#00ff00", 47: "#00ff5f", 48: "#00ff87", 49: "#00ffaf", 50: "#00ffd7", 51: "#00ffff", 52: "#5f0000", 53: "#5f005f", 54: "#5f0087", 55: "#5f00af", 56: "#5f00d7", 57: "#5f00ff", 58: "#5f5f00", 59: "#5f5f5f", 60: "#5f5f87", 61: "#5f5faf", 62: "#5f5fd7", 63: "#5f5fff", 64: "#5f8700", 65: "#5f875f", 66: "#5f8787", 67: "#5f87af", 68: "#5f87d7", 69: "#5f87ff", 70: "#5faf00", 71: "#5faf5f", 72: "#5faf87", 73: "#5fafaf", 74: "#5fafd7", 75: "#5fafff", 76: "#5fd700", 77: "#5fd75f", 78: "#5fd787", 79: "#5fd7af", 80: "#5fd7d7", 81: "#5fd7ff", 82: "#5fff00", 83: "#5fff5f", 84: "#5fff87", 85: "#5fffaf", 86: "#5fffd7", 87: "#5fffff", 88: "#870000", 89: "#87005f", 90: "#870087", 91: "#8700af", 92: "#8700d7", 93: "#8700ff", 94: "#875f00", 95: "#875f5f", 96: "#875f87", 97: "#875faf", 98: "#875fd7", 99: "#875fff", 100: "#878700", 101: "#87875f", 102: "#878787", 103: "#8787af", 104: "#8787d7", 105: "#8787ff", 106: "#87af00", 107: "#87af5f", 108: "#87af87", 109: "#87afaf", 110: "#87afd7", 111: "#87afff", 112: "#87d700", 113: "#87d75f", 114: "#87d787", 115: "#87d7af", 116: "#87d7d7", 117: "#87d7ff", 118: "#87ff00", 119: "#87ff5f", 120: "#87ff87", 121: "#87ffaf", 122: "#87ffd7", 123: "#87ffff", 124: "#af0000", 125: "#af005f", 126: "#af0087", 127: "#af00af", 128: "#af00d7", 129: "#af00ff", 130: "#af5f00", 131: "#af5f5f", 132: "#af5f87", 133: "#af5faf", 134: "#af5fd7", 135: "#af5fff", 136: "#af8700", 137: "#af875f", 138: "#af8787", 139: "#af87af", 140: "#af87d7", 141: "#af87ff", 142: "#afaf00", 143: "#afaf5f", 144: "#afaf87", 145: "#afafaf", 146: "#afafd7", 147: "#afafff", 148: "#afd700", 149: "#afd75f", 150: "#afd787", 151: "#afd7af", 152: "#afd7d7", 153: "#afd7ff", 154: "#afff00", 155: "#afff5f", 156: "#afff87", 157: "#afffaf", 158: "#afffd7", 159: "#afffff", 160: "#d70000", 161: "#d7005f", 162: "#d70087", 163: "#d700af", 164: "#d700d7", 165: "#d700ff", 166: "#d75f00", 167: "#d75f5f", 168: "#d75f87", 169: "#d75faf", 170: "#d75fd7", 171: "#d75fff", 172: "#d78700", 173: "#d7875f", 174: "#d78787", 175: "#d787af", 176: "#d787d7", 177: "#d787ff", 178: "#d7af00", 179: "#d7af5f", 180: "#d7af87", 181: "#d7afaf", 182: "#d7afd7", 183: "#d7afff", 184: "#d7d700", 185: "#d7d75f", 186: "#d7d787", 187: "#d7d7af", 188: "#d7d7d7", 189: "#d7d7ff", 190: "#d7ff00", 191: "#d7ff5f", 192: "#d7ff87", 193: "#d7ffaf", 194: "#d7ffd7", 195: "#d7ffff", 196: "#ff0000", 197: "#ff005f", 198: "#ff0087", 199: "#ff00af", 200: "#ff00d7", 201: "#ff00ff", 202: "#ff5f00", 203: "#ff5f5f", 204: "#ff5f87", 205: "#ff5faf", 206: "#ff5fd7", 207: "#ff5fff", 208: "#ff8700", 209: "#ff875f", 210: "#ff8787", 211: "#ff87af", 212: "#ff87d7", 213: "#ff87ff", 214: "#ffaf00", 215: "#ffaf5f", 216: "#ffaf87", 217: "#ffafaf", 218: "#ffafd7", 219: "#ffafff", 220: "#ffd700", 221: "#ffd75f", 222: "#ffd787", 223: "#ffd7af", 224: "#ffd7d7", 225: "#ffd7ff", 226: "#ffff00", 227: "#ffff5f", 228: "#ffff87", 229: "#ffffaf", 230: "#ffffd7", 231: "#ffffff", 232: "#080808", 233: "#121212", 234: "#1c1c1c", 235: "#262626", 236: "#303030", 237: "#3a3a3a", 238: "#444444", 239: "#4e4e4e", 240: "#585858", 241: "#626262", 242: "#6c6c6c", 243: "#767676", 244: "#808080", 245: "#8a8a8a", 246: "#949494", 247: "#9e9e9e", 248: "#a8a8a8", 249: "#b2b2b2", 250: "#bcbcbc", 251: "#c6c6c6", 252: "#d0d0d0", 253: "#dadada", 254: "#e4e4e4", 255: "#eeeeee", } xterm_to_rgb_map = {k: (int(v[1:3], 16), int(v[3:5], 16), int(v[5:7], 16)) for k, v in xterm_to_hex_map.items()} def hex_to_xterm(hex_color: str) -> int: """Convert RGB Hex colors to their closest XTerm-256 color. Args: hex_color (str): RGB Hex color code, '#' is optional Returns: int: (0-255) XTerm-256 color code """ # Strip '#' if present and convert hex to RGB color_string = hex_color.strip("#") input_rgb = tuple(int(color_string[i : i + 2], 16) for i in range(0, 6, 2)) # Compute the differences between input color and each xterm color min_diff = float("inf") for xterm_color, xterm_rgb in xterm_to_rgb_map.items(): diff = sum(abs(input_rgb[i] - xterm_rgb[i]) for i in range(3)) / 3 if diff < min_diff: min_diff = diff closest_color = xterm_color return closest_color # type: ignore[unbound] def xterm_to_hex(xterm_color: int) -> str: """Convert XTerm-256 color code to RGB Hex color code. Args: xterm_color (int): (0-255) XTerm-256 color code Returns: int: RGB Hex color code Raises: ValueError: The input is not a valid XTerm-256 color code (0-255). """ if xterm_color not in xterm_to_hex_map: msg = f"Invalid XTerm-256 color code: {xterm_color}" raise ValueError(msg) return xterm_to_hex_map[xterm_color].strip("#") def is_valid_color(color: int | str) -> bool: """Check if the input is a valid RGB Hex color code. Args: color (int | str): X-Term 256 color code or RGB Hex color code, '#' is optional Returns: bool: True if the input is a valid color code """ if isinstance(color, str): if len(color.lstrip("#")) not in [6, 7]: return False try: int(color.strip("#"), 16) except ValueError: return False return True return color in range(256) terminaltexteffects-release-0.12.1/tests/000077500000000000000000000000001507200677100204405ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/tests/__init__.py000066400000000000000000000001171507200677100225500ustar00rootroot00000000000000"""Marks this directory as a Python package for test discovery and imports.""" terminaltexteffects-release-0.12.1/tests/conftest.py000066400000000000000000000246441507200677100226510ustar00rootroot00000000000000"""Pytest fixtures and constants for terminaltexteffects package.""" from __future__ import annotations from typing import TYPE_CHECKING, Any, Literal import pytest from terminaltexteffects.effects import ( effect_beams, effect_binarypath, effect_blackhole, effect_bouncyballs, effect_bubbles, effect_burn, effect_colorshift, effect_crumble, effect_decrypt, effect_errorcorrect, effect_expand, effect_fireworks, effect_highlight, effect_laseretch, effect_matrix, effect_middleout, effect_orbittingvolley, effect_overflow, effect_pour, effect_print, effect_rain, effect_random_sequence, effect_rings, effect_scattered, effect_slice, effect_slide, effect_spotlights, effect_spray, effect_swarm, effect_sweep, effect_synthgrid, effect_unstable, effect_vhstape, effect_waves, effect_wipe, ) from terminaltexteffects.engine.terminal import TerminalConfig from terminaltexteffects.utils import geometry from terminaltexteffects.utils.easing import ( EasingFunction, in_back, in_bounce, in_circ, in_cubic, in_elastic, in_expo, in_out_back, in_out_bounce, in_out_circ, in_out_cubic, in_out_elastic, in_out_expo, in_out_quad, in_out_quart, in_out_quint, in_out_sine, in_quad, in_quart, in_quint, in_sine, out_back, out_bounce, out_circ, out_cubic, out_elastic, out_expo, out_quad, out_quart, out_quint, out_sine, ) from terminaltexteffects.utils.graphics import Color, Gradient if TYPE_CHECKING: from collections.abc import Generator from terminaltexteffects.engine.base_effect import BaseEffect INPUT_EMPTY = "" INPUT_SINGLE_CHAR = "a" INPUT_SINGLE_COLUMN = """ a b c d e f""" INPUT_SINGLE_ROW = "abcdefg" INPUT_LARGE = """ 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0 23456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01 3456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012 456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123 56789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234 6789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345 789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456 89abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567 9abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678 """ INPUT_MEDIUM = """ 0123456789abcdefg 123456789abcdefgh 23456789abcdefghi 3456789abcdefghij 456789abcdefghijk""" INPUT_TABS = """Tabs\tTabs\t\tTabs\t\t\tTabs""" CANVAS_TEST_INPUT = """TL!!!!!!!!!!!!!!!!!!!!!TOP*********************TR + <----50----> # + | # + | # L ^ | R E | | I F MID 13 | CENTER MID G T | | H - v | T - | @ - | @ - | @ BL--------------------BOTTOM...................BR""" COLOR_SEQUENCES = ( "\x1b[38;5;231m....\x1b[39m....| \x1b[38;5;95m\x1b[48;5;128mggggggg\x1b[0m \x1b[38;5;180mggggggg " "\x1b[38;5;146m:gggggg; \x1b[38;5;64mggggggg \x1b[38;5;182mggggggg \x1b[38;5;195m:gggggg; " "\x1b[38;5;214mggggggg \x1b[38;5;146m;gggggg \x1b[38;5;174mggggggg \x1b[0m" ) TEST_INPUTS = { "empty": INPUT_EMPTY, "single_char": INPUT_SINGLE_CHAR, "single_column": INPUT_SINGLE_COLUMN, "single_row": INPUT_SINGLE_ROW, "medium": INPUT_MEDIUM, "tabs": INPUT_TABS, "large": INPUT_LARGE, "canvas": CANVAS_TEST_INPUT, "color_sequences": COLOR_SEQUENCES, } EFFECTS = [ effect_beams.Beams, effect_binarypath.BinaryPath, effect_blackhole.Blackhole, effect_bouncyballs.BouncyBalls, effect_bubbles.Bubbles, effect_burn.Burn, effect_colorshift.ColorShift, effect_crumble.Crumble, effect_decrypt.Decrypt, effect_errorcorrect.ErrorCorrect, effect_expand.Expand, effect_fireworks.Fireworks, effect_highlight.Highlight, effect_laseretch.LaserEtch, effect_matrix.Matrix, effect_middleout.MiddleOut, effect_orbittingvolley.OrbittingVolley, effect_overflow.Overflow, effect_pour.Pour, effect_print.Print, effect_rain.Rain, effect_random_sequence.RandomSequence, effect_rings.Rings, effect_scattered.Scattered, effect_slice.Slice, effect_slide.Slide, effect_spotlights.Spotlights, effect_spray.Spray, effect_swarm.Swarm, effect_sweep.Sweep, effect_synthgrid.SynthGrid, effect_unstable.Unstable, effect_vhstape.VHSTape, effect_waves.Waves, effect_wipe.Wipe, ] EASING_FUNCTIONS = [ in_sine, out_sine, in_out_sine, in_quad, out_quad, in_out_quad, in_cubic, out_cubic, in_out_cubic, in_quart, out_quart, in_out_quart, in_quint, out_quint, in_out_quint, in_expo, out_expo, in_out_expo, in_circ, out_circ, in_out_circ, in_elastic, out_elastic, in_out_elastic, in_back, out_back, in_out_back, in_bounce, out_bounce, in_out_bounce, ] ANCHORS = ["sw", "s", "se", "e", "ne", "n", "nw", "w", "c"] @pytest.fixture(autouse=True) def clear_lru_cache() -> Generator[None, Any, None]: """Fixture to clear the LRU caches for geometry functions.""" yield geometry.find_coords_on_circle.cache_clear() # type: ignore[attr-defined] geometry.find_coords_in_circle.cache_clear() # type: ignore[attr-defined] geometry.find_coords_in_rect.cache_clear() # type: ignore[attr-defined] geometry.find_coord_at_distance.cache_clear() # type: ignore[attr-defined] geometry.find_coord_on_bezier_curve.cache_clear() # type: ignore[attr-defined] geometry.find_coord_on_line.cache_clear() # type: ignore[attr-defined] geometry.find_length_of_bezier_curve.cache_clear() # type: ignore[attr-defined] geometry.find_length_of_line.cache_clear() # type: ignore[attr-defined] geometry.find_normalized_distance_from_center.cache_clear() # type: ignore[attr-defined] @pytest.fixture def input_data(request: pytest.FixtureRequest) -> str: """Fixture to provide input data for tests.""" return TEST_INPUTS[request.param] @pytest.fixture(params=EFFECTS) def effect(request: pytest.FixtureRequest) -> BaseEffect: """Fixture to provide effect instances for tests.""" return request.param @pytest.fixture(params=EASING_FUNCTIONS) def easing_function_1(request: pytest.FixtureRequest) -> EasingFunction: """Fixture to provide the first easing function for tests.""" return request.param @pytest.fixture(params=EASING_FUNCTIONS) def easing_function_2(request: pytest.FixtureRequest) -> EasingFunction: """Fixture to provide the second easing function for tests.""" return request.param @pytest.fixture(params=[True, False]) def no_color(request: pytest.FixtureRequest) -> bool: """Fixture to provide a boolean indicating whether to disable color.""" return request.param @pytest.fixture(params=[True, False]) def xterm_colors(request: pytest.FixtureRequest) -> bool: """Fixture to provide a boolean indicating whether to use xterm colors.""" return request.param @pytest.fixture(params=ANCHORS) def canvas_anchor(request: pytest.FixtureRequest) -> str: """Fixture to provide canvas anchor positions for tests.""" return request.param @pytest.fixture(params=ANCHORS) def text_anchor(request: pytest.FixtureRequest) -> str: """Fixture to provide text anchor positions for tests.""" return request.param @pytest.fixture(params=[(60, 30), (25, 8)], ids=["60x30", "25x8"]) def canvas_dimensions(request: pytest.FixtureRequest) -> tuple[int, int]: """Fixture to provide canvas dimensions for tests.""" return request.param @pytest.fixture def terminal_config_with_color_options(xterm_colors: bool, no_color: bool) -> TerminalConfig: # noqa: FBT001 """Fixture to provide terminal configuration with color options.""" terminal_config = TerminalConfig() terminal_config.xterm_colors = xterm_colors terminal_config.no_color = no_color terminal_config.frame_rate = 0 return terminal_config @pytest.fixture def terminal_config_default_no_framerate() -> TerminalConfig: """Fixture to provide terminal configuration with default settings and no frame rate.""" terminal_config = TerminalConfig() terminal_config.frame_rate = 0 return terminal_config @pytest.fixture def terminal_config_with_anchoring( canvas_dimensions: tuple[int, int], canvas_anchor: Literal["sw", "s", "se", "e", "ne", "n", "nw", "w", "c"], text_anchor: Literal["sw", "s", "se", "e", "ne", "n", "nw", "w", "c"], ) -> TerminalConfig: """Fixture to provide terminal configuration with anchoring options.""" terminal_config = TerminalConfig() terminal_config.frame_rate = 0 terminal_config.canvas_width = canvas_dimensions[0] terminal_config.canvas_height = canvas_dimensions[1] terminal_config.anchor_canvas = canvas_anchor terminal_config.anchor_text = text_anchor return terminal_config @pytest.fixture(params=[(Color("000000"), Color("ff00ff"), Color("0ffff0")), (Color("ff0fff"),)]) def gradient_stops(request: pytest.FixtureRequest) -> Color | tuple[Color, ...]: """Fixture to provide gradient stops for tests.""" return request.param @pytest.fixture(params=[1, 4, (1, 3)]) def gradient_steps(request: pytest.FixtureRequest) -> int | tuple[int, ...]: """Fixture to provide gradient steps for tests.""" return request.param @pytest.fixture(params=[1, 4]) def gradient_frames(request: pytest.FixtureRequest) -> int: """Fixture to provide gradient frames for tests.""" return request.param @pytest.fixture( params=[ Gradient.Direction.DIAGONAL, Gradient.Direction.HORIZONTAL, Gradient.Direction.VERTICAL, Gradient.Direction.RADIAL, ], ) def gradient_direction(request: pytest.FixtureRequest) -> Gradient.Direction: """Fixture to provide gradient direction for tests.""" return request.param @pytest.fixture(params=[True, False]) def bool_arg(request: pytest.FixtureRequest) -> bool: """Fixture to provide boolean arguments for tests.""" return request.param terminaltexteffects-release-0.12.1/tests/effects_tests/000077500000000000000000000000001507200677100233015ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/tests/effects_tests/__init__.py000066400000000000000000000001171507200677100254110ustar00rootroot00000000000000"""Marks this directory as a Python package for test discovery and imports.""" terminaltexteffects-release-0.12.1/tests/effects_tests/test_beams.py000066400000000000000000000073031507200677100260040ustar00rootroot00000000000000"""Test the beams effect with various configuration arguments.""" from __future__ import annotations from typing import TYPE_CHECKING import pytest from terminaltexteffects.effects import effect_beams if TYPE_CHECKING: from terminaltexteffects import Color, Gradient from terminaltexteffects.engine.terminal import TerminalConfig @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True, ) def test_beams_effect(input_data: str, terminal_config_default_no_framerate: TerminalConfig) -> None: """Test the beams effect with various input data and default terminal configuration.""" effect = effect_beams.Beams(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_beams_effect_terminal_color_options( input_data: str, terminal_config_with_color_options: TerminalConfig, ) -> None: """Test the beams effect with terminal color options.""" effect = effect_beams.Beams(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_beams_final_gradient( terminal_config_default_no_framerate: TerminalConfig, input_data: str, gradient_direction: Gradient.Direction, gradient_steps: int, gradient_stops: tuple[Color, ...], ) -> None: """Test the final gradient configuration of the beams effect.""" effect = effect_beams.Beams(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.final_gradient_direction = gradient_direction effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_stops = gradient_stops with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("beam_row_symbols", [("a",), ("a", "b", "c")]) @pytest.mark.parametrize("beam_column_symbols", [("a",), ("a", "b", "c")]) @pytest.mark.parametrize("beam_delay", [1, 3]) @pytest.mark.parametrize("beam_row_speed_range", [(1, 3), (2, 4)]) @pytest.mark.parametrize("beam_column_speed_range", [(1, 3), (2, 4)]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_beams_effect_args( input_data: str, terminal_config_default_no_framerate: TerminalConfig, beam_row_symbols: tuple[str, ...], beam_column_symbols: tuple[str, ...], beam_delay: int, beam_row_speed_range: tuple[int, int], beam_column_speed_range: tuple[int, int], gradient_stops: tuple[Color, ...], gradient_steps: int, gradient_frames: int, ) -> None: """Test the beams effect with various configuration arguments.""" effect = effect_beams.Beams(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.beam_row_symbols = beam_row_symbols effect.effect_config.beam_column_symbols = beam_column_symbols effect.effect_config.beam_delay = beam_delay effect.effect_config.beam_row_speed_range = beam_row_speed_range effect.effect_config.beam_column_speed_range = beam_column_speed_range effect.effect_config.beam_gradient_stops = gradient_stops effect.effect_config.beam_gradient_steps = gradient_steps effect.effect_config.beam_gradient_frames = gradient_frames with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_binarypath.py000066400000000000000000000047241507200677100270620ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_binarypath from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_binarypath_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_binarypath.BinaryPath(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_binarypath_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_binarypath.BinaryPath(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_binarypath_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_binarypath.BinaryPath(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("binary_colors", [(Color("ffffff"),), (Color("f0f0f0"), Color("0f0f0f"))]) @pytest.mark.parametrize("movement_speed", [0.5, 1, 4]) @pytest.mark.parametrize("active_binary_groups", [0.0001, 0.5, 1.0]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_binarypath_args( terminal_config_default_no_framerate, input_data, binary_colors, movement_speed, active_binary_groups ) -> None: effect = effect_binarypath.BinaryPath(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.binary_colors = binary_colors effect.effect_config.movement_speed = movement_speed effect.effect_config.active_binary_groups = active_binary_groups with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_blackhole.py000066400000000000000000000044641507200677100266460ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_blackhole from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_blackhole_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_blackhole.Blackhole(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_blackhole_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_blackhole.Blackhole(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_blackhole_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_blackhole.Blackhole(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("blackhole_color", [Color("ffffff"), Color("f0f0f0")]) @pytest.mark.parametrize("star_colors", [(Color("ffffff"),), (Color("f0f0f0"), Color("0f0f0f"))]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_blackhole_args(terminal_config_default_no_framerate, input_data, blackhole_color, star_colors) -> None: effect = effect_blackhole.Blackhole(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.blackhole_color = blackhole_color effect.effect_config.star_colors = star_colors with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_bouncyballs.py000066400000000000000000000052331507200677100272320ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_bouncyballs from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_bouncyballs_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_bouncyballs.BouncyBalls(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_bouncyballs_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_bouncyballs.BouncyBalls(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_bouncyballs_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_bouncyballs.BouncyBalls(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("ball_colors", [(Color("ffffff"),), (Color("f0f0f0"), Color("0f0f0f"))]) @pytest.mark.parametrize("ball_symbols", [("a",), ("a", "b", "c")]) @pytest.mark.parametrize("ball_delay", [0, 10]) @pytest.mark.parametrize("movement_speed", [0.01, 0.5, 2.0]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_bouncyballs_args( terminal_config_default_no_framerate, input_data, ball_colors, ball_symbols, ball_delay, movement_speed, easing_function_1, ) -> None: effect = effect_bouncyballs.BouncyBalls(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.ball_colors = ball_colors effect.effect_config.ball_symbols = ball_symbols effect.effect_config.ball_delay = ball_delay effect.effect_config.movement_speed = movement_speed effect.effect_config.movement_easing = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_bubbles.py000066400000000000000000000055441507200677100263400ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_bubbles from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_bubbles_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_bubbles.Bubbles(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_bubbles_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_bubbles.Bubbles(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_bubbles_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_bubbles.Bubbles(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("rainbow", [True, False]) @pytest.mark.parametrize("bubble_colors", [(Color("ff00ff"),), (Color("0ffff0"), Color("0000ff"))]) @pytest.mark.parametrize("pop_color", [Color("ff00ff"), Color("0ffff0")]) @pytest.mark.parametrize("bubble_speed", [0.1, 4.0]) @pytest.mark.parametrize("bubble_delay", [0, 10]) @pytest.mark.parametrize("pop_condition", ["row", "bottom", "anywhere"]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_bubbles_args( terminal_config_default_no_framerate, input_data, rainbow, bubble_colors, pop_color, bubble_speed, bubble_delay, pop_condition, easing_function_1, ) -> None: effect = effect_bubbles.Bubbles(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.rainbow = rainbow effect.effect_config.bubble_colors = bubble_colors effect.effect_config.pop_color = pop_color effect.effect_config.bubble_speed = bubble_speed effect.effect_config.bubble_delay = bubble_delay effect.effect_config.pop_condition = pop_condition effect.effect_config.movement_easing = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_burn.py000066400000000000000000000043571507200677100256710ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_burn from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_burn_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_burn.Burn(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_burn_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_burn.Burn(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_burn_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_burn.Burn(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("starting_color", [Color("ff00ff"), Color("0ffff0")]) @pytest.mark.parametrize("burn_colors", [(Color("ff00ff"),), (Color("0ffff0"), Color("0000ff"))]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_burn_args(terminal_config_default_no_framerate, input_data, starting_color, burn_colors) -> None: effect = effect_burn.Burn(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.starting_color = starting_color effect.effect_config.burn_colors = burn_colors with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_colorshift.py000066400000000000000000000056101507200677100270700ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_colorshift @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_colorshift_effect_all_inputs(input_data, terminal_config_default_no_framerate) -> None: effect = effect_colorshift.ColorShift(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_colorshift_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_colorshift.ColorShift(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_colorshift_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_colorshift.ColorShift(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.final_gradient_direction = gradient_direction effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_stops = gradient_stops with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("no_loop", [True, False]) @pytest.mark.parametrize("travel", [True, False]) @pytest.mark.parametrize("reverse_travel_direction", [True, False]) @pytest.mark.parametrize("cycles", [1, 3]) @pytest.mark.parametrize("skip_final_gradient", [True, False]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_colorshift_args( input_data, no_loop, travel, reverse_travel_direction, cycles, terminal_config_default_no_framerate, skip_final_gradient, gradient_direction, gradient_stops, gradient_steps, gradient_frames, ) -> None: effect = effect_colorshift.ColorShift(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.gradient_stops = gradient_stops effect.effect_config.gradient_steps = gradient_steps effect.effect_config.gradient_frames = gradient_frames effect.effect_config.no_loop = no_loop effect.effect_config.travel = travel effect.effect_config.travel_direction = gradient_direction effect.effect_config.reverse_travel_direction = reverse_travel_direction effect.effect_config.cycles = cycles effect.effect_config.skip_final_gradient = skip_final_gradient with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_crumble.py000066400000000000000000000036621507200677100263520ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_crumble @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_crumble_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_crumble.Crumble(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_crumble_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_crumble.Crumble(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_crumble_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_crumble.Crumble(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_crumble_args( terminal_config_default_no_framerate, input_data, ) -> None: effect = effect_crumble.Crumble(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_decrypt.py000066400000000000000000000044121507200677100263650ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_decrypt from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_decrypt_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_decrypt.Decrypt(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_decrypt_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_decrypt.Decrypt(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_decrypt_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_decrypt.Decrypt(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("typing_speed", [1, 4]) @pytest.mark.parametrize("ciphertext_colors", [(Color("ff00ff"),), (Color("0ffff0"), Color("0000ff"))]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_decrypt_args(terminal_config_default_no_framerate, input_data, typing_speed, ciphertext_colors) -> None: effect = effect_decrypt.Decrypt(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.typing_speed = typing_speed effect.effect_config.ciphertext_colors = ciphertext_colors with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_errorcorrect.py000066400000000000000000000053011507200677100274240ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_errorcorrect from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_errorcorrect_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_errorcorrect.ErrorCorrect(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_errorcorrect_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_errorcorrect.ErrorCorrect(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_errorcorrect_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_errorcorrect.ErrorCorrect(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("error_pairs", [0.001, 0.5, 1]) @pytest.mark.parametrize("swap_delay", [1, 10]) @pytest.mark.parametrize("error_color", [Color("ff00ff"), Color("0ffff0")]) @pytest.mark.parametrize("correct_color", [Color("ff00ff"), Color("0ffff0")]) @pytest.mark.parametrize("movement_speed", [0.01, 4]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_errorcorrect_args( terminal_config_default_no_framerate, input_data, error_pairs, swap_delay, error_color, correct_color, movement_speed, ) -> None: effect = effect_errorcorrect.ErrorCorrect(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.error_pairs = error_pairs effect.effect_config.swap_delay = swap_delay effect.effect_config.error_color = error_color effect.effect_config.correct_color = correct_color effect.effect_config.movement_speed = movement_speed with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_expand.py000066400000000000000000000041471507200677100261770ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_expand @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_expand_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_expand.Expand(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_expand_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_expand.Expand(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_expand_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_expand.Expand(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("movement_speed", [0.01, 4]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_expand_args(terminal_config_default_no_framerate, input_data, movement_speed, easing_function_1) -> None: effect = effect_expand.Expand(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.movement_speed = movement_speed effect.effect_config.expand_easing = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_fireworks.py000066400000000000000000000055451507200677100267360ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_fireworks from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_fireworks_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_fireworks.Fireworks(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_fireworks_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_fireworks.Fireworks(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_fireworks_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_fireworks.Fireworks(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("explode_anywhere", [True, False]) @pytest.mark.parametrize("firework_colors", [(Color("ff00ff"),), (Color("0ffff0"), Color("0000ff"))]) @pytest.mark.parametrize("firework_symbol", ["+", "x"]) @pytest.mark.parametrize("firework_volume", [0.001, 0.2, 1]) @pytest.mark.parametrize("launch_delay", [0, 10]) @pytest.mark.parametrize("explode_distance", [0.001, 0.5, 1]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_fireworks_args( terminal_config_default_no_framerate, input_data, explode_anywhere, firework_colors, firework_symbol, firework_volume, launch_delay, explode_distance, ) -> None: effect = effect_fireworks.Fireworks(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.explode_anywhere = explode_anywhere effect.effect_config.firework_colors = firework_colors effect.effect_config.firework_symbol = firework_symbol effect.effect_config.firework_volume = firework_volume effect.effect_config.launch_delay = launch_delay effect.effect_config.explode_distance = explode_distance with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_highlight.py000066400000000000000000000053511507200677100266650ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_highlight @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True, ) def test_highlight_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_highlight.Highlight(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_highlight_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_highlight.Highlight(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_highlight_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops, gradient_frames, ) -> None: effect = effect_highlight.Highlight(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize( "highlight_direction", [ "column_left_to_right", "row_top_to_bottom", "row_bottom_to_top", "diagonal_top_left_to_bottom_right", "diagonal_bottom_left_to_top_right", "diagonal_top_right_to_bottom_left", "diagonal_bottom_right_to_top_left", "outside_to_center", "center_to_outside", ], ) @pytest.mark.parametrize("highlight_width", [1, 20]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) @pytest.mark.parametrize("highlight_brightness", [0.5, 2]) def test_highlight_args( terminal_config_default_no_framerate, input_data, highlight_direction, highlight_brightness, highlight_width, ) -> None: effect = effect_highlight.Highlight(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.highlight_direction = highlight_direction effect.effect_config.highlight_brightness = highlight_brightness effect.effect_config.highlight_width = highlight_width with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_laseretch.py000066400000000000000000000052601507200677100266670ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_laseretch @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True, ) def test_laseretch_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_laseretch.LaserEtch(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_laseretch_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_laseretch.LaserEtch(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_laseretch_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops, gradient_frames, ) -> None: effect = effect_laseretch.LaserEtch(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize( "etch_direction", [ "column_left_to_right", "row_top_to_bottom", "row_bottom_to_top", "diagonal_top_left_to_bottom_right", "diagonal_bottom_left_to_top_right", "diagonal_top_right_to_bottom_left", "diagonal_bottom_right_to_top_left", "outside_to_center", "center_to_outside", ], ) @pytest.mark.parametrize("etch_speed", [1, 20]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) @pytest.mark.parametrize("etch_delay", [0, 5]) def test_laseretch_args( terminal_config_default_no_framerate, input_data, etch_speed, etch_delay, etch_direction, ) -> None: effect = effect_laseretch.LaserEtch(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.etch_direction = etch_direction effect.effect_config.etch_speed = etch_speed effect.effect_config.etch_delay = etch_delay with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_matrix.py000066400000000000000000000061671507200677100262300ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_matrix from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_matrix_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_matrix.Matrix(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.rain_time = 1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_matrix_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_matrix.Matrix(input_data) effect.terminal_config = terminal_config_with_color_options effect.effect_config.rain_time = 1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_matrix_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_matrix.Matrix(input_data) effect.effect_config.rain_time = 1 effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("highlight_color", [Color("ff00ff"), Color("0ffff0")]) @pytest.mark.parametrize("rain_color_gradient", [(Color("ff0fff"),), (Color("ff0fff"), Color("ff0fff"))]) @pytest.mark.parametrize("rain_symbols", [("a",), ("a", "b")]) @pytest.mark.parametrize("rain_fall_delay_range", [(1, 2), (2, 3)]) @pytest.mark.parametrize("rain_column_delay_range", [(1, 2), (2, 3)]) @pytest.mark.parametrize("rain_time", [1, 2]) @pytest.mark.parametrize("resolve_delay", [1, 5]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_matrix_args( terminal_config_default_no_framerate, input_data, highlight_color, rain_color_gradient, rain_symbols, rain_fall_delay_range, rain_column_delay_range, rain_time, resolve_delay, ) -> None: effect = effect_matrix.Matrix(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.highlight_color = highlight_color effect.effect_config.rain_color_gradient = rain_color_gradient effect.effect_config.rain_symbols = rain_symbols effect.effect_config.rain_fall_delay_range = rain_fall_delay_range effect.effect_config.rain_column_delay_range = rain_column_delay_range effect.effect_config.rain_time = rain_time effect.effect_config.resolve_delay = resolve_delay with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_middleout.py000066400000000000000000000062561507200677100267110ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_middleout from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_middleout_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_middleout.MiddleOut(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_middleout_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_middleout.MiddleOut(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_middleout_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_middleout.MiddleOut(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("starting_color", [Color("000000"), Color("ff00ff")]) @pytest.mark.parametrize("expand_direction", ["horizontal", "vertical"]) @pytest.mark.parametrize("center_movement_speed", [0.001, 2.0]) @pytest.mark.parametrize("full_movement_speed", [0.001, 2.0]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_middleout_args( terminal_config_default_no_framerate, input_data, starting_color, expand_direction, center_movement_speed, full_movement_speed, ) -> None: effect = effect_middleout.MiddleOut(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.starting_color = starting_color effect.effect_config.expand_direction = expand_direction effect.effect_config.center_movement_speed = center_movement_speed effect.effect_config.full_movement_speed = full_movement_speed with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_middleout_easing( terminal_config_default_no_framerate, input_data, easing_function_1, easing_function_2, ) -> None: effect = effect_middleout.MiddleOut(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.center_easing = easing_function_1 effect.effect_config.full_easing = easing_function_2 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_orbittingvolley.py000066400000000000000000000104501507200677100301460ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_orbittingvolley @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_orbittingvolley_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_orbittingvolley.OrbittingVolley(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_orbittingvolley_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_orbittingvolley.OrbittingVolley(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_orbittingvolley_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_orbittingvolley.OrbittingVolley(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("top_launcher_symbol", ["a", "b"]) @pytest.mark.parametrize("right_launcher_symbol", ["a", "b"]) @pytest.mark.parametrize("bottom_launcher_symbol", ["a", "b"]) @pytest.mark.parametrize("left_launcher_symbol", ["a", "b"]) @pytest.mark.parametrize("launcher_movement_speed", [0.1, 2.0]) @pytest.mark.parametrize("character_movement_speed", [0.1, 2.0]) @pytest.mark.parametrize("volley_size", [0.0001, 0.5, 1.0]) @pytest.mark.parametrize("launch_delay", [1, 5]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_orbittingvolley_args( terminal_config_default_no_framerate, input_data, top_launcher_symbol, right_launcher_symbol, bottom_launcher_symbol, left_launcher_symbol, launcher_movement_speed, character_movement_speed, volley_size, launch_delay, ) -> None: effect = effect_orbittingvolley.OrbittingVolley(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.top_launcher_symbol = top_launcher_symbol effect.effect_config.right_launcher_symbol = right_launcher_symbol effect.effect_config.bottom_launcher_symbol = bottom_launcher_symbol effect.effect_config.left_launcher_symbol = left_launcher_symbol effect.effect_config.launcher_movement_speed = launcher_movement_speed effect.effect_config.character_movement_speed = character_movement_speed effect.effect_config.volley_size = volley_size effect.effect_config.launch_delay = launch_delay with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("launcher_movement_speed", [0.1, 2.0]) @pytest.mark.parametrize("character_movement_speed", [0.1, 2.0]) @pytest.mark.parametrize("volley_size", [0.0001, 0.5, 1.0]) @pytest.mark.parametrize("launch_delay", [1, 5]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_orbittingvolley_easing( terminal_config_default_no_framerate, input_data, launcher_movement_speed, character_movement_speed, volley_size, launch_delay, easing_function_1, ) -> None: effect = effect_orbittingvolley.OrbittingVolley(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.launcher_movement_speed = launcher_movement_speed effect.effect_config.character_movement_speed = character_movement_speed effect.effect_config.volley_size = volley_size effect.effect_config.launch_delay = launch_delay effect.effect_config.character_easing = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_overflow.py000066400000000000000000000047611507200677100265650ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_overflow from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_overflow_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_overflow.Overflow(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_overflow_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_overflow.Overflow(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_overflow_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_overflow.Overflow(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("overflow_gradient_stops", [(Color("000000"),), (Color("ff00ff"), Color("0ffff0"))]) @pytest.mark.parametrize("overflow_cycles_range", [(1, 5), (5, 10)]) @pytest.mark.parametrize("overflow_speed", [1, 5]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_overflow_args( terminal_config_default_no_framerate, input_data, overflow_gradient_stops, overflow_cycles_range, overflow_speed, ) -> None: effect = effect_overflow.Overflow(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.overflow_gradient_stops = overflow_gradient_stops effect.effect_config.overflow_cycles_range = overflow_cycles_range effect.effect_config.overflow_speed = overflow_speed with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_pour.py000066400000000000000000000050741507200677100257050ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_pour from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_pour_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_pour.Pour(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_pour_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_pour.Pour(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_pour_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_pour.Pour(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("pour_direction", ["up", "down", "left", "right"]) @pytest.mark.parametrize("pour_speed", [1, 5]) @pytest.mark.parametrize("movement_speed", [0.1, 2]) @pytest.mark.parametrize("gap", [0, 10]) @pytest.mark.parametrize("starting_color", [Color("ffffff"), Color("000000")]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_pour_args( terminal_config_default_no_framerate, input_data, pour_direction, pour_speed, movement_speed, gap, starting_color, ) -> None: effect = effect_pour.Pour(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.pour_direction = pour_direction effect.effect_config.pour_speed = pour_speed effect.effect_config.movement_speed = movement_speed effect.effect_config.gap = gap effect.effect_config.starting_color = starting_color with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_print.py000066400000000000000000000043671507200677100260600ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_print @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_print_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_print.Print(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_print_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_print.Print(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_print_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_print.Print(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("print_head_return_speed", [0.1, 2]) @pytest.mark.parametrize("print_speed", [1, 5]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_print_args( terminal_config_default_no_framerate, input_data, print_head_return_speed, print_speed, easing_function_1 ) -> None: effect = effect_print.Print(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.print_head_return_speed = print_head_return_speed effect.effect_config.print_speed = print_speed effect.effect_config.print_head_easing = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_rain.py000066400000000000000000000046671507200677100256600ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_rain from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_rain_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_rain.Rain(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_rain_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_rain.Rain(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_rain_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_rain.Rain(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("rain_colors", [(Color("000000"),), (Color("ff00ff"), Color("0ffff0"))]) @pytest.mark.parametrize("movement_speed", [(0.1, 1), (2, 4)]) @pytest.mark.parametrize("rain_symbols", [("a",), ("b", "c")]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_rain_args( terminal_config_default_no_framerate, input_data, rain_colors, movement_speed, rain_symbols, easing_function_1 ) -> None: effect = effect_rain.Rain(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.rain_colors = rain_colors effect.effect_config.movement_speed = movement_speed effect.effect_config.rain_symbols = rain_symbols effect.effect_config.movement_easing = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_randomsequence.py000066400000000000000000000045111507200677100277240ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_random_sequence from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_randomsequence_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_random_sequence.RandomSequence(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_randomsequence_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_random_sequence.RandomSequence(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_randomsequence_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_random_sequence.RandomSequence(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("starting_color", [Color("000000"), Color("ff00ff")]) @pytest.mark.parametrize("speed", [0.0001, 0.5, 1]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_randomsequence_args( terminal_config_default_no_framerate, input_data, starting_color, speed, ) -> None: effect = effect_random_sequence.RandomSequence(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.starting_color = starting_color effect.effect_config.speed = speed with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_rings.py000066400000000000000000000054121507200677100260360ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_rings from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_rings_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_rings.Rings(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_rings_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_rings.Rings(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_rings_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_rings.Rings(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("ring_colors", [(Color("ffffff"),), (Color("f0f0f0"), Color("00ff00"))]) @pytest.mark.parametrize("ring_gap", [0.0001, 0.5, 2]) @pytest.mark.parametrize("spin_duration", [0, 10]) @pytest.mark.parametrize("spin_speed", [(0.01, 2.0), (1.0, 3.0)]) @pytest.mark.parametrize("disperse_duration", [1, 10]) @pytest.mark.parametrize("spin_disperse_cycles", [1, 3]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_rings_args( terminal_config_default_no_framerate, input_data, ring_colors, ring_gap, spin_duration, spin_speed, disperse_duration, spin_disperse_cycles, ) -> None: effect = effect_rings.Rings(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.ring_colors = ring_colors effect.effect_config.ring_gap = ring_gap effect.effect_config.spin_duration = spin_duration effect.effect_config.spin_speed = spin_speed effect.effect_config.disperse_duration = disperse_duration effect.effect_config.spin_disperse_cycles = spin_disperse_cycles with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_scattered.py000066400000000000000000000042201507200677100266660ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_scattered @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_scattered_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_scattered.Scattered(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_scattered_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_scattered.Scattered(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_scattered_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_scattered.Scattered(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("movement_speed", [0.01, 1]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_scattered_args(terminal_config_default_no_framerate, input_data, movement_speed, easing_function_1) -> None: effect = effect_scattered.Scattered(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.movement_speed = movement_speed effect.effect_config.movement_easing = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_slice.py000066400000000000000000000044041507200677100260130ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_slice @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_slice_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_slice.Slice(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_slice_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_slice.Slice(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_slice_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_slice.Slice(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("slice_direction", ["vertical", "horizontal", "diagonal"]) @pytest.mark.parametrize("movement_speed", [0.01, 2.0]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_slice_args( terminal_config_default_no_framerate, input_data, slice_direction, movement_speed, easing_function_1 ) -> None: effect = effect_slice.Slice(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.slice_direction = slice_direction effect.effect_config.movement_speed = movement_speed effect.effect_config.movement_easing = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_slide.py000066400000000000000000000052371507200677100260210ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_slide @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_slide_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_slide.Slide(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_slide_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_slide.Slide(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_slide_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops, gradient_frames, ) -> None: effect = effect_slide.Slide(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.effect_config.final_gradient_frames = gradient_frames effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("movement_speed", [0.01, 1]) @pytest.mark.parametrize("grouping", ["row", "column", "diagonal"]) @pytest.mark.parametrize("gap", [0, 3]) @pytest.mark.parametrize("reverse_direction", [True, False]) @pytest.mark.parametrize("merge", [True, False]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_slide_args( terminal_config_default_no_framerate, input_data, movement_speed, grouping, gap, reverse_direction, merge, easing_function_1, ) -> None: effect = effect_slide.Slide(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.movement_speed = movement_speed effect.effect_config.grouping = grouping effect.effect_config.gap = gap effect.effect_config.reverse_direction = reverse_direction effect.effect_config.merge = merge effect.effect_config.movement_easing = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_spotlights.py000066400000000000000000000052041507200677100271130ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_spotlights @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_spotlights_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_spotlights.Spotlights(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_spotlights_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_spotlights.Spotlights(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_spotlights_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_spotlights.Spotlights(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("beam_width_ratio", [0.01, 3]) @pytest.mark.parametrize("beam_falloff", [0, 3.0]) @pytest.mark.parametrize("search_duration", [1, 5]) @pytest.mark.parametrize("search_speed_range", [(0.01, 1), (2, 4)]) @pytest.mark.parametrize("spotlight_count", [1, 10]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_spotlights_args( terminal_config_default_no_framerate, input_data, beam_width_ratio, beam_falloff, search_duration, search_speed_range, spotlight_count, ) -> None: effect = effect_spotlights.Spotlights(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.beam_width_ratio = beam_width_ratio effect.effect_config.beam_falloff = beam_falloff effect.effect_config.search_duration = search_duration effect.effect_config.search_speed_range = search_speed_range effect.effect_config.spotlight_count = spotlight_count with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_spray.py000066400000000000000000000046271507200677100260610ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_spray @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_spray_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_spray.Spray(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_spray_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_spray.Spray(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_spray_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_spray.Spray(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("spray_position", ["n", "ne", "e", "se", "s", "sw", "w", "nw", "center"]) @pytest.mark.parametrize("spray_volume", [0.0001, 1]) @pytest.mark.parametrize("movement_speed", [(0.01, 1), (2, 4)]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_spray_args( terminal_config_default_no_framerate, input_data, spray_position, spray_volume, movement_speed, easing_function_1 ) -> None: effect = effect_spray.Spray(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.spray_position = spray_position effect.effect_config.spray_volume = spray_volume effect.effect_config.movement_speed_range = movement_speed effect.effect_config.movement_easing = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_swarm.py000066400000000000000000000052631507200677100260510ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_swarm from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_swarm_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_swarm.Swarm(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_swarm_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_swarm.Swarm(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_swarm_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_swarm.Swarm(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("base_color", [(Color("ffffff"),), (Color("f0f0f0"), Color("00ff00"))]) @pytest.mark.parametrize("flash_color", [Color("ff0000"), Color("0000ff")]) @pytest.mark.parametrize("swarm_size", [0.0001, 1]) @pytest.mark.parametrize("swarm_coordination", [0.0001, 1]) @pytest.mark.parametrize("swarm_area_count_range", [(1, 2), (3, 4)]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_swarm_args( terminal_config_default_no_framerate, input_data, base_color, flash_color, swarm_size, swarm_coordination, swarm_area_count_range, ) -> None: effect = effect_swarm.Swarm(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.base_color = base_color effect.effect_config.flash_color = flash_color effect.effect_config.swarm_size = swarm_size effect.effect_config.swarm_coordination = swarm_coordination effect.effect_config.swarm_area_count_range = swarm_area_count_range with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_sweep.py000066400000000000000000000053641507200677100260450ustar00rootroot00000000000000"""Tests for the sweep effect in the terminaltexteffects package.""" from __future__ import annotations from typing import TYPE_CHECKING import pytest from terminaltexteffects.effects import effect_sweep if TYPE_CHECKING: import terminaltexteffects as tte from terminaltexteffects.engine.terminal import TerminalConfig @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True, ) def test_sweep_effect(input_data: str, terminal_config_default_no_framerate: TerminalConfig) -> None: """Test the sweep effect with default terminal configuration.""" effect = effect_sweep.Sweep(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_sweep_effect_terminal_color_options( input_data: str, terminal_config_with_color_options: TerminalConfig, ) -> None: """Test the sweep effect with terminal color options.""" effect = effect_sweep.Sweep(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_sweep_final_gradient( terminal_config_default_no_framerate: TerminalConfig, input_data: str, gradient_direction: tte.Gradient.Direction, gradient_steps: tuple[int, ...] | int, gradient_stops: tuple[tte.Color, ...], ) -> None: """Test the sweep effect with final gradient configuration.""" effect = effect_sweep.Sweep(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) @pytest.mark.parametrize("sweep_symbols", [("0", "1"), (" "), ("a", "b", "c")]) def test_sweep_args( terminal_config_default_no_framerate: TerminalConfig, input_data: str, sweep_symbols: tuple[str, ...], ) -> None: """Test the sweep effect with different sweep symbols.""" effect = effect_sweep.Sweep(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.sweep_symbols = sweep_symbols with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_synthgrid.py000066400000000000000000000073451507200677100267360ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_synthgrid from terminaltexteffects.utils.graphics import Color, Gradient @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_synthgrid_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_synthgrid.SynthGrid(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_synthgrid_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_synthgrid.SynthGrid(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize( "grid_gradient_stops", [(Color("000000"), Color("ff00ff"), Color("0ffff0")), (Color("ff0fff"),)] ) @pytest.mark.parametrize("grid_gradient_steps", [1, 4, (1, 3)]) @pytest.mark.parametrize( "grid_gradient_direction", [ Gradient.Direction.DIAGONAL, Gradient.Direction.HORIZONTAL, Gradient.Direction.VERTICAL, Gradient.Direction.RADIAL, ], ) @pytest.mark.parametrize( "text_gradient_stops", [(Color("000000"), Color("ff00ff"), Color("0ffff0")), (Color("ff0fff"),)] ) @pytest.mark.parametrize("text_gradient_steps", [1, 4, (1, 3)]) @pytest.mark.parametrize( "text_gradient_direction", [ Gradient.Direction.DIAGONAL, Gradient.Direction.HORIZONTAL, Gradient.Direction.VERTICAL, Gradient.Direction.RADIAL, ], ) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_synthgrid_gradients( terminal_config_default_no_framerate, input_data, grid_gradient_stops, grid_gradient_steps, grid_gradient_direction, text_gradient_stops, text_gradient_steps, text_gradient_direction, ) -> None: effect = effect_synthgrid.SynthGrid(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.grid_gradient_stops = grid_gradient_stops effect.effect_config.grid_gradient_steps = grid_gradient_steps effect.effect_config.grid_gradient_direction = grid_gradient_direction effect.effect_config.text_gradient_stops = text_gradient_stops effect.effect_config.text_gradient_steps = text_gradient_steps effect.effect_config.text_gradient_direction = text_gradient_direction with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("grid_row_symbol", ["a", "b"]) @pytest.mark.parametrize("grid_column_symbol", ["c", "d"]) @pytest.mark.parametrize("text_generation_symbols", [("e",), ("f", "g"), "h"]) @pytest.mark.parametrize("max_active_blocks", [0.001, 1]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_synthgrid_args( terminal_config_default_no_framerate, input_data, grid_row_symbol, grid_column_symbol, text_generation_symbols, max_active_blocks, ) -> None: effect = effect_synthgrid.SynthGrid(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.grid_row_symbol = grid_row_symbol effect.effect_config.grid_column_symbol = grid_column_symbol effect.effect_config.text_generation_symbols = text_generation_symbols effect.effect_config.max_active_blocks = max_active_blocks with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_unstable.py000066400000000000000000000065461507200677100265420ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_unstable from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_unstable_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_unstable.Unstable(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_unstable_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_unstable.Unstable(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_unstable_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_unstable.Unstable(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("unstable_color", [Color("ff00ff"), Color("0ffff0")]) @pytest.mark.parametrize("explosion_speed", [0.001, 2]) @pytest.mark.parametrize("reassembly_speed", [0.001, 2]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_unstable_args( terminal_config_default_no_framerate, input_data, unstable_color, explosion_speed, reassembly_speed, ) -> None: effect = effect_unstable.Unstable(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.unstable_color = unstable_color effect.effect_config.explosion_speed = explosion_speed effect.effect_config.reassembly_speed = reassembly_speed with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_unstable_explosion_ease(terminal_config_default_no_framerate, input_data, easing_function_1) -> None: effect = effect_unstable.Unstable(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.explosion_ease = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_unstable_reassembly_ease(terminal_config_default_no_framerate, input_data, easing_function_1) -> None: effect = effect_unstable.Unstable(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.reassembly_ease = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_vhstape.py000066400000000000000000000056641507200677100263770ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_vhstape from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_vhstape_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_vhstape.VHSTape(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_vhstape_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_vhstape.VHSTape(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_vhstape_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_vhstape.VHSTape(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("glitch_line_colors", [(Color("ff00ff"), Color("0ffff0")), (Color("ff0fff"),)]) @pytest.mark.parametrize("glitch_wave_colors", [(Color("ff00ff"), Color("0ffff0")), (Color("ff0fff"),)]) @pytest.mark.parametrize("noise_colors", [(Color("ff00ff"), Color("0ffff0")), (Color("ff0fff"),)]) @pytest.mark.parametrize("glitch_line_chance", [0, 0.5, 1]) @pytest.mark.parametrize("noise_chance", [0, 0.5, 1]) @pytest.mark.parametrize("total_glitch_time", [1, 20]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_vhstape_args( terminal_config_default_no_framerate, input_data, glitch_line_colors, glitch_wave_colors, noise_colors, glitch_line_chance, noise_chance, total_glitch_time, ) -> None: effect = effect_vhstape.VHSTape(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.glitch_line_colors = glitch_line_colors effect.effect_config.glitch_wave_colors = glitch_wave_colors effect.effect_config.noise_colors = noise_colors effect.effect_config.glitch_line_chance = glitch_line_chance effect.effect_config.noise_chance = noise_chance effect.effect_config.total_glitch_time = total_glitch_time with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_waves.py000066400000000000000000000066631507200677100260520ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_waves from terminaltexteffects.utils.graphics import Color @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_waves_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_waves.Waves(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_waves_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_waves.Waves(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_waves_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops ) -> None: effect = effect_waves.Waves(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("wave_symbols", [("a", "b"), ("c")]) @pytest.mark.parametrize( "wave_gradient_stops", [(Color("000000"), Color("ff00ff"), Color("0ffff0")), (Color("ff0fff"),)] ) @pytest.mark.parametrize("wave_gradient_steps", [1, 4, (1, 3)]) @pytest.mark.parametrize("wave_count", [1, 4]) @pytest.mark.parametrize("wave_length", [1, 3]) @pytest.mark.parametrize( "wave_direction", [ "column_left_to_right", "column_right_to_left", "row_top_to_bottom", "row_bottom_to_top", "center_to_outside", "outside_to_center", ], ) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_waves_args( terminal_config_default_no_framerate, input_data, wave_symbols, wave_gradient_stops, wave_gradient_steps, wave_count, wave_length, wave_direction, ) -> None: effect = effect_waves.Waves(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.wave_symbols = wave_symbols effect.effect_config.wave_gradient_stops = wave_gradient_stops effect.effect_config.wave_gradient_steps = wave_gradient_steps effect.effect_config.wave_count = wave_count effect.effect_config.wave_length = wave_length effect.effect_config.wave_direction = wave_direction with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_waves_effect_easing(input_data, terminal_config_default_no_framerate, easing_function_1) -> None: effect = effect_waves.Waves(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.wave_easing = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/effects_tests/test_wipe.py000066400000000000000000000061741507200677100256660ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_wipe @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs"], indirect=True ) def test_wipe_effect(input_data, terminal_config_default_no_framerate) -> None: effect = effect_wipe.Wipe(input_data) effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_wipe_effect_terminal_color_options(input_data, terminal_config_with_color_options) -> None: effect = effect_wipe.Wipe(input_data) effect.terminal_config = terminal_config_with_color_options with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("input_data", ["medium"], indirect=True) def test_wipe_final_gradient( terminal_config_default_no_framerate, input_data, gradient_direction, gradient_steps, gradient_stops, gradient_frames, ) -> None: effect = effect_wipe.Wipe(input_data) effect.effect_config.final_gradient_stops = gradient_stops effect.effect_config.final_gradient_steps = gradient_steps effect.effect_config.final_gradient_direction = gradient_direction effect.effect_config.final_gradient_frames = gradient_frames effect.terminal_config = terminal_config_default_no_framerate effect.effect_config with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize( "wipe_direction", [ "column_left_to_right", "row_top_to_bottom", "row_bottom_to_top", "diagonal_top_left_to_bottom_right", "diagonal_bottom_left_to_top_right", "diagonal_top_right_to_bottom_left", "diagonal_bottom_right_to_top_left", "outside_to_center", "center_to_outside", ], ) @pytest.mark.parametrize("wipe_delay", [0, 5]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_wipe_args(terminal_config_default_no_framerate, input_data, wipe_direction, wipe_delay) -> None: effect = effect_wipe.Wipe(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.wipe_direction = wipe_direction effect.effect_config.wipe_delay = wipe_delay with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.parametrize("wipe_ease_stepsize", [0.01, 0.1, 1]) @pytest.mark.parametrize("input_data", ["single_char", "medium"], indirect=True) def test_wipe_ease(terminal_config_default_no_framerate, input_data, wipe_ease_stepsize, easing_function_1) -> None: effect = effect_wipe.Wipe(input_data) effect.terminal_config = terminal_config_default_no_framerate effect.effect_config.wipe_ease_stepsize = wipe_ease_stepsize effect.effect_config.wipe_ease = easing_function_1 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/engine_tests/000077500000000000000000000000001507200677100231275ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/tests/engine_tests/__init__.py000066400000000000000000000001171507200677100252370ustar00rootroot00000000000000"""Marks this directory as a Python package for test discovery and imports.""" terminaltexteffects-release-0.12.1/tests/engine_tests/test_animation.py000066400000000000000000000564441507200677100265340ustar00rootroot00000000000000"""Unit tests for the animation functionality within the terminaltexteffects package.""" import pytest from terminaltexteffects.engine.animation import CharacterVisual, Frame, Scene from terminaltexteffects.engine.base_character import EffectCharacter from terminaltexteffects.utils import easing from terminaltexteffects.utils.exceptions import ( ActivateEmptySceneError, ApplyGradientToSymbolsEmptyGradientsError, ApplyGradientToSymbolsInvalidSymbolError, ApplyGradientToSymbolsNoGradientsError, FrameDurationError, SceneNotFoundError, ) from terminaltexteffects.utils.geometry import Coord from terminaltexteffects.utils.graphics import Color, ColorPair, Gradient pytestmark = [pytest.mark.engine, pytest.mark.animation, pytest.mark.smoke] @pytest.fixture def character_visual_default() -> CharacterVisual: """Return a default CharacterVisual instance with the symbol set to "a". Returns: CharacterVisual: A new instance of CharacterVisual with the default symbol "a". """ return CharacterVisual( symbol="a", ) @pytest.fixture def character_visual_all_modes_enabled() -> CharacterVisual: """Return a CharacterVisual instance with all modes enabled. Returns: CharacterVisual: A new instance of CharacterVisual with all attributes set. """ return CharacterVisual( symbol="a", bold=True, dim=True, italic=True, underline=True, blink=True, reverse=True, hidden=True, strike=True, colors=ColorPair(fg="ffffff", bg="ffffff"), _fg_color_code="ffffff", _bg_color_code="ffffff", ) @pytest.fixture def character() -> EffectCharacter: """Return a default EffectCharacter instance.""" return EffectCharacter(0, "a", 0, 0) def test_character_visual_init(character_visual_all_modes_enabled: CharacterVisual) -> None: """Test that the formatted_symbol of character_visual_all_modes_enabled is correctly initialized.""" assert ( character_visual_all_modes_enabled.formatted_symbol == "\x1b[1m\x1b[3m\x1b[4m\x1b[5m\x1b[7m\x1b[8m\x1b[9m\x1b[38;2;255;255;255m\x1b[48;2;255;255;255ma\x1b[0m" ) def test_character_visual_init_default(character_visual_default: CharacterVisual) -> None: """Test that the default formatted symbol is 'a'.""" assert character_visual_default.formatted_symbol == "a" def test_frame_init(character_visual_default: CharacterVisual) -> None: """Test that the Frame instance is correctly initialized.""" frame = Frame(character_visual=character_visual_default, duration=5) assert frame.character_visual == character_visual_default assert frame.duration == 5 assert frame.ticks_elapsed == 0 def test_scene_init() -> None: """Test that the Scene instance is correctly initialized.""" scene = Scene(scene_id="test_scene", is_looping=True, sync=Scene.SyncMetric.STEP, ease=easing.in_sine) assert scene.scene_id == "test_scene" assert scene.is_looping is True assert scene.sync == Scene.SyncMetric.STEP assert scene.ease == easing.in_sine def test_scene_add_frame() -> None: """Test that a frame can be added to the Scene instance.""" scene = Scene(scene_id="test_scene") scene.add_frame( symbol="a", duration=5, colors=ColorPair(fg="ffffff", bg="ffffff"), bold=True, italic=True, blink=True, hidden=True, ) assert len(scene.frames) == 1 frame = scene.frames[0] assert ( frame.character_visual.formatted_symbol == "\x1b[1m\x1b[3m\x1b[5m\x1b[8m\x1b[38;2;255;255;255m\x1b[48;2;255;255;255ma\x1b[0m" ) assert frame.duration == 5 assert frame.character_visual.colors == ColorPair(fg="ffffff", bg="ffffff") assert frame.character_visual.bold is True def test_scene_add_frame_invalid_duration() -> None: """Test that a FrameDurationError is raised when a frame with a duration of 0 is added to the scene.""" scene = Scene(scene_id="test_scene") with pytest.raises(FrameDurationError): scene.add_frame(symbol="a", duration=0, colors=ColorPair(fg="ffffff", bg="ffffff")) def test_scene_apply_gradient_to_symbols_equal_colors_and_symbols() -> None: """Test symbols are correctly assigned colors from a gradient when the colors and symbols are equal in length.""" scene = Scene(scene_id="test_scene") gradient = Gradient(Color("000000"), Color("ffffff"), steps=2) symbols = ["a", "b", "c"] scene.apply_gradient_to_symbols(symbols, duration=1, fg_gradient=gradient) assert len(scene.frames) == 3 for i, frame in enumerate(scene.frames): assert frame.duration == 1 assert frame.character_visual._fg_color_code == gradient.spectrum[i].rgb_color def test_scene_apply_gradient_to_symbols_unequal_colors_and_symbols() -> None: """Test that all colors and symbols are represented when the gradient and symbols length are unequal. Verify the gradient is represented in the scene frames and the symbols are progressed such that the first and final symbols align to the first and final colors. """ scene = Scene(scene_id="test_scene") gradient = Gradient(Color("000000"), Color("ffffff"), steps=4) symbols = ["q", "z"] scene.apply_gradient_to_symbols(symbols, duration=1, fg_gradient=gradient) assert len(scene.frames) == 5 assert scene.frames[0].character_visual._fg_color_code == gradient.spectrum[0].rgb_color assert "q" in scene.frames[0].character_visual.symbol assert scene.frames[-1].character_visual._fg_color_code == gradient.spectrum[-1].rgb_color assert "z" in scene.frames[-1].character_visual.symbol def test_animation_init(character: EffectCharacter) -> None: """Test that the EffectCharacter instance is correctly initialized.""" assert character.animation.character == character assert character.animation.scenes == {} assert character.animation.active_scene is None assert character.animation.use_xterm_colors is False assert character.animation.no_color is False assert character.animation.xterm_color_map == {} assert character.animation.active_scene_current_step == 0 def test_animation_new_scene(character: EffectCharacter) -> None: """Test that a new scene can be created.""" animation = character.animation scene = animation.new_scene(scene_id="test_scene", is_looping=True) assert isinstance(scene, Scene) assert scene.scene_id == "test_scene" assert scene.is_looping is True assert "test_scene" in animation.scenes def test_animation_new_scene_without_id(character: EffectCharacter) -> None: """Test that a new scene can be created without a specified ID.""" animation = character.animation scene = animation.new_scene() assert isinstance(scene, Scene) assert scene.scene_id == "0" assert "0" in animation.scenes def test_animation_new_scene_id_generation_deleted_scene(character: EffectCharacter) -> None: """Test that a new scene ID is generated when the previous scene ID has been deleted.""" for _ in range(4): character.animation.new_scene() character.animation.scenes.pop("2") character.animation.new_scene() def test_animation_query_scene(character: EffectCharacter) -> None: """Test that a scene can be queried from the animation.""" animation = character.animation scene = animation.new_scene(scene_id="test_scene", is_looping=True) assert animation.query_scene("test_scene") is scene def test_animation_query_nonexistent_scene(character: EffectCharacter) -> None: """Test that querying a non-existent scene on the EffectCharacter's animation raises a SceneNotFoundError.""" animation = character.animation with pytest.raises(SceneNotFoundError): animation.query_scene("nonexistent_scene") def test_animation_looping_active_scene_is_complete(character: EffectCharacter) -> None: """Test that the looping active scene is complete after all frames have been processed.""" animation = character.animation scene = animation.new_scene(scene_id="test_scene", is_looping=True) scene.add_frame(symbol="a", duration=2) animation.activate_scene(scene) assert animation.active_scene_is_complete() is True def test_animation_non_looping_active_scene_is_complete(character: EffectCharacter) -> None: """Test that the non-looping active scene is complete after processing all frames.""" animation = character.animation scene = animation.new_scene(scene_id="test_scene") scene.add_frame(symbol="a", duration=1) animation.activate_scene(scene) assert animation.active_scene_is_complete() is False animation.step_animation() assert animation.active_scene_is_complete() is True def test_animation_get_color_code_no_color(character: EffectCharacter) -> None: """Test that the color code is None when no_color is enabled.""" character.animation.no_color = True assert character.animation._get_color_code(Color("ffffff")) is None def test_animation_get_color_code_use_xterm_colors(character: EffectCharacter) -> None: character.animation.use_xterm_colors = True assert character.animation._get_color_code(Color("ffffff")) == 15 assert character.animation._get_color_code(Color(0)) == 0 assert character.animation._get_color_code(Color("ffffff")) == 15 def test_animation_get_color_code_rgb_color(character: EffectCharacter) -> None: assert character.animation._get_color_code(Color("ffffff")) == "ffffff" def test_animation_get_color_code_color_is_none(character: EffectCharacter) -> None: assert character.animation._get_color_code(None) is None def test_animation_set_appearance_existing_colors(character: EffectCharacter) -> None: character.animation.existing_color_handling = "always" character.animation.input_fg_color = Color("ffffff") character.animation.input_bg_color = Color("000000") character.animation.set_appearance("a", colors=ColorPair(fg="f0f0f0", bg="0f0f0f")) assert character.animation.current_character_visual.colors == ColorPair( fg="ffffff", bg="000000", ) def test_animation_adjust_color_brightness_half(character: EffectCharacter) -> None: red = Color("ff0000") new_color = character.animation.adjust_color_brightness(red, 0.5) assert new_color == Color("7f0000") def test_animation_adjust_color_brightness_double(character: EffectCharacter) -> None: red = Color("ff0000") new_color = character.animation.adjust_color_brightness(red, 2) assert new_color == Color("ffffff") def test_animation_adjust_color_brightness_quarter(character: EffectCharacter) -> None: red = Color("ff0000") new_color = character.animation.adjust_color_brightness(red, 0.25) assert new_color == Color("3f0000") def test_animation_adjust_color_brightness_zero(character: EffectCharacter) -> None: red = Color("ff0000") new_color = character.animation.adjust_color_brightness(red, 0) assert new_color == Color("000000") def test_animation_adjust_color_brightness_negative(character: EffectCharacter) -> None: red = Color("ff0000") new_color = character.animation.adjust_color_brightness(red, -0.5) assert new_color == Color("000000") def test_animation_adjust_color_brightness_black(character: EffectCharacter) -> None: black = Color("000000") new_color = character.animation.adjust_color_brightness(black, 0.5) assert new_color == Color("000000") def test_animation_ease_animation_no_active_scene(character: EffectCharacter) -> None: assert character.animation._ease_animation(easing.in_sine) == 0 def test_animation_ease_animation_active_scene(character: EffectCharacter) -> None: scene = character.animation.new_scene(scene_id="test_scene", ease=easing.in_sine) scene.add_frame(symbol="a", duration=10) scene.add_frame(symbol="b", duration=10) character.animation.activate_scene(scene) for _ in range(10): character.animation.step_animation() n = character.animation._ease_animation(easing.in_sine) assert n == 0.2928932188134524 def test_animation_step_animation_sync_step(character: EffectCharacter) -> None: p = character.motion.new_path() p.new_waypoint(Coord(10, 10)) character.motion.activate_path(p) s = character.animation.new_scene(sync=Scene.SyncMetric.STEP) s.add_frame(symbol="a", duration=10) s.add_frame(symbol="b", duration=10) character.animation.activate_scene(s) for _ in range(5): character.animation.step_animation() def test_animation_step_animation_sync_distance(character: EffectCharacter) -> None: p = character.motion.new_path() p.new_waypoint(Coord(10, 10)) character.motion.activate_path(p) s = character.animation.new_scene(sync=Scene.SyncMetric.DISTANCE) s.add_frame(symbol="a", duration=10) s.add_frame(symbol="b", duration=10) character.animation.activate_scene(s) for _ in range(5): character.animation.step_animation() def test_animation_step_animation_sync_waypoint_deactivated(character: EffectCharacter) -> None: p = character.motion.new_path() p.new_waypoint(Coord(10, 10)) character.motion.activate_path(p) s = character.animation.new_scene(sync=Scene.SyncMetric.DISTANCE) s.add_frame(symbol="a", duration=10) s.add_frame(symbol="b", duration=10) character.animation.activate_scene(s) for _ in range(5): character.animation.step_animation() character.motion.deactivate_path(p) character.animation.step_animation() def test_animation_step_animation_eased_scene(character: EffectCharacter) -> None: scene = character.animation.new_scene(scene_id="test_scene", ease=easing.in_sine) scene.add_frame(symbol="a", duration=10) scene.add_frame(symbol="b", duration=10) character.animation.activate_scene(scene) while character.animation.active_scene: character.animation.step_animation() def test_animation_step_animation_eased_scene_looping(character: EffectCharacter) -> None: scene = character.animation.new_scene(scene_id="test_scene", ease=easing.in_sine, is_looping=True) scene.add_frame(symbol="a", duration=10) scene.add_frame(symbol="b", duration=10) character.animation.activate_scene(scene) for _ in range(100): character.animation.step_animation() def test_animation_deactivate_scene(character: EffectCharacter) -> None: scene = character.animation.new_scene(scene_id="test_scene") scene.add_frame(symbol="a", duration=10) character.animation.activate_scene(scene) character.animation.deactivate_scene(scene) assert character.animation.active_scene is None def test_scene_get_color_code_no_color(character: EffectCharacter) -> None: character.animation.no_color = True new_scene = character.animation.new_scene() assert new_scene._get_color_code(Color("ffffff")) is None def test_scene_get_color_code_use_xterm_colors(character: EffectCharacter) -> None: character.animation.use_xterm_colors = True new_scene = character.animation.new_scene() assert new_scene._get_color_code(Color("ffffff")) == 15 assert new_scene._get_color_code(Color(0)) == 0 assert new_scene._get_color_code(Color("ffffff")) == 15 def test_scene_input_color_from_existing(character: EffectCharacter) -> None: character.animation.existing_color_handling = "always" character.animation.input_fg_color = Color("ffffff") character.animation.input_bg_color = Color("000000") new_scene = character.animation.new_scene() assert new_scene.preexisting_colors == ColorPair(fg="ffffff", bg="000000") def test_scene_add_frame_existing_colors(character: EffectCharacter) -> None: character.animation.existing_color_handling = "always" character.animation.input_fg_color = Color("ffffff") character.animation.input_bg_color = Color("000000") new_scene = character.animation.new_scene() new_scene.add_frame(symbol="a", duration=1, colors=ColorPair(fg="f0f0f0", bg="0f0f0f")) # the frame colors should be overridden by the scene colors derived from the input assert new_scene.frames[0].character_visual.colors == ColorPair(fg="ffffff", bg="000000") def test_activate_scene_with_no_frames(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") with pytest.raises(ActivateEmptySceneError): character.animation.activate_scene(new_scene) def test_scene_get_next_visual_looping(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene", is_looping=True) new_scene.add_frame(symbol="a", duration=1) new_scene.add_frame(symbol="b", duration=1) character.animation.activate_scene(new_scene) visual = new_scene.get_next_visual() assert visual.symbol == "a" visual = new_scene.get_next_visual() assert visual.symbol == "b" visual = new_scene.get_next_visual() assert visual.symbol == "a" def test_scene_apply_gradient_to_symbols_empty_gradient(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") gradient = Gradient(Color("000000"), Color("ffffff"), steps=2) gradient.spectrum.clear() symbols = ["a", "b", "c"] with pytest.raises(ApplyGradientToSymbolsEmptyGradientsError): new_scene.apply_gradient_to_symbols(symbols, duration=1, fg_gradient=gradient) def test_scene_apply_gradient_to_symbols_both_gradients_empty(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") gradient = Gradient(Color("000000"), Color("ffffff"), steps=2) gradient.spectrum.clear() symbols = ["a", "b", "c"] with pytest.raises(ApplyGradientToSymbolsEmptyGradientsError): new_scene.apply_gradient_to_symbols(symbols, duration=1, fg_gradient=gradient, bg_gradient=gradient) def test_scene_apply_gradient_to_symbols_invalid_symbols(character: EffectCharacter) -> None: """Test that an ApplyGradientToSymbolsInvalidSymbolError is raised when a symbol with length > 1 is passed.""" new_scene = character.animation.new_scene(scene_id="test_scene") gradient = Gradient(Color("000000"), Color("ffffff"), steps=2) symbols = ["aa", "b", "c"] with pytest.raises(ApplyGradientToSymbolsInvalidSymbolError): new_scene.apply_gradient_to_symbols(symbols, duration=1, fg_gradient=gradient) def test_scene_apply_gradient_to_symbols_single_single_step(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") gradient = Gradient(Color("000000"), Color("ffffff"), steps=1) symbols = ["a"] new_scene.apply_gradient_to_symbols(symbols, duration=1, fg_gradient=gradient, bg_gradient=gradient) assert len(new_scene.frames) == 2 for i, frame in enumerate(new_scene.frames): assert frame.character_visual._fg_color_code == gradient.spectrum[i].rgb_color assert symbols[0] in frame.character_visual.symbol def test_scene_apply_gradient_to_symbols_fg_bg_spectrums_not_equal(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") fg_gradient = Gradient(Color("000000"), Color("ffffff"), steps=8) bg_gradient = Gradient(Color("ffffff"), Color("000000"), steps=6) symbols = ["a", "b", "c"] new_scene.apply_gradient_to_symbols(symbols, duration=1, fg_gradient=fg_gradient, bg_gradient=bg_gradient) assert len(new_scene.frames) == 9 for i, frame in enumerate(new_scene.frames): assert frame.character_visual._fg_color_code == fg_gradient.spectrum[i].rgb_color def test_scene_apply_gradient_to_symbols_empty_spectrums(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") fg_gradient = Gradient(Color("000000"), Color("ffffff"), steps=1) bg_gradient = Gradient(Color("ffffff"), Color("000000"), steps=1) fg_gradient.spectrum.clear() bg_gradient.spectrum.clear() symbols = ["a", "b", "c"] with pytest.raises(ApplyGradientToSymbolsEmptyGradientsError): new_scene.apply_gradient_to_symbols(symbols, duration=1, fg_gradient=fg_gradient, bg_gradient=bg_gradient) def test_scene_apply_gradient_to_symbols_no_gradients(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") symbols = ["a", "b", "c"] with pytest.raises(ApplyGradientToSymbolsNoGradientsError): new_scene.apply_gradient_to_symbols(symbols, duration=1) def test_scene_apply_gradient_to_symbols_larger_bg_spectrum(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") fg_gradient = Gradient(Color("000000"), Color("ffffff"), steps=3) bg_gradient = Gradient(Color("ffffff"), Color("000000"), steps=6) symbols = ["a", "b", "c"] new_scene.apply_gradient_to_symbols(symbols, duration=1, fg_gradient=fg_gradient, bg_gradient=bg_gradient) assert len(new_scene.frames) == 7 for i, frame in enumerate(new_scene.frames): assert frame.character_visual._bg_color_code == bg_gradient.spectrum[i].rgb_color def test_scene_apply_gradient_to_symbols_larger_fg_spectrum(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") fg_gradient = Gradient(Color("000000"), Color("ffffff"), steps=6) bg_gradient = Gradient(Color("ffffff"), Color("000000"), steps=3) symbols = ["a", "b", "c"] new_scene.apply_gradient_to_symbols(symbols, duration=1, fg_gradient=fg_gradient, bg_gradient=bg_gradient) assert len(new_scene.frames) == 7 for i, frame in enumerate(new_scene.frames): assert frame.character_visual._fg_color_code == fg_gradient.spectrum[i].rgb_color def test_scene_apply_gradient_to_symbols_fg_gradient_only(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") fg_gradient = Gradient(Color("000000"), Color("ffffff"), steps=3) symbols = ["a", "b", "c"] new_scene.apply_gradient_to_symbols(symbols, duration=1, fg_gradient=fg_gradient) assert len(new_scene.frames) == 4 for i, frame in enumerate(new_scene.frames): assert frame.character_visual._fg_color_code == fg_gradient.spectrum[i].rgb_color def test_scene_apply_gradient_to_symbols_bg_gradient_only(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") bg_gradient = Gradient(Color("ffffff"), Color("000000"), steps=3) symbols = ["a", "b", "c"] new_scene.apply_gradient_to_symbols(symbols, duration=1, bg_gradient=bg_gradient) assert len(new_scene.frames) == 4 for i, frame in enumerate(new_scene.frames): assert frame.character_visual._bg_color_code == bg_gradient.spectrum[i].rgb_color def test_scene_reset_scene(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") new_scene.add_frame(symbol="a", duration=3) new_scene.add_frame(symbol="b", duration=3) for _ in range(4): new_scene.get_next_visual() new_scene.reset_scene() for sequence in new_scene.frames: assert sequence.ticks_elapsed == 0 assert not new_scene.played_frames def test_scene_id_equality(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") new_scene2 = character.animation.new_scene(scene_id="test_scene") assert new_scene == new_scene2 def test_scene_equality_incorrect_type(character: EffectCharacter) -> None: new_scene = character.animation.new_scene(scene_id="test_scene") assert new_scene != "test_scene" terminaltexteffects-release-0.12.1/tests/engine_tests/test_base_character.py000066400000000000000000000166421507200677100274770ustar00rootroot00000000000000"""Tests for the base_character module including the BaseCharacter class and the EventHandler class.""" from __future__ import annotations from typing import Any import pytest from terminaltexteffects.engine.base_character import EffectCharacter, EventHandler from terminaltexteffects.engine.motion import Path from terminaltexteffects.utils.exceptions.base_character_exceptions import ( EventRegistrationCallerError, EventRegistrationTargetError, ) from terminaltexteffects.utils.geometry import Coord pytestmark = [pytest.mark.engine, pytest.mark.base_character, pytest.mark.smoke] @pytest.fixture def effectcharacter() -> EffectCharacter: """Fixture for creating an EffectCharacter instance.""" return EffectCharacter(0, "a", 1, 1) @pytest.fixture def eventhandler(effectcharacter: EffectCharacter) -> EventHandler: """Fixture for creating an EventHandler instance.""" return EventHandler(effectcharacter) def test_eventhandler_init(eventhandler: EventHandler, effectcharacter: EffectCharacter) -> None: """Test the initialization of EventHandler.""" assert eventhandler.character == effectcharacter assert eventhandler.registered_events == {} def test_eventhandler_callback_init(eventhandler: EventHandler) -> None: """Test the initialization of EventHandler.Callback.""" def func(*_: Any) -> None: pass cb = eventhandler.Callback(func, "a") assert cb.callback == func assert len(cb.args) == 1 @pytest.mark.parametrize( "event", [ EventHandler.Event.PATH_COMPLETE, EventHandler.Event.PATH_ACTIVATED, EventHandler.Event.PATH_HOLDING, EventHandler.Event.SCENE_ACTIVATED, EventHandler.Event.SCENE_COMPLETE, EventHandler.Event.SEGMENT_ENTERED, EventHandler.Event.SEGMENT_EXITED, ], ) def test_eventhandler_register_event_invalid_event_caller( event: EventHandler.Event, eventhandler: EventHandler, ) -> None: """Test registering an event with an invalid event caller.""" with pytest.raises(EventRegistrationCallerError): eventhandler.register_event(event, "invalid_caller", EventHandler.Action.ACTIVATE_PATH, Path("a")) # type: ignore[call-overload] @pytest.mark.parametrize( "event_caller_action_target", [ (EventHandler.Event.PATH_COMPLETE, Path("a"), EventHandler.Action.ACTIVATE_PATH, "invalid_target"), (EventHandler.Event.PATH_COMPLETE, Path("a"), EventHandler.Action.DEACTIVATE_PATH, "invalid_target"), (EventHandler.Event.PATH_COMPLETE, Path("a"), EventHandler.Action.ACTIVATE_SCENE, "invalid_target"), (EventHandler.Event.PATH_COMPLETE, Path("a"), EventHandler.Action.DEACTIVATE_SCENE, "invalid_target"), (EventHandler.Event.PATH_COMPLETE, Path("a"), EventHandler.Action.CALLBACK, "invalid_target"), (EventHandler.Event.PATH_COMPLETE, Path("a"), EventHandler.Action.SET_LAYER, "invalid_target"), (EventHandler.Event.PATH_COMPLETE, Path("a"), EventHandler.Action.SET_COORDINATE, "invalid_target"), (EventHandler.Event.PATH_COMPLETE, Path("a"), EventHandler.Action.RESET_APPEARANCE, "invalid_target"), ], ) def test_eventhandler_register_event_invalid_target( eventhandler: EventHandler, event_caller_action_target: tuple[EventHandler.Event, Path, EventHandler.Action, str], ) -> None: """Test registering an event with an invalid target.""" event, caller, action, target = event_caller_action_target with pytest.raises(EventRegistrationTargetError): eventhandler.register_event(event, caller, action, target) # type: ignore[call-overload] def test_eventhandler_register_event(eventhandler: EventHandler) -> None: """Test registering a valid event.""" p1 = Path("a") p2 = Path("b") eventhandler.register_event(EventHandler.Event.PATH_COMPLETE, p1, EventHandler.Action.ACTIVATE_PATH, p2) assert ( eventhandler.registered_events[(EventHandler.Event.PATH_COMPLETE, p1)][0][0] is EventHandler.Action.ACTIVATE_PATH ) assert eventhandler.registered_events[(EventHandler.Event.PATH_COMPLETE, p1)][0][1] is p2 def test_eventhandler_handle_event(eventhandler: EventHandler) -> None: """Test handling an event.""" p1 = Path("a") p2 = Path("b") p2.new_waypoint(Coord(0, 0)) eventhandler.register_event(EventHandler.Event.PATH_COMPLETE, p1, EventHandler.Action.ACTIVATE_PATH, p2) eventhandler._handle_event(EventHandler.Event.PATH_COMPLETE, p1) assert eventhandler.character.motion.active_path == p2 def test_effectcharacter_init(effectcharacter: EffectCharacter) -> None: """Test the initialization of EffectCharacter.""" assert effectcharacter.character_id == 0 assert effectcharacter._input_symbol == "a" assert effectcharacter._input_coord == Coord(1, 1) assert effectcharacter._input_ansi_sequences == {"fg_color": None, "bg_color": None} assert effectcharacter._is_visible is False assert effectcharacter.layer == 0 assert effectcharacter.is_fill_character is False def test_effectcharacter_repr(effectcharacter: EffectCharacter) -> None: """Test the __repr__ method of EffectCharacter.""" assert repr(effectcharacter) == "EffectCharacter(character_id=0, symbol='a', input_column=1, input_row=1)" def test_effectcharacter_hash_consistency(effectcharacter: EffectCharacter) -> None: """Test the consistency of the __hash__ method of EffectCharacter.""" assert hash(effectcharacter) == hash(effectcharacter) def test_effectcharacter_objects_have_same_hash(effectcharacter: EffectCharacter) -> None: """Test that two EffectCharacter objects with the same attributes have the same hash.""" effectcharacter2 = EffectCharacter(0, "a", 1, 1) assert hash(effectcharacter) == hash(effectcharacter2) def test_effectcharacter_properties(effectcharacter: EffectCharacter) -> None: """Test the properties of EffectCharacter.""" assert effectcharacter.input_symbol == "a" assert effectcharacter.input_coord == Coord(1, 1) assert effectcharacter.is_visible is False assert effectcharacter.character_id == 0 assert effectcharacter.is_active is False def test_effectcharacter_is_active(effectcharacter: EffectCharacter) -> None: """Test the is_active property of EffectCharacter.""" assert effectcharacter.is_active is False p = effectcharacter.motion.new_path() p.new_waypoint(Coord(0, 0)) effectcharacter.motion.activate_path(p) assert effectcharacter.is_active is True def test_effectcharacter_tick_no_paths_or_scenes(effectcharacter: EffectCharacter) -> None: """Test that tick does not fail when there are no paths or scenes.""" effectcharacter.tick() def test_effectcharacter_tick_scene_and_path(effectcharacter: EffectCharacter) -> None: """Test that tick updates both scene and path correctly.""" p = effectcharacter.motion.new_path() p.new_waypoint(Coord(3, 3)) effectcharacter.motion.activate_path(p) s = effectcharacter.animation.new_scene() s.add_frame("a", duration=2) effectcharacter.animation.activate_scene(s) effectcharacter.tick() assert effectcharacter.animation.active_scene.frames[0].ticks_elapsed == 1 # type: ignore[union-attr] assert effectcharacter.motion.active_path.current_step == 1 # type: ignore[union-attr] def test_effectcharacter_equal_invalid_type(effectcharacter: EffectCharacter) -> None: """Test that __eq__ returns NotImplemented when comparing with an invalid type.""" assert effectcharacter.__eq__("a") is NotImplemented terminaltexteffects-release-0.12.1/tests/engine_tests/test_motion.py000066400000000000000000000440251507200677100260520ustar00rootroot00000000000000"""Tests for the Path, Segment, Waypoint and Motion classes.""" import pytest from terminaltexteffects.engine.base_character import EffectCharacter, EventHandler from terminaltexteffects.engine.motion import Path, Segment, Waypoint from terminaltexteffects.utils import easing from terminaltexteffects.utils.exceptions import ( ActivateEmptyPathError, DuplicatePathIDError, DuplicateWaypointIDError, PathInvalidSpeedError, PathNotFoundError, WaypointNotFoundError, ) from terminaltexteffects.utils.geometry import Coord, find_length_of_bezier_curve, find_length_of_line pytestmark = [pytest.mark.engine, pytest.mark.motion, pytest.mark.smoke] @pytest.fixture def character() -> EffectCharacter: """Fixture for creating an EffectCharacter instance.""" return EffectCharacter(0, "a", 0, 0) @pytest.fixture def waypoint() -> Waypoint: """Fixture for creating a Waypoint instance.""" return Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0), bezier_control=(Coord(0, 10),)) def test_waypoint_init(waypoint: Waypoint) -> None: """Test the initialization of a Waypoint.""" assert waypoint.waypoint_id == "waypoint_0" assert waypoint.coord == Coord(0, 0) assert waypoint.bezier_control == (Coord(0, 10),) def test_waypoint_equal_waypoint(waypoint: Waypoint) -> None: """Test equality of waypoints with the same ID and coordinates.""" assert waypoint == Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0), bezier_control=(Coord(0, 10),)) def test_waypoint_equal_unqual_waypoint(waypoint: Waypoint) -> None: """Test inequality of waypoints with different IDs and coordinates.""" assert waypoint != Waypoint(waypoint_id="waypoint_1", coord=Coord(1, 0), bezier_control=(Coord(0, 10),)) def test_waypoint_equal_different_type(waypoint: Waypoint) -> None: """Test inequality of waypoint with a different type.""" assert waypoint != "waypoint_0" def test_segment_length_no_bezier() -> None: """Test segment length calculation without bezier control points.""" waypoint_0 = Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0)) waypoint_1 = Waypoint(waypoint_id="waypoint_1", coord=Coord(10, 0)) segment = Segment(waypoint_0, waypoint_1, find_length_of_line(waypoint_0.coord, waypoint_1.coord)) line_length = 10 assert segment.distance == line_length def test_segment_length_bezier() -> None: """Test segment length calculation with bezier control points.""" waypoint_0 = Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0), bezier_control=(Coord(5, 5),)) waypoint_1 = Waypoint(waypoint_id="waypoint_1", coord=Coord(10, 0), bezier_control=(Coord(10, 10),)) segment = Segment( waypoint_0, waypoint_1, find_length_of_bezier_curve(waypoint_0.coord, waypoint_0.bezier_control, waypoint_1.coord), # type: ignore[arg-type] ) bezier_length = 10.242640687119286 assert segment.distance == bezier_length def test_segment_is_hashable() -> None: """Test that a Segment instance is hashable.""" waypoint_0 = Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0)) waypoint_1 = Waypoint(waypoint_id="waypoint_1", coord=Coord(10, 0)) segment = Segment(waypoint_0, waypoint_1, find_length_of_line(waypoint_0.coord, waypoint_1.coord)) assert hash(segment) == hash((waypoint_0, waypoint_1)) def test_segment_equal_segment() -> None: """Test equality of segments with the same waypoints and distance.""" waypoint_0 = Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0)) waypoint_1 = Waypoint(waypoint_id="waypoint_1", coord=Coord(10, 0)) segment = Segment(waypoint_0, waypoint_1, find_length_of_line(waypoint_0.coord, waypoint_1.coord)) assert segment == Segment(waypoint_0, waypoint_1, find_length_of_line(waypoint_0.coord, waypoint_1.coord)) def test_segment_equal_incorrect_type() -> None: """Test inequality of segment with a different type.""" waypoint_0 = Waypoint(waypoint_id="waypoint_0", coord=Coord(0, 0)) waypoint_1 = Waypoint(waypoint_id="waypoint_1", coord=Coord(10, 0)) segment = Segment(waypoint_0, waypoint_1, find_length_of_line(waypoint_0.coord, waypoint_1.coord)) assert segment != "segment" def test_path_init() -> None: """Test the initialization of a Path.""" p = Path("path_0") assert p.path_id == "path_0" assert p.speed == 1 assert p.ease is None assert p.layer is None assert p.hold_time == 0 assert p.loop is False assert p.segments == [] assert p.waypoints == [] assert p.waypoint_lookup == {} assert p.total_distance == 0 assert p.current_step == 0 assert p.max_steps == 0 assert p.hold_time_remaining == 0 assert p.last_distance_reached == 0 assert p.origin_segment is None def test_path_init_invalid_speed() -> None: """Test initialization of a Path with invalid speed.""" with pytest.raises(PathInvalidSpeedError): Path("path_0", speed=-1) def test_path_new_waypoint_auto_id_generation() -> None: """Test auto ID generation for new waypoints. ID's should start at 0 and increment by 1 for each new waypoint. """ p = Path("p") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) assert p.waypoints[0].waypoint_id == "0" assert p.waypoints[1].waypoint_id == "1" def test_path_new_waypoint_auto_id_deleted_waypoints() -> None: """Test auto ID generation for new waypoints after deletion. ID's should start at 0 and increment by 1 for each new waypoint, even if waypoints have been deleted. """ # waypoint auto ID's start at 0 p = Path("p") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) p.new_waypoint(Coord(20, 0)) p.waypoints.pop(-1) new_waypoint = p.new_waypoint(Coord(30, 0)) assert new_waypoint.waypoint_id == "3" def test_path_new_waypoint_duplicate_waypoint_id() -> None: """Test that creating a waypoint with a duplicate ID raises an error.""" p = Path("p") p.new_waypoint(Coord(0, 0), waypoint_id="0") with pytest.raises(DuplicateWaypointIDError): p.new_waypoint(Coord(10, 0), waypoint_id="0") def test_path_new_waypoint_bezier_as_single_coord() -> None: """Test that a single coordinate can be used as a bezier control point.""" p = Path("p") p.new_waypoint(Coord(0, 0), bezier_control=Coord(0, 10)) assert p.waypoints[0].bezier_control == (Coord(0, 10),) def test_path_new_waypoint_bezier_as_tuple() -> None: """Test that a tuple can be used as bezier control points.""" p = Path("p") p.new_waypoint(Coord(0, 0), bezier_control=(Coord(0, 10),)) assert p.waypoints[0].bezier_control == (Coord(0, 10),) def test_path_new_waypoint_multiple_waypoints_with_bezier_segment() -> None: """Test multiple waypoints with bezier segments.""" p = Path("p") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0), bezier_control=Coord(10, 10)) assert p.segments[0].distance == find_length_of_bezier_curve(Coord(0, 0), Coord(10, 10), Coord(10, 0)) def test_path_query_waypoint_valid_waypoint() -> None: """Test querying an existing waypoint ID in a path.""" p = Path("p") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) assert p.query_waypoint("0") == p.waypoints[0] def test_path_query_waypoint_invalid_waypoint() -> None: """Test querying a non-existing waypoint ID in a path.""" p = Path("p") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) with pytest.raises(WaypointNotFoundError): p.query_waypoint("2") def test_path_step_zero_distance(character: EffectCharacter) -> None: """Test stepping through a path with zero distance.""" p = Path("p") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(0, 0)) assert p.step(character.event_handler) == Coord(0, 0) def test_path_step_single_segment(character: EffectCharacter) -> None: """Test stepping through a single segment path.""" p = Path("p") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) current_point = p.step(character.event_handler) while current_point != Coord(10, 0): current_point = p.step(character.event_handler) def test_path_step_single_segment_eased(character: EffectCharacter) -> None: """Test stepping through a single segment path with easing.""" p = Path("p", ease=easing.in_out_sine) p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) current_point = p.step(character.event_handler) while current_point != Coord(10, 0): current_point = p.step(character.event_handler) def test_path_step_multiple_segments(character: EffectCharacter) -> None: """Test stepping through a path with multiple segments.""" p = Path("p") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) p.new_waypoint(Coord(10, 10)) p.new_waypoint(Coord(0, 10)) current_point = p.step(character.event_handler) while current_point != Coord(0, 10): current_point = p.step(character.event_handler) def test_path_step_multiple_segments_eased(character: EffectCharacter) -> None: """Test stepping through a path with multiple segments and easing.""" p = Path("p", ease=easing.in_out_elastic) p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) p.new_waypoint(Coord(10, 10)) p.new_waypoint(Coord(0, 10)) current_point = p.step(character.event_handler) while current_point != Coord(0, 10): current_point = p.step(character.event_handler) def test_path_step_multiple_segments_zero_distance(character: EffectCharacter) -> None: """Test stepping through a path with multiple segments and zero distance.""" p = Path("p") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(0, 0)) current_point = p.step(character.event_handler) while current_point != Coord(0, 0): current_point = p.step(character.event_handler) def test_path_step_multiple_segments_mutiple_bezier(character: EffectCharacter) -> None: """Test stepping through a path with multiple segments and multiple bezier control points.""" p = Path("p") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0), bezier_control=Coord(10, 10)) p.new_waypoint(Coord(10, 10), bezier_control=Coord(0, 10)) p.new_waypoint(Coord(0, 10), bezier_control=Coord(0, 0)) current_point = p.step(character.event_handler) while current_point != Coord(0, 10): current_point = p.step(character.event_handler) def test_path_equality() -> None: """Test equality of paths with the same waypoints.""" p1 = Path("p") p1.new_waypoint(Coord(0, 0)) p1.new_waypoint(Coord(10, 0)) p1.new_waypoint(Coord(10, 10)) p1.new_waypoint(Coord(0, 10)) p2 = Path("p") p2.new_waypoint(Coord(0, 0)) p2.new_waypoint(Coord(10, 0)) p2.new_waypoint(Coord(10, 10)) p2.new_waypoint(Coord(0, 10)) assert p1 == p2 def test_path_equality_invalid_type() -> None: """Test inequality of path with a different type.""" p = Path("p") assert p != "p" def test_motion_set_coordinate(character: EffectCharacter) -> None: """Test setting the coordinate of a character.""" character.motion.set_coordinate(Coord(10, 10)) assert character.motion.current_coord == Coord(10, 10) def test_motion_new_path_duplicate_path_id(character: EffectCharacter) -> None: """Test creating a new path with a duplicate ID raises an error.""" character.motion.new_path(path_id="0") with pytest.raises(DuplicatePathIDError): character.motion.new_path(path_id="0") def test_motion_new_path_auto_id_avoid_duplicate(character: EffectCharacter) -> None: """Test auto ID generation for new paths avoiding duplicates.""" character.motion.new_path(path_id="1") character.motion.new_path(path_id="2") character.motion.new_path(path_id="3") new_path = character.motion.new_path() assert new_path.path_id == "4" def test_motion_query_path_valid_path(character: EffectCharacter) -> None: """Test querying an existing path ID.""" character.motion.new_path(path_id="0") character.motion.new_path(path_id="1") assert character.motion.query_path("0").path_id == "0" def test_motion_query_path_invalid_path(character: EffectCharacter) -> None: """Test querying a non-existing path ID raises an error.""" character.motion.new_path(path_id="0") character.motion.new_path(path_id="1") with pytest.raises(PathNotFoundError): character.motion.query_path("2") def test_motion_movement_is_complete_no_active_paths(character: EffectCharacter) -> None: """Test checking if movement is complete with no active paths.""" assert character.motion.movement_is_complete() is True def test_motion_movement_is_complete_active_path_complete(character: EffectCharacter) -> None: """Test checking if movement is complete with an active path that is complete.""" p = character.motion.new_path(path_id="0") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) character.motion.activate_path(p) while character.motion.active_path: character.motion.move() assert character.motion.movement_is_complete() is True def test_motion_movement_is_complete_active_path_incomplete(character: EffectCharacter) -> None: """Test checking if movement is complete with an active path that is incomplete.""" p = character.motion.new_path(path_id="0") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) character.motion.activate_path(p) assert character.motion.movement_is_complete() is False def test_motion_chain_paths_single_path(character: EffectCharacter) -> None: """Test chaining a single path.""" p = character.motion.new_path(path_id="0") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) character.motion.chain_paths( [ p, ], ) def test_motion_chain_paths_multiple_paths(character: EffectCharacter) -> None: """Test chaining multiple paths.""" p1 = character.motion.new_path(path_id="0") p1.new_waypoint(Coord(0, 0)) p1.new_waypoint(Coord(10, 0)) p2 = character.motion.new_path(path_id="1") p2.new_waypoint(Coord(10, 0)) p2.new_waypoint(Coord(10, 10)) character.motion.chain_paths([p1, p2]) assert (EventHandler.Event.PATH_COMPLETE, p1) in character.event_handler.registered_events assert character.event_handler.registered_events[(EventHandler.Event.PATH_COMPLETE, p1)] == [ (EventHandler.Action.ACTIVATE_PATH, p2), ] def test_motion_chain_paths_multiple_paths_looping(character: EffectCharacter) -> None: """Test chaining multiple paths with looping.""" p1 = character.motion.new_path(path_id="0") p1.new_waypoint(Coord(0, 0)) p1.new_waypoint(Coord(10, 0)) p2 = character.motion.new_path(path_id="1") p2.new_waypoint(Coord(10, 0)) p2.new_waypoint(Coord(10, 10)) character.motion.chain_paths([p1, p2], loop=True) assert (EventHandler.Event.PATH_COMPLETE, p1) in character.event_handler.registered_events assert character.event_handler.registered_events[(EventHandler.Event.PATH_COMPLETE, p1)] == [ (EventHandler.Action.ACTIVATE_PATH, p2), ] assert (EventHandler.Event.PATH_COMPLETE, p2) in character.event_handler.registered_events assert character.event_handler.registered_events[(EventHandler.Event.PATH_COMPLETE, p2)] == [ (EventHandler.Action.ACTIVATE_PATH, p1), ] def test_motion_activate_path_first_waypoint_bezier(character: EffectCharacter) -> None: """Test activating a path with the first waypoint having a bezier control point.""" p = character.motion.new_path(path_id="0") p.new_waypoint(Coord(0, 0), bezier_control=Coord(0, 10)) p.new_waypoint(Coord(10, 0)) character.motion.activate_path(p) assert character.motion.active_path == p def test_motion_activate_path_no_waypoints(character: EffectCharacter) -> None: """Test activating a path with no waypoints raises an error.""" p = character.motion.new_path(path_id="0") with pytest.raises(ActivateEmptyPathError): character.motion.activate_path(p) def test_motion_active_path_with_layer(character: EffectCharacter) -> None: """Test activating a path with a layer.""" p = character.motion.new_path(path_id="0", layer=1) p.new_waypoint(Coord(0, 0), bezier_control=Coord(0, 10)) p.new_waypoint(Coord(10, 0)) character.motion.activate_path(p) assert character.motion.active_path == p assert character.layer == 1 def test_motion_activate_path_previously_deactivated(character: EffectCharacter) -> None: """Test reactivating a path that was previously deactivated.""" character.motion.set_coordinate(Coord(5, 5)) p = character.motion.new_path(path_id="0") p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) character.motion.activate_path(p) first_origin_distance = p.origin_segment.distance if p.origin_segment else 0 character.motion.deactivate_path(p) character.motion.set_coordinate(Coord(2, 2)) character.motion.activate_path(p) second_origin_distance = p.origin_segment.distance if p.origin_segment else 0 assert character.motion.active_path == p assert second_origin_distance < first_origin_distance def test_motion_move_no_active_path(character: EffectCharacter) -> None: """Test moving a character with no active path.""" assert character.motion.active_path is None character.motion.move() def test_motion_move_path_hold_time(character: EffectCharacter) -> None: """Test moving a character along a path with hold time.""" p = character.motion.new_path(path_id="0", hold_time=5) p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) character.motion.activate_path(p) character.motion.move() while character.motion.active_path: character.motion.move() assert character.motion.active_path is None def test_motion_move_path_looping(character: EffectCharacter) -> None: """Test moving a character along a looping path.""" p = character.motion.new_path(path_id="0", loop=True) p.new_waypoint(Coord(0, 0)) p.new_waypoint(Coord(10, 0)) p.new_waypoint(Coord(10, 10)) character.motion.activate_path(p) for _ in range(100): character.motion.move() terminaltexteffects-release-0.12.1/tests/engine_tests/test_terminal.py000066400000000000000000000554071507200677100263660ustar00rootroot00000000000000import shutil import time import pytest from terminaltexteffects.engine.base_character import EffectCharacter from terminaltexteffects.engine.terminal import Canvas, Terminal, TerminalConfig from terminaltexteffects.utils.exceptions import ( InvalidCharacterGroupError, InvalidCharacterSortError, InvalidColorSortError, ) from terminaltexteffects.utils.geometry import Coord from terminaltexteffects.utils.graphics import Color pytestmark = [pytest.mark.engine, pytest.mark.terminal, pytest.mark.smoke] def test_canvas_init_even(): canvas = Canvas(10, 10) assert canvas.width == 10 assert canvas.height == 10 assert canvas.center_row == 5 assert canvas.center_column == 5 assert canvas.center == Coord(5, 5) def test_canvas_init_odd(): canvas = Canvas(11, 11) assert canvas.width == 11 assert canvas.height == 11 assert canvas.center_row == 6 assert canvas.center_column == 6 assert canvas.center == Coord(6, 6) def test_canvas_single_col_row(): canvas = Canvas(1, 1) assert canvas.width == 1 assert canvas.height == 1 assert canvas.center_row == 1 assert canvas.center_column == 1 assert canvas.center == Coord(1, 1) @pytest.mark.parametrize("anchor", ["n", "ne", "e", "se", "s", "sw", "w", "nw", "c"]) def test_canvas_anchor_text(anchor): c0 = EffectCharacter(0, symbol="a", input_column=1, input_row=1) c1 = EffectCharacter(1, symbol="b", input_column=2, input_row=1) canvas = Canvas(10, 10) chars = canvas._anchor_text([c0, c1], anchor=anchor) if anchor == "sw": assert chars[0].motion.current_coord == Coord(1, 1) elif anchor == "s": assert chars[0].motion.current_coord == Coord(5, 1) elif anchor == "se": assert chars[1].motion.current_coord == Coord(10, 1) elif anchor == "e": assert chars[1].motion.current_coord == Coord(10, 6) elif anchor == "ne": assert chars[1].motion.current_coord == Coord(10, 10) elif anchor == "n": assert chars[0].motion.current_coord == Coord(5, 10) elif anchor == "nw": assert chars[0].motion.current_coord == Coord(1, 10) elif anchor == "w": assert chars[0].motion.current_coord == Coord(1, 6) elif anchor == "c": assert chars[0].motion.current_coord == Coord(5, 6) assert chars[1].motion.current_coord == Coord(6, 6) def test_canvas_coord_is_in_canvas(): canvas = Canvas(10, 10) assert canvas.coord_is_in_canvas(Coord(5, 5)) assert canvas.coord_is_in_canvas(Coord(1, 1)) assert canvas.coord_is_in_canvas(Coord(10, 10)) assert canvas.coord_is_in_canvas(Coord(1, 10)) assert canvas.coord_is_in_canvas(Coord(10, 1)) assert not canvas.coord_is_in_canvas(Coord(0, 0)) assert not canvas.coord_is_in_canvas(Coord(11, 11)) assert not canvas.coord_is_in_canvas(Coord(0, 5)) assert not canvas.coord_is_in_canvas(Coord(5, 0)) assert not canvas.coord_is_in_canvas(Coord(11, 5)) assert not canvas.coord_is_in_canvas(Coord(5, 11)) assert not canvas.coord_is_in_canvas(Coord(0, 0)) assert not canvas.coord_is_in_canvas(Coord(11, 11)) assert not canvas.coord_is_in_canvas(Coord(0, 5)) assert not canvas.coord_is_in_canvas(Coord(5, 0)) assert not canvas.coord_is_in_canvas(Coord(11, 5)) assert not canvas.coord_is_in_canvas(Coord(5, 11)) def test_canvas_random_column(): canvas = Canvas(10, 10) random_column = canvas.random_column() assert 1 <= random_column <= 10 def test_canvas_random_row(): canvas = Canvas(10, 10) random_row = canvas.random_row() assert 1 <= random_row <= 10 def test_canvas_random_coord_inside_canvas(): canvas = Canvas(10, 10) random_coord = canvas.random_coord() assert 1 <= random_coord.column <= 10 assert 1 <= random_coord.row <= 10 def test_canvas_random_coord_outside_canvas(): canvas = Canvas(10, 10) random_coord = canvas.random_coord(outside_scope=True) if 1 <= random_coord.column <= 10: assert random_coord.row == 0 or random_coord.row == 11 elif 1 <= random_coord.row <= 10: assert random_coord.column == 0 or random_coord.column == 11 def test_terminal_init_no_config(): terminal = Terminal("test") assert terminal.config == TerminalConfig() def test_terminal_init_with_config(): config = TerminalConfig() config.frame_rate = 10 terminal = Terminal("test", config=config) assert terminal.config.frame_rate == 10 def test_terminal_init_no_input(): terminal = Terminal(input_data="") assert len(terminal.get_characters()) == 8 def test_terminal_init_ignore_terminal_dimensions(): config = TerminalConfig(ignore_terminal_dimensions=True) terminal = Terminal("test", config=config) terminal._terminal_height = 1 terminal._terminal_width = 4 def test_terminal_preprocess_input_data_existing_color(): # test ANSI color string # char pos - symbol - fg - bg # (1,1) - a - 255,0,0 - 0,0,0 # (2,1) - b - 0,255,0 - 0,0,0 # (3,1) - c - 0,255,0 - 0,0,255 # (4,1) - d - 196 - 0,0,0 # (5,1) - e - 196 - 106 # (6,1) - f - 196 - 68 # (7,1) - g - 0,255,0 - 68 # \x1b[38;2;255;0;0ma # \x1b[38;2;0;255;0mb # \x1b[48;2;0;0;255mc # \x1b[0m # \033[38;5;196md # \033[48;5;106me # \033[48;5;68mf # \x1b[38;2;;255;mg # \x1b[0m config = TerminalConfig(existing_color_handling="always") terminal = Terminal(input_data="", config=config) input_data = "\x1b[38;2;255;0;0ma\x1b[38;2;0;255;0mb\x1b[48;2;0;0;255mc\x1b[0m\033[38;5;196md\033[48;5;106me\033[48;5;68mf\x1b[38;2;;255;mg\x1b[0m" chars = terminal._preprocess_input_data(input_data)[0] assert len(chars) == 7 assert chars[0].animation.input_bg_color is None assert chars[0].animation.input_fg_color == Color("FF0000") assert chars[1].animation.input_bg_color is None assert chars[1].animation.input_fg_color == Color("00FF00") assert chars[2].animation.input_bg_color == Color("0000FF") assert chars[2].animation.input_fg_color == Color("00FF00") assert chars[3].animation.input_bg_color is None assert chars[3].animation.input_fg_color == Color(196) assert chars[4].animation.input_bg_color == Color(106) assert chars[4].animation.input_fg_color == Color(196) assert chars[5].animation.input_bg_color == Color(68) assert chars[5].animation.input_fg_color == Color(196) assert chars[6].animation.input_bg_color == Color(68) assert chars[6].animation.input_fg_color == Color("00FF00") chars = terminal._preprocess_input_data(input_data)[0] test_char_colors = chars[0].animation.current_character_visual.colors assert test_char_colors is not None assert test_char_colors.bg_color is None assert test_char_colors.fg_color == Color("FF0000") @pytest.mark.parametrize("anchor", ["n", "ne", "e", "se", "s", "sw", "w", "nw", "c"]) def test_terminal_calc_canvas_offsets(anchor, monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(Terminal, "_get_terminal_dimensions", lambda _: (10, 10)) config = TerminalConfig(anchor_canvas=anchor) terminal = Terminal(input_data="test", config=config) assert terminal._terminal_height == 10 assert terminal._terminal_width == 10 column_offset, row_offset = terminal._calc_canvas_offsets() if anchor in ["s", "n", "c"]: assert column_offset == 3 elif anchor in ["se", "e", "ne"]: assert column_offset == 6 else: assert column_offset == 0 if anchor in ["e", "w", "c"]: assert row_offset == 5 elif anchor in ["nw", "n", "ne"]: assert row_offset == 9 else: assert row_offset == 0 def test_terminal_get_canvas_dimensions_exact(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(Terminal, "_get_terminal_dimensions", lambda _: (10, 10)) config = TerminalConfig() config.canvas_width = 5 config.canvas_height = 5 terminal = Terminal(input_data="test", config=config) assert terminal.canvas.width == 5 assert terminal.canvas.height == 5 def test_terminal_get_canvas_dimensions_match_terminal(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(Terminal, "_get_terminal_dimensions", lambda _: (10, 10)) config = TerminalConfig() config.canvas_width = 0 config.canvas_height = 0 terminal = Terminal(input_data="test", config=config) assert terminal.canvas.width == 10 assert terminal.canvas.height == 10 def test_terminal_get_canvas_dimensions_match_input_text(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(Terminal, "_get_terminal_dimensions", lambda _: (10, 10)) config = TerminalConfig() config.canvas_width = -1 config.canvas_height = -1 terminal = Terminal(input_data="test", config=config) assert terminal.canvas.width == 4 assert terminal.canvas.height == 1 def test_terminal_get_canvas_dimensions_match_input_text_wrap_text(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(Terminal, "_get_terminal_dimensions", lambda _: (10, 10)) config = TerminalConfig() config.canvas_width = -1 config.canvas_height = -1 config.wrap_text = True terminal = Terminal(input_data="testtesttest", config=config) assert terminal.canvas.width == 10 assert terminal.canvas.height == 2 def test_get_terminal_dimensions_raise_oserror(monkeypatch: pytest.MonkeyPatch): def raise_oserror(): raise OSError config = TerminalConfig() terminal = Terminal(input_data="test", config=config) old_f = shutil.get_terminal_size monkeypatch.setattr(shutil, "get_terminal_size", raise_oserror) w, h = terminal._get_terminal_dimensions() monkeypatch.setattr(shutil, "get_terminal_size", old_f) # unpatch to avoid side effects in pytest output assert w == 80 assert h == 24 def test_terminal_get_piped_input_is_tty(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr("sys.stdin.isatty", lambda: True) assert Terminal.get_piped_input() == "" def test_terminal_get_piped_input_is_not_tty(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr("sys.stdin.isatty", lambda: False) monkeypatch.setattr("sys.stdin.read", lambda: "test") assert Terminal.get_piped_input() == "test" def test_terminal_wrap_lines(): config = TerminalConfig() terminal = Terminal(input_data="testtesttest", config=config) lines = terminal._wrap_lines(terminal._preprocessed_character_lines, 4) assert len(lines) == 3 def test_terminal_make_inner_fill_characters(): config = TerminalConfig() terminal = Terminal(input_data="test test test", config=config) assert len(terminal._inner_fill_characters) == 2 def test_terminal_make_outer_fill_characters(): config = TerminalConfig() config.canvas_width = 16 terminal = Terminal(input_data="test test test", config=config) assert len(terminal._outer_fill_characters) == 2 def test_terminal_add_character(): config = TerminalConfig() terminal = Terminal(input_data="test", config=config) terminal.add_character("a", Coord(0, 0)) assert len(terminal.get_characters(added_chars=True)) == 5 assert len(terminal._added_characters) == 1 assert terminal._added_characters[0].input_symbol == "a" @pytest.mark.parametrize( "sort", [Terminal.ColorSort.LEAST_TO_MOST, Terminal.ColorSort.MOST_TO_LEAST, Terminal.ColorSort.RANDOM], ) def test_terminal_get_input_colors(sort): config = TerminalConfig() input_data = "\x1b[38;2;255;0;0maaaaaaa\x1b[38;2;0;255;0mb\x1b[48;2;0;0;255mcccc" terminal = Terminal(input_data=input_data, config=config) colors = terminal.get_input_colors(sort=sort) if sort == Terminal.ColorSort.MOST_TO_LEAST: assert colors[0] == Color("FF0000") elif sort == Terminal.ColorSort.LEAST_TO_MOST: assert colors[0] == Color("0000FF") else: assert len(colors) == 3 def test_terminal_get_input_colors_no_colors(): config = TerminalConfig() input_data = "test" terminal = Terminal(input_data=input_data, config=config) colors = terminal.get_input_colors() assert len(colors) == 0 def test_terminal_get_input_colors_invalid_sort(): config = TerminalConfig() input_data = "\x1b[38;2;255;0;0maaaaaaa\x1b[38;2;0;255;0mb\x1b[48;2;0;0;255mcccc" terminal = Terminal(input_data=input_data, config=config) with pytest.raises(InvalidColorSortError): terminal.get_input_colors(sort="invalid") # type: ignore[arg-type] # testing invalid sort @pytest.mark.parametrize("input_chars", [True, False]) @pytest.mark.parametrize("inner_fill_chars", [True, False]) @pytest.mark.parametrize("outer_fill_chars", [True, False]) @pytest.mark.parametrize("added_chars", [True, False]) def test_terminal_get_characters(input_chars, inner_fill_chars, outer_fill_chars, added_chars): config = TerminalConfig() config.canvas_width = 6 config.canvas_height = 2 terminal = Terminal(input_data="abcd\nef gh", config=config) terminal.add_character("a", Coord(0, 0)) chars = terminal.get_characters( input_chars=input_chars, inner_fill_chars=inner_fill_chars, outer_fill_chars=outer_fill_chars, added_chars=added_chars, ) expected_chars = 0 if input_chars: expected_chars += 8 if inner_fill_chars: expected_chars += 2 if outer_fill_chars: expected_chars += 2 if added_chars: expected_chars += 1 assert len(chars) == expected_chars @pytest.mark.parametrize( "sort", [ Terminal.CharacterSort.BOTTOM_TO_TOP_LEFT_TO_RIGHT, Terminal.CharacterSort.OUTSIDE_ROW_TO_MIDDLE, Terminal.CharacterSort.BOTTOM_TO_TOP_RIGHT_TO_LEFT, Terminal.CharacterSort.MIDDLE_ROW_TO_OUTSIDE, Terminal.CharacterSort.TOP_TO_BOTTOM_LEFT_TO_RIGHT, Terminal.CharacterSort.TOP_TO_BOTTOM_RIGHT_TO_LEFT, Terminal.CharacterSort.RANDOM, ], ) def test_terminal_get_characters_with_character_sort(sort): config = TerminalConfig() terminal = Terminal(input_data="abcde\nfghij\nklmno", config=config) chars = terminal.get_characters(sort=sort) if sort == Terminal.CharacterSort.BOTTOM_TO_TOP_LEFT_TO_RIGHT: assert chars[0].input_symbol == "k" assert chars[-1].input_symbol == "e" elif sort == Terminal.CharacterSort.OUTSIDE_ROW_TO_MIDDLE: assert chars[0].input_symbol == "a" assert chars[-1].input_symbol == "h" elif sort == Terminal.CharacterSort.BOTTOM_TO_TOP_RIGHT_TO_LEFT: assert chars[0].input_symbol == "o" assert chars[-1].input_symbol == "a" elif sort == Terminal.CharacterSort.MIDDLE_ROW_TO_OUTSIDE: assert chars[0].input_symbol == "h" assert chars[-1].input_symbol == "a" elif sort == Terminal.CharacterSort.TOP_TO_BOTTOM_LEFT_TO_RIGHT: assert chars[0].input_symbol == "a" assert chars[-1].input_symbol == "o" elif sort == Terminal.CharacterSort.TOP_TO_BOTTOM_RIGHT_TO_LEFT: assert chars[0].input_symbol == "e" assert chars[-1].input_symbol == "k" else: assert len(chars) == 15 def test_terminal_get_characters_invalid_character_sort(): config = TerminalConfig() terminal = Terminal(input_data="abcde\nfghij\nklmno", config=config) with pytest.raises(InvalidCharacterSortError): terminal.get_characters(sort="invalid") # type: ignore[arg-type] # testing invalid sort @pytest.mark.parametrize("input_chars", [True, False]) @pytest.mark.parametrize("inner_fill_chars", [True, False]) @pytest.mark.parametrize("outer_fill_chars", [True, False]) @pytest.mark.parametrize("added_chars", [True, False]) def test_terminal_get_characters_grouped(input_chars, inner_fill_chars, outer_fill_chars, added_chars): config = TerminalConfig() config.canvas_width = 7 terminal = Terminal(input_data="abcde\nfg hij\nklmno", config=config) terminal.add_character("a", Coord(0, 0)) chars = terminal.get_characters_grouped( input_chars=input_chars, inner_fill_chars=inner_fill_chars, outer_fill_chars=outer_fill_chars, added_chars=added_chars, ) expected_string = "" for group in chars: expected_string += "".join([char.input_symbol for char in group]) expected_string_length = 0 if input_chars: expected_string_length += 15 if inner_fill_chars: expected_string_length += 3 if outer_fill_chars: expected_string_length += 3 if added_chars: expected_string_length += 1 assert len(expected_string) == expected_string_length @pytest.mark.parametrize( "grouping", [ Terminal.CharacterGroup.CENTER_TO_OUTSIDE_DIAMONDS, Terminal.CharacterGroup.COLUMN_LEFT_TO_RIGHT, Terminal.CharacterGroup.COLUMN_RIGHT_TO_LEFT, Terminal.CharacterGroup.ROW_TOP_TO_BOTTOM, Terminal.CharacterGroup.ROW_BOTTOM_TO_TOP, Terminal.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS, Terminal.CharacterGroup.DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT, Terminal.CharacterGroup.DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT, Terminal.CharacterGroup.DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT, Terminal.CharacterGroup.DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT, ], ) def test_terminal_get_characters_grouped_with_grouping(grouping): # test data: # abcde # fghij # klmno config = TerminalConfig() terminal = Terminal(input_data="abcde\nfghij\nklmno", config=config) terminal.add_character("a", Coord(0, 0)) chars = terminal.get_characters_grouped(grouping=grouping) if grouping == Terminal.CharacterGroup.CENTER_TO_OUTSIDE_DIAMONDS: assert chars[0][0].input_symbol == "h" assert chars[-1][-1].input_symbol == "e" elif grouping == Terminal.CharacterGroup.COLUMN_LEFT_TO_RIGHT: assert chars[0][0].input_symbol == "k" assert chars[-1][-1].input_symbol == "e" elif grouping == Terminal.CharacterGroup.COLUMN_RIGHT_TO_LEFT: assert chars[0][0].input_symbol == "o" assert chars[-1][-1].input_symbol == "a" elif grouping == Terminal.CharacterGroup.ROW_TOP_TO_BOTTOM: assert chars[0][0].input_symbol == "a" assert chars[-1][-1].input_symbol == "o" elif grouping == Terminal.CharacterGroup.ROW_BOTTOM_TO_TOP: assert chars[0][0].input_symbol == "k" assert chars[-1][-1].input_symbol == "e" elif grouping == Terminal.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS: assert chars[0][0].input_symbol == "k" assert chars[-1][-1].input_symbol == "h" elif grouping == Terminal.CharacterGroup.DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT: assert chars[0][0].input_symbol == "e" assert chars[-1][-1].input_symbol == "k" elif grouping == Terminal.CharacterGroup.DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT: assert chars[0][0].input_symbol == "a" assert chars[-1][-1].input_symbol == "o" elif grouping == Terminal.CharacterGroup.DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT: assert chars[0][0].input_symbol == "o" assert chars[-1][-1].input_symbol == "a" elif grouping == Terminal.CharacterGroup.DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT: assert chars[0][0].input_symbol == "k" assert chars[-1][-1].input_symbol == "e" def test_terminal_get_characters_grouped_invalid_grouping(): config = TerminalConfig() terminal = Terminal(input_data="abcde\nfghij\nklmno", config=config) with pytest.raises(InvalidCharacterGroupError): terminal.get_characters_grouped(grouping="invalid") # type: ignore[arg-type] # testing invalid group def test_terminal_get_character_by_input_coord_valid(): config = TerminalConfig() terminal = Terminal(input_data="abcd", config=config) char = terminal.get_character_by_input_coord(Coord(1, 1)) assert char is not None assert char.input_symbol == "a" def test_terminal_get_character_by_input_coord_invalid(): config = TerminalConfig() terminal = Terminal(input_data="abcd", config=config) char = terminal.get_character_by_input_coord(Coord(1, 2)) assert char is None @pytest.mark.parametrize("visiblity", [True, False]) def test_terminal_set_character_visibility(visiblity): config = TerminalConfig() terminal = Terminal(input_data="abcd", config=config) assert len(terminal._visible_characters) == 0 c = terminal.get_character_by_input_coord(Coord(1, 1)) terminal.set_character_visibility(c, visiblity) # type: ignore[arg-type] if visiblity: assert len(terminal._visible_characters) == 1 else: assert len(terminal._visible_characters) == 0 def test_terminal_get_formatted_output_string(): config = TerminalConfig() terminal = Terminal(input_data="abcd", config=config) output_string = terminal.get_formatted_output_string() assert output_string == " " terminal.set_character_visibility(terminal.get_character_by_input_coord(Coord(1, 1)), is_visible=True) # type: ignore[arg-type] output_string = terminal.get_formatted_output_string() assert output_string == "a " def test_terminal_update_terminal_state(): config = TerminalConfig() terminal = Terminal(input_data="abcd", config=config) terminal._update_terminal_state() assert terminal.terminal_state == [" "] terminal.set_character_visibility(terminal.get_character_by_input_coord(Coord(1, 1)), is_visible=True) # type: ignore[arg-type] terminal._update_terminal_state() assert terminal.terminal_state == ["a "] def test_terminal_prep_canvas(capsys): config = TerminalConfig() terminal = Terminal(input_data="abcd\nefgh\nijkl", config=config) terminal.prep_canvas() captured = capsys.readouterr() assert captured.out == "\x1b[?25l\n\n\n\x1b7" def test_terminal_restore_cursor(capsys): config = TerminalConfig() terminal = Terminal(input_data="abcd\nefgh\nijkl", config=config) terminal.restore_cursor() captured = capsys.readouterr() assert captured.out == "\x1b[?25h\n" def test_terminal_restore_cursor_end_symbol(capsys): config = TerminalConfig() terminal = Terminal(input_data="abcd\nefgh\nijkl", config=config) terminal.restore_cursor(end_symbol="test") captured = capsys.readouterr() assert captured.out == "\x1b[?25htest" def test_terminal_print(capsys): config = TerminalConfig() terminal = Terminal(input_data="abcd\nefgh\nijkl", config=config) terminal.print("abcd\nefgh\nijkl") captured = capsys.readouterr() assert captured.out == "\x1b8\x1b7\x1b[3Aabcd\nefgh\nijkl" def test_terminal_enforce_framerate(monkeypatch: pytest.MonkeyPatch): slept = False def patched_sleep(_): nonlocal slept slept = True config = TerminalConfig() terminal = Terminal(input_data="abcd\nefgh\nijkl", config=config) time.sleep(0.01) monkeypatch.setattr(time, "sleep", patched_sleep) terminal.enforce_framerate() assert not slept terminal.enforce_framerate() assert slept def test_terminal_move_cursor_to_top(capsys): config = TerminalConfig() terminal = Terminal(input_data="abcd\nefgh\nijkl", config=config) terminal.move_cursor_to_top() captured = capsys.readouterr() assert captured.out == "\x1b8\x1b7\x1b[3A" terminaltexteffects-release-0.12.1/tests/test_effects.py000066400000000000000000000047021507200677100234730ustar00rootroot00000000000000import pytest from terminaltexteffects.effects import effect_colorshift, effect_matrix @pytest.mark.smoke @pytest.mark.effects @pytest.mark.parametrize( "input_data", ["empty", "single_char", "single_column", "single_row", "medium", "tabs", "color_sequences"], indirect=True, ) def test_effect(effect, input_data, terminal_config_default_no_framerate) -> None: effect = effect(input_data) # customize some effect configs to shorten testing time if isinstance(effect, effect_matrix.Matrix): effect.effect_config.rain_time = 1 effect.terminal_config = terminal_config_default_no_framerate with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.smoke @pytest.mark.effects @pytest.mark.parametrize("input_data", ["medium", "color_sequences"], indirect=True) @pytest.mark.parametrize("existing_color_handling", ["always", "dynamic", "ignore"]) def test_effect_color_sequence_handling( effect, input_data, terminal_config_default_no_framerate, existing_color_handling, ) -> None: effect = effect(input_data) if isinstance(effect, effect_matrix.Matrix): effect.effect_config.rain_time = 1 effect.terminal_config = terminal_config_default_no_framerate effect.terminal_config.existing_color_handling = existing_color_handling with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.visual @pytest.mark.parametrize("input_data", ["large"], indirect=True) def test_effect_visual(effect, input_data) -> None: # customize some effect configs to shorten testing time or test # specific features effect = effect(input_data) if isinstance(effect, effect_matrix.Matrix): effect.effect_config.rain_time = 5 if isinstance(effect, effect_colorshift.ColorShift): effect.effect_config.travel = True effect.effect_config.cycles = 2 with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) @pytest.mark.skip @pytest.mark.manual @pytest.mark.parametrize("input_data", ["canvas"], indirect=True) def test_canvas_anchoring_large_small_canvas(input_data, effect, terminal_config_with_anchoring) -> None: effect = effect(input_data) effect.terminal_config = terminal_config_with_anchoring with effect.terminal_output() as terminal: for frame in effect: terminal.print(frame) terminaltexteffects-release-0.12.1/tests/testinput/000077500000000000000000000000001507200677100224775ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/tests/testinput/canvas.txt000066400000000000000000000012111507200677100245060ustar00rootroot00000000000000TL!!!!!!!!!!!!!!!!!!!!!TOP*********************TR + <----50----> # + | # + | # L ^ | R E | | I F MID 13 | CENTER MID G T | | H - v | T - | @ - | @ - | @ BL--------------------BOTTOM...................BRterminaltexteffects-release-0.12.1/tests/testinput/color_sequence_test.txt000066400000000000000000000070041507200677100273060ustar00rootroot00000000000000........ | ggggggg ggggggg :gggggg; ggggggg ggggggg :gggggg; ggggggg ;gggggg ggggggg | @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ [@@@@@@ @@@@@@g | @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ [@@@@@@ @@@@@@g | BBBBBBB BBBBBBB !BBBBBB" BBBBBBB BBBBBBB !BBBBBB" BBBBBBB "BBBBBB BBBBBBN | ggggggg ggggggg :gg gg; ggggggg ggggggg :gggggg; ggggggg ;gggggg ggggggg | @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ [@@@@@@ @@@@@@g | @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ [@@@@@@ @@@@@@g | BBBBBBB BBBBBBB !BBBBBB" BBBBBBB BBBBBBB !BBBBBB" BBBBBBB "BBBBBB BBBBBBN | ggggggg ggggggg :gggggg; ggggggg ggggggg :gggggg; ggggggg ;gggggg ggggggg | @@@@@@@ @@@@@@@ |@@@@@@| @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ [@@@@@@ @@@@@@g | @@@@@@@ @@@@@@@ |@@@@@@| @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ [@@@@@@ @@@@@@g | ======= ======= !BBBBBB! BBBBBBB BBBBBBB '======" ======= "====== BBBBBB8 | ggggggg ggggggg :gggggg; ggggggg ggggggg :gggggg; ggggggg ggggggg ggggggg | @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ @@@@@@@ @@@@@@g | @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ @@@@@@@ @@@@@@g | BBBBBBB BBBBBBB !BBBBBB" BBBBBBB BBBBBBB !BBBBBB" BBBBBBB BBBBBBB BBBBBBN | ggggggg ggggggg :gggggg; ggggggg ggggggg :gggggg; ggggggg ;gggggg gggggg; | @@@@@@@ @@@@@@@ |@@@@@@| @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ [@@@@@@ @@@@@@] | @@@@@@@ @@@@@@@ |@@@@@@| @@@@@@@ @@@@@@@ |@@@@@@] @@@@@@@ [@@@@@@ @@@@@@] | BBBBBBB ======= !BBBBBB! ======= 8BBBBBB !BBBBBB" BBBBBBB "BBBBBB ======" terminaltexteffects-release-0.12.1/tests/testinput/demo.txt000066400000000000000000000030361507200677100241660ustar00rootroot00000000000000 _______ _______ _______ (_______|_______|_______) _ _ _____ | | | | | ___) | | | | | |_____ |_| |_| |_______) ========================== TerminalTextEffects applies visual effects to text in the terminal. The TTE animation engine has the following features: * Xterm 256 / RGB hex color support * Complex character movement via Paths, Waypoints, and motion easing. * Complex animations via Scenes with symbol/color changes, layers, easing, and Path synced progression. * Event handling for Path/Scene state changes with custom callback support and many pre-defined actions. * Variable stop/step color gradient generation. * Extensive effect customization via per-effect arguments. * Runs inline, preserving terminal state and workflow. Installation: * pip install TerminalTextEffects * pipx install TerminalTextEffects More Info: https://github.com/ChrisBuilds/terminaltexteffects https://pypi.org/project/terminaltexteffects/terminaltexteffects-release-0.12.1/tests/testinput/fullscreen_text.txt000066400000000000000000000252501507200677100264520ustar00rootroot00000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffgggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZterminaltexteffects-release-0.12.1/tests/testinput/modify.txt000066400000000000000000000003511507200677100245260ustar00rootroot00000000000000Testing Input Top Line Test Input Second Line Test Input Third Line Test Input Fourth Line Test Input Bottom Lineterminaltexteffects-release-0.12.1/tests/testinput/single_char.txt000066400000000000000000000000011507200677100255050ustar00rootroot00000000000000aterminaltexteffects-release-0.12.1/tests/testinput/small_text.txt000066400000000000000000000000241507200677100254100ustar00rootroot00000000000000012345 6789ab cdefghterminaltexteffects-release-0.12.1/tests/testinput/solid.txt000066400000000000000000000076131507200677100243610ustar00rootroot00000000000000██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████ ██████████████████████████████████████████████████████████████████terminaltexteffects-release-0.12.1/tests/testinput/sparse_text.txt000066400000000000000000000003511507200677100256000ustar00rootroot00000000000000a b c d e f g h i j k l 1 2 3 4 5 6 7 8terminaltexteffects-release-0.12.1/tests/testinput/square.txt000066400000000000000000000001321507200677100245340ustar00rootroot00000000000000000000000000 000000000000 000000000000 000000000000 000000000000 000000000000 X00000000000terminaltexteffects-release-0.12.1/tests/testinput/tall_text.txt000066400000000000000000000002131507200677100252340ustar00rootroot000000000000000 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 49terminaltexteffects-release-0.12.1/tests/testinput/titles.txt000066400000000000000000000125021507200677100245440ustar00rootroot00000000000000 _________ _______ ___ ___ _________ _______ ________ ________ _______ ________ _________ ________ |\___ ___\\ ___ \ |\ \ / /|\___ ___\ |\ ___ \ |\ _____\\ _____\\ ___ \ |\ ____\\___ ___\\ ____\ \|___ \ \_\ \ __/| \ \ \/ / ||___ \ \_| \ \ __/|\ \ \__/\ \ \__/\ \ __/|\ \ \___\|___ \ \_\ \ \___|_ \ \ \ \ \ \_|/__ \ \ / / \ \ \ \ \ \_|/_\ \ __\\ \ __\\ \ \_|/_\ \ \ \ \ \ \ \_____ \ \ \ \ \ \ \_|\ \ / \/ \ \ \ \ \ \_|\ \ \ \_| \ \ \_| \ \ \_|\ \ \ \____ \ \ \ \|____|\ \ \ \__\ \ \_______\/ /\ \ \ \__\ \ \_______\ \__\ \ \__\ \ \_______\ \_______\ \ \__\ ____\_\ \ \|__| \|_______/__/ /\ __\ \|__| \|_______|\|__| \|__| \|_______|\|_______| \|__| |\_________\ |__|/ \|__| \|_________| ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ ▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░░░░░░░░░░░▌ ▐░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌ ▐░░░░░░░░░░░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ ▀▀▀▀▀▀▀▀▀█░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▄▄▄▄▄▄▄▄▄█░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ▐░▌ ▐░░░░░░░░░░░▌ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ::::::::::: :::::::::: ::: ::: ::::::::::: :::::::::: :::::::::: :::::::::: :::::::::: :::::::: ::::::::::: :::::::: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +#+ +#++:++# +#++:+ +#+ +#++:++# :#::+::# :#::+::# +#++:++# +#+ +#+ +#++:++#++ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# ### ########## ### ### ### ########## ### ### ########## ######## ### ######## terminaltexteffects-release-0.12.1/tests/testinput/two_char.txt000066400000000000000000000001021507200677100250370ustar00rootroot00000000000000a bterminaltexteffects-release-0.12.1/tests/testinput/wide_text.txt000066400000000000000000000004351507200677100252360ustar00rootroot00000000000000This is a very long line. This is a very long line. This is a very long line. This is a very long line. This is a very long line. This is a very long line. This is a very long line. This is a very long line. This is a very long line. This is a very long line. This is a very long line.terminaltexteffects-release-0.12.1/tests/utils_tests/000077500000000000000000000000001507200677100230225ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/tests/utils_tests/__init__.py000066400000000000000000000000001507200677100251210ustar00rootroot00000000000000terminaltexteffects-release-0.12.1/tests/utils_tests/test_ansitools.py000066400000000000000000000045361507200677100264560ustar00rootroot00000000000000import pytest from terminaltexteffects.utils import ansitools pytestmark = [pytest.mark.utils, pytest.mark.smoke] @pytest.mark.parametrize("escape", ["\033[", "\x1b["]) @pytest.mark.parametrize("position", ["38;2", "48;2"]) def test_parse_ansi_color_sequence_24_bit(escape, position): assert ansitools.parse_ansi_color_sequence(f"{escape}{position};255;255;255m") == "FFFFFF" assert ansitools.parse_ansi_color_sequence(f"{escape}{position};") == "00" assert ansitools.parse_ansi_color_sequence(f"{escape}{position};255;;255m") == "FF00FF" @pytest.mark.parametrize("escape", ["\033[", "\x1b["]) @pytest.mark.parametrize("position", ["38;5", "48;5"]) def test_parse_ansi_color_sequence_8_bit(escape, position): assert ansitools.parse_ansi_color_sequence(f"{escape}{position};255m") == 255 assert ansitools.parse_ansi_color_sequence(f"{escape}{position};128") == 128 @pytest.mark.parametrize("escape", ["\032[", "\x2b["]) @pytest.mark.parametrize("position", ["37;5", "49;5"]) def test_parse_ansi_color_sequence_invalid(escape, position): with pytest.raises(ValueError): ansitools.parse_ansi_color_sequence(f"{escape}{position};255;255;255m") def test_DEC_SAVE_CURSOR_POSITION(): assert ansitools.dec_save_cursor_position() == "\0337" def test_DEC_RESTORE_CURSOR_POSITION(): assert ansitools.dec_restore_cursor_position() == "\0338" def test_HIDE_CURSOR(): assert ansitools.hide_cursor() == "\033[?25l" def test_SHOW_CURSOR(): assert ansitools.show_cursor() == "\033[?25h" def test_MOVE_CURSOR_UP(): assert ansitools.move_cursor_up(5) == "\033[5A" def test_MOVE_CURSOR_TO_COLUMN(): assert ansitools.move_cursor_to_column(5) == "\033[5G" def test_RESET_ALL(): assert ansitools.reset_all() == "\033[0m" def test_APPLY_BOLD(): assert ansitools.apply_bold() == "\033[1m" def test_APPLY_DIM(): assert ansitools.apply_dim() == "\033[2m" def test_APPLY_ITALIC(): assert ansitools.apply_italic() == "\033[3m" def test_APPLY_UNDERLINE(): assert ansitools.apply_underline() == "\033[4m" def test_APPLY_BLINK(): assert ansitools.apply_blink() == "\033[5m" def test_APPLY_REVERSE(): assert ansitools.apply_reverse() == "\033[7m" def test_APPLY_HIDDEN(): assert ansitools.apply_hidden() == "\033[8m" def test_APPLY_STRIKETHROUGH(): assert ansitools.apply_strikethrough() == "\033[9m" terminaltexteffects-release-0.12.1/tests/utils_tests/test_argvalidators.py000066400000000000000000000122201507200677100272720ustar00rootroot00000000000000from argparse import ArgumentTypeError import pytest from terminaltexteffects.utils import argvalidators, easing from terminaltexteffects.utils.graphics import Color, Gradient pytestmark = [pytest.mark.utils, pytest.mark.smoke] def test_postive_int_valid_int(): assert argvalidators.PositiveInt.type_parser("1") == 1 @pytest.mark.parametrize("arg", ["-1", "0", "1.1", "a"]) def test_postive_int_invalid_int(arg): with pytest.raises(ArgumentTypeError): argvalidators.PositiveInt.type_parser(arg) def test_non_negative_int_valid_int(): assert argvalidators.NonNegativeInt.type_parser("0") == 0 @pytest.mark.parametrize("arg", ["-1", "1.1", "a"]) def test_non_negative_int_invalid_int(arg): with pytest.raises(ArgumentTypeError): argvalidators.NonNegativeInt.type_parser(arg) def test_positive_int_range_valid_range(): assert argvalidators.PositiveIntRange.type_parser("1-10") == (1, 10) @pytest.mark.parametrize("arg", ["-1-10", "1.1-10", "a-10", "1-10.1", "1-a", "2-1", "0-3"]) def test_positive_int_range_invalid_range(arg): with pytest.raises(ArgumentTypeError): argvalidators.PositiveIntRange.type_parser(arg) def test_positive_float_valid_float(): assert argvalidators.PositiveFloat.type_parser("1.1") == 1.1 @pytest.mark.parametrize("arg", ["-1.1", "0", "a"]) def test_positive_float_invalid_float(arg): with pytest.raises(ArgumentTypeError): argvalidators.PositiveFloat.type_parser(arg) def test_non_negative_float_valid_float(): assert argvalidators.NonNegativeFloat.type_parser("0") == 0 assert argvalidators.NonNegativeFloat.type_parser("1.1") == 1.1 @pytest.mark.parametrize("arg", ["-1.1", "a"]) def test_non_negative_float_invalid_float(arg): with pytest.raises(ArgumentTypeError): argvalidators.NonNegativeFloat.type_parser(arg) def test_positive_float_range_valid_range(): assert argvalidators.PositiveFloatRange.type_parser("1.1-10.1") == (1.1, 10.1) @pytest.mark.parametrize("arg", ["-1.1-10.1", "a-10.1", "1.1-10.1.1", "1.1-a", "2.1-1.1", "0-3"]) def test_positive_float_range_invalid_range(arg): with pytest.raises(ArgumentTypeError): argvalidators.PositiveFloatRange.type_parser(arg) def test_NonNegativeRatio_valid_ratio(): assert argvalidators.NonNegativeRatio.type_parser("0.5") == 0.5 assert argvalidators.NonNegativeRatio.type_parser("1") == 1 assert argvalidators.NonNegativeRatio.type_parser("0") == 0 @pytest.mark.parametrize("arg", ["-1", "1.1", "a"]) def test_NonNegativeRatio_invalid_ratio(arg): with pytest.raises(ArgumentTypeError): argvalidators.NonNegativeRatio.type_parser(arg) def test_PositiveRatio_valid_ratio(): assert argvalidators.PositiveRatio.type_parser("0.5") == 0.5 assert argvalidators.PositiveRatio.type_parser("1.0") == 1 assert argvalidators.PositiveRatio.type_parser("0.01") == 0.01 @pytest.mark.parametrize("arg", ["-1", "1.1", "0", "a"]) def test_PositiveRatio_invalid_ratio(arg): with pytest.raises(ArgumentTypeError): argvalidators.PositiveRatio.type_parser(arg) def test_gradient_direction_valid_direction(): assert argvalidators.GradientDirection.type_parser("horizontal") == Gradient.Direction.HORIZONTAL assert argvalidators.GradientDirection.type_parser("vertical") == Gradient.Direction.VERTICAL def test_gradient_direction_invalid_direction(): with pytest.raises(ArgumentTypeError): argvalidators.GradientDirection.type_parser("invalid") def test_color_arg_valid_color(): assert argvalidators.ColorArg.type_parser("125") == Color(125) assert argvalidators.ColorArg.type_parser("ffffff") == Color("ffffff") @pytest.mark.parametrize("arg", ["-1", "256", "ffffzz", "aaa"]) def test_color_arg_invalid_color(arg): with pytest.raises(ArgumentTypeError): argvalidators.ColorArg.type_parser(arg) def test_symbol_valid_symbol(): assert argvalidators.Symbol.type_parser("a") == "a" @pytest.mark.parametrize("arg", ["", "aa"]) def test_symbol_invalid_symbol(arg): with pytest.raises(ArgumentTypeError): argvalidators.Symbol.type_parser(arg) def test_canvas_dimensions_valid_dimension(): assert argvalidators.CanvasDimension.type_parser("0") == 0 assert argvalidators.CanvasDimension.type_parser("1") == 1 assert argvalidators.CanvasDimension.type_parser("-1") == -1 @pytest.mark.parametrize("arg", ["-2", "a", "1.1"]) def test_canvas_dimensions_invalid_dimension(arg): with pytest.raises(ArgumentTypeError): argvalidators.CanvasDimension.type_parser(arg) def test_terminal_dimension_valid_dimension(): assert argvalidators.TerminalDimension.type_parser("0") == 0 assert argvalidators.TerminalDimension.type_parser("1") == 1 @pytest.mark.parametrize("arg", ["a", "1.1", "-1"]) def test_terminal_dimension_invalid_dimension(arg): with pytest.raises(ArgumentTypeError): argvalidators.TerminalDimension.type_parser(arg) def test_ease_valid_ease(): assert argvalidators.Ease.type_parser("linear") == easing.linear assert argvalidators.Ease.type_parser("in_sine") == easing.in_sine def test_ease_invalid_ease(): with pytest.raises(ArgumentTypeError): argvalidators.Ease.type_parser("invalid") terminaltexteffects-release-0.12.1/tests/utils_tests/test_colorterm.py000066400000000000000000000023141507200677100264410ustar00rootroot00000000000000import pytest from terminaltexteffects.utils import colorterm pytestmark = [pytest.mark.utils, pytest.mark.smoke] def test_fg_hex_with_hash(): assert colorterm.fg("#ffffff") == "\x1b[38;2;255;255;255m" def test_fg_hex(): assert colorterm.fg("ffffff") == "\x1b[38;2;255;255;255m" def test_fg_xterm(): assert colorterm.fg(255) == "\x1b[38;5;255m" def test_fg_invalid_hex(): with pytest.raises(ValueError): colorterm.fg("fgffff") def test_fg_invalid_xterm(): with pytest.raises(ValueError): colorterm.fg(256) def test_fg_invalid_type(): with pytest.raises(TypeError): colorterm.fg(3.14) # type: ignore[arg-type] def test_bg_hex_with_hash(): assert colorterm.bg("#ffffff") == "\x1b[48;2;255;255;255m" def test_bg_hex(): assert colorterm.bg("ffffff") == "\x1b[48;2;255;255;255m" def test_bg_xterm(): assert colorterm.bg(255) == "\x1b[48;5;255m" def test_bg_invalid_hex(): with pytest.raises(ValueError): colorterm.bg("fgffff") def test_bg_invalid_xterm(): with pytest.raises(ValueError): colorterm.bg(256) def test_bg_invalid_type(): with pytest.raises(TypeError): colorterm.bg(3.14) # type: ignore[arg-type] terminaltexteffects-release-0.12.1/tests/utils_tests/test_easing.py000066400000000000000000000017621507200677100257070ustar00rootroot00000000000000import pytest from terminaltexteffects.utils.easing import eased_step_function pytestmark = [pytest.mark.utils, pytest.mark.smoke] def test_ease_valid_progress(easing_function_1): assert round(easing_function_1(0)) == 0 assert round(easing_function_1(1)) == 1 @pytest.mark.parametrize("progress", [n / 10 for n in range(1, 11)]) def test_ease_progress_ratios(progress, easing_function_1): easing_function_1(progress) # should not raise an exception @pytest.mark.parametrize("clamp", [True, False]) def test_eased_step_function(easing_function_1, clamp): f = eased_step_function(easing_function_1, 0.01, clamp=clamp) used_step = 0 while used_step < 1: used_step, eased_value = f() if clamp: assert 0 <= eased_value <= 1 @pytest.mark.parametrize("step_size", [-1, 0, 1.1]) def test_eased_step_function_invalid_step_size(easing_function_1, step_size): with pytest.raises(ValueError): _ = eased_step_function(easing_function_1, step_size) terminaltexteffects-release-0.12.1/tests/utils_tests/test_geometry.py000066400000000000000000000106301507200677100262660ustar00rootroot00000000000000from __future__ import annotations import pytest from terminaltexteffects.utils import geometry pytestmark = [pytest.mark.utils, pytest.mark.smoke] @pytest.fixture def coord(): return geometry.Coord(1, 2) def test_coord_init(coord: geometry.Coord): assert coord.column == 1 assert coord.row == 2 def test_coord_equalities(coord): coord1 = geometry.Coord(1, 2) assert coord1 == coord def test_find_coords_on_circle_coords_limit(coord): coords = geometry.find_coords_on_circle(coord, 5, 5, unique=False) assert len(coords) == 5 def test_find_coords_on_circle_zero_radius(coord): coords = geometry.find_coords_on_circle(coord, 0, 5, unique=False) assert len(coords) == 0 def test_find_coords_on_circle_unique(coord): coords = geometry.find_coords_on_circle(coord, 5, 0, unique=True) assert len(set(coords)) == len(coords) def test_find_coords_in_circle(coord): coords = geometry.find_coords_in_circle(coord, 5) assert len(coords) > 0 def test_find_coords_in_circle_zero_radius(coord): coords = geometry.find_coords_in_circle(coord, 0) assert len(coords) == 0 def test_find_coords_in_rect(coord): coords = geometry.find_coords_in_rect(coord, 5) assert len(coords) > 0 def test_find_coords_in_rect_zero_width(coord): coords = geometry.find_coords_in_rect(coord, 0) assert len(coords) == 0 def test_find_coord_at_distance(coord): new_coord = geometry.Coord(coord.column + 5, coord.row + 5) coord_at_distance = geometry.find_coord_at_distance(coord, new_coord, 3) # verify the coord returned is further away from the target coord assert coord_at_distance == geometry.Coord(8, 9) def test_find_coord_at_distance_zero_distance(coord): coord_at_distance = geometry.find_coord_at_distance(coord, coord, 0) assert coord_at_distance.column == coord.column and coord_at_distance.row == coord.row def test_find_coord_on_bezier_curve(): start = geometry.Coord(0, 0) end = geometry.Coord(10, 10) control = geometry.Coord(5, 0) coord_on_curve = geometry.find_coord_on_bezier_curve(start, (control,), end, 0.5) assert coord_on_curve == geometry.Coord(5, 2) def test_find_coord_on_bezier_curve_two_control_points(): start = geometry.Coord(0, 0) end = geometry.Coord(10, 10) control1 = geometry.Coord(5, 0) control2 = geometry.Coord(5, 10) # verify a Coord is returned and no exception is raised assert isinstance(geometry.find_coord_on_bezier_curve(start, (control1, control2), end, 0.5), geometry.Coord) def test_find_coord_on_line(): start = geometry.Coord(0, 0) end = geometry.Coord(10, 10) coord = geometry.find_coord_on_line(start, end, 0.5) assert coord.column == 5 and coord.row == 5 def test_find_length_of_bezier_curve(): start = geometry.Coord(0, 0) end = geometry.Coord(10, 10) control = geometry.Coord(5, 0) length = geometry.find_length_of_bezier_curve(start, end, control) assert length == 12.307135789365265 def test_find_length_of_bezier_curve_two_control_points(): start = geometry.Coord(0, 0) end = geometry.Coord(10, 10) control1 = geometry.Coord(5, 0) control2 = geometry.Coord(5, 10) length = geometry.find_length_of_bezier_curve(start, (control1, control2), end) assert length == 13.957417329238151 def test_find_length_of_line(): start = geometry.Coord(0, 0) end = geometry.Coord(10, 10) length = geometry.find_length_of_line(start, end) assert length == 14.142135623730951 def test_find_length_of_line_double_row_diff(): start = geometry.Coord(0, 0) end = geometry.Coord(0, 10) length = geometry.find_length_of_line(start, end, double_row_diff=True) assert length == 20 def test_find_normalized_distance_from_center(): coord = geometry.Coord(3, 3) distance = geometry.find_normalized_distance_from_center(1, 10, 1, 10, coord) assert distance == 0.4 def test_find_normalized_distance_from_center_with_offset(): coord = geometry.Coord(6, 6) distance = geometry.find_normalized_distance_from_center(4, 13, 4, 13, coord) assert distance == 0.4 def test_find_normalized_distance_from_center_out_of_bounds(): coord = geometry.Coord(1, 1) with pytest.raises(ValueError): geometry.find_normalized_distance_from_center(4, 13, 4, 13, coord) coord = geometry.Coord(14, 14) with pytest.raises(ValueError): geometry.find_normalized_distance_from_center(4, 13, 4, 13, coord) terminaltexteffects-release-0.12.1/tests/utils_tests/test_gradient.py000066400000000000000000000205571507200677100262410ustar00rootroot00000000000000import pytest from terminaltexteffects.engine.motion import Coord from terminaltexteffects.utils.graphics import Color, ColorPair, Gradient, random_color pytestmark = [pytest.mark.utils, pytest.mark.smoke] def test_random_color() -> None: assert isinstance(random_color(), Color) def test_color_pair_init() -> None: cp = ColorPair(fg="ffffff", bg="000000") assert cp.fg_color == Color("ffffff") assert cp.bg_color == Color("000000") def test_color_pair_init_single_color() -> None: cp = ColorPair(fg="ffffff") assert cp.fg_color == Color("ffffff") assert cp.bg_color is None def test_gradient_zero_stops() -> None: with pytest.raises(ValueError): Gradient() def test_gradient_zero_steps() -> None: with pytest.raises(ValueError): Gradient(Color("ffffff"), steps=0) def test_gradient_zero_steps_tuple() -> None: with pytest.raises(ValueError): Gradient(Color("ffffff"), Color("000000"), Color("ff0000"), steps=(1, 0)) def test_gradient_slice() -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4) assert g[0] == Color("ffffff") assert g[-1] == Color("000000") assert g[1:3] == [Color("bfbfbf"), Color("7f7f7f")] def test_gradient_iter() -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4) for color in g: assert isinstance(color, Color) def test_gradient_str() -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4) assert "Stops(ffffff, 000000)" in str(g) def test_gradient_len() -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4) assert len(g) == 5 def test_gradient_length_single_color() -> None: g = Gradient(Color("ffffff"), steps=5) assert len(g.spectrum) == 5 def test_gradient_length_two_colors() -> None: g = Gradient(Color("000000"), Color("ffffff"), steps=5) assert len(g.spectrum) == 6 def test_gradient_length_three_colors() -> None: g = Gradient(Color("000000"), Color("ffffff"), Color("000000"), steps=5) assert len(g.spectrum) == 11 def test_gradient_length_same_color_multiple_times() -> None: g = Gradient(Color("ffffff"), Color("ffffff"), Color("ffffff"), Color("ffffff"), steps=4) assert len(g.spectrum) == 13 def test_gradient_length_same_color_multiple_times_with_tuple_steps() -> None: g = Gradient(Color("ffffff"), Color("ffffff"), Color("ffffff"), Color("ffffff"), steps=(4, 6)) assert len(g.spectrum) == 17 def test_gradient_single_color() -> None: g = Gradient(Color("ffffff"), steps=5) assert all(color == Color("ffffff") for color in g.spectrum) def test_gradient_two_colors() -> None: g = Gradient(Color("000000"), Color("ffffff"), steps=3) assert g.spectrum[0] == Color("000000") and g.spectrum[-1] == Color("ffffff") def test_gradient_single_step() -> None: g = Gradient(Color("ffffff"), steps=1) assert g.spectrum[0] == Color("ffffff") def test_gradient_three_colors() -> None: g = Gradient(Color("ffffff"), Color("000000"), Color("ffffff"), steps=4) assert g.spectrum[0] == Color("ffffff") and g.spectrum[4] == Color("000000") and g.spectrum[-1] == Color("ffffff") def test_gradient_loop() -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4, loop=True) assert g.spectrum[-1] == Color("ffffff") def test_gradient_get_color_at_fraction() -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4) assert g.get_color_at_fraction(0) == Color("ffffff") assert g.get_color_at_fraction(0.5) == Color("7f7f7f") assert g.get_color_at_fraction(1) == Color("000000") def test_gradient_get_color_at_fraction_invalid_float() -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4) with pytest.raises(ValueError): g.get_color_at_fraction(1.1) @pytest.mark.parametrize( "direction", [ Gradient.Direction.DIAGONAL, Gradient.Direction.HORIZONTAL, Gradient.Direction.VERTICAL, Gradient.Direction.RADIAL, ], ) def test_gradient_build_coordinate_color_mapping(direction) -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4) coordinate_map = g.build_coordinate_color_mapping(1, 10, 1, 10, direction) if direction == Gradient.Direction.DIAGONAL: assert coordinate_map[Coord(1, 1)] == Color("ffffff") assert coordinate_map[Coord(10, 10)] == Color("000000") elif direction == Gradient.Direction.HORIZONTAL: assert coordinate_map[Coord(1, 1)] == Color("ffffff") assert coordinate_map[Coord(10, 1)] == Color("000000") elif direction == Gradient.Direction.VERTICAL: assert coordinate_map[Coord(1, 1)] == Color("ffffff") assert coordinate_map[Coord(1, 10)] == Color("000000") elif direction == Gradient.Direction.RADIAL: assert coordinate_map[Coord(5, 5)] == Color("ffffff") assert coordinate_map[Coord(10, 10)] == Color("000000") @pytest.mark.parametrize( "direction", [ Gradient.Direction.DIAGONAL, Gradient.Direction.HORIZONTAL, Gradient.Direction.VERTICAL, Gradient.Direction.RADIAL, ], ) @pytest.mark.parametrize("min_column", [1, 5]) @pytest.mark.parametrize("max_column", [5, 10]) @pytest.mark.parametrize("min_row", [1, 5]) @pytest.mark.parametrize("max_row", [5, 10]) def test_gradient_build_coordinate_color_mapping_no_exceptions( direction, min_column, max_column, min_row, max_row, ) -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4) if min_column > max_column or min_row > max_row: with pytest.raises(ValueError): coordinate_map = g.build_coordinate_color_mapping(min_row, max_row, min_column, max_column, direction) else: # check for exceptions across single row/column and issue that might arise from math calculations coordinate_map = g.build_coordinate_color_mapping(min_row, max_row, min_column, max_column, direction) def test_gradient_build_coordinate_color_mapping_single_row() -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4) coordinate_map = g.build_coordinate_color_mapping(1, 1, 1, 10, Gradient.Direction.HORIZONTAL) assert coordinate_map[Coord(1, 1)] == Color("ffffff") assert coordinate_map[Coord(10, 1)] == Color("000000") def test_gradient_build_coordinate_color_mapping_single_column() -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4) coordinate_map = g.build_coordinate_color_mapping(1, 10, 1, 1, Gradient.Direction.VERTICAL) assert coordinate_map[Coord(1, 1)] == Color("ffffff") assert coordinate_map[Coord(1, 10)] == Color("000000") def test_gradient_build_coordinate_color_mapping_single_row_column() -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4) coordinate_map = g.build_coordinate_color_mapping(1, 1, 1, 1, Gradient.Direction.HORIZONTAL) assert coordinate_map[Coord(1, 1)] == Color("000000") def test_gradient_build_coordinate_color_mapping_invalid_row_column() -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4) with pytest.raises(ValueError): g.build_coordinate_color_mapping(0, 10, 0, 10, Gradient.Direction.HORIZONTAL) with pytest.raises(ValueError): g.build_coordinate_color_mapping(10, 0, 10, 0, Gradient.Direction.HORIZONTAL) def test_gradient_build_coordinate_color_mapping_max_less_than_min() -> None: g = Gradient(Color("ffffff"), Color("000000"), steps=4) with pytest.raises(ValueError): g.build_coordinate_color_mapping(10, 1, 10, 1, Gradient.Direction.HORIZONTAL) def test_color_invalid_xterm_color() -> None: with pytest.raises(ValueError): Color(256) def test_color_invalid_hex_color() -> None: with pytest.raises(ValueError): Color("ffffzz") def test_color_valid_hex_with_hash(): assert Color("#ffffff") == Color("ffffff") def test_color_hex_rgb_ints(): assert Color("000000").rgb_ints == (0, 0, 0) def test_color_xterm_rgb_ints(): assert Color(0).rgb_ints == (0, 0, 0) def test_color_not_equal(): assert Color("ffffff") != Color("000000") def test_color_not_equal_different_types(): assert Color("ffffff") != 0 assert Color(0) != "ffffff" def test_color_is_hashable(): hash(Color("ffffff")) hash(Color(0)) def test_color_is_iterable(): assert list(Color("ffffff")) == [Color("ffffff")] def test_color_repr(): assert repr(Color("ffffff")) == "Color('ffffff')" def test_color_str(): assert "Color Code: ffffff" in str(Color("ffffff")) terminaltexteffects-release-0.12.1/tests/utils_tests/test_hexterm.py000066400000000000000000000046771507200677100261250ustar00rootroot00000000000000"""Unit tests for the hexterm module in terminaltexteffects.utils. This module tests the following functions: - hexterm.hex_to_xterm: * Validates the conversion from hexadecimal color codes to xterm color indices. * Ensures that invalid hexadecimal strings raise a ValueError. - hexterm.xterm_to_hex: * Validates the conversion from xterm color indices to hexadecimal color codes. * Ensures that invalid xterm color indices raise a ValueError. - hexterm.is_valid_color: * Checks whether a provided value (hexadecimal string or xterm index) is a valid color. * The function returns True for valid colors and False for invalid inputs. Tests are marked with 'utils' and 'smoke' pytest markers to classify them appropriately. """ import pytest from terminaltexteffects.utils import hexterm pytestmark = [pytest.mark.utils, pytest.mark.smoke] def test_hex_to_xterm() -> None: """Test conversion from hex to xterm colors.""" assert hexterm.hex_to_xterm("#ffffff") == 15 def test_hex_to_xterm_invalid_hex_chars() -> None: """Test hex_to_xterm with invalid hexadecimal characters.""" with pytest.raises(ValueError): # noqa: PT011 hexterm.hex_to_xterm("zzzzzz") def test_xterm_to_hex() -> None: """Test conversion from xterm to hex colors.""" assert hexterm.xterm_to_hex(1) == "800000" def test_xterm_to_hex_invalid_xterm() -> None: """Test that converting an invalid xterm color raises a ValueError.""" with pytest.raises(ValueError): # noqa: PT011 hexterm.xterm_to_hex(256) def test_is_valid_color_valid_hex_color() -> None: """Test that a valid hexadecimal color is recognized as valid.""" assert hexterm.is_valid_color("#ffffff") is True def test_is_valid_color_valid_xterm_color() -> None: """Test that a valid xterm color is recognized as valid.""" assert hexterm.is_valid_color(255) is True def test_is_valid_color_invalid_hex_color_chars() -> None: """Test that invalid hexadecimal color characters are recognized as invalid.""" assert hexterm.is_valid_color("#zzzzzz") is False def test_is_valid_color_invalid_hex_length() -> None: """Test that an invalid hexadecimal color length is recognized as invalid.""" assert hexterm.is_valid_color("ff") is False def test_is_valid_color_invalid_xterm_color() -> None: """Test that an invalid xterm color is recognized as invalid.""" assert hexterm.is_valid_color(256) is False terminaltexteffects-release-0.12.1/tox.ini000066400000000000000000000003231507200677100206070ustar00rootroot00000000000000[tox] envlist = py38, py39, py310, py311, py312 isolated_build = True skip_missing_interpreters = True [testenv] deps = pytest pytest-cov pytest-randomly pytest-xdist commands = pytest -m smoke -n=auto