Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Add Transitions to arcade GUI [after 3.0 release] #1420

Draft
wants to merge 3 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions arcade/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@
UILayout,
)
from arcade.gui.widgets import UIDummy, Rect
from arcade.gui.transition import (
TransitionBase,
EventTransitionBase,
TransitionAttr,
TransitionAttrIncr,
TransitionChain,
TransitionParallel,
TransitionDelay,
TransitionAttrSet,
)
from arcade.gui.widgets import UIInteractiveWidget
from arcade.gui.widgets.text import UILabel, UIInputText, UITextArea
from arcade.gui.widgets.toggle import UITextureToggle
Expand Down Expand Up @@ -88,6 +98,15 @@
"Surface",
"Rect",
"NinePatchTexture",
# Transitions
"TransitionBase",
"EventTransitionBase",
"TransitionAttr",
"TransitionAttrIncr",
"TransitionAttrSet",
"TransitionChain",
"TransitionParallel",
"TransitionDelay",
# Property classes
"ListProperty",
"DictProperty",
Expand Down
64 changes: 64 additions & 0 deletions arcade/gui/examples/transitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import arcade
from arcade import linear
from arcade.gui import UIManager, TransitionChain, TransitionAttr, TransitionAttrIncr
from arcade.gui.transition import TransitionAttrSet
from arcade.gui.widgets.buttons import UIFlatButton


class DemoWindow(arcade.Window):
def __init__(self):
super().__init__(800, 600, "UI Mockup", resizable=True)
arcade.set_background_color(arcade.color.DARK_BLUE_GRAY)

# Init UIManager
self.manager = UIManager()
self.manager.enable()

button = self.manager.add(UIFlatButton(text="Click me I can move!"))
button.center_on_screen()

@button.event
def on_click(event):
# button.disabled = True

start_x, start_y = button.center
chain = TransitionChain()

chain.add(TransitionAttrSet(attribute="disabled", value=True, duration=0))

chain.add(TransitionAttrIncr(
attribute="center_x",
increment=100,
duration=1.0
))
chain.add(TransitionAttrIncr(
attribute="center_y",
increment=100,
duration=1,
ease_function=linear
))

# Go back
chain.add(TransitionAttr(
attribute="center_x",
end=start_x,
duration=1,
ease_function=linear
))
chain.add(TransitionAttr(
attribute="center_y",
end=start_y,
duration=1,
ease_function=linear
))
chain.add(TransitionAttrSet(attribute="disabled", value=False, duration=0))

button.add_transition(chain)

def on_draw(self):
self.clear()
self.manager.draw()


if __name__ == "__main__":
DemoWindow().run()
248 changes: 248 additions & 0 deletions arcade/gui/transition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
from abc import ABC, abstractmethod
from typing import Callable, Any, Optional, List, TypeVar

from pyglet.event import EventDispatcher

from arcade import linear

T = TypeVar("T", bound="TransitionBase")


class TransitionBase(ABC):
@abstractmethod
def tick(self, subject, dt) -> float:
"""
Update

:return: dt, which is not consumed
"""
pass

@property
@abstractmethod
def finished(self) -> bool:
raise NotImplementedError()

def __add__(self, other):
return TransitionChain(self, other)

def __or__(self, other):
return TransitionParallel(self, other)


class EventTransitionBase(TransitionBase, EventDispatcher):
"""
Extension of TransitionBase, providing hooks via

- on_tick(subject, progress: float)
- on_finish(subject)

:param duration: Duration of the transition in seconds
:param delay: Start transition after x seconds
"""

def __init__(
self,
*,
duration: float,
delay=0.0,
):
self._duration = duration
self._elapsed = -delay

self.register_event_type("on_tick")
self.register_event_type("on_finish")

def tick(self, subject, dt) -> float:
self._elapsed += dt
if self._elapsed >= 0:
progress = min(self._elapsed / self._duration, 1) if self._duration else 1
self.dispatch_event("on_tick", subject, progress)

if self.finished:
self.dispatch_event("on_finish", subject)

return max(0.0, self._elapsed - self._duration)

def on_tick(self, subject, progress):
pass

def on_finish(self, subject):
pass

@property
def finished(self):
return self._elapsed >= self._duration


class TransitionDelay(EventTransitionBase):
def __init__(self, duration: float):
super().__init__(duration=duration)


class TransitionAttr(EventTransitionBase):
"""
Changes an attribute over time.

:param start: start value, if None, the subjects value is read via `getattr`
:param end: target value
:param attribute: attribute to set
:param duration: Duration of the transition in seconds
:param ease_function:
:param delay: Start transition after x seconds
:param mutation_function: function to be used to set new value
"""

def __init__(
self,
*,
end,
attribute,
duration: float,
start=None,
ease_function=linear,
delay=0.0,
mutation_function: Callable[[Any, str, float], None] = setattr,
):
super().__init__(duration=duration, delay=delay)
self._start: Optional[float] = start
self._end = end
self._attribute = attribute

self._ease_function = ease_function
self._mutation_function = mutation_function

def on_tick(self, subject, progress):
if self._start is None:
self._start = getattr(subject, self._attribute)

factor = self._ease_function(progress)
new_value = self._start + (self._end - self._start) * factor

self._mutation_function(subject, self._attribute, new_value)


class TransitionAttrIncr(TransitionAttr):
"""
Changes an attribute over time.

:param increment: difference the value should be changed over time (can be negative)
:param attribute: attribute to set
:param duration: Duration of the transition in seconds
:param ease_function:
:param delay: Start transition after x seconds
:param mutation_function: function to be used to set new value
"""

def __init__(
self,
*,
increment: float,
attribute,
duration: float,
ease_function=linear,
delay=0.0,
mutation_function: Callable[[Any, str, float], None] = setattr,
):
super().__init__(end=increment, attribute=attribute, duration=duration, delay=delay)
self._attribute = attribute

self._ease_function = ease_function
self._mutation_function = mutation_function

def on_tick(self, subject, progress):
if self._start is None:
self._start = getattr(subject, self._attribute)
self._end += self._start

factor = self._ease_function(progress)
new_value = self._start + (self._end - self._start) * factor

self._mutation_function(subject, self._attribute, new_value)


class TransitionAttrSet(EventTransitionBase):
"""
Set the attribute when expired.

:param value: value to set
:param attribute: attribute to set
:param duration: Duration of the transition in seconds
"""

def __init__(
self,
*,
value: float,
attribute,
duration: float,
mutation_function=setattr
):
super().__init__(duration=duration)
self._attribute = attribute
self._value = value
self._mutation_function = mutation_function

def on_finish(self, subject):
setattr(subject, self._attribute, self._value)


class TransitionParallel(TransitionBase):
"""
A transition assembled by multiple transitions.
Executing them in parallel.
"""

def __init__(self, *transactions: TransitionBase):
super().__init__()
self._transitions: List[TransitionBase] = list(transactions)

def add(self, transition: T) -> T:
self._transitions.append(transition)
return transition

def tick(self, subject, dt):
remaining_dt = dt

for transition in self._transitions[:]:

r = transition.tick(subject, dt)
remaining_dt = min(remaining_dt, r)

if transition.finished:
self._transitions.remove(transition)

return remaining_dt

@property
def finished(self) -> bool:
return not self._transitions


class TransitionChain(TransitionBase):
"""
A transition assembled by multiple transitions.
Executing them sequential.
"""

def __init__(self, *transactions: TransitionBase):
super().__init__()
self._transitions: List[TransitionBase] = list(transactions)

def add(self, transition: T) -> T:
self._transitions.append(transition)
return transition

def tick(self, subject, dt):
while dt and not self.finished:
transition = self._transitions[0]
dt = transition.tick(subject, dt)

if transition.finished:
self._transitions.pop(0)

return min(0.0, dt)

@property
def finished(self) -> bool:
return not self._transitions
Loading