initial commit

This commit is contained in:
Rokas Puzonas 2023-07-17 16:30:30 +03:00
commit 61b2683818
26 changed files with 4768 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

11
.luarc.json Normal file
View File

@ -0,0 +1,11 @@
{
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
"Lua.runtime.version": "LuaJIT",
"Lua.workspace.checkThirdParty": false,
"Lua.workspace.library": [
"${3rd}/love2d/library"
],
"Lua.workspace.userThirdParty": [
"love2d"
]
}

6
.vimrc.lua Normal file
View File

@ -0,0 +1,6 @@
local map = vim.api.nvim_set_keymap
map('n', '<leader>l', ":execute 'silent !kitty love src &' | redraw!<cr>", {
silent = true,
noremap = true
})

1
README.md Normal file
View File

@ -0,0 +1 @@
# Love2D project

17
build.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
BUILD_NAME="love2d-project"
ZIP_OPTIONS="-x '*.xcf'"
# Remove existing contents
rm -r build
mkdir build
# Build project
echo Building project...
cd src
zip -q $ZIP_OPTIONS -r ../build/$BUILD_NAME.love *
cd - > /dev/null
# Jobs done
echo Done.

11
src/conf.lua Normal file
View File

@ -0,0 +1,11 @@
function love.conf(t)
t.title = "Love2D project"
t.console = true
t.window.width = 1280
t.window.height = 720
t.window.resizable = true
t.modules.joystick = false
end

8
src/helpers/darken.lua Normal file
View File

@ -0,0 +1,8 @@
local vivid = require("lib.vivid")
local hueShift = require("helpers.hue-shift")
local BLUE_HUE = 240/360
return function(r, g, b)
return hueShift(BLUE_HUE, 0.08, vivid.desaturate(0.1, vivid.darken(0.1, r, g, b)))
end

View File

@ -0,0 +1,8 @@
local lerp = require("lib.lume").lerp
local vivid = require("lib.vivid")
return function (target_hue, amount, r, g, b)
local h, s, l = vivid.RGBtoHSL(r, g, b)
h = lerp(h, target_hue, amount)
return vivid.HSLtoRGB(h, s, l)
end

View File

@ -0,0 +1,4 @@
return function(x0, y0, x, y, w, h)
return x0 >= x and x0 < x+w and y0 >= y and y0 < y+h
end

8
src/helpers/lighten.lua Normal file
View File

@ -0,0 +1,8 @@
local vivid = require("lib.vivid")
local hueShift = require("helpers.hue-shift")
local YELLOW_HUE = 60/360
return function(r, g, b)
return hueShift(YELLOW_HUE, 0.05, vivid.saturate(0.1, vivid.lighten(0.1, r, g, b)))
end

View File

@ -0,0 +1,17 @@
-- Pick a color for text that would be approriate depending on the given
-- background color.
-- If background color is bright, returns black.
-- IF background color is dark, returns white.
return function(p1, p2, p3)
local r, g, b
if p1 and p2 and p3 then
r, g, b = p1, p2, p3
else
r, g, b = p1[1], p1[2], p1[3]
end
if r+g+b < 1.5 then
return 1, 1, 1, 1
else
return 0, 0, 0, 1
end
end

279
src/lib/brinevector.lua Normal file
View File

@ -0,0 +1,279 @@
-- LAST UPDATED: 9/25/18
-- added Vector.getCeil and v.ceil
-- added Vector.clamp
-- changed float to double as per Mike Pall's advice
-- added Vector.floor
-- added fallback to table if luajit ffi is not detected (used for unit tests)
--[[
BrineVector: a luajit ffi-accelerated vector library
Copyright 2018 Brian Sarfati
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--]]
local ffi
local VECTORTYPE = "cdata"
if jit and jit.status() then
ffi = require "ffi"
ffi.cdef[[
typedef struct {
double x;
double y;
} brinevector;
]]
else
VECTORTYPE = "table"
end
local Vector = {}
setmetatable(Vector,Vector)
local special_properties = {
length = "getLength",
normalized = "getNormalized",
angle = "getAngle",
length2 = "getLengthSquared",
copy = "getCopy",
inverse = "getInverse",
floor = "getFloor",
ceil = "getCeil",
}
function Vector.__index(t, k)
if special_properties[k] then
return Vector[special_properties[k]](t)
end
return rawget(Vector,k)
end
function Vector.getLength(v)
return math.sqrt(v.x * v.x + v.y * v.y)
end
function Vector.getLengthSquared(v)
return v.x*v.x + v.y*v.y
end
function Vector.getNormalized(v)
local length = v.length
if length == 0 then return Vector(0,0) end
return Vector(v.x / length, v.y / length)
end
function Vector.getAngle(v)
return math.atan2(v.y, v.x)
end
function Vector.getCopy(v)
return Vector(v.x,v.y)
end
function Vector.getInverse(v)
return Vector(1 / v.x, 1 / v.y)
end
function Vector.getFloor(v)
return Vector(math.floor(v.x), math.floor(v.y))
end
function Vector.getCeil(v)
return Vector(math.ceil(v.x), math.ceil(v.y))
end
function Vector.__newindex(t,k,v)
if k == "length" then
local res = t.normalized * v
t.x = res.x
t.y = res.y
return
end
if k == "angle" then
local res = t:angled(v)
t.x = res.x
t.y = res.y
return
end
if t == Vector then
rawset(t,k,v)
else
error("Cannot assign a new property '" .. k .. "' to a Vector", 2)
end
end
function Vector.angled(v, angle)
local length = v.length
return Vector(math.cos(angle) * length, math.sin(angle) * length)
end
function Vector.trim(v,mag)
if v.length < mag then return v end
return v.normalized * mag
end
function Vector.split(v)
return v.x, v.y
end
local function clamp(x, min, max)
-- because Mike Pall says math.min and math.max are JIT-optimized
return math.min(math.max(min, x), max)
end
function Vector.clamp(v, topleft, bottomright)
-- clamps a vector to a certain bounding box about the origin
return Vector(
clamp(v.x, topleft.x, bottomright.x),
clamp(v.y, topleft.y, bottomright.y)
)
end
function Vector.hadamard(v1, v2) -- also known as "Componentwise multiplication"
return Vector(v1.x * v2.x, v1.y * v2.y)
end
function Vector.rotated(v, angle)
local cos = math.cos(angle)
local sin = math.sin(angle)
return Vector(v.x * cos - v.y * sin, v.x * sin + v.y * cos)
end
local iteraxes_lookup = {
xy = {"x","y"},
yx = {"y","x"}
}
local function iteraxes(ordertable, i)
i = i + 1
if i > 2 then return nil end
return i, ordertable[i]
end
function Vector.axes(order)
return iteraxes, iteraxes_lookup[order or "yx"], 0
end
local function iterpairs(vector, k)
if k == nil then
k = "x"
elseif k == "x" then
k = "y"
else
k = nil
end
return k, vector[k]
end
function Vector.__pairs(v)
return iterpairs, v, nil
end
if ffi then
function Vector.isVector(arg)
return ffi.istype("brinevector",arg)
end
else
function Vector.isVector(arg)
return type(arg) == VECTORTYPE and arg.x and arg.y
end
end
function Vector.__add(v1, v2)
return Vector(v1.x + v2.x, v1.y + v2.y)
end
function Vector.__sub(v1, v2)
return Vector(v1.x - v2.x, v1.y - v2.y)
end
function Vector.__mul(v1, op)
-- acts as a dot multiplication if op is a vector
-- if op is a scalar then works as usual
if type(v1) == "number" then
return Vector(op.x * v1, op.y * v1)
end
if type(op) == VECTORTYPE then
return v1.x * op.x + v1.y * op.y
else
return Vector(v1.x * op, v1.y * op)
end
end
function Vector.__div(v1, op)
if type(op) == "number" then
if op == 0 then error("Vector NaN occured", 2) end
return Vector(v1.x / op, v1.y / op)
elseif type(op) == VECTORTYPE then
if op.x * op.y == 0 then error("Vector NaN occured", 2) end
return Vector(v1.x / op.x, v1.y / op.y)
end
end
function Vector.__unm(v)
return Vector(-v.x, -v.y)
end
function Vector.__eq(v1,v2)
if (not Vector.isVector(v1)) or (not Vector.isVector(v2)) then return false end
return v1.x == v2.x and v1.y == v2.y
end
function Vector.__mod(v1,v2) -- ran out of symbols, so i chose % for the hadamard product
return Vector(v1.x * v2.x, v1.y * v2.y)
end
function Vector.__tostring(t)
return string.format("Vector{%.4f, %.4f}",t.x,t.y)
end
function Vector.__concat(str, v)
return tostring(str) .. tostring(v)
end
function Vector.__len(v)
return math.sqrt(v.x * v.x + v.y * v.y)
end
if ffi then
function Vector.__call(t,x,y)
return ffi.new("brinevector",x or 0,y or 0)
end
else
function Vector.__call(t,x,y)
return setmetatable({x = x or 0, y = y or 0}, Vector)
end
end
local dirs = {
up = Vector(0,-1),
down = Vector(0,1),
left = Vector(-1,0),
right = Vector(1,0),
top = Vector(0,-1),
bottom = Vector(0,1)
}
function Vector.dir(dir)
return dirs[dir] and dirs[dir].copy or Vector()
end
if ffi then
ffi.metatype("brinevector",Vector)
end
return Vector

773
src/lib/bump.lua Normal file
View File

