Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/lib/pytweener.py
blob: f5cacd7821cb2dff48dc9e92bd3372ce9cf23860 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
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