# 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
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
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()
def tween_values(self, function, parent_value, value):
"""Call a scalar tween function on two values.
"""
return function(parent_value, value)
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)]
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)
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')