@ -0,0 +1,773 @@
local bump = {
_VERSION = 'bump v3.1.7',
_URL = 'https://github.com/kikito/bump.lua',
_DESCRIPTION = 'A collision detection library for Lua',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2014 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
------------------------------------------
-- Auxiliary functions
------------------------------------------
local DELTA = 1e-10 -- floating-point margin of error
local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max
local function sign(x)
if x > 0 then return 1 end
if x == 0 then return 0 end
return -1
end
local function nearest(x, a, b)
if abs(a - x) < abs(b - x) then return a else return b end
end
local function assertType(desiredType, value, name)
if type(value) ~= desiredType then
error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')')
end
end
local function assertIsPositiveNumber(value, name)
if type(value) ~= 'number' or value <= 0 then
error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')')
end
end
local function assertIsRect(x,y,w,h)
assertType('number', x, 'x')
assertType('number', y, 'y')
assertIsPositiveNumber(w, 'w')
assertIsPositiveNumber(h, 'h')
end
local defaultFilter = function()
return 'slide'
end
------------------------------------------
-- Rectangle functions
------------------------------------------
local function rect_getNearestCorner(x,y,w,h, px, py)
return nearest(px, x, x+w), nearest(py, y, y+h)
end
-- This is a generalized implementation of the liang-barsky algorithm, which also returns
-- the normals of the sides where the segment intersects.
-- Returns nil if the segment never touches the rect
-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
local function rect_getSegmentIntersectionIndices(x,y,w,h, x1,y1,x2,y2, ti1,ti2)
ti1, ti2 = ti1 or 0, ti2 or 1
local dx, dy = x2-x1, y2-y1
local nx, ny
local nx1, ny1, nx2, ny2 = 0,0,0,0
local p, q, r
for side = 1,4 do
if side == 1 then nx,ny,p,q = -1, 0, -dx, x1 - x -- left
elseif side == 2 then nx,ny,p,q = 1, 0, dx, x + w - x1 -- right
elseif side == 3 then nx,ny,p,q = 0, -1, -dy, y1 - y -- top
else nx,ny,p,q = 0, 1, dy, y + h - y1 -- bottom
end
if p == 0 then
if q <= 0 then return nil end
else
r = q / p
if p < 0 then
if r > ti2 then return nil
elseif r > ti1 then ti1,nx1,ny1 = r,nx,ny
end
else -- p > 0
if r < ti1 then return nil
elseif r < ti2 then ti2,nx2,ny2 = r,nx,ny
end
end
end
end
return ti1,ti2, nx1,ny1, nx2,ny2
end
-- Calculates the minkowsky difference between 2 rects, which is another rect
local function rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
return x2 - x1 - w1,
y2 - y1 - h1,
w1 + w2,
h1 + h2
end
local function rect_containsPoint(x,y,w,h, px,py)
return px - x > DELTA and py - y > DELTA and
x + w - px > DELTA and y + h - py > DELTA
end
local function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2)
return x1 < x2+w2 and x2 < x1+w1 and
y1 < y2+h2 and y2 < y1+h1
end
local function rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2)
local dx = x1 - x2 + (w1 - w2)/2
local dy = y1 - y2 + (h1 - h2)/2
return dx*dx + dy*dy
end
local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY)
goalX = goalX or x1
goalY = goalY or y1
local dx, dy = goalX - x1, goalY - y1
local x,y,w,h = rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
local overlaps, ti, nx, ny
if rect_containsPoint(x,y,w,h, 0,0) then -- item was intersecting other
local px, py = rect_getNearestCorner(x,y,w,h, 0, 0)
local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection
ti = -wi * hi -- ti is the negative area of intersection
overlaps = true
else
local ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, math.huge)
-- item tunnels into other
if ti1
and ti1 < 1
and (abs(ti1 - ti2) >= DELTA) -- special case for rect going through another rect's corner
and (0 < ti1 + DELTA
or 0 == ti1 and ti2 > 0)
then
ti, nx, ny = ti1, nx1, ny1
overlaps = false
end
end
if not ti then return end
local tx, ty
if overlaps then
if dx == 0 and dy == 0 then
-- intersecting and not moving - use minimum displacement vector
local px, py = rect_getNearestCorner(x,y,w,h, 0,0)
if abs(px) < abs(py) then py = 0 else px = 0 end
nx, ny = sign(px), sign(py)
tx, ty = x1 + px, y1 + py
else
-- intersecting and moving - move in the opposite direction
local ti1, _
ti1,_,nx,ny = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, 1)
if not ti1 then return end
tx, ty = x1 + dx * ti1, y1 + dy * ti1
end
else -- tunnel
tx, ty = x1 + dx * ti, y1 + dy * ti
end
return {
overlaps = overlaps,
ti = ti,
move = {x = dx, y = dy},
normal = {x = nx, y = ny},
touch = {x = tx, y = ty},
itemRect = {x = x1, y = y1, w = w1, h = h1},
otherRect = {x = x2, y = y2, w = w2, h = h2}
}
end
------------------------------------------
-- Grid functions
------------------------------------------
local function grid_toWorld(cellSize, cx, cy)
return (cx - 1)*cellSize, (cy-1)*cellSize
end
local function grid_toCell(cellSize, x, y)
return floor(x / cellSize) + 1, floor(y / cellSize) + 1
end
-- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing",
-- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf
-- It has been modified to include both cells when the ray "touches a grid corner",
-- and with a different exit condition
local function grid_traverse_initStep(cellSize, ct, t1, t2)
local v = t2 - t1
if v > 0 then
return 1, cellSize / v, ((ct + v) * cellSize - t1) / v
elseif v < 0 then
return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v
else
return 0, math.huge, math.huge
end
end
local function grid_traverse(cellSize, x1,y1,x2,y2, f)
local cx1,cy1 = grid_toCell(cellSize, x1,y1)
local cx2,cy2 = grid_toCell(cellSize, x2,y2)
local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
local cx,cy = cx1,cy1
f(cx, cy)
-- The default implementation had an infinite loop problem when
-- approaching the last cell in some occassions. We finish iterating
-- when we are *next* to the last cell
while abs(cx - cx2) + abs(cy - cy2) > 1 do
if tx < ty then
tx, cx = tx + dx, cx + stepX
f(cx, cy)
else
-- Addition: include both cells when going through corners
if tx == ty then f(cx + stepX, cy) end
ty, cy = ty + dy, cy + stepY
f(cx, cy)
end
end
-- If we have not arrived to the last cell, use it
if cx ~= cx2 or cy ~= cy2 then f(cx2, cy2) end
end
local function grid_toCellRect(cellSize, x,y,w,h)
local cx,cy = grid_toCell(cellSize, x, y)
local cr,cb = ceil((x+w) / cellSize), ceil((y+h) / cellSize)
return cx, cy, cr - cx + 1, cb - cy + 1
end
------------------------------------------
-- Responses
------------------------------------------
local touch = function(world, col, x,y,w,h, goalX, goalY, filter)
return col.touch.x, col.touch.y, {}, 0
end
local cross = function(world, col, x,y,w,h, goalX, goalY, filter)
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
local slide = function(world, col, x,y,w,h, goalX, goalY, filter)
goalX = goalX or x
goalY = goalY or y
local tch, move = col.touch, col.move
if move.x ~= 0 or move.y ~= 0 then
if col.normal.x ~= 0 then
goalX = tch.x
else
goalY = tch.y
end
end
col.slide = {x = goalX, y = goalY}
x,y = tch.x, tch.y
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
local bounce = function(world, col, x,y,w,h, goalX, goalY, filter)
goalX = goalX or x
goalY = goalY or y
local tch, move = col.touch, col.move
local tx, ty = tch.x, tch.y
local bx, by = tx, ty
if move.x ~= 0 or move.y ~= 0 then
local bnx, bny = goalX - tx, goalY - ty
if col.normal.x == 0 then bny = -bny else bnx = -bnx end
bx, by = tx + bnx, ty + bny
end
col.bounce = {x = bx, y = by}
x,y = tch.x, tch.y
goalX, goalY = bx, by
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
------------------------------------------
-- World
------------------------------------------
local World = {}
local World_mt = {__index = World}
-- Private functions and methods
local function sortByWeight(a,b) return a.weight < b.weight end
local function sortByTiAndDistance(a,b)
if a.ti == b.ti then
local ir, ar, br = a.itemRect, a.otherRect, b.otherRect
local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h)
local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h)
return ad < bd
end
return a.ti < b.ti
end
local function addItemToCell(self, item, cx, cy)
self.rows[cy] = self.rows[cy] or setmetatable({}, {__mode = 'v'})
local row = self.rows[cy]
row[cx] = row[cx] or {itemCount = 0, x = cx, y = cy, items = setmetatable({}, {__mode = 'k'})}
local cell = row[cx]
self.nonEmptyCells[cell] = true
if not cell.items[item] then
cell.items[item] = true
cell.itemCount = cell.itemCount + 1
end
end
local function removeItemFromCell(self, item, cx, cy)
local row = self.rows[cy]
if not row or not row[cx] or not row[cx].items[item] then return false end
local cell = row[cx]
cell.items[item] = nil
cell.itemCount = cell.itemCount - 1
if cell.itemCount == 0 then
self.nonEmptyCells[cell] = nil
end
return true
end
local function getDictItemsInCellRect(self, cl,ct,cw,ch)
local items_dict = {}
for cy=ct,ct+ch-1 do
local row = self.rows[cy]
if row then
for cx=cl,cl+cw-1 do
local cell = row[cx]
if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling
for item,_ in pairs(cell.items) do
items_dict[item] = true
end
end
end
end
end
return items_dict
end
local function getCellsTouchedBySegment(self, x1,y1,x2,y2)
local cells, cellsLen, visited = {}, 0, {}
grid_traverse(self.cellSize, x1,y1,x2,y2, function(cx, cy)
local row = self.rows[cy]
if not row then return end
local cell = row[cx]
if not cell or visited[cell] then return end
visited[cell] = true
cellsLen = cellsLen + 1
cells[cellsLen] = cell
end)
return cells, cellsLen
end
local function getInfoAboutItemsTouchedBySegment(self, x1,y1, x2,y2, filter)
local cells, len = getCellsTouchedBySegment(self, x1,y1,x2,y2)
local cell, rect, l,t,w,h, ti1,ti2, tii0,tii1
local visited, itemInfo, itemInfoLen = {},{},0
for i=1,len do
cell = cells[i]
for item in pairs(cell.items) do
if not visited[item] then
visited[item] = true
if (not filter or filter(item)) then
rect = self.rects[item]
l,t,w,h = rect.x,rect.y,rect.w,rect.h
ti1,ti2 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, 0, 1)
if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then
-- the sorting is according to the t of an infinite line, not the segment
tii0,tii1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, -math.huge, math.huge)
itemInfoLen = itemInfoLen + 1
itemInfo[itemInfoLen] = {item = item, ti1 = ti1, ti2 = ti2, weight = min(tii0,tii1)}
end
end
end
end
end
table.sort(itemInfo, sortByWeight)
return itemInfo, itemInfoLen
end
local function getResponseByName(self, name)
local response = self.responses[name]
if not response then
error(('Unknown collision type: %s (%s)'):format(name, type(name)))
end
return response
end
-- Misc Public Methods
function World:addResponse(name, response)
self.responses[name] = response
end
function World:project(item, x,y,w,h, goalX, goalY, filter)
assertIsRect(x,y,w,h)
goalX = goalX or x
goalY = goalY or y
filter = filter or defaultFilter
local collisions, len = {}, 0
local visited = {}
if item ~= nil then visited[item] = true end
-- This could probably be done with less cells using a polygon raster over the cells instead of a
-- bounding rect of the whole movement. Conditional to building a queryPolygon method
local tl, tt = min(goalX, x), min(goalY, y)
local tr, tb = max(goalX + w, x+w), max(goalY + h, y+h)
local tw, th = tr-tl, tb-tt
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, tl,tt,tw,th)
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
for other,_ in pairs(dictItemsInCellRect) do
if not visited[other] then
visited[other] = true
local responseName = filter(item, other)
if responseName then
local ox,oy,ow,oh = self:getRect(other)
local col = rect_detectCollision(x,y,w,h, ox,oy,ow,oh, goalX, goalY)
if col then
col.other = other
col.item = item
col.type = responseName
len = len + 1
collisions[len] = col
end
end
end
end
table.sort(collisions, sortByTiAndDistance)
return collisions, len
end
function World:countCells()
local count = 0
for _,row in pairs(self.rows) do
for _,_ in pairs(row) do
count = count + 1
end
end
return count
end
function World:hasItem(item)
return not not self.rects[item]
end
function World:getItems()
local items, len = {}, 0
for item,_ in pairs(self.rects) do
len = len + 1
items[len] = item
end
return items, len
end
function World:countItems()
local len = 0
for _ in pairs(self.rects) do len = len + 1 end
return len
end
function World:getRect(item)
local rect = self.rects[item]
if not rect then
error('Item ' .. tostring(item) .. ' must be added to the world before getting its rect. Use world:add(item, x,y,w,h) to add it first.')
end
return rect.x, rect.y, rect.w, rect.h
end
function World:toWorld(cx, cy)
return grid_toWorld(self.cellSize, cx, cy)
end
function World:toCell(x,y)
return grid_toCell(self.cellSize, x, y)
end
--- Query methods
function World:queryRect(x,y,w,h, filter)
assertIsRect(x,y,w,h)
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
local items, len = {}, 0
local rect
for item,_ in pairs(dictItemsInCellRect) do
rect = self.rects[item]
if (not filter or filter(item))
and rect_isIntersecting(x,y,w,h, rect.x, rect.y, rect.w, rect.h)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:queryPoint(x,y, filter)
local cx,cy = self:toCell(x,y)
local dictItemsInCellRect = getDictItemsInCellRect(self, cx,cy,1,1)
local items, len = {}, 0
local rect
for item,_ in pairs(dictItemsInCellRect) do
rect = self.rects[item]
if (not filter or filter(item))
and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:querySegment(x1, y1, x2, y2, filter)
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
local items = {}
for i=1, len do
items[i] = itemInfo[i].item
end
return items, len
end
function World:querySegmentWithCoords(x1, y1, x2, y2, filter)
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
local dx, dy = x2-x1, y2-y1
local info, ti1, ti2
for i=1, len do
info = itemInfo[i]
ti1 = info.ti1
ti2 = info.ti2
info.weight = nil
info.x1 = x1 + dx * ti1
info.y1 = y1 + dy * ti1
info.x2 = x1 + dx * ti2
info.y2 = y1 + dy * ti2
end
return itemInfo, len
end
--- Main methods
function World:add(item, x,y,w,h)
local rect = self.rects[item]
if rect then
error('Item ' .. tostring(item) .. ' added to the world twice.')
end
assertIsRect(x,y,w,h)
self.rects[item] = {x=x,y=y,w=w,h=h}
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
for cy = ct, ct+ch-1 do
for cx = cl, cl+cw-1 do
addItemToCell(self, item, cx, cy)
end
end
return item
end
function World:remove(item)
local x,y,w,h = self:getRect(item)
self.rects[item] = nil
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
for cy = ct, ct+ch-1 do
for cx = cl, cl+cw-1 do
removeItemFromCell(self, item, cx, cy)
end
end
end
function World:update(item, x2,y2,w2,h2)
local x1,y1,w1,h1 = self:getRect(item)
w2,h2 = w2 or w1, h2 or h1
assertIsRect(x2,y2,w2,h2)
if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then
local cellSize = self.cellSize
local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x1,y1,w1,h1)
local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2)
if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then
local cr1, cb1 = cl1+cw1-1, ct1+ch1-1
local cr2, cb2 = cl2+cw2-1, ct2+ch2-1
local cyOut
for cy = ct1, cb1 do
cyOut = cy < ct2 or cy > cb2
for cx = cl1, cr1 do
if cyOut or cx < cl2 or cx > cr2 then
removeItemFromCell(self, item, cx, cy)
end
end
end
for cy = ct2, cb2 do
cyOut = cy < ct1 or cy > cb1
for cx = cl2, cr2 do
if cyOut or cx < cl1 or cx > cr1 then
addItemToCell(self, item, cx, cy)
end
end
end
end
local rect = self.rects[item]
rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2
end
end
function World:move(item, goalX, goalY, filter)
local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter)
self:update(item, actualX, actualY)
return actualX, actualY, cols, len
end
function World:check(item, goalX, goalY, filter)
filter = filter or defaultFilter
local visited = {[item] = true}
local visitedFilter = function(itm, other)
if visited[other] then return false end
return filter(itm, other)
end
local cols, len = {}, 0
local x,y,w,h = self:getRect(item)
local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter)
while projected_len > 0 do
local col = projected_cols[1]
len = len + 1
cols[len] = col
visited[col.other] = true
local response = getResponseByName(self, col.type)
goalX, goalY, projected_cols, projected_len = response(
self,
col,
x, y, w, h,
goalX, goalY,
visitedFilter
)
end
return goalX, goalY, cols, len
end
-- Public library functions
bump.newWorld = function(cellSize)
cellSize = cellSize or 64
assertIsPositiveNumber(cellSize, 'cellSize')
local world = setmetatable({
cellSize = cellSize,
rects = {},
rows = {},
nonEmptyCells = {},
responses = {}
}, World_mt)
world:addResponse('touch', touch)
world:addResponse('cross', cross)
world:addResponse('slide', slide)
world:addResponse('bounce', bounce)
return world
end
bump.rect = {
getNearestCorner = rect_getNearestCorner,
getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices,
getDiff = rect_getDiff,
containsPoint = rect_containsPoint,
isIntersecting = rect_isIntersecting,
getSquareDistance = rect_getSquareDistance,
detectCollision = rect_detectCollision
}
bump.responses = {
touch = touch,
cross = cross,
slide = slide,
bounce = bounce
}
return bump

257
src/lib/hump/camera.lua Normal file
View File

