animation system

This commit is contained in:
Will McGugan
2021-12-20 14:25:56 +00:00
parent bd1ebc5fe2
commit 129015dea6
6 changed files with 74 additions and 24 deletions

View File

@@ -12,7 +12,7 @@ App > View {
layer: panels;
border-right: outer #09312e;
offset-x: -100%;
transition: offset-x 1.2s in_cubic 200ms, offset-y 1s linear;
transition: offset 1.2s in_cubic 200ms;
}
#sidebar.-active {

View File

@@ -8,14 +8,15 @@ from typing import Callable, TypeVar
from dataclasses import dataclass
from . import log
from ._easing import DEFAULT_EASING, EASING
from ._timer import Timer
from ._types import MessageTarget
if sys.version_info >= (3, 8):
from typing import Protocol
from typing import Protocol, runtime_checkable
else:
from typing_extensions import Protocol
from typing_extensions import Protocol, runtime_checkable
EasingFunction = Callable[[float], float]
@@ -23,6 +24,7 @@ EasingFunction = Callable[[float], float]
T = TypeVar("T")
@runtime_checkable
class Animatable(Protocol):
def blend(self: T, destination: T, factor: float) -> T:
...
@@ -47,28 +49,30 @@ class Animation:
def blend(start: AnimatableT, end: AnimatableT, factor: float) -> AnimatableT:
return start.blend(end, factor)
blend_function = (
blend_float if isinstance(self.start_value, (int, float)) else blend
)
if self.duration == 0:
value = self.end_value
else:
factor = min(1.0, (time - self.start_time) / self.duration)
eased_factor = self.easing_function(factor)
# value = blend_function(self.start_value, self.end_value, eased_factor)
if self.end_value > self.start_value:
eased_factor = self.easing_function(factor)
value = (
self.start_value
+ (self.end_value - self.start_value) * eased_factor
)
if isinstance(self.start_value, Animatable):
assert isinstance(self.end_value, Animatable)
value = self.start_value.blend(self.end_value, eased_factor)
else:
eased_factor = 1 - self.easing_function(factor)
value = (
self.end_value + (self.start_value - self.end_value) * eased_factor
)
assert isinstance(self.start_value, float)
assert isinstance(self.end_value, float)
if self.end_value > self.start_value:
eased_factor = self.easing_function(factor)
value = (
self.start_value
+ (self.end_value - self.start_value) * eased_factor
)
else:
eased_factor = 1 - self.easing_function(factor)
value = (
self.end_value
+ (self.start_value - self.end_value) * eased_factor
)
setattr(self.obj, self.attribute, value)
return value == self.end_value
@@ -134,7 +138,7 @@ class Animator:
speed: float | None = None,
easing: EasingFunction | str = DEFAULT_EASING,
) -> None:
log("animate", obj, attribute, value)
start_time = time()
animation_key = (obj, attribute)

View File

@@ -10,6 +10,7 @@ from rich.color import Color
import rich.repr
from rich.style import Style
from .. import log
from .errors import StyleValueError
from ._error_tools import friendly_list
from .constants import (
@@ -134,6 +135,18 @@ class Styles:
layers = NameListProperty()
transitions = TransitionsProperty()
ANIMATABLE = {
"offset-x",
"offset-y",
"offset",
"padding",
"margin",
"width",
"height",
"min_width",
"min_height",
}
@classmethod
@lru_cache(maxsize=1024)
def parse(cls, css: str, path: str) -> Styles:
@@ -173,6 +186,12 @@ class Styles:
def has_offset(self) -> bool:
return self._rule_offset is not None
def get_transition(self, key: str) -> Transition | None:
if key in self.ANIMATABLE:
return self.transitions.get(key, None)
else:
return None
def extract_rules(
self, specificity: tuple[int, int, int]
) -> list[tuple[str, tuple[int, int, int, int], Any]]:
@@ -188,7 +207,7 @@ class Styles:
]
return rules
def apply_rules(self, rules: Iterable[tuple[str, Any]]):
def apply_rules(self, rules: Iterable[tuple[str, object]]):
for key, value in rules:
setattr(self, f"_rule_{key}", value)

View File

@@ -129,7 +129,7 @@ class Stylesheet:
for name, specificity_rules in rule_attributes.items()
]
node.styles.apply_rules(node_rules)
node.apply_style_rules(node_rules)
def update(self, root: DOMNode) -> None:
"""Update a node and its children."""

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import cast, Iterable, Iterator, TYPE_CHECKING
from typing import Any, cast, Iterable, Iterator, TYPE_CHECKING
from rich.highlighter import ReprHighlighter
import rich.repr
@@ -135,11 +135,14 @@ class DOMNode(MessagePump):
add_children(tree, self)
return tree
def apply_style_rules(self, rules: Iterable[tuple[str, Any]]) -> None:
self.styles.apply_rules(rules)
def reset_styles(self) -> None:
from .widget import Widget
for node in self.walk_children():
node.styles = Styles()
# node.styles = Styles()
if isinstance(node, Widget):
node.clear_render_cache()
@@ -185,7 +188,6 @@ class DOMNode(MessagePump):
"""Toggle class names"""
self._classes.symmetric_difference_update(class_names)
self.app.stylesheet.update(self.app)
self.log(self.styles.css)
def has_psuedo_class(self, *class_names: str) -> bool:
"""Check for psuedo class (such as hover, focus etc)"""

View File

@@ -7,6 +7,7 @@ from typing import (
TYPE_CHECKING,
Callable,
ClassVar,
Iterable,
NamedTuple,
cast,
)
@@ -236,6 +237,30 @@ class Widget(DOMNode):
offset_x, offset_y = self.root_view.get_offset(self)
return self.root_view.get_style_at(x + offset_x, y + offset_y)
def apply_style_rules(self, rules: Iterable[tuple[str, Any]]) -> None:
styles = self.styles
is_animatable = styles.ANIMATABLE.__contains__
for key, value in rules:
current = getattr(styles, f"_rule_{key}")
if current == value:
continue
self.log(key, "=", value)
if is_animatable(key):
self.log("animatable", key)
transition = styles.get_transition(key)
self.log("transition", transition)
if transition is None:
setattr(styles, f"_rule_{key}", value)
else:
duration, easing, delay = transition
self.log("ANIMATING")
self.app.animator.animate(
styles, key, value, duration=duration, easing=easing
)
else:
self.log("not animatable")
setattr(styles, f"_rule_{key}", value)
async def call_later(self, callback: Callable, *args, **kwargs) -> None:
await self.app.call_later(callback, *args, **kwargs)