batteries/mathx.lua
2024-03-07 16:34:23 +11:00

233 lines
5.4 KiB
Lua

--[[
extra mathematical functions
]]
local mathx = setmetatable({}, {
__index = math,
})
--wrap v around range [lo, hi)
function mathx.wrap(v, lo, hi)
return (v - lo) % (hi - lo) + lo
end
--wrap i around the indices of t
function mathx.wrap_index(i, t)
return math.floor(mathx.wrap(i, 1, #t + 1))
end
--clamp v to range [lo, hi]
function mathx.clamp(v, lo, hi)
return math.max(lo, math.min(v, hi))
end
--clamp v to range [0, 1]
function mathx.clamp01(v)
return mathx.clamp(v, 0, 1)
end
--round v to nearest whole, away from zero
function mathx.round(v)
if v < 0 then
return math.ceil(v - 0.5)
end
return math.floor(v + 0.5)
end
--round v to one-in x
-- (eg x = 2, v rounded to increments of 0.5)
function mathx.to_one_in(v, x)
return mathx.round(v * x) / x
end
--round v to a given decimal precision
function mathx.to_precision(v, decimal_points)
return mathx.to_one_in(v, math.pow(10, decimal_points))
end
--0, 1, -1 sign of a scalar
--todo: investigate if a branchless or `/abs` approach is faster in general case
function mathx.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 mathx.lerp(a, b, t)
return a * (1.0 - t) + b * t
end
--linear interpolation with a minimum "final step" distance
--useful for making sure dynamic lerps do actually reach their final destination
function mathx.lerp_eps(a, b, t, eps)
local v = mathx.lerp(a, b, t)
if math.abs(v - b) < eps then
v = b
end
return v
end
--bilinear interpolation between 4 samples
function mathx.bilerp(a, b, c, d, u, v)
return mathx.lerp(
mathx.lerp(a, b, u),
mathx.lerp(c, d, u),
v
)
end
--get the lerp factor on a range, inverse_lerp(6, 0, 10) == 0.6
function mathx.inverse_lerp(v, min, max)
return (v - min) / (max - min)
end
--remap a value from one range to another
function mathx.remap_range(v, in_min, in_max, out_min, out_max)
return mathx.lerp(out_min, out_max, mathx.inverse_lerp(v, in_min, in_max))
end
--remap a value from one range to another, staying within that range
function mathx.remap_range_clamped(v, in_min, in_max, out_min, out_max)
return mathx.lerp(out_min, out_max, mathx.clamp01(mathx.inverse_lerp(v, in_min, in_max)))
end
--easing curves
--(generally only "safe" for 0-1 range, see mathx.clamp01)
--no curve - can be used as a default to avoid needing a branch
function mathx.identity(f)
return f
end
--classic smoothstep
function mathx.smoothstep(f)
return f * f * (3 - 2 * f)
end
--classic smootherstep; zero 2nd order derivatives at 0 and 1
function mathx.smootherstep(f)
return f * f * f * (f * (f * 6 - 15) + 10)
end
--pingpong from 0 to 1 and back again
function mathx.pingpong(f)
return 1 - math.abs(1 - (f * 2) % 2)
end
--quadratic ease in
function mathx.ease_in(f)
return f * f
end
--quadratic ease out
function mathx.ease_out(f)
local oneminus = (1 - f)
return 1 - oneminus * oneminus
end
--quadratic ease in and out
--(a lot like smoothstep)
function mathx.ease_inout(f)
if f < 0.5 then
return f * f * 2
end
local oneminus = (1 - f)
return 1 - 2 * oneminus * oneminus
end
--branchless but imperfect quartic in/out
--either smooth or smootherstep are usually a better alternative
function mathx.ease_inout_branchless(f)
local halfsquared = f * f / 2
return halfsquared * (1 - halfsquared) * 4
end
--todo: more easings - back, bounce, elastic
--(internal; use a provided random generator object, or not)
local function _random(rng, ...)
if rng then return rng:random(...) end
if love then return love.math.random(...) end
return math.random(...)
end
--return a random sign
function mathx.random_sign(rng)
return _random(rng) < 0.5 and -1 or 1
end
--return a random value between two numbers (continuous)
function mathx.random_lerp(min, max, rng)
return mathx.lerp(min, max, _random(rng))
end
--nan checking
function mathx.isnan(v)
return v ~= v
end
--angle handling stuff
--superior constant handy for expressing things in turns
mathx.tau = math.pi * 2
--normalise angle onto the interval [-math.pi, math.pi)
--so each angle only has a single value representing it
function mathx.normalise_angle(a)
return mathx.wrap(a, -math.pi, math.pi)
end
--alias for americans
mathx.normalize_angle = mathx.normalise_angle
--get the normalised difference between two angles
function mathx.angle_difference(a, b)
a = mathx.normalise_angle(a)
b = mathx.normalise_angle(b)
return mathx.normalise_angle(b - a)
end
--mathx.lerp equivalent for angles
function mathx.lerp_angle(a, b, t)
local dif = mathx.angle_difference(a, b)
return mathx.normalise_angle(a + dif * t)
end
--mathx.lerp_eps equivalent for angles
function mathx.lerp_angle_eps(a, b, t, eps)
--short circuit to avoid having to wrap so many angles
if a == b then
return a
end
--same logic as lerp_eps
local v = mathx.lerp_angle(a, b, t)
if math.abs(mathx.angle_difference(v, b)) < eps then
v = b
end
return v
end
--geometric functions standalone/"unpacked" components and multi-return
--consider using vec2 if you need anything complex!
--rotate a point around the origin by an angle
function mathx.rotate(x, y, r)
local s = math.sin(r)
local c = math.cos(r)
return c * x - s * y, s * x + c * y
end
--get the length of a vector from the origin
function mathx.length(x, y)
return math.sqrt(x * x + y * y)
end
--get the distance between two points
function mathx.distance(x1, y1, x2, y2)
local dx = x1 - x2
local dy = y1 - y2
return mathx.length(dx, dy)
end
return mathx