You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

327 lines
8.1 KiB
Lua

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