Files
YataiDONNijiiro/Scripts/anim/sampler.lua
2026-09-30 15:29:22 -04:00

107 lines
3.4 KiB
Lua

-- Sampler for the exported clip tables: lerp between kept frames, clamp to the ends.
local M = {}
local Track = {}
Track.__index = Track
local Clip = {}
Clip.__index = Clip
local FPS_MS = 60.0 / 1000.0 -- ms -> arcade frames
function M.frames(ms) return ms * FPS_MS end
function M.ms(frames) return frames / FPS_MS end
-- per-clip converters honour the table's own fps field
function Clip:frames(ms) return ms * (self.fps or 60) / 1000.0 end
function Clip:msof(frames) return frames * 1000.0 / (self.fps or 60) end
-- binary search: the last row whose frame is <= f
local function seek(rows, f)
local lo, hi = 1, #rows
if f <= rows[lo][1] then return lo end
if f >= rows[hi][1] then return hi end
while lo < hi do
local mid = math.floor((lo + hi + 1) / 2)
-- upper-biased midpoint: must decrement hi or the loop never shrinks
if rows[mid][1] <= f then lo = mid else hi = mid - 1 end
end
return lo
end
-- unpack is `table.unpack` in 5.2+; fall back to the global otherwise
local tunpack = table.unpack
if tunpack == nil then tunpack = unpack end
function Clip:at(name, f)
local rows = self.tracks[name]
if not rows or #rows == 0 then return nil end
local n = #rows
local i = seek(rows, f)
local a = rows[i]
if i >= n or f <= a[1] then return tunpack(a, 2, #a) end
local b = rows[i + 1]
local u = (f - a[1]) / (b[1] - a[1])
local out = {}
for k = 2, #a do out[k - 1] = a[k] + (b[k] - a[k]) * u end
return tunpack(out, 1, #out)
end
-- one named field of one track, by the name the exporter wrote into `fields`
function Clip:get(name, f, field)
local idx = self.index[field]
if not idx then return nil end
local rows = self.tracks[name]
if not rows or #rows == 0 then return nil end
local n = #rows
local i = seek(rows, f)
local a = rows[i]
if i >= n or f <= a[1] then return a[idx + 1] end
local b = rows[i + 1]
local u = (f - a[1]) / (b[1] - a[1])
return a[idx + 1] + (b[idx + 1] - a[idx + 1]) * u
end
function Clip:label(name) return self.labels and self.labels[name] end
function Clip:has(name) return self.tracks[name] ~= nil end
function Clip:first() return self.range[1] end
function Clip:last() return self.range[2] end
-- M.safe returns the real clip or this NULL clip (readers return nil / empty range).
-- Trap: files here must not share a stem with any screen script (flat indexing, first wins).
local NULL = {}
NULL.__index = NULL
function NULL:at() return nil end
function NULL:get() return nil end
function NULL:label() return nil end
function NULL:has() return false end
function NULL:first() return 0 end
function NULL:last() return 0 end
function NULL:frames(ms) return ms * FPS_MS end -- keep NULL's interface complete
function NULL:msof(frames) return frames / FPS_MS end
M.NULL = setmetatable({}, NULL)
-- never raises: returns the clip, or the NULL clip if the table is missing or malformed
function M.safe(path)
local ok, clip = pcall(M.load, path)
if ok and clip and clip.tracks and clip.fields and clip.range then return clip end
return M.NULL
end
local cache = {}
-- `path` is a require() path, e.g. "anim/folder_board".
function M.load(path)
local c = cache[path]
if c then return c end
local data = require(path)
data.index = {}
for i, name in ipairs(data.fields) do data.index[name] = i end
setmetatable(data, Clip)
cache[path] = data
return data
end
return M