Commits

Chris Klimas  committed 4e95874

Remove onComplete handlers in favor of promises

This breaks the existing APIs, beware. In particular timers and tweens work
differently now.

  • Participants
  • Parent commits 1ac7109

Comments (0)

Files changed (6)

 		other.velocity.x = other.velocity.x + optMomentum * self.mass * centX
 		other.velocity.y = other.velocity.y + optMomentum * self.mass * centY
 
-		if math.abs(1 - self.alpha) > NEARLY_ZERO and not the.view.tween:status(self, 'alpha', 1) then
-			the.view.tween:start{ target = self, prop = 'alpha', to = 1, duration = 0.25, force = true }
-		end
-
-		if math.abs(1 - other.alpha) > NEARLY_ZERO and not the.view.tween:status(other, 'alpha', 1) then
-			the.view.tween:start{ target = other, prop = 'alpha', to = 1, duration = 0.25, force = true }
-		end
+		the.view.tween:start(self, 'alpha', 1, 0.25) 
+		the.view.tween:start(other, 'alpha', 1, 0.25)
 	end,
 
 	onUpdate = function (self)
 		if math.abs(self.velocity.x + self.velocity.y) < 5 and
 		   self.alpha ~= 0 and not the.view.tween:status(self, 'alpha', 0) then
-			the.view.tween:start{ target = self, prop = 'alpha', to = 0, force = true }
+			the.view.tween:start(self, 'alpha', 0)
 		end
 
 		if (self.x < 480 and self.velocity.x < 0) or
 			end
 		end
 
-		self.view.timer:start{ func = self.pushBlock, bind = self, delay = 1, repeats = true }
+		self.view.timer:every(1, function() self:pushBlock() end)
 
 		-- menu buttons
 
 		block.acceleration.y = math.random(-400, 400)
 	
 		if math.abs(1 - block.alpha) > NEARLY_ZERO and not self.view.tween:status(block, 'alpha', 1) then
-			self.view.tween:start{ target = block, prop = 'alpha', to = 1, duration = 0.25, force = true }
+			self.view.tween:start(block, 'alpha', 1, 0.25)
 		end
 	end
 }

File tests/timers.lua

 		self.blue = Fill:new{ x = 500, y = 250, width = 100, height = 100, fill = { 0, 0, 255 }, visible = false }
 		self:add(self.blue)
 		
-		self.view.timer:start{ delay = 0.5, func = self.toggle, arg = { self.red } }
-		self.view.timer:start{ delay = 1, func = self.toggle, arg = { self.green }, repeats = true }
-		self.view.timer:start{ delay = 1.5, func = self.toggle, arg = { self.blue } }
-		self.view.timer:start{ delay = 0, func = self.view.flash, bind = self.view, arg = { {255, 255, 255} } }
+		self.view.timer:after(1, function() end)
+			:andThen(function()
+				self:toggle(self.red)
+			end)
+			:andThen(function()
+				return self.view.timer:after(1, function() self:toggle(self.blue) end)
+			end)
+			:andThen(function()
+				return self.view.timer:after(1, function() self:toggle(self.red) end)
+			end)
+			:andThen(function()
+				return self.view.timer:after(1, function() self:toggle(self.red) end)	
+			end)
+
+		self.view.timer:after(0, function() self.view:flash{255, 255, 255} end)
+		self.view.timer:every(1, function() self:toggle(self.green) end)
 
 		self:add(Text:new
 		{
 			x = 10, y = 550, width = 600, font = 14,
-			text = 'Function calls can be delayed or repeated in game time. If you switch to another ' ..
-				   'window, the green square won\'t blink until you bring the window to the top again.'
+			text = 'Function calls can be delayed or repeated in game time, and linked in sequence. ' ..
+				   'If you switch to another window, the green square won\'t blink until you bring the window to the top again.'
 		})
 	end,
 	
-	toggle = function (sprite)
+	toggle = function (self, sprite)
 		sprite.visible = not sprite.visible
 	end
 }

File tests/tweens.lua

 	onRun = function (self)
 		local y = 250
 		
