Source code for gillcup.animation

# 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


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)