[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/andevsr/pythonnative/main/src/pythonnative/animated.py [Back]  [Original]

"""Animated values, derived animated nodes, and native-driven animation.

Modeled on React Native's ``Animated`` API with an ``async``-aware
completion contract. The core primitives are:

- [`AnimatedValue`][pythonnative.animated.AnimatedValue]: a numeric
  cell attached to native view properties; animations drive it over
  time.
- **Derived nodes**: every animated node supports
  [`interpolate`][pythonnative.animated.AnimatedNode.interpolate]
  (range mapping with numeric, color, and angle outputs) and Python
  arithmetic (``opacity * 0.5``, ``x + y``, ``-value``), producing
  read-only [`AnimatedNode`][pythonnative.animated.AnimatedNode]
  instances that update whenever their inputs change.
- ``Animated.timing`` / ``Animated.spring`` / ``Animated.decay``:
  animation factories. The objects they return implement
  ``__await__``, so you can write ``await Animated.timing(v, to=1.0)``
  to suspend until the animation finishes.
- ``Animated.sequence`` / ``Animated.parallel`` / ``Animated.stagger``
  / ``Animated.delay`` / ``Animated.loop``: composition; also
  awaitable.
- ``Animated.event``: build an event-prop callback that copies event
  fields into animated values (``on_scroll=pn.Animated.event(y=v)``).
- ``Animated.diff_clamp``: accumulate an input's *deltas* into a
  clamped range (the collapsing-header primitive).
- ``Animated.View`` / ``Animated.Text`` / ``Animated.Image``:
  components whose ``style`` may contain animated nodes, including
  inside ``transform`` entries.

Driver architecture (the **native driver**):

When an animation starts, PythonNative compiles its spec (curve,
duration, target value) and offers it to the platform handler of every
native view the value is attached to
([`ViewHandler.start_animation`][pythonnative.native_views.base.ViewHandler.start_animation]).

- **Accepted** (iOS Core Animation, Android ``ViewPropertyAnimator`` /
  ``DynamicAnimation``): the platform animates the property entirely
  natively; no Python code runs per frame. Python receives exactly one
  callback when the animation settles, updates the
  [`AnimatedValue`][pythonnative.animated.AnimatedValue], and resolves
  any awaiting tasks.
- **Declined** (desktop preview, unattached values, callable easings,
  values feeding Python-side listeners or derived nodes): a single
  background thread ticks the animation at ~60 Hz from Python, pushing
  each frame through ``set_animated_property``. Semantics are
  identical; only the frame source differs.

Values driven by *events* (scroll offsets via ``Animated.event``,
gesture translations) flow through Python: the native listener fires,
the bound values update, and every attachment (including derived
nodes) is pushed in the same call.

Example:
    ```python
    import pythonnative as pn


    @pn.component
    def FadeIn():
        opacity = pn.use_animated_value(0.0)

        async def fade_in():
            await pn.Animated.timing(opacity, to=1.0, duration=400)
            await pn.Animated.timing(opacity, to=0.5, duration=200)

        pn.use_effect(fade_in, [])

        return pn.Animated.View(
            pn.Text("Hello!"),
            style={"opacity": opacity, "padding": 20},
        )
    ```
"""

from __future__ import annotations

import asyncio
import bisect
import itertools
import math
import threading
import time
import weakref
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple

from .element import Element
from .hooks import use_effect, use_ref
from .runtime import resolve_future
from .style import StyleProp, resolve_style

# Maximum frame rate at which the Python fallback ticker drives
# animations (native-driven animations run at the display's refresh
# rate, managed by the platform).
_TARGET_FPS = 60.0
_FRAME_DT = 1.0 / _TARGET_FPS

# Upper bound on how much wall-clock time the fallback loop will try to
# catch up on in a single iteration after thread starvation. At 60 fps
# this is ~333 ms of simulated motion; further drift is dropped to keep
# the loop responsive.
_MAX_CATCHUP_FRAMES = 20

_EASINGS: Dict[str, Callable[[float], float]] = {
    "linear": lambda t: t,
    "ease_in": lambda t: t * t,
    "ease_out": lambda t: 1.0 - (1.0 - t) * (1.0 - t),
    "ease_in_out": lambda t: 3.0 * t * t - 2.0 * t * t * t,
    "ease_in_quad": lambda t: t * t,
    "ease_out_quad": lambda t: 1.0 - (1.0 - t) * (1.0 - t),
    "bounce": lambda t: (
        # Robert Penner's bounce out, common easing.
        7.5625 * t * t
        if t < 1 / 2.75
        else (
            7.5625 * (t - 1.5 / 2.75) * (t - 1.5 / 2.75) + 0.75
            if t < 2 / 2.75
            else (
                7.5625 * (t - 2.25 / 2.75) * (t - 2.25 / 2.75) + 0.9375
                if t < 2.5 / 2.75
                else 7.5625 * (t - 2.625 / 2.75) * (t - 2.625 / 2.75) + 0.984375
            )
        )
    ),
}


def _resolve_easing(name: Any) -> Callable[[float], float]:
    if callable(name):
        return name
    return _EASINGS.get(str(name), _EASINGS["ease_in_out"])


def _backend() -> Any:
    """Return the active native-view registry (the animation backend)."""
    from .native_views import get_registry

    return get_registry()


# Process-unique ids for native animations, so completion callbacks can
# be routed without holding references on the native side.
_anim_id_counter = itertools.count(1)


# ======================================================================
# AnimatedNode: the shared graph-node base
# ======================================================================


class AnimatedNode:
    """Base class for every animated node (settable leaves and derived nodes).

    An animated node holds a current output value and a set of
    ``(tag, prop)`` **attachments** binding it to native view
    properties. Whenever the node's output changes (a leaf was set or
    animated, or an input of a derived node changed), the new value is
    pushed to every attachment through the registry's
    ``set_animated_property`` and to every Python-side listener, then
    propagated to derived nodes built from this one.

    Derived nodes are constructed with
    [`interpolate`][pythonnative.animated.AnimatedNode.interpolate],
    with Python arithmetic operators (``+``, ``-``, ``*``, ``/``,
    ``%``, unary ``-``), or with ``Animated.diff_clamp``. They are
    read-only: only [`AnimatedValue`][pythonnative.AnimatedValue]
    leaves can be set or animated directly.
    """

    __slots__ = ("_subscribers", "_attachments", "_lock", "_children", "__weakref__")

    def __init__(self) -> None:
        self._subscribers: List[Tuple[str, Callable[[Any], None]]] = []
        self._attachments: List[Tuple[int, str]] = []
        self._lock = threading.Lock()
        # Derived nodes built from this one. Weak so a discarded
        # interpolation doesn't keep receiving pushes forever.
        self._children: "weakref.WeakSet[AnimatedNode]" = weakref.WeakSet()

    # -- value -----------------------------------------------------------

    @property
    def value(self) -> Any:
        """Return the node's current output value."""
        raise NotImplementedError

    def __float__(self) -> float:
        try:
            return float(self.value)
        except (TypeError, ValueError):
            return 0.0

    # -- graph -----------------------------------------------------------

    def _adopt_child(self, child: "AnimatedNode") -> None:
        with self._lock:
            self._children.add(child)

    def _has_dependents(self) -> bool:
        """Whether any derived node consumes this node's output."""
        with self._lock:
            return len(self._children) > 0

    def _refresh(self) -> None:
        """Hook for stateful derived nodes to update from their inputs."""

    def _propagate(self) -> None:
        """Push the current output to attachments/listeners and descend."""
        self._refresh()
        current = self.value
        with self._lock:
            subs = list(self._subscribers)
            attachments = list(self._attachments)
            children = list(self._children)
        if attachments:
            try:
                backend = _backend()
                for tag, prop in attachments:
                    backend.set_animated_property(tag, prop, current)
            except Exception:
                pass
        for _prop, cb in subs:
            try:
                cb(current)
            except Exception:
                pass
        for child in children:
            child._propagate()

    # -- bindings ----------------------------------------------------------

    def attach(self, tag: int, prop: str) -> Callable[[], None]:
        """Bind this node to ``prop`` of the native view under ``tag``.

        The current value is pushed immediately so the view reflects it
        even if no animation is running. Returns a detach callable.
        """
        binding = (tag, prop)
        with self._lock:
            self._attachments.append(binding)
        try:
            _backend().set_animated_property(tag, prop, self.value)
        except Exception:
            pass

        def _detach() -> None:
            with self._lock:
                try:
                    self._attachments.remove(binding)
                except ValueError:
                    pass

        return _detach

    def attachments(self) -> List[Tuple[int, str]]:
        """Snapshot of the current ``(tag, prop)`` bindings."""
        with self._lock:
            return list(self._attachments)

    # -- listeners ---------------------------------------------------------

    def add_listener(self, prop: str, callback: Callable[[Any], None]) -> Callable[[], None]:
        """Register ``callback`` for Python-driven changes to this node.

        Returns an unsubscribe callable. ``prop`` is metadata only; it
        lets the subscriber differentiate this binding from others on
        the same node.
        """
        with self._lock:
            self._subscribers.append((prop, callback))

        def _unsubscribe() -> None:
            with self._lock:
                try:
                    self._subscribers.remove((prop, callback))
                except ValueError:
                    pass

        return _unsubscribe

    def has_listeners(self) -> bool:
        """Whether any Python-side listeners are registered."""
        with self._lock:
            return bool(self._subscribers)

    # -- derivation --------------------------------------------------------

    def interpolate(
        self,
        input_range: Sequence[float],
        output_range: Sequence[Any],
        extrapolate: str = "extend",
        extrapolate_left: Optional[str] = None,
        extrapolate_right: Optional[str] = None,
    ) -> "AnimatedInterpolation":
        """Map this node's value through an input/output range.

        Mirrors React Native's ``interpolate``. ``output_range`` may
        contain numbers, colors (``"#RRGGBB"`` / ``"#AARRGGBB"``), or
        angle strings (``"45deg"`` / ``"0.5rad"``, emitted as numeric
        degrees for the ``rotate`` transform).

        Args:
            input_range: Monotonically non-decreasing breakpoints for
                this node's value. At least two entries.
            output_range: Output breakpoints, same length as
                ``input_range``.
            extrapolate: Behavior outside the input range:
                ``"extend"`` (continue the edge segment's slope,
                default), ``"clamp"`` (pin to the edge output), or
                ``"identity"`` (return the input unchanged).
            extrapolate_left: Override ``extrapolate`` below the range.
            extrapolate_right: Override ``extrapolate`` above the range.

        Returns:
            A derived, read-only animated node.

        Example:
            ```python
            header_height = scroll_y.interpolate(
                input_range=[0, 120],
                output_range=[160, 56],
                extrapolate="clamp",
            )
            ```
        """
        return AnimatedInterpolation(
            self,
            input_range,
            output_range,
            extrapolate=extrapolate,
            extrapolate_left=extrapolate_left,
            extrapolate_right=extrapolate_right,
        )

    # -- arithmetic --------------------------------------------------------

    def __add__(self, other: Any) -> "_AnimatedOperation":
        return _AnimatedOperation("add", lambda a, b: a + b, [self, other])

    def __radd__(self, other: Any) -> "_AnimatedOperation":
        return _AnimatedOperation("add", lambda a, b: a + b, [other, self])

    def __sub__(self, other: Any) -> "_AnimatedOperation":
        return _AnimatedOperation("subtract", lambda a, b: a - b, [self, other])

    def __rsub__(self, other: Any) -> "_AnimatedOperation":
        return _AnimatedOperation("subtract", lambda a, b: a - b, [other, self])

    def __mul__(self, other: Any) -> "_AnimatedOperation":
        return _AnimatedOperation("multiply", lambda a, b: a * b, [self, other])

    def __rmul__(self, other: Any) -> "_AnimatedOperation":
        return _AnimatedOperation("multiply", lambda a, b: a * b, [other, self])

    def __truediv__(self, other: Any) -> "_AnimatedOperation":
        return _AnimatedOperation("divide", lambda a, b: a / b if b else 0.0, [self, other])

    def __rtruediv__(self, other: Any) -> "_AnimatedOperation":
        return _AnimatedOperation("divide", lambda a, b: a / b if b else 0.0, [other, self])

    def __mod__(self, other: Any) -> "_AnimatedOperation":
        return _AnimatedOperation("modulo", lambda a, b: math.fmod(a, b) if b else 0.0, [self, other])

    def __neg__(self) -> "_AnimatedOperation":
        return _AnimatedOperation("negate", lambda a: -a, [self])


# ======================================================================
# AnimatedValue: the settable leaf
# ======================================================================


class AnimatedValue(AnimatedNode):
    """A numeric cell that can be attached to native view properties.

    Animated components (``Animated.View`` et al.) **attach** the value
    to ``(tag, prop)`` bindings after mount. Setting the value pushes
    the new number to every attached native view through the registry's
    ``set_animated_property`` (and through every derived node built
    from this value), and when an animation can be driven natively, the
    platform animates those same bindings directly.

    Python-side listeners registered via
    [`add_listener`][pythonnative.animated.AnimatedNode.add_listener]
    observe every Python-driven change. Natively-driven animations
    intentionally skip per-frame Python callbacks (that's the point);
    listeners see the final settled value.
    """

    __slots__ = ("_value", "_native_group")

    def __init__(self, initial: float = 0.0) -> None:
        super().__init__()
        self._value = float(initial)
        # The in-flight native animation group driving this value, if any.
        self._native_group: Optional["_NativeAnimationGroup"] = None

    @property
    def value(self) -> float:
        """Return the current numeric value (without subscribing)."""
        return self._value

    def set_value(self, new_value: float) -> None:
        """Set the value immediately, pushing to native views and listeners."""
        self._apply(float(new_value), push_native=True)

    def _apply(self, new_value: float, push_native: bool) -> None:
        with self._lock:
            self._value = new_value
            subs = list(self._subscribers)
            attachments = list(self._attachments)
            children = list(self._children)
        if push_native and attachments:
            try:
                backend = _backend()
                for tag, prop in attachments:
                    backend.set_animated_property(tag, prop, new_value)
            except Exception:
                pass
        for prop, cb in subs:
            try:
                cb(new_value)
            except Exception:
                pass
        for child in children:
            child._propagate()

    # -- native handoff ------------------------------------------------

    def _adopt_native_group(self, group: Optional["_NativeAnimationGroup"]) -> None:
        previous = self._native_group
        self._native_group = group
        if previous is not None and previous is not group:
            previous.cancel()

    def stop_animation(self) -> None:
        """Cancel any in-flight animation on this value (native or Python)."""
        self._adopt_native_group(None)
        _manager.cancel_for_value(self)

    def __repr__(self) -> str:
        return f"AnimatedValue({self._value:g})"


# ======================================================================
# Derived nodes
# ======================================================================


def _parse_color_output(value: str) -> Optional[Tuple[int, int, int, int]]:
    """Parse ``"#RRGGBB"`` / ``"#AARRGGBB"`` into an ``(a, r, g, b)`` tuple."""
    c = value.strip().lstrip("#")
    if len(c) == 6:
        c = "FF" + c
    if len(c) != 8:
        return None
    try:
        raw = int(c, 16)
    except ValueError:
        return None
    return ((raw >> 24) & 0xFF, (raw >> 16) & 0xFF, (raw >> 8) & 0xFF, raw & 0xFF)


def _parse_angle_output(value: str) -> Optional[float]:
    """Parse ``"45deg"`` / ``"0.5rad"`` into numeric degrees."""
    text = value.strip()
    try:
        if text.endswith("deg"):
            return float(text[:-3])
        if text.endswith("rad"):
            return math.degrees(float(text[:-3]))
    except ValueError:
        return None
    return None


class AnimatedInterpolation(AnimatedNode):
    """Read-only node mapping a parent node through an input/output range.

    Built via
    [`AnimatedNode.interpolate`][pythonnative.animated.AnimatedNode.interpolate];
    see that method for the semantics of the arguments.
    """

    __slots__ = (
        "_parent",
        "_inputs",
        "_outputs",
        "_kind",
        "_left",
        "_right",
    )

    def __init__(
        self,
        parent: AnimatedNode,
        input_range: Sequence[float],
        output_range: Sequence[Any],
        extrapolate: str = "extend",
        extrapolate_left: Optional[str] = None,
        extrapolate_right: Optional[str] = None,
    ) -> None:
        super().__init__()
        inputs = [float(v) for v in input_range]
        outputs = list(output_range)
        if len(inputs) < 2:
            raise ValueError("interpolate() needs at least two input_range entries")
        if len(inputs) != len(outputs):
            raise ValueError("interpolate() input_range and output_range must have the same length")
        for a, b in zip(inputs, inputs[1:]):
            if b < a:
                raise ValueError("interpolate() input_range must be monotonically non-decreasing")

        kind = "number"
        first = outputs[0]
        if isinstance(first, str):
            if _parse_color_output(first) is not None:
                kind = "color"
                outputs = [_parse_color_output(str(v)) for v in outputs]
                if any(v is None for v in outputs):
                    raise ValueError("interpolate() color output_range entries must all be colors")
            else:
                angles = [_parse_angle_output(str(v)) for v in outputs]
                if any(v is None for v in angles):
                    raise ValueError(f"interpolate() cannot parse output value {first!r}")
                outputs = angles
        else:
            outputs = [float(v) for v in outputs]

        self._parent = parent
        self._inputs = inputs
        self._outputs: List[Any] = outputs
        self._kind = kind
        self._left = extrapolate_left or extrapolate
        self._right = extrapolate_right or extrapolate
        parent._adopt_child(self)

    @property
    def value(self) -> Any:
        """Return the interpolated output for the parent's current value."""
        return self._compute(float(self._parent))

    def _compute(self, x: float) -> Any:
        inputs = self._inputs
        n = len(inputs)
        if x < inputs[0]:
            if self._left == "identity":
                return x
            if self._left == "clamp":
                x = inputs[0]
            i = 0
        elif x > inputs[-1]:
            if self._right == "identity":
                return x
            if self._right == "clamp":
                x = inputs[-1]
            i = n - 2
        else:
            i = max(0, min(n - 2, bisect.bisect_right(inputs, x) - 1))

        x0, x1 = inputs[i], inputs[i + 1]
        span = x1 - x0
        t = 0.0 if span  str:
        return f"AnimatedInterpolation({self._inputs} -> {self._outputs})"


class _AnimatedOperation(AnimatedNode):
    """Read-only node computed from other nodes (and constants) by ``fn``."""

    __slots__ = ("_op", "_fn", "_parents")

    def __init__(self, op: str, fn: Callable[..., float], parents: List[Any]) -> None:
        super().__init__()
        self._op = op
        self._fn = fn
        self._parents = list(parents)
        for parent in self._parents:
            if isinstance(parent, AnimatedNode):
                parent._adopt_child(self)

    @property
    def value(self) -> float:
        args = [float(p) if isinstance(p, AnimatedNode) else float(p) for p in self._parents]
        try:
            return float(self._fn(*args))
        except Exception:
            return 0.0

    def __repr__(self) -> str:
        return f"AnimatedOperation({self._op})"


class _AnimatedDiffClamp(AnimatedNode):
    """Accumulate a parent's *deltas* into a clamped range.

    Mirrors React Native's ``Animated.diffClamp``: the output moves by
    the same amount as the input but is pinned to ``[min, max]``, so
    scrolling far down then slightly up immediately re-reveals a
    collapsing header regardless of absolute offset.
    """

    __slots__ = ("_parent", "_min", "_max", "_last_input", "_current")

    def __init__(self, parent: AnimatedNode, min_value: float, max_value: float) -> None:
        super().__init__()
        if max_value < min_value:
            raise ValueError("diff_clamp() requires min_value  _AnimationHandle:
        """Run a damped harmonic spring toward ``to``."""

        def _spec() -> Dict[str, Any]:
            return {
                "kind": "spring",
                "from": value.value,
                "to": float(to),
                "stiffness": float(stiffness),
                "damping": float(damping),
                "mass": float(mass),
                "initial_velocity": float(initial_velocity),
            }

        def _fallback() -> _RunningAnimation:
            return _SpringAnimation(value, to, stiffness, damping, mass, initial_velocity)

        return _AnimationHandle(value, _spec, _fallback)

    @staticmethod
    def decay(
        value: AnimatedValue,
        *,
        velocity: float,
        deceleration: float = 0.997,
    ) -> _AnimationHandle:
        """Decelerate ``value`` from ``velocity`` (units/ms) until it rests."""

        def _spec() -> Dict[str, Any]:
            return {
                "kind": "decay",
                "from": value.value,
                "velocity": float(velocity),
                "deceleration": float(deceleration),
            }

        def _fallback() -> _RunningAnimation:
            return _DecayAnimation(value, velocity, deceleration)

        return _AnimationHandle(value, _spec, _fallback)

    @staticmethod
    def parallel(animations: List[Any]) -> _CompositeAnimation:
        """Run all ``animations`` concurrently; complete when all finish."""
        return _CompositeAnimation(animations, "parallel")

    @staticmethod
    def sequence(animations: List[Any]) -> _CompositeAnimation:
        """Run ``animations`` one after another."""
        return _CompositeAnimation(animations, "sequence")

    @staticmethod
    def stagger(delay: float, animations: List[Any]) -> _CompositeAnimation:
        """Run ``animations`` in parallel, each starting ``delay`` ms after the previous.

        Args:
            delay: Milliseconds between successive starts.
            animations: Animation handles (or awaitables) to run.

        Example:
            ```python
            pn.Animated.stagger(80, [
                pn.Animated.timing(v, to=1.0) for v in card_opacities
            ]).start()
            ```
        """
        return _CompositeAnimation(animations, "stagger", stagger_ms=delay)

    @staticmethod
    def loop(animation: Any, *, iterations: int = -1, reset: bool = True) -> _LoopAnimation:
        """Repeat ``animation``, optionally forever.

        Values driven by the animation are captured when the loop
        starts and restored before each iteration (matching React
        Native's ``resetBeforeIteration``), so ``timing`` loops replay
        the same motion instead of animating in place.

        Args:
            animation: A handle from ``timing`` / ``spring`` / ``decay``
                or a ``sequence`` / ``parallel`` / ``stagger`` composite.
            iterations: Number of repetitions; ``-1`` (default) loops
                until [`stop`][pythonnative.animated._LoopAnimation.stop]
                is called or the awaiting task is cancelled.
            reset: When ``False``, values continue from wherever the
                previous iteration ended.

        Example:
            ```python
            pulse = pn.Animated.loop(
                pn.Animated.sequence([
                    pn.Animated.timing(scale, to=1.15, duration=350),
                    pn.Animated.timing(scale, to=1.0, duration=350),
                ]),
            ).start()
            # later: pulse.stop()
            ```
        """
        return _LoopAnimation(animation, iterations=iterations, reset=reset)

    @staticmethod
    def delay(duration: float) -> _AnimationHandle:
        """Wait ``duration`` ms before continuing in a sequence."""

        def _spec() -> Dict[str, Any]:
            return {"kind": "delay", "duration_ms": float(duration)}

        def _fallback() -> _RunningAnimation:
            return _DelayAnimation(duration)

        return _AnimationHandle(None, _spec, _fallback)

    @staticmethod
    def event(listener: Optional[Callable[..., None]] = None, **bindings: AnimatedValue) -> AnimatedEvent:
        """Build a callback that copies event fields into animated values.

        Pass the result to any event prop. Each keyword maps a payload
        field (dict key or dataclass attribute) onto an
        [`AnimatedValue`][pythonnative.AnimatedValue]:

        ```python
        scroll_y = pn.use_animated_value(0.0)
        pn.ScrollView(..., on_scroll=pn.Animated.event(y=scroll_y))

        tx = pn.use_animated_value(0.0)
        gestures.Pan(on_change=pn.Animated.event(translation_x=tx))
        ```

        Args:
            listener: Optional plain callback invoked with the raw
                event after the values update.
            **bindings: ``field_name=animated_value`` pairs.

        Returns:
            A callable [`AnimatedEvent`][pythonnative.animated.AnimatedEvent].
        """
        return AnimatedEvent(listener, **bindings)

    @staticmethod
    def diff_clamp(node: AnimatedNode, min_value: float, max_value: float) -> AnimatedNode:
        """Accumulate ``node``'s deltas into ``[min_value, max_value]``.

        The classic collapsing-header primitive: unlike ``interpolate``
        with ``"clamp"`` (which pins the *absolute* input), the output
        tracks input *movement*, so a small scroll upward immediately
        re-reveals the header no matter how far down the list is.

        ```python
        header_shift = pn.Animated.diff_clamp(scroll_y, 0, 56)
        ```
        """
        return _AnimatedDiffClamp(node, min_value, max_value)

    View = staticmethod(_make_animated_factory("View", accept_children=True))
    Text = staticmethod(_make_animated_factory("Text", accept_children=False))
    Image = staticmethod(_make_animated_factory("Image", accept_children=False))


Animated = _AnimatedNamespace()


def use_animated_value(initial: float = 0.0) -> AnimatedValue:
    """Return an [`AnimatedValue`][pythonnative.AnimatedValue] that is stable across renders.

    Convenience wrapper for the common pattern
    ``pn.use_memo(lambda: AnimatedValue(initial), [])``. The same
    instance is returned on every render of the same component, so
    you can drive it from event handlers without recreating it.

    Args:
        initial: The starting numeric value.

    Returns:
        A mount-stable [`AnimatedValue`][pythonnative.AnimatedValue].

    Example:
        ```python
        import pythonnative as pn


        @pn.component
        def FadeIn():
            opacity = pn.use_animated_value(0.0)

            async def fade_in():
                await pn.Animated.timing(opacity, to=1.0, duration=300)

            pn.use_effect(fade_in, [])
            return pn.Animated.View(
                pn.Text("Hello"),
                style=pn.style(opacity=opacity),
            )
        ```
    """
    from .hooks import use_memo

    return use_memo(lambda: AnimatedValue(initial), [])


__all__ = [
    "AnimatedNode",
    "AnimatedValue",
    "AnimatedInterpolation",
    "AnimatedEvent",
    "Animated",
    "use_animated_value",
    "native_animation_completed",
]

Web Proxy Viewer  |  New URL  |  Original Page