pytermgui.animations

All animation-related classes & functions.

The biggest exports are Animation and its subclasses, as well as Animator. A global instance of Animator is also exported, under the animator name.

These can be used both within a WindowManager context (where stepping is done automatically by the pytermgui.window_manager.Compositor on every frame, or manually, by calling animator.step with an elapsed time argument.

You can register animations to the Animator using either its schedule method, with an already constructed Animation subclass, or either Animator.animate_attr or Animator.animate_float for an in-place construction of the animation instance.

  1"""All animation-related classes & functions.
  2
  3The biggest exports are `Animation` and its subclasses, as well as `Animator`. A
  4global instance of `Animator` is also exported, under the `animator` name.
  5
  6These can be used both within a WindowManager context (where stepping is done
  7automatically by the `pytermgui.window_manager.Compositor` on every frame, or manually,
  8by calling `animator.step` with an elapsed time argument.
  9
 10You can register animations to the Animator using either its `schedule` method, with
 11an already constructed `Animation` subclass, or either `Animator.animate_attr` or
 12`Animator.animate_float` for an in-place construction of the animation instance.
 13"""
 14
 15# pylint: disable=too-many-arguments, too-many-instance-attributes
 16
 17from __future__ import annotations
 18
 19from dataclasses import dataclass, field
 20from enum import Enum
 21from typing import TYPE_CHECKING, Any, Callable
 22
 23if TYPE_CHECKING:
 24    from .widgets import Widget
 25else:
 26    Widget = Any
 27
 28__all__ = ["Animator", "FloatAnimation", "AttrAnimation", "animator", "is_animated"]
 29
 30
 31def _add_flag(target: object, attribute: str) -> None:
 32    """Adds attribute to `target.__ptg_animated__`.
 33
 34    If the list doesn't exist, it is created with the attribute.
 35    """
 36
 37    if not hasattr(target, "__ptg_animated__"):
 38        setattr(target, "__ptg_animated__", [])
 39
 40    animated = getattr(target, "__ptg_animated__")
 41    animated.append(attribute)
 42
 43
 44def _remove_flag(target: object, attribute: str) -> None:
 45    """Removes attribute from `target.__ptg_animated__`.
 46
 47    If the animated list is empty, it is `del`-d from the object.
 48    """
 49
 50    animated = getattr(target, "__ptg_animated__", None)
 51    if animated is None:
 52        raise ValueError(f"Object {target!r} seems to not be animated.")
 53
 54    animated.remove(attribute)
 55    if len(animated) == 0:
 56        del target.__dict__["__ptg_animated__"]
 57
 58
 59def is_animated(target: object, attribute: str) -> bool:
 60    """Determines whether the given object.attribute is animated.
 61
 62    This looks for `__ptg_animated__`, and whether it contains the given attribute.
 63    """
 64
 65    if not hasattr(target, "__ptg_animated__"):
 66        return False
 67
 68    animated = getattr(target, "__ptg_animated__")
 69
 70    return attribute in animated
 71
 72
 73class Direction(Enum):
 74    """Animation directions."""
 75
 76    FORWARD = 1
 77    BACKWARD = -1
 78
 79
 80@dataclass
 81class Animation:
 82    """The baseclass for all animations."""
 83
 84    duration: int
 85    direction: Direction
 86    loop: bool
 87
 88    on_step: Callable[[Animation], bool] | None
 89    on_finish: Callable[[Animation], None] | None
 90
 91    state: float
 92    _remaining: float
 93
 94    def __post_init__(self) -> None:
 95        self.state = 0.0 if self.direction is Direction.FORWARD else 1.0
 96        self._remaining = self.duration
 97        self._is_paused = False
 98
 99    def _update_state(self, elapsed: float) -> bool:
100        """Updates the internal float state of the animation.
101
102        Args:
103            elapsed: The time elapsed since last update.
104
105        Returns:
106            True if the animation deems itself complete, False otherwise.
107        """
108
109        if self._is_paused:
110            return False
111
112        self._remaining -= elapsed * 1000
113
114        self.state = (self.duration - self._remaining) / self.duration
115
116        if self.direction is Direction.BACKWARD:
117            self.state = 1 - self.state
118
119        self.state = min(self.state, 1.0)
120
121        if not 0.0 <= self.state < 1.0:
122            if not self.loop:
123                return True
124
125            self._remaining = self.duration
126            self.direction = Direction(self.direction.value * -1)
127
128        return False
129
130    def pause(self, setting: bool = True) -> None:
131        """Pauses the animation."""
132
133        self._is_paused = setting
134
135    def resume(self) -> None:
136        """Resumes the animation."""
137
138        self.pause(False)
139
140    def step(self, elapsed: float) -> bool:
141        """Updates animation state.
142
143        This should call `_update_state`, passing in the elapsed value. That call
144        will update the `state` attribute, which can then be used to animate things.
145
146        Args:
147            elapsed: The time elapsed since last update.
148        """
149
150        state_finished = self._update_state(elapsed)
151
152        step_finished = False
153        if self.on_step is not None:
154            step_finished = self.on_step(self)
155
156        return state_finished or step_finished
157
158    def finish(self) -> None:
159        """Finishes and cleans up after the animation.
160
161        Called by `Animator` after `on_step` returns True. Should call `on_finish` if it
162        is not None.
163        """
164
165        if self.on_finish is not None:
166            self.on_finish(self)
167
168
169@dataclass
170class FloatAnimation(Animation):
171    """Transitions a floating point number from 0.0 to 1.0.
172
173    Note that this is just a wrapper over the base class, and provides no extra
174    functionality.
175    """
176
177    duration: int
178
179    on_step: Callable[[Animation], bool] | None = None
180    on_finish: Callable[[Animation], None] | None = None
181
182    direction: Direction = Direction.FORWARD
183    loop: bool = False
184
185    state: float = field(init=False)
186    _remaining: int = field(init=False)
187
188
189@dataclass
190class AttrAnimation(Animation):
191    """Animates an attribute going from one value to another."""
192
193    target: object = None
194    attr: str = ""
195    value_type: type = int
196    end: int | float = 0
197    start: int | float | None = None
198
199    on_step: Callable[[Animation], bool] | None = None
200    on_finish: Callable[[Animation], None] | None = None
201
202    direction: Direction = Direction.FORWARD
203    loop: bool = False
204
205    state: float = field(init=False)
206    _remaining: int = field(init=False)
207
208    def __post_init__(self) -> None:
209        super().__post_init__()
210
211        if self.start is None:
212            self.start = getattr(self.target, self.attr)
213
214        if self.end < self.start:
215            self.start, self.end = self.end, self.start
216            self.direction = Direction.BACKWARD
217
218        self.end -= self.start
219
220        _add_flag(self.target, self.attr)
221
222    def step(self, elapsed: float) -> bool:
223        """Steps forward in the attribute animation."""
224
225        state_finished = self._update_state(elapsed)
226
227        step_finished = False
228
229        assert self.start is not None
230
231        updated = self.start + (self.end * self.state)
232        setattr(self.target, self.attr, self.value_type(updated))
233
234        if self.on_step is not None:
235            step_finished = self.on_step(self)
236
237        if step_finished or state_finished:
238            return True
239
240        return False
241
242    def finish(self) -> None:
243        """Deletes `__ptg_animated__` flag, calls `on_finish`."""
244
245        _remove_flag(self.target, self.attr)
246        super().finish()
247
248
249class Animator:
250    """The Animator class
251
252    This class maintains a list of animations (self._animations), stepping
253    each of them forward as long as they return False. When they return
254    False, the animation is removed from the tracked animations.
255
256    This stepping is done when `step` is called.
257    """
258
259    def __init__(self) -> None:
260        """Initializes an animator."""
261
262        self._animations: list[Animation] = []
263
264    def __contains__(self, item: object) -> bool:
265        """Returns whether the item is inside _animations."""
266
267        return item in self._animations
268
269    @property
270    def is_active(self) -> bool:
271        """Determines whether there are any active animations."""
272
273        return len(self._animations) > 0
274
275    def step(self, elapsed: float) -> None:
276        """Steps the animation forward by the given elapsed time."""
277
278        for animation in self._animations.copy():
279            if animation.step(elapsed):
280                self._animations.remove(animation)
281                animation.finish()
282
283    def schedule(self, animation: Animation) -> None:
284        """Starts an animation on the next step."""
285
286        self._animations.append(animation)
287
288    def animate_attr(self, **animation_args: Any) -> AttrAnimation:
289        """Creates and schedules an AttrAnimation.
290
291        All arguments are passed to the `AttrAnimation` constructor. `direction`, if
292        given as an integer, will be converted to a `Direction` before being passed.
293
294        Returns:
295            The created animation.
296        """
297
298        if "direction" in animation_args:
299            animation_args["direction"] = Direction(animation_args["direction"])
300
301        anim = AttrAnimation(**animation_args)
302        self.schedule(anim)
303
304        return anim
305
306    def animate_float(self, **animation_args: Any) -> FloatAnimation:
307        """Creates and schedules an Animation.
308
309        All arguments are passed to the `Animation` constructor. `direction`, if
310        given as an integer, will be converted to a `Direction` before being passed.
311
312        Returns:
313            The created animation.
314        """
315
316        if "direction" in animation_args:
317            animation_args["direction"] = Direction(animation_args["direction"])
318
319        anim = FloatAnimation(**animation_args)
320        self.schedule(anim)
321
322        return anim
323
324
325animator = Animator()
326"""The global Animator instance used by all of the library."""
class Animator:
250class Animator:
251    """The Animator class
252
253    This class maintains a list of animations (self._animations), stepping
254    each of them forward as long as they return False. When they return
255    False, the animation is removed from the tracked animations.
256
257    This stepping is done when `step` is called.
258    """
259
260    def __init__(self) -> None:
261        """Initializes an animator."""
262
263        self._animations: list[Animation] = []
264
265    def __contains__(self, item: object) -> bool:
266        """Returns whether the item is inside _animations."""
267
268        return item in self._animations
269
270    @property
271    def is_active(self) -> bool:
272        """Determines whether there are any active animations."""
273
274        return len(self._animations) > 0
275
276    def step(self, elapsed: float) -> None:
277        """Steps the animation forward by the given elapsed time."""
278
279        for animation in self._animations.copy():
280            if animation.step(elapsed):
281                self._animations.remove(animation)
282                animation.finish()
283
284    def schedule(self, animation: Animation) -> None:
285        """Starts an animation on the next step."""
286
287        self._animations.append(animation)
288
289    def animate_attr(self, **animation_args: Any) -> AttrAnimation:
290        """Creates and schedules an AttrAnimation.
291
292        All arguments are passed to the `AttrAnimation` constructor. `direction`, if
293        given as an integer, will be converted to a `Direction` before being passed.
294
295        Returns:
296            The created animation.
297        """
298
299        if "direction" in animation_args:
300            animation_args["direction"] = Direction(animation_args["direction"])
301
302        anim = AttrAnimation(**animation_args)
303        self.schedule(anim)
304
305        return anim
306
307    def animate_float(self, **animation_args: Any) -> FloatAnimation:
308        """Creates and schedules an Animation.
309
310        All arguments are passed to the `Animation` constructor. `direction`, if
311        given as an integer, will be converted to a `Direction` before being passed.
312
313        Returns:
314            The created animation.
315        """
316
317        if "direction" in animation_args:
318            animation_args["direction"] = Direction(animation_args["direction"])
319
320        anim = FloatAnimation(**animation_args)
321        self.schedule(anim)
322
323        return anim

The Animator class

This class maintains a list of animations (self._animations), stepping each of them forward as long as they return False. When they return False, the animation is removed from the tracked animations.

This stepping is done when step is called.

Animator()
260    def __init__(self) -> None:
261        """Initializes an animator."""
262
263        self._animations: list[Animation] = []

Initializes an animator.

is_active: bool

Determines whether there are any active animations.

def step(self, elapsed: float) -> None:
276    def step(self, elapsed: float) -> None:
277        """Steps the animation forward by the given elapsed time."""
278
279        for animation in self._animations.copy():
280            if animation.step(elapsed):
281                self._animations.remove(animation)
282                animation.finish()

Steps the animation forward by the given elapsed time.

def schedule(self, animation: pytermgui.animations.Animation) -> None:
284    def schedule(self, animation: Animation) -> None:
285        """Starts an animation on the next step."""
286
287        self._animations.append(animation)

Starts an animation on the next step.

def animate_attr(self, **animation_args: Any) -> pytermgui.animations.AttrAnimation:
289    def animate_attr(self, **animation_args: Any) -> AttrAnimation:
290        """Creates and schedules an AttrAnimation.
291
292        All arguments are passed to the `AttrAnimation` constructor. `direction`, if
293        given as an integer, will be converted to a `Direction` before being passed.
294
295        Returns:
296            The created animation.
297        """
298
299        if "direction" in animation_args:
300            animation_args["direction"] = Direction(animation_args["direction"])
301
302        anim = AttrAnimation(**animation_args)
303        self.schedule(anim)
304
305        return anim

Creates and schedules an AttrAnimation.

All arguments are passed to the AttrAnimation constructor. direction, if given as an integer, will be converted to a Direction before being passed.

Returns

The created animation.

def animate_float(self, **animation_args: Any) -> pytermgui.animations.FloatAnimation:
307    def animate_float(self, **animation_args: Any) -> FloatAnimation:
308        """Creates and schedules an Animation.
309
310        All arguments are passed to the `Animation` constructor. `direction`, if
311        given as an integer, will be converted to a `Direction` before being passed.
312
313        Returns:
314            The created animation.
315        """
316
317        if "direction" in animation_args:
318            animation_args["direction"] = Direction(animation_args["direction"])
319
320        anim = FloatAnimation(**animation_args)
321        self.schedule(anim)
322
323        return anim

Creates and schedules an Animation.

All arguments are passed to the Animation constructor. direction, if given as an integer, will be converted to a Direction before being passed.

Returns

The created animation.

@dataclass
class FloatAnimation(Animation):
170@dataclass
171class FloatAnimation(Animation):
172    """Transitions a floating point number from 0.0 to 1.0.
173
174    Note that this is just a wrapper over the base class, and provides no extra
175    functionality.
176    """
177
178    duration: int
179
180    on_step: Callable[[Animation], bool] | None = None
181    on_finish: Callable[[Animation], None] | None = None
182
183    direction: Direction = Direction.FORWARD
184    loop: bool = False
185
186    state: float = field(init=False)
187    _remaining: int = field(init=False)

Transitions a floating point number from 0.0 to 1.0.

Note that this is just a wrapper over the base class, and provides no extra functionality.

FloatAnimation( duration: int, direction: pytermgui.animations.Direction = <Direction.FORWARD: 1>, loop: bool = False, on_step: Optional[Callable[[pytermgui.animations.Animation], bool]] = None, on_finish: Optional[Callable[[pytermgui.animations.Animation], NoneType]] = None)
on_step: Optional[Callable[[pytermgui.animations.Animation], bool]] = None
on_finish: Optional[Callable[[pytermgui.animations.Animation], NoneType]] = None
direction: pytermgui.animations.Direction = <Direction.FORWARD: 1>
loop: bool = False
Inherited Members
Animation
pause
resume
step
finish
@dataclass
class AttrAnimation(Animation):
190@dataclass
191class AttrAnimation(Animation):
192    """Animates an attribute going from one value to another."""
193
194    target: object = None
195    attr: str = ""
196    value_type: type = int
197    end: int | float = 0
198    start: int | float | None = None
199
200    on_step: Callable[[Animation], bool] | None = None
201    on_finish: Callable[[Animation], None] | None = None
202
203    direction: Direction = Direction.FORWARD
204    loop: bool = False
205
206    state: float = field(init=False)
207    _remaining: int = field(init=False)
208
209    def __post_init__(self) -> None:
210        super().__post_init__()
211
212        if self.start is None:
213            self.start = getattr(self.target, self.attr)
214
215        if self.end < self.start:
216            self.start, self.end = self.end, self.start
217            self.direction = Direction.BACKWARD
218
219        self.end -= self.start
220
221        _add_flag(self.target, self.attr)
222
223    def step(self, elapsed: float) -> bool:
224        """Steps forward in the attribute animation."""
225
226        state_finished = self._update_state(elapsed)
227
228        step_finished = False
229
230        assert self.start is not None
231
232        updated = self.start + (self.end * self.state)
233        setattr(self.target, self.attr, self.value_type(updated))
234
235        if self.on_step is not None:
236            step_finished = self.on_step(self)
237
238        if step_finished or state_finished:
239            return True
240
241        return False
242
243    def finish(self) -> None:
244        """Deletes `__ptg_animated__` flag, calls `on_finish`."""
245
246        _remove_flag(self.target, self.attr)
247        super().finish()

Animates an attribute going from one value to another.

AttrAnimation( duration: int, direction: pytermgui.animations.Direction = <Direction.FORWARD: 1>, loop: bool = False, on_step: Optional[Callable[[pytermgui.animations.Animation], bool]] = None, on_finish: Optional[Callable[[pytermgui.animations.Animation], NoneType]] = None, target: object = None, attr: str = '', value_type: type = , end: int | float = 0, start: int | float | None = None)
target: object = None
attr: str = ''
end: int | float = 0
start: int | float | None = None
on_step: Optional[Callable[[pytermgui.animations.Animation], bool]] = None
on_finish: Optional[Callable[[pytermgui.animations.Animation], NoneType]] = None
direction: pytermgui.animations.Direction = <Direction.FORWARD: 1>
loop: bool = False
def step(self, elapsed: float) -> bool:
223    def step(self, elapsed: float) -> bool:
224        """Steps forward in the attribute animation."""
225
226        state_finished = self._update_state(elapsed)
227
228        step_finished = False
229
230        assert self.start is not None
231
232        updated = self.start + (self.end * self.state)
233        setattr(self.target, self.attr, self.value_type(updated))
234
235        if self.on_step is not None:
236            step_finished = self.on_step(self)
237
238        if step_finished or state_finished:
239            return True
240
241        return False

Steps forward in the attribute animation.

def finish(self) -> None:
243    def finish(self) -> None:
244        """Deletes `__ptg_animated__` flag, calls `on_finish`."""
245
246        _remove_flag(self.target, self.attr)
247        super().finish()

Deletes __ptg_animated__ flag, calls on_finish.

Inherited Members
Animation
pause
resume
class AttrAnimation.value_type:

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.

>>> int('0b100', base=0)
4
Inherited Members
builtins.int
int
conjugate
bit_length
bit_count
to_bytes
from_bytes
as_integer_ratio
real
imag
numerator
denominator
animator = <pytermgui.animations.Animator object>

The global Animator instance used by all of the library.

def is_animated(target: object, attribute: str) -> bool:
60def is_animated(target: object, attribute: str) -> bool:
61    """Determines whether the given object.attribute is animated.
62
63    This looks for `__ptg_animated__`, and whether it contains the given attribute.
64    """
65
66    if not hasattr(target, "__ptg_animated__"):
67        return False
68
69    animated = getattr(target, "__ptg_animated__")
70
71    return attribute in animated

Determines whether the given object.attribute is animated.

This looks for __ptg_animated__, and whether it contains the given attribute.