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.

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

Initializes an animator.

#   is_active: bool

Determines whether there are any active animations.

#   def step(self, elapsed: float) -> None:
View Source
256    def step(self, elapsed: float) -> None:
257        """Steps the animation forward by the given elapsed time."""
258
259        for animation in self._animations.copy():
260            if animation.step(elapsed):
261                self._animations.remove(animation)
262                animation.finish()

Steps the animation forward by the given elapsed time.

#   def schedule(self, animation: pytermgui.animations.Animation) -> None:
View Source
264    def schedule(self, animation: Animation) -> None:
265        """Starts an animation on the next step."""
266
267        self._animations.append(animation)

Starts an animation on the next step.

#   def animate_attr(self, **animation_args: Any) -> pytermgui.animations.AttrAnimation:
View Source
269    def animate_attr(self, **animation_args: Any) -> AttrAnimation:
270        """Creates and schedules an AttrAnimation.
271
272        All arguments are passed to the `AttrAnimation` constructor. `direction`, if
273        given as an integer, will be converted to a `Direction` before being passed.
274
275        Returns:
276            The created animation.
277        """
278
279        if "direction" in animation_args:
280            animation_args["direction"] = Direction(animation_args["direction"])
281
282        anim = AttrAnimation(**animation_args)
283        self.schedule(anim)
284
285        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:
View Source
287    def animate_float(self, **animation_args: Any) -> FloatAnimation:
288        """Creates and schedules an Animation.
289
290        All arguments are passed to the `Animation` constructor. `direction`, if
291        given as an integer, will be converted to a `Direction` before being passed.
292
293        Returns:
294            The created animation.
295        """
296
297        if "direction" in animation_args:
298            animation_args["direction"] = Direction(animation_args["direction"])
299
300        anim = FloatAnimation(**animation_args)
301        self.schedule(anim)
302
303        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):
View Source
155@dataclass
156class FloatAnimation(Animation):
157    """Transitions a floating point number from 0.0 to 1.0.
158
159    Note that this is just a wrapper over the base class, and provides no extra
160    functionality.
161    """
162
163    duration: int
164
165    on_step: Callable[[Animation], bool] | None = None
166    on_finish: Callable[[Animation], None] | None = None
167
168    direction: Direction = Direction.FORWARD
169    loop: bool = False
170
171    state: float = field(init=False)
172    _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
step
finish
#  
@dataclass
class AttrAnimation(Animation):
View Source
175@dataclass
176class AttrAnimation(Animation):
177    """Animates an attribute going from one value to another."""
178
179    target: object = None
180    attr: str = ""
181    value_type: type = int
182    end: int | float = 0
183    start: int | float | None = None
184
185    on_step: Callable[[Animation], bool] | None = None
186    on_finish: Callable[[Animation], None] | None = None
187
188    direction: Direction = Direction.FORWARD
189    loop: bool = False
190
191    state: float = field(init=False)
192    _remaining: int = field(init=False)
193
194    def __post_init__(self) -> None:
195        super().__post_init__()
196
197        if self.start is None:
198            self.start = getattr(self.target, self.attr)
199
200        if self.end < self.start:
201            self.start, self.end = self.end, self.start
202            self.direction = Direction.BACKWARD
203
204        self.end -= self.start
205
206        _add_flag(self.target, self.attr)
207
208    def step(self, elapsed: float) -> bool:
209        """Steps forward in the attribute animation."""
210
211        state_finished = self._update_state(elapsed)
212
213        step_finished = False
214
215        assert self.start is not None
216
217        updated = self.start + (self.end * self.state)
218        setattr(self.target, self.attr, self.value_type(updated))
219
220        if self.on_step is not None:
221            step_finished = self.on_step(self)
222
223        if step_finished or state_finished:
224            return True
225
226        return False
227
228    def finish(self) -> None:
229        """Deletes `__ptg_animated__` flag, calls `on_finish`."""
230
231        _remove_flag(self.target, self.attr)
232        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 = <class 'int'>, 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:
View Source
208    def step(self, elapsed: float) -> bool:
209        """Steps forward in the attribute animation."""
210
211        state_finished = self._update_state(elapsed)
212
213        step_finished = False
214
215        assert self.start is not None
216
217        updated = self.start + (self.end * self.state)
218        setattr(self.target, self.attr, self.value_type(updated))
219
220        if self.on_step is not None:
221            step_finished = self.on_step(self)
222
223        if step_finished or state_finished:
224            return True
225
226        return False

Steps forward in the attribute animation.

#   def finish(self) -> None:
View Source
228    def finish(self) -> None:
229        """Deletes `__ptg_animated__` flag, calls `on_finish`."""
230
231        _remove_flag(self.target, self.attr)
232        super().finish()

Deletes __ptg_animated__ flag, calls on_finish.

#   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

The global Animator instance used by all of the library.

#   def is_animated(target: object, attribute: str) -> bool:
View Source
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

Determines whether the given object.attribute is animated.

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