fix and test for animator

This commit is contained in:
Will McGugan
2022-02-21 15:32:02 +00:00
parent fd9f545608
commit 8be6ea91f6
2 changed files with 175 additions and 16 deletions

View File

@@ -16,7 +16,7 @@ from ._types import MessageTarget
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
from typing import Protocol, runtime_checkable from typing import Protocol, runtime_checkable
else: else: # pragma: no cover
from typing_extensions import Protocol, runtime_checkable from typing_extensions import Protocol, runtime_checkable
@@ -27,13 +27,13 @@ T = TypeVar("T")
@runtime_checkable @runtime_checkable
class Animatable(Protocol): class Animatable(Protocol):
def blend(self: T, destination: T, factor: float) -> T: def blend(self: T, destination: T, factor: float) -> T: # pragma: no cover
... ...
class Animation(ABC): class Animation(ABC):
@abstractmethod @abstractmethod
def __call__(self, time: float) -> bool: def __call__(self, time: float) -> bool: # pragma: no cover
raise NotImplementedError("") raise NotImplementedError("")
@@ -45,26 +45,19 @@ class SimpleAnimation(Animation):
duration: float duration: float
start_value: float | Animatable start_value: float | Animatable
end_value: float | Animatable end_value: float | Animatable
final_value: float | Animatable final_value: object
easing: EasingFunction easing: EasingFunction
def __call__(self, time: float) -> bool: def __call__(self, time: float) -> bool:
def blend_float(start: float, end: float, factor: float) -> float:
return start + (end - start) * factor
AnimatableT = TypeVar("AnimatableT", bound=Animatable)
def blend(start: AnimatableT, end: AnimatableT, factor: float) -> AnimatableT:
return start.blend(end, factor)
if self.duration == 0: if self.duration == 0:
value = self.end_value value = self.final_value
else: else:
factor = min(1.0, (time - self.start_time) / self.duration) factor = min(1.0, (time - self.start_time) / self.duration)
eased_factor = self.easing(factor) eased_factor = self.easing(factor)
if factor == 1.0: if factor == 1.0:
value = self.end_value value = self.final_value
elif isinstance(self.start_value, Animatable): elif isinstance(self.start_value, Animatable):
assert isinstance( assert isinstance(
self.end_value, Animatable self.end_value, Animatable
@@ -88,7 +81,7 @@ class SimpleAnimation(Animation):
+ (self.start_value - self.end_value) * eased_factor + (self.start_value - self.end_value) * eased_factor
) )
setattr(self.obj, self.attribute, value) setattr(self.obj, self.attribute, value)
return value == self.end_value return value == self.final_value
class BoundAnimator: class BoundAnimator:
@@ -101,7 +94,7 @@ class BoundAnimator:
attribute: str, attribute: str,
value: float, value: float,
*, *,
final_value: Any = ..., final_value: object = ...,
duration: float | None = None, duration: float | None = None,
speed: float | None = None, speed: float | None = None,
easing: EasingFunction | str = DEFAULT_EASING, easing: EasingFunction | str = DEFAULT_EASING,
@@ -153,7 +146,7 @@ class Animator:
attribute: str, attribute: str,
value: Any, value: Any,
*, *,
final_value: Any = ..., final_value: object = ...,
duration: float | None = None, duration: float | None = None,
speed: float | None = None, speed: float | None = None,
easing: EasingFunction | str = DEFAULT_EASING, easing: EasingFunction | str = DEFAULT_EASING,

166
tests/test_animator.py Normal file
View File

@@ -0,0 +1,166 @@
from __future__ import annotations
from dataclasses import dataclass
from unittest.mock import Mock
import pytest
from textual._animator import SimpleAnimation
class Animatable:
"""An animatable object."""
def __init__(self, value):
self.value = value
def blend(self, destination: Animatable, factor: float) -> Animatable:
return Animatable(self.value + (destination.value - self.value) * factor)
@dataclass
class AnimateTest:
"""An object to animate."""
foo: float | None = 0 # Plain float that may be set to None on final_value
bar: Animatable = Animatable(0) # A mock object supporting the animatable protocol
def test_simple_animation():
"""Test an animation from one float to another."""
# Thing that may be animated
animatable = AnimateTest()
# Fake wall-clock time
time = 100.0
# Object that does the animation
animation = SimpleAnimation(
animatable,
"foo",
time,
3.0,
start_value=20.0,
end_value=50.0,
final_value=None,
easing=lambda x: x,
)
assert animation(time) is False
assert animatable.foo == 20.0
assert animation(time + 1.0) is False
assert animatable.foo == 30.0
assert animation(time + 2.0) is False
assert animatable.foo == 40.0
assert animation(time + 2.9) is False
assert pytest.approx(animatable.foo, 49.0)
assert animation(time + 3.0) is True # True to indicate animation is complete
assert animatable.foo is None # This is final_value
assert animation(time + 3.0) is True
assert animatable.foo is None
def test_simple_animation_duration_zero():
"""Test animation handles duration of 0."""
# Thing that may be animated
animatable = AnimateTest()
# Fake wall-clock time
time = 100.0
# Object that does the animation
animation = SimpleAnimation(
animatable,
"foo",
time,
0.0,
start_value=20.0,
end_value=50.0,
final_value=50.0,
easing=lambda x: x,
)
assert animation(time) is True
assert animatable.foo == 50.0
assert animation(time + 1.0) is True
assert animatable.foo == 50.0
def test_simple_animation_reverse():
"""Test an animation from one float to another, where the end value is less than the start."""
# Thing that may be animated
animatable = AnimateTest()
# Fake wall-clock time
time = 100.0
# Object that does the animation
animation = SimpleAnimation(
animatable,
"foo",
time,
3.0,
start_value=50.0,
end_value=20.0,
final_value=20.0,
easing=lambda x: x,
)
assert animation(time) is False
assert animatable.foo == 50.0
assert animation(time + 1.0) is False
assert animatable.foo == 40.0
assert animation(time + 2.0) is False
assert animatable.foo == 30.0
assert animation(time + 3.0) is True
assert animatable.foo == 20.0
def test_animatable():
"""Test SimpleAnimation works with the Animatable protocol"""
animatable = AnimateTest()
# Fake wall-clock time
time = 100.0
# Object that does the animation
animation = SimpleAnimation(
animatable,
"bar",
time,
3.0,
start_value=Animatable(20.0),
end_value=Animatable(50.0),
final_value=Animatable(50.0),
easing=lambda x: x,
)
assert animation(time) is False
assert animatable.bar.value == 20.0
assert animation(time + 1.0) is False
assert animatable.bar.value == 30.0
assert animation(time + 2.0) is False
assert animatable.bar.value == 40.0
assert animation(time + 2.9) is False
assert pytest.approx(animatable.bar.value, 49.0)
assert animation(time + 3.0) is True # True to indicate animation is complete
assert animatable.bar.value == 50.0