pythonnative 0.25.0__py3-none-any.whl → 0.27.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pythonnative/__init__.py +1 -1
- pythonnative/animated.py +697 -62
- pythonnative/cli/pn.py +172 -12
- pythonnative/components.py +61 -7
- pythonnative/diagnostics.py +27 -0
- pythonnative/gestures.py +446 -34
- pythonnative/native_modules/app_state.py +2 -1
- pythonnative/native_modules/battery.py +3 -2
- pythonnative/native_modules/biometrics.py +2 -1
- pythonnative/native_modules/camera.py +16 -7
- pythonnative/native_modules/clipboard.py +3 -2
- pythonnative/native_modules/file_system.py +3 -2
- pythonnative/native_modules/haptics.py +7 -6
- pythonnative/native_modules/linking.py +62 -6
- pythonnative/native_modules/location.py +11 -8
- pythonnative/native_modules/net_info.py +75 -5
- pythonnative/native_modules/notifications.py +134 -15
- pythonnative/native_modules/permissions.py +3 -2
- pythonnative/native_modules/share.py +2 -1
- pythonnative/native_views/android.py +461 -137
- pythonnative/native_views/desktop.py +816 -96
- pythonnative/native_views/ios.py +750 -193
- pythonnative/project/android.py +15 -3
- pythonnative/project/builder.py +172 -70
- pythonnative/project/config.py +28 -6
- pythonnative/project/devices.py +291 -0
- pythonnative/project/doctor.py +5 -4
- pythonnative/project/ios.py +51 -77
- pythonnative/project/permissions.py +19 -1
- pythonnative/project/runtime_assets.py +78 -166
- pythonnative/reconciler.py +28 -0
- pythonnative/style.py +19 -0
- pythonnative/templates/android_template/app/build.gradle +9 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +5 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +148 -12
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNFrameLayout.kt +50 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +41 -17
- pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +5 -2
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +397 -0
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +31 -20
- pythonnative/templates/ios_template/ios_template/ViewController.swift +77 -198
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +72 -47
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/METADATA +3 -3
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/RECORD +49 -46
- pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -24
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.25.0.dist-info → pythonnative-0.27.0.dist-info}/top_level.txt +0 -0
pythonnative/animated.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Animated values,
|
|
1
|
+
"""Animated values, derived animated nodes, and native-driven animation.
|
|
2
2
|
|
|
3
3
|
Modeled on React Native's ``Animated`` API with an ``async``-aware
|
|
4
4
|
completion contract. The core primitives are:
|
|
@@ -6,14 +6,26 @@ completion contract. The core primitives are:
|
|
|
6
6
|
- [`AnimatedValue`][pythonnative.animated.AnimatedValue]: a numeric
|
|
7
7
|
cell attached to native view properties; animations drive it over
|
|
8
8
|
time.
|
|
9
|
+
- **Derived nodes**: every animated node supports
|
|
10
|
+
[`interpolate`][pythonnative.animated.AnimatedNode.interpolate]
|
|
11
|
+
(range mapping with numeric, color, and angle outputs) and Python
|
|
12
|
+
arithmetic (``opacity * 0.5``, ``x + y``, ``-value``), producing
|
|
13
|
+
read-only [`AnimatedNode`][pythonnative.animated.AnimatedNode]
|
|
14
|
+
instances that update whenever their inputs change.
|
|
9
15
|
- ``Animated.timing`` / ``Animated.spring`` / ``Animated.decay``:
|
|
10
16
|
animation factories. The objects they return implement
|
|
11
17
|
``__await__``, so you can write ``await Animated.timing(v, to=1.0)``
|
|
12
18
|
to suspend until the animation finishes.
|
|
13
|
-
- ``Animated.sequence`` / ``Animated.parallel`` / ``Animated.
|
|
14
|
-
composition; also
|
|
19
|
+
- ``Animated.sequence`` / ``Animated.parallel`` / ``Animated.stagger``
|
|
20
|
+
/ ``Animated.delay`` / ``Animated.loop``: composition; also
|
|
21
|
+
awaitable.
|
|
22
|
+
- ``Animated.event``: build an event-prop callback that copies event
|
|
23
|
+
fields into animated values (``on_scroll=pn.Animated.event(y=v)``).
|
|
24
|
+
- ``Animated.diff_clamp``: accumulate an input's *deltas* into a
|
|
25
|
+
clamped range (the collapsing-header primitive).
|
|
15
26
|
- ``Animated.View`` / ``Animated.Text`` / ``Animated.Image``:
|
|
16
|
-
components whose ``style`` may contain
|
|
27
|
+
components whose ``style`` may contain animated nodes, including
|
|
28
|
+
inside ``transform`` entries.
|
|
17
29
|
|
|
18
30
|
Driver architecture (the **native driver**):
|
|
19
31
|
|
|
@@ -29,10 +41,15 @@ native view the value is attached to
|
|
|
29
41
|
[`AnimatedValue`][pythonnative.animated.AnimatedValue], and resolves
|
|
30
42
|
any awaiting tasks.
|
|
31
43
|
- **Declined** (desktop preview, unattached values, callable easings,
|
|
32
|
-
values feeding Python-side listeners): a single
|
|
33
|
-
ticks the animation at ~60 Hz from Python, pushing
|
|
34
|
-
``set_animated_property``. Semantics are
|
|
35
|
-
source differs.
|
|
44
|
+
values feeding Python-side listeners or derived nodes): a single
|
|
45
|
+
background thread ticks the animation at ~60 Hz from Python, pushing
|
|
46
|
+
each frame through ``set_animated_property``. Semantics are
|
|
47
|
+
identical; only the frame source differs.
|
|
48
|
+
|
|
49
|
+
Values driven by *events* (scroll offsets via ``Animated.event``,
|
|
50
|
+
gesture translations) flow through Python: the native listener fires,
|
|
51
|
+
the bound values update, and every attachment (including derived
|
|
52
|
+
nodes) is pushed in the same call.
|
|
36
53
|
|
|
37
54
|
Example:
|
|
38
55
|
```python
|
|
@@ -59,11 +76,13 @@ Example:
|
|
|
59
76
|
from __future__ import annotations
|
|
60
77
|
|
|
61
78
|
import asyncio
|
|
79
|
+
import bisect
|
|
62
80
|
import itertools
|
|
63
81
|
import math
|
|
64
82
|
import threading
|
|
65
83
|
import time
|
|
66
|
-
|
|
84
|
+
import weakref
|
|
85
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
|
67
86
|
|
|
68
87
|
from .element import Element
|
|
69
88
|
from .hooks import use_effect, use_ref
|
|
@@ -125,67 +144,93 @@ _anim_id_counter = itertools.count(1)
|
|
|
125
144
|
|
|
126
145
|
|
|
127
146
|
# ======================================================================
|
|
128
|
-
#
|
|
147
|
+
# AnimatedNode: the shared graph-node base
|
|
129
148
|
# ======================================================================
|
|
130
149
|
|
|
131
150
|
|
|
132
|
-
class
|
|
133
|
-
"""
|
|
151
|
+
class AnimatedNode:
|
|
152
|
+
"""Base class for every animated node (settable leaves and derived nodes).
|
|
134
153
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
154
|
+
An animated node holds a current output value and a set of
|
|
155
|
+
``(tag, prop)`` **attachments** binding it to native view
|
|
156
|
+
properties. Whenever the node's output changes (a leaf was set or
|
|
157
|
+
animated, or an input of a derived node changed), the new value is
|
|
158
|
+
pushed to every attachment through the registry's
|
|
159
|
+
``set_animated_property`` and to every Python-side listener, then
|
|
160
|
+
propagated to derived nodes built from this one.
|
|
140
161
|
|
|
141
|
-
|
|
142
|
-
[`
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
162
|
+
Derived nodes are constructed with
|
|
163
|
+
[`interpolate`][pythonnative.animated.AnimatedNode.interpolate],
|
|
164
|
+
with Python arithmetic operators (``+``, ``-``, ``*``, ``/``,
|
|
165
|
+
``%``, unary ``-``), or with ``Animated.diff_clamp``. They are
|
|
166
|
+
read-only: only [`AnimatedValue`][pythonnative.AnimatedValue]
|
|
167
|
+
leaves can be set or animated directly.
|
|
146
168
|
"""
|
|
147
169
|
|
|
148
|
-
__slots__ = ("
|
|
170
|
+
__slots__ = ("_subscribers", "_attachments", "_lock", "_children", "__weakref__")
|
|
149
171
|
|
|
150
|
-
def __init__(self
|
|
151
|
-
self.
|
|
152
|
-
self._subscribers: List[Tuple[str, Callable[[float], None]]] = []
|
|
172
|
+
def __init__(self) -> None:
|
|
173
|
+
self._subscribers: List[Tuple[str, Callable[[Any], None]]] = []
|
|
153
174
|
self._attachments: List[Tuple[int, str]] = []
|
|
154
175
|
self._lock = threading.Lock()
|
|
155
|
-
#
|
|
156
|
-
|
|
176
|
+
# Derived nodes built from this one. Weak so a discarded
|
|
177
|
+
# interpolation doesn't keep receiving pushes forever.
|
|
178
|
+
self._children: "weakref.WeakSet[AnimatedNode]" = weakref.WeakSet()
|
|
179
|
+
|
|
180
|
+
# -- value -----------------------------------------------------------
|
|
157
181
|
|
|
158
182
|
@property
|
|
159
|
-
def value(self) ->
|
|
160
|
-
"""Return the current
|
|
161
|
-
|
|
183
|
+
def value(self) -> Any:
|
|
184
|
+
"""Return the node's current output value."""
|
|
185
|
+
raise NotImplementedError
|
|
162
186
|
|
|
163
|
-
def
|
|
164
|
-
|
|
165
|
-
|
|
187
|
+
def __float__(self) -> float:
|
|
188
|
+
try:
|
|
189
|
+
return float(self.value)
|
|
190
|
+
except (TypeError, ValueError):
|
|
191
|
+
return 0.0
|
|
166
192
|
|
|
167
|
-
|
|
193
|
+
# -- graph -----------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
def _adopt_child(self, child: "AnimatedNode") -> None:
|
|
196
|
+
with self._lock:
|
|
197
|
+
self._children.add(child)
|
|
198
|
+
|
|
199
|
+
def _has_dependents(self) -> bool:
|
|
200
|
+
"""Whether any derived node consumes this node's output."""
|
|
201
|
+
with self._lock:
|
|
202
|
+
return len(self._children) > 0
|
|
203
|
+
|
|
204
|
+
def _refresh(self) -> None:
|
|
205
|
+
"""Hook for stateful derived nodes to update from their inputs."""
|
|
206
|
+
|
|
207
|
+
def _propagate(self) -> None:
|
|
208
|
+
"""Push the current output to attachments/listeners and descend."""
|
|
209
|
+
self._refresh()
|
|
210
|
+
current = self.value
|
|
168
211
|
with self._lock:
|
|
169
|
-
self._value = new_value
|
|
170
212
|
subs = list(self._subscribers)
|
|
171
213
|
attachments = list(self._attachments)
|
|
172
|
-
|
|
214
|
+
children = list(self._children)
|
|
215
|
+
if attachments:
|
|
173
216
|
try:
|
|
174
217
|
backend = _backend()
|
|
175
218
|
for tag, prop in attachments:
|
|
176
|
-
backend.set_animated_property(tag, prop,
|
|
219
|
+
backend.set_animated_property(tag, prop, current)
|
|
177
220
|
except Exception:
|
|
178
221
|
pass
|
|
179
|
-
for
|
|
222
|
+
for _prop, cb in subs:
|
|
180
223
|
try:
|
|
181
|
-
cb(
|
|
224
|
+
cb(current)
|
|
182
225
|
except Exception:
|
|
183
226
|
pass
|
|
227
|
+
for child in children:
|
|
228
|
+
child._propagate()
|
|
184
229
|
|
|
185
|
-
# -- bindings
|
|
230
|
+
# -- bindings ----------------------------------------------------------
|
|
186
231
|
|
|
187
232
|
def attach(self, tag: int, prop: str) -> Callable[[], None]:
|
|
188
|
-
"""Bind this
|
|
233
|
+
"""Bind this node to ``prop`` of the native view under ``tag``.
|
|
189
234
|
|
|
190
235
|
The current value is pushed immediately so the view reflects it
|
|
191
236
|
even if no animation is running. Returns a detach callable.
|
|
@@ -194,7 +239,7 @@ class AnimatedValue:
|
|
|
194
239
|
with self._lock:
|
|
195
240
|
self._attachments.append(binding)
|
|
196
241
|
try:
|
|
197
|
-
_backend().set_animated_property(tag, prop, self.
|
|
242
|
+
_backend().set_animated_property(tag, prop, self.value)
|
|
198
243
|
except Exception:
|
|
199
244
|
pass
|
|
200
245
|
|
|
@@ -212,14 +257,14 @@ class AnimatedValue:
|
|
|
212
257
|
with self._lock:
|
|
213
258
|
return list(self._attachments)
|
|
214
259
|
|
|
215
|
-
# -- listeners
|
|
260
|
+
# -- listeners ---------------------------------------------------------
|
|
216
261
|
|
|
217
|
-
def add_listener(self, prop: str, callback: Callable[[
|
|
218
|
-
"""Register ``callback`` for Python-driven changes to this
|
|
262
|
+
def add_listener(self, prop: str, callback: Callable[[Any], None]) -> Callable[[], None]:
|
|
263
|
+
"""Register ``callback`` for Python-driven changes to this node.
|
|
219
264
|
|
|
220
265
|
Returns an unsubscribe callable. ``prop`` is metadata only; it
|
|
221
266
|
lets the subscriber differentiate this binding from others on
|
|
222
|
-
the same
|
|
267
|
+
the same node.
|
|
223
268
|
"""
|
|
224
269
|
with self._lock:
|
|
225
270
|
self._subscribers.append((prop, callback))
|
|
@@ -238,6 +283,149 @@ class AnimatedValue:
|
|
|
238
283
|
with self._lock:
|
|
239
284
|
return bool(self._subscribers)
|
|
240
285
|
|
|
286
|
+
# -- derivation --------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
def interpolate(
|
|
289
|
+
self,
|
|
290
|
+
input_range: Sequence[float],
|
|
291
|
+
output_range: Sequence[Any],
|
|
292
|
+
extrapolate: str = "extend",
|
|
293
|
+
extrapolate_left: Optional[str] = None,
|
|
294
|
+
extrapolate_right: Optional[str] = None,
|
|
295
|
+
) -> "AnimatedInterpolation":
|
|
296
|
+
"""Map this node's value through an input/output range.
|
|
297
|
+
|
|
298
|
+
Mirrors React Native's ``interpolate``. ``output_range`` may
|
|
299
|
+
contain numbers, colors (``"#RRGGBB"`` / ``"#AARRGGBB"``), or
|
|
300
|
+
angle strings (``"45deg"`` / ``"0.5rad"``, emitted as numeric
|
|
301
|
+
degrees for the ``rotate`` transform).
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
input_range: Monotonically non-decreasing breakpoints for
|
|
305
|
+
this node's value. At least two entries.
|
|
306
|
+
output_range: Output breakpoints, same length as
|
|
307
|
+
``input_range``.
|
|
308
|
+
extrapolate: Behavior outside the input range:
|
|
309
|
+
``"extend"`` (continue the edge segment's slope,
|
|
310
|
+
default), ``"clamp"`` (pin to the edge output), or
|
|
311
|
+
``"identity"`` (return the input unchanged).
|
|
312
|
+
extrapolate_left: Override ``extrapolate`` below the range.
|
|
313
|
+
extrapolate_right: Override ``extrapolate`` above the range.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
A derived, read-only animated node.
|
|
317
|
+
|
|
318
|
+
Example:
|
|
319
|
+
```python
|
|
320
|
+
header_height = scroll_y.interpolate(
|
|
321
|
+
input_range=[0, 120],
|
|
322
|
+
output_range=[160, 56],
|
|
323
|
+
extrapolate="clamp",
|
|
324
|
+
)
|
|
325
|
+
```
|
|
326
|
+
"""
|
|
327
|
+
return AnimatedInterpolation(
|
|
328
|
+
self,
|
|
329
|
+
input_range,
|
|
330
|
+
output_range,
|
|
331
|
+
extrapolate=extrapolate,
|
|
332
|
+
extrapolate_left=extrapolate_left,
|
|
333
|
+
extrapolate_right=extrapolate_right,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# -- arithmetic --------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
def __add__(self, other: Any) -> "_AnimatedOperation":
|
|
339
|
+
return _AnimatedOperation("add", lambda a, b: a + b, [self, other])
|
|
340
|
+
|
|
341
|
+
def __radd__(self, other: Any) -> "_AnimatedOperation":
|
|
342
|
+
return _AnimatedOperation("add", lambda a, b: a + b, [other, self])
|
|
343
|
+
|
|
344
|
+
def __sub__(self, other: Any) -> "_AnimatedOperation":
|
|
345
|
+
return _AnimatedOperation("subtract", lambda a, b: a - b, [self, other])
|
|
346
|
+
|
|
347
|
+
def __rsub__(self, other: Any) -> "_AnimatedOperation":
|
|
348
|
+
return _AnimatedOperation("subtract", lambda a, b: a - b, [other, self])
|
|
349
|
+
|
|
350
|
+
def __mul__(self, other: Any) -> "_AnimatedOperation":
|
|
351
|
+
return _AnimatedOperation("multiply", lambda a, b: a * b, [self, other])
|
|
352
|
+
|
|
353
|
+
def __rmul__(self, other: Any) -> "_AnimatedOperation":
|
|
354
|
+
return _AnimatedOperation("multiply", lambda a, b: a * b, [other, self])
|
|
355
|
+
|
|
356
|
+
def __truediv__(self, other: Any) -> "_AnimatedOperation":
|
|
357
|
+
return _AnimatedOperation("divide", lambda a, b: a / b if b else 0.0, [self, other])
|
|
358
|
+
|
|
359
|
+
def __rtruediv__(self, other: Any) -> "_AnimatedOperation":
|
|
360
|
+
return _AnimatedOperation("divide", lambda a, b: a / b if b else 0.0, [other, self])
|
|
361
|
+
|
|
362
|
+
def __mod__(self, other: Any) -> "_AnimatedOperation":
|
|
363
|
+
return _AnimatedOperation("modulo", lambda a, b: math.fmod(a, b) if b else 0.0, [self, other])
|
|
364
|
+
|
|
365
|
+
def __neg__(self) -> "_AnimatedOperation":
|
|
366
|
+
return _AnimatedOperation("negate", lambda a: -a, [self])
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ======================================================================
|
|
370
|
+
# AnimatedValue: the settable leaf
|
|
371
|
+
# ======================================================================
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class AnimatedValue(AnimatedNode):
|
|
375
|
+
"""A numeric cell that can be attached to native view properties.
|
|
376
|
+
|
|
377
|
+
Animated components (``Animated.View`` et al.) **attach** the value
|
|
378
|
+
to ``(tag, prop)`` bindings after mount. Setting the value pushes
|
|
379
|
+
the new number to every attached native view through the registry's
|
|
380
|
+
``set_animated_property`` (and through every derived node built
|
|
381
|
+
from this value), and when an animation can be driven natively, the
|
|
382
|
+
platform animates those same bindings directly.
|
|
383
|
+
|
|
384
|
+
Python-side listeners registered via
|
|
385
|
+
[`add_listener`][pythonnative.animated.AnimatedNode.add_listener]
|
|
386
|
+
observe every Python-driven change. Natively-driven animations
|
|
387
|
+
intentionally skip per-frame Python callbacks (that's the point);
|
|
388
|
+
listeners see the final settled value.
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
__slots__ = ("_value", "_native_group")
|
|
392
|
+
|
|
393
|
+
def __init__(self, initial: float = 0.0) -> None:
|
|
394
|
+
super().__init__()
|
|
395
|
+
self._value = float(initial)
|
|
396
|
+
# The in-flight native animation group driving this value, if any.
|
|
397
|
+
self._native_group: Optional["_NativeAnimationGroup"] = None
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def value(self) -> float:
|
|
401
|
+
"""Return the current numeric value (without subscribing)."""
|
|
402
|
+
return self._value
|
|
403
|
+
|
|
404
|
+
def set_value(self, new_value: float) -> None:
|
|
405
|
+
"""Set the value immediately, pushing to native views and listeners."""
|
|
406
|
+
self._apply(float(new_value), push_native=True)
|
|
407
|
+
|
|
408
|
+
def _apply(self, new_value: float, push_native: bool) -> None:
|
|
409
|
+
with self._lock:
|
|
410
|
+
self._value = new_value
|
|
411
|
+
subs = list(self._subscribers)
|
|
412
|
+
attachments = list(self._attachments)
|
|
413
|
+
children = list(self._children)
|
|
414
|
+
if push_native and attachments:
|
|
415
|
+
try:
|
|
416
|
+
backend = _backend()
|
|
417
|
+
for tag, prop in attachments:
|
|
418
|
+
backend.set_animated_property(tag, prop, new_value)
|
|
419
|
+
except Exception:
|
|
420
|
+
pass
|
|
421
|
+
for prop, cb in subs:
|
|
422
|
+
try:
|
|
423
|
+
cb(new_value)
|
|
424
|
+
except Exception:
|
|
425
|
+
pass
|
|
426
|
+
for child in children:
|
|
427
|
+
child._propagate()
|
|
428
|
+
|
|
241
429
|
# -- native handoff ------------------------------------------------
|
|
242
430
|
|
|
243
431
|
def _adopt_native_group(self, group: Optional["_NativeAnimationGroup"]) -> None:
|
|
@@ -251,13 +439,272 @@ class AnimatedValue:
|
|
|
251
439
|
self._adopt_native_group(None)
|
|
252
440
|
_manager.cancel_for_value(self)
|
|
253
441
|
|
|
254
|
-
def __float__(self) -> float:
|
|
255
|
-
return self._value
|
|
256
|
-
|
|
257
442
|
def __repr__(self) -> str:
|
|
258
443
|
return f"AnimatedValue({self._value:g})"
|
|
259
444
|
|
|
260
445
|
|
|
446
|
+
# ======================================================================
|
|
447
|
+
# Derived nodes
|
|
448
|
+
# ======================================================================
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _parse_color_output(value: str) -> Optional[Tuple[int, int, int, int]]:
|
|
452
|
+
"""Parse ``"#RRGGBB"`` / ``"#AARRGGBB"`` into an ``(a, r, g, b)`` tuple."""
|
|
453
|
+
c = value.strip().lstrip("#")
|
|
454
|
+
if len(c) == 6:
|
|
455
|
+
c = "FF" + c
|
|
456
|
+
if len(c) != 8:
|
|
457
|
+
return None
|
|
458
|
+
try:
|
|
459
|
+
raw = int(c, 16)
|
|
460
|
+
except ValueError:
|
|
461
|
+
return None
|
|
462
|
+
return ((raw >> 24) & 0xFF, (raw >> 16) & 0xFF, (raw >> 8) & 0xFF, raw & 0xFF)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _parse_angle_output(value: str) -> Optional[float]:
|
|
466
|
+
"""Parse ``"45deg"`` / ``"0.5rad"`` into numeric degrees."""
|
|
467
|
+
text = value.strip()
|
|
468
|
+
try:
|
|
469
|
+
if text.endswith("deg"):
|
|
470
|
+
return float(text[:-3])
|
|
471
|
+
if text.endswith("rad"):
|
|
472
|
+
return math.degrees(float(text[:-3]))
|
|
473
|
+
except ValueError:
|
|
474
|
+
return None
|
|
475
|
+
return None
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class AnimatedInterpolation(AnimatedNode):
|
|
479
|
+
"""Read-only node mapping a parent node through an input/output range.
|
|
480
|
+
|
|
481
|
+
Built via
|
|
482
|
+
[`AnimatedNode.interpolate`][pythonnative.animated.AnimatedNode.interpolate];
|
|
483
|
+
see that method for the semantics of the arguments.
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
__slots__ = (
|
|
487
|
+
"_parent",
|
|
488
|
+
"_inputs",
|
|
489
|
+
"_outputs",
|
|
490
|
+
"_kind",
|
|
491
|
+
"_left",
|
|
492
|
+
"_right",
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
def __init__(
|
|
496
|
+
self,
|
|
497
|
+
parent: AnimatedNode,
|
|
498
|
+
input_range: Sequence[float],
|
|
499
|
+
output_range: Sequence[Any],
|
|
500
|
+
extrapolate: str = "extend",
|
|
501
|
+
extrapolate_left: Optional[str] = None,
|
|
502
|
+
extrapolate_right: Optional[str] = None,
|
|
503
|
+
) -> None:
|
|
504
|
+
super().__init__()
|
|
505
|
+
inputs = [float(v) for v in input_range]
|
|
506
|
+
outputs = list(output_range)
|
|
507
|
+
if len(inputs) < 2:
|
|
508
|
+
raise ValueError("interpolate() needs at least two input_range entries")
|
|
509
|
+
if len(inputs) != len(outputs):
|
|
510
|
+
raise ValueError("interpolate() input_range and output_range must have the same length")
|
|
511
|
+
for a, b in zip(inputs, inputs[1:]):
|
|
512
|
+
if b < a:
|
|
513
|
+
raise ValueError("interpolate() input_range must be monotonically non-decreasing")
|
|
514
|
+
|
|
515
|
+
kind = "number"
|
|
516
|
+
first = outputs[0]
|
|
517
|
+
if isinstance(first, str):
|
|
518
|
+
if _parse_color_output(first) is not None:
|
|
519
|
+
kind = "color"
|
|
520
|
+
outputs = [_parse_color_output(str(v)) for v in outputs]
|
|
521
|
+
if any(v is None for v in outputs):
|
|
522
|
+
raise ValueError("interpolate() color output_range entries must all be colors")
|
|
523
|
+
else:
|
|
524
|
+
angles = [_parse_angle_output(str(v)) for v in outputs]
|
|
525
|
+
if any(v is None for v in angles):
|
|
526
|
+
raise ValueError(f"interpolate() cannot parse output value {first!r}")
|
|
527
|
+
outputs = angles
|
|
528
|
+
else:
|
|
529
|
+
outputs = [float(v) for v in outputs]
|
|
530
|
+
|
|
531
|
+
self._parent = parent
|
|
532
|
+
self._inputs = inputs
|
|
533
|
+
self._outputs: List[Any] = outputs
|
|
534
|
+
self._kind = kind
|
|
535
|
+
self._left = extrapolate_left or extrapolate
|
|
536
|
+
self._right = extrapolate_right or extrapolate
|
|
537
|
+
parent._adopt_child(self)
|
|
538
|
+
|
|
539
|
+
@property
|
|
540
|
+
def value(self) -> Any:
|
|
541
|
+
"""Return the interpolated output for the parent's current value."""
|
|
542
|
+
return self._compute(float(self._parent))
|
|
543
|
+
|
|
544
|
+
def _compute(self, x: float) -> Any:
|
|
545
|
+
inputs = self._inputs
|
|
546
|
+
n = len(inputs)
|
|
547
|
+
if x < inputs[0]:
|
|
548
|
+
if self._left == "identity":
|
|
549
|
+
return x
|
|
550
|
+
if self._left == "clamp":
|
|
551
|
+
x = inputs[0]
|
|
552
|
+
i = 0
|
|
553
|
+
elif x > inputs[-1]:
|
|
554
|
+
if self._right == "identity":
|
|
555
|
+
return x
|
|
556
|
+
if self._right == "clamp":
|
|
557
|
+
x = inputs[-1]
|
|
558
|
+
i = n - 2
|
|
559
|
+
else:
|
|
560
|
+
i = max(0, min(n - 2, bisect.bisect_right(inputs, x) - 1))
|
|
561
|
+
|
|
562
|
+
x0, x1 = inputs[i], inputs[i + 1]
|
|
563
|
+
span = x1 - x0
|
|
564
|
+
t = 0.0 if span <= 0 else (x - x0) / span
|
|
565
|
+
|
|
566
|
+
if self._kind == "color":
|
|
567
|
+
c0 = self._outputs[i]
|
|
568
|
+
c1 = self._outputs[i + 1]
|
|
569
|
+
t_cl = max(0.0, min(1.0, t))
|
|
570
|
+
channels = [int(round(c0[j] + (c1[j] - c0[j]) * t_cl)) for j in range(4)]
|
|
571
|
+
a, r, g, b = (max(0, min(255, ch)) for ch in channels)
|
|
572
|
+
return f"#{a:02X}{r:02X}{g:02X}{b:02X}"
|
|
573
|
+
|
|
574
|
+
y0 = self._outputs[i]
|
|
575
|
+
y1 = self._outputs[i + 1]
|
|
576
|
+
return y0 + (y1 - y0) * t
|
|
577
|
+
|
|
578
|
+
def __repr__(self) -> str:
|
|
579
|
+
return f"AnimatedInterpolation({self._inputs} -> {self._outputs})"
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class _AnimatedOperation(AnimatedNode):
|
|
583
|
+
"""Read-only node computed from other nodes (and constants) by ``fn``."""
|
|
584
|
+
|
|
585
|
+
__slots__ = ("_op", "_fn", "_parents")
|
|
586
|
+
|
|
587
|
+
def __init__(self, op: str, fn: Callable[..., float], parents: List[Any]) -> None:
|
|
588
|
+
super().__init__()
|
|
589
|
+
self._op = op
|
|
590
|
+
self._fn = fn
|
|
591
|
+
self._parents = list(parents)
|
|
592
|
+
for parent in self._parents:
|
|
593
|
+
if isinstance(parent, AnimatedNode):
|
|
594
|
+
parent._adopt_child(self)
|
|
595
|
+
|
|
596
|
+
@property
|
|
597
|
+
def value(self) -> float:
|
|
598
|
+
args = [float(p) if isinstance(p, AnimatedNode) else float(p) for p in self._parents]
|
|
599
|
+
try:
|
|
600
|
+
return float(self._fn(*args))
|
|
601
|
+
except Exception:
|
|
602
|
+
return 0.0
|
|
603
|
+
|
|
604
|
+
def __repr__(self) -> str:
|
|
605
|
+
return f"AnimatedOperation({self._op})"
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
class _AnimatedDiffClamp(AnimatedNode):
|
|
609
|
+
"""Accumulate a parent's *deltas* into a clamped range.
|
|
610
|
+
|
|
611
|
+
Mirrors React Native's ``Animated.diffClamp``: the output moves by
|
|
612
|
+
the same amount as the input but is pinned to ``[min, max]``, so
|
|
613
|
+
scrolling far down then slightly up immediately re-reveals a
|
|
614
|
+
collapsing header regardless of absolute offset.
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
__slots__ = ("_parent", "_min", "_max", "_last_input", "_current")
|
|
618
|
+
|
|
619
|
+
def __init__(self, parent: AnimatedNode, min_value: float, max_value: float) -> None:
|
|
620
|
+
super().__init__()
|
|
621
|
+
if max_value < min_value:
|
|
622
|
+
raise ValueError("diff_clamp() requires min_value <= max_value")
|
|
623
|
+
self._parent = parent
|
|
624
|
+
self._min = float(min_value)
|
|
625
|
+
self._max = float(max_value)
|
|
626
|
+
self._last_input = float(parent)
|
|
627
|
+
self._current = max(self._min, min(self._max, self._last_input))
|
|
628
|
+
parent._adopt_child(self)
|
|
629
|
+
|
|
630
|
+
@property
|
|
631
|
+
def value(self) -> float:
|
|
632
|
+
return self._current
|
|
633
|
+
|
|
634
|
+
def _refresh(self) -> None:
|
|
635
|
+
latest = float(self._parent)
|
|
636
|
+
delta = latest - self._last_input
|
|
637
|
+
self._last_input = latest
|
|
638
|
+
self._current = max(self._min, min(self._max, self._current + delta))
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
# ======================================================================
|
|
642
|
+
# Animated.event
|
|
643
|
+
# ======================================================================
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
class AnimatedEvent:
|
|
647
|
+
"""Callable event handler copying event fields into animated values.
|
|
648
|
+
|
|
649
|
+
Built via ``pn.Animated.event(...)``. Each keyword argument names a
|
|
650
|
+
field on the incoming event payload (a dict key for scroll payloads
|
|
651
|
+
such as ``{"x": ..., "y": ...}``, or an attribute for
|
|
652
|
+
[`GestureEvent`][pythonnative.gestures.GestureEvent] instances) and
|
|
653
|
+
maps it onto an [`AnimatedValue`][pythonnative.AnimatedValue].
|
|
654
|
+
|
|
655
|
+
Because the result is an ordinary callable, it can be passed to any
|
|
656
|
+
event prop:
|
|
657
|
+
|
|
658
|
+
```python
|
|
659
|
+
scroll_y = pn.use_animated_value(0.0)
|
|
660
|
+
pn.ScrollView(..., on_scroll=pn.Animated.event(y=scroll_y))
|
|
661
|
+
|
|
662
|
+
tx = pn.use_animated_value(0.0)
|
|
663
|
+
gestures.Pan(on_change=pn.Animated.event(translation_x=tx))
|
|
664
|
+
```
|
|
665
|
+
"""
|
|
666
|
+
|
|
667
|
+
__slots__ = ("_bindings", "_listener")
|
|
668
|
+
|
|
669
|
+
def __init__(self, listener: Optional[Callable[..., None]] = None, **bindings: AnimatedValue) -> None:
|
|
670
|
+
for name, node in bindings.items():
|
|
671
|
+
if not isinstance(node, AnimatedValue):
|
|
672
|
+
raise TypeError(
|
|
673
|
+
f"Animated.event() field {name!r} must map to an AnimatedValue "
|
|
674
|
+
f"(got {type(node).__name__}); derived nodes are read-only."
|
|
675
|
+
)
|
|
676
|
+
self._bindings = dict(bindings)
|
|
677
|
+
self._listener = listener
|
|
678
|
+
|
|
679
|
+
def __call__(self, payload: Any = None, *args: Any) -> None:
|
|
680
|
+
"""Write bound payload fields into their values, then run the listener.
|
|
681
|
+
|
|
682
|
+
Args:
|
|
683
|
+
payload: The event payload; a dict is read by key, any
|
|
684
|
+
other object by attribute. Missing or non-numeric
|
|
685
|
+
fields are skipped.
|
|
686
|
+
*args: Extra positional arguments forwarded to the
|
|
687
|
+
listener.
|
|
688
|
+
"""
|
|
689
|
+
for name, node in self._bindings.items():
|
|
690
|
+
raw: Any = None
|
|
691
|
+
if isinstance(payload, dict):
|
|
692
|
+
raw = payload.get(name)
|
|
693
|
+
elif payload is not None:
|
|
694
|
+
raw = getattr(payload, name, None)
|
|
695
|
+
if raw is None:
|
|
696
|
+
continue
|
|
697
|
+
try:
|
|
698
|
+
node.set_value(float(raw))
|
|
699
|
+
except (TypeError, ValueError):
|
|
700
|
+
continue
|
|
701
|
+
if self._listener is not None:
|
|
702
|
+
try:
|
|
703
|
+
self._listener(payload, *args)
|
|
704
|
+
except Exception:
|
|
705
|
+
pass
|
|
706
|
+
|
|
707
|
+
|
|
261
708
|
# ======================================================================
|
|
262
709
|
# Python fallback driver
|
|
263
710
|
# ======================================================================
|
|
@@ -601,6 +1048,10 @@ def _start_native(value: AnimatedValue, spec: Dict[str, Any]) -> Optional[_Nativ
|
|
|
601
1048
|
# Python listeners want per-frame values; only the ticker
|
|
602
1049
|
# provides those.
|
|
603
1050
|
return None
|
|
1051
|
+
if value._has_dependents():
|
|
1052
|
+
# Derived nodes (interpolations, arithmetic) need per-frame
|
|
1053
|
+
# Python evaluation to keep their own attachments in sync.
|
|
1054
|
+
return None
|
|
604
1055
|
try:
|
|
605
1056
|
backend = _backend()
|
|
606
1057
|
except Exception:
|
|
@@ -733,8 +1184,19 @@ class _AnimationHandle(_AwaitableAnimation):
|
|
|
733
1184
|
anim._finish()
|
|
734
1185
|
_manager.remove(anim)
|
|
735
1186
|
|
|
1187
|
+
def _is_running(self) -> bool:
|
|
1188
|
+
if self._native_group is not None and not self._native_group._completed:
|
|
1189
|
+
return True
|
|
1190
|
+
if self._python_anim is not None and not self._python_anim._completed:
|
|
1191
|
+
return True
|
|
1192
|
+
return False
|
|
1193
|
+
|
|
736
1194
|
async def _drive(self) -> None:
|
|
737
|
-
|
|
1195
|
+
# (Re)start unless an instance is currently mid-flight, so a
|
|
1196
|
+
# reused handle (``Animated.loop``, awaiting the same handle
|
|
1197
|
+
# twice) runs a fresh animation instead of resolving instantly
|
|
1198
|
+
# against the finished previous instance.
|
|
1199
|
+
if not self._is_running():
|
|
738
1200
|
self.start()
|
|
739
1201
|
loop = asyncio.get_running_loop()
|
|
740
1202
|
future: asyncio.Future[None] = loop.create_future()
|
|
@@ -748,11 +1210,12 @@ class _AnimationHandle(_AwaitableAnimation):
|
|
|
748
1210
|
|
|
749
1211
|
|
|
750
1212
|
class _CompositeAnimation(_AwaitableAnimation):
|
|
751
|
-
"""Run a list of animations in sequence
|
|
1213
|
+
"""Run a list of animations in sequence, in parallel, or staggered."""
|
|
752
1214
|
|
|
753
|
-
def __init__(self, items: List[Any], mode: str) -> None:
|
|
1215
|
+
def __init__(self, items: List[Any], mode: str, stagger_ms: float = 0.0) -> None:
|
|
754
1216
|
self._items = list(items)
|
|
755
1217
|
self._mode = mode
|
|
1218
|
+
self._stagger_ms = float(stagger_ms)
|
|
756
1219
|
|
|
757
1220
|
def start(self) -> "_CompositeAnimation":
|
|
758
1221
|
"""Schedule the composite on the framework runtime, fire-and-forget."""
|
|
@@ -772,6 +1235,16 @@ class _CompositeAnimation(_AwaitableAnimation):
|
|
|
772
1235
|
if self._mode == "parallel":
|
|
773
1236
|
await asyncio.gather(*(self._await_item(item) for item in self._items))
|
|
774
1237
|
return
|
|
1238
|
+
if self._mode == "stagger":
|
|
1239
|
+
delay_s = max(0.0, self._stagger_ms) / 1000.0
|
|
1240
|
+
|
|
1241
|
+
async def _delayed(index: int, item: Any) -> None:
|
|
1242
|
+
if index > 0 and delay_s > 0.0:
|
|
1243
|
+
await asyncio.sleep(delay_s * index)
|
|
1244
|
+
await self._await_item(item)
|
|
1245
|
+
|
|
1246
|
+
await asyncio.gather(*(_delayed(i, item) for i, item in enumerate(self._items)))
|
|
1247
|
+
return
|
|
775
1248
|
for item in self._items:
|
|
776
1249
|
await self._await_item(item)
|
|
777
1250
|
|
|
@@ -784,26 +1257,95 @@ class _CompositeAnimation(_AwaitableAnimation):
|
|
|
784
1257
|
await item
|
|
785
1258
|
|
|
786
1259
|
|
|
1260
|
+
class _LoopAnimation(_AwaitableAnimation):
|
|
1261
|
+
"""Repeat an animation, resetting its values before each iteration."""
|
|
1262
|
+
|
|
1263
|
+
def __init__(self, animation: Any, iterations: int = -1, reset: bool = True) -> None:
|
|
1264
|
+
self._animation = animation
|
|
1265
|
+
self._iterations = int(iterations)
|
|
1266
|
+
self._reset = bool(reset)
|
|
1267
|
+
self._stopped = False
|
|
1268
|
+
|
|
1269
|
+
def start(self) -> "_LoopAnimation":
|
|
1270
|
+
from .runtime import run_async
|
|
1271
|
+
|
|
1272
|
+
self._stopped = False
|
|
1273
|
+
run_async(self._drive())
|
|
1274
|
+
return self
|
|
1275
|
+
|
|
1276
|
+
def stop(self) -> None:
|
|
1277
|
+
self._stopped = True
|
|
1278
|
+
try:
|
|
1279
|
+
self._animation.stop()
|
|
1280
|
+
except Exception:
|
|
1281
|
+
pass
|
|
1282
|
+
|
|
1283
|
+
def _collect_values(self, item: Any, out: List[AnimatedValue]) -> None:
|
|
1284
|
+
if isinstance(item, _AnimationHandle):
|
|
1285
|
+
if item._value is not None and item._value not in out:
|
|
1286
|
+
out.append(item._value)
|
|
1287
|
+
elif isinstance(item, _CompositeAnimation):
|
|
1288
|
+
for sub in item._items:
|
|
1289
|
+
self._collect_values(sub, out)
|
|
1290
|
+
elif isinstance(item, _LoopAnimation):
|
|
1291
|
+
self._collect_values(item._animation, out)
|
|
1292
|
+
|
|
1293
|
+
async def _drive(self) -> None:
|
|
1294
|
+
self._stopped = False
|
|
1295
|
+
values: List[AnimatedValue] = []
|
|
1296
|
+
self._collect_values(self._animation, values)
|
|
1297
|
+
origins = [(v, v.value) for v in values]
|
|
1298
|
+
count = 0
|
|
1299
|
+
while not self._stopped and (self._iterations < 0 or count < self._iterations):
|
|
1300
|
+
if self._reset and count > 0:
|
|
1301
|
+
for value, origin in origins:
|
|
1302
|
+
value.set_value(origin)
|
|
1303
|
+
await self._animation
|
|
1304
|
+
count += 1
|
|
1305
|
+
|
|
1306
|
+
|
|
787
1307
|
# ======================================================================
|
|
788
1308
|
# Animated component wrappers
|
|
789
1309
|
# ======================================================================
|
|
790
1310
|
|
|
1311
|
+
# Transform-entry keys that may carry animated nodes; the key doubles
|
|
1312
|
+
# as the ``set_animated_property`` prop name.
|
|
1313
|
+
_ANIMATED_TRANSFORM_KEYS = frozenset(
|
|
1314
|
+
{"translate_x", "translate_y", "scale", "scale_x", "scale_y", "rotate"},
|
|
1315
|
+
)
|
|
791
1316
|
|
|
792
|
-
|
|
1317
|
+
|
|
1318
|
+
def _resolve_style_with_values(style: StyleProp) -> Tuple[Dict[str, Any], Dict[str, AnimatedNode]]:
|
|
793
1319
|
"""Split ``style`` into a plain dict and animated bindings.
|
|
794
1320
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
``
|
|
798
|
-
after mount.
|
|
1321
|
+
Animated nodes in the style (top-level values *and* values inside
|
|
1322
|
+
``transform`` entries) are replaced with their current numeric
|
|
1323
|
+
value in ``plain_style`` and recorded in ``animated_bindings`` so
|
|
1324
|
+
the wrapping component can attach them after mount.
|
|
799
1325
|
"""
|
|
800
1326
|
flat = resolve_style(style)
|
|
801
|
-
bindings: Dict[str,
|
|
1327
|
+
bindings: Dict[str, AnimatedNode] = {}
|
|
802
1328
|
plain: Dict[str, Any] = {}
|
|
803
1329
|
for k, v in flat.items():
|
|
804
|
-
if isinstance(v,
|
|
1330
|
+
if isinstance(v, AnimatedNode):
|
|
805
1331
|
bindings[k] = v
|
|
806
1332
|
plain[k] = v.value
|
|
1333
|
+
elif k == "transform" and v is not None:
|
|
1334
|
+
entries = v if isinstance(v, list) else [v]
|
|
1335
|
+
plain_entries: List[Any] = []
|
|
1336
|
+
for entry in entries:
|
|
1337
|
+
if not isinstance(entry, dict):
|
|
1338
|
+
plain_entries.append(entry)
|
|
1339
|
+
continue
|
|
1340
|
+
clean_entry: Dict[str, Any] = {}
|
|
1341
|
+
for prop, val in entry.items():
|
|
1342
|
+
if isinstance(val, AnimatedNode) and prop in _ANIMATED_TRANSFORM_KEYS:
|
|
1343
|
+
bindings[prop] = val
|
|
1344
|
+
clean_entry[prop] = val.value
|
|
1345
|
+
else:
|
|
1346
|
+
clean_entry[prop] = val
|
|
1347
|
+
plain_entries.append(clean_entry)
|
|
1348
|
+
plain[k] = plain_entries
|
|
807
1349
|
else:
|
|
808
1350
|
plain[k] = v
|
|
809
1351
|
return plain, bindings
|
|
@@ -877,8 +1419,9 @@ def _animated_prop_name(prop: str) -> str:
|
|
|
877
1419
|
class _AnimatedNamespace:
|
|
878
1420
|
"""Public ``Animated`` namespace.
|
|
879
1421
|
|
|
880
|
-
Exposes the ``Value`` type, animation factories, composers,
|
|
881
|
-
|
|
1422
|
+
Exposes the ``Value`` type, animation factories, composers,
|
|
1423
|
+
derived-node helpers (``event``, ``diff_clamp``), and component
|
|
1424
|
+
wrappers (``View``, ``Text``, ``Image``).
|
|
882
1425
|
"""
|
|
883
1426
|
|
|
884
1427
|
Value = AnimatedValue
|
|
@@ -968,6 +1511,54 @@ class _AnimatedNamespace:
|
|
|
968
1511
|
"""Run ``animations`` one after another."""
|
|
969
1512
|
return _CompositeAnimation(animations, "sequence")
|
|
970
1513
|
|
|
1514
|
+
@staticmethod
|
|
1515
|
+
def stagger(delay: float, animations: List[Any]) -> _CompositeAnimation:
|
|
1516
|
+
"""Run ``animations`` in parallel, each starting ``delay`` ms after the previous.
|
|
1517
|
+
|
|
1518
|
+
Args:
|
|
1519
|
+
delay: Milliseconds between successive starts.
|
|
1520
|
+
animations: Animation handles (or awaitables) to run.
|
|
1521
|
+
|
|
1522
|
+
Example:
|
|
1523
|
+
```python
|
|
1524
|
+
pn.Animated.stagger(80, [
|
|
1525
|
+
pn.Animated.timing(v, to=1.0) for v in card_opacities
|
|
1526
|
+
]).start()
|
|
1527
|
+
```
|
|
1528
|
+
"""
|
|
1529
|
+
return _CompositeAnimation(animations, "stagger", stagger_ms=delay)
|
|
1530
|
+
|
|
1531
|
+
@staticmethod
|
|
1532
|
+
def loop(animation: Any, *, iterations: int = -1, reset: bool = True) -> _LoopAnimation:
|
|
1533
|
+
"""Repeat ``animation``, optionally forever.
|
|
1534
|
+
|
|
1535
|
+
Values driven by the animation are captured when the loop
|
|
1536
|
+
starts and restored before each iteration (matching React
|
|
1537
|
+
Native's ``resetBeforeIteration``), so ``timing`` loops replay
|
|
1538
|
+
the same motion instead of animating in place.
|
|
1539
|
+
|
|
1540
|
+
Args:
|
|
1541
|
+
animation: A handle from ``timing`` / ``spring`` / ``decay``
|
|
1542
|
+
or a ``sequence`` / ``parallel`` / ``stagger`` composite.
|
|
1543
|
+
iterations: Number of repetitions; ``-1`` (default) loops
|
|
1544
|
+
until [`stop`][pythonnative.animated._LoopAnimation.stop]
|
|
1545
|
+
is called or the awaiting task is cancelled.
|
|
1546
|
+
reset: When ``False``, values continue from wherever the
|
|
1547
|
+
previous iteration ended.
|
|
1548
|
+
|
|
1549
|
+
Example:
|
|
1550
|
+
```python
|
|
1551
|
+
pulse = pn.Animated.loop(
|
|
1552
|
+
pn.Animated.sequence([
|
|
1553
|
+
pn.Animated.timing(scale, to=1.15, duration=350),
|
|
1554
|
+
pn.Animated.timing(scale, to=1.0, duration=350),
|
|
1555
|
+
]),
|
|
1556
|
+
).start()
|
|
1557
|
+
# later: pulse.stop()
|
|
1558
|
+
```
|
|
1559
|
+
"""
|
|
1560
|
+
return _LoopAnimation(animation, iterations=iterations, reset=reset)
|
|
1561
|
+
|
|
971
1562
|
@staticmethod
|
|
972
1563
|
def delay(duration: float) -> _AnimationHandle:
|
|
973
1564
|
"""Wait ``duration`` ms before continuing in a sequence."""
|
|
@@ -980,6 +1571,47 @@ class _AnimatedNamespace:
|
|
|
980
1571
|
|
|
981
1572
|
return _AnimationHandle(None, _spec, _fallback)
|
|
982
1573
|
|
|
1574
|
+
@staticmethod
|
|
1575
|
+
def event(listener: Optional[Callable[..., None]] = None, **bindings: AnimatedValue) -> AnimatedEvent:
|
|
1576
|
+
"""Build a callback that copies event fields into animated values.
|
|
1577
|
+
|
|
1578
|
+
Pass the result to any event prop. Each keyword maps a payload
|
|
1579
|
+
field (dict key or dataclass attribute) onto an
|
|
1580
|
+
[`AnimatedValue`][pythonnative.AnimatedValue]:
|
|
1581
|
+
|
|
1582
|
+
```python
|
|
1583
|
+
scroll_y = pn.use_animated_value(0.0)
|
|
1584
|
+
pn.ScrollView(..., on_scroll=pn.Animated.event(y=scroll_y))
|
|
1585
|
+
|
|
1586
|
+
tx = pn.use_animated_value(0.0)
|
|
1587
|
+
gestures.Pan(on_change=pn.Animated.event(translation_x=tx))
|
|
1588
|
+
```
|
|
1589
|
+
|
|
1590
|
+
Args:
|
|
1591
|
+
listener: Optional plain callback invoked with the raw
|
|
1592
|
+
event after the values update.
|
|
1593
|
+
**bindings: ``field_name=animated_value`` pairs.
|
|
1594
|
+
|
|
1595
|
+
Returns:
|
|
1596
|
+
A callable [`AnimatedEvent`][pythonnative.animated.AnimatedEvent].
|
|
1597
|
+
"""
|
|
1598
|
+
return AnimatedEvent(listener, **bindings)
|
|
1599
|
+
|
|
1600
|
+
@staticmethod
|
|
1601
|
+
def diff_clamp(node: AnimatedNode, min_value: float, max_value: float) -> AnimatedNode:
|
|
1602
|
+
"""Accumulate ``node``'s deltas into ``[min_value, max_value]``.
|
|
1603
|
+
|
|
1604
|
+
The classic collapsing-header primitive: unlike ``interpolate``
|
|
1605
|
+
with ``"clamp"`` (which pins the *absolute* input), the output
|
|
1606
|
+
tracks input *movement*, so a small scroll upward immediately
|
|
1607
|
+
re-reveals the header no matter how far down the list is.
|
|
1608
|
+
|
|
1609
|
+
```python
|
|
1610
|
+
header_shift = pn.Animated.diff_clamp(scroll_y, 0, 56)
|
|
1611
|
+
```
|
|
1612
|
+
"""
|
|
1613
|
+
return _AnimatedDiffClamp(node, min_value, max_value)
|
|
1614
|
+
|
|
983
1615
|
View = staticmethod(_make_animated_factory("View", accept_children=True))
|
|
984
1616
|
Text = staticmethod(_make_animated_factory("Text", accept_children=False))
|
|
985
1617
|
Image = staticmethod(_make_animated_factory("Image", accept_children=False))
|
|
@@ -1027,7 +1659,10 @@ def use_animated_value(initial: float = 0.0) -> AnimatedValue:
|
|
|
1027
1659
|
|
|
1028
1660
|
|
|
1029
1661
|
__all__ = [
|
|
1662
|
+
"AnimatedNode",
|
|
1030
1663
|
"AnimatedValue",
|
|
1664
|
+
"AnimatedInterpolation",
|
|
1665
|
+
"AnimatedEvent",
|
|
1031
1666
|
"Animated",
|
|
1032
1667
|
"use_animated_value",
|
|
1033
1668
|
"native_animation_completed",
|