-		for name, _ in pairs(Tween.easers) do
+		for easer, _ in pairs(Tween.easers) do
 			local block = Fill:new{ x = 200, y = y, width = 25, height = 25 }
 			self:add(block)
-			self.view.tween:start{ target = block, prop = 'x', to = 275, ease = name,
-									onComplete = Tween.reverse }
-									 
+			self.view.tween:start(block, 'x', 275, 1, easer):andThen(Tween.reverseForever)
 			y = y + 25
 		end
 		
 		local colorBlock = Fill:new{ x = 500, y = 250, width = 100, height = 100,
 									  fill = { 255, 0, 0 } }
 		self:add(colorBlock)
-		self.view.tween:start{ target = colorBlock, prop = 'fill',
-								to = { 0, 0, 255 }, onComplete = Tween.reverse }
+		self.view.tween:start(colorBlock, 'fill', { 0, 0, 255 }):andThen(Tween.reverseForever)
 
 		self:add(Text:new
 		{

File zoetrope/core/timer.lua

 	active = false,
 	solid = false,
 
-	-- Method: start
-	-- Adds a timer to be tracked. All arguments are passed as properties
-	-- of a single object as follows:
+	-- Method: wait
+
+	-- Method: after
+	-- Delays a function call after a certain a mount of time.
 	--
 	-- Arguments:
+	--		* delay - how long to wait, in seconds
 	--		* func - function to call
-	--		* delay - how long to wait to call it, in seconds
-	--		* repeats - if true, then the function is called periodically, not once
-	--		* bind - first argument to pass to the function, imitates bind:func()
-	--		* arg - a table of arguments to pass to the function when called
 	--
 	-- Returns:
-	--		nothing
+	--		A <Promise> that is fulfilled after the function is called
+	
+	after = function (self, delay, func)
+		if STRICT then
+			assert(type(func) == 'function', 'func property of timer must be a function')
+			assert(type(delay) == 'number', 'delay must be a number')
 
-	start = function (self, timer)
-		if STRICT then
-			assert(type(timer.func) == 'function', 'func property of timer must be a function')
-			assert(type(timer.delay) == 'number', 'delay property of timer must be a number')
-			assert(not timer.arg or type(timer.arg) == 'table', 'arg property of timer, if specified, must be a table')
-
-			if timer.delay <= 0 then
+			if delay <= 0 then
 				local info = debug.getinfo(2, 'Sl')
-				print('Warning: timer delay is ' .. timer.delay .. ', will be triggered immediately (' .. 
+				print('Warning: timer delay is ' .. delay .. ', will be triggered immediately (' .. 
 					  info.short_src .. ', line ' .. info.currentline .. ')')
 			end
 		end
 		
 		self.active = true
-		timer.timeLeft = timer.delay
-		table.insert(self.timers, timer)
+		local promise = Promise:new()
+		table.insert(self.timers, { func = func, timeLeft = delay, promise = promise })
+		return promise
+	end,
+
+	-- Method: every
+	-- Repeatedly makes a function call. To stop these calls from
+	-- happening in the future, you must call stop().
+	--
+	-- Arguments:
+	--		* delay - how often to make the function call, in seconds
+	--		* func - function to call
+	--
+	-- Returns:
+	--		nothing
+	
+	every = function (self, delay, func)
+		if STRICT then
+			assert(type(func) == 'function', 'func property of timer must be a function')
+			assert(type(delay) == 'number', 'delay must be a number')
+
+			if delay <= 0 then
+				local info = debug.getinfo(2, 'Sl')
+				print('Warning: timer delay is ' .. delay .. ', will be triggered immediately (' .. 
+					  info.short_src .. ', line ' .. info.currentline .. ')')
+			end
+		end
+		
+		self.active = true
+		table.insert(self.timers, { func = func, timeLeft = delay, interval = delay })
 	end,
 
 	-- Method: status
 	--
 	-- Arguments:
 	--		func - the function that is queued
-	--		bind - the bind property used with <start()>, optional
 	--
 	-- Returns:
 	--		the time left until the soonest call matching these arguments,
 		local result
 
 		for _, t in pairs(self.timers) do
-			if t.func == func and (not bind or t.bind == bind) and
-			   (not result or result < t.timeLeft) then
+			if t.func == func and (not result or result < t.timeLeft) then
 			   result = t.timeLeft
 			end
 		end
 	end,
 	
 	-- Method: stop
-	-- Stops a timer from executing. If there is no function associated
-	-- with this timer, then this has no effect.
+	-- Stops a timer from executing. The promise belonging to it is failed. 
+	-- If there is no function associated with this timer, then this has no effect.
 	--
 	-- Arguments:
 	--		func - function to stop; if omitted, stops all timers
-	--		bind - bind to stop; if omitted, stops all function calls of func argument
 	--
 	-- Returns:
 	--		nothing
 		local found = false
 
 		for i, timer in ipairs(self.timers) do
-			if not func or (timer.func == func and (not bind or timer.bind == bind)) then
+			if not func or timer.func == func then
+				if timer.promise then
+					timer.promise:fail('Timer stopped')
+				end
+
 				table.remove(self.timers, i)
 				found = true
 			end
 			timer.timeLeft = timer.timeLeft - elapsed
 			
 			if timer.timeLeft <= 0 then
-				if timer.arg then
-					if timer.bind then
-						timer.func(timer.bind, unpack(timer.arg))
-					else
-						timer.func(unpack(timer.arg))
-					end
-				else
-					if timer.bind then
-						timer.func(timer.bind)
-					else
-						timer.func()
-					end
+				timer.func()
+				
+				if timer.promise then
+					timer.promise:fulfill()
 				end
-				
-				if timer.repeats then
-					timer.timeLeft = timer.delay
+
+				if timer.interval then
+					timer.timeLeft = timer.interval
 					keepActive = true
 				else
 					table.remove(self.timers, i)

File zoetrope/core/tween.lua

 		end
 	},
 	
-	-- Method: reverse
-	-- A utility function; if set as an onComplete handler for an individual
+	-- 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).
 	
-	reverse = function (tween, tweener)
+	reverseForever = function (tween, tweener)
 		tween.to = tween.from
-		tweener:start(tween)
+		tweener:start(tween.target, tween.property, tween.to, tween.duration, tween.ease):andThen(Tween.reverseForever)
 	end,
 
 	-- Method: reverseOnce
-	-- A utility function; if set as as an onComplete handler for an individual
+	-- 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
-		tween.onComplete = nil
-		tweener:start(tween)	
+		tweener:start(tween.target, tween.property, tween.to, tween.duration, tween.ease)
 	end,
 
 	-- Method: start
-	-- Begins a tweened transition. *All* arguments are passed via
-	-- properties of a single object as follows:
+	-- Begins a tweened transition, overriding any existing tween.
 	--
 	-- Arguments:
 	--		target - target object
-	--		prop - name of property of the target object to tween;
-	--             can be either a number or a table of numbers (e.g. a color)
+	--		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
-	--		getter - getter function for the property; overrides prop
-	--		setter - setter function for the property; overrides prop
 	--		duration - how long the tween should last in seconds, default 1
-	--		force - override any pre-existing tweens on this object/property?
-	--		ease - function name (in Tweener.easers) to use to control how the value changes
-	--		onComplete - function to call when the tween finishes; is passed the individual tween object 
+	--		ease - function name (in Tween.easers) to use to control how the value changes, default 'linear'
 	--
 	-- Returns:
-	--		nothing
+	--		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, tween)
-		tween.duration = tween.duration or 1
-		tween.ease = tween.ease or 'linear'
+	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(tween.target) == 'table' or type(tween.target) == 'userdata',
-				   'tween target must be a table or userdata')
-			assert(tween.prop or tween.getter, 'neither tween prop (property) nor getter are defined')
-			assert(not tween.prop or tween.target[tween.prop],
-				   'no such property ' .. tostring(tween.prop) .. ' on target') 
-			assert(type(tween.duration) == 'number', 'tween duration must be a number')
-			assert(self.easers[tween.ease], 'easer ' .. tween.ease .. ' is not defined')
+			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, otherTweener in ipairs(self.tweens) do
-			if tween.target == otherTweener.target and
-			   tween.prop == otherTweener.prop then
-				if tween.force then
+		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)
-				else
-					if STRICT then
-						local info = debug.getinfo(2, 'Sl')
-						print('Warning: asked to tween a value that\'s already being tweened, ' ..
-							  'ignoring request; use force = true to override this (' ..
-							  info.short_src .. ', line ' .. info.currentline .. ')')
-					end
-
-					return
 				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)
 		
 		if tween.type == 'number' then
 			tween.change = tween.to - tween.from
 			if math.abs(tween.change) < NEARLY_ZERO then
