/ld26

To get this branch, use:
bzr branch http://9ix.org/bzr/ld26
1 by Josh C
zoetrope 1.4
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
}