@ -0,0 +1,257 @@
--[[
Copyright (c) 2010-2015 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
local _PATH = (...):match('^(.*[%./])[^%.%/]+$') or ''
local cos, sin = math.cos, math.sin
local camera = {}
camera.__index = camera
-- Movement interpolators (for camera locking/windowing)
camera.smooth = {}
function camera.smooth.none()
return function(dx,dy) return dx,dy end
end
function camera.smooth.linear(speed)
assert(type(speed) == "number", "Invalid parameter: speed = "..tostring(speed))
return function(dx,dy, s)
-- normalize direction
local d = math.sqrt(dx*dx+dy*dy)
local dts = math.min((s or speed) * love.timer.getDelta(), d) -- prevent overshooting the goal
if d > 0 then
dx,dy = dx/d, dy/d
end
return dx*dts, dy*dts
end
end
function camera.smooth.damped(stiffness)
assert(type(stiffness) == "number", "Invalid parameter: stiffness = "..tostring(stiffness))
return function(dx,dy, s)
local dts = love.timer.getDelta() * (s or stiffness)
return dx*dts, dy*dts
end
end
local function new(x,y, zoom, rot, smoother)
x,y = x or love.graphics.getWidth()/2, y or love.graphics.getHeight()/2
zoom = zoom or 1
rot = rot or 0
smoother = smoother or camera.smooth.none() -- for locking, see below
return setmetatable({x = x, y = y, scale = zoom, rot = rot, smoother = smoother}, camera)
end
function camera:lookAt(x,y)
self.x, self.y = x, y
return self
end
function camera:move(dx,dy)
self.x, self.y = self.x + dx, self.y + dy
return self
end
function camera:position()
return self.x, self.y
end
function camera:rotate(phi)
self.rot = self.rot + phi
return self
end
function camera:rotateTo(phi)
self.rot = phi
return self
end
function camera:zoom(mul)
self.scale = self.scale * mul
return self
end
function camera:zoomTo(zoom)
self.scale = zoom
return self
end
local function rotate(phi, x,y)
local c, s = cos(phi), sin(phi)
return c*x - s*y, s*x + c*y
end
-- (dx, dy) = (((x,y) - center) * (1 / cam.scale - 1 / cam.new_scale)):rotated(-cam.rot)
function camera:zoomInto(zoom, tx, ty, x, y, w, h)
local oldScale = self.scale
self:zoom(zoom)
x, y = x or 0, y or 0
w, h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
local cx, cy = x+w/2, y+h/2
local scalar = (1/oldScale - 1/self.scale)
local dx = (tx - cx) * scalar
local dy = (ty - cy) * scalar
self:move(rotate(-self.rot, dx, dy))
end
-- (dx, dy) = (((new_x, new_y) - (x, y)) / cam.scale):rotated(-cam.rot)
function camera:relativeMove(x, y)
local dx = x / self.scale
local dy = y / self.scale
self:move(rotate(-self.rot, dx, dy))
end
function camera:getViewport(x, y, w, h)
x,y = x or 0, y or 0
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
local corner1_x, corner1_y = self:worldCoords(0, 0, x, y, w, h)
local corner2_x, corner2_y = self:worldCoords(w, 0, x, y, w, h)
local corner3_x, corner3_y = self:worldCoords(0, h, x, y, w, h)
local corner4_x, corner4_y = self:worldCoords(w, h, x, y, w, h)
local left = math.min(corner1_x, corner2_x, corner3_x, corner4_x)
local top = math.min(corner1_y, corner2_y, corner3_y, corner4_y)
local right = math.max(corner1_x, corner2_x, corner3_x, corner4_x)
local bottom = math.max(corner1_y, corner2_y, corner3_y, corner4_y)
return left, top, right, bottom
end
function camera:attach(x,y,w,h, noclip)
x,y = x or 0, y or 0
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
self._sx,self._sy,self._sw,self._sh = love.graphics.getScissor()
if not noclip then
love.graphics.setScissor(x,y,w,h)
end
local cx,cy = x+w/2, y+h/2
love.graphics.push()
love.graphics.translate(cx, cy)
love.graphics.scale(self.scale)
love.graphics.rotate(self.rot)
love.graphics.translate(-self.x, -self.y)
end
function camera:detach()
love.graphics.pop()
love.graphics.setScissor(self._sx,self._sy,self._sw,self._sh)
end
function camera:draw(...)
local x,y,w,h,noclip,func
local nargs = select("#", ...)
if nargs == 1 then
func = ...
elseif nargs == 5 then
x,y,w,h,func = ...
elseif nargs == 6 then
x,y,w,h,noclip,func = ...
else
error("Invalid arguments to camera:draw()")
end
self:attach(x,y,w,h,noclip)
func()
self:detach()
end
-- world coordinates to camera coordinates
function camera:cameraCoords(x,y, ox,oy,w,h)
ox, oy = ox or 0, oy or 0
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
-- x,y = ((x,y) - (self.x, self.y)):rotated(self.rot) * self.scale + center
x, y = rotate(self.rot, x - self.x, y - self.y)
return x*self.scale + w/2 + ox, y*self.scale + h/2 + oy
end
-- camera coordinates to world coordinates
function camera:worldCoords(x,y, ox,oy,w,h)
ox, oy = ox or 0, oy or 0
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
-- x,y = (((x,y) - center) / self.scale):rotated(-self.rot) + (self.x,self.y)
x,y = (x - w/2 - ox) / self.scale, (y - h/2 - oy) / self.scale
x,y = rotate(-self.rot, x, y)
return x+self.x, y+self.y
end
function camera:mousePosition(ox,oy,w,h)
local mx,my = love.mouse.getPosition()
return self:worldCoords(mx,my, ox,oy,w,h)
end
-- camera scrolling utilities
function camera:lockX(x, smoother, ...)
local dx, dy = (smoother or self.smoother)(x - self.x, self.y, ...)
self.x = self.x + dx
return self
end
function camera:lockY(y, smoother, ...)
local dx, dy = (smoother or self.smoother)(self.x, y - self.y, ...)
self.y = self.y + dy
return self
end
function camera:lockPosition(x,y, smoother, ...)
return self:move((smoother or self.smoother)(x - self.x, y - self.y, ...))
end
function camera:lockWindow(x, y, x_min, x_max, y_min, y_max, smoother, ...)
-- figure out displacement in camera coordinates
x,y = self:cameraCoords(x,y)
local dx, dy = 0,0
if x < x_min then
dx = x - x_min
elseif x > x_max then
dx = x - x_max
end
if y < y_min then
dy = y - y_min
elseif y > y_max then
dy = y - y_max
end
-- transform displacement to movement in world coordinates
local c,s = cos(-self.rot), sin(-self.rot)
dx,dy = (c*dx - s*dy) / self.scale, (s*dx + c*dy) / self.scale
-- move
self:move((smoother or self.smoother)(dx,dy,...))
end
-- the module
return setmetatable({new = new, smooth = camera.smooth},
{__call = function(_, ...) return new(...) end})

98
src/lib/hump/class.lua Normal file
View File

@ -0,0 +1,98 @@
--[[
Copyright (c) 2010-2013 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
local function include_helper(to, from, seen)
if from == nil then
return to
elseif type(from) ~= 'table' then
return from
elseif seen[from] then
return seen[from]
end
seen[from] = to
for k,v in pairs(from) do
k = include_helper({}, k, seen) -- keys might also be tables
if to[k] == nil then
to[k] = include_helper({}, v, seen)
end
end
return to
end
-- deeply copies `other' into `class'. keys in `other' that are already
-- defined in `class' are omitted
local function include(class, other)
return include_helper(class, other, {})
end
-- returns a deep copy of `other'
local function clone(other)
return setmetatable(include({}, other), getmetatable(other))
end
local function new(class)
-- mixins
class = class or {} -- class can be nil
local inc = class.__includes or {}
if getmetatable(inc) then inc = {inc} end
for _, other in ipairs(inc) do
if type(other) == "string" then
other = _G[other]
end
include(class, other)
end
-- class implementation
class.__index = class
class.init = class.init or class[1] or function() end
class.include = class.include or include
class.clone = class.clone or clone
-- constructor call
return setmetatable(class, {__call = function(c, ...)
local o = setmetatable({}, c)
o:init(...)
return o
end})
end
-- interface for cross class-system compatibility (see https://github.com/bartbes/Class-Commons).
if class_commons ~= false and not common then
common = {}
function common.class(name, prototype, parent)
return new{__includes = {prototype, parent}}
end
function common.instance(class, ...)
return class(...)
end
end
-- the module
return setmetatable({new = new, include = include, clone = clone},
{__call = function(_,...) return new(...) end})

113
src/lib/hump/gamestate.lua Normal file
View File

@ -0,0 +1,113 @@
--[[
Copyright (c) 2010-2013 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
local function __NULL__() end
-- default gamestate produces error on every callback
local state_init = setmetatable({leave = __NULL__},
{__index = function() error("Gamestate not initialized. Use Gamestate.switch()") end})
local stack = {state_init}
local initialized_states = setmetatable({}, {__mode = "k"})
local state_is_dirty = true
local GS = {}
function GS.new(t) return t or {} end -- constructor - deprecated!
local function change_state(stack_offset, to, ...)
local pre = stack[#stack]
-- initialize only on first call
;(initialized_states[to] or to.init or __NULL__)(to)
initialized_states[to] = __NULL__
stack[#stack+stack_offset] = to
state_is_dirty = true
return (to.enter or __NULL__)(to, pre, ...)
end
function GS.switch(to, ...)
assert(to, "Missing argument: Gamestate to switch to")
assert(to ~= GS, "Can't call switch with colon operator")
;(stack[#stack].leave or __NULL__)(stack[#stack])
return change_state(0, to, ...)
end
function GS.push(to, ...)
assert(to, "Missing argument: Gamestate to switch to")
assert(to ~= GS, "Can't call push with colon operator")
return change_state(1, to, ...)
end
function GS.pop(...)
assert(#stack > 1, "No more states to pop!")
local pre, to = stack[#stack], stack[#stack-1]
stack[#stack] = nil
;(pre.leave or __NULL__)(pre)
state_is_dirty = true
return (to.resume or __NULL__)(to, pre, ...)
end
function GS.current()
return stack[#stack]
end
-- XXX: don't overwrite love.errorhandler by default:
-- this callback is different than the other callbacks
-- (see http://love2d.org/wiki/love.errorhandler)
-- overwriting thi callback can result in random crashes (issue #95)
local all_callbacks = { 'draw', 'update' }
-- fetch event callbacks from love.handlers
for k in pairs(love.handlers) do
all_callbacks[#all_callbacks+1] = k
end
function GS.registerEvents(callbacks)
local registry = {}
callbacks = callbacks or all_callbacks
for _, f in ipairs(callbacks) do
registry[f] = love[f] or __NULL__
love[f] = function(...)
registry[f](...)
return GS[f](...)
end
end
end
-- forward any undefined functions
setmetatable(GS, {__index = function(_, func)
-- call function only if at least one 'update' was called beforehand
-- (see issue #46)
if not state_is_dirty or func == 'update' then
state_is_dirty = false
return function(...)
return (stack[#stack][func] or __NULL__)(stack[#stack], ...)
end
end
return __NULL__
end})
return GS

102
src/lib/hump/signal.lua Normal file
View File

@ -0,0 +1,102 @@
--[[
Copyright (c) 2012-2013 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
local Registry = {}
Registry.__index = function(self, key)
return Registry[key] or (function()
local t = {}
rawset(self, key, t)
return t
end)()
end
function Registry:register(s, f)
self[s][f] = f
return f
end
function Registry:emit(s, ...)
for f in pairs(self[s]) do
f(...)
end
end
function Registry:remove(s, ...)
local f = {...}
for i = 1,select('#', ...) do
self[s][f[i]] = nil
end
end
function Registry:clear(...)
local s = {...}
for i = 1,select('#', ...) do
self[s[i]] = {}
end
end
function Registry:emitPattern(p, ...)
for s in pairs(self) do
if s:match(p) then self:emit(s, ...) end
end
end
function Registry:registerPattern(p, f)
for s in pairs(self) do
if s:match(p) then self:register(s, f) end
end
return f
end
function Registry:removePattern(p, ...)
for s in pairs(self) do
if s:match(p) then self:remove(s, ...) end
end
end
function Registry:clearPattern(p)
for s in pairs(self) do
if s:match(p) then self[s] = {} end
end
end
-- instancing
function Registry.new()
return setmetatable({}, Registry)
end
-- default instance
local default = Registry.new()
-- module forwards calls to default instance
local module = {}
for k in pairs(Registry) do
if k ~= "__index" then
module[k] = function(...) return default[k](default, ...) end
end
end
return setmetatable(module, {__call = Registry.new})

215
src/lib/hump/timer.lua Normal file
View File

@ -0,0 +1,215 @@
--[[
Copyright (c) 2010-2013 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
local Timer = {}
Timer.__index = Timer
local function _nothing_() end
local function updateTimerHandle(handle, dt)
-- handle: {
-- time = <number>,
-- after = <function>,
-- during = <function>,
-- limit = <number>,
-- count = <number>,
-- }
handle.time = handle.time + dt
handle.during(dt, math.max(handle.limit - handle.time, 0))
while handle.time >= handle.limit and handle.count > 0 do
if handle.after(handle.after) == false then
handle.count = 0
break
end
handle.time = handle.time - handle.limit
handle.count = handle.count - 1
end
end
function Timer:update(dt)
-- timers may create new timers, which leads to undefined behavior
-- in pairs() - so we need to put them in a different table first
local to_update = {}
for handle in pairs(self.functions) do
to_update[handle] = handle
end
for handle in pairs(to_update) do
if self.functions[handle] then
updateTimerHandle(handle, dt)
if handle.count == 0 then
self.functions[handle] = nil
end
end
end
end
function Timer:during(delay, during, after)
local handle = { time = 0, during = during, after = after or _nothing_, limit = delay, count = 1 }
self.functions[handle] = true
return handle
end
function Timer:after(delay, func)
return self:during(delay, _nothing_, func)
end
function Timer:every(delay, after, count)
local count = count or math.huge -- exploit below: math.huge - 1 = math.huge
local handle = { time = 0, during = _nothing_, after = after, limit = delay, count = count }
self.functions[handle] = true
return handle
end
function Timer:cancel(handle)
self.functions[handle] = nil
end
function Timer:clear()
self.functions = {}
end
function Timer:script(f)
local co = coroutine.wrap(f)
co(function(t)
self:after(t, co)
coroutine.yield()
end)
end
Timer.tween = setmetatable({
-- helper functions
out = function(f) -- 'rotates' a function
return function(s, ...) return 1 - f(1-s, ...) end
end,
chain = function(f1, f2) -- concatenates two functions
return function(s, ...) return (s < .5 and f1(2*s, ...) or 1 + f2(2*s-1, ...)) * .5 end
end,
-- useful tweening functions
linear = function(s) return s end,
quad = function(s) return s*s end,
cubic = function(s) return s*s*s end,
quart = function(s) return s*s*s*s end,
quint = function(s) return s*s*s*s*s end,
sine = function(s) return 1-math.cos(s*math.pi/2) end,
expo = function(s) return 2^(10*(s-1)) end,
circ = function(s) return 1 - math.sqrt(1-s*s) end,
back = function(s,bounciness)
bounciness = bounciness or 1.70158
return s*s*((bounciness+1)*s - bounciness)
end,
bounce = function(s) -- magic numbers ahead
local a,b = 7.5625, 1/2.75
return math.min(a*s^2, a*(s-1.5*b)^2 + .75, a*(s-2.25*b)^2 + .9375, a*(s-2.625*b)^2 + .984375)
end,
elastic = function(s, amp, period)
amp, period = amp and math.max(1, amp) or 1, period or .3
return (-amp * math.sin(2*math.pi/period * (s-1) - math.asin(1/amp))) * 2^(10*(s-1))
end,
}, {
-- register new tween
__call = function(tween, self, len, subject, target, method, after, ...)
-- recursively collects fields that are defined in both subject and target into a flat list
local function tween_collect_payload(subject, target, out)
for k,v in pairs(target) do
local ref = subject[k]
assert(type(v) == type(ref), 'Type mismatch in field "'..k..'".')
if type(v) == 'table' then
tween_collect_payload(ref, v, out)
else
local ok, delta = pcall(function() return (v-ref)*1 end)
assert(ok, 'Field "'..k..'" does not support arithmetic operations')
out[#out+1] = {subject, k, delta}
end
end
return out
end
method = tween[method or 'linear'] -- see __index
local payload, t, args = tween_collect_payload(subject, target, {}), 0, {...}
local last_s = 0
return self:during(len, function(dt)
t = t + dt
local s = method(math.min(1, t/len), unpack(args))
local ds = s - last_s
last_s = s
for _, info in ipairs(payload) do
local ref, key, delta = unpack(info)
ref[key] = ref[key] + delta * ds
end
end, after)
end,
-- fetches function and generated compositions for method `key`
__index = function(tweens, key)
if type(key) == 'function' then return key end
assert(type(key) == 'string', 'Method must be function or string.')
if rawget(tweens, key) then return rawget(tweens, key) end
local function construct(pattern, f)
local method = rawget(tweens, key:match(pattern))
if method then return f(method) end
return nil
end
local out, chain = rawget(tweens,'out'), rawget(tweens,'chain')
return construct('^in%-([^-]+)$', function(...) return ... end)
or construct('^out%-([^-]+)$', out)
or construct('^in%-out%-([^-]+)$', function(f) return chain(f, out(f)) end)
or construct('^out%-in%-([^-]+)$', function(f) return chain(out(f), f) end)
or error('Unknown interpolation method: ' .. key)
end})
-- Timer instancing
function Timer.new()
return setmetatable({functions = {}, tween = Timer.tween}, Timer)
end
-- default instance
local default = Timer.new()
-- module forwards calls to default instance
local module = {}
for k in pairs(Timer) do
if k ~= "__index" then
module[k] = function(...) return default[k](default, ...) end
end
end
module.tween = setmetatable({}, {
__index = Timer.tween,
__newindex = function(k,v) Timer.tween[k] = v end,
__call = function(t, ...) return default:tween(...) end,
})
return setmetatable(module, {__call = Timer.new})

89
src/lib/log.lua Normal file
View File

@ -0,0 +1,89 @@
--
-- log.lua
--
-- Copyright (c) 2016 rxi
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
--
local log = { _version = "0.1.0" }
log.usecolor = true
log.outfile = nil
log.level = "trace"
local modes = {
{ name = "trace", color = "\27[34m", },
{ name = "debug", color = "\27[36m", },
{ name = "info", color = "\27[32m", },
{ name = "warn", color = "\27[33m", },
{ name = "error", color = "\27[31m", },
{ name = "fatal", color = "\27[35m", },
}
local levels = {}
for i, v in ipairs(modes) do
levels[v.name] = i
end
local round = function(x, increment)
increment = increment or 1
x = x / increment
return (x > 0 and math.floor(x + .5) or math.ceil(x - .5)) * increment
end
local _tostring = tostring
local tostring = function(...)
local t = {}
for i = 1, select('#', ...) do
local x = select(i, ...)
if type(x) == "number" then
x = round(x, .01)
end
t[#t + 1] = _tostring(x)
end
return table.concat(t, " ")
end
local dateFormat = "%H:%M:%S %d/%m/%Y"
for i, x in ipairs(modes) do
local nameupper = x.name:upper()
log[x.name] = function(...)
-- Return early if we're below the log level
if i < levels[log.level] then
return
end
local msg = tostring(...)
-- local info = debug.getinfo(2, "Sl")
-- local lineinfo = info.short_src .. ":" .. info.currentline
-- Output to console
print(string.format("%s[%-6s%s]%s %s",
log.usecolor and x.color or "",
nameupper,
os.date(dateFormat),
log.usecolor and "\27[0m" or "",
msg))
-- Output to log file
if log.outfile then
local fp = io.open(log.outfile, "a")
local str = string.format("[%-6s%s] %s\n", nameupper, os.date(dateFormat), msg)
fp:write(str)
fp:close()
end
end
end
return log

688
src/lib/lume.lua Normal file
View File

@ -0,0 +1,688 @@
--
-- lume
--
-- Copyright (c) 2018 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
local lume = {_version = "2.3.0"}
local pairs, ipairs = pairs, ipairs
local type, assert, unpack = type, assert, unpack or table.unpack
local tostring, tonumber = tostring, tonumber
local math_floor = math.floor
local math_ceil = math.ceil
local math_atan2 = math.atan2 or math.atan
local math_sqrt = math.sqrt
local math_abs = math.abs
local noop = function()
end
local identity = function(x)
return x
end
local patternescape = function(str)
return str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1")
end
local absindex = function(len, i)
return i < 0 and (len + i + 1) or i
end
local iscallable = function(x)
if type(x) == "function" then return true end
local mt = getmetatable(x)
return mt and mt.__call ~= nil
end
local getiter = function(x)
if lume.isarray(x) then
return ipairs
elseif type(x) == "table" then
return pairs
end
error("expected table", 3)
end
local iteratee = function(x)
if x == nil then return identity end
if iscallable(x) then return x end
if type(x) == "table" then
return function(z)
for k, v in pairs(x) do if z[k] ~= v then return false end end
return true
end
end
return function(z)
return z[x]
end
end
function lume.clamp(x, min, max)
return x < min and min or (x > max and max or x)
end
function lume.round(x, increment)
if increment then return lume.round(x / increment) * increment end
return x >= 0 and math_floor(x + .5) or math_ceil(x - .5)
end
function lume.sign(x)
return x < 0 and -1 or 1
end
function lume.lerp(a, b, amount)
return a + (b - a) * lume.clamp(amount, 0, 1)
end
function lume.smooth(a, b, amount)
local t = lume.clamp(amount, 0, 1)
local m = t * t * (3 - 2 * t)
return a + (b - a) * m
end
function lume.pingpong(x)
return 1 - math_abs(1 - x % 2)
end
function lume.distance(x1, y1, x2, y2, squared)
local dx = x1 - x2
local dy = y1 - y2
local s = dx * dx + dy * dy
return squared and s or math_sqrt(s)
end
function lume.angle(x1, y1, x2, y2)
return math_atan2(y2 - y1, x2 - x1)
end
function lume.vector(angle, magnitude)
return math.cos(angle) * magnitude, math.sin(angle) * magnitude
end
function lume.random(a, b)
if not a then a, b = 0, 1 end
if not b then b = 0 end
return a + math.random() * (b - a)
end
function lume.randomchoice(t)
return t[math.random(#t)]
end
function lume.weightedchoice(t)
local sum = 0
for _, v in pairs(t) do
assert(v >= 0, "weight value less than zero")
sum = sum + v
end
assert(sum ~= 0, "all weights are zero")
local rnd = lume.random(sum)
for k, v in pairs(t) do
if rnd < v then return k end
rnd = rnd - v
end
end
function lume.isarray(x)
return type(x) == "table" and x[1] ~= nil
end
function lume.push(t, ...)
local n = select("#", ...)
for i = 1, n do t[#t + 1] = select(i, ...) end
return ...
end
function lume.remove(t, x)
local iter = getiter(t)
for i, v in iter(t) do
if v == x then
if lume.isarray(t) then
table.remove(t, i)
break
else
t[i] = nil
break
end
end
end
return x
end
function lume.clear(t)
local iter = getiter(t)
for k in iter(t) do t[k] = nil end
return t
end
function lume.extend(t, ...)
for i = 1, select("#", ...) do
local x = select(i, ...)
if x then for k, v in pairs(x) do t[k] = v end end
end
return t
end
function lume.shuffle(t)
local rtn = {}
for i = 1, #t do
local r = math.random(i)
if r ~= i then rtn[i] = rtn[r] end
rtn[r] = t[i]
end
return rtn
end
function lume.sort(t, comp)
local rtn = lume.clone(t)
if comp then
if type(comp) == "string" then
table.sort(rtn, function(a, b)
return a[comp] < b[comp]
end)
else
table.sort(rtn, comp)
end
else
table.sort(rtn)
end
return rtn
end
function lume.array(...)
local t = {}
for x in ... do t[#t + 1] = x end
return t
end
function lume.each(t, fn, ...)
local iter = getiter(t)
if type(fn) == "string" then
for _, v in iter(t) do v[fn](v, ...) end
else
for _, v in iter(t) do fn(v, ...) end
end
return t
end
function lume.map(t, fn)
fn = iteratee(fn)
local iter = getiter(t)
local rtn = {}
for k, v in iter(t) do rtn[k] = fn(v) end
return rtn
end
function lume.all(t, fn)
fn = iteratee(fn)
local iter = getiter(t)
for _, v in iter(t) do if not fn(v) then return false end end
return true
end
function lume.any(t, fn)
fn = iteratee(fn)
local iter = getiter(t)
for _, v in iter(t) do if fn(v) then return true end end
return false
end
function lume.reduce(t, fn, first)
local acc = first
local started = first and true or false
local iter = getiter(t)
for _, v in iter(t) do
if started then
acc = fn(acc, v)
else
acc = v
started = true
end
end
assert(started, "reduce of an empty table with no first value")
return acc
end
function lume.unique(t)
local rtn = {}
for k in pairs(lume.invert(t)) do rtn[#rtn + 1] = k end
return rtn
end
function lume.filter(t, fn, retainkeys)
fn = iteratee(fn)
local iter = getiter(t)
local rtn = {}
if retainkeys then
for k, v in iter(t) do if fn(v) then rtn[k] = v end end
else
for _, v in iter(t) do if fn(v) then rtn[#rtn + 1] = v end end
end
return rtn
end
function lume.reject(t, fn, retainkeys)
fn = iteratee(fn)
local iter = getiter(t)
local rtn = {}
if retainkeys then
for k, v in iter(t) do if not fn(v) then rtn[k] = v end end
else
for _, v in iter(t) do if not fn(v) then rtn[#rtn + 1] = v end end
end
return rtn
end
function lume.merge(...)
local rtn = {}
for i = 1, select("#", ...) do
local t = select(i, ...)
local iter = getiter(t)
for k, v in iter(t) do rtn[k] = v end
end
return rtn
end
function lume.concat(...)
local rtn = {}
for i = 1, select("#", ...) do
local t = select(i, ...)
if t ~= nil then
local iter = getiter(t)
for _, v in iter(t) do rtn[#rtn + 1] = v end
end
end
return rtn
end
function lume.find(t, value)
local iter = getiter(t)
for k, v in iter(t) do if v == value then return k end end
return nil
end
function lume.match(t, fn)
fn = iteratee(fn)
local iter = getiter(t)
for k, v in iter(t) do if fn(v) then return v, k end end
return nil
end
function lume.count(t, fn)
local count = 0
local iter = getiter(t)
if fn then
fn = iteratee(fn)
for _, v in iter(t) do if fn(v) then count = count + 1 end end
else
if lume.isarray(t) then return #t end
for _ in iter(t) do count = count + 1 end
end
return count
end
function lume.slice(t, i, j)
i = i and absindex(#t, i) or 1
j = j and absindex(#t, j) or #t
local rtn = {}
for x = i < 1 and 1 or i, j > #t and #t or j do rtn[#rtn + 1] = t[x] end
return rtn
end
function lume.first(t, n)
if not n then return t[1] end
return lume.slice(t, 1, n)
end
function lume.last(t, n)
if not n then return t[#t] end
return lume.slice(t, -n, -1)
end
function lume.invert(t)
local rtn = {}
for k, v in pairs(t) do rtn[v] = k end
return rtn
end
function lume.pick(t, ...)
local rtn = {}
for i = 1, select("#", ...) do
local k = select(i, ...)
rtn[k] = t[k]
end
return rtn
end
function lume.keys(t)
local rtn = {}
local iter = getiter(t)
for k in iter(t) do rtn[#rtn + 1] = k end
return rtn
end
function lume.clone(t)
local rtn = {}
for k, v in pairs(t) do rtn[k] = v end
return rtn
end
function lume.fn(fn, ...)
assert(iscallable(fn), "expected a function as the first argument")
local args = {...}
return function(...)
local a = lume.concat(args, {...})
return fn(unpack(a))
end
end
function lume.once(fn, ...)
local f = lume.fn(fn, ...)
local done = false
return function(...)
if done then return end
done = true
return f(...)
end
end
local memoize_fnkey = {}
local memoize_nil = {}
function lume.memoize(fn)
local cache = {}
return function(...)
local c = cache
for i = 1, select("#", ...) do
local a = select(i, ...) or memoize_nil
c[a] = c[a] or {}
c = c[a]
end
c[memoize_fnkey] = c[memoize_fnkey] or {fn(...)}
return unpack(c[memoize_fnkey])
end
end
function lume.combine(...)
local n = select("#", ...)
if n == 0 then return noop end
if n == 1 then
local fn = select(1, ...)
if not fn then return noop end
assert(iscallable(fn), "expected a function or nil")
return fn
end
local funcs = {}
for i = 1, n do
local fn = select(i, ...)
if fn ~= nil then
assert(iscallable(fn), "expected a function or nil")
funcs[#funcs + 1] = fn
end
end
return function(...)
for _, f in ipairs(funcs) do f(...) end
end
end
function lume.call(fn, ...)
if fn then return fn(...) end
end
function lume.time(fn, ...)
local start = os.clock()
local rtn = {fn(...)}
return (os.clock() - start), unpack(rtn)
end
local lambda_cache = {}
function lume.lambda(str)
if not lambda_cache[str] then
local args, body = str:match([[^([%w,_ ]-)%->(.-)$]])
assert(args and body, "bad string lambda")
local s = "return function(" .. args .. ")\nreturn " .. body .. "\nend"
lambda_cache[str] = lume.dostring(s)
end
return lambda_cache[str]
end
local serialize
local serialize_map = {
["boolean"] = tostring,
["nil"] = tostring,
["string"] = function(v)
return string.format("%q", v)
end,
["number"] = function(v)
if v ~= v then
return "0/0" -- nan
elseif v == 1 / 0 then
return "1/0" -- inf
elseif v == -1 / 0 then
return "-1/0"
end -- -inf
return tostring(v)
end,
["table"] = function(t, stk)
stk = stk or {}
if stk[t] then error("circular reference") end
local rtn = {}
stk[t] = true
for k, v in pairs(t) do rtn[#rtn + 1] = "[" .. serialize(k, stk) .. "]=" .. serialize(v, stk) end
stk[t] = nil
return "{" .. table.concat(rtn, ",") .. "}"
end
}
setmetatable(serialize_map, {
__index = function(_, k)
error("unsupported serialize type: " .. k)
end
})
serialize = function(x, stk)
return serialize_map[type(x)](x, stk)
end
function lume.serialize(x)
return serialize(x)
end
function lume.deserialize(str)
return lume.dostring("return " .. str)
end
function lume.split(str, sep)
if not sep then
return lume.array(str:gmatch("([%S]+)"))
else
assert(sep ~= "", "empty separator")
local psep = patternescape(sep)
return lume.array((str .. sep):gmatch("(.-)(" .. psep .. ")"))
end
end
function lume.trim(str, chars)
if not chars then return str:match("^[%s]*(.-)[%s]*$") end
chars = patternescape(chars)
return str:match("^[" .. chars .. "]*(.-)[" .. chars .. "]*$")
end
function lume.wordwrap(str, limit)
limit = limit or 72
local check
if type(limit) == "number" then
check = function(s)
return #s >= limit
end
else
check = limit
end
local rtn = {}
local line = ""
for word, spaces in str:gmatch("(%S+)(%s*)") do
local s = line .. word
if check(s) then
table.insert(rtn, line .. "\n")
line = word
else
line = s
end
for c in spaces:gmatch(".") do
if c == "\n" then
table.insert(rtn, line .. "\n")
line = ""
else
line = line .. c
end
end
end
table.insert(rtn, line)
return table.concat(rtn)
end
function lume.format(str, vars)
if not vars then return str end
local f = function(x)
return tostring(vars[x] or vars[tonumber(x)] or "{" .. x .. "}")
end
return (str:gsub("{(.-)}", f))
end
function lume.trace(...)
local info = debug.getinfo(2, "Sl")
local t = {info.short_src .. ":" .. info.currentline .. ":"}
for i = 1, select("#", ...) do
local x = select(i, ...)
if type(x) == "number" then x = string.format("%g", lume.round(x, .01)) end
t[#t + 1] = tostring(x)
end
print(table.concat(t, " "))
end
function lume.dostring(str)
return assert((loadstring or load)(str))()
end
function lume.uuid()
local fn = function(x)
local r = math.random(16) - 1
r = (x == "x") and (r + 1) or (r % 4) + 9
return ("0123456789abcdef"):sub(r, r)
end
return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
end
function lume.hotswap(modname)
local oldglobal = lume.clone(_G)
local updated = {}
local function update(old, new)
if updated[old] then return end
updated[old] = true
local oldmt, newmt = getmetatable(old), getmetatable(new)
if oldmt and newmt then update(oldmt, newmt) end
for k, v in pairs(new) do
if type(v) == "table" then
update(old[k], v)
else
old[k] = v
end
end
end
local err = nil
local function onerror(e)
for k in pairs(_G) do _G[k] = oldglobal[k] end
err = lume.trim(e)
end
local ok, oldmod = pcall(require, modname)
oldmod = ok and oldmod or nil
xpcall(function()
package.loaded[modname] = nil
local newmod = require(modname)
if type(oldmod) == "table" then update(oldmod, newmod) end
for k, v in pairs(oldglobal) do
if v ~= _G[k] and type(v) == "table" then
update(v, _G[k])
_G[k] = v
end
end
end, onerror)
package.loaded[modname] = oldmod
if err then return nil, err end
return oldmod
end
local ripairs_iter = function(t, i)
i = i - 1
local v = t[i]
if v ~= nil then return i, v end
end
function lume.ripairs(t)
return ripairs_iter, t, (#t + 1)
end
function lume.color(str, mul)
mul = mul or 1
local r, g, b, a
r, g, b = str:match("#(%x%x)(%x%x)(%x%x)")
if r then
r = tonumber(r, 16) / 0xff
g = tonumber(g, 16) / 0xff
b = tonumber(b, 16) / 0xff
a = 1
elseif str:match("rgba?%s*%([%d%s%.,]+%)") then
local f = str:gmatch("[%d.]+")
r = (f() or 0) / 0xff
g = (f() or 0) / 0xff
b = (f() or 0) / 0xff
a = f() or 1
else
error(("bad color string '%s'"):format(str))
end
return r * mul, g * mul, b * mul, a * mul
end
local chain_mt = {}
chain_mt.__index = lume.map(lume.filter(lume, iscallable, true), function(fn)
return function(self, ...)
self._value = fn(self._value, ...)
return self
end
end)
chain_mt.__index.result = function(x)
return x._value
end
function lume.chain(value)
return setmetatable({_value = value}, chain_mt)
end
setmetatable(lume, {
__call = function(_, ...)
return lume.chain(...)
end
})
return lume

546
src/lib/nata.lua Normal file
View File

@ -0,0 +1,546 @@
--- Entity management for Lua.
-- @module nata
local nata = {
_VERSION = 'Nata v0.3.3',
_DESCRIPTION = 'Entity management for Lua.',
_URL = 'https://github.com/tesselode/nata',
_LICENSE = [[
MIT License
Copyright (c) 2020 Andrew Minnich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
}
-- gets the error level needed to make an error appear
-- in the user's code, not the library code
local function getUserErrorLevel()
local source = debug.getinfo(1).source
local level = 1
while debug.getinfo(level).source == source do
level = level + 1
end
--[[
we return level - 1 here and not just level
because the level was calculated one function
deeper than the function that will actually
use this value. if we produced an error *inside*
this function, level would be correct, but
for the function calling this function, level - 1
is correct.
]]
return level - 1
end
-- gets the name of the function that the user called
-- that eventually caused an error
local function getUserCalledFunctionName()
return debug.getinfo(getUserErrorLevel() - 1).name
end
local function checkCondition(condition, message)
if condition then return end
error(message, getUserErrorLevel())
end
-- changes a list of types into a human-readable phrase
-- i.e. string, table, number -> "string, table, or number"
local function getAllowedTypesText(...)
local numberOfArguments = select('#', ...)
if numberOfArguments >= 3 then
local text = ''
for i = 1, numberOfArguments - 1 do
text = text .. string.format('%s, ', select(i, ...))
end
text = text .. string.format('or %s', select(numberOfArguments, ...))
return text
elseif numberOfArguments == 2 then
return string.format('%s or %s', select(1, ...), select(2, ...))
end
return select(1, ...)
end
-- checks if an argument is of the correct type, and if not,
-- throws a "bad argument" error consistent with the ones
-- lua and love produce
local function checkArgument(argumentIndex, argument, ...)
for i = 1, select('#', ...) do
-- allow tables with the __call metamethod to be treated like functions
if select(i, ...) == 'function' then
if type(argument) == 'table' and getmetatable(argument).__call then
return
end
end
if type(argument) == select(i, ...) then return end
end
error(
string.format(
"bad argument #%i to '%s' (expected %s, got %s)",
argumentIndex,
getUserCalledFunctionName(),
getAllowedTypesText(...),
type(argument)
),
getUserErrorLevel()
)
end
local function checkOptionalArgument(argumentIndex, argument, ...)
if argument == nil then return end
checkArgument(argumentIndex, argument, ...)
end
local function removeByValue(t, v)
for i = #t, 1, -1 do
if t[i] == v then table.remove(t, i) end
end
end
local function entityHasKeys(entity, keys)
for _, key in ipairs(keys) do
if not entity[key] then return false end
end
return true
end
local function filterEntity(entity, filter)
if type(filter) == 'table' then
return entityHasKeys(entity, filter)
elseif type(filter) == 'function' then
return filter(entity)
end
return true
end
--- Defines the behaviors of a system.
--
-- There's no constructor for SystemDefinitions. Rather, you simply
-- define a table with functions that correspond to events. These
-- events can be named anything you like. Below are the built-in events
-- that the pool will automatically call.
-- @type SystemDefinition
--- Called when the pool is first created.
-- @function SystemDefinition:init
-- @param ... additional arguments that were passed to `nata.new`.
--- Called when an entity is added to the pool.
-- @function SystemDefinition:add
-- @tparam table e the entity that was added
--- Called when an entity is removed from the pool.
-- @function SystemDefinition:remove
-- @tparam table e the entity that was removed
--- Called when an entity is added to a group.
-- @function SystemDefinition:addToGroup
-- @string groupName the name of the group that the entity was added to
-- @tparam table e the entity that was added
--- Called when an entity is removed from a group.
-- @function SystemDefinition:removeFromGroup
-- @string groupName the name of the group that the entity was removed from
-- @tparam table e the entity that was removed
--- Responds to events in a pool.
--
-- Systems are not created directly. They're created by the @{Pool}
-- according to the @{SystemDefinition}s passed to `nata.new`.
-- Each system instance inherits all of the functions of its
-- @{SystemDefinition}.
-- @type System
--- The @{Pool} that this system is running on.
-- @tfield Pool pool
--- Manages a subset of entities.
-- @type Group
--- The filter that defines which entities are added to this group.
-- Can be either:
--
-- - A list of required keys
-- - A function that takes the entity as the first argument
-- and returns true if the entity should be added to the group
-- @tfield[opt] table|function filter
--- A function that specifies how the entities in this group should be sorted.
-- Has the same requirements as the function argument to Lua's built-in `table.sort`.
-- @tfield[opt] function sort
--- A list of all the entities in the group.
-- @tfield table entities
--- A set of all the entities in the group.
-- @tfield table hasEntity
-- @usage
-- print(pool.groups.physical.hasEntity[e]) -- prints "true" if the entity is in the "physical" group, or "nil" if not
--- Manages entities in a game world.
-- @type Pool
local Pool = {}
Pool.__index = Pool
--- A list of all the entities in the pool.
-- @tfield table entities
--- A set of all the entities in the pool.
-- @tfield table hasEntity
-- @usage
-- print(pool.hasEntity[e]) -- prints "true" if the entity is in the pool, or "nil" if not
--- A dictionary of the @{Group}s in the pool.
-- @tfield table groups
--- A field containing any data you want.
-- @field data
---
function Pool:_validateOptions(options)
checkOptionalArgument(1, options, 'table')
if not options then return end
if options.groups then
checkCondition(type(options.groups) == 'table', "groups must be a table")
for groupName, groupOptions in pairs(options.groups) do
checkCondition(type(groupOptions) == 'table',
string.format("options for group '$s' must be a table", groupName))
local filter = groupOptions.filter
if filter ~= nil then
checkCondition(type(filter) == 'table' or type(filter) == 'function',
string.format("filter for group '%s' must be a table or function", groupName))
end
local sort = groupOptions.sort
if sort ~= nil then
checkCondition(type(sort) == 'function',
string.format("sort for group '%s' must be a function", groupName))
end
end
end
if options.systems then
checkCondition(type(options.systems) == 'table', "systems must be a table")
for _, system in ipairs(options.systems) do
checkCondition(type(system) == 'table', "all systems must be tables")
end
end
end
function Pool:_init(options, ...)
self:_validateOptions(options)
options = options or {}
-- entities that will be added to the pool on the next flush
self._queue = {}
-- a temporary table for entities that will be added to the pool
-- on the current flush (see Pool.flush for more details)
self._entitiesToFlush = {}
self.entities = {}
self.hasEntity = {}
self.groups = {}
self._systems = {}
self._events = {}
self.data = options.data or {}
local groups = options.groups or {}
local systems = options.systems or {nata.oop()}
for groupName, groupOptions in pairs(groups) do
self.groups[groupName] = {
filter = groupOptions.filter,
sort = groupOptions.sort,
entities = {},
hasEntity = {},
}
end
for _, systemDefinition in ipairs(systems) do
local system = setmetatable({
pool = self,
}, {__index = systemDefinition})
table.insert(self._systems, system)
end
self:emit('init', ...)
end
--- Queues an entity to be added to the pool.
-- @tparam table entity the entity to add
-- @treturn table the queued entity
function Pool:queue(entity)
table.insert(self._queue, entity)
return entity
end
--- Adds the queued entities to the pool. Entities are added
-- in the order they were queued.
function Pool:flush()
--[[
Move the currently queued entities to a temporary
table. This way, if an add/addToGroup/removeToGroup
event emission leads to another entity being queued,
it will be saved for the next flush, rather than
adding entities to the table we're in the middle
of iterating over, which would lead to an array with
holes and screw everything up.
]]
for i = 1, #self._queue do
local entity = self._queue[i]
self._entitiesToFlush[i] = entity
self._queue[i] = nil
end
for i = 1, #self._entitiesToFlush do
local entity = self._entitiesToFlush[i]
-- check if the entity belongs in each group and
-- add it to/remove it from the group as needed
for groupName, group in pairs(self.groups) do
if filterEntity(entity, group.filter) then
if not group.hasEntity[entity] then
table.insert(group.entities, entity)
group.hasEntity[entity] = true
self:emit('addToGroup', groupName, entity)
end
if group.sort then group._needsResort = true end
elseif group.hasEntity[entity] then
removeByValue(group.entities, entity)
group.hasEntity[entity] = nil
self:emit('removeFromGroup', groupName, entity)
end
end
-- add the entity to the pool if it hasn't been added already
if not self.hasEntity[entity] then
table.insert(self.entities, entity)
self.hasEntity[entity] = true
self:emit('add', entity)
end
self._entitiesToFlush[i] = nil
end
-- re-sort groups
for _, group in pairs(self.groups) do
if group._needsResort then
table.sort(group.entities, group.sort)
group._needsResort = nil
end
end
end
--- Removes entities from the pool.
-- @tparam function f the condition upon which an entity should be
-- removed. The function should take an entity as the first argument
-- and return `true` if the entity should be removed.
function Pool:remove(f)
checkArgument(1, f, 'function')
for groupName, group in pairs(self.groups) do
for i = #group.entities, 1, -1 do
local entity = group.entities[i]
if f(entity) then
self:emit('removeFromGroup', groupName, entity)
table.remove(group.entities, i)
group.hasEntity[entity] = nil
end
end
end
for i = #self.entities, 1, -1 do
local entity = self.entities[i]
if f(entity) then
self:emit('remove', entity)
table.remove(self.entities, i)
self.hasEntity[entity] = nil
end
end
end
--- Registers a function to be called when an event is emitted.
-- @string event the event to listen for
-- @tparam function f the function to call
-- @treturn function the function that was registered
function Pool:on(event, f)
checkCondition(event ~= nil, "event cannot be nil")
checkArgument(2, f, 'function')
self._events[event] = self._events[event] or {}
table.insert(self._events[event], f)
return f
end
--- Unregisters a function from an event.
-- @string event the event to unregister the function from
-- @tparam function f the function to unregister
function Pool:off(event, f)
checkCondition(event ~= nil, "event cannot be nil")
checkArgument(2, f, 'function')
if self._events[event] then
removeByValue(self._events[event], f)
end
end
--- Emits an event. The `system[event]` function will be called
-- for each system that has it, and functions registered
-- to the event will be called as well.
-- @string event the event to emit
-- @param ... additional arguments to pass to the functions that are called
function Pool:emit(event, ...)
checkCondition(event ~= nil, "event cannot be nil")
for _, system in ipairs(self._systems) do
if type(system[event]) == 'function' then
system[event](system, ...)
end
end
if self._events[event] then
for _, f in ipairs(self._events[event]) do
f(...)
end
end
end
--- Gets this pool's instance of a system.
-- @tparam SystemDefinition systemDefinition the system class to get the instance of
-- @treturn System the instance of the system running in this pool
function Pool:getSystem(systemDefinition)
checkArgument(1, systemDefinition, 'table')
for _, system in ipairs(self._systems) do
if getmetatable(system).__index == systemDefinition then
return system
end
end
end
---
-- @section end
local function validateOopOptions(options)
checkOptionalArgument(1, options, 'table')
if not options then return end
if options.include then
checkCondition(type(options.include) == 'table', "include must be a table")
end
if options.exclude then
checkCondition(type(options.exclude) == 'table', "exclude must be a table")
end
end
--- Defines the behavior of an OOP system.
-- @type OopOptions
-- @see nata.oop
--- A list of events to forward to entities. If not defined,
-- the system will forward all events to entities (except
-- for the ones in the exclude list).
-- @tfield table include
--- A list of events *not* to forward to entities.
-- @tfield table exclude
--- The name of the group of entities to forward events to.
-- If not defined, the system will forward events to all entities.
-- @tfield string group
---
-- @section end
--- Creates a new OOP system definition.
-- An OOP system, upon receiving an event, will call
-- the function of the same name on each entity it monitors
-- (if it exists). This facilitates a more traditional, OOP-style
-- entity management, where you loop over a table of entities and
-- call update and draw functions on them.
-- @tparam[opt] OopOptions options how to set up the OOP system
-- @treturn SystemDefinition the new OOP system definition
function nata.oop(options)
validateOopOptions(options)
local group = options and options.group
local include, exclude
if options and options.include then
include = {}
for _, event in ipairs(options.include) do
include[event] = true
end
end
if options and options.exclude then
exclude = {}
for _, event in ipairs(options.exclude) do
exclude[event] = true
end
end
return setmetatable({_cache = {}}, {
__index = function(t, event)
t._cache[event] = t._cache[event] or function(self, ...)
local shouldCallEvent = true
if include and not include[event] then shouldCallEvent = false end
if exclude and exclude[event] then shouldCallEvent = false end
if shouldCallEvent then
local entities
-- not using ternary here because if the group doesn't exist,
-- i'd rather it cause an error than just silently falling back
-- to the main entity pool
if group then
entities = self.pool.groups[group].entities
else
entities = self.pool.entities
end
for _, entity in ipairs(entities) do
if type(entity[event]) == 'function' then
entity[event](entity, ...)
end
end
end
end
return t._cache[event]
end
})
end
--- Defines the filter and sort function for a @{Group}.
-- @type GroupOptions
--- The filter that defines which entities are added to this group.
-- Can be either:
--
-- - A list of required keys
-- - A function that takes the entity as the first argument
-- and returns true if the entity should be added to the group
-- @tfield[opt] table|function filter
--- A function that specifies how the entities in this group should be sorted.
-- Has the same requirements as the function argument to Lua's built-in `table.sort`.
-- @tfield[opt] function sort
--- Defines the groups and systems for a @{Pool}.
-- @type PoolOptions
--- A dictionary of groups for the pool to have.
-- Each key is the name of the group, and each value
-- should be a @{GroupOptions} table.
-- @tfield[opt={}] table groups
--- A list of @{SystemDefinition}s for the pool to use.
-- @tfield[opt={nata.oop()}] table systems
--- An initial value to set @{Pool.data} to.
-- @field[opt={}] data
---
-- @section end
--- Creates a new @{Pool}.
-- @tparam[opt] PoolOptions options how to set up the pool
-- @param[opt] ... additional arguments to pass to the pool's init event
-- @treturn Pool the new pool
function nata.new(options, ...)
local pool = setmetatable({}, Pool)
pool:_init(options, ...)
return pool
end
return nata

475
src/lib/pprint.lua Normal file
View File

@ -0,0 +1,475 @@
local pprint = { VERSION = '0.1' }
local depth = 1
pprint.defaults = {
-- If set to number N, then limit table recursion to N deep.
depth_limit = false,
-- type display trigger, hide not useful datatypes by default
-- custom types are treated as table
show_nil = false,
show_boolean = true,
show_number = true,
show_string = true,
show_table = true,
show_function = false,
show_thread = false,
show_userdata = false,
-- additional display trigger
show_metatable = false, -- show metatable
show_all = false, -- override other show settings and show everything
use_tostring = true, -- use __tostring to print table if available
filter_function = nil, -- called like callback(value[,key, parent]), return truty value to hide
object_cache = 'local', -- cache blob and table to give it a id, 'local' cache per print, 'global' cache
-- per process, falsy value to disable (might cause infinite loop)
-- format settings
indent_size = 2, -- indent for each nested table level
level_width = 80, -- max width per indent level
wrap_string = true, -- wrap string when it's longer than level_width
wrap_array = false, -- wrap every array elements
sort_keys = true, -- sort table keys
}
local TYPES = {
['nil'] = 1, ['boolean'] = 2, ['number'] = 3, ['string'] = 4,
['table'] = 5, ['function'] = 6, ['thread'] = 7, ['userdata'] = 8
}
-- seems this is the only way to escape these, as lua don't know how to map char '\a' to 'a'
local ESCAPE_MAP = {
['\a'] = '\\a', ['\b'] = '\\b', ['\f'] = '\\f', ['\n'] = '\\n', ['\r'] = '\\r',
['\t'] = '\\t', ['\v'] = '\\v', ['\\'] = '\\\\',
}
-- generic utilities
local function escape(s)
s = s:gsub('([%c\\])', ESCAPE_MAP)
local dq = s:find('"')
local sq = s:find("'")
if dq and sq then
return s:gsub('"', '\\"'), '"'
elseif sq then
return s, '"'
else
return s, "'"
end
end
local function is_plain_key(key)
return type(key) == 'string' and key:match('^[%a_][%a%d_]*$')
end
local CACHE_TYPES = {
['table'] = true, ['function'] = true, ['thread'] = true, ['userdata'] = true
}
-- cache would be populated to be like:
-- {
-- function = { `fun1` = 1, _cnt = 1 }, -- object id
-- table = { `table1` = 1, `table2` = 2, _cnt = 2 },
-- visited_tables = { `table1` = 7, `table2` = 8 }, -- visit count
-- }
-- use weakrefs to avoid accidentall adding refcount
local function cache_apperance(obj, cache, option)
if not cache.visited_tables then
cache.visited_tables = setmetatable({}, {__mode = 'k'})
end
local t = type(obj)
-- TODO can't test filter_function here as we don't have the ix and key,
-- might cause different results?
-- respect show_xxx and filter_function to be consistent with print results
if (not TYPES[t] and not option.show_table)
or (TYPES[t] and not option['show_'..t]) then
return
end
if CACHE_TYPES[t] or TYPES[t] == nil then
if not cache[t] then
cache[t] = setmetatable({}, {__mode = 'k'})
cache[t]._cnt = 0
end
if not cache[t][obj] then
cache[t]._cnt = cache[t]._cnt + 1
cache[t][obj] = cache[t]._cnt
end
end
if t == 'table' or TYPES[t] == nil then
if cache.visited_tables[obj] == false then
-- already printed, no need to mark this and its children anymore
return
elseif cache.visited_tables[obj] == nil then
cache.visited_tables[obj] = 1
else
-- visited already, increment and continue
cache.visited_tables[obj] = cache.visited_tables[obj] + 1
return
end
for k, v in pairs(obj) do
cache_apperance(k, cache, option)
cache_apperance(v, cache, option)
end
local mt = getmetatable(obj)
if mt and option.show_metatable then
cache_apperance(mt, cache, option)
end
end
end
-- makes 'foo2' < 'foo100000'. string.sub makes substring anyway, no need to use index based method
local function str_natural_cmp(lhs, rhs)
while #lhs > 0 and #rhs > 0 do
local lmid, lend = lhs:find('%d+')
local rmid, rend = rhs:find('%d+')
if not (lmid and rmid) then return lhs < rhs end
local lsub = lhs:sub(1, lmid-1)
local rsub = rhs:sub(1, rmid-1)
if lsub ~= rsub then
return lsub < rsub
end
local lnum = tonumber(lhs:sub(lmid, lend))
local rnum = tonumber(rhs:sub(rmid, rend))
if lnum ~= rnum then
return lnum < rnum
end
lhs = lhs:sub(lend+1)
rhs = rhs:sub(rend+1)
end
return lhs < rhs
end
local function cmp(lhs, rhs)
local tleft = type(lhs)
local tright = type(rhs)
if tleft == 'number' and tright == 'number' then return lhs < rhs end
if tleft == 'string' and tright == 'string' then return str_natural_cmp(lhs, rhs) end
if tleft == tright then return str_natural_cmp(tostring(lhs), tostring(rhs)) end
-- allow custom types
local oleft = TYPES[tleft] or 9
local oright = TYPES[tright] or 9
return oleft < oright
end
-- setup option with default
local function make_option(option)
if option == nil then
option = {}
end
for k, v in pairs(pprint.defaults) do
if option[k] == nil then
option[k] = v
end
if option.show_all then
for t, _ in pairs(TYPES) do
option['show_'..t] = true
end
option.show_metatable = true
end
end
return option
end
-- override defaults and take effects for all following calls
function pprint.setup(option)
pprint.defaults = make_option(option)
end
-- format lua object into a string
function pprint.pformat(obj, option, printer)
option = make_option(option)
local buf = {}
local function default_printer(s)
table.insert(buf, s)
end
printer = printer or default_printer
local cache
if option.object_cache == 'global' then
-- steal the cache into a local var so it's not visible from _G or anywhere
-- still can't avoid user explicitly referentce pprint._cache but it shouldn't happen anyway
cache = pprint._cache or {}
pprint._cache = nil
elseif option.object_cache == 'local' then
cache = {}
end
local last = '' -- used for look back and remove trailing comma
local status = {
indent = '', -- current indent
len = 0, -- current line length
}
local wrapped_printer = function(s)
printer(last)
last = s
end
local function _indent(d)
status.indent = string.rep(' ', d + #(status.indent))
end
local function _n(d)
wrapped_printer('\n')
wrapped_printer(status.indent)
if d then
_indent(d)
end
status.len = 0
return true -- used to close bracket correctly
end
local function _p(s, nowrap)
status.len = status.len + #s
if not nowrap and status.len > option.level_width then
_n()
wrapped_printer(s)
status.len = #s
else
wrapped_printer(s)
end
end
local formatter = {}
local function format(v)
local f = formatter[type(v)]
f = f or formatter.table -- allow patched type()
if option.filter_function and option.filter_function(v, nil, nil) then
return ''
else
return f(v)
end
end
local function tostring_formatter(v)
return tostring(v)
end
local function number_formatter(n)
return n == math.huge and '[[math.huge]]' or tostring(n)
end
local function nop_formatter(v)
return ''
end
local function make_fixed_formatter(t, has_cache)
if has_cache then
return function (v)
return string.format('[[%s %d]]', t, cache[t][v])
end
else
return function (v)
return '[['..t..']]'
end
end
end
local function string_formatter(s, force_long_quote)
local s, quote = escape(s)
local quote_len = force_long_quote and 4 or 2
if quote_len + #s + status.len > option.level_width then
_n()
-- only wrap string when is longer than level_width
if option.wrap_string and #s + quote_len > option.level_width then
-- keep the quotes together
_p('[[')
while #s + status.len >= option.level_width do
local seg = option.level_width - status.len
_p(string.sub(s, 1, seg), true)
_n()
s = string.sub(s, seg+1)
end
_p(s) -- print the remaining parts
return ']]'
end
end
return force_long_quote and '[['..s..']]' or quote..s..quote
end
local function table_formatter(t)
if option.use_tostring then
local mt = getmetatable(t)
if mt and mt.__tostring then
return string_formatter(tostring(t), true)
end
end
local print_header_ix = nil
local ttype = type(t)
if option.object_cache then
local cache_state = cache.visited_tables[t]
local tix = cache[ttype][t]
-- FIXME should really handle `cache_state == nil`
-- as user might add things through filter_function
if cache_state == false then
-- already printed, just print the the number
return string_formatter(string.format('%s %d', ttype, tix), true)
elseif cache_state > 1 then
-- appeared more than once, print table header with number
print_header_ix = tix
cache.visited_tables[t] = false
else
-- appeared exactly once, print like a normal table
end
end
local limit = tonumber(option.depth_limit)
if limit and depth > limit then
if print_header_ix then
return string.format('[[%s %d]]...', ttype, print_header_ix)
end
return string_formatter(tostring(t), true)
end
local tlen = #t
local wrapped = false
_p('{')
_indent(option.indent_size)
_p(string.rep(' ', option.indent_size - 1))
if print_header_ix then
_p(string.format('--[[%s %d]] ', ttype, print_header_ix))
end
for ix = 1,tlen do
local v = t[ix]
if formatter[type(v)] == nop_formatter or
(option.filter_function and option.filter_function(v, ix, t)) then
-- pass
else
if option.wrap_array then
wrapped = _n()
end
depth = depth+1
_p(format(v)..', ')
depth = depth-1
end
end
-- hashmap part of the table, in contrast to array part
local function is_hash_key(k)
if type(k) ~= 'number' then
return true
end
local numkey = math.floor(tonumber(k))
if numkey ~= k or numkey > tlen or numkey <= 0 then
return true
end
end
local function print_kv(k, v, t)
-- can't use option.show_x as obj may contain custom type
if formatter[type(v)] == nop_formatter or
formatter[type(k)] == nop_formatter or
(option.filter_function and option.filter_function(v, k, t)) then
return
end
wrapped = _n()
if is_plain_key(k) then
_p(k, true)
else
_p('[')
-- [[]] type string in key is illegal, needs to add spaces inbetween
local k = format(k)
if string.match(k, '%[%[') then
_p(' '..k..' ', true)
else
_p(k, true)
end
_p(']')
end
_p(' = ', true)
depth = depth+1
_p(format(v), true)
depth = depth-1
_p(',', true)
end
if option.sort_keys then
local keys = {}
for k, _ in pairs(t) do
if is_hash_key(k) then
table.insert(keys, k)
end
end
table.sort(keys, cmp)
for _, k in ipairs(keys) do
print_kv(k, t[k], t)
end
else
for k, v in pairs(t) do
if is_hash_key(k) then
print_kv(k, v, t)
end
end
end
if option.show_metatable then
local mt = getmetatable(t)
if mt then
print_kv('__metatable', mt, t)
end
end
_indent(-option.indent_size)
-- make { } into {}
last = string.gsub(last, '^ +$', '')
-- peek last to remove trailing comma
last = string.gsub(last, ',%s*$', ' ')
if wrapped then
_n()
end
_p('}')
return ''
end
-- set formatters
formatter['nil'] = option.show_nil and tostring_formatter or nop_formatter
formatter['boolean'] = option.show_boolean and tostring_formatter or nop_formatter
formatter['number'] = option.show_number and number_formatter or nop_formatter -- need to handle math.huge
formatter['function'] = option.show_function and make_fixed_formatter('function', option.object_cache) or nop_formatter
formatter['thread'] = option.show_thread and make_fixed_formatter('thread', option.object_cache) or nop_formatter
formatter['userdata'] = option.show_userdata and make_fixed_formatter('userdata', option.object_cache) or nop_formatter
formatter['string'] = option.show_string and string_formatter or nop_formatter
formatter['table'] = option.show_table and table_formatter or nop_formatter
if option.object_cache then
-- needs to visit the table before start printing
cache_apperance(obj, cache, option)
end
_p(format(obj))
printer(last) -- close the buffered one
-- put cache back if global
if option.object_cache == 'global' then
pprint._cache = cache
end
return table.concat(buf)
end
-- pprint all the arguments
function pprint.pprint( ... )
local args = {...}
-- select will get an accurate count of array len, counting trailing nils
local len = select('#', ...)
for ix = 1,len do
pprint.pformat(args[ix], nil, io.write)
io.write('\n')
end
end
setmetatable(pprint, {
__call = function (_, ...)
pprint.pprint(...)
end
})
return pprint

522
src/lib/ripple.lua Normal file
View File

@ -0,0 +1,522 @@
local ripple = {
_VERSION = 'Ripple',
_DESCRIPTION = 'Audio helpers for LÖVE.',
_URL = 'https://github.com/tesselode/ripple',
_LICENSE = [[
MIT License
Copyright (c) 2019 Andrew Minnich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
}
local unpack = unpack or table.unpack -- luacheck: ignore
--[[
Represents an object that:
- can have tags applied
- has a volume
- can have effects applied
Tags, instances, and sounds are all taggable.
Note that not all taggable objects have children - tags and sounds
do, but instances do not.
]]
local Taggable = {}
--[[
Gets the total volume of this object given its own volume
and the volume of each of its tags.
]]
function Taggable:_getTotalVolume()
local volume = self.volume
for tag, _ in pairs(self._tags) do
volume = volume * tag:_getTotalVolume()
end
return volume
end
--[[
Gets all the effects that should be applied to this object given
its own effects and the effects of each of its tags. The object's
own effects will override tag effects.
Note: currently, if multiple tags define settings for the same effect,
the final result is undefined, as taggable objects use pairs to iterate
through the tags, which iterates in an undefined order.
]]
function Taggable:_getAllEffects()
local effects = {}
for tag, _ in pairs(self._tags) do
for name, filterSettings in pairs(tag:_getAllEffects()) do
effects[name] = filterSettings
end
end
for name, filterSettings in pairs(self._effects) do
effects[name] = filterSettings
end
return effects
end
--[[
A callback that is called when anything happens that could
lead to a change in the object's total volume.
]]
function Taggable:_onChangeVolume() end
--[[
A callback that is called when anything happens that could
change which effects are applied to the object.
]]
function Taggable:_onChangeEffects() end
function Taggable:_setVolume(volume)
self._volume = volume
self:_onChangeVolume()
end
--[[
_tag, _untag, and _setEffect are analogous to the
similarly named public API functions (see below), but
they don't call the _onChangeVolume and _onChangeEffects
callbacks. This allows me to have finer control over when
to call those callbacks, so I can set multiple tags and
effects without needlessly calling the callbacks for each
one.
]]
function Taggable:_tag(tag)
self._tags[tag] = true
tag._children[self] = true
end
function Taggable:_untag(tag)
self._tags[tag] = nil
tag._children[self] = nil
end
function Taggable:_setEffect(name, filterSettings)
if filterSettings == nil then filterSettings = true end
self._effects[name] = filterSettings
end
--[[
Given an options table, initializes the object's volume,
tags, and effects.
]]
function Taggable:_setOptions(options)
self.volume = options and options.volume or 1
-- reset tags
for tag in pairs(self._tags) do
self:_untag(tag)
end
-- apply new tags
if options and options.tags then
for _, tag in ipairs(options.tags) do
self:_tag(tag)
end
end
-- reset effects
for name in pairs(self._effects) do
self._effects[name] = nil
end
-- apply new effects
if options and options.effects then
for name, filterSettings in pairs(options.effects) do
self:_setEffect(name, filterSettings)
end
end
-- update final volume and effects
self:_onChangeVolume()
self:_onChangeEffects()
end
function Taggable:tag(...)
for i = 1, select('#', ...) do
local tag = select(i, ...)
self:_tag(tag)
end
self:_onChangeVolume()
self:_onChangeEffects()
end
function Taggable:untag(...)
for i = 1, select('#', ...) do
local tag = select(i, ...)
self:_untag(tag)
end
self:_onChangeVolume()
self:_onChangeEffects()
end
--[[
Sets an effect for this object. filterSettings can be the following types:
- table - the effect will be enabled with the filter settings given in the table
- true/nil - the effect will be enabled with no filter
- false - the effect will be explicitly disabled, overriding effect settings
from a parent sound or tag
]]
function Taggable:setEffect(name, filterSettings)
self:_setEffect(name, filterSettings)
self:_onChangeEffects()
end
function Taggable:removeEffect(name)
self._effects[name] = nil
self:_onChangeEffects()
end
function Taggable:getEffect(name)
return self._effects[name]
end
function Taggable:__index(key)
if key == 'volume' then
return self._volume
end
return Taggable[key]
end
function Taggable:__newindex(key, value)
if key == 'volume' then
self:_setVolume(value)
else
rawset(self, key, value)
end
end
--[[
Represents a tag that can be applied to sounds,
instances of sounds, or other tags.
]]
local Tag = {__newindex = Taggable.__newindex}
function Tag:__index(key)
if Tag[key] then return Tag[key] end
return Taggable.__index(self, key)
end
function Tag:_onChangeVolume()
-- tell objects using this tag about a potential
-- volume change
for child, _ in pairs(self._children) do
child:_onChangeVolume()
end
end
function Tag:_onChangeEffect()
-- tell objects using this tag about a potential
-- effect change
for child, _ in pairs(self._children) do
child:_onChangeEffect()
end
end
-- Pauses all the sounds and instances tagged with this tag.
function Tag:pause(fadeDuration)
for child, _ in pairs(self._children) do
child:pause(fadeDuration)
end
end
-- Resumes all the sounds and instances tagged with this tag.
function Tag:resume(fadeDuration)
for child, _ in pairs(self._children) do
child:resume(fadeDuration)
end
end
-- Stops all the sounds and instances tagged with this tag.
function Tag:stop(fadeDuration)
for child, _ in pairs(self._children) do
child:stop(fadeDuration)
end
end
function ripple.newTag(options)
local tag = setmetatable({
_effects = {},
_tags = {},
_children = {},
}, Tag)
tag:_setOptions(options)
return tag
end
-- Represents a specific occurrence of a sound.
local Instance = {}
function Instance:__index(key)
if key == 'pitch' then
return self._source:getPitch()
elseif key == 'loop' then
return self._source:isLooping()
elseif Instance[key] then
return Instance[key]
end
return Taggable.__index(self, key)
end
function Instance:__newindex(key, value)
if key == 'pitch' then
self._source:setPitch(value)
elseif key == 'loop' then
self._source:setLooping(value)
else
Taggable.__newindex(self, key, value)
end
end
function Instance:_getTotalVolume()
local volume = Taggable._getTotalVolume(self)
-- apply sound volume as well as tag/self volumes
volume = volume * self._sound:_getTotalVolume()
-- apply fade volume
volume = volume * self._fadeVolume
return volume
end
function Instance:_getAllEffects()
local effects = {}
for tag, _ in pairs(self._tags) do
for name, filterSettings in pairs(tag:_getAllEffects()) do
effects[name] = filterSettings
end
end
-- apply sound effects as well as tag/self effects
for name, filterSettings in pairs(self._sound:_getAllEffects()) do
effects[name] = filterSettings
end
for name, filterSettings in pairs(self._effects) do
effects[name] = filterSettings
end
return effects
end
function Instance:_onChangeVolume()
-- update the source's volume
self._source:setVolume(self:_getTotalVolume())
end
function Instance:_onChangeEffects()
-- get the list of effects that should be applied
local effects = self:_getAllEffects()
for name, filterSettings in pairs(effects) do
-- remember which effects are currently applied to the source
if filterSettings == false then
self._appliedEffects[name] = nil
else
self._appliedEffects[name] = true
end
if filterSettings == true then
self._source:setEffect(name)
else
self._source:setEffect(name, filterSettings)
end
end
-- remove effects that are currently applied but shouldn't be anymore
for name in pairs(self._appliedEffects) do
if not effects[name] then
self._source:setEffect(name, false)
self._appliedEffects[name] = nil
end
end
end
function Instance:_play(options)
if options and options.fadeDuration then
self._fadeVolume = 0
self._fadeSpeed = 1 / options.fadeDuration
else
self._fadeVolume = 1
end
self._fadeDirection = 1
self._afterFadingOut = false
self._paused = false
self:_setOptions(options)
self.pitch = options and options.pitch or 1
if options and options.loop ~= nil then
self.loop = options.loop
end
self._source:seek(options and options.seek or 0)
self._source:play()
end
function Instance:_update(dt)
-- fade in
if self._fadeDirection == 1 and self._fadeVolume < 1 then
self._fadeVolume = self._fadeVolume + self._fadeSpeed * dt
if self._fadeVolume > 1 then self._fadeVolume = 1 end
self:_onChangeVolume()
-- fade out
elseif self._fadeDirection == -1 and self._fadeVolume > 0 then
self._fadeVolume = self._fadeVolume - self._fadeSpeed * dt
if self._fadeVolume < 0 then
self._fadeVolume = 0
-- pause or stop after fading out
if self._afterFadingOut == 'pause' then
self:pause()
elseif self._afterFadingOut == 'stop' then
self:stop()
end
end
self:_onChangeVolume()
end
end
function Instance:setPosition(x, y, z)
self._source:setPosition(x, y, z or 0)
end
function Instance:isStopped()
return (not self._source:isPlaying()) and (not self._paused)
end
function Instance:pause(fadeDuration)
if fadeDuration and not self._paused then
self._fadeDirection = -1
self._fadeSpeed = 1 / fadeDuration
self._afterFadingOut = 'pause'
else
self._source:pause()
self._paused = true
end
end
function Instance:resume(fadeDuration)
if fadeDuration then
if self._paused then
self._fadeVolume = 0
self:_onChangeVolume()
end
self._fadeDirection = 1
self._fadeSpeed = 1 / fadeDuration
end
self._source:play()
self._paused = false
end
function Instance:stop(fadeDuration)
if fadeDuration and not self._paused then
self._fadeDirection = -1
self._fadeSpeed = 1 / fadeDuration
self._afterFadingOut = 'stop'
else
self._source:stop()
self._paused = false
end
end
-- Represents a sound that can be played.
local Sound = {}
function Sound:__index(key)
if key == 'loop' then
return self._source:isLooping()
elseif Sound[key] then
return Sound[key]
end
return Taggable.__index(self, key)
end
function Sound:__newindex(key, value)
if key == 'loop' then
self._source:setLooping(value)
for _, instance in ipairs(self._instances) do
instance.loop = value
end
else
Taggable.__newindex(self, key, value)
end
end
function Sound:_onChangeVolume()
-- tell instances about potential volume changes
for _, instance in ipairs(self._instances) do
instance:_onChangeVolume()
end
end
function Sound:_onChangeEffects()
-- tell instances about potential effect changes
for _, instance in ipairs(self._instances) do
instance:_onChangeEffects()
end
end
function Sound:play(options)
-- reuse a stopped instance if one is available
for _, instance in ipairs(self._instances) do
if instance:isStopped() then
instance:_play(options)
return instance
end
end
-- otherwise, create a brand new one
local instance = setmetatable({
_sound = self,
_source = self._source:clone(),
_effects = {},
_tags = {},
_appliedEffects = {},
}, Instance)
table.insert(self._instances, instance)
instance:_play(options)
return instance
end
function Sound:pause(fadeDuration)
for _, instance in ipairs(self._instances) do
instance:pause(fadeDuration)
end
end
function Sound:resume(fadeDuration)
for _, instance in ipairs(self._instances) do
instance:resume(fadeDuration)
end
end
function Sound:stop(fadeDuration)
for _, instance in ipairs(self._instances) do
instance:stop(fadeDuration)
end
end
function Sound:update(dt)
for _, instance in ipairs(self._instances) do
instance:_update(dt)
end
end
function ripple.newSound(source, options)
local sound = setmetatable({
_source = source,
_effects = {},
_tags = {},
_instances = {},
}, Sound)
sound:_setOptions(options)
if options and options.loop then sound.loop = true end
return sound
end
return ripple

503
src/lib/vivid.lua Normal file
View File

@ -0,0 +1,503 @@
-- The MIT License (MIT)
--
-- Copyright (c) 2015 WetDesertRock
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.
local vivid = {}
local math_min = math.min
local math_max = math.max
local math_atan = math.atan
local math_pi = math.pi
local math_sqrt = math.sqrt
local math_atan2 = math.atan2
local math_cos = math.cos
local math_sin = math.sin
local math_rad = math.rad
local math_abs = math.abs
-- Helper function to mimic the LOVE API, allows you to pass a table or four arguments
local function getColorArgs(first, ...)
if type(first) == "table" then
return unpack(first)
end
return first, ...
end
--RGB to `colorspace`
function vivid.RGBtoHSL(...)
local r,g,b,a = getColorArgs(...)
local min = math_min(r,g,b)
local max = math_max(r,g,b)
local delta_max = max-min
local h,s,l
l = (max+min)/2
if (delta_max == 0) then
h,s = 0,0
else
if l < 0.5 then
s = delta_max / (max+min)
else
s = delta_max / (2-max-min)
end
local delta_r = (((max-r)/6) + (delta_max/2)) / delta_max
local delta_g = (((max-g)/6) + (delta_max/2)) / delta_max
local delta_b = (((max-b)/6) + (delta_max/2)) / delta_max
if r == max then
h = delta_b - delta_g
elseif g == max then
h = (1/3) + delta_r - delta_b
elseif b == max then
h = (2/3) + delta_b - delta_r
end
if h < 0 then h = h + 1 end
if h > 1 then h = h - 1 end
end
return h,s,l,a
end
function vivid.RGBtoHSV(...)
local r,g,b,a = getColorArgs(...)
local min = math_min(r,g,b)
local max = math_max(r,g,b)
local delta_max = max-min
local h,s,v
v = max
if (delta_max == 0) then
h,s = 0,0
else
s = delta_max / max
local delta_r = (((max-r)/6) + (delta_max/2)) / delta_max
local delta_g = (((max-g)/6) + (delta_max/2)) / delta_max
local delta_b = (((max-b)/6) + (delta_max/2)) / delta_max
if r == max then
h = delta_b - delta_g
elseif g == max then
h = (1/3) + delta_r - delta_b
elseif b == max then
h = (2/3) + delta_b - delta_r
end
if h < 0 then h = h + 1 end
if h > 1 then h = h - 1 end
end
return h,s,v,a
end
function vivid.RGBtoXYZ(...)
--(Observer = 2°, Illuminant = D65)
local r,g,b,a = getColorArgs(...)
if r > 0.04045 then
r = ((r+0.055)/1.055)^2.4
else
r = r/12.92
end
if g > 0.04045 then
g = ((g+0.055)/1.055)^2.4
else
g = g/12.92
end
if b > 0.04045 then
b = ((b+0.055)/1.055)^2.4
else
b = b/12.92
end
r = r*100
g = g*100
b = b*100
local x = r * 0.4124 + g * 0.3576 + b * 0.1805
local y = r * 0.2126 + g * 0.7152 + b * 0.0722
local z = r * 0.0193 + g * 0.1192 + b * 0.9505
return x,y,z,a
end
function vivid.RGBtoLab(...)
return vivid.XYZtoLab(vivid.RGBtoXYZ(...))
end
function vivid.RGBtoLCH(...)
return vivid.LabtoLCH(vivid.RGBtoLab(...))
end
function vivid.RGBtoLuv(...)
return vivid.XYZtoLuv(vivid.RGBtoXYZ(...))
end
--`colorspace` to RGB
function vivid.HSLtoRGB(...)
local h,s,l,a = getColorArgs(...)
h,s,l = h,s,l
local r,g,b
if s == 0 then
r = l
g = l
b = l
else
local var1,var2
if l < 0.5 then
var2 = l*(1+s)
else
var2 = (l+s) - (s*l)
end
var1 = 2*l-var2
local function huetorgb(v1,v2,vh)
if vh < 0 then vh = vh+1 end
if vh > 1 then vh = vh-1 end
if 6*vh < 1 then return v1 + (v2-v1) * 6 * vh end
if 2*vh < 1 then return v2 end
if 3*vh < 2 then return v1 + (v2-v1) * ((2/3)-vh) * 6 end
return v1
end
r = huetorgb(var1, var2, h + (1/3))
g = huetorgb(var1, var2, h)
b = huetorgb(var1, var2, h - (1/3))
end
return r,g,b,a
end
function vivid.HSVtoRGB(...)
local h,s,v,a = getColorArgs(...)
local r,g,b
if s == 0 then
r = v
g = v
b = v
else
local varh,vari,var1,var2,var3
varh = h*6
if varh == 6 then varh = 0 end
vari = math.floor(varh)
var1 = v*(1-s)
var2 = v*(1-s*(varh-vari))
var3 = v*(1-s*(1-(varh-vari)))
if vari == 0 then
r = v
g = var3
b = var1
elseif vari == 1 then
r = var2
g = v
b = var1
elseif vari == 2 then
r = var1
g = v
b = var3
elseif vari == 3 then
r = var1
g = var2
b = v
elseif vari == 4 then
r = var3
g = var1
b = v
else
r = v
g = var1
b = var2
end
end
return r,g,b,a
end
function vivid.XYZtoRGB(...)
--(Observer = 2°, Illuminant = D65)
local x,y,z,a = getColorArgs(...)
x,y,z = x/100,y/100,z/100
local r,g,b
r = x * 3.2406 + y * -1.5372 + z * -0.4986
g = x * -0.9689 + y * 1.8758 + z * 0.0415
b = x * 0.0557 + y * -0.2040 + z * 1.0570
if r > 0.0031308 then
r = 1.055*(r^(1/2.4))-0.055
else
r = r*12.92
end
if g > 0.0031308 then
g = 1.055*(g^(1/2.4))-0.055
else
g = g*12.92
end
if b > 0.0031308 then
b = 1.055*(b^(1/2.4))-0.055
else
b = b*12.92
end
return r,g,b,a
end
function vivid.LabtoRGB(...)
return vivid.XYZtoRGB(vivid.LabtoXYZ(...))
end
function vivid.LCHtoRGB(...)
return vivid.LabtoRGB(vivid.LCHtoLab(...))
end
function vivid.LuvtoRGB(...)
return vivid.XYZtoRGB(vivid.LuvtoXYZ(...))
end
--Other conversions
local refx,refy,refz = 95.047,100.000,108.883
local refu = (4 * refx) / (refx + (15 * refy) + (3 * refz))
local refv = (9 * refy) / (refx + (15 * refy) + (3 * refz))
function vivid.XYZtoLab(...)
local x,y,z,alpha = getColorArgs(...)
local L,a,b
x,y,z = x/refx,y/refy,z/refz
if x > 0.008856 then
x = x^(1/3)
else
x = (7.787*x) + (16/116)
end
if y > 0.008856 then
y = y^(1/3)
else
y = (7.787*y) + (16/116)
end
if z > 0.008856 then
z = z^(1/3)
else
z = (7.787*z) + (16/116)
end
L = (116*y) - 16
a = 500*(x-y)
b = 200*(y-z)
return L,a,b,alpha
end
function vivid.LabtoXYZ(...)
local L,a,b,alpha = getColorArgs(...)
local y = (L+16) / 116
local x = a / 500 + y
local z = y - b / 200
if x^3 > 0.008856 then
x = x^3
else
x = (x - 16 / 116) / 7.787
end
if y^3 > 0.008856 then
y = y^3
else
y = (y - 16 / 116) / 7.787
end
if z^3 > 0.008856 then
z = z^3
else
z = (z - 16 / 116) / 7.787
end
return refx*x,refy*y,refz*z,alpha
end
function vivid.LabtoLCH(...)
local L,a,b,alpha = getColorArgs(...)
local C,H
H = math_atan2(b,a)
if H > 0 then
H = (H / math_pi) * 180
else
H = 360 - ( math_abs(H) / math_pi) * 180
end
C = math_sqrt(a ^ 2 + b ^ 2)
return L,C,H
end
function vivid.LCHtoLab(...)
local L,C,H,alpha = getColorArgs(...)
return L,math_cos(math_rad(H))*C,math_sin(math_rad(H))*C
end
function vivid.XYZtoLuv(...)
local x,y,z,alpha = getColorArgs(...)
local L,u,v
u = (4 * x) / (x + (15 * y) + (3 * z))
v = (9 * y) / (x + (15 * y) + (3 * z))
y = y/100
if y > 0.008856 then
y = y ^ (1/3)
else
y = (7.787 * y) + (16 / 116)
end
L = (116 * y) - 16
u = 13 * L * (u - refu)
v = 13 * L * (v - refv)
return L,u,v
end
function vivid.LuvtoXYZ(...)
local L,u,v,alpha = getColorArgs(...)
local x,y,z
y = (L + 16) / 116
if y^3 > 0.008856 then
y = y^3
else
y = (y - 16 / 116) / 7.787
end
u = u / (13 * L) + refu
v = v / (13 * L) + refv
y = y*100
x = -(9 * y * u) / ((u - 4 ) * v - u * v)
z = (9 * y - (15 * v * y) - (v * x)) / (3 * v)
return x,y,z
end
--Manipulations:
function vivid.lighten(amount, ...)
local h,s,l,a = vivid.RGBtoHSL(getColorArgs(...))
return vivid.HSLtoRGB(h,s,l+amount,a)
end
function vivid.darken(amount, ...)
local h,s,l,a = vivid.RGBtoHSL(getColorArgs(...))
return vivid.HSLtoRGB(h,s,l-amount,a)
end
function vivid.saturate(amount, ...)
local h,s,v,a = vivid.RGBtoHSV(getColorArgs(...))
return vivid.HSVtoRGB(h,s+amount,v,a)
end
function vivid.desaturate(amount, ...)
local h,s,v,a = vivid.RGBtoHSV(getColorArgs(...))
return vivid.HSVtoRGB(h,s-amount,v,a)
end
function vivid.hue(hue, ...)
local h,s,l,a = vivid.RGBtoHSL(getColorArgs(...))
return vivid.HSLtoRGB(hue,s,l,a)
end
function vivid.invert(...)
local r,g,b,a = getColorArgs(...)
return 1-r, 1-g, 1-b, a
end
function vivid.invertHue(...)
local h,s,l,a = vivid.RGBtoHSL(getColorArgs(...))
return vivid.HSLtoRGB(1-h,s,l,a)
end
--Spread
function vivid.HSLSpread(count,hoffset,s,l,a)
local incval = 1/count
local colors = {}
for i=0,count-1 do
table.insert(colors,{vivid.HSLtoRGB((i*incval+hoffset)%1,s,l,a)})
end
return colors
end
function vivid.HSVSpread(count,hoffset,s,v,a)
local incval = 1/count
local colors = {}
for i=0,count-1 do
table.insert(colors,{vivid.HSVtoRGB((i*incval+hoffset)%1,s,v,a)})
end
return colors
end
function vivid.LCHSpread(count,l,c,hoffset,a)
local incval = 360/count
local colors = {}
for i=0,count-1 do
table.insert(colors,{vivid.LCHtoRGB(l,c,(i*incval+hoffset)%360,a)})
end
return colors
end
--Wrap functions:
function vivid.wrapHSV(fn)
return function(...)
return fn(vivid.RGBtoHSV(...))
end
end
function vivid.wrapHSL(fn)
return function(...)
return fn(vivid.RGBtoHSL(...))
end
end
function vivid.wrapXYZ(fn)
return function(...)
return fn(vivid.RGBtoXYZ(...))
end
end
function vivid.wrapLab(fn)
return function(...)
return fn(vivid.RGBtoLab(...))
end
end
function vivid.wrapLCH(fn)
return function(...)
return fn(vivid.RGBtoLCH(...))
end
end
function vivid.wrapLuv(fn)
return function(...)
return fn(vivid.RGBtoLuv(...))
end
end
return vivid

9
src/main.lua Normal file
View File

@ -0,0 +1,9 @@
local Gamestate = require("lib.hump.gamestate")
function love.load()
love.math.setRandomSeed(love.timer.getTime())
math.randomseed(love.timer.getTime())
Gamestate.switch(require("states.main"))
Gamestate.registerEvents()
end

7
src/states/main.lua Normal file
View File

@ -0,0 +1,7 @@
local MainState = {}
function MainState:draw()
love.graphics.rectangle("fill", 20, 20, 100, 100)
end
return MainState