Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/lib/pytweener.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pytweener.py')
-rw-r--r--lib/pytweener.py343
1 files changed, 343 insertions, 0 deletions
diff --git a/lib/pytweener.py b/lib/pytweener.py
new file mode 100644
index 0000000..f5cacd7
--- /dev/null
+++ b/lib/pytweener.py
@@ -0,0 +1,343 @@
+# pyTweener
+#
+# Tweening functions for python
+#
+# Heavily based on caurina Tweener: http://code.google.com/p/tweener/
+#
+# Released under M.I.T License - see above url
+# Python version by Ben Harling 2009
+# All kinds of slashing and dashing by Toms Baugis 2010
+import math
+import collections
+import datetime as dt
+import time
+import re
+
+class Tweener(object):
+ def __init__(self, default_duration = None, tween = None):
+ """Tweener
+ This class manages all active tweens, and provides a factory for
+ creating and spawning tween motions."""
+ self.current_tweens = collections.defaultdict(set)
+ self.default_easing = tween or Easing.Cubic.ease_in_out
+ self.default_duration = default_duration or 1.0
+
+ def has_tweens(self):
+ return len(self.current_tweens) > 0
+
+
+ def add_tween(self, obj, duration = None, easing = None, on_complete = None, on_update = None, **kwargs):
+ """
+ Add tween for the object to go from current values to set ones.
+ Example: add_tween(sprite, x = 500, y = 200, duration = 0.4)
+ This will move the sprite to coordinates (500, 200) in 0.4 seconds.
+ For parameter "easing" you can use one of the pytweener.Easing
+ functions, or specify your own.
+ The tweener can handle numbers, dates and color strings in hex ("#ffffff").
+ This function performs overwrite style conflict solving - in case
+ if a previous tween operates on same attributes, the attributes in
+ question are removed from that tween.
+ """
+ if duration is None:
+ duration = self.default_duration
+
+ easing = easing or self.default_easing
+
+ tw = Tween(obj, duration, easing, on_complete, on_update, **kwargs )
+
+ if obj in self.current_tweens:
+ for current_tween in tuple(self.current_tweens[obj]):
+ prev_keys = set((key for (key, tweenable) in current_tween.tweenables))
+ dif = prev_keys & set(kwargs.keys())
+
+ for key, tweenable in tuple(current_tween.tweenables):
+ if key in dif:
+ current_tween.tweenables.remove((key, tweenable))
+
+ if not current_tween.tweenables:
+ current_tween.finish()
+ self.current_tweens[obj].remove(current_tween)
+
+
+ self.current_tweens[obj].add(tw)
+ return tw
+
+
+ def get_tweens(self, obj):
+ """Get a list of all tweens acting on the specified object
+ Useful for manipulating tweens on the fly"""
+ return self.current_tweens.get(obj, None)
+
+ def kill_tweens(self, obj = None):
+ """Stop tweening an object, without completing the motion or firing the
+ on_complete"""
+ if obj:
+ try:
+ del self.current_tweens[obj]
+ except:
+ pass
+ else:
+ self.current_tweens = collections.defaultdict(set)
+
+ def remove_tween(self, tween):
+ """"remove given tween without completing the motion or firing the on_complete"""
+ if tween.target in self.current_tweens and tween in self.current_tweens[tween.target]:
+ self.current_tweens[tween.target].remove(tween)
+ if not self.current_tweens[tween.target]:
+ del self.current_tweens[tween.target]
+
+ def finish(self):
+ """jump the the last frame of all tweens"""
+ for obj in self.current_tweens:
+ for tween in self.current_tweens[obj]:
+ tween.finish()
+ self.current_tweens = {}
+
+ def update(self, delta_seconds):
+ """update tweeners. delta_seconds is time in seconds since last frame"""
+
+ for obj in tuple(self.current_tweens):
+ for tween in tuple(self.current_tweens[obj]):
+ done = tween._update(delta_seconds)
+ if done:
+ self.current_tweens[obj].remove(tween)
+ if tween.on_complete: tween.on_complete(tween.target)
+
+ if not self.current_tweens[obj]:
+ del self.current_tweens[obj]
+
+ return self.current_tweens
+
+
+class Tween(object):
+ __slots__ = ('tweenables', 'target', 'delta', 'duration', 'ease', 'delta',
+ 'on_complete', 'on_update', 'complete')
+
+ def __init__(self, obj, duration, easing, on_complete, on_update, **kwargs):
+ """Tween object use Tweener.add_tween( ... ) to create"""
+ self.duration = duration
+ self.target = obj
+ self.ease = easing
+
+ # list of (property, start_value, delta)
+ self.tweenables = set()
+ for key, value in kwargs.items():
+ self.tweenables.add((key, Tweenable(self.target.__dict__[key], value)))
+
+ self.delta = 0
+ self.on_complete = on_complete
+ self.on_update = on_update
+ self.complete = False
+
+ def finish(self):
+ self._update(self.duration)
+
+ def _update(self, ptime):
+ """Update tween with the time since the last frame"""
+ self.delta = self.delta + ptime
+ if self.delta > self.duration:
+ self.delta = self.duration
+
+ if self.delta == self.duration:
+ for key, tweenable in self.tweenables:
+ self.target.__setattr__(key, tweenable.target_value)
+ else:
+ fraction = self.ease(self.delta / self.duration)
+
+ for key, tweenable in self.tweenables:
+ self.target.__setattr__(key, tweenable.update(fraction))
+
+ if self.delta == self.duration or len(self.tweenables) == 0:
+ self.complete = True
+
+ if self.on_update:
+ self.on_update(self.target)
+
+ return self.complete
+
+
+
+
+class Tweenable(object):
+ """a single attribute that has to be tweened from start to target"""
+ __slots__ = ('start_value', 'change', 'decode_func', 'target_value', 'update')
+
+ hex_color_normal = re.compile("#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})")
+ hex_color_short = re.compile("#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])")
+
+
+ def __init__(self, start_value, target_value):
+ self.decode_func = lambda x: x
+ self.target_value = target_value
+
+ def float_update(fraction):
+ return self.start_value + self.change * fraction
+
+ def date_update(fraction):
+ return dt.date.fromtimestamp(self.start_value + self.change * fraction)
+
+ def datetime_update(fraction):
+ return dt.datetime.fromtimestamp(self.start_value + self.change * fraction)
+
+ def color_update(fraction):
+ val = [max(min(self.start_value[i] + self.change[i] * fraction, 255), 0) for i in range(3)]
+ return "#%02x%02x%02x" % (val[0], val[1], val[2])
+
+
+ if isinstance(start_value, int) or isinstance(start_value, float):
+ self.start_value = start_value
+ self.change = target_value - start_value
+ self.update = float_update
+ else:
+ if isinstance(start_value, dt.datetime) or isinstance(start_value, dt.date):
+ if isinstance(start_value, dt.datetime):
+ self.update = datetime_update
+ else:
+ self.update = date_update
+
+ self.decode_func = lambda x: time.mktime(x.timetuple())
+ self.start_value = self.decode_func(start_value)
+ self.change = self.decode_func(target_value) - self.start_value
+
+ elif isinstance(start_value, basestring) \
+ and (self.hex_color_normal.match(start_value) or self.hex_color_short.match(start_value)):
+ self.update = color_update
+ if self.hex_color_normal.match(start_value):
+ self.decode_func = lambda val: [int(match, 16)
+ for match in self.hex_color_normal.match(val).groups()]
+
+ elif self.hex_color_short.match(start_value):
+ self.decode_func = lambda val: [int(match + match, 16)
+ for match in self.hex_color_short.match(val).groups()]
+
+ if self.hex_color_normal.match(target_value):
+ target_value = [int(match, 16)
+ for match in self.hex_color_normal.match(target_value).groups()]
+ else:
+ target_value = [int(match + match, 16)
+ for match in self.hex_color_short.match(target_value).groups()]
+
+ self.start_value = self.decode_func(start_value)
+ self.change = [target - start for start, target in zip(self.start_value, target_value)]
+
+
+
+"""Robert Penner's classes stripped from the repetetive c,b,d mish-mash
+(discovery of Patryk Zawadzki). This way we do the math once and apply to
+all the tweenables instead of repeating it for each attribute
+"""
+
+def inverse(method):
+ def real_inverse(t, *args, **kwargs):
+ t = 1 - t
+ return 1 - method(t, *args, **kwargs)
+ return real_inverse
+
+def symmetric(ease_in, ease_out):
+ def real_symmetric(t, *args, **kwargs):
+ if t < 0.5:
+ return ease_in(t * 2, *args, **kwargs) / 2
+
+ return ease_out((t - 0.5) * 2, *args, **kwargs) / 2 + 0.5
+ return real_symmetric
+
+class Symmetric(object):
+ def __init__(self, ease_in = None, ease_out = None):
+ self.ease_in = ease_in or inverse(ease_out)
+ self.ease_out = ease_out or inverse(ease_in)
+ self.ease_in_out = symmetric(self.ease_in, self.ease_out)
+
+
+class Easing(object):
+ """Class containing easing classes to use together with the tweener.
+ All of the classes have :func:`ease_in`, :func:`ease_out` and
+ :func:`ease_in_out` functions."""
+
+ Linear = Symmetric(lambda t: t, lambda t: t)
+ Quad = Symmetric(lambda t: t*t)
+ Cubic = Symmetric(lambda t: t*t*t)
+ Quart = Symmetric(lambda t: t*t*t*t)
+ Quint = Symmetric(lambda t: t*t*t*t*t)
+ Strong = Quint #oh i wonder why but the ported code is the same as in Quint
+
+ Circ = Symmetric(lambda t: 1 - math.sqrt(1 - t * t))
+ Sine = Symmetric(lambda t: 1 - math.cos(t * (math.pi / 2)))
+
+
+ def _back_in(t, s=1.70158):
+ return t * t * ((s + 1) * t - s)
+ Back = Symmetric(_back_in)
+
+
+ def _bounce_out(t):
+ if t < 1 / 2.75:
+ return 7.5625 * t * t
+ elif t < 2 / 2.75:
+ t = t - 1.5 / 2.75
+ return 7.5625 * t * t + 0.75
+ elif t < 2.5 / 2.75:
+ t = t - 2.25 / 2.75
+ return 7.5625 * t * t + .9375
+ else:
+ t = t - 2.625 / 2.75
+ return 7.5625 * t * t + 0.984375
+ Bounce = Symmetric(ease_out = _bounce_out)
+
+
+ def _elastic_in(t, springiness = 0, wave_length = 0):
+ if t in(0, 1):
+ return t
+
+ wave_length = wave_length or (1 - t) * 0.3
+
+ if springiness <= 1:
+ springiness = t
+ s = wave_length / 4
+ else:
+ s = wave_length / (2 * math.pi) * math.asin(t / springiness)
+
+ t = t - 1
+ return -(springiness * math.pow(2, 10 * t) * math.sin((t * t - s) * (2 * math.pi) / wave_length))
+ Elastic = Symmetric(_elastic_in)
+
+
+ def _expo_in(t):
+ if t in (0, 1): return t
+ return math.pow(2, 10 * t) * 0.001
+ Expo = Symmetric(_expo_in)
+
+
+
+class _Dummy(object):
+ def __init__(self, a, b, c):
+ self.a = a
+ self.b = b
+ self.c = c
+
+if __name__ == "__main__":
+ import datetime as dt
+
+ tweener = Tweener()
+ objects = []
+
+ for i in range(10000):
+ objects.append(_Dummy(dt.datetime.now(), i-100, i-100))
+
+
+ total = dt.datetime.now()
+
+ t = dt.datetime.now()
+ print "Adding 10000 objects..."
+ for i, o in enumerate(objects):
+ tweener.add_tween(o, a = dt.datetime.now() - dt.timedelta(days=3),
+ b = i,
+ c = i,
+ duration = 1.0,
+ easing=Easing.Circ.ease_in_out)
+ print dt.datetime.now() - t
+
+ t = dt.datetime.now()
+ print "Updating 10 times......"
+ for i in range(11): #update 1000 times
+ tweener.update(0.1)
+ print dt.datetime.now() - t