Skip to content

Commit

Permalink
Merge pull request #303 from Textualize/test-animation
Browse files Browse the repository at this point in the history
Test animation
  • Loading branch information
willmcgugan authored Feb 21, 2022
2 parents fd9f545 + 9ea2c6e commit d0b1ca5
Show file tree
Hide file tree
Showing 2 changed files with 300 additions and 47 deletions.
92 changes: 45 additions & 47 deletions src/textual/_animator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

if sys.version_info >= (3, 8):
from typing import Protocol, runtime_checkable
else:
else: # pragma: no cover
from typing_extensions import Protocol, runtime_checkable


Expand All @@ -27,13 +27,13 @@

@runtime_checkable
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):
@abstractmethod
def __call__(self, time: float) -> bool:
def __call__(self, time: float) -> bool: # pragma: no cover
raise NotImplementedError("")


Expand All @@ -45,50 +45,41 @@ class SimpleAnimation(Animation):
duration: float
start_value: float | Animatable
end_value: float | Animatable
final_value: float | Animatable
final_value: object
easing: EasingFunction

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:
value = self.end_value
setattr(self.obj, self.attribute, self.final_value)
return True

factor = min(1.0, (time - self.start_time) / self.duration)
eased_factor = self.easing(factor)

if factor == 1.0:
value = self.final_value
elif isinstance(self.start_value, Animatable):
assert isinstance(
self.end_value, Animatable
), "end_value must be animatable"
value = self.start_value.blend(self.end_value, eased_factor)
else:
factor = min(1.0, (time - self.start_time) / self.duration)
eased_factor = self.easing(factor)

if factor == 1.0:
value = self.end_value
elif isinstance(self.start_value, Animatable):
assert isinstance(
self.end_value, Animatable
), "end_value must be animatable"
value = self.start_value.blend(self.end_value, eased_factor)
assert isinstance(self.start_value, float), "`start_value` must be float"
assert isinstance(self.end_value, float), "`end_value` must be float"
if self.end_value > self.start_value:
eased_factor = self.easing(factor)
value = (
self.start_value
+ (self.end_value - self.start_value) * eased_factor
)
else:
assert isinstance(
self.start_value, float
), "`start_value` must be float"
assert isinstance(self.end_value, float), "`end_value` must be float"
if self.end_value > self.start_value:
eased_factor = self.easing(factor)
value = (
self.start_value
+ (self.end_value - self.start_value) * eased_factor
)
else:
eased_factor = 1 - self.easing(factor)
value = (
self.end_value
+ (self.start_value - self.end_value) * eased_factor
)
eased_factor = 1 - self.easing(factor)
value = (
self.end_value + (self.start_value - self.end_value) * eased_factor
)
setattr(self.obj, self.attribute, value)
return value == self.end_value
return factor >= 1


class BoundAnimator:
Expand All @@ -101,13 +92,13 @@ def __call__(
attribute: str,
value: float,
*,
final_value: Any = ...,
final_value: object = ...,
duration: float | None = None,
speed: float | None = None,
easing: EasingFunction | str = DEFAULT_EASING,
) -> None:
easing_function = EASING[easing] if isinstance(easing, str) else easing
self._animator.animate(
return self._animator.animate(
self._obj,
attribute=attribute,
value=value,
Expand All @@ -131,6 +122,10 @@ def __init__(self, target: MessageTarget, frames_per_second: int = 60) -> None:
pause=True,
)

def get_time(self) -> float:
"""Get the current wall clock time."""
return time()

async def start(self) -> None:
"""Start the animator task."""

Expand All @@ -153,7 +148,7 @@ def animate(
attribute: str,
value: Any,
*,
final_value: Any = ...,
final_value: object = ...,
duration: float | None = None,
speed: float | None = None,
easing: EasingFunction | str = DEFAULT_EASING,
Expand All @@ -172,7 +167,7 @@ def animate(

if final_value is ...:
final_value = value
start_time = time()
start_time = self.get_time()

animation_key = (id(obj), attribute)
if animation_key in self._animations:
Expand Down Expand Up @@ -216,15 +211,18 @@ def animate(
self._animations[animation_key] = animation
self._timer.resume()

async def __call__(self) -> None:
def __call__(self) -> None:
if not self._animations:
self._timer.pause()
else:
animation_time = time()
animation_time = self.get_time()
animation_keys = list(self._animations.keys())
for animation_key in animation_keys:
animation = self._animations[animation_key]
if animation(animation_time):
del self._animations[animation_key]
# TODO: We should be able to do animation without refreshing everything
self.target.view.refresh(True, True)
self.on_animation_frame()

def on_animation_frame(self) -> None:
# TODO: We should be able to do animation without refreshing everything
self.target.view.refresh(True, True)
Loading

0 comments on commit d0b1ca5

Please sign in to comment.