/ld26-basecode

To get this branch, use:
bzr branch /bzr/ld26-basecode

« back to all changes in this revision

Viewing changes to zoetrope/core/tween.lua

  • Committer: Josh C
  • Date: 2013-04-23 15:22:15 UTC
  • Revision ID: josh@9ix.org-20130423152215-hacpxl91eiiq2ras
zoetrope 1.4

Show diffs side-by-side

added added

removed removed

 
1
-- Class: Tween
 
2
-- A tween transitions a property from one state to another
 
3
-- in in-game time. A tween instance is designed to manage
 
4
-- many of these transitions at once, in fact. In order for it
 
5
-- to work properly, it must receive update events, so it must
 
6
-- be added somewhere in the current view or app. If you are using
 
7
-- the <View> class, this is already done for you.
 
8
 
 
9
Tween = Sprite:extend{
 
10
        tweens = {},
 
11
        visible = false,
 
12
        active = false,
 
13
        solid = false,
 
14
 
 
15
        -- Property: easers
 
16
        -- These are different methods of easing a tween, and
 
17
        -- can be set via the ease property of an individual tween.
 
18
        -- They should be referred to by their key name, not the property
 
19
        -- (e.g. 'linear', no Tweener.easers.linear).
 
20
        -- See http://www.gizma.com/easing/ for details.
 
21
        
 
22
        easers =
 
23
        {
 
24
                linear = function (elapsed, start, change, duration)
 
25
                        return change * elapsed / duration + start
 
26
                end,
 
27
                
 
28
                quadIn = function (elapsed, start, change, duration)
 
29
                        elapsed = elapsed / duration
 
30
                        return change * elapsed * elapsed + start
 
31
                end,
 
32
                
 
33
                quadOut = function (elapsed, start, change, duration)
 
34
                        elapsed = elapsed / duration
 
35
                        return - change * elapsed * (elapsed - 2) + start
 
36
                end,
 
37
                
 
38
                quadInOut = function (elapsed, start, change, duration)
 
39
                        elapsed = elapsed / (duration / 2)
 
40
                        
 
41
                        if (elapsed < 1) then
 
42
                                return change / 2 * elapsed * elapsed + start
 
43
                        else
 
44
                                elapsed = elapsed - 1
 
45
                                return - change / 2 * (elapsed * (elapsed - 2) - 1) + start
 
46
                        end
 
47
                end
 
48
        },
 
49
        
 
50
        -- Method: reverseForever
 
51
        -- A utility function; if set via <Promise.andAfter()> for an individual
 
52
        -- tween, it reverses the tween that just happened. Use this to get a tween
 
53
        -- to repeat back and forth indefinitely (e.g. to have something glow).
 
54
        
 
55
        reverseForever = function (tween, tweener)
 
56
                tween.to = tween.from
 
57
                tweener:start(tween.target, tween.property, tween.to, tween.duration, tween.ease):andThen(Tween.reverseForever)
 
58
        end,
 
59
 
 
60
        -- Method: reverseOnce
 
61
        -- A utility function; if set via <Promise.andAfter()> for an individual
 
62
        -- tween, it reverses the tween that just happened-- then stops the tween after that.
 
63
        
 
64
        reverseOnce = function (tween, tweener)
 
65
                tween.to = tween.from
 
66
                tweener:start(tween.target, tween.property, tween.to, tween.duration, tween.ease)
 
67
        end,
 
68
 
 
69
        -- Method: start
 
70
        -- Begins a tweened transition, overriding any existing tween.
 
71
        --
 
72
        -- Arguments:
 
73
        --              target - target object
 
74
        --              property - Usually, this is a string name of a property of the target object.
 
75
        --                                 You may also specify a table of getter and setter methods instead,
 
76
        --                                 i.e. { myGetter, mySetter }. In either case, the property or functions
 
77
        --                                 must work with either number values, or tables of numbers.
 
78
        --              to - destination value, either number or color table
 
79
        --              duration - how long the tween should last in seconds, default 1
 
80
        --              ease - function name (in Tween.easers) to use to control how the value changes, default 'linear'
 
81
        --
 
82
        -- Returns:
 
83
        --              A <Promise> that is fulfilled when the tween completes. If the object is already
 
84
        --              in the state requested, the promise resolves immediately. The tween object returns two
 
85
        --              things to the promise: a table of properties about the tween that match the arguments initially
 
86
        --              passed, and a reference to the Tween that completing the tween.
 
87
 
 
88
        start = function (self, target, property, to, duration, ease)
 
89
                duration = duration or 1
 
90
                ease = ease or 'linear'
 
91
                local propType = type(property)
 
92
                
 
93
                if STRICT then
 
94
                        assert(type(target) == 'table' or type(target) == 'userdata', 'target must be a table or userdata')
 
95
                        assert(propType == 'string' or propType == 'number' or propType == 'table', 'property must be a key or table of getter/setter methods')
 
96
                        
 
97
                        if propType == 'string' or propType == 'number' then
 
98
                                assert(target[property], 'no such property ' .. tostring(property) .. ' on target') 
 
99
                        end
 
100
 
 
101
                        assert(type(duration) == 'number', 'duration must be a number')
 
102
                        assert(self.easers[ease], 'easer ' .. ease .. ' is not defined')
 
103
                end
 
104
 
 
105
                -- check for an existing tween for this target and property
 
106
                
 
107
                for i, existing in ipairs(self.tweens) do
 
108
                        if target == existing.target and property == existing.property then
 
109
                                if to == existing.to then
 
110
                                        return existing.promise
 
111
                                else
 
112
                                        table.remove(self.tweens, i)
 
113
                                end
 
114
                        end
 
115
                end
 
116
                
 
117
                -- add it
 
118
 
 
119
                tween = { target = target, property = property, propType = propType, to = to, duration = duration, ease = ease }
 
120
                tween.from = self:getTweenValue(tween)
 
121
                tween.type = type(tween.from)
 
122
                
 
123
                -- calculate change; if it's trivial, skip the tween
 
124
                
 
125
                if tween.type == 'number' then
 
126
                        tween.change = tween.to - tween.from
 
127
                        if math.abs(tween.change) < NEARLY_ZERO then
 
128
                                return Promise:new{ state = 'fulfilled', _resolvedWith = { tween, self } }
 
129
                        end
 
130
                elseif tween.type == 'table' then
 
131
                        tween.change = {}
 
132
                        
 
133
                        local skip = true
 
134
                        
 
135
                        for i, value in ipairs(tween.from) do
 
136
                                tween.change[i] = tween.to[i] - tween.from[i]
 
137
                                
 
138
                                if math.abs(tween.change[i]) > NEARLY_ZERO then
 
139
                                        skip = false
 
140
                                end
 
141
                        end
 
142
                        
 
143
                        if skip then
 
144
                                return Promise:new{ state = 'fulfilled', _resolvedWith = { tween, self } }
 
145
                        end
 
146
                else
 
147
                        error('tweened property must either be a number or a table of numbers, is ' .. tween.type)
 
148
                end
 
149
                        
 
150
                tween.elapsed = 0
 
151
                tween.promise = Promise:new()
 
152
                table.insert(self.tweens, tween)
 
153
                self.active = true
 
154
                return tween.promise
 
155
        end,
 
156
 
 
157
        -- Method: status
 
158
        -- Returns how much time is left for a particular tween to run.
 
159
        --
 
160
        -- Arguments:
 
161
        --              target - target object
 
162
        --              property - name of the property being tweened, or getter
 
163
        --                                 (as set in the orignal <start()> call)
 
164
        --
 
165
        -- Returns:
 
166
        --              Either the time left in the tween, or nil if there is
 
167
        --              no tween matching the arguments passed.
 
168
 
 
169
        status = function (self, target, property)
 
170
                for _, t in pairs(self.tweens) do
 
171
                        if t.target == target then
 
172
                                if t.property == property or (type(t.property) == 'table' and t.property[1] == property) then
 
173
                                        return t.duration - t.elapsed
 
174
                                end
 
175
                        end
 
176
                end
 
177
 
 
178
                return nil
 
179
        end,
 
180
 
 
181
        -- Method: stop
 
182
        -- Stops a tween. The promise associated with it will be failed.
 
183
        --
 
184
        -- Arguments:
 
185
        --              target - tween target
 
186
        --              property - name of property being tweened, or getter (as set in the original <start()> call); 
 
187
        --                                 if omitted, stops all tweens on the target
 
188
        --
 
189
        -- Returns:
 
190
        --              nothing
 
191
 
 
192
        stop = function (self, target, property)
 
193
                local found = false
 
194
 
 
195
                for i, tween in ipairs(self.tweens) do
 
196
                        if tween.target == target and (tween.property == property or
 
197
                           (type(tween.property) == 'table' and tween.property[1] == property) or
 
198
                           not property) then
 
199
                                found = true
 
200
                                tween.promise:fail('Tween stopped')
 
201
                                table.remove(self.tweens, i)
 
202
                        end
 
203
                end
 
204
 
 
205
                if STRICT and not found then
 
206
                        local info = debug.getinfo(2, 'Sl')
 
207
                        print('Warning: asked to stop a tween, but no active tweens match it (' ..
 
208
                                  info.short_src .. ', line ' .. info.currentline .. ')')
 
209
                end
 
210
        end,
 
211
 
 
212
        update = function (self, elapsed)       
 
213
                for i, tween in ipairs(self.tweens) do
 
214
                        self.active = true
 
215
                        tween.elapsed = tween.elapsed + elapsed
 
216
                        
 
217
                        if tween.elapsed >= tween.duration then
 
218
                                -- tween is completed
 
219
                                
 
220
                                self:setTweenValue(tween, tween.to)
 
221
                                table.remove(self.tweens, i)
 
222
                                tween.promise:fulfill(tween, self)
 
223
                        else
 
224
                                -- move tween towards finished state
 
225
                                
 
226
                                if tween.type == 'number' then
 
227
                                        self:setTweenValue(tween, self.easers[tween.ease](tween.elapsed,
 
228
                                                                           tween.from, tween.change, tween.duration))
 
229
                                elseif tween.type == 'table' then
 
230
                                        local now = {}
 
231
                                        
 
232
                                        for i, value in ipairs(tween.from) do
 
233
                                                now[i] = self.easers[tween.ease](tween.elapsed, tween.from[i],
 
234
                                                                                                                 tween.change[i], tween.duration)
 
235
                                        end
 
236
                                        
 
237
                                        self:setTweenValue(tween, now)
 
238
                                end
 
239
                        end
 
240
                end
 
241
                
 
242
                self.active = (#self.tweens > 0)
 
243
        end,
 
244
 
 
245
        getTweenValue = function (self, tween)
 
246
                if tween.propType == 'string' or tween.propType == 'number' then
 
247
                        return tween.target[tween.property]
 
248
                else
 
249
                        return tween.property[1](tween.target)
 
250
                end
 
251
        end,
 
252
 
 
253
        setTweenValue = function (self, tween, value)
 
254
                if tween.propType == 'string' or tween.propType == 'number' then
 
255
                        tween.target[tween.property] = value
 
256
                else
 
257
                        tween.property[2](tween.target, value)
 
258
                end
 
259
        end,
 
260
 
 
261
        __tostring = function (self)
 
262
                local result = 'Tween ('
 
263
 
 
264
                if self.active then
 
265
                        result = result .. 'active, '
 
266
                        result = result .. #self.tweens .. ' tweens running'
 
267
                else
 
268
                        result = result .. 'inactive'
 
269
                end
 
270
 
 
271
                return result .. ')'
 
272
        end
 
273
}