diff --git a/time.lua b/time.lua index 2b877da..038db9a 100644 --- a/time.lua +++ b/time.lua @@ -1,6 +1,24 @@ -- ubiquitousse.time local ease = require((...):match("^(.-ubiquitousse)%.")..".lib.easing") +--- Returns true if all the values in the list are true ; functions in the list will be called and the test will be performed on their return value. +-- Returns default if the list is empty. +local function all(list, default) + if #list == 0 then + return default + else + local r = true + for _,v in ipairs(list) do + if type(v) == "function" then + r = r and v() + else + r = r and v + end + end + return r + end +end + --- Time related functions local function newTimerRegistry() --- Used to store all the functions delayed with ubiquitousse.time.delay @@ -51,34 +69,42 @@ local function newTimerRegistry() local d = delayed for func, t in pairs(d) do - local co = t.coroutine - t.after = t.after - dt - if t.after <= 0 then - d[func] = false -- niling here cause the next pair iteration to error - table.insert(done, func) - if not co then - co = coroutine.create(func) - t.coroutine = co - t.started = registry.get() - if t.times > 0 then t.times = t.times - 1 end - t.onStart() - end - assert(coroutine.resume(co, function(delay) - t.after = delay - d[func] = t - coroutine.yield() - end, dt)) - if coroutine.status(co) == "dead" then - if (t.during >= 0 and t.started + t.during < registry.get()) - or (t.times == 0) - or (t.every == -1 and t.times == -1 and t.during == -1) -- no repeat - then - t.onEnd() - else + if all(t.initWhen, true) then + t.initWhen = {} + local co = t.coroutine + t.after = t.after - dt + if t.forceStart or (t.after <= 0 and all(t.startWhen, true)) then + t.startWhen = {} + d[func] = false -- niling here cause the next pair iteration to error + table.insert(done, func) + if not co then + co = coroutine.create(func) + t.coroutine = co + t.started = registry.get() if t.times > 0 then t.times = t.times - 1 end - t.after = t.every - t.coroutine = coroutine.create(func) + for _, f in ipairs(t.onStart) do f() end + end + assert(coroutine.resume(co, function(delay) + t.after = delay d[func] = t + coroutine.yield() + end, dt)) + for _, f in ipairs(t.onUpdate) do f() end + if all(t.stopWhen, false) then t.forceStop = true end + if t.forceStop or coroutine.status(co) == "dead" then + if t.forceStop + or (t.during >= 0 and t.started + t.during < registry.get()) + or (t.times == 0) + or (not all(t.repeatWhile, true)) + or (t.every == -1 and t.times == -1 and t.during == -1 and #t.repeatWhile == 0) -- no repeat + then + for _, f in ipairs(t.onEnd) do f() end + else + if t.times > 0 then t.times = t.times - 1 end + t.after = t.every + t.coroutine = coroutine.create(func) + d[func] = t + end end end end @@ -111,13 +137,23 @@ local function newTimerRegistry() times = -1, during = -1, - onStart = function() end, - onEnd = function() end + initWhen = {}, + startWhen = {}, + repeatWhile = {}, + stopWhen = {}, + + forceStart = false, + forceStop = false, + + onStart = {}, + onUpdate = {}, + onEnd = {} } local t = delayed[func] -- internal data local r -- external interface r = { + --- Timed conditions --- --- Wait time milliseconds before running the function. after = function(_, time) t.after = time @@ -139,15 +175,71 @@ local function newTimerRegistry() return r end, + --- Function conditions --- + --- Starts the function execution when func() returns true. Checked before the "after" condition, + -- meaning the "after" countdown starts when func() returns true. + -- If multiple init functions are added, init will trigger only when all of them returns true. + initWhen = function(_, func) + table.insert(t.initWhen, func) + return r + end, + --- Starts the function execution when func() returns true. Checked after the "after" condition. + -- If multiple start functions are added, start will trigger only when all of them returns true. + startWhen = function(_, func) + table.insert(t.startWhen, func) + return r + end, + --- When the functions ends, the execution won't stop and will repeat as long as func() returns true. + -- Will cancel timed repeat conditions if false but needs other timed repeat conditions to be true to create a new repeat. + -- If multiple repeat functions are added, a repeat will trigger only when all of them returns true. + repeatWhile = function(_, func) + table.insert(t.repeatWhile, func) + return r + end, + --- Stops the function execution when func() returns true. Checked before all timed conditions. + -- If multiple stop functions are added, stop will trigger only when all of them returns true. + stopWhen = function(_, func) + table.insert(t.stopWhen, func) + return r + end, + + --- Conditions override --- + --- Force the function to start its execution. + start = function(_) + t.forceStart = true + return r + end, + --- Force the function to stop its execution. + stop = function(_) + t.forceStop = true + return r + end, + + --- Callbacks functions --- --- Will execute func() when the function execution start. onStart = function(_, func) - t.onStart = func + table.insert(t.onStart, func) + return r + end, + --- Will execute func() each frame the main function is run.. + onUpdate = function(_, func) + table.insert(t.onUpdate, func) return r end, --- Will execute func() when the function execution end. onEnd = function(_, func) - t.onEnd = func + table.insert(t.onEnd, func) return r + end, + + --- Chaining --- + --- Creates another TimedFunction which will be initialized when the current one ends. + -- Returns the new TimedFunction. + chain = function(_, func) + local done = false + r:onEnd(function() done = true end) + return registry.run(func) + :initWhen(function() return done end) end } return r @@ -158,22 +250,68 @@ local function newTimerRegistry() -- @tparam table tbl the table containing the values to tween -- @tparam table to the new values -- @tparam[opt="linear"] string/function method tweening method (string name or the actual function(time, start, change, duration)) - -- @treturn TimedFunction the object + -- @treturn TimedFunction the object. A duration is already defined, and the :chain methods takes the same arguments as tween (and creates a tween). -- @impl ubiquitousse tween = function(duration, tbl, to, method) method = method or "linear" method = type(method) == "string" and ease[method] or method - local time = 0 - local from = {} - for k in pairs(to) do from[k] = tbl[k] end + local time = 0 -- tweening time elapsed + local from = {} -- initial state - return registry.run(function(wait, dt) - time = time + dt - for k in pairs(to) do - tbl[k] = method(time, from[k], to[k] - from[k], duration) + local function update(tbl_, from_, to_) -- apply the method to tbl_ recursively (doesn't handle cycles) + for k, v in pairs(to_) do + if type(v) == "table" then + update(tbl_[k], from_[k], to_[k]) + else + if time < duration then + tbl_[k] = method(time, from_[k], v - from_[k], duration) + else + tbl_[k] = v + end + end end + end + + local r = registry.run(function(wait, dt) + time = time + dt + update(tbl, from, to) end):during(duration) + :onStart(function() + local function copy(stencil, source, dest) -- copy initial state recursively + for k, v in pairs(stencil) do + if type(v) == "table" then + if not dest[k] then dest[k] = {} end + copy(stencil[k], source[k], dest[k]) + else + dest[k] = source[k] + end + end + end + copy(to, tbl, from) + end) + + --- Creates another tween which will be initialized when the current one ends. + -- If tbl_ and/or method_ are not specified, the values from the current tween will be used. + -- Returns the new tween. + r.chain = function(_, duration_, tbl_, to_, method_) + if not method_ and to_ then + if type(to_) == "string" then + tbl_, to_, method_ = tbl, tbl_, to_ + else + method_ = method + end + elseif not method_ and not to_ then + tbl_, to_, method_ = tbl, tbl_, method + end + + local done = false + r:onEnd(function() done = true end) + return registry.tween(duration_, tbl_, to_, method_) + :initWhen(function() return done end) + end + + return r end, --- Cancels all the running TimedFunctions.