Skip to content

Commit a1e89ee

Browse files
Add 'smooth_attr'
1 parent 01a2390 commit a1e89ee

File tree

3 files changed

+159
-0
lines changed

3 files changed

+159
-0
lines changed

examples/smoothing.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from random import random, randint
2+
from kivy.event import EventDispatcher
3+
from kivy.graphics import Color, Rectangle, CanvasBase
4+
from kivy.properties import NumericProperty, ReferenceListProperty, ColorProperty
5+
from kivy.app import App
6+
from kivy.uix.widget import Widget
7+
import asynckivy as ak
8+
9+
10+
class AnimatedRectangle(EventDispatcher):
11+
x = NumericProperty()
12+
y = NumericProperty()
13+
pos = ReferenceListProperty(x, y)
14+
width = NumericProperty()
15+
height = NumericProperty()
16+
size = ReferenceListProperty(width, height)
17+
color = ColorProperty("#FFFFFFFF")
18+
19+
def __init__(self, **kwargs):
20+
super().__init__(**kwargs)
21+
self.canvas = canvas = CanvasBase()
22+
with canvas:
23+
ak.smooth_attr((self, "color"), (Color(self.color), "rgba"), min_diff=0.02, speed=4)
24+
rect = Rectangle(pos=self.pos, size=self.size)
25+
ak.smooth_attr((self, "pos"), (rect, "pos"))
26+
ak.smooth_attr((self, "size"), (rect, "size"))
27+
28+
29+
class SampleApp(App):
30+
def build(self):
31+
root = Widget()
32+
rect = AnimatedRectangle(width=160, height=160)
33+
root.canvas.add(rect.canvas)
34+
35+
def on_touch_down(_, touch):
36+
rect.pos = touch.pos
37+
rect.color = (random(), random(), random(), 1)
38+
rect.width = randint(50, 200)
39+
rect.height = randint(50, 200)
40+
root.bind(on_touch_down=on_touch_down)
41+
return root
42+
43+
44+
if __name__ == '__main__':
45+
SampleApp().run()

src/asynckivy/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
'run_in_thread',
2222
'sleep',
2323
'sleep_free',
24+
'smooth_attr',
2425
'suppress_event',
2526
'sync_attr',
2627
'sync_attrs',
@@ -39,3 +40,4 @@
3940
from ._n_frames import n_frames
4041
from ._utils import transform, suppress_event, sync_attr, sync_attrs
4142
from ._managed_start import managed_start
43+
from ._smooth_attr import smooth_attr

src/asynckivy/_smooth_attr.py

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
__all__ = ("smooth_attr", )
2+
import typing as T
3+
from functools import partial
4+
import math
5+
6+
from kivy.metrics import dp
7+
from kivy.clock import Clock
8+
from kivy.event import EventDispatcher
9+
from kivy import properties as P
10+
NUMERIC_TYPES = (P.NumericProperty, P.BoundedNumericProperty, )
11+
SEQUENCE_TYPES = (P.ColorProperty, P.ReferenceListProperty, P.ListProperty, )
12+
13+
14+
class smooth_attr:
15+
'''
16+
Makes an attribute smoothly follow another.
17+
18+
.. code-block::
19+
20+
import types
21+
22+
widget = Widget(x=0)
23+
obj = types.SimpleNamespace(xx=100)
24+
25+
# 'obj.xx' will smoothly follow 'widget.x'.
26+
smooth_attr(target=(widget, 'x'), follower=(obj, 'xx'))
27+
28+
To make its effect temporary, use it with a with-statement:
29+
30+
.. code-block::
31+
32+
# The effect lasts only within the with-block.
33+
with smooth_attr(...):
34+
...
35+
36+
A key feature of this API is that if the target value changes while being followed,
37+
the follower automatically adjusts to the new value.
38+
39+
:param target: Must be a numeric or numeric sequence type property, that is, one of the following:
40+
41+
* :class:`~kivy.properties.NumericProperty`
42+
* :class:`~kivy.properties.BoundedNumericProperty`
43+
* :class:`~kivy.properties.ReferenceListProperty`
44+
* :class:`~kivy.properties.ListProperty`
45+
* :class:`~kivy.properties.ColorProperty`
46+
47+
:param speed: The speed coefficient for following. A larger value results in faster following.
48+
:param min_diff: If the difference between the target and the follower is less than this value,
49+
the follower will instantly jump to the target's value. When the target is a ``ColorProperty``,
50+
you most likely want to set this to a very small value, such as ``0.01``. Defaults to ``dp(2)``.
51+
52+
.. versionadded:: 0.8.0
53+
'''
54+
__slots__ = ("__exit__", )
55+
56+
def __init__(self, target: tuple[EventDispatcher, str], follower: tuple[T.Any, str],
57+
*, speed=10.0, min_diff=dp(2)):
58+
target_obj, target_attr = target
59+
target_desc = target_obj.property(target_attr)
60+
if isinstance(target_desc, NUMERIC_TYPES):
61+
update = self._update_follower
62+
elif isinstance(target_desc, SEQUENCE_TYPES):
63+
update = self._update_follower_ver_seq
64+
else:
65+
raise ValueError(f"Unsupported target type: {target_desc}")
66+
trigger = Clock.schedule_interval(
67+
partial(update, *target, *follower, -speed, -min_diff, min_diff), 0
68+
)
69+
bind_uid = target_obj.fbind(target_attr, trigger)
70+
self.__exit__ = partial(self._cleanup, trigger, target_obj, target_attr, bind_uid)
71+
72+
@staticmethod
73+
def _cleanup(trigger, target_obj, target_attr, bind_uid, *__):
74+
trigger.cancel()
75+
target_obj.unbind_uid(target_attr, bind_uid)
76+
77+
def __enter__(self):
78+
pass
79+
80+
def _update_follower(getattr, setattr, math_exp, target_obj, target_attr, follower_obj, follower_attr,
81+
negative_speed, min, max, dt):
82+
t_value = getattr(target_obj, target_attr)
83+
f_value = getattr(follower_obj, follower_attr)
84+
diff = f_value - t_value
85+
86+
if min < diff < max:
87+
setattr(follower_obj, follower_attr, t_value)
88+
return False
89+
90+
new_value = t_value + math_exp(negative_speed * dt) * diff
91+
setattr(follower_obj, follower_attr, new_value)
92+
93+
_update_follower = partial(_update_follower, getattr, setattr, math.exp)
94+
95+
def _update_follower_ver_seq(getattr, setattr, math_exp, seq_cls, zip, target_obj, target_attr,
96+
follower_obj, follower_attr, negative_speed, min, max, dt):
97+
t_value = getattr(target_obj, target_attr)
98+
f_value = getattr(follower_obj, follower_attr)
99+
p = math_exp(negative_speed * dt)
100+
still_going = False
101+
new_value = seq_cls(
102+
(t_elem + p * diff) if (
103+
diff := f_elem - t_elem,
104+
_still_going := (diff <= min or max <= diff),
105+
still_going := (still_going or _still_going),
106+
) and _still_going else t_elem
107+
for t_elem, f_elem in zip(t_value, f_value)
108+
)
109+
setattr(follower_obj, follower_attr, new_value)
110+
return still_going
111+
112+
_update_follower_ver_seq = partial(_update_follower_ver_seq, getattr, setattr, math.exp, tuple, zip)

0 commit comments

Comments
 (0)