initial commit

This commit is contained in:
Max Cahill 2020-01-29 14:26:28 +11:00
commit 2f470cf7c7
9 changed files with 2053 additions and 0 deletions

407
functional.lua Normal file
View File

@ -0,0 +1,407 @@
--[[
functional programming facilities
]]
--collect all keys of a table into a sequence
function table.keys(t)
local r = {}
for k,v in pairs(t) do
table.insert(r, k)
end
return r
end
--collect all values of a table into a sequence
--(shallow copy if it's already a sequence)
function table.values(t)
local r = sequence:new()
for k,v in pairs(t) do
table.insert(r, v)
end
return r
end
--simple sequential iteration, f is called for all elements of t
--f can return non-nil to break the loop (and return the value)
function table.foreach(t, f)
for i,v in ipairs(t) do
local r = f(v, i)
if r ~= nil then
return r
end
end
end
--performs a left to right reduction of t using f, with o as the initial value
-- reduce({1, 2, 3}, f, 0) -> f(f(f(0, 1), 2), 3)
-- (but performed iteratively, so no stack smashing)
function table.reduce(t, f, o)
for i,v in ipairs(t) do
o = f(o, v)
end
return o
end
--maps a sequence {a, b, c} -> {f(a), f(b), f(c)}
-- (automatically drops any nils due to table.insert, which can be used to simultaneously map and filter)
function table.map(t, f)
local r = {}
for i,v in ipairs(t) do
local mapped = f(v, i)
if mapped ~= nil then
table.insert(r, mapped)
end
end
return r
end
--filters a sequence
function table.filter(t, f)
local r = {}
for i,v in ipairs(t) do
if f(v, i) then
table.insert(r, v)
end
end
return r
end
--partitions a sequence based on filter criteria
function table.partition(t, f)
local a = {}
local b = {}
for i,v in ipairs(t) do
if f(v, i) then
table.insert(a, v)
else
table.insert(b, v)
end
end
return a, b
end
--zips two sequences together into a new table, based on another function
--iteration limited by min(#t1, #t2)
--function receives arguments (t1, t2, i)
--nil results ignored
function table.zip(t1, t2, f)
local ret = {}
local limit = math.min(#t2, #t2)
for i=1, limit do
local v1 = t1[i]
local v2 = t2[i]
local zipped = f(v1, v2, i)
if zipped ~= nil then
table.insert(ret, zipped)
end
end
return ret
end
--return a copy of a sequence with all duplicates removed
-- causes a little "extra" gc churn; one table and one closure
-- as well as the copied deduped table
function table.dedupe(t)
local seen = {}
return table.filter(t, function(v)
if seen[v] then
return false
end
seen[v] = true
return true
end)
end
--append sequence t2 into t1, modifying t1
function table.append_inplace(t1, t2)
table.foreach(t2, function(v)
table.insert(t1, v)
end)
return t1
end
--return a new sequence with the elements of both t1 and t2
function table.append(t1, t2)
local r = {}
append_inplace(r, t1)
append_inplace(r, t2)
return r
end
--copy a table
-- if deep specified:
-- calls copy method of member directly if it exists
-- and recurses into all "normal" table children
function table.copy(t, deep)
local r = {}
for k,v in pairs(t) do
if deep and type(v) == "table" then
if type(v.copy) == "function" then
v = v:copy()
else
v = table.copy(v, deep)
end
end
r[k] = v
end
return r
end
-----------------------------------------------------------
--common queries and reductions
-----------------------------------------------------------
--true if any element of the table matches f
function table.any(t, f)
for i,v in ipairs(t) do
if f(v) then
return true
end
end
return false
end
--true if no element of the table matches f
function table.none(t, f)
for i,v in ipairs(t) do
if f(v) then
return false
end
end
return true
end
--true if all elements of the table match f
function table.all(t, f)
for i,v in ipairs(t) do
if not f(v) then
return false
end
end
return true
end
--counts the elements of t that match f
function table.count(t, f)
local c = 0
for i,v in ipairs(t) do
if f(v) then
c = c + 1
end
end
return c
end
--true if the table contains element e
function table.contains(t, e)
for i, v in ipairs(t) do
if v == e then
return true
end
end
return false
end
--return the numeric sum of all elements of t
function table.sum(t)
return table.reduce(t, function(a, b)
return a + b
end, 0)
end
--return the numeric mean of all elements of t
function table.mean(t)
local len = #t
if len == 0 then
return 0
end
return table.sum(t) / len
end
--return the minimum and maximum of t in one pass
function table.minmax(t)
local a = table.reduce(t, function(a, b)
a.min = a.min and math.min(a.min, b) or b
a.max = a.max and math.max(a.max, b) or b
return a
end, {})
if a.min == nil then
a.min = 0
a.max = 0
end
return a.min, a.max
end
function table.max(t)
local min, max = table.minmax(t)
return max
end
function table.min(t)
local min, max = table.minmax(t)
return min
end
--return the element of the table that results in the greatest numeric value
--(function receives element and key respectively, table evaluated in pairs order)
function table.find_best(t, f)
local current = nil
local current_best = -math.huge
for k,e in pairs(t) do
local v = f(e, k)
if v > current_best then
current_best = v
current = e
end
end
return current
end
--return the element of the table that results in the value nearest to the passed value
function table.find_nearest(t, f, v)
return table.find_best(t, function(e)
return -math.abs(f(e) - v)
end)
end
--return the first element of the table that results in a true filter
function table.find_match(t, f)
for i,v in ipairs(t) do
if f(v) then
return v
end
end
return nil
end
-----------------------------------------------------------
--sequence - functional wrapper for ordered tables
-----------------------------------------------------------
sequence = {}
sequence.mt = {__index = sequence}
--upgrade a table into a functional sequence
function sequence:new(t)
return setmetatable(t or {}, sequence.mt)
end
--import table functions to sequence as-is
sequence.insert = table.insert
sequence.remove = table.remove
sequence.concat = table.concat
sequence.join = table.concat --alias
--sorting
sequence.sort = table.stable_sort --(default stable)
sequence.stable_sort = table.stable_sort
sequence.unstable_sort = table.sort
--import functional interface to sequence
function sequence:keys()
return sequence:new(table.keys(self))
end
function sequence:values()
return sequence:new(table.values(self))
end
function sequence:foreach(f)
return table.foreach(self, f)
end
function sequence:reduce(f, o)
return table.foreach(self, f, o)
end
function sequence:map(f)
return sequence:new(table.map(self, f))
end
function sequence:filter(f)
return sequence:new(table.filter(self, f))
end
function sequence:partition(f)
local a, b = table.partition(self, f)
return sequence:new(a), sequence:new(b)
end
function sequence:zip(other, f)
return sequence:new(table.zip(self, other, f))
end
function sequence:dedupe()
return table.dedupe(self)
end
function sequence:append_inplace(other)
return table.append_inplace(self, other)
end
function sequence:append(other)
return sequence:new():append_inplace(self):append_inplace(other)
end
function sequence:copy(deep)
return sequence:new(table.copy(self, deep))
end
--
sequence.any = table.any
sequence.none = table.none
sequence.all = table.all
sequence.count = table.count
sequence.contains = table.contains
sequence.sum = table.sum
sequence.mean = table.mean
sequence.minmax = table.minmax
sequence.max = table.max
sequence.min = table.min
sequence.find_best = table.find_best
sequence.find_nearest = table.find_nearest
sequence.find_match = table.find_match
--generate a mapping from unique values to plain numbers
--useful for arbitrarily ordering things that don't have
--a natural ordering implied (eg textures for batching)
unique_mapping = {}
unique_mapping.mt = {
__index = unique_mapping,
__mode = "kv", --weak refs
}
--(used as storage for non-weak data)
local _MAP_VARS = setmetatable({}, {
__mode = "k" --only keys are weak
})
function unique_mapping:new()
local r = setmetatable({}, unique_mapping.mt)
--set up the actual vars
_MAP_VARS[r] = {
current_index = 0,
}
return r
end
function unique_mapping:_increment()
local vars = _MAP_VARS[self]
vars.current_index = vars.current_index + 1
return vars.current_index
end
function unique_mapping:map(value)
local val = self[value]
if val then
return val
end
local i = self:_increment()
self[value] = i
return i
end

28
init.lua Normal file
View File

@ -0,0 +1,28 @@
--[[
core modules
if required as the "entire library" (ie by this file), puts everything into
global namespace as it'll presumably be commonly used
if not, several of the modules work as "normal" modules and return a table
for local-friendly use
]]
local path = ...
local function relative_file(p)
return table.concat({path, p}, ".")
end
require(relative_file("oo"))
require(relative_file("math"))
require(relative_file("table"))
require(relative_file("stable_sort"))
require(relative_file("functional"))
vec2 = require(relative_file("vec2"))
intersect = require(relative_file("intersect"))
state_machine = require(relative_file("state_machine"))

389
intersect.lua Normal file
View File

@ -0,0 +1,389 @@
--[[
geometric intersection routines
from simple point tests to shape vs shape tests
options for boolean or minimum separating vector results
continuous sweeps (where provided) also return the
time-domain position of first intersection
TODO: refactor storage to be pooled rather than fully local
so these functions can be reentrant
]]
local intersect = {}
--epsilon for collisions
local COLLIDE_EPS = 1e-6
------------------------------------------------------------------------------
-- circles
function intersect.circle_point_overlap(pos, rad, v)
return pos:distance_squared(v) < rad * rad
end
function intersect.circle_circle_overlap(a_pos, a_rad, b_pos, b_rad)
local rad = a_rad + b_rad
return a_pos:distance_squared(b_pos) < rad * rad
end
local _ccc_delta = vec2:zero()
function intersect.circle_circle_collide(a_pos, a_rad, b_pos, b_rad, into)
--get delta
_ccc_delta:vset(a_pos):vsubi(b_pos)
--squared threshold
local rad = a_rad + b_rad
local dist = _ccc_delta:length_squared()
if dist < rad * rad then
if dist == 0 then
--singular case; just resolve vertically
dist = 1
_ccc_delta:sset(0,1)
else
--get actual distance
dist = math.sqrt(dist)
end
--allocate if needed
if into == nil then
into = vec2:zero()
end
--normalise, scale to separating distance
into:vset(_ccc_delta):sdiv(dist):smuli(rad - dist)
return into
end
return false
end
------------------------------------------------------------------------------
-- line segments
-- todo: separate double-sided, one-sided, and pull-through (along normal) collisions?
--vector from line seg to point
function intersect._line_to_point(a_start, a_end, b_pos, into)
if into == nil then into = vec2:zero() end
--direction of line
into:vset(a_end):vsub(a_start)
--detect degenerate case
if into:length_squared() <= COLLIDE_EPS then
return intersect.circle_circle_collide(a_start, a_rad, b_pos, b_rad)
end
--solve for factor along line
local dx = (b_pos.x - a_start.x) * (a_end.x - a_start.x)
local dy = (b_pos.y - a_start.y) * (a_end.y - a_start.y)
local u = (dx + dy) / into:length_squared()
--clamp onto segment
u = math.clamp01(u)
--get the displacement to the nearest point (invalidate direction)
into:smuli(u):vaddi(a_start):vsubi(b_pos)
return into
end
--line displacement vector from separation vector
function intersect._line_displacement_to_sep(a_start, a_end, separation, total_rad)
local distance = separation:normalisei_len()
local sep = distance - total_rad
if sep <= 0 then
if distance <= COLLIDE_EPS then
--point intersecting the line; push out along normal
separation:vset(a_end):vsub(a_start):normalisei():rot90li()
else
separation:smuli(-sep)
end
return separation
end
return false
end
--collide a line segment with a circle
function intersect.line_circle_collide(a_start, a_end, a_rad, b_pos, b_rad, into)
into = intersect._line_to_point(a_start, a_end, b_pos, into)
return intersect._line_displacement_to_sep(a_start, a_end, into, a_rad + b_rad)
end
--collide 2 line segments
function intersect.line_line_collide(a_start, a_end, a_rad, b_start, b_end, b_rad, into)
--segment directions from start points
local a_dir = a_end:vsub(a_start)
local b_dir = b_end:vsub(b_start)
--detect degenerate cases
local a_degen = a_dir:length_squared() <= COLLIDE_EPS
local b_degen = a_dir:length_squared() <= COLLIDE_EPS
if a_degen and b_degen then
--actually just circles
return intersect.circle_circle_collide(a_start, a_rad, b_start, b_rad, into)
elseif a_degen then
-- a is just circle; annoying, need reversed msv
local collided = intersect.line_circle_collide(b_start, b_end, b_rad, a_start, a_rad, into)
if collided then
collided:smuli(-1)
end
return collided
elseif b_degen then
--b is just circle
return intersect.line_circle_collide(a_start, a_end, a_rad, b_start, b_rad, into)
end
--otherwise we're _actually_ 2 line segs :)
if into == nil then into = vec2:zero() end
--first, check intersection
--(c to lua translation of paul bourke's
-- line intersection algorithm)
local dx1 = (a_end.x - a_start.x)
local dx2 = (b_end.x - b_start.x)
local dy1 = (a_end.y - a_start.y)
local dy2 = (b_end.y - b_start.y)
local dxab = (a_start.x - b_start.x)
local dyab = (a_start.y - b_start.y)
local denom = dy2 * dx1 - dx2 * dy1
local numera = dx2 * dyab - dy2 * dxab
local numerb = dx1 * dyab - dy1 * dxab
--check coincident lines
local intersected = "none"
if
math.abs(numera) < COLLIDE_EPS and
math.abs(numerb) < COLLIDE_EPS and
math.abs(denom) < COLLIDE_EPS
then
intersected = "both"
else
--check parallel, non-coincident lines
if math.abs(denom) < COLLIDE_EPS then
intersected = "none"
else
--get interpolants along segments
local mua = numera / denom
local mub = numerb / denom
--intersection outside segment bounds?
local outside_a = mua < 0 or mua > 1
local outside_b = mub < 0 or mub > 1
if outside_a and outside_b then
intersected = "none"
elseif outside_a then
intersected = "b"
elseif outside_b then
intersected = "a"
else
intersected = "both"
--collision point =
--[[vec2:xy(
a_start.x + mua * dx1,
a_start.y + mua * dy1,
)]]
end
end
end
if intersected == "both" then
--simply displace along A normal
return into:vset(a_dir):normalisei():smuli(a_rad + b_rad):rot90li()
else
--dumb as a rock check-corners approach
--todo: pool storage
--todo calculus from http://geomalgorithms.com/a07-_distance.html
local search_tab = {}
--only insert corners from the non-intersected line
--since intersected line is potentially the apex
if intersected ~= "a" then
--a endpoints
table.insert(search_tab, {intersect._line_to_point(b_start, b_end, a_start), 1})
table.insert(search_tab, {intersect._line_to_point(b_start, b_end, a_end), 1})
end
if intersected ~= "b" then
--b endpoints
table.insert(search_tab, {intersect._line_to_point(a_start, a_end, b_start), -1})
table.insert(search_tab, {intersect._line_to_point(a_start, a_end, b_end), -1})
end
local best = table.find_best(search_tab, function(v)
return -(v[1]:length_squared())
end)
--fix direction
into:vset(best[1]):smuli(best[2])
return intersect._line_displacement_to_sep(a_start, a_end, into, a_rad + b_rad)
end
return false
end
------------------------------------------------------------------------------
-- axis aligned bounding boxes
--return true on overlap, false otherwise
local _apo_delta = vec2:zero()
function intersect.aabb_point_overlap(pos, hs, v)
_apo_delta:vset(pos):vsubi(v):absi()
return _apo_delta.x < hs.x and _apo_delta.y < hs.y
end
--return true on overlap, false otherwise
local _aao_abs_delta = vec2:zero()
local _aao_total_size = vec2:zero()
function intersect.aabb_aabb_overlap(pos, hs, opos, ohs)
_aao_abs_delta:vset(pos):vsubi(opos):absi()
_aao_total_size:vset(hs):vaddi(ohs)
return _aao_abs_delta.x < _aao_total_size.x and _aao_abs_delta.y < _aao_total_size.y
end
--discrete displacement
--return msv on collision, false otherwise
local _aac_delta = vec2:zero()
local _aac_abs_delta = vec2:zero()
local _aac_size = vec2:zero()
local _aac_abs_amount = vec2:zero()
function intersect.aabb_aabb_collide(apos, ahs, bpos, bhs, into)
if not into then into = vec2:zero() end
_aac_delta:vset(apos):vsubi(bpos)
_aac_abs_delta:vset(_aac_delta):absi()
_aac_size:vset(ahs):vaddi(bhs)
_aac_abs_amount:vset(_aac_size):vsubi(_aac_abs_delta)
if _aac_abs_amount.x > COLLIDE_EPS and _aac_abs_amount.y > COLLIDE_EPS then
--actually collided
if _aac_abs_amount.x <= _aac_abs_amount.y then
--x min
if _aac_delta.x < 0 then
return into:sset(-_aac_abs_amount.x, 0)
else
return into:sset(_aac_abs_amount.x, 0)
end
else
--y min
if _aac_delta.y < 0 then
return into:sset(0, -_aac_abs_amount.y)
else
return into:sset(0, _aac_abs_amount.y)
end
end
end
return false
end
--return normal and fraction of dt encountered on collision, false otherwise
--TODO: re-pool storage here
function intersect.aabb_aabb_collide_continuous(
a_startpos, a_endpos, ahs,
b_startpos, b_endpos, bhs,
into
)
if not into then into = vec2:zero() end
--compute delta motion
local _self_delta_motion = a_endpos:vsub(a_startpos)
local _other_delta_motion = b_endpos:vsub(b_startpos)
--cheap "is this even possible" early-out
do
local _self_half_delta = _self_delta_motion:smul(0.5)
local _self_bounds_pos = _self_half_delta:vadd(a_endpos)
local _self_bounds_hs = _self_half_delta:vadd(ahs)
local _other_half_delta = _other_delta_motion:smul(0.5)
local _other_bounds_pos = _other_half_delta:vadd(b_endpos)
local _other_bounds_hs = _other_half_delta:vadd(bhs)
if not body._overlap_raw(
_self_bounds_pos, _self_bounds_hs,
_other_bounds_pos, _other_bounds_hs
) then
return false
end
end
--get ccd minkowski box
--this is a relative-space box
local _relative_delta_motion = _self_delta_motion:vsub(_other_delta_motion)
local _minkowski_halfsize = ahs:vadd(bhs)
local _minkowski_pos = b_startpos:vsub(a_startpos)
--if a line seg from our relative motion hits the minkowski box, we're in luck
--slab raycast is speedy
--alias
local _rmx = _relative_delta_motion.x
local _rmy = _relative_delta_motion.y
local _inv_x = math.huge
if _rmx ~= 0 then _inv_x = 1 / _rmx end
local _inv_y = math.huge
if _rmy ~= 0 then _inv_y = 1 / _rmy end
local _minkowski_tl = _minkowski_pos:vsub(_minkowski_halfsize)
local _minkowski_br = _minkowski_pos:vadd(_minkowski_halfsize)
--clip x
--get edge t along line
local tx1 = (_minkowski_tl.x) * _inv_x
local tx2 = (_minkowski_br.x) * _inv_x
--clip to existing clip space
local txmin = math.min(tx1, tx2)
local txmax = math.max(tx1, tx2)
--clip y
--get edge t along line
local ty1 = (_minkowski_tl.y) * _inv_y
local ty2 = (_minkowski_br.y) * _inv_y
--clip to existing clip space
local tymin = math.min(ty1, ty2)
local tymax = math.max(ty1, ty2)
--clip space
local tmin = math.max(0, txmin, tymin)
local tmax = math.min(1, txmax, tymax)
--still some unclipped? collision!
if tmin <= tmax then
--"was colliding at start"
if tmin == 0 then
--todo: maybe collide at old pos, not new pos
local msv = self:collide(other, into)
if msv then
return msv, tmin
else
return false
end
end
--delta before colliding
local _self_collide_pre = _self_delta_motion:smul(tmin)
--delta after colliding (to be discarded or projected or whatever)
local _self_collide_post = _self_delta_motion:smul(1.0 - tmin)
--get the collision normal
--(whichever boundary crossed _last_ -> normal)
local _self_collide_normal = vec2:zero()
if txmin > tymin then
_self_collide_normal.x = -math.sign(_self_delta_motion.x)
else
_self_collide_normal.y = -math.sign(_self_delta_motion.y)
end
--travelling away from normal?
if _self_collide_normal:dot(_self_delta_motion) >= 0 then
return false
end
--just "slide" projection for now
_self_collide_post:vreji(_self_collide_normal)
--combine
local _final_delta = _self_collide_pre:vadd(_self_collide_post)
--construct the target position
local _target_pos = a_startpos:vadd(_final_delta)
--return delta to target pos
local msv = _target_pos:vsub(a_endpos)
if math.abs(msv.x) > COLLIDE_EPS or math.abs(msv.y) > COLLIDE_EPS then
into:vset(msv)
return into, tmin
end
end
return false
end
return intersect

212
math.lua Normal file
View File

@ -0,0 +1,212 @@
--[[
extra mathematical functions
]]
local bit = require("bit")
--wrap v around range [lo, hi)
function math.wrap(v, lo, hi)
local range = hi - lo
local relative = v - lo
local relative_wrapped = relative % range
local relative_add = relative_wrapped + range
local final_wrap = relative_add % range
return lo + final_wrap
end
--clamp v to range [lo, hi]
function math.clamp(v, lo, hi)
return math.max(lo, math.min(v, hi))
end
function math.clamp01(v)
return math.clamp(v, 0, 1)
end
--round v to nearest whole
function math.round(v)
return math.floor(v + 0.5)
end
--round v to one-in x
-- (eg x = 2, v rounded to increments of 0.5)
function math.to_one_in(v, x)
return math.round(v * x) / x
end
--round v to a given decimal precision
function math.to_precision(v, decimal_points)
return math.to_one_in(v, math.pow(10, decimal_points))
end
--0, 1, -1 sign of a scalar
function math.sign(v)
if v < 0 then return -1 end
if v > 0 then return 1 end
return 0
end
--linear interpolation between a and b
function math.lerp(a, b, t)
return a * (1.0 - t) + b * t
end
--classic smoothstep
--(only "safe" for 0-1 range)
function math.smoothstep(v)
return v * v * (3 - 2 * v)
end
--classic smootherstep; zero 2nd order derivatives at 0 and 1
--(only safe for 0-1 range)
function math.smootherstep(v)
return v * v * v * (v * (v * 6 - 15) + 10)
end
--todo: various other easing curves
--prime number stuff
local primes_1k = {
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281,
283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409,
419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541,
547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659,
661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809,
811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941,
947, 953, 967, 971, 977, 983, 991, 997, 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069,
1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223,
1229, 1231, 1237, 1249, 1259, 1277, 1279, 1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373,
1381, 1399, 1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511,
1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597, 1601, 1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657,
1663, 1667, 1669, 1693, 1697, 1699, 1709, 1721, 1723, 1733, 1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789, 1801, 1811,
1823, 1831, 1847, 1861, 1867, 1871, 1873, 1877, 1879, 1889, 1901, 1907, 1913, 1931, 1933, 1949, 1951, 1973, 1979, 1987,
1993, 1997, 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069, 2081, 2083, 2087, 2089, 2099, 2111, 2113, 2129,
2131, 2137, 2141, 2143, 2153, 2161, 2179, 2203, 2207, 2213, 2221, 2237, 2239, 2243, 2251, 2267, 2269, 2273, 2281, 2287,
2293, 2297, 2309, 2311, 2333, 2339, 2341, 2347, 2351, 2357, 2371, 2377, 2381, 2383, 2389, 2393, 2399, 2411, 2417, 2423,
2437, 2441, 2447, 2459, 2467, 2473, 2477, 2503, 2521, 2531, 2539, 2543, 2549, 2551, 2557, 2579, 2591, 2593, 2609, 2617,
2621, 2633, 2647, 2657, 2659, 2663, 2671, 2677, 2683, 2687, 2689, 2693, 2699, 2707, 2711, 2713, 2719, 2729, 2731, 2741,
2749, 2753, 2767, 2777, 2789, 2791, 2797, 2801, 2803, 2819, 2833, 2837, 2843, 2851, 2857, 2861, 2879, 2887, 2897, 2903,
2909, 2917, 2927, 2939, 2953, 2957, 2963, 2969, 2971, 2999, 3001, 3011, 3019, 3023, 3037, 3041, 3049, 3061, 3067, 3079,
3083, 3089, 3109, 3119, 3121, 3137, 3163, 3167, 3169, 3181, 3187, 3191, 3203, 3209, 3217, 3221, 3229, 3251, 3253, 3257,
3259, 3271, 3299, 3301, 3307, 3313, 3319, 3323, 3329, 3331, 3343, 3347, 3359, 3361, 3371, 3373, 3389, 3391, 3407, 3413,
3433, 3449, 3457, 3461, 3463, 3467, 3469, 3491, 3499, 3511, 3517, 3527, 3529, 3533, 3539, 3541, 3547, 3557, 3559, 3571,
3581, 3583, 3593, 3607, 3613, 3617, 3623, 3631, 3637, 3643, 3659, 3671, 3673, 3677, 3691, 3697, 3701, 3709, 3719, 3727,
3733, 3739, 3761, 3767, 3769, 3779, 3793, 3797, 3803, 3821, 3823, 3833, 3847, 3851, 3853, 3863, 3877, 3881, 3889, 3907,
3911, 3917, 3919, 3923, 3929, 3931, 3943, 3947, 3967, 3989, 4001, 4003, 4007, 4013, 4019, 4021, 4027, 4049, 4051, 4057,
4073, 4079, 4091, 4093, 4099, 4111, 4127, 4129, 4133, 4139, 4153, 4157, 4159, 4177, 4201, 4211, 4217, 4219, 4229, 4231,
4241, 4243, 4253, 4259, 4261, 4271, 4273, 4283, 4289, 4297, 4327, 4337, 4339, 4349, 4357, 4363, 4373, 4391, 4397, 4409,
4421, 4423, 4441, 4447, 4451, 4457, 4463, 4481, 4483, 4493, 4507, 4513, 4517, 4519, 4523, 4547, 4549, 4561, 4567, 4583,
4591, 4597, 4603, 4621, 4637, 4639, 4643, 4649, 4651, 4657, 4663, 4673, 4679, 4691, 4703, 4721, 4723, 4729, 4733, 4751,
4759, 4783, 4787, 4789, 4793, 4799, 4801, 4813, 4817, 4831, 4861, 4871, 4877, 4889, 4903, 4909, 4919, 4931, 4933, 4937,
4943, 4951, 4957, 4967, 4969, 4973, 4987, 4993, 4999, 5003, 5009, 5011, 5021, 5023, 5039, 5051, 5059, 5077, 5081, 5087,
5099, 5101, 5107, 5113, 5119, 5147, 5153, 5167, 5171, 5179, 5189, 5197, 5209, 5227, 5231, 5233, 5237, 5261, 5273, 5279,
5281, 5297, 5303, 5309, 5323, 5333, 5347, 5351, 5381, 5387, 5393, 5399, 5407, 5413, 5417, 5419, 5431, 5437, 5441, 5443,
5449, 5471, 5477, 5479, 5483, 5501, 5503, 5507, 5519, 5521, 5527, 5531, 5557, 5563, 5569, 5573, 5581, 5591, 5623, 5639,
5641, 5647, 5651, 5653, 5657, 5659, 5669, 5683, 5689, 5693, 5701, 5711, 5717, 5737, 5741, 5743, 5749, 5779, 5783, 5791,
5801, 5807, 5813, 5821, 5827, 5839, 5843, 5849, 5851, 5857, 5861, 5867, 5869, 5879, 5881, 5897, 5903, 5923, 5927, 5939,
5953, 5981, 5987, 6007, 6011, 6029, 6037, 6043, 6047, 6053, 6067, 6073, 6079, 6089, 6091, 6101, 6113, 6121, 6131, 6133,
6143, 6151, 6163, 6173, 6197, 6199, 6203, 6211, 6217, 6221, 6229, 6247, 6257, 6263, 6269, 6271, 6277, 6287, 6299, 6301,
6311, 6317, 6323, 6329, 6337, 6343, 6353, 6359, 6361, 6367, 6373, 6379, 6389, 6397, 6421, 6427, 6449, 6451, 6469, 6473,
6481, 6491, 6521, 6529, 6547, 6551, 6553, 6563, 6569, 6571, 6577, 6581, 6599, 6607, 6619, 6637, 6653, 6659, 6661, 6673,
6679, 6689, 6691, 6701, 6703, 6709, 6719, 6733, 6737, 6761, 6763, 6779, 6781, 6791, 6793, 6803, 6823, 6827, 6829, 6833,
6841, 6857, 6863, 6869, 6871, 6883, 6899, 6907, 6911, 6917, 6947, 6949, 6959, 6961, 6967, 6971, 6977, 6983, 6991, 6997,
7001, 7013, 7019, 7027, 7039, 7043, 7057, 7069, 7079, 7103, 7109, 7121, 7127, 7129, 7151, 7159, 7177, 7187, 7193, 7207,
7211, 7213, 7219, 7229, 7237, 7243, 7247, 7253, 7283, 7297, 7307, 7309, 7321, 7331, 7333, 7349, 7351, 7369, 7393, 7411,
7417, 7433, 7451, 7457, 7459, 7477, 7481, 7487, 7489, 7499, 7507, 7517, 7523, 7529, 7537, 7541, 7547, 7549, 7559, 7561,
7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621, 7639, 7643, 7649, 7669, 7673, 7681, 7687, 7691, 7699, 7703, 7717, 7723,
7727, 7741, 7753, 7757, 7759, 7789, 7793, 7817, 7823, 7829, 7841, 7853, 7867, 7873, 7877, 7879, 7883, 7901, 7907, 7919,
}
local sparse_primes_1k = {
2, 3, 5, 11, 19, 37, 59, 79, 109, 151, 191, 239, 293, 367, 431, 499, 571, 653, 733, 853, 1009, 1151, 1301, 1481, 1613,
1783, 1951, 2113, 2137, 2311, 2477, 2687, 2857, 3079, 3323, 3541, 3853, 4211, 4549, 4933, 5101, 5479, 5843, 6247, 6653,
6689, 7039, 7307, 7559, 7573, 7919,
}
function math.first_above(v, t)
for _,p in ipairs(t) do
if p > v then
return p
end
end
return t[#t]
end
function math.next_prime_1k(v)
return math.first_above(v, primes_1k)
end
function math.next_prime_1k_sparse(v)
return math.first_above(v, sparse_primes_1k)
end
--color handling stuff
local band, bor = bit.band, bit.bor
local lshift, rshift = bit.lshift, bit.rshift
function math.colorToRGB(r, g, b)
local br = lshift(band(0xff, r * 255), 16)
local bg = lshift(band(0xff, g * 255), 8)
local bb = lshift(band(0xff, b * 255), 0)
return bor( br, bg, bb )
end
function math.colorToARGB(r, g, b, a)
local ba = lshift(band(0xff, a * 255), 24)
local br = lshift(band(0xff, r * 255), 16)
local bg = lshift(band(0xff, g * 255), 8)
local bb = lshift(band(0xff, b * 255), 0)
return bor( br, bg, bb, ba )
end
function math.colorToRGBA(r, g, b, a)
local br = lshift(band(0xff, r * 255), 24)
local bg = lshift(band(0xff, g * 255), 16)
local bb = lshift(band(0xff, b * 255), 8)
local ba = lshift(band(0xff, a * 255), 0)
return bor( br, bg, bb, ba )
end
function math.ARGBToColor(argb)
local r = rshift(band(argb, 0x00ff0000), 16) / 255.0
local g = rshift(band(argb, 0x0000ff00), 8) / 255.0
local b = rshift(band(argb, 0x000000ff), 0) / 255.0
local a = rshift(band(argb, 0xff000000), 24) / 255.0
return r, g, b, a
end
function math.RGBAToColor(rgba)
local r = rshift(band(rgba, 0xff000000), 24) / 255.0
local g = rshift(band(rgba, 0x00ff0000), 16) / 255.0
local b = rshift(band(rgba, 0x0000ff00), 8) / 255.0
local a = rshift(band(rgba, 0x000000ff), 0) / 255.0
return r, g, b, a
end
function math.RGBToColor(rgb)
local r = rshift(band(rgb, 0x00ff0000), 16) / 255.0
local g = rshift(band(rgb, 0x0000ff00), 8) / 255.0
local b = rshift(band(rgb, 0x000000ff), 0) / 255.0
local a = 1.0
return r, g, b, a
end
--angle handling stuff
function math.normalise_angle(a)
return math.wrap(a, -math.pi, math.pi)
end
function math.relative_angle(a1, a2)
a1 = math.normalise_angle(a1)
a2 = math.normalise_angle(a2)
return math.normalise_angle(a1 - a2)
end
--geometric rotation multi-return
function math.rotate(x, y, r)
local s = math.sin(r)
local c = math.cos(r)
return c * x - s * y, s * x + c * y
end

43
oo.lua Normal file
View File

@ -0,0 +1,43 @@
--[[
barebones oop basics
supports basic inheritance and means you don't have to build/set your own metatable each time
todo: collect some stats on classes/optional global class registry
]]
function class(inherits)
local c = {}
c.__mt = {__index = c}
--handle single inheritence
if type(inherits) == "table" and inherits.__mt then
setmetatable(c, inherits.__mt)
end
--common class functions
--internal initialisation
--sets up an initialised object with a default value table
--performing a super construction if necessary and assigning the right metatable
function c:init(t, ...)
if inherits then
--super ctor, then overlay args table
t = table.overlay(inherits:new(...), t)
end
--upgrade to this class and return
return setmetatable(t, self.__mt)
end
--constructor
--generally to be overridden
function c:new()
return self:init({})
end
--get the inherited class for super calls if/as needed
--allows overrides that still refer to superclass behaviour
function c:super()
return inherits
end
--done
return c
end

152
stable_sort.lua Normal file
View File

@ -0,0 +1,152 @@
-- stable sorting routines for lua
--
-- modifies the global table namespace so you don't have
-- to re-require it everywhere.
--
-- table.stable_sort
-- a fast stable sort
-- table.unstable_sort
-- alias for the builtin unstable table.sort
-- table.insertion_sort
-- an insertion sort, should you prefer it
--
--this is based on MIT licensed code from Dirk Laurie and Steve Fisher
--license as follows:
--[[
Copyright © 2013 Dirk Laurie and Steve Fisher.
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.
]]
-- (modifications by Max Cahill 2018)
local _sort_core = {}
--tunable size for
_sort_core.max_chunk_size = 24
function _sort_core.insertion_sort_impl( array, first, last, less )
for i = first + 1, last do
local k = first
local v = array[i]
for j = i, first + 1, -1 do
if less( v, array[j-1] ) then
array[j] = array[j-1]
else
k = j
break
end
end
array[k] = v
end
end
function _sort_core.merge( array, workspace, low, middle, high, less )
local i, j, k
i = 1
-- copy first half of array to auxiliary array
for j = low, middle do
workspace[ i ] = array[ j ]
i = i + 1
end
-- sieve through
i = 1
j = middle + 1
k = low
while true do
if (k >= j) or (j > high) then
break
end
if less( array[ j ], workspace[ i ] ) then
array[ k ] = array[ j ]
j = j + 1
else
array[ k ] = workspace[ i ]
i = i + 1
end
k = k + 1
end
-- copy back any remaining elements of first half
for k = k, j-1 do
array[ k ] = workspace[ i ]
i = i + 1
end
end
function _sort_core.merge_sort_impl(array, workspace, low, high, less)
if high - low <= _sort_core.max_chunk_size then
_sort_core.insertion_sort_impl( array, low, high, less )
else
local middle = math.floor((low + high)/2)
_sort_core.merge_sort_impl( array, workspace, low, middle, less )
_sort_core.merge_sort_impl( array, workspace, middle + 1, high, less )
_sort_core.merge( array, workspace, low, middle, high, less )
end
end
--inline common setup stuff
function _sort_core.sort_setup(array, less)
local n = #array
local trivial = false
--trivial cases; empty or 1 element
if n <= 1 then
trivial = true
else
--default less
less = less or function (a, b)
return a < b
end
--check less
if less(array[1], array[1]) then
error("invalid order function for sorting")
end
end
--setup complete
return trivial, n, less
end
function _sort_core.stable_sort(array, less)
--setup
local trivial, n, less = _sort_core.sort_setup(array, less)
if not trivial then
--temp storage
local workspace = {}
workspace[ math.floor( (n+1)/2 ) ] = array[1]
--dive in
_sort_core.merge_sort_impl( array, workspace, 1, n, less )
end
return array
end
function _sort_core.insertion_sort(array, less)
--setup
local trivial, n, less = _sort_core.sort_setup(array, less)
if not trivial then
_sort_core.insertion_sort_impl(array, 1, n, less)
end
return array
end
--export sort core
table.insertion_sort = _sort_core.insertion_sort
table.stable_sort = _sort_core.stable_sort
table.unstable_sort = table.sort

135
state_machine.lua Normal file
View File

@ -0,0 +1,135 @@
--[[
state machine
a finite state machine implementation;
each state is a table with optional enter, exit, update and draw callbacks
which each optionally take the machine, and the state table as arguments
on changing state, the outgoing state's exit callback is called, then the incoming state's
enter callback is called.
on update, the current state's update callback is called
on draw, the current state's draw callback is called
TODO: consider coroutine friendliness
depends on oo.lua supplying class()
]]
local state_machine = class()
function state_machine:new(states, start)
local ret = self:init({
states = states or {},
current_state = ""
})
if start then
ret:set_state(start)
end
return ret
end
-------------------------------------------------------------------------------
--internal helpers
function state_machine:_get_state()
return self.states[self.current_state]
end
--make an internal call, with up to 4 arguments
function state_machine:_call(name, a, b, c, d)
local state = self:_get_state()
if state and type(state[name]) == "function" then
return state[name](self, state, a, b, c, d)
end
return nil
end
-------------------------------------------------------------------------------
--various checks
function state_machine:in_state(name)
return self.current_state == name
end
function state_machine:has_state(name)
return self.states[name] ~= nil
end
-------------------------------------------------------------------------------
--state adding/removing
--add a state
function state_machine:add_state(name, data)
if self.has_state(name) then
error("error: added duplicate state "..name)
else
self.states[name] = data
if self:in_state(name) then
self:_call("enter")
end
end
return self
end
--remove a state
function state_machine:remove_state(name)
if not self.has_state(name) then
error("error: removed missed state "..name)
else
if self:in_state(name) then
self:_call("exit")
end
self.states[name] = nil
end
return self
end
--hard-replace a state table
--if do_transitions is truthy and we're replacing the current state,
--exit is called on the old state and enter is called on the new state
function state_machine:replace_state(name, data, do_transitions)
local current = self:in_state(name)
if do_transitions and current then
self:_call("exit")
end
self.states[name] = data
if do_transitions and current then
self:_call("enter")
end
return self
end
--ensure a state doesn't exist
function state_machine:clear_state(name)
return self:replace_state(name, nil, true)
end
-------------------------------------------------------------------------------
--transitions and updates
function state_machine:set_state(state, reset)
if self.current_state ~= state or reset then
self:_call("exit")
self.current_state = state
self:_call("enter")
end
return self
end
--perform an update
--pass in an optional delta time which is passed as an arg to the state functions
function state_machine:update(dt)
return self:_call("update", dt)
end
function state_machine:draw()
self:_call("draw")
end
return state_machine

108
table.lua Normal file
View File

@ -0,0 +1,108 @@
--[[
extra table routines
]]
--return the back element of a table
function table.back(t)
return t[#t]
end
--remove the back element of a table and return it
function table.pop(t)
return table.remove(t)
end
--insert to the back of a table
function table.push(t, v)
return table.insert(t, v)
end
--remove the front element of a table and return it
function table.shift(t)
return table.remove(t, 1)
end
--insert to the front of a table
function table.unshift(t, v)
return table.insert(t, 1, v)
end
--find the index in a sequential table that a resides at
--or nil if nothing was found
--(todo: consider pairs version?)
function table.index_of(t, a)
if a == nil then return nil end
for i,b in ipairs(t) do
if a == b then
return i
end
end
return nil
end
--remove the first instance of value from a table (linear search)
--returns true if the value was removed, else false
function table.remove_value(t, a)
local i = table.index_of(t, a)
if i then
table.remove(t, i)
return true
end
return false
end
--add a value to a table if it doesn't already exist (linear search)
--returns true if the value was added, else false
function table.add_value(t, a)
local i = table.index_of(t, a)
if not i then
table.insert(t, a)
return true
end
return false
end
--helper for optionally passed random
local _global_random = love.math.random or math.random
local function _random(min, max, r)
return r and r:random(min, max)
or _global_random(min, max)
end
--pick a random value from a table (or nil if it's empty)
function table.pick_random(t, r)
if #t == 0 then
return nil
end
return t[_random(1, #t, r)]
end
--shuffle the order of a table
function table.shuffle(t, r)
for i = 1, #t do
local j = _random(1, #t, r)
t[i], t[j] = t[j], t[i]
end
end
--(might already exist depending on luajit)
if table.clear == nil then
--destructively clear a numerically keyed table
--useful when multiple references are floating around
--so you cannot just pop a new table out of nowhere
function table.clear(t)
assert(type(to) == "table", "table.overlay - argument 'to' must be a table")
while t[1] ~= nil do
table.remove(t)
end
end
end
function table.overlay(to, from)
assert(type(to) == "table", "table.overlay - argument 'to' must be a table")
assert(type(from) == "table", "table.overlay - argument 'from' must be a table")
for k,v in pairs(from) do
to[k] = v
end
return to
end

579
vec2.lua Normal file
View File

@ -0,0 +1,579 @@
--[[
2d vector type
]]
--[[
notes:
depends on a class() function as in oo.lua
some methods depend on math library extensions
math.clamp(v, min, max) - return v clamped between min and max
math.round(v) - round v downwards if fractional part is < 0.5
]]
local vec2 = class()
vec2.x = 0
vec2.y = 0
--probably-too-flexible ctor
function vec2:new(x, y)
if x and y then
return vec2:xy(x,y)
elseif x then
if type(x) == "number" then
return vec2:filled(x)
elseif x.copy then
return x:copy()
end
end
return vec2:zero()
end
--explicit ctors
function vec2:copy()
return self:init({
x = self.x, y = self.y
})
end
function vec2:xy(x, y)
return self:init({
x = x, y = y
})
end
function vec2:filled(v)
return self:init({
x = v, y = v
})
end
function vec2:zero()
return vec2:filled(0)
end
--shared pooled storage
local _vec2_pool = {}
--size limit for tuning memory upper bound
local _vec2_pool_limit = 128
function vec2.pool_size()
return #_vec2_pool
end
--flush the entire pool
function vec2.flush_pool()
if vec2.pool_size() > 0 then
_vec2_pool = {}
end
end
--drain one element from the pool, if it exists
function vec2.drain_pool()
if #_vec2_pool > 0 then
return table.remove(_vec2_pool)
end
return nil
end
--get a pooled vector (initialise it yourself)
function vec2:pooled()
return vec2.drain_pool() or vec2:zero()
end
--get a pooled copy of an existing vector
function vec2:pooled_copy()
return vec2:pooled():vset(self)
end
--release a vector to the pool
function vec2:release()
if vec2.pool_size() < _vec2_pool_limit then
table.insert(_vec2_pool, self)
end
end
--unpack for multi-args
function vec2:unpack()
return self.x, self.y
end
--pack when a sequence is needed
--(not particularly useful)
function vec2:pack()
return {self:unpack()}
end
--modify
function vec2:sset(x, y)
if not y then y = x end
self.x = x
self.y = y
return self
end
function vec2:vset(v)
self.x = v.x
self.y = v.y
return self
end
function vec2:swap(v)
local sx, sy = self.x, self.y
self:vset(v)
v:sset(sx, sy)
return self
end
-----------------------------------------------------------
--equality comparison
-----------------------------------------------------------
--threshold for equality in each dimension
local EQUALS_EPSILON = 1e-9
--true if a and b are functionally equivalent
function vec2.equals(a, b)
return (
math.abs(a.x - b.x) <= EQUALS_EPSILON and
math.abs(a.y - b.y) <= EQUALS_EPSILON
)
end
--true if a and b are not functionally equivalent
--(very slightly faster than `not vec2.equals(a, b)`)
function vec2.nequals(a, b)
return (
math.abs(a.x - b.x) > EQUALS_EPSILON or
math.abs(a.y - b.y) > EQUALS_EPSILON
)
end
-----------------------------------------------------------
--arithmetic
-----------------------------------------------------------
--immediate mode
--vector
function vec2:vaddi(v)
self.x = self.x + v.x
self.y = self.y + v.y
return self
end
function vec2:vsubi(v)
self.x = self.x - v.x
self.y = self.y - v.y
return self
end
function vec2:vmuli(v)
self.x = self.x * v.x
self.y = self.y * v.y
return self
end
function vec2:vdivi(v)
self.x = self.x / v.x
self.y = self.y / v.y
return self
end
--scalar
function vec2:saddi(x, y)
if y then
self.x = self.x + x
self.y = self.y + y
else
self.x = self.x + x
self.y = self.y + x
end
return self
end
function vec2:ssubi(x, y)
if y then
self.x = self.x - x
self.y = self.y - y
else
self.x = self.x - x
self.y = self.y - x
end
return self
end
function vec2:smuli(x, y)
if y then
self.x = self.x * x
self.y = self.y * y
else
self.x = self.x * x
self.y = self.y * x
end
return self
end
function vec2:sdivi(x, y)
if y then
self.x = self.x / x
self.y = self.y / y
else
self.x = self.x / x
self.y = self.y / x
end
return self
end
--garbage mode
function vec2:vadd(v)
return self:copy():vaddi(v)
end
function vec2:vsub(v)
return self:copy():vsubi(v)
end
function vec2:vmul(v)
return self:copy():vmuli(v)
end
function vec2:vdiv(v)
return self:copy():vdivi(v)
end
function vec2:sadd(x, y)
return self:copy():saddi(x, y)
end
function vec2:ssub(x, y)
return self:copy():ssubi(x, y)
end
function vec2:smul(x, y)
return self:copy():smuli(x, y)
end
function vec2:sdiv(x, y)
return self:copy():sdivi(x, y)
end
--fused multiply-add (a + (b * t))
function vec2:fmai(v, t)
self.x = self.x + (v.x * t)
self.y = self.y + (v.y * t)
return self
end
function vec2:fma(v, t)
return self:copy():fmai(v, t)
end
-----------------------------------------------------------
-- geometric methods
-----------------------------------------------------------
function vec2:length_squared()
return self.x * self.x + self.y * self.y
end
function vec2:length()
return math.sqrt(self:length_squared())
end
function vec2:distance_squared(other)
local dx = self.x - other.x
local dy = self.y - other.y
return dx * dx + dy * dy
end
function vec2:distance(other)
return math.sqrt(self:distance_squared(other))
end
--immediate mode
function vec2:normalisei_both()
local len = self:length()
if len == 0 then
return self, 0
end
return self:sdivi(len), len
end
function vec2:normalisei()
local v, len = self:normalisei_both()
return v
end
function vec2:normalisei_len()
local v, len = self:normalisei_both()
return len
end
function vec2:inversei()
return self:smuli(-1)
end
function vec2:rotatei(angle)
local s = math.sin(angle)
local c = math.cos(angle)
local ox = self.x
local oy = self.y
self.x = c * ox - s * oy
self.y = s * ox + c * oy
return self
end
function vec2:rot90ri()
local ox = self.x
local oy = self.y
self.x = -oy
self.y = ox
return self
end
function vec2:rot90li()
local ox = self.x
local oy = self.y
self.x = oy
self.y = -ox
return self
end
vec2.rot180i = vec2.inversei --alias
function vec2:rotate_aroundi(angle, pivot)
local s = math.sin(angle)
local c = math.cos(angle)
local ox = self.x - pivot.x
local oy = self.y - pivot.y
self.x = (c * ox - s * oy) + pivot.x
self.y = (s * ox + c * oy) + pivot.y
return self
end
function vec2:rotate_around(angle, pivot)
return self:copy():rotate_aroundi(angle, pivot)
end
--garbage mode
function vec2:normalised()
return self:copy():normalisei()
end
function vec2:normalised_len()
local v = self:copy()
local len = v:normalisei_len()
return v, len
end
function vec2:inverse()
return self:copy():inversei()
end
function vec2:rotate(angle)
return self:copy():rotatei(angle)
end
function vec2:rot90r()
return self:copy():rot90ri()
end
function vec2:rot90l()
return self:copy():rot90li()
end
vec2.rot180 = vec2.inverse --alias
function vec2:angle()
return math.atan2(self.y, self.x)
end
-----------------------------------------------------------
-- per-component clamping ops
-----------------------------------------------------------
function vec2:mini(v)
self.x = math.min(self.x, v.x)
self.y = math.min(self.y, v.y)
return self
end
function vec2:maxi(v)
self.x = math.max(self.x, v.x)
self.y = math.max(self.y, v.y)
return self
end
function vec2:clampi(min, max)
self.x = math.clamp(self.x, min.x, max.x)
self.y = math.clamp(self.y, min.y, max.y)
return self
end
function vec2:min(v)
return self:copy():mini(v)
end
function vec2:max(v)
return self:copy():maxi(v)
end
function vec2:clamp(min, max)
return self:copy():clampi(min, max)
end
-----------------------------------------------------------
-- absolute value
-----------------------------------------------------------
function vec2:absi()
self.x = math.abs(self.x)
self.y = math.abs(self.y)
return self
end
function vec2:abs()
return self:copy():absi()
end
-----------------------------------------------------------
-- truncation/rounding
-----------------------------------------------------------
function vec2:floori()
self.x = math.floor(self.x)
self.y = math.floor(self.y)
return self
end
function vec2:ceili()
self.x = math.ceil(self.x)
self.y = math.ceil(self.y)
return self
end
function vec2:roundi()
self.x = math.round(self.x)
self.y = math.round(self.y)
return self
end
function vec2:floor()
return self:copy():floori()
end
function vec2:ceil()
return self:copy():ceili()
end
function vec2:round()
return self:copy():roundi()
end
-----------------------------------------------------------
-- interpolation
-----------------------------------------------------------
function vec2:lerpi(other, amount)
self.x = math.lerp(self.x, other.x, amount)
self.y = math.lerp(self.y, other.y, amount)
return self
end
function vec2:lerp(other, amount)
return self:copy():lerpi(other, amount)
end
-----------------------------------------------------------
-- vector products and projections
-----------------------------------------------------------
function vec2.dot(a, b)
return a.x * b.x + a.y * b.y
end
--"fake", but useful - also called the wedge product apparently
function vec2.cross(a, b)
return a.x * b.y - a.y * b.x
end
--scalar projection a onto b
function vec2.sproj(a, b)
local len = b:length()
if len == 0 then
return 0
end
return a:dot(b) / len
end
--vector projection a onto b (writes into a)
function vec2.vproji(a, b)
local div = b:dot(b)
if div == 0 then
return a:sset(0,0)
end
local fac = a:dot(b) / div
return a:vset(b):smuli(fac)
end
function vec2.vproj(a, b)
return a:copy():vproji(b)
end
--vector rejection a onto b (writes into a)
function vec2.vreji(a, b)
local tx, ty = a.x, a.y
a:vproji(b)
a:sset(tx - a.x, ty - a.y)
return a
end
function vec2.vrej(a, b)
return a:copy():vreji(b)
end
-----------------------------------------------------------
-- vector extension methods for special purposes
-- (any common vector ops worth naming)
-----------------------------------------------------------
--"physical" friction
local _v_friction = vec2:zero() --avoid alloc
function vec2:apply_friction(mu, dt)
_v_friction:vset(self):smuli(mu * dt)
if _v_friction:length_squared() > self:length_squared() then
self:sset(0, 0)
else
self:vsubi(_v_friction)
end
return self
end
--"gamey" friction in one dimension
function apply_friction_1d(v, mu, dt)
local friction = mu * v * dt
if math.abs(friction) > math.abs(v) then
return 0
else
return v - friction
end
end
--"gamey" friction in both dimensions
function vec2:apply_friction_xy(mu_x, mu_y, dt)
self.x = apply_friction_1d(self.x, mu_x, dt)
self.y = apply_friction_1d(self.y, mu_y, dt)
return self
end
return vec2