init commit - move things over from uranium-template to uranium-core

This commit is contained in:
Jill 2022-09-19 19:15:08 +03:00
commit 75876607e9
15 changed files with 2347 additions and 0 deletions

34
README.md Normal file
View File

@ -0,0 +1,34 @@
#### This is the development-oriented README. If you're looking to learn how to install and use the template, see [MANUAL.MD](/MANUAL.md).
---
<br>
<center id="start">
<h2 style="font-size: 42px">
<img src="docs/uranium.png" height="42px" alt="">
<b>Uranium Template</b>
</h2>
</center>
<br>
**Uranium Template** is a Love2D-inspired NotITG game development template, focusing on keeping things as **Lua-pure** as possible with enough abstractions to make you feel like you're not dealing with Stepmania jank at all.
But you probably already knew that; how do you develop this darn thing?
## Installation
Installation follows the same steps as [the manual installation](/MANUAL.md#installation) - extract as a modfile, run as a modfile.
## Distribution
During distribution, there are a couple of files that should be left out:
- This very `README.md` - keeping it will be a little confusing, I think!
## Development
Everything related to the core functionality - loading actors, callbacks - goes in `template/main.xml`. It's not the most convinient, but I want to eventually move this out to its own Lua file.
Everything related to the standard library should go into `template/stdlib/` - all files there are manually required in `template/stdlib/index.lua`.

6
actors.xml Normal file
View File

@ -0,0 +1,6 @@
<ActorFrame InitCommand="%oat._actor.initFrame">
<children>
<Layer Condition="oat._actor.cond()" Type="@oat._actor.type()" File="@oat._actor.file()" Font="@oat._actor.font()" InitCommand="%oat._actor.init"/>
<Layer Condition="oat._actor.next()" File="actors.xml" />
</children>
</ActorFrame>

528
main.xml Normal file
View File

@ -0,0 +1,528 @@
<Layer Type="ActorFrame" InitCommand="%function(self)
_G.oat = {}
oat._main = self
setmetatable(oat, {
-- if something isn't found in the table, fall back to a global lookup
__index = _G,
-- handle oat() calls to set the environment
__call = function(self, f)
setfenv(f or 2, self)
return f
end
})
oat()
local function copy(src)
local dest = {}
for k, v in pairs(src) do
dest[k] = v
end
return dest
end
oat.oat = _G.oat
oat.type = _G.type
oat.print = _G.print
oat.pairs = _G.pairs
oat.ipairs = _G.ipairs
oat.unpack = _G.unpack
oat.tonumber = _G.tonumber
oat.tostring = _G.tostring
oat.math = copy(_G.math)
oat.table = copy(_G.table)
oat.string = copy(_G.string)
oat.scx = SCREEN_CENTER_X
oat.scy = SCREEN_CENTER_Y
oat.sw = SCREEN_WIDTH
oat.sh = SCREEN_HEIGHT
oat.dw = DISPLAY:GetDisplayWidth()
oat.dh = DISPLAY:GetDisplayHeight()
local uraniumFunc = {}
function uraniumFunc:call(event, ...)
if self._callbacks[event] then
for _, callback in ipairs(self._callbacks[event]) do
callback(unpack(arg))
end
end
end
local uraniumMeta = {}
function uraniumMeta:__newindex(key, value)
if self._callbacks[key] then
table.insert(self._callbacks[key], value)
else
self._callbacks[key] = {value}
end
end
uraniumMeta.__index = uraniumFunc
uranium = setmetatable({_callbacks = {}}, uraniumMeta)
function backToSongWheel(message)
if message then
SCREENMAN:SystemMessage(message)
print(message)
end
GAMESTATE:FinishSong()
-- disable update_command
self:hidden(1)
end
local actorsInitialized = false -- if true, no new actors can be created
local luaobj
local globalQueue = {} -- for resetting
local patchedFunctions = {}
local function patchFunction(f, obj)
if not patchedFunctions[f] then patchedFunctions[f] = {} end
if not patchedFunctions[f][obj] then
patchedFunctions[f][obj] = function(...)
arg[1] = obj
local results
local status, result = pcall(function()
-- doing it this way instead of returning because lua
-- offers no way of grabbing everything BUT the first
-- argument out of pcall
results = {f(unpack(arg))}
end)
if not status then
error(result, 2)
else
return unpack(results)
end
end
end
return patchedFunctions[f][obj]
end
local function onCommand(self)
actorsInitialized = true
uranium:call('init')
end
function reset(actor)
if not actorsInitialized then error('uranium: cannot reset an actor during initialization', 2) end
for _, q in ipairs(globalQueue) do
local queueActor = q[1]
if queueActor == actor.__raw then
local v = q[2]
local func = queueActor[v[1]]
if not func then
-- uhmmm ??? hm. what do we do??
else
patchFunction(func, queueActor)(unpack(v[2]))
end
end
end
end
-- runs once during ScreenReadyCommand, before the user code is loaded
-- hides various actors that are placed by the theme
local function hideThemeActors()
for _, element in ipairs {
'Overlay', 'Underlay',
'ScoreP1', 'ScoreP2',
'LifeP1', 'LifeP2',
} do
local child = SCREENMAN(element)
if child then child:hidden(1) end
end
end
local lastt = GAMESTATE:GetSongTime()
local function screen_ready_command(self)
hideThemeActors()
self:hidden(0)
local errored = false
local firstrun = true
self:addcommand('Update', function()
if errored then
return 0
end
errored = true
t = os.clock()
b = GAMESTATE:GetSongBeat()
local dt = t - lastt
lastt = t
if firstrun then
firstrun = false
dt = 0
self:GetChildren()[2]:hidden(1)
end
for _, q in ipairs(globalQueue) do
local actor = q[1]
local v = q[2]
local func = actor[v[1]]
if not func then
-- uhmmm ??? hm. what do we do??
else
patchFunction(func, actor)(unpack(v[2]))
end
end
uranium:call('update', dt)
errored = false
return 0
end)
self:luaeffect('Update')
end
GAMESTATE:ApplyModifiers('clearall')
local function formatError(file, e)
local _, _, err = string.find(e, 'error loading package `[^`\']+\' %((.+)%)')
return (file and err) and ('error loading \'' .. file .. '\':\n' .. err) or e
end
local songName = GAMESTATE:GetCurrentSong():GetSongDir()
local additionalSongFolders = PREFSMAN:GetPreference('AdditionalSongFolders')
local additionalFolders = PREFSMAN:GetPreference('AdditionalFolders')
local function attemptload(f, filename) -- not to be confused with tryload
local err = ''
local lasterr = ''
local func
local s = '.' .. songName .. f
func, lasterr = loadfile(s)
if func then return func end
if not string.find(lasterr, '\' from path `') then return nil, formatError(filename, lasterr), nil end
err = err .. s .. '\n'
s = f
func, lasterr = loadfile(s)
if func then return func end
if not string.find(lasterr, '\' from path `') then return nil, formatError(filename, lasterr), nil end
err = err .. s .. '\n'
-- cut off 'Songs/' from the path
local _,index = string.find(songName,'Songs/')
local songLoc = string.sub(songName,index)
-- for every songfolder in the additionalsongfolders
if additionalSongFolders and additionalSongFolders ~= '' then
for songFolder in string.gfind(additionalSongFolders,'[^,]+') do
s = songFolder .. songLoc .. f
func, lasterr = loadfile(s)
if func then return func end
err = err .. s .. '\n'
end
end
if additionalFolders and additionalFolders ~= '' then
for folder in string.gfind(additionalFolders,'[^,]+') do
s = folder .. songName .. f
func, lasterr = loadfile(s)
if func then return func end
err = err .. s .. '\n'
end
end
return nil, lasterr, string.sub(err, 0, -2)
end
oat._loadPath = ''
function tryload(f)
local func, err, trace = attemptload(oat._loadPath .. f, f)
if not func then
if trace then
backToSongWheel('finding \'' .. f .. '\' failed, check log for more details')
error('uranium: finding \'' .. f .. '\' failed! tried these paths: ' .. '\n' .. trace, 2)
Trace(trace)
else
error(err, 2)
Trace('loading \'' .. f .. '\' failed!' .. '\n' .. err)
end
else
return oat(func)
end
end
local function getFolderStructure(path)
local folders = {}
for folder in string.gfind(path, '[^/]+') do
table.insert(folders, folder)
end
table.remove(folders, #folders)
return table.concat(folders, '/')
end
oat._requirePath = ''
function require(f)
-- . -> /
f = string.gsub(f, '%.', '/')
-- add .lua
f = f .. '.lua'
local oldpath = oat._requirePath
local folder = getFolderStructure(f)
if folder ~= '' then
oat._requirePath = oat._requirePath .. folder .. '/'
end
local res, c = pcall(tryload, oldpath .. f)
if not res then
error(c, 2)
else
local success, s = pcall(c)
if success then
return s
else
error('uranium: error loading \'' .. f .. '\':\n' .. s, 2)
end
end
oat._requirePath = oldpath
end
-- actors
local actorQueue = {}
local currentActor = nil
oat._actor = {}
function oat._actor.next()
local actor = actorQueue[1]
if actor then
table.remove(actorQueue, 1)
currentActor = actor
return true
else
return false
end
end
function oat._actor.cond()
return currentActor ~= nil
end
function oat._actor.type()
return currentActor.type
end
function oat._actor.file()
return currentActor.file
end
function oat._actor.font()
return currentActor.font
end
function oat._actor.init(self)
currentActor.init(self)
end
function oat._actor.initFrame(self)
local nextChild = self(2)
self:SetDrawFunction(function()
if nextChild then
nextChild:Draw()
end
end)
end
local function createProxyActor(name)
local queue = {}
local initCommands = {}
local lockedActor
return setmetatable({}, {
__index = function(self, key)
if key == '__raw' then
return lockedActor
end
if lockedActor then
local val = lockedActor[key]
if type(val) == 'function' then
return patchFunction(val, lockedActor)
end
return val
end
if key == '__lock' then
return function(actor)
for _, v in ipairs(queue) do
local func = actor[v[1]]
if not func then
error(
'uranium: error on \'' .. name .. '\' initialization on ' .. v[3].short_src .. ':' .. v[3].currentline .. ':\n' ..
'you\'re calling a function \'' .. v[1] .. '\' on a ' .. name .. ' which doesn\'t exist!:\n'
)
else
local success, result = pcall(function()
patchFunction(func, actor)(unpack(v[2]))
end)
if not success then
error(
'uranium: error on \'' .. name .. '\' initialization on ' .. v[3].short_src .. ':' .. v[3].currentline .. ':\n' ..
result
)
end
end
end
-- now that we know there's no poisonous methods in queue, let's offload them
for _, v in ipairs(queue) do
table.insert(globalQueue, {actor, v})
end
-- let's also properly route everything from the proxied actor to the actual actor
lockedActor = actor
-- and now let's run the initcommands
for _, c in ipairs(initCommands) do
local func = c[1]
local success, result = pcall(function()
func(actor)
end)
if not success then
error(
'uranium: error on \'' .. name .. '\' InitCommand defined on ' .. v[3].short_src .. ':' .. v[3].currentline .. ':\n' ..
result
)
end
end
initCommands = {}
end
else
return function(...)
if key == 'addcommand' and arg[2] == 'Init' then
table.insert(initCommands, {arg[3], debug.getinfo(2, 'Sl')})
else
table.insert(queue, {key, arg, debug.getinfo(2, 'Sl')})
end
end
end
end,
__newindex = function()
error('uranium: cannot set properties on actors!', 2)
end,
__tostring = function() return name end,
__name = name
})
end
local function createGenericFunc(type)
return function()
if actorsInitialized then error('uranium: cannot create an actor during runtime!!', 2) end
local actor = createProxyActor(type)
table.insert(actorQueue, {
type = type,
file = nil,
init = function(a)
actor.__lock(a)
end
})
return actor
end
end
Quad = createGenericFunc('Quad')
ActorProxy = createGenericFunc('ActorProxy')
Polygon = createGenericFunc('Polygon')
function Sprite(file)
if actorsInitialized then error('uranium: cannot create an actor during runtime!!', 2) end
if not file then error('uranium: cannot create a Sprite without a file', 2) end
local actor = createProxyActor('Sprite')
table.insert(actorQueue, {
type = nil,
file = '../src/' .. oat._requirePath .. file,
init = function(a)
actor.__lock(a)
end
})
return actor
end
function Model(file)
if actorsInitialized then error('uranium: cannot create an actor during runtime!!', 2) end
if not file then error('uranium: cannot create a Model without a file', 2) end
local actor = createProxyActor('Model')
table.insert(actorQueue, {
type = nil,
file = '../src/' .. oat._requirePath .. file,
init = function(a)
actor.__lock(a)
end
})
return actor
end
function BitmapText(font, text)
if actorsInitialized then error('uranium: cannot create an actor during runtime!!', 2) end
local actor = createProxyActor('BitmapText')
table.insert(actorQueue, {
type = 'BitmapText',
font = font and ('../src/' .. oat._requirePath .. font) or 'common',
init = function(a)
if text then a:settext(text) end
actor.__lock(a)
end
})
return actor
end
function ActorSound(file)
if actorsInitialized then error('uranium: cannot create an actor during runtime!!', 2) end
if not file then error('uranium: cannot create an ActorSound without a file', 2) end
local actor = createProxyActor('ActorSound')
table.insert(actorQueue, {
type = 'ActorSound',
file = '../src/' .. oat._requirePath .. file,
init = function(a)
actor.__lock(a)
end
})
return actor
end
oat._loadPath = 'template/stdlib/'
oat.tryload('index')()
oat._loadPath = 'src/'
lua = oat.tryload('main')
if lua then
local success, result = pcall(lua)
if success then
luaobj = result
self:addcommand('On', onCommand)
self:addcommand('Ready', screen_ready_command)
self:queuecommand('Ready')
else
Trace('got an error loading main.lua!')
Trace(result)
backToSongWheel('loading .lua file failed, check log for details')
error('uranium: loading main.lua file failed:\n' .. result)
end
else
Trace('luaobj doesnt exist despite passing checks. thats VERY odd')
backToSongWheel('uranium: loading .lua file failed, check log for details')
error('loading main.lua file failed: \'lua\' variable doesn\'t exist despite passing checks')
end
-- NotITG and OpenITG have a long standing bug where the InitCommand on an actor can run twice in certain cases.
-- By removing the command after it's done, it can only ever run once
self:removecommand('Init')
end"><children>
<Layer Condition="oat._actor.next()" File="actors.xml"/>
<Layer Type="Quad" InitCommand="xywh,SCREEN_CENTER_X,SCREEN_CENTER_Y,SCREEN_WIDTH,SCREEN_HEIGHT;diffuse,#000000;sleep,9e9"/>
</children></Layer>

343
stdlib/bitop.lua Normal file
View File

@ -0,0 +1,343 @@
local M = {_TYPE='module', _NAME='bitop.funcs', _VERSION='1.0-0'}
local floor = math.floor
local MOD = math.pow(2, 32)
local MODM = MOD-1
local function memoize(f)
local mt = {}
local t = setmetatable({}, mt)
function mt:__index(k)
local v = f(k)
t[k] = v
return v
end
return t
end
local function make_bitop_uncached(t, m)
local function bitop(a, b)
local res,p = 0,1
while a ~= 0 and b ~= 0 do
local am, bm = a%m, b%m
res = res + t[am][bm]*p
a = (a - am) / m
b = (b - bm) / m
p = p*m
end
res = res + (a+b) * p
return res
end
return bitop
end
local function make_bitop(t)
local op1 = make_bitop_uncached(t, math.pow(2, 1))
local op2 = memoize(function(a)
return memoize(function(b)
return op1(a, b)
end)
end)
return make_bitop_uncached(op2, math.pow(2, (t.n or 1)))
end
-- ok? probably not if running on a 32-bit int Lua number type platform
function M.tobit(x)
return x % math.pow(2, 32)
end
M.bxor = make_bitop {[0]={[0]=0,[1]=1},[1]={[0]=1,[1]=0}, n=4}
local bxor = M.bxor
function M.bnot(a) return MODM - a end
local bnot = M.bnot
function M.band(a,b) return ((a+b) - bxor(a,b))/2 end
local band = M.band
function M.bor(a,b) return MODM - band(MODM - a, MODM - b) end
local bor = M.bor
local lshift, rshift -- forward declare
function M.rshift(a,disp) -- Lua5.2 insipred
if disp < 0 then return lshift(a,-disp) end
return floor(a % math.pow(2, 32) / math.pow(2, disp))
end
rshift = M.rshift
function M.lshift(a,disp) -- Lua5.2 inspired
if disp < 0 then return rshift(a,-disp) end
return (a * math.pow(2, disp)) % math.pow(2, 32)
end
lshift = M.lshift
function M.tohex(x, n) -- BitOp style
n = n or 8
local up
if n <= 0 then
if n == 0 then return '' end
up = true
n = - n
end
x = band(x, math.pow(16, n-1))
return ('%0'..n..(up and 'X' or 'x')):format(x)
end
local tohex = M.tohex
function M.extract(n, field, width) -- Lua5.2 inspired
width = width or 1
return band(rshift(n, field), math.pow(2, width-1))
end
local extract = M.extract
function M.replace(n, v, field, width) -- Lua5.2 inspired
width = width or 1
local mask1 = math.pow(2, width-1)
v = band(v, mask1) -- required by spec?
local mask = bnot(lshift(mask1, field))
return band(n, mask) + lshift(v, field)
end
local replace = M.replace
function M.bswap(x) -- BitOp style
local a = band(x, 0xff); x = rshift(x, 8)
local b = band(x, 0xff); x = rshift(x, 8)
local c = band(x, 0xff); x = rshift(x, 8)
local d = band(x, 0xff)
return lshift(lshift(lshift(a, 8) + b, 8) + c, 8) + d
end
local bswap = M.bswap
function M.rrotate(x, disp) -- Lua5.2 inspired
disp = disp % 32
local low = band(x, math.pow(2, disp-1))
return rshift(x, disp) + lshift(low, 32-disp)
end
local rrotate = M.rrotate
function M.lrotate(x, disp) -- Lua5.2 inspired
return rrotate(x, -disp)
end
local lrotate = M.lrotate
M.rol = M.lrotate -- LuaOp inspired
M.ror = M.rrotate -- LuaOp insipred
function M.arshift(x, disp) -- Lua5.2 inspired
local z = rshift(x, disp)
if x >= 0x80000000 then z = z + lshift(math.pow(2, disp-1), 32-disp) end
return z
end
local arshift = M.arshift
function M.btest(x, y) -- Lua5.2 inspired
return band(x, y) ~= 0
end
--
-- Start Lua 5.2 "bit32" compat section.
--
M.bit32 = {} -- Lua 5.2 'bit32' compatibility
local function bit32_bnot(x)
return (-1 - x) % MOD
end
M.bit32.bnot = bit32_bnot
-- something here causes a syntax error so im just commenting out since i dont need it anyways
--[[
local function bit32_bxor(a, b, c, ...)
local z
if b then
a = a % MOD
b = b % MOD
z = bxor(a, b)
if c then
z = bit32_bxor(z, c, ...)
end
return z
elseif a then
return a % MOD
else
return 0
end
end
M.bit32.bxor = bit32_bxor
local function bit32_band(a, b, c, ...)
local z
if b then
a = a % MOD
b = b % MOD
z = ((a+b) - bxor(a,b)) / 2
if c then
z = bit32_band(z, c, ...)
end
return z
elseif a then
return a % MOD
else
return MODM
end
end
M.bit32.band = bit32_band
local function bit32_bor(a, b, c, ...)
local z
if b then
a = a % MOD
b = b % MOD
z = MODM - band(MODM - a, MODM - b)
if c then
z = bit32_bor(z, c, ...)
end
return z
elseif a then
return a % MOD
else
return 0
end
end
M.bit32.bor = bit32_bor
function M.bit32.btest(...)
return bit32_band(...) ~= 0
end
function M.bit32.lrotate(x, disp)
return lrotate(x % MOD, disp)
end
function M.bit32.rrotate(x, disp)
return rrotate(x % MOD, disp)
end
function M.bit32.lshift(x,disp)
if disp > 31 or disp < -31 then return 0 end
return lshift(x % MOD, disp)
end
function M.bit32.rshift(x,disp)
if disp > 31 or disp < -31 then return 0 end
return rshift(x % MOD, disp)
end
function M.bit32.arshift(x,disp)
x = x % MOD
if disp >= 0 then
if disp > 31 then
return (x >= 0x80000000) and MODM or 0
else
local z = rshift(x, disp)
if x >= 0x80000000 then z = z + lshift(math.pow(2, disp-1), 32-disp) end
return z
end
else
return lshift(x, -disp)
end
end
function M.bit32.extract(x, field, ...)
local width = ... or 1
if field < 0 or field > 31 or width < 0 or field+width > 32 then error 'out of range' end
x = x % MOD
return extract(x, field, ...)
end
function M.bit32.replace(x, v, field, ...)
local width = ... or 1
if field < 0 or field > 31 or width < 0 or field+width > 32 then error 'out of range' end
x = x % MOD
v = v % MOD
return replace(x, v, field, ...)
end
--
-- Start LuaBitOp "bit" compat section.
--
M.bit = {} -- LuaBitOp "bit" compatibility
function M.bit.tobit(x)
x = x % MOD
if x >= 0x80000000 then x = x - MOD end
return x
end
local bit_tobit = M.bit.tobit
function M.bit.tohex(x, ...)
return tohex(x % MOD, ...)
end
function M.bit.bnot(x)
return bit_tobit(bnot(x % MOD))
end
local function bit_bor(a, b, c, ...)
if c then
return bit_bor(bit_bor(a, b), c, ...)
elseif b then
return bit_tobit(bor(a % MOD, b % MOD))
else
return bit_tobit(a)
end
end
M.bit.bor = bit_bor
local function bit_band(a, b, c, ...)
if c then
return bit_band(bit_band(a, b), c, ...)
elseif b then
return bit_tobit(band(a % MOD, b % MOD))
else
return bit_tobit(a)
end
end
M.bit.band = bit_band
local function bit_bxor(a, b, c, ...)
if c then
return bit_bxor(bit_bxor(a, b), c, ...)
elseif b then
return bit_tobit(bxor(a % MOD, b % MOD))
else
return bit_tobit(a)
end
end
M.bit.bxor = bit_bxor
function M.bit.lshift(x, n)
return bit_tobit(lshift(x % MOD, n % 32))
end
function M.bit.rshift(x, n)
return bit_tobit(rshift(x % MOD, n % 32))
end
function M.bit.arshift(x, n)
return bit_tobit(arshift(x % MOD, n % 32))
end
function M.bit.rol(x, n)
return bit_tobit(lrotate(x % MOD, n % 32))
end
function M.bit.ror(x, n)
return bit_tobit(rrotate(x % MOD, n % 32))
end
function M.bit.bswap(x)
return bit_tobit(bswap(x % MOD))
end
]]
return M

300
stdlib/color.lua Normal file
View File

@ -0,0 +1,300 @@
--[[
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 1].
]]
local function hslToRgb(h, s, l)
local r, g, b
if s == 0 then
r, g, b = l, l, l -- achromatic
else
function hue2rgb(p, q, t)
if t < 0 then t = t + 1 end
if t > 1 then t = t - 1 end
if t < 1/6 then return p + (q - p) * 6 * t end
if t < 1/2 then return q end
if t < 2/3 then return p + (q - p) * (2/3 - t) * 6 end
return p
end
local q
if l < 0.5 then q = l * (1 + s) else q = l + s - l * s end
local p = 2 * l - q
r = hue2rgb(p, q, h + 1/3)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1/3)
end
return r, g, b
end
--[[
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 1].
]]
local function rgbToHsl(r, g, b)
local max, min = math.max(r, g, b), math.min(r, g, b)
local h, s, l
l = (max + min) / 2
if max == 0 then s = 0 else s = (max - min) / max end
if max == min then
h, s = 0, 0 -- achromatic
else
local d = max - min
local s
if l > 0.5 then s = d / (2 - max - min) else s = d / (max + min) end
if max == r then
h = (g - b) / d
if g < b then h = h + 6 end
elseif max == g then h = (b - r) / d + 2
elseif max == b then h = (r - g) / d + 4
end
h = h / 6
end
return h, s, l
end
--[[
* Converts an RGB color value to HSV. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSV_color_space.
* Assumes r, g, and b are contained in the set [0, 1] and
* returns h, s, and v in the set [0, 1].
]]
local function rgbToHsv(r, g, b)
local max, min = math.max(r, g, b), math.min(r, g, b)
local h, s, v
v = max
local d = max - min
if max == 0 then s = 0 else s = d / max end
if max == min then
h = 0 -- achromatic
else
if max == r then
h = (g - b) / d
if g < b then h = h + 6 end
elseif max == g then h = (b - r) / d + 2
elseif max == b then h = (r - g) / d + 4
end
h = h / 6
end
return h, s, v
end
--[[
* Converts an HSV color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSV_color_space.
* Assumes h, s, and v are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 1].
]]
local function hsvToRgb(h, s, v)
local r, g, b
local i = math.floor(h * 6);
local f = h * 6 - i;
local p = v * (1 - s);
local q = v * (1 - f * s);
local t = v * (1 - (1 - f) * s);
i = i % 6
if i == 0 then r, g, b = v, t, p
elseif i == 1 then r, g, b = q, v, p
elseif i == 2 then r, g, b = p, v, t
elseif i == 3 then r, g, b = p, q, v
elseif i == 4 then r, g, b = t, p, v
elseif i == 5 then r, g, b = v, p, q
end
return r, g, b
end
---@class color
---@field r number @red, 0.0 - 1.0
---@field g number @green, 0.0 - 1.0
---@field b number @blue, 0.0 - 1.0
---@field a number @alpha, 0.0 - 1.0
---@operator add(color): color
---@operator add(number): color
---@operator sub(color): color
---@operator sub(number): color
---@operator mul(color): color
---@operator mul(number): color
---@operator div(color): color
---@operator div(number): color
local col = {}
--- for use in actor:diffuse(col:unpack())
---@return number, number, number, number
function col:unpack()
return self.r, self.g, self.b, self.a
end
-- conversions
---@return number, number, number
function col:rgb()
return self.r, self.g, self.b
end
---@return number, number, number
function col:hsl()
return rgbToHsl(self.r, self.g, self.b)
end
---@return number, number, number
function col:hsv()
return rgbToHsv(self.r, self.g, self.b)
end
---@return string
function col:hex()
return string.format('%02x%02x%02x',
math.floor(self.r * 255),
math.floor(self.g * 255),
math.floor(self.b * 255))
end
-- setters
---@return color
function col:hue(h)
local _, s, v = self:hsv()
return hsv(h % 1, s, v, self.a)
end
---@return color
function col:huesmooth(h)
local _, s, v = self:hsv()
return shsv(h % 1, s, v, self.a)
end
---@return color
function col:alpha(a)
return rgb(self.r, self.g, self.b, a)
end
--- multiplies current alpha by provided value
---@return color
function col:malpha(a)
return rgb(self.r, self.g, self.b, self.a * a)
end
-- effects
---@return color
function col:invert()
return rgb(1 - self.r, 1 - self.g, 1 - self.b, self.a)
end
---@return color
function col:grayscale()
return rgb(self.r * 0.299 + self.g * 0.587 + self.b * 0.114, self.a)
end
---@return color
function col:hueshift(a)
local h, s, v = self:hsv()
return hsv((h + a) % 1, s, v, self.a)
end
local colmeta = {}
function colmeta:__index(i)
if i == 1 then return self.r end
if i == 2 then return self.g end
if i == 3 then return self.b end
if i == 4 then return self.a end
return col[i]
end
local function typ(a)
return (type(a) == 'table' and a.r and a.g and a.b and a.a) and 'color' or type(a)
end
local function genericop(a, b, f, name)
local typea = typ(a)
local typeb = typ(b)
if typea == 'number' then
return rgb(f(b.r, a), f(b.g, a), f(b.b, a), b.a)
elseif typeb == 'number' then
return rgb(f(a.r, b), f(a.g, b), f(a.b, b), a.a)
elseif typea == 'color' and typeb == 'color' then
return rgb(f(a.r, b.r), f(a.g, b.g), f(a.b, b.b), f(a.a, b.a))
end
error('cant apply ' .. name .. ' to ' .. typea .. ' and ' .. typeb, 3)
end
function colmeta.__add(a, b)
return genericop(a, b, function(a, b) return a + b end, 'add')
end
function colmeta.__sub(a, b)
return genericop(a, b, function(a, b) return a - b end, 'sub')
end
function colmeta.__mul(a, b)
return genericop(a, b, function(a, b) return a * b end, 'mul')
end
function colmeta.__div(a, b)
return genericop(a, b, function(a, b) return a / b end, 'div')
end
function colmeta.__eq(a, b)
return (typ(a) == 'color' and typ(b) == 'color') and (a.r == b.r and a.g == b.g and a.b == b.b and a.a == b.a)
end
function colmeta:__tostring()
return '#' .. self:hex()
end
colmeta.__name = 'color'
-- constructors
---@return color
function rgb(r, g, b, a)
a = a or 1
return setmetatable({r = r, g = g, b = b, a = a or 1}, colmeta)
end
---@return color
function hsl(h, s, l, a)
a = a or 1
local r, g, b = hslToRgb(h % 1, s, l)
return setmetatable({r = r, g = g, b = b, a = a or 1}, colmeta)
end
---@return color
function hsv(h, s, v, a)
a = a or 1
local r, g, b = hsvToRgb(h % 1, s, v)
return setmetatable({r = r, g = g, b = b, a = a or 1}, colmeta)
end
--- smoother hsv. not correct but looks nicer
---@return color
function shsv(h, s, v, a)
h = h % 1
return hsv(h * h * (3 - 2 * h), s, v, a)
end
---@param hex string
---@return color
function hex(hex)
hex = string.gsub(hex, '#', '')
if string.len(hex) == 3 then
return rgb((tonumber('0x' .. string.sub(hex, 1, 1)) * 17) / 255, (tonumber('0x' .. string.sub(hex, 2, 2)) * 17) / 255, (tonumber('0x' .. string.sub(hex, 3, 3)) * 17) / 255)
else
return rgb(tonumber('0x' .. string.sub(hex, 1, 2)) / 255, tonumber('0x' .. string.sub(hex, 3, 4)) / 255, tonumber('0x' .. string.sub(hex, 5, 6)) / 255)
end
end

