-- https://github.com/bakpakin/corope local setmetatable = setmetatable local create = coroutine.create local status = coroutine.status local resume = coroutine.resume local yield = coroutine.yield local running = coroutine.running local type = type local select = select local assert = assert local unpack = unpack or table.unpack -- Object definitions local Bundle = {} local Bundle_mt = { __index = Bundle, } local Rope = {} local Rope_mt = { __index = Rope, } local function newBundle(options) options = options or {} return setmetatable({ ropes = {}, -- Active threads signals = {}, -- Signal listeners time = 0, errhand = options.errhand or print, }, Bundle_mt) end local function newRope(fn, ...) return setmetatable({ thread = create(fn), primer = { args = {...}, n = select('#', ...), }, }, Rope_mt) end -- Bundle implementation -- TODO: Should ropes be allowed to change bundles? function Bundle:suspend(rope) assert(rope.bundle == self, 'rope does not belong to this bundle') assert(not rope.paused, 'cannot suspend already suspended rope') local ts = self.ropes local index = rope.index rope.paused = true ts[index] = ts[#ts] ts[index].index = index ts[#ts] = nil end function Bundle:resume(rope) assert(rope.bundle == self, 'rope does not belong to this bundle') assert(rope.paused, 'cannot resume active rope') local ts = self.ropes local newIndex = #ts + 1 ts[newIndex] = rope rope.index = newIndex rope.paused = nil end function Bundle:update(dseconds) local ropes = self.ropes local i = 1 while i <= #ropes do local rope = ropes[i] local t = rope.thread local stat, err if rope.primer then local n = rope.primer.n local args = rope.primer.args rope.primer = nil stat, err = resume(t, rope, unpack(args, 1, n)) else stat, err = resume(t, dseconds, rope.signal) end if not stat then local errhand = rope.errhand or self.errhand errhand(err) end if status(t) == 'dead' then -- rope has finished ropes[i] = ropes[#ropes] ropes[i].index = i ropes[#ropes] = nil else i = i + 1 end end end function Bundle:rope(fn, ...) local ropes = self.ropes local index = #ropes + 1 local rope = newRope(fn, ...) rope.index = index rope.bundle = self ropes[index] = rope return rope end Bundle_mt.__call = Bundle.rope -- Rope implmentation local timeUnitToSeconds = { s = 1, sec = 1, seconds = 1, second = 1, ms = 0.001, milliseconds = 0.0001, millisecond = 0.0001, min = 60, min = 60, mn = 60, minutes = 60, minute = 60, h = 3600, hs = 3600, hours = 3600, hour = 3600, } local timeUnitFrames = { f = true, fs = true, frames = true, frame = true, } local function checkCorrectCoroutine(rope) if running() ~= rope.thread then error('rope function called outside of dispatch function or inside coroutine', 3) end end function Rope:wait(time) checkCorrectCoroutine(self) if time == nil then return yield() end local tp = type(time) if tp == 'number' then while (time > 0) do time = time - yield() end elseif tp == 'string' then local numstr, unit = time:match('^(.-)(%a*)$') local num = tonumber(numstr) or 1 if timeUnitFrames[unit] then for i = 1, num do yield() end else local time = num * (timeUnitToSeconds[unit] or 1) while (time > 0) do time = time - yield() end end else local f = time local time = 0 while not f(time) do time = time + yield() end end end -- Generate ease functions - https://github.com/rxi/flux/ local easeFunctions = {} do local expressions = { quad = "p * p", cubic = "p * p * p", quart = "p * p * p * p", quint = "p * p * p * p * p", expo = "2 ^ (10 * (p - 1))", sine = "-math.cos(p * (math.pi * .5)) + 1", circ = "-(math.sqrt(1 - (p * p)) - 1)", back = "p * p * (2.7 * p - 1.7)", elastic = "-(2^(10 * (p - 1)) * math.sin((p - 1.075) * (math.pi * 2) / .3))", } local function makeEaseFunction(str, expr) local load = loadstring or load return load("return function(p) " .. str:gsub("%$e", expr) .. " end")() end local function generateEase(name, expression) easeFunctions[name] = makeEaseFunction("return $e", expression) easeFunctions[name .. "in"] = easeFunctions[name] easeFunctions[name .. "out"] = makeEaseFunction([[ p = 1 - p return 1 - ($e) ]], expression) easeFunctions[name .. "inout"] = makeEaseFunction([[ p = p * 2 if p < 1 then return .5 * ($e) else p = 2 - p return .5 * (1 - ($e)) + .5 end ]], expression) end for k, v in pairs(expressions) do generateEase(k, v) end easeFunctions['linear'] = makeEaseFunction('return $e', 'p') end function Rope:tween(options) checkCorrectCoroutine(self) local object = options.object local key = options.key local to = options.to local ease = options.ease or 'linear' local timenumstr, timeunit = options.time:match('^(.-)(%a*)$') local timenum = tonumber(timenumstr) or 1 local from = options.from or object[key] local scale = to - from ease = easeFunctions[ease] or ease assert(type(ease) == 'function' or type(ease) == 'table', 'expected valid name of callable object for easing function') if timeUnitFrames[timeunit] then local t = 0 local dt = 1 / timenum for frame = 1, timenum do object[key] = from + scale * ease(t) t = t + dt yield() end else object[key] = to local tscale = 1 / (timenum * (timeUnitToSeconds[timeunit] or 1)) local t = 0 while t < 1 do object[key] = from + scale * ease(t) local dt = yield() t = t + tscale * dt end end object[key] = to end function Rope:tweenFork(options) return self.bundle(Rope.tween, options) end function Rope:fork(fn, ...) checkCorrectCoroutine(self) return self.bundle(fn, ...) end function Rope:listen(name) checkCorrectCoroutine(self) local bundle = self.bundle local signals = bundle.signals local slist = signals[name] bundle:suspend(self) if not slist then slist = {} signals[name] = slist end slist[#slist + 1] = self local dt, sig = yield() return sig end function Rope:signal(name, data) checkCorrectCoroutine(self) local bundle = self.bundle local signals = bundle.signals local slist = signals[name] if slist then for i = 1, #slist do local rope = slist[i] bundle:resume(rope) rope.signal = data end signals[name] = nil end end function Rope:parallel(...) checkCorrectCoroutine(self) local bundle = self.bundle local n = select('#', ...) local ropes = {} -- also used as signal local function onDone(rope) local pindex = rope.pindex ropes[pindex] = ropes[#ropes] ropes[pindex].pindex = pindex ropes[#ropes] = nil if #ropes == 0 then rope:signal(ropes, false) -- no error end end for i = 1, n do local fn = select(i, ...) local function wrappedfn(r) fn(r) onDone(r) end local rope = bundle(wrappedfn) local function errhand(err) for i = 1, #ropes do bundle:suspend(ropes[i]) end rope:signal(ropes, err) -- we errored out end rope.errhand = errhand rope.pindex = i ropes[#ropes + 1] = rope end return self:listen(ropes) end return newBundle