Source code for gillcup.properties

# Encoding: UTF-8
"""Gillcup's Animated Properties

To animate Python objects, we need to change values of their attributes over
time.
There are two kinds of changes we can make: *discrete* and *continuous*.
A discrete change happens at a single point in time: for example, an object
is shown, some output is written, a sound starts playing.
:mod:`Actions <gillcup.actions>` are used for effecting
discrete changes.

Continuous changes happen over a period of time: an object smoothly moves
to the left, or a sound fades out.
These changes are made by animating special properties on objects,
using :mod:`Animation <gillcup.animation>` classes on so-called
:mod:`animated properties <gillcup.properties>`.

Under the hood, Gillcup uses Python's `descriptor interface
<http://docs.python.org/howto/descriptor.html>`_ to provide efficient
animated properties.

Assigment to an animated attribute causes the property to get set to the given
value and cancels any running animations on it.
"""

from __future__ import unicode_literals, division, print_function

from gillcup.effect import Effect, ConstantEffect


[docs]class AnimatedProperty(object): """A scalar animated property The idiomatic way to define animated properties is as follows:: class Tone(object): pitch = AnimatedProperty(440) volume = AnimatedProperty(0) Now, Tone instances will have `pitch` and `volume` set to the corresponding defaults, and can be animated. The **docstring** argument becomes the property's ``__doc__`` attribute. """ def __init__(self, default, docstring=None): self.default = default if docstring: self.__doc__ = docstring
[docs] def adjust_value(self, values): """Convert an animation's ``*args`` values into a property value For scalar properties, this converts a 1-tuple into its only element """ [value] = values return value
def get_target_property(self): """Return a property used for a dynamic animation's target """ # Since AnimatedProperty doesn't care about its name, we can just # reuse it return self def __get__(self, instance, owner): if instance: return self.get_effect(instance).value else: return self def __set__(self, instance, value): self.animate(instance, ConstantEffect(value)) self.do_replacements(instance) def __delete__(self, instance): self.animate(instance, ConstantEffect(self.default)) self.do_replacements(instance) def get_effect(self, instance): """Get the current effect; possibly create a default one beforehand""" # pylint: disable=W0212 try: effects = instance.__gillcup_effects except AttributeError: effects = instance.__gillcup_effects = {} try: effect = effects[self] except KeyError: effect = effects[self] = ConstantEffect(self.default) return effect def animate(self, instance, animation): """Set a new effect on this property; return the old one""" # pylint: disable=W0212 parent = self.get_effect(instance) instance.__gillcup_effects[self] = animation return parent.get_replacement() def do_replacements(self, instance): """Possibly replace current effect w/ a more lightweight equivalent""" # pylint: disable=W0212 try: effects = instance.__gillcup_effects current_effect = effects[self] except (AttributeError, KeyError): pass else: effects[self] = current_effect.get_replacement()
[docs] def tween_values(self, function, parent_value, value): """Call a scalar tween function on two values. """ return function(parent_value, value)
[docs]class TupleProperty(AnimatedProperty): """A tuple animated property Iterating the TupleProperty itself yields sub-properties that can be animated individually. The intended idiom for declaring a tuple property is:: x, y, z = position = TupleProperty(0, 0, 0) """ def __init__(self, *default, **kwargs): super(TupleProperty, self).__init__(default, **kwargs) self.size = len(default) self.subproperties = [ _TupleElementProperty(self, i) for i in range(self.size)]
[docs] def adjust_value(self, value): """Convert an animation's ``*args`` values into a property value For tuple properties, return the tuple unchanged """ return value
def __set__(self, instance, value): self.animate(instance, ConstantEffect(self.adjust_value(value))) def __iter__(self): return iter(self.subproperties)
[docs] def tween_values(self, function, parent_value, value): """Call a scalar tween function on two values. Calls the function on corresponding pairs of elements, returns the tuple of results """ return tuple(map(function, parent_value, value))
class _TupleElementProperty(AnimatedProperty): """Animated property for one element of a TupleProperty """ def __init__(self, parent, index): super(_TupleElementProperty, self).__init__(parent.default[index]) self.parent = parent self.index = index def get_effect(self, instance): parent_effect = self.parent.get_effect(instance) return _TupleExtractEffect(parent_effect, self.index) def animate(self, instance, animation): tuple_effect = _TupleMakeEffect(animation, self.index) parent = self.parent.animate(instance, tuple_effect) tuple_effect.parent = parent return _TupleExtractEffect(parent, self.index) def do_replacements(self, instance): self.parent.do_replacements(instance) class _TupleExtractEffect(Effect): """Effect that extracts one element of a tuple """ def __init__(self, parent, index): super(_TupleExtractEffect, self).__init__() self.parent = parent self.index = index @property def value(self): """Value to be used for the property this effect is on""" return self.parent.value[self.index] def get_replacement(self): self.parent = self.parent.get_replacement() if (isinstance(self.parent, _TupleMakeEffect) and self.parent.index == self.index): return self.parent.element_effect elif isinstance(self.parent, ConstantEffect): return ConstantEffect(self.value) else: return self class _TupleMakeEffect(Effect): """Effect that recombines one changed element of a tuple with the rest `element_effect` is an Effect whose `value` is used for the changed element The `parent` attribute has the Effect with the original, full tuple. This attribute must be set after instantiation. """ element_effect = parent = None def __init__(self, element_effect, index): super(_TupleMakeEffect, self).__init__() self.element_effect = element_effect self.index = index @property def value(self): """Value to be used for the property this effect is on""" element_effect = self.element_effect return tuple( element_effect.value if i == self.index else val for i, val in enumerate(self.parent.value)) def get_replacement(self): self.parent = self.parent.get_replacement() self.element_effect = self.element_effect.get_replacement() if (isinstance(self.parent, _TupleMakeEffect) and self.parent.index == self.index): self.parent = self.parent.parent return self.get_replacement() elif (isinstance(self.parent, ConstantEffect) and isinstance(self.element_effect, ConstantEffect)): return ConstantEffect(self.value) else: return self
[docs]class ScaleProperty(TupleProperty): """A TupleProperty used for scales or sizes in multiple dimensions It acts as a regular TupleProperty, but supports scalars or short tuples in assignment or animation. Instead of a default value, __init__ takes the number of dimensions; the default value will be ``(1,) * num_dimensions``. If a scalar, or a tuple with only one element, is given, the value is repeated across all dimensions. If another short tuple is given, the remaining dimensions are set to 1. For example, given:: width, height, length = size = ScaleProperty(3) the following pairs are equivalent:: obj.size = 2 obj.size = 2, 2, 2 obj.size = 2, 3 obj.size = 2, 3, 1 obj.size = 2, obj.size = 2, 2, 2 Similarly, ``Animation(obj, 'size', 2)`` is equivalent to ``Animation(obj, 'size', 2, 2, 2)``. """ def __init__(self, num_dimensions, **kwargs): super(ScaleProperty, self).__init__(*(1, ) * num_dimensions, **kwargs)
[docs] def adjust_value(self, value): """Expand the given tuple or scalar to a tuple of len=num_dimensions """ try: size = len(value) except TypeError: return (value, ) * self.size if size == self.size: return value elif size == 1: return value * self.size elif size < self.size: return value + (1, ) * (self.size - size) else: raise ValueError('Too many dimensions for ScaleProperty')
[docs]class VectorProperty(TupleProperty): """A TupleProperty used for vectors It acts as a regular TupleProperty, but supports short tuples in assignment or animation by setting all remaining dimensions to 0. Instead of a default value, __init__ takes the number of dimensions; the default value will be ``(0,) * num_dimensions``. For example, given:: x, y, z = position = VectorProperty(3) the following pairs are equivalent:: obj.position = 2, 3 obj.position = 2, 3, 0 obj.position = 2, obj.position = 2, 0, 0 Similarly, ``Animation(obj, 'position', 1, 2)`` is equivalent to ``Animation(obj, 'position', 1, 2, 0)``. """ def __init__(self, num_dimensions, **kwargs): super(VectorProperty, self).__init__(*(0, ) * num_dimensions, **kwargs)
[docs] def adjust_value(self, value): """Expand the given tuple to the correct number of dimensions """ size = len(value) if size == self.size: return value elif size < self.size: return value + (0, ) * (self.size - size) else: raise ValueError('Too many dimensions for VectorProperty')