Skip to content

Commit 00ae9e1

Browse files
committed
UI: Add Transitions to arcade GUI
1 parent dad7007 commit 00ae9e1

File tree

9 files changed

+608
-1
lines changed

9 files changed

+608
-1
lines changed

arcade/gui/__init__.py

+21
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@
2727
UILayout,
2828
)
2929
from arcade.gui.widgets import UIDummy, Rect
30+
from arcade.gui.transition import (
31+
EaseFunctions,
32+
TransitionBase,
33+
EventTransitionBase,
34+
TransitionAttr,
35+
TransitionAttrIncr,
36+
TransitionChain,
37+
TransitionParallel,
38+
TransitionDelay,
39+
TransitionAttrSet,
40+
)
3041
from arcade.gui.widgets import UIInteractiveWidget
3142
from arcade.gui.widgets.text import UILabel, UIInputText, UITextArea
3243
from arcade.gui.widgets.toggle import UITextureToggle
@@ -88,6 +99,16 @@
8899
"Surface",
89100
"Rect",
90101
"NinePatchTexture",
102+
# Transitions
103+
"EaseFunctions",
104+
"TransitionBase",
105+
"EventTransitionBase",
106+
"TransitionAttr",
107+
"TransitionAttrIncr",
108+
"TransitionAttrSet",
109+
"TransitionChain",
110+
"TransitionParallel",
111+
"TransitionDelay",
91112
# Property classes
92113
"ListProperty",
93114
"DictProperty",

arcade/gui/examples/transitions.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import arcade
2+
from arcade.gui import UIManager, TransitionChain, TransitionAttr, EaseFunctions, TransitionAttrIncr
3+
from arcade.gui.transition import TransitionAttrSet
4+
from arcade.gui.widgets.buttons import UIFlatButton
5+
6+
7+
class DemoWindow(arcade.Window):
8+
def __init__(self):
9+
super().__init__(800, 600, "UI Mockup", resizable=True)
10+
arcade.set_background_color(arcade.color.DARK_BLUE_GRAY)
11+
12+
# Init UIManager
13+
self.manager = UIManager()
14+
self.manager.enable()
15+
16+
button = self.manager.add(UIFlatButton(text="Click me I can move!"))
17+
button.center_on_screen()
18+
19+
@button.event
20+
def on_click(event):
21+
# button.disabled = True
22+
23+
start_x, start_y = button.center
24+
chain = TransitionChain()
25+
26+
chain.add(TransitionAttrSet(attribute="disabled", value=True, duration=0))
27+
28+
chain.add(TransitionAttrIncr(
29+
attribute="center_x",
30+
increment=100,
31+
duration=1.0
32+
))
33+
chain.add(TransitionAttrIncr(
34+
attribute="center_y",
35+
increment=100,
36+
duration=1,
37+
ease_function=EaseFunctions.sine
38+
))
39+
40+
# Go back
41+
chain.add(TransitionAttr(
42+
attribute="center_x",
43+
end=start_x,
44+
duration=1,
45+
ease_function=EaseFunctions.sine
46+
))
47+
chain.add(TransitionAttr(
48+
attribute="center_y",
49+
end=start_y,
50+
duration=1,
51+
ease_function=EaseFunctions.sine
52+
))
53+
chain.add(TransitionAttrSet(attribute="disabled", value=False, duration=0))
54+
55+
button.add_transition(chain)
56+
57+
def on_draw(self):
58+
self.clear()
59+
self.manager.draw()
60+
61+
62+
if __name__ == "__main__":
63+
DemoWindow().run()

arcade/gui/transition.py

