Skip to content

Commit

Permalink
Fix layers, fix animation defaults, make docs for both, incremenet de…
Browse files Browse the repository at this point in the history
…signer
  • Loading branch information
acbart committed Nov 21, 2023
1 parent d4ad479 commit 76f18c9
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 15 deletions.
5 changes: 5 additions & 0 deletions change_log.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Change Log
==========

# Versio 0.6.4

* Fixed bugs with animations' default values
* Fixed bugs with layer's weakref handling of set_window_layers

# Version 0.6.3

* Contributed by @codeBodger: allow custom fonts with font files
Expand Down
2 changes: 1 addition & 1 deletion designer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from os import environ

environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1'
__version__ = '0.6.3'
__version__ = '0.6.4'

# For `debug` support on Mac, we need to preload tkinter
from designer.system import setup_debug_mode
Expand Down
50 changes: 46 additions & 4 deletions designer/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,51 @@ def spin(obj, duration=5, angle_limit=360):
obj.animate(Animation('angle', Linear(0, angle_limit), duration, loop=True))


def linear_animation(obj, property, start, end, duration, absolute=False, shift=False, loop=False):
obj.animate(Animation(property, Linear(start, end), duration, absolute, shift, loop))
def linear_animation(obj, property, start, end, duration, absolute=True, shift=None, loop=False):
"""
Animates an object's property, interpolating linearly between ``start`` and ``end``. For example, if you want
the object to glide from the left to the right of the screen, you can animate the object's ``x`` property from
``0`` to ``get_width()``. Or you could animate the object's ``angle`` property from ``0`` to ``360`` to make it
spin in place.
:param obj: The designerobject to animate
:type obj: DesignerObject
:param property: The name of the property to animate (e.g., 'x', 'y', 'angle')
:type property: str
:param start: The starting value of the property
:type start: int or float
:param end: The ending value of the property
:type end: int or float
:param duration: The duration of the animation in seconds
:type duration: float
:param absolute: (TODO) This parameter is not implemented yet
:param shift: (TODO) this parameter is not implemented yet
:param loop: Whether to loop the animation
:type loop: bool
:return: This DesignerObject
"""
return obj.animate(Animation(property, Linear(start, end), duration, absolute, shift, loop))


def sequence_animation(obj, property, items, duration, times=1, absolute=False, shift=False, loop=False):
obj.animate(Animation(property, Iterate(items, times), duration, absolute, shift, loop))
def sequence_animation(obj, property, items, duration, times=1, absolute=True, shift=None, loop=False):
"""
Animates an object's property in sequence. For example, if you have a list of images, you can animate the object's
filename property to change the image repeatedly. This is useful for creating animations.
:param obj: The designerobject to animate
:type obj: DesignerObject
:param property: The name of the property to animate (e.g., 'x', 'y', 'angle', 'filename', 'name')
:type property: str
:param items: The items to iterate through with the animation
:type items: list
:param duration: The duration of the animation in seconds
:type duration: float
:param times: The number of times to iterate through the items
:type times: int
:param absolute: (TODO) This parameter is not implemented yet
:param shift: (TODO) this parameter is not implemented yet
:param loop: Whether to loop the animation
:type loop: bool
:return: This DesignerObject
"""
return obj.animate(Animation(property, Iterate(items, times), duration, absolute, shift, loop))
1 change: 1 addition & 0 deletions designer/objects/designer_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ def animate(self, animation):
# Loop over all possible properties
for a_property in animation.properties:
designer.core.event.handle(f"{self.__class__.__name__}.{a_property}.animation.start", e)
return self

def stop_animation(self, animation):
"""
Expand Down
10 changes: 5 additions & 5 deletions designer/utilities/layer_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
A number representing a Relative Depth Chain collapsed into a single
integer (or long, possibly)
"""
from designer.utilities.weak_tuple_dictionary import MultiWeakKeyDict

