From c3bb096b5b8abaabe53883e7d568caae50752b18 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Sun, 17 May 2026 19:55:32 +0200 Subject: [PATCH] feat: add sample_animation with linear interpolation Samples an animation at time t. Wraps t for loops. Finds the keyframe segment containing t and linearly interpolates per-bone angles. Returns frame with angles in degrees (intuitive for tests and callers; internal storage uses radians). Edge cases handled: - t = 0 returns first-keyframe values - t = duration returns last-keyframe values (no overshoot) - t > duration with loop=true wraps via modulo - Keyframes with disjoint bone-sets: each bone interpolated where available, snapped where only one side has it. --- init.lua | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/init.lua b/init.lua index 8bdc772..e641fb0 100644 --- a/init.lua +++ b/init.lua @@ -178,4 +178,58 @@ function M.build_animation(anim_table, rig) } end +-- ==================================================================== +-- Animation sampling +-- ==================================================================== +function M.sample_animation(anim, t) + -- Wrap t for loops. + if anim.loop and t > anim.duration then + t = t % anim.duration + end + if t < 0 then t = 0 end + if t > anim.duration then t = anim.duration end + + local kfs = anim.keyframes + -- Find segment [kf_i, kf_{i+1}] containing t. + local prev_kf, next_kf = kfs[1], kfs[1] + for i = 1, #kfs - 1 do + if t >= kfs[i].t and t <= kfs[i + 1].t then + prev_kf = kfs[i] + next_kf = kfs[i + 1] + break + end + end + + -- If t equals last keyframe's t (or beyond and not looping), snap to last. + if t >= kfs[#kfs].t then + prev_kf = kfs[#kfs] + next_kf = kfs[#kfs] + end + + -- Linear interpolation per bone present in either keyframe. + local frame = {} + local span = next_kf.t - prev_kf.t + local alpha = (span > 0) and ((t - prev_kf.t) / span) or 0 + + local all_bones = {} + for bid, _ in pairs(prev_kf.bones) do all_bones[bid] = true end + for bid, _ in pairs(next_kf.bones) do all_bones[bid] = true end + + for bid, _ in pairs(all_bones) do + local prev_v = prev_kf.bones[bid] + local next_v = next_kf.bones[bid] + if prev_v and next_v then + frame[bid] = { + angle = math.deg(prev_v.angle * (1 - alpha) + next_v.angle * alpha), + } + elseif prev_v then + frame[bid] = { angle = math.deg(prev_v.angle) } + elseif next_v then + frame[bid] = { angle = math.deg(next_v.angle) } + end + end + + return frame +end + return M