-				if STRICT then
-					local info = debug.getinfo(2, 'Sl')
-					print('Warning: asked to tween a value to its current state, ignoring request (' ..
-						  info.short_src .. ', line ' .. info.currentline .. ')')
-				end
-
-				return
+				return Promise:new{ state = 'fulfilled', _resolvedWith = { tween, self } }
 			end
 		elseif tween.type == 'table' then
 			tween.change = {}
 			end
 			
 			if skip then
-				if STRICT then
-					local info = debug.getinfo(2, 'Sl')
-					print('Warning: asked to tween a value to its current state, ignoring request (' ..
-						  info.short_src .. ', line ' .. info.currentline .. ')')
-				end
-
-				return
+				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
 	--
 	-- Arguments:
 	--		target - target object
-	--		prop - name of the property being tweened, or getter (as set in the orignal <start()> call)
-	--		to - value being tweened to, optional
+	--		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, prop, to)
+	status = function (self, target, property)
 		for _, t in pairs(self.tweens) do
-			if t.target == target and (t.prop == prop or t.getter == prop) and
-			   (not to or t.to == to) then
-				return t.duration - t.elapsed
+			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
 
 	end,
 
 	-- Method: stop
-	-- Stops a tween.
+	-- Stops a tween. The promise associated with it will be failed.
 	--
 	-- Arguments:
 	--		target - tween target
