# Encoding: UTF-8
"""Gillcup's Animation classes
Animations are :mod:`Actions <gillcup.actions>` that modify
:mod:`animated properties <gillcup.properties>`.
To use one, create it and schedule it on a Clock.
Once an animation is in effect, it will smoothly change a property's value
over a specified time interval.
The value is computed as a tween between the property's original value and
the Animation's **target** value.
The tween parameters can be set by the **timing** and **easing** keyword
arguments.
The “original value” of a property is not fixed: it is whatever the
value would have been if this animation wasn't applied (in other words,
it's determined by the :mod:`~gillcup.effect` that was originally on the
property).
Also, if you set the **dynamic** argument to Animation, the animation's
*target* becomes an :class:`~gillcup.AnimatedProperty`.
Animating these allows one to create very complex effects in a modular way.
"""
from __future__ import unicode_literals, division, print_function
from six import string_types
from gillcup.actions import Action
from gillcup.effect import Effect, ConstantEffect
from gillcup.properties import AnimatedProperty
from gillcup import easing as easing_module
[docs]class Animation(Effect, Action):
"""An object that modifies an AnimatedProperty based on Clock time
Positional init arguments:
:argument instance:
The object whose property is animated
:argument property_name:
Name of the animated property
:argument target:
Value at which the animation should arrive (tuple properties
accept more arguments, i.e. ``Animation(obj, 'position', 1, 2, 3)``)
Keyword init arguments:
:argument time:
The duration of the animation
:argument delay:
Delay between the time the animation is scheduled and its actual start
:argument timing:
A function that maps global time to animation's time.
Possible values:
* ``None``: normalizes time so that 0 corresponds to the start of the
animation, and 1 to the end (i.e. start + `time`); clamps to [0, 1]
* ``'infinite'``: same as above, but doesn't clamp: the animation
goes forever on (in both directions; it only starts to take effect
when it's scheduled, but a `delay` can cause negative local times).
The animation's time is normalized to 0 at the start and
1 at start + `time`.
* ``'absolute'``: the animation is infinite, with the same speed as
with the 'infinite' option, but zero corresponds to the clock's
zero.
Useful for synchronized periodic animations.
* `function(time, start, duration)`: apply a custom function
:argument easing:
An easing function to use. Can be either a one-argument
function, or a dotted name which is looked up in the
:mod:`gillcup.easing` module.
:argument dynamic:
If true, the **target** atribute becomes an AnimatedProperty, allowing
for more complex animations.
.. note::
In order to conserve resources, ordinary Animations are released
(replaced by a simple :class:`~gillcup.ConstantEffect`) when
they are “done”.
Arguments such as ``timing``, or the
:class:`~gillcup.animation.Add` or :class:`~gillcup.animation.Multiply`
animation subclasses, which allow the value to be modified after the
``time`` elapses, turn this behavior off by setting the ``dynamic``
attibute to true.
When subclassing Animation, remember to do the same if your subclass
needs to change its value after ``time`` elapses.
This includes cases where the value depends on the value of the
previous (parent) animation.
"""
dynamic = False
start_time = None
parent = None
strength = 1
def __init__(self, instance, property_name, *target, **kwargs):
super(Animation, self).__init__()
self.instance = instance
self.property = self.get_property(instance, property_name)
try:
new_target = kwargs.pop('target')
except KeyError:
pass
else:
if target:
raise ValueError(
'Target specified as both positional and keyword argument')
target = new_target
self.target = self.property.adjust_value(target)
self.dynamic = (
self.dynamic or 'timing' in kwargs or kwargs.pop('dynamic', False))
if not self.dynamic:
self.chain(lambda: self.property.do_replacements(instance))
self.time = kwargs.pop('time', 1)
self.delay = kwargs.pop('delay', 0)
easing = kwargs.pop('easing', 'linear')
timing = kwargs.pop('timing', None)
if timing == 'infinite':
self.get_time = self._infinite_timing
elif timing == 'absolute':
self.get_time = self._absolute_timing
elif timing:
self.get_time = lambda: timing(
self.clock.time, self.start_time, self.time)
if isinstance(easing, string_types):
e = easing_module
for attr in easing.split('.'):
e = getattr(e, attr)
self.easing = e
else:
self.easing = easing
@classmethod
def get_property(cls, instance, property_name):
"""Get a property object off an instance's class"""
return getattr(type(instance), property_name)
def __new__(cls, instance, property_name, *args, **kwargs):
if kwargs.get('dynamic', False):
# We need the target to act the same as the animated property
# (wrt adjust_value: being scalar/tuple, etc).
# An AnimatedProperty needs to be on a class, we can't just put
# a descriptor on an instance.
# So, we create a trivial subclass that has the target property.
prop = cls.get_property(instance, property_name)
class AnimatedAnimation(cls):
"""A more dynamic flavor of gillcup.Animation"""
target = prop.get_target_property()
strength = AnimatedProperty(1)
ani_class = AnimatedAnimation
else:
ani_class = cls
super_new = super(Animation, cls).__new__
return super_new(ani_class, instance, property_name, *args, **kwargs)
def __call__(self):
self.expire()
self.parent = self.property.animate(self.instance, self)
self.start_time = self.clock.time + self.delay
self.clock.schedule(self.trigger_chain, self.time + self.delay)
@property
def value(self):
"""Value to be used for the property this animation is on"""
parent_value = self.parent.value
target = self.target
return self.property.tween_values(
self.compute_value, parent_value, target)
def get_time(self): # pylint: disable=E0202
"""Get the local time for tweening, usually in the [0..1] range"""
elapsed = self.clock.time - self.start_time
if elapsed <= 0:
return 0
if elapsed >= self.time:
return 1
else:
return elapsed / self.time
get_time.finite = True
def _absolute_timing(self):
return self.clock.time / self.time
def _infinite_timing(self):
return (self.clock.time - self.start_time) / self.time
def compute_value(self, previous, target):
"""Given the previous value and a target, compute value"""
t = self.easing(self.get_time()) * self.strength
return previous * (1 - t) + target * t
def get_replacement(self):
if not self.dynamic and self.get_time() >= 1:
# Not gonna change from now on
return ConstantEffect(self.value)
else:
self.parent = self.parent.get_replacement()
return self
[docs]class Add(Animation):
"""An additive animation: the target value is added to the original
"""
dynamic = True
def compute_value(self, previous, target):
t = self.easing(self.get_time())
return previous + target * t
[docs]class Multiply(Animation):
"""A multiplicative animation: target value is multiplied to the original
"""
dynamic = True
def compute_value(self, previous, target):
t = self.easing(self.get_time())
return previous * ((1 - t) + target * t)
[docs]class Computed(Animation):
"""A custom-valued animation: the target is computed by a function
Pass a **func** keyword argument with the function to the constructor.
The function will get one argument: the time elapsed, normalized by the
animation's `timing` function.
"""
def __init__(self, instance, property_name, func, **kwargs):
self.func = func
prop = self.get_property(instance, property_name)
kwargs.setdefault('target', [prop.default])
super(Computed, self).__init__(instance, property_name, **kwargs)
def compute_value(self, previous, target):
t = self.get_time()
return self.func(t)