try:
from weakref import WeakKeyDictionary, ref as _wref
Expand Down Expand Up @@ -45,7 +46,7 @@ class _LayerTree:
def __init__(self, scene):
self.layers = WeakKeyDictionary({scene : []})
self.child_views = WeakKeyDictionary({scene : []})
self.layer_location = WeakKeyDictionary({scene : [0]})
self.layer_location = MultiWeakKeyDict({scene : [0]})
self.scene = _wref(scene)
self.tree_height = WeakKeyDictionary({scene : 1})
self._precompute_positions()
Expand Down Expand Up @@ -74,10 +75,9 @@ def add_view(self, view):
:type view: View (not a weakref)
"""
parent = view._parent
view = _wref(view)
self.layers[view] = []
self.child_views[view] = []
self.child_views[parent].append(view)
self.child_views[parent].append(_wref(view))
self.tree_height[view] = 1
if len(self.child_views[parent]) == 1:
self.tree_height[parent] += 1
Expand Down Expand Up @@ -109,7 +109,7 @@ def set_view_layers(self, view, layers):
:param layers: the name of the layer on the parent
:type layers: a list of strings
"""
self.layers[_wref(view)] = list(layers)
self.layers[view] = list(layers)
self._precompute_positions()

def _compute_positional_chain(self, chain):
Expand Down Expand Up @@ -175,7 +175,7 @@ def get_layer_position(self, parent, layer):
:type layer: string
:returns: A `float` representing where this layer is relative to others.
"""
parent = _wref(parent)
#parent = _wref(parent)
if not layer:
layer = ""
s = layer.split(':')
Expand Down
54 changes: 54 additions & 0 deletions designer/utilities/weak_tuple_dictionary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import weakref
from collections.abc import MutableMapping


class MultiWeakKeyDict(MutableMapping):
"""
https://stackoverflow.com/a/74270121/1718155
A dictionary that uses weak references to keys, and can have multiple keys
"""
def __init__(self, initial_data=None):
self.data = {}
self.helpers = {}

if initial_data is not None:
for key, value in initial_data.items():
self[key] = value

def _remove(self, wref):
for data_key in self.helpers.pop(wref, ()):
try:
del self.data[data_key]
except KeyError:
pass

def _build_key(self, keys):
if isinstance(keys, tuple):
return tuple(weakref.ref(item, self._remove) if not isinstance(item, str) else item
for item in keys)
else:
return (weakref.ref(keys, self._remove) if not isinstance(keys, str) else keys
,)

def __setitem__(self, keys, value):
weakrefs = self._build_key(keys)
for item in weakrefs:
self.helpers.setdefault(item, set()).add(weakrefs)
self.data[weakrefs] = value

def __getitem__(self, keys):
return self.data[self._build_key(keys)]

def __delitem__(self, keys):
del self.data[self._build_key(keys)]

def __iter__(self):
for key in self.data:
yield tuple(item if isinstance(item, str) else item()
for item in key)

def __len__(self):
return len(self.data)

def __repr__(self):
return f"{self.__class__.__name__}({', '.join('{!r}:{!r}'.format(k, v) for k, v in self.items())})"
58 changes: 56 additions & 2 deletions docsrc/students/docs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,51 @@ Designer Objects Attributes
# Fully visible
ada.alpha = 1.0
.. py:attribute:: layer
:type: string

A string indicating the layer that this object should be drawn on. Objects
with the same layer will be drawn on top of each other in the order that
they were created. The order of the layers should be set using :py:func:`set_window_layers`
function, providing a list of the layer names (strings) in order from bottom to top.

Alternatively, you can force a specific object to be drawn on top of all other layers using the ``"top"`` layer
or below everything using the ``"bottom"`` layers, which are built in.

There are also layer modifiers, which can be used to relatively position objects within layers:

- ``"layer_name:top"``
- ``"layer_name:above"``
- ``"layer_name"``
- ``"layer_name:below"``
- ``"layer_name:bottom"``

Swap out the name of the layer for the layer you want to modify. For example, if you have a layer called
``"background"`` and you want to draw an object on top of everything else in that layer, you would use
``"background:top"``. Note that you cannot use these modifiers with the ``"top"`` and ``"bottom"`` layers,
but they can be used without a layer name to modify the default layer.

.. code-block:: python
box = rectangle('red', 20, 50)
box2 = rectangle('blue', 50, 20)
# box on top
box.layer = 'top'
# box on bottom
box.layer = 'bottom'
# box2 above box
box.layer = 'below
box2.layer = 'above'
# Can only call set_window_layer once, but makes things explicit
set_window_layers(['background', 'foreground'])
box.layer = 'background'
box2.layer = 'foreground'
# Make circle in background layer appear behind everything else
green_circle = circle('green', 200, 200)
green_circle.layer = 'background:bottom'
.. py:attribute:: scale
:type: [scale_x, scale_y]

Expand Down Expand Up @@ -614,8 +659,7 @@ Animation
---------

.. automodule:: designer.animation
:members: glide_down, glide_in_degrees, glide_left, glide_right, glide_up, spin

:members: linear_animation, sequence_animation

Settings
--------
Expand Down Expand Up @@ -672,6 +716,16 @@ Settings
can be a local file or a URL to a remote image. Keep in mind that
loading large URLs can take a little while.

.. py:function:: set_window_layers(layer_names)
:param layer_names: List of layer names in order from bottom to top
:type layer_names: list[str]

Sets the order of the layers in the window. The first layer in the list will be drawn first, and the last layer
in the list will be drawn last. The order of the layers is important, because objects in the same layer will
be drawn on top of each other in the order that they were created. You can use layer modifiers to relatively
position objects within layers. See the documentation on the ``layer`` attribute for more information.

Events
------

Expand Down
6 changes: 3 additions & 3 deletions docsrc/students/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ Creating objects
``line(color, start_x, start_y, end_x, end_y, thickness)``
====================================================================== =================================================

========================================================================================== =================================================
============================================================================================ =================================================
:ref:`Arc between two points<arc>`
========================================================================================== =================================================
============================================================================================ =================================================
``arc(color, start_angle, stop_angle, width, height)``
``arc(color, start_angle, stop_angle, width, height, x, y)``
``arc(color, start_angle, stop_angle, width, height, x, y, thickness=1, anchor='center')``
========================================================================================== =================================================
============================================================================================ =================================================

====================================================================== =================================================
:ref:`Shape<shape>`
Expand Down
18 changes: 18 additions & 0 deletions examples/animating_emoji.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from designer import *
from dataclasses import dataclass


@dataclass
class World:
spinner: DesignerObject

def create_world() -> World:
""" Create the world """
spinner = emoji("dog")
linear_animation(spinner, 'angle', 0, 360, 3, loop=True)
sequence_animation(spinner, 'name', ['dog', 'cat', 'frog', 'dragon', 'sheep'], 5, loop=True)
return World(spinner)


when('starting', create_world)
start()
42 changes: 42 additions & 0 deletions examples/explicit_layers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from designer import *
from dataclasses import dataclass

@dataclass
class World:
box: DesignerObject
label: DesignerObject
box2: DesignerObject
label2: DesignerObject
green_circle: DesignerObject
orange_circle: DesignerObject

set_window_layers(['background', 'foreground'])


@starting
def create_world() -> World:
box = rectangle('pink', 20, 50)
label = text('black', "Background Box", 20)
box.size = label.size
box2 = rectangle('cornflowerblue', 50, 20)
label2 = text('black', "Foreground Box", 20)
box2.size = label2.size
box.layer = 'background'
label.layer = 'background:above'
box2.layer = 'foreground'
label2.layer = 'foreground:above'
# Make circle in background layer appear behind everything else
green_circle = circle('green', 200, 200)
green_circle.layer = 'background:bottom'
orange_circle = circle('orange', 5)
orange_circle.layer = 'foreground:top'

linear_animation(box2, 'x', 0, get_width(), 3, loop=True)
linear_animation(label2, 'x', 0, get_width(), 3, loop=True)

linear_animation(box, 'y', 0, get_height(), 3, loop=True)
linear_animation(label, 'y', 0, get_height(), 3, loop=True)

return World(box, label, box2, label2, green_circle, orange_circle)

start()
42 changes: 42 additions & 0 deletions examples/layer_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from designer import *
from dataclasses import dataclass

@dataclass
class World:
dog: DesignerObject
cat: DesignerObject
frog: DesignerObject
dragon: DesignerObject
sheep: DesignerObject

def create_world() -> World:
""" Create the world """
dog = emoji("dog")
cat = emoji("cat")
frog = emoji("frog")
dragon = emoji("dragon")
sheep = emoji("sheep")

# Cat -> Frog -> Dog
dog.layer = 'top'
cat.layer = 'bottom'
frog.pos = cat.pos = dog.pos = [get_width() / 3, get_height() / 3]
cat.x -= 10
dog.x += 10

# Sheep and Dragon layered
set_window_layers(['first', 'second'])
sheep.layer = 'first'
dragon.layer = 'second'

return World(dog, cat, frog, dragon, sheep)

def swap_layers(world: World):
""" Swap the layers of the dog and cat """
world.dog.layer, world.cat.layer = world.cat.layer, world.dog.layer

world.sheep.layer, world.dragon.layer = world.dragon.layer, world.sheep.layer

when('starting', create_world)
when('clicking', swap_layers)
start()

0 comments on commit 76f18c9

Please sign in to comment.