-	-- 		prop - name of property being tweened; if omitted, stops all tweens on the 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, prop)
+	stop = function (self, target, property)
 		local found = false
 
 		for i, tween in ipairs(self.tweens) do
-			if tween.target == target and (not prop or tween.prop == prop) then
+			if tween.target == target and (t.property == property or
+			   (type(t.property) == 'table' and t.property[1] == property) or
+			   not property) then
 			   	found = true
+				tween.promise:fail('Tween stopped')
 				table.remove(self.tweens, i)
-			end
+				end
 		end
 
 		if STRICT and not found then
 				
 				self:setTweenValue(tween, tween.to)
 				table.remove(self.tweens, i)
-				
-				-- this must happen after the tween is removed
-				-- so that it doesn't appear that it is still running
-				-- to the callback
-				
-				if tween.onComplete then tween.onComplete(tween, self) end
+				tween.promise:fulfill(tween, self)
 			else
 				-- move tween towards finished state
 				
 	end,
 
 	getTweenValue = function (self, tween)
-		if tween.getter then
-			return tween.getter(tween.target)
+		if tween.propType == 'string' or tween.propType == 'number' then
+			return tween.target[tween.property]
 		else
-			return tween.target[tween.prop]
+			return tween.property[1](tween.target)
 		end
 	end,
 
 	setTweenValue = function (self, tween, value)
-		if tween.setter then
-			tween.setter(tween.target, value)
+		if tween.propType == 'string' or tween.propType == 'number' then
+			tween.target[tween.property] = value
 		else
-			tween.target[tween.prop] = value
+			tween.property[2](tween.target, value)
 		end
 	end,
 

File zoetrope/core/view.lua

 	-- Arguments:
 	--		target - sprite or coordinate pair to pan to
 	--		duration - how long the pan will take, in seconds
