diff --git a/docs/examples/guide/animator/animation04.py b/docs/examples/guide/animator/animation04.py new file mode 100644 index 000000000..5fbbb7fe8 --- /dev/null +++ b/docs/examples/guide/animator/animation04.py @@ -0,0 +1,29 @@ +from rich.console import RenderableType + +from textual.app import App, ComposeResult +from textual.reactive import reactive +from textual.widget import Widget + + +class ValueBox(Widget): + value = reactive(0.0) + + def render(self) -> RenderableType: + return str(self.value) + + +class AnimationApp(App): + def compose(self) -> ComposeResult: + self.box = ValueBox() + self.box.styles.background = "red" + self.box.styles.color = "black" + self.box.styles.padding = (1, 2) + yield self.box + + async def on_mount(self): + self.box.animate("value", value=100.0, duration=100.0, easing="linear") + + +if __name__ == "__main__": + app = AnimationApp() + app.run() diff --git a/docs/guide/animator.md b/docs/guide/animator.md index bf8c34724..8c9d1e4ed 100644 --- a/docs/guide/animator.md +++ b/docs/guide/animator.md @@ -1,11 +1,11 @@ # Animator Textual ships with an easy-to-use system which lets you add animation to your application. -To get a feel for what animation looks like in Textual and try out different easing functions, run `textual easing` in your terminal. +To get a feel for what animation looks like in Textual, run `textual easing` from the command line. !!! note - The easing preview requires the `dev` extras (using `pip install textual[dev]`). + The `textual easing` preview requires the `dev` extras to be installed (using `pip install textual[dev]`). ## Animating styles @@ -23,9 +23,8 @@ The app below contains a single `Static` widget which is immediately animated to --8<-- "docs/examples/guide/animator/animation01.py" ``` -Internally, the animator deals with updating the value of the `opacity` attribute on the `styles` object. -In a single line, we've achieved a fading animation: - +Internally, the animator repeatedly updates the value of the `opacity` attribute on the `styles` object. +With a single line of code, we've achieved a fading animation: === "After 0s" @@ -43,28 +42,56 @@ In a single line, we've achieved a fading animation: ``` Remember, when the value of a property on the `styles` object gets updated, Textual automatically updates the display. -This means there's no additional code required to trigger a display update. +This means there's no additional code required to trigger a display update - the animation just works. In the example above we specified a `duration` of two seconds, but you can alternatively pass in a `speed` value. -## Animating other attributes +## The `Animatable` protocol -You can animate non-style attributes on widgets too. -This could be used to drive more complex animations involving styles, or to keep animations in sync with each other. +You can animate `float` values and any type which implements the `Animatable` protocol. + +To implement the `Animatable` protocol, add a `def blend(self: T, destination: T, factor: float) -> T` method to the class. +The `blend` method should return a new object which represents `self` blended with `destination` by a factor of `factor`. +The animator will repeatedly call this method to retrieve the current value to display for the current. + +An example of an object which implements this protocol is [Color][textual.color.Color]. +It follows that you can use `animate` to animate from one `Color` to another. + +## Animating widget attributes + +You can animate non-`style` attributes on widgets too, assuming they implement `Animatable`. Again, the animation system will take care of updating the attribute on the widget as time progresses. -If the attribute being animated is [reactive](./reactivity.md), Textual can handle the refreshing of the display each time the animator updates the value. +If the attribute being animated is [reactive](./reactivity.md), Textual can refresh the display each time the animator updates the value. -## Animating arbitrary values +The example below shows a simple incrementing timer that counts from 0 to 100 over 100 seconds. -Sometimes, you'll want to animate a value that isn't directly accessible as an attribute on a widget. -For example, perhaps the value to be animated is nested inside some object structure, and you don't want to restructure your code to make it a top-level attribute. +=== "animation04.py" -In these cases, you can make use of an "unbound" animator. -These are animators which aren't pre-emptively associated with an object. -They let you pass in an object, _and_ the name of the attribute you wish to animate on it. + ```python + --8<-- "docs/examples/guide/animator/animation04.py" + ``` + +=== "Output" + + ```{.textual path="docs/examples/guide/animator/animation04.py"} + ``` + +Since `value` is reactive, the display is automatically updated each time the animator modifies it. + +## Animating Python object attributes + +Sometimes you'll want to animate a value that exists inside a plain old Python object. + +In these cases, you can make use of the "unbound" animator. +An unbound animator is an animator which isn't pre-emptively associated with (bound to) an object. + +Unbound animators let you pass the name of the attribute you wish to animate, _and_ the object that attribute exists on. This is unlike the animators discussed above, which are already _bound_ to the object they were retrieved from. +You can retrieve the unbound animator from the `App` instance via `App.animator`, and call the `animate` method on it. +This method is the same as the one described earlier, except the first argument is the object containing the attribute. + ## Easing functions Easing functions control the "look and feel" of an animation. @@ -72,6 +99,15 @@ The easing function determines the journey a value takes on its way to the targe Perhaps the value will be transformed linearly, moving towards the target at a constant rate. Or maybe it'll start off slow, then accelerate towards the final value as the animation progresses. +Easing functions take a single input representing the time, and output a "factor". +This factor is what gets passed to the `blend` method in the `Animatable` protocol. + +!!! warning + + The factor output by the easing function will usually remain between 0 and 1. + However, some easing functions (such as `in_out_elastic`) will produce values slightly below 0 and slightly above 1. + Because of this, any implementation of `blend` should support values outwith the range 0 to 1. + Textual supports the easing functions listed on this [very helpful page](https://easings.net/). In order to use them, you'll need to write them as `snake_case` and remove the `ease` at the start. To use `easeInOutSine`, for example, you'll write `in_out_sine`.