/ld27

To get this branch, use:
bzr branch http://9ix.org/bzr/ld27
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
35 by Josh C
cluke009 zoetrope + my spritebatch changes
105
		local tween = { target = target, property = property, propType = propType, to = to, duration = duration, ease = ease }
1 by Josh C
zoetrope 1.4
106
		tween.from = self:getTweenValue(tween)
107
		tween.type = type(tween.from)
108
		
109
		-- calculate change; if it's trivial, skip the tween
110
		
111
		if tween.type == 'number' then
112
			tween.change = tween.to - tween.from
113
			if math.abs(tween.change) < NEARLY_ZERO then
114
				return Promise:new{ state = 'fulfilled', _resolvedWith = { tween, self } }
115
			end
116
		elseif tween.type == 'table' then
117
			tween.change = {}
118
			
119
			local skip = true
120
			
121
			for i, value in ipairs(tween.from) do
122
				tween.change[i] = tween.to[i] - tween.from[i]
123
				
124
				if math.abs(tween.change[i]) > NEARLY_ZERO then
125
					skip = false
126
				end
127
			end
128
			
129
			if skip then
130
				return Promise:new{ state = 'fulfilled', _resolvedWith = { tween, self } }
131
			end
132
		else
133
			error('tweened property must either be a number or a table of numbers, is ' .. tween.type)
134
		end
35 by Josh C
cluke009 zoetrope + my spritebatch changes
135
136
		-- check for an existing tween for this target and property
137
		
138
		for i, existing in ipairs(self.tweens) do
139
			if target == existing.target and property == existing.property then
140
				if to == existing.to then
141
					return existing.promise
142
				else
143
					existing.promise:fail('Overridden by a later tween')
144
					table.remove(self.tweens, i)
145
				end
146
			end
147
		end
148
		
1 by Josh C
zoetrope 1.4
149
		tween.elapsed = 0
150
		tween.promise = Promise:new()
151
		table.insert(self.tweens, tween)
152
		self.active = true
153
		return tween.promise
154
	end,
155
156
	-- Method: status
157
	-- Returns how much time is left for a particular tween to run.
158
	--
159
	-- Arguments:
160
	--		target - target object
161
	--		property - name of the property being tweened, or getter
162
	--				   (as set in the orignal <start()> call)
163
	--
164
	-- Returns:
165
	--		Either the time left in the tween, or nil if there is
166
	--		no tween matching the arguments passed.
167
168
	status = function (self, target, property)
169
		for _, t in pairs(self.tweens) do
170
			if t.target == target then
171
				if t.property == property or (type(t.property) == 'table' and t.property[1] == property) then
172
					return t.duration - t.elapsed
173
				end
174
			end
175
		end
176
177
		return nil
178
	end,
179
180
	-- Method: stop
181
	-- Stops a tween. The promise associated with it will be failed.
182
	--
183
	-- Arguments:
184
	--		target - tween target
185
	-- 		property - name of property being tweened, or getter (as set in the original <start()> call); 
186
	--				   if omitted, stops all tweens on the target
187
	--
188
	-- Returns:
189
	--		nothing
190
191
	stop = function (self, target, property)
192
		local found = false
193
194
		for i, tween in ipairs(self.tweens) do
195
			if tween.target == target and (tween.property == property or
196
			   (type(tween.property) == 'table' and tween.property[1] == property) or
197
			   not property) then
198
			   	found = true
35 by Josh C
cluke009 zoetrope + my spritebatch changes
199
				tween.promise:fail('Stopped')
1 by Josh C
zoetrope 1.4
200
				table.remove(self.tweens, i)
201
			end
202
		end
203
204
		if STRICT and not found then
205
			local info = debug.getinfo(2, 'Sl')
206
			print('Warning: asked to stop a tween, but no active tweens match it (' ..
207
				  info.short_src .. ', line ' .. info.currentline .. ')')
208
		end
209
	end,
210
211
	update = function (self, elapsed)	
212
		for i, tween in ipairs(self.tweens) do
213
			self.active = true
214
			tween.elapsed = tween.elapsed + elapsed
215
			
216
			if tween.elapsed >= tween.duration then
217
				-- tween is completed
218
				
219
				self:setTweenValue(tween, tween.to)
220
				table.remove(self.tweens, i)
221
				tween.promise:fulfill(tween, self)
222
			else
223
				-- move tween towards finished state
224
				
225
				if tween.type == 'number' then
226
					self:setTweenValue(tween, self.easers[tween.ease](tween.elapsed,
227
									   tween.from, tween.change, tween.duration))
228
				elseif tween.type == 'table' then
229
					local now = {}
230
					
231
					for i, value in ipairs(tween.from) do
232
						now[i] = self.easers[tween.ease](tween.elapsed, tween.from[i],
233
														 tween.change[i], tween.duration)
234
					end
235
					
236
					self:setTweenValue(tween, now)
237
				end
238
			end
239
		end
240
		
241
		self.active = (#self.tweens > 0)
242
	end,
243
244
	getTweenValue = function (self, tween)
245
		if tween.propType == 'string' or tween.propType == 'number' then
246
			return tween.target[tween.property]
247
		else
248
			return tween.property[1](tween.target)
249
		end
250
	end,
251
252
	setTweenValue = function (self, tween, value)
253
		if tween.propType == 'string' or tween.propType == 'number' then
254
			tween.target[tween.property] = value
255
		else
256
			tween.property[2](tween.target, value)
257
		end
258
	end,
259
260
	__tostring = function (self)
261
		local result = 'Tween ('
262
263
		if self.active then
264
			result = result .. 'active, '
265
			result = result .. #self.tweens .. ' tweens running'
266
		else
267
			result = result .. 'inactive'
268
		end
269
270
		return result .. ')'
271
	end
272
}