-	--		onComplete - function to call when the pan is complete, optional
 	--		ease - what easing to apply, see <Tween> for details, defaults to 'quadInOut'
 	--
 	-- Returns:
-	--		nothing
+	--		A <Promise> that is fulfilled when the pan completes.
 
-	panTo = function (self, target, duration, onComplete, ease)
+	panTo = function (self, target, duration, ease)
 		ease = ease or 'quadInOut'
 		local targetX, targetY
 
 			assert((target.x and target.y and target.width and target.height) or (#target == 2),
 				   'pan target does not appear to be a sprite or coordinate pair')
 			assert(type(duration) == 'number', 'pan duration is not a number')
-			assert(not onComplete or type(onComplete) == 'function', 'pan onComplete is not a function')
 			assert(self.tween.easers[ease], 'pan easing method is not defined')
 		end
 
 		end
 
 		-- tween the appropriate properties
-		-- some care has to be taken to avoid calling onComplete twice
+		-- some care has to be taken to avoid fulfilling the promise twice
 
 		self.focus = nil
+		local promise = Promise:new()
 
 		if tranX ~= self.translate.x then
-			self.tween:start{ target = self.translate, prop = 'x', to = tranX, ease = ease,
-							  duration = duration, force = true, onComplete = onComplete }
+			self.tween:start(self.translate, 'x', tranX, duration, ease)
+				:andThen(function() promise:fulfill() end)
 
 			if tranY ~= self.translate.y then
-				self.tween:start{ target = self.translate, prop = 'y', to = tranY, ease = ease,
-								  duration = duration, force = true }
+				self.tween:start(self.translate, 'y', tranY, duration, ease)
 			end
 		elseif tranY ~= self.translate.y then
-			self.tween:start{ target = self.translate, prop = 'y', to = tranY, ease = ease,
-							  duration = duration, force = true, onComplete = onComplete }
-		elseif onComplete then
-			onComplete()
+			self.tween:start(self.translate, 'y', tranY, duration, ease)
+				:andThen(function() promise:fulfill() end)
+		else
+			promise:fulfill()
 		end
+
+		return promise
 	end,
 
 	-- Method: fade
 	-- Arguments:
 	--		color - color table to fade to, e.g. { 0, 0, 0 }
 	--		duration - how long to fade out in seconds, default 1
-	--		onComplete - function to call when done, passed the tween related to this
+	--
 	-- Returns:
-	--		nothing
+	--		A <Promise> that is fulfilled when the effect completes.
 
-	fade = function (self, color, duration, onComplete)
+	fade = function (self, color, duration)
 		assert(type(color) == 'table', 'color to fade to is ' .. type(color) .. ', not a table')
 		local alpha = color[4] or 255
 		self._fx = color
 		self._fx[4] = 0
-		self.tween:start{ target = self._fx, prop = 4, to = alpha, duration = duration or 1,
-						   ease = 'quadOut', force = true, onComplete = onComplete }
+		return self.tween:start(self._fx, 4, alpha, duration or 1, 'quadOut')
 	end,
 
 	-- Method: flash
 	-- Arguments:
 	--		color - color table to flash, e.g. { 0, 0, 0 }
 	--		duration - how long to restore normal view in seconds, default 1
-	--		onComplete - function to call when done, passed the tween related to this
 	--
 	-- Returns:
-	--		nothing
+	--		A <Promise> that is fulfilled when the effect completes.
 
-	flash = function (self, color, duration, onComplete)
-		local s = self
-		local done = function (t)
-			t.target = nil
-			if onComplete then onComplete(t) end
-		end
-
+	flash = function (self, color, duration)
 		assert(type(color) == 'table', 'color to flash is ' .. type(color) .. ', not a table')
 		color[4] = color[4] or 255
 		self._fx = color
-		self.tween:start{ target = self._fx, prop = 4, to = 0, duration = duration or 1,
-						   ease = 'quadOut', force = true, onComplete = done }
+		return self.tween:start(self._fx, 4, 0, duration or 1, 'quadOut')
 	end,
 
 	-- Method: tint