/spacey

To get this branch, use:
bzr branch /bzr/spacey
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
-- Class: Tween
-- A tween transitions a property from one state to another
-- in in-game time. A tween instance is designed to manage
-- many of these transitions at once, in fact. In order for it
-- to work properly, it must receive update events, so it must
-- be added somewhere in the current view or app. If you are using
-- the <View> class, this is already done for you.

Tween = Sprite:extend{
	tweens = {},
	visible = false,
	active = false,
	solid = false,

	-- Property: easers
	-- These are different methods of easing a tween, and
	-- can be set via the ease property of an individual tween.
	-- They should be referred to by their key name, not the property
	-- (e.g. 'linear', no Tweener.easers.linear).
	-- See http://www.gizma.com/easing/ for details.
	
	easers =
	{
		linear = function (elapsed, start, change, duration)
			return change * elapsed / duration + start
		end,
		
		quadIn = function (elapsed, start, change, duration)
			elapsed = elapsed / duration
			return change * elapsed * elapsed + start
		end,
		
		quadOut = function (elapsed, start, change, duration)
			elapsed = elapsed / duration
			return - change * elapsed * (elapsed - 2) + start
		end,
		
		quadInOut = function (elapsed, start, change, duration)
			elapsed = elapsed / (duration / 2)
			
			if (elapsed < 1) then
				return change / 2 * elapsed * elapsed + start
			else
				elapsed = elapsed - 1
				return - change / 2 * (elapsed * (elapsed - 2) - 1) + start
			end
		end
	},
	
	-- Method: reverseForever
	-- A utility function; if set via <Promise.andAfter()> for an individual
	-- tween, it reverses the tween that just happened. Use this to get a tween
	-- to repeat back and forth indefinitely (e.g. to have something glow).
	
	reverseForever = function (tween, tweener)
		tween.to = tween.from
		tweener:start(tween.target, tween.property, tween.to, tween.duration, tween.ease):andThen(Tween.reverseForever)
	end,

	-- Method: reverseOnce
	-- A utility function; if set via <Promise.andAfter()> for an individual
	-- tween, it reverses the tween that just happened-- then stops the tween after that.
	
	reverseOnce = function (tween, tweener)
		tween.to = tween.from
		tweener:start(tween.target, tween.property, tween.to, tween.duration, tween.ease)
	end,

	-- Method: start
	-- Begins a tweened transition, overriding any existing tween.
	--
	-- Arguments:
	--		target - target object
	--		property - Usually, this is a string name of a property of the target object.
	--				   You may also specify a table of getter and setter methods instead,
	--				   i.e. { myGetter, mySetter }. In either case, the property or functions
	--				   must work with either number values, or tables of numbers.
	--		to - destination value, either number or color table
	--		duration - how long the tween should last in seconds, default 1
	--		ease - function name (in Tween.easers) to use to control how the value changes, default 'linear'
	--
	-- Returns:
	--		A <Promise> that is fulfilled when the tween completes. If the object is already
	--		in the state requested, the promise resolves immediately. The tween object returns two
	--		things to the promise: a table of properties about the tween that match the arguments initially
	--		passed, and a reference to the Tween that completing the tween.

	start = function (self, target, property, to, duration, ease)
		duration = duration or 1
		ease = ease or 'linear'
		local propType = type(property)
		
		if STRICT then
			assert(type(target) == 'table' or type(target) == 'userdata', 'target must be a table or userdata')
			assert(propType == 'string' or propType == 'number' or propType == 'table', 'property must be a key or table of getter/setter methods')
			
			if propType == 'string' or propType == 'number' then
				assert(target[property], 'no such property ' .. tostring(property) .. ' on target') 
			end

			assert(type(duration) == 'number', 'duration must be a number')
			assert(self.easers[ease], 'easer ' .. ease .. ' is not defined')
		end

		-- check for an existing tween for this target and property
		
		for i, existing in ipairs(self.tweens) do
			if target == existing.target and property == existing.property then
				if to == existing.to then
					return existing.promise
				else
					table.remove(self.tweens, i)
				end
			end
		end
		
		-- add it

		tween = { target = target, property = property, propType = propType, to = to, duration = duration, ease = ease }
		tween.from = self:getTweenValue(tween)
		tween.type = type(tween.from)
		
		-- calculate change; if it's trivial, skip the tween
		
		if tween.type == 'number' then
			tween.change = tween.to - tween.from
			if math.abs(tween.change) < NEARLY_ZERO then
				return Promise:new{ state = 'fulfilled', _resolvedWith = { tween, self } }
			end
		elseif tween.type == 'table' then
			tween.change = {}
			
			local skip = true
			
			for i, value in ipairs(tween.from) do
				tween.change[i] = tween.to[i] - tween.from[i]
				
				if math.abs(tween.change[i]) > NEARLY_ZERO then
					skip = false
				end
			end
			
			if skip then
				return Promise:new{ state = 'fulfilled', _resolvedWith = { tween, self } }
			end
		else
			error('tweened property must either be a number or a table of numbers, is ' .. tween.type)
		end
			
		tween.elapsed = 0
		tween.promise = Promise:new()
		table.insert(self.tweens, tween)
		self.active = true
		return tween.promise
	end,

	-- Method: status
	-- Returns how much time is left for a particular tween to run.
	--
	-- Arguments:
	--		target - target object
	--		property - name of the property being tweened, or getter
	--				   (as set in the orignal <start()> call)
	--
	-- Returns:
	--		Either the time left in the tween, or nil if there is
	--		no tween matching the arguments passed.

	status = function (self, target, property)
		for _, t in pairs(self.tweens) do
			if t.target == target then
				if t.property == property or (type(t.property) == 'table' and t.property[1] == property) then
					return t.duration - t.elapsed
				end
			end
		end

		return nil
	end,

	-- Method: stop
	-- Stops a tween. The promise associated with it will be failed.
	--
	-- Arguments:
	--		target - tween target
	-- 		property - name of property being tweened, or getter (as set in the original <start()> call); 
	--				   if omitted, stops all tweens on the target
	--
	-- Returns:
	--		nothing

	stop = function (self, target, property)
		local found = false

		for i, tween in ipairs(self.tweens) do
			if tween.target == target and (tween.property == property or
			   (type(tween.property) == 'table' and tween.property[1] == property) or
			   not property) then
			   	found = true
				tween.promise:fail('Tween stopped')
				table.remove(self.tweens, i)
			end
		end

		if STRICT and not found then
			local info = debug.getinfo(2, 'Sl')
			print('Warning: asked to stop a tween, but no active tweens match it (' ..
				  info.short_src .. ', line ' .. info.currentline .. ')')
		end
	end,

	update = function (self, elapsed)	
		for i, tween in ipairs(self.tweens) do
			self.active = true
			tween.elapsed = tween.elapsed + elapsed
			
			if tween.elapsed >= tween.duration then
				-- tween is completed
				
				self:setTweenValue(tween, tween.to)
				table.remove(self.tweens, i)
				tween.promise:fulfill(tween, self)
			else
				-- move tween towards finished state
				
				if tween.type == 'number' then
					self:setTweenValue(tween, self.easers[tween.ease](tween.elapsed,
									   tween.from, tween.change, tween.duration))
				elseif tween.type == 'table' then
					local now = {}
					
					for i, value in ipairs(tween.from) do
						now[i] = self.easers[tween.ease](tween.elapsed, tween.from[i],
														 tween.change[i], tween.duration)
					end
					
					self:setTweenValue(tween, now)
				end
			end
		end
		
		self.active = (#self.tweens > 0)
	end,

	getTweenValue = function (self, tween)
		if tween.propType == 'string' or tween.propType == 'number' then
			return tween.target[tween.property]
		else
			return tween.property[1](tween.target)
		end
	end,

	setTweenValue = function (self, tween, value)
		if tween.propType == 'string' or tween.propType == 'number' then
			tween.target[tween.property] = value
		else
			tween.property[2](tween.target, value)
		end
	end,

	__tostring = function (self)
		local result = 'Tween ('

		if self.active then
			result = result .. 'active, '
			result = result .. #self.tweens .. ' tweens running'
		else
			result = result .. 'inactive'
		end

		return result .. ')'
	end
}