+257
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import math
2+
from abc import ABC, abstractmethod
3+
from typing import Callable, Any, Optional, List, TypeVar
4+
5+
from pyglet.event import EventDispatcher
6+
7+
T = TypeVar("T", bound="TransitionBase")
8+
9+
10+
class EaseFunctions:
11+
@staticmethod
12+
def linear(x: float):
13+
return x
14+
15+
@staticmethod
16+
def sine(x: float):
17+
return 1 - math.cos((x * math.pi) / 2)
18+
19+
20+
class TransitionBase(ABC):
21+
@abstractmethod
22+
def tick(self, subject, dt) -> float:
23+
"""
24+
Update
25+
26+
:return: dt, which is not consumed
27+
"""
28+
pass
29+
30+
@property
31+
@abstractmethod
32+
def finished(self) -> bool:
33+
raise not NotImplementedError()
34+
35+
def __add__(self, other):
36+
return TransitionChain(self, other)
37+
38+
def __or__(self, other):
39+
return TransitionParallel(self, other)
40+
41+
42+
class EventTransitionBase(TransitionBase, EventDispatcher):
43+
"""
44+
Extension of TransitionBase, providing hooks via
45+
46+
- on_tick(subject, progress: float)
47+
- on_finish(subject)
48+
49+
:param duration: Duration of the transition in seconds
50+
:param delay: Start transition after x seconds
51+
"""
52+
53+
def __init__(
54+
self,
55+
*,
56+
duration: float,
57+
delay=0.0,
58+
):
59+
self._duration = duration
60+
self._elapsed = -delay
61+
62+
self.register_event_type("on_tick")
63+
self.register_event_type("on_finish")
64+
65+
def tick(self, subject, dt) -> float:
66+
self._elapsed += dt
67+
if self._elapsed >= 0:
68+
progress = min(self._elapsed / self._duration, 1) if self._duration else 1
69+
self.dispatch_event("on_tick", subject, progress)
70+
71+
if self.finished:
72+
self.dispatch_event("on_finish", subject)
73+
74+
return max(0.0, self._elapsed - self._duration)
75+
76+
def on_tick(self, subject, progress):
77+
pass
78+
79+
def on_finish(self, subject):
80+
pass
81+
82+
@property
83+
def finished(self):
84+
return self._elapsed >= self._duration
85+
86+
87+
class TransitionDelay(EventTransitionBase):
88+
def __init__(self, duration: float):
89+
super().__init__(duration=duration)
90+
91+
92+
class TransitionAttr(EventTransitionBase):
93+
"""
94+
Changes an attribute over time.
95+
96+
:param start: start value, if None, the subjects value is read via `getattr`
97+
:param end: target value
98+
:param attribute: attribute to set
99+
:param duration: Duration of the transition in seconds
100+
:param ease_function:
101+
:param delay: Start transition after x seconds
102+
:param mutation_function: function to be used to set new value
103+
"""
104+
105+
def __init__(
106+
self,
107+
*,
108+
end,
109+
attribute,
110+
duration: float,
111+
start=None,
112+
ease_function=EaseFunctions.linear,
113+
delay=0.0,
114+
mutation_function: Callable[[Any, str, float], None] = setattr,
115+
):
116+
super().__init__(duration=duration, delay=delay)
117+
self._start: Optional[float] = start
118+
self._end = end
119+
self._attribute = attribute
120+
121+
self._ease_function = ease_function
122+
self._mutation_function = mutation_function
123+
124+
def on_tick(self, subject, progress):
125+
if self._start is None:
126+
self._start = getattr(subject, self._attribute)
127+
128+
factor = self._ease_function(progress)
129+
new_value = self._start + (self._end - self._start) * factor
130+
131+
self._mutation_function(subject, self._attribute, new_value)
132+
133+
134+
class TransitionAttrIncr(TransitionAttr):
135+
"""
136+
Changes an attribute over time.
137+
138+
:param increment: difference the value should be changed over time (can be negative)
139+
:param attribute: attribute to set
140+
:param duration: Duration of the transition in seconds
141+
:param ease_function:
142+
:param delay: Start transition after x seconds
143+
:param mutation_function: function to be used to set new value
144+
"""
145+
146+
def __init__(
147+
self,
148+
*,
149+
increment: float,
150+
attribute,
151+
duration: float,
152+
ease_function=EaseFunctions.linear,
153+
delay=0.0,
154+
mutation_function: Callable[[Any, str, float], None] = setattr,
155+
):
156+
super().__init__(end=increment, attribute=attribute, duration=duration, delay=delay)
157+
self._attribute = attribute
158+
159+
self._ease_function = ease_function
160+
self._mutation_function = mutation_function
161+
162+
def on_tick(self, subject, progress):
163+
if self._start is None:
164+
self._start = getattr(subject, self._attribute)
165+
self._end += self._start
166+
167+
factor = self._ease_function(progress)
168+
new_value = self._start + (self._end - self._start) * factor
169+
170+
self._mutation_function(subject, self._attribute, new_value)
171+
172+
173+
class TransitionAttrSet(EventTransitionBase):
174+
"""
175+
Set the attribute when expired.
176+
177+
:param value: value to set
178+
:param attribute: attribute to set
179+
:param duration: Duration of the transition in seconds
180+
"""
181+
182+
def __init__(
183+
self,
184+
*,
185+
value: float,
186+
attribute,
187+
duration: float,
188+
mutation_function=setattr
189+
):
190+
super().__init__(duration=duration)
191+
self._attribute = attribute
192+
self._value = value
193+
self._mutation_function = mutation_function
194+
195+
def on_finish(self, subject):
196+
setattr(subject, self._attribute, self._value)
197+
198+
199+
class TransitionParallel(TransitionBase):
200+
"""
201+
A transition assembled by multiple transitions.
202+
Executing them in parallel.
203+
"""
204+
205+
def __init__(self, *transactions: TransitionBase):
206+
super().__init__()
207+
self._transitions: List[TransitionBase] = list(transactions)
208+
209+
def add(self, transition: T) -> T:
210+
self._transitions.append(transition)
211+
return transition
212+
213+
def tick(self, subject, dt):
214+
remaining_dt = dt
215+
216+
for transition in self._transitions[:]:
217+
218+
r = transition.tick(subject, dt)
219+
remaining_dt = min(remaining_dt, r)
220+
221+
if transition.finished:
222+
self._transitions.remove(transition)
223+
224+
return remaining_dt
225+
226+
@property
227+
def finished(self) -> bool:
228+
return not self._transitions
229+
230+
231+
class TransitionChain(TransitionBase):
232+
"""
233+
A transition assembled by multiple transitions.
234+
Executing them sequential.
235+
"""
236+
237+
def __init__(self, *transactions: TransitionBase):
238+
super().__init__()
239+
self._transitions: List[TransitionBase] = list(transactions)
240+
241+
def add(self, transition: T) -> T:
242+
self._transitions.append(transition)
243+
return transition
244+
245+
def tick(self, subject, dt):
246+
while dt and not self.finished:
247+
transition = self._transitions[0]
248+
dt = transition.tick(subject, dt)
249+
250+
if transition.finished:
251+
self._transitions.pop(0)
252+
253+
return min(0.0, dt)
254+
255+
@property
256+
def finished(self) -> bool:
257+
return not self._transitions

0 commit comments

Comments
 (0)