Skip to content
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
53 changes: 53 additions & 0 deletions arcade/examples/gui/transitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import arcade
from arcade.easing 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()
20 changes: 20 additions & 0 deletions arcade/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@
from arcade.gui.widgets import UILayout
from arcade.gui.widgets import UISpace
from arcade.gui.view import UIView
from arcade.gui.transition import (
TransitionBase,
EventTransitionBase,
TransitionAttr,
TransitionAttrIncr,
TransitionChain,
TransitionParallel,
TransitionDelay,
TransitionAttrSet,
)
from arcade.gui.widgets.dropdown import UIDropdown
from arcade.gui.widgets import UISpriteWidget
from arcade.gui.widgets import UIWidget
Expand Down Expand Up @@ -97,6 +107,16 @@
"UIWidget",
"Surface",
"NinePatchTexture",
# Transitions
"EaseFunctions",
"TransitionBase",
"EventTransitionBase",
"TransitionAttr",
"TransitionAttrIncr",
"TransitionAttrSet",
"TransitionChain",
"TransitionParallel",
"TransitionDelay",
# Property classes
"ListProperty",
"DictProperty",
Expand Down
240 changes: 240 additions & 0 deletions arcade/gui/transition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
from abc import ABC, abstractmethod
from typing import Callable, Any, Optional, List, TypeVar

from pyglet.event import EventDispatcher

from arcade.easing 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
26 changes: 26 additions & 0 deletions arcade/gui/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@
from arcade.gui.surface import Surface
from arcade.types import AnchorPoint, AsFloat, Color
from arcade.utils import copy_dunders_unimplemented
from arcade.gui.transition import TransitionBase

if TYPE_CHECKING:
from arcade.gui.ui_manager import UIManager

W = TypeVar("W", bound="UIWidget")
P = TypeVar("P")
T = TypeVar("T", bound="TransitionBase")


class FocusMode(IntEnum):
Expand Down Expand Up @@ -182,6 +184,9 @@ def __init__(
for child in children:
self.add(child)

self._transitions: list[TransitionBase] = []
self.event("on_update")(self._update_transitions)

bind(self, "rect", UIWidget.trigger_full_render)
bind(self, "focused", UIWidget.trigger_full_render)
bind(
Expand Down Expand Up @@ -401,6 +406,27 @@ def dispatch_ui_event(self, event: UIEvent):
"""Dispatch a :class:`UIEvent` using pyglet event dispatch mechanism"""
return self.dispatch_event("on_event", event)

def _update_transitions(self, dt):
# Update transitions
for transaction in self._transitions[:]:
transaction.tick(self, dt)

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

def add_transition(self, transition: T) -> T:
"""
Add a transition, which will be updated using on_update time.
"""
self._transitions.append(transition)
return transition

def clear_transitions(self):
"""
Remove all transitions from this widget. Finished Transitions are removed automatically.
"""
self._transitions.clear()

def move(self, dx=0, dy=0):
"""Move the widget by dx and dy.

Expand Down
Loading
Loading