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 |
} |