80
stdlib/easable.lua Normal file
View File

@ -0,0 +1,80 @@
---@class easable
---@field public a number @the eased value
---@field public toa number @the target, uneased value
---@field protected onUpdateFuncs fun():nil[]
local eas = {}
---@param new number @New value to ease to
---@return void
function eas:set(new)
self.toa = new
end
---@param new number @New value
---@return void
function eas:reset(new)
self.toa = new
self.a = new
end
---@param new number @How much to add to current value to ease to
---@return void
function eas:add(new)
self.toa = self.toa + new
end
---@param func fun(a: number):void @Adds a callback function that will run each time the eased value changes
---@return void
function eas:onUpdate(func)
table.insert(self.onUpdateFuncs, func)
end
local easmeta = {}
easmeta.__index = eas
easmeta.__name = 'easable'
function easmeta.__add(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) + ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__sub(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) - ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__mul(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) * ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__div(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) / ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__mod(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) % ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__eq(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) == ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__lt(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) < ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__le(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) <= ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta:__call(dt)
self.a = mix(self.a, self.toa, dt)
for _, callback in ipairs(self.onUpdateFuncs) do
callback(self.a)
end
end
function easmeta:__tostring()
return tostring(self.a)
end
function easmeta:__unm(self)
return -self.a
end
---@param default number
---@return easable
function easable(default)
default = default or 0
return setmetatable({a = default, toa = default, onUpdateFuncs = {}}, easmeta)
end

90
stdlib/easable2.lua Normal file
View File

@ -0,0 +1,90 @@
---@class easable2
---@field public a number @the eased value
---@field public toa number @the target, uneased value
---@field public ease fun(a:number):number @the ease to use
---@field protected onUpdateFuncs fun(a:number):nil[]
---@field protected _a number @the internal value, linearly eased
local eas = {}
---@param new number @New value to ease to
---@return void
function eas:set(new)
self.toa = new
end
---@param new number @New value
---@return void
function eas:reset(new)
self.toa = new
self._a = new
end
---@param new number @How much to add to current value to ease to
---@return void
function eas:add(new)
self.toa = self.toa + new
end
---@param func fun(a: number):void @Adds a callback function that will run each time the eased value changes
---@return void
function eas:onUpdate(func)
table.insert(self.onUpdateFuncs, func)
end
local easmeta = {}
easmeta.__index = eas
easmeta.__name = 'easable2'
function easmeta.__add(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) + ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__sub(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) - ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__mul(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) * ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__div(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) / ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__mod(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) % ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__eq(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) == ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__lt(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) < ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta.__le(a, b)
return ((type(a) == 'table' and a.a) and a.a or a) <= ((type(b) == 'table' and b.a) and b.a or b)
end
function easmeta:__call(dt)
if self._a == self.toa then
-- do nothing
elseif self._a < self.toa then
self._a = self._a + math.min(dt, math.abs(self._a - self.toa))
else
self._a = self._a - math.min(dt, math.abs(self._a - self.toa))
end
self.a = self.ease(self._a)
for _, callback in ipairs(self.onUpdateFuncs) do
callback(self.a)
end
end
function easmeta:__tostring()
return tostring(self.a)
end
function easmeta:__unm(self)
return -self.a
end
---@param default number
---@return easable2
function easable2(default, ease)
default = default or 0
return setmetatable({a = default, toa = default, onUpdateFuncs = {}, ease = ease or outSine, _a = default}, easmeta)
end

338
stdlib/ease.lua Normal file
View File

@ -0,0 +1,338 @@
-- nabbed straight from mirin template:
-- https://github.com/XeroOl/notitg-mirin/blob/d1e9a8e71026aeabe81c682a114ce265cbd6362a/template/ease.lua
local sqrt = math.sqrt
local sin = math.sin
local asin = math.asin
local cos = math.cos
local pow = math.pow
local exp = math.exp
local pi = math.pi
local abs = math.abs
-- ===================================================================== --
-- Utility functions
--- Flip any easing function, making it go from 1 to 0
-- Example use:
-- ```lua
-- ease {0, 20, flip(outQuad), 50, 'modname'}
-- ```
flip = setmetatable({}, {
__call = function(self, fn)
self[fn] = self[fn] or function(x) return 1 - fn(x) end
return self[fn]
end
})
-- Mix two easing functions together into a new ease
-- the new ease starts by acting like the first argument, and then ends like the second argument
-- Example: ease {0, 20, blendease(inQuad, outQuad), 100, 'modname'}
blendease = setmetatable({}, {
__index = function(self, key)
self[key] = {}
return self[key]
end,
__call = function(self, fn1, fn2)
if not self[fn1][fn2] then
local transient1 = fn1(1) <= 0.5
local transient2 = fn2(1) <= 0.5
if transient1 and not transient2 then
error('blendease: the first argument is a transient ease, but the second argument doesn\'t match')
end
if transient2 and not transient1 then
error('blendease: the second argument is a transient ease, but the first argument doesn\'t match')
end
self[fn1][fn2] = function(x)
local mixFactor = 3*x^2-2*x^3
return (1 - mixFactor) * fn1(x) + mixFactor * fn2(x)
end
end
return self[fn1][fn2]
end
})
local function param1cache(self, param1)
self.cache[param1] = self.cache[param1] or function(x)
return self.fn(x, param1)
end
return self.cache[param1]
end
local param1mt = {
__call = function(self, x, param1)
return self.fn(x, param1 or self.dp1)
end,
__index = {
param = param1cache,
params = param1cache,
}
}
-- Declare an easing function taking one custom parameter
function with1param(fn, defaultparam1)
return setmetatable({
fn = fn,
dp1 = defaultparam1,
cache = {},
}, param1mt)
end
local function param2cache(self, param1, param2)
self.cache[param1] = self.cache[param1] or {}
self.cache[param1][param2] = self.cache[param1][param2] or function(x)
return self.fn(x, param1, param2)
end
return self.cache[param1][param2]
end
local param2mt = {
__call = function(self, x, param1, param2)
return self.fn(x, param1 or self.dp1, param2 or self.dp2)
end,
__index = {
param=param2cache,
params=param2cache,
}
}
-- Declare an easing function taking two custom parameters
function with2params(fn, defaultparam1, defaultparam2)
return setmetatable({
fn = fn,
dp1 = defaultparam1,
dp2 = defaultparam2,
cache = {},
}, param2mt)
end
-- ===================================================================== --
-- Easing functions
function bounce(t) return 4 * t * (1 - t) end
function tri(t) return 1 - abs(2 * t - 1) end
function bell(t) return inOutQuint(tri(t)) end
function pop(t) return 3.5 * (1 - t) * (1 - t) * sqrt(t) end
function tap(t) return 3.5 * t * t * sqrt(1 - t) end
function pulse(t) return t < .5 and tap(t * 2) or -pop(t * 2 - 1) end
function spike(t) return exp(-10 * abs(2 * t - 1)) end
function inverse(t) return t * t * (1 - t) * (1 - t) / (0.5 - t) end
local function popElasticInternal(t, damp, count)
return (1000 ^ -(t ^ damp) - 0.001) * sin(count * pi * t)
end
local function tapElasticInternal(t, damp, count)
return (1000 ^ -((1 - t) ^ damp) - 0.001) * sin(count * pi * (1 - t))
end
local function pulseElasticInternal(t, damp, count)
if t < .5 then
return tapElasticInternal(t * 2, damp, count)
else
return -popElasticInternal(t * 2 - 1, damp, count)
end
end
popElastic = with2params(popElasticInternal, 1.4, 6)
tapElastic = with2params(tapElasticInternal, 1.4, 6)
pulseElastic = with2params(pulseElasticInternal, 1.4, 6)
impulse = with1param(function(t, damp)
t = t ^ damp
return t * (1000 ^ -t - 0.001) * 18.6
end, 0.9)
function instant() return 1 end
function linear(t) return t end
function inQuad(t) return t * t end
function outQuad(t) return -t * (t - 2) end
function inOutQuad(t)
t = t * 2
if t < 1 then
return 0.5 * t ^ 2
else
return 1 - 0.5 * (2 - t) ^ 2
end
end
function outInQuad(t)
t = t * 2
if t < 1 then
return 0.5 - 0.5 * (1 - t) ^ 2
else
return 0.5 + 0.5 * (t - 1) ^ 2
end
end
function inCubic(t) return t * t * t end
function outCubic(t) return 1 - (1 - t) ^ 3 end
function inOutCubic(t)
t = t * 2
if t < 1 then
return 0.5 * t ^ 3
else
return 1 - 0.5 * (2 - t) ^ 3
end
end
function outInCubic(t)
t = t * 2
if t < 1 then
return 0.5 - 0.5 * (1 - t) ^ 3
else
return 0.5 + 0.5 * (t - 1) ^ 3
end
end
function inQuart(t) return t * t * t * t end
function outQuart(t) return 1 - (1 - t) ^ 4 end
function inOutQuart(t)
t = t * 2
if t < 1 then
return 0.5 * t ^ 4
else
return 1 - 0.5 * (2 - t) ^ 4
end
end
function outInQuart(t)
t = t * 2
if t < 1 then
return 0.5 - 0.5 * (1 - t) ^ 4
else<