-- Timing helpers for collab backgrounds ported from Lumen data. Lengths are multiples of -- `unit` ms: a beat (60000 / bpm) for things that follow the song, 1 for plain ms. -- Moves { length, distance, ease } run back to back (distance 0 is a hold); the offset -- they describe is the sum of their attributes local function chain_moves(moves, unit, scale) local out, at = {}, 0 for _, m in ipairs(moves) do local length, distance, ease = m[1], m[2], m[3] if distance ~= 0 then local move = anim.move(length * unit, { total_distance = math.floor(distance * scale), delay = at, ease_in = ease == "in" and "quadratic" or nil, ease_out = ease == "out" and "quadratic" or nil, }) move:start() table.insert(out, move) end at = at + length * unit end return out end -- Frames { index, length } shown back to back local function frame_change(steps, unit, opts) local textures, t = {}, 0 for _, step in ipairs(steps) do table.insert(textures, { t, t + step[2] * unit, step[1] }) t = t + step[2] * unit end return anim.texture_change(t, textures, opts) end local function sum(moves) local total = 0 for _, m in ipairs(moves) do total = total + m.attribute end return total end local function concat(...) local out = {} for _, t in ipairs({ ... }) do for _, v in ipairs(t) do table.insert(out, v) end end return out end -- Ambient loop: `build()` returns a table of one-shot anims (or lists of them) that is -- rebuilt every `period` ms, so several anims stay in step without each looping alone local Loop = {} Loop.__index = Loop function Loop.new(period, build) return setmetatable({ period = period, build = build }, Loop) end function Loop:update(current_time_ms) if not self.t0 or current_time_ms - self.t0 >= self.period then self.t0 = current_time_ms self.anims = self.build() end for _, a in pairs(self.anims) do if a.update then a:update(current_time_ms) else for _, m in ipairs(a) do m:update(current_time_ms) end end end end return { chain_moves = chain_moves, frame_change = frame_change, sum = sum, concat = concat, Loop = Loop, }