--- Tween.lua - tweening library for Lua --- @module tween --- @alias TweenModule --- @field _VERSION string --- @field _DESCRIPTION string --- @field _URL string --- @field _LICENSE string --- @field easing EasingFunctions --- @field new fun(duration: number, subject: table|userdata, target: table, easing: string|function): Tween --- Easing function type signature --- @alias EasingFunction fun(t: number, b: number, c: number, d: number): number --- Collection of all easing functions --- @alias EasingFunctions --- @field linear EasingFunction --- @field inQuad EasingFunction --- @field outQuad EasingFunction --- @field inOutQuad EasingFunction --- @field outInQuad EasingFunction --- @field inCubic EasingFunction --- @field outCubic EasingFunction --- @field inOutCubic EasingFunction --- @field outInCubic EasingFunction --- @field inQuart EasingFunction --- @field outQuart EasingFunction --- @field inOutQuart EasingFunction --- @field outInQuart EasingFunction --- @field inQuint EasingFunction --- @field outQuint EasingFunction --- @field inOutQuint EasingFunction --- @field outInQuint EasingFunction --- @field inSine EasingFunction --- @field outSine EasingFunction --- @field inOutSine EasingFunction --- @field outInSine EasingFunction --- @field inExpo EasingFunction --- @field outExpo EasingFunction --- @field inOutExpo EasingFunction --- @field outInExpo EasingFunction --- @field inCirc EasingFunction --- @field outCirc EasingFunction --- @field inOutCirc EasingFunction --- @field outInCirc EasingFunction --- @field inElastic fun(t: number, b: number, c: number, d: number, a?: number, p?: number): number --- @field outElastic fun(t: number, b: number, c: number, d: number, a?: number, p?: number): number --- @field inOutElastic fun(t: number, b: number, c: number, d: number, a?: number, p?: number): number --- @field outInElastic fun(t: number, b: number, c: number, d: number, a?: number, p?: number): number --- @field inBack fun(t: number, b: number, c: number, d: number, s?: number): number --- @field outBack fun(t: number, b: number, c: number, d: number, s?: number): number --- @field inOutBack fun(t: number, b: number, c: number, d: number, s?: number): number --- @field outInBack fun(t: number, b: number, c: number, d: number, s?: number): number --- @field inBounce EasingFunction --- @field outBounce EasingFunction --- @field inOutBounce EasingFunction --- @field outInBounce EasingFunction local tween = { _VERSION = 'tween 2.1.1', _DESCRIPTION = 'tweening for lua', _URL = 'https://github.com/kikito/tween.lua', _LICENSE = [[ MIT LICENSE Copyright (c) 2014 Enrique GarcĂ­a Cota, Yuichi Tateno, Emmanuel Oga 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. ]] } -- easing -- Adapted from https://github.com/EmmanuelOga/easing. See LICENSE.txt for credits. -- For all easing functions: -- @param t number - time (how much time has to pass for the tweening to complete) -- @param b number - begin (starting property value) -- @param c number - change (ending - beginning) -- @param d number - duration (running time, how much time has passed *right now*) -- @return number - the eased value local pow, sin, cos, pi, sqrt, abs, asin = math.pow, math.sin, math.cos, math.pi, math.sqrt, math.abs, math.asin -- linear --- @type EasingFunction local function linear(t, b, c, d) return c * t / d + b end -- quad --- @type EasingFunction local function inQuad(t, b, c, d) return c * pow(t / d, 2) + b end --- @type EasingFunction local function outQuad(t, b, c, d) t = t / d return -c * t * (t - 2) + b end --- @type EasingFunction local function inOutQuad(t, b, c, d) t = t / d * 2 if t < 1 then return c / 2 * pow(t, 2) + b end return -c / 2 * ((t - 1) * (t - 3) - 1) + b end --- @type EasingFunction local function outInQuad(t, b, c, d) if t < d / 2 then return outQuad(t * 2, b, c / 2, d) end return inQuad((t * 2) - d, b + c / 2, c / 2, d) end -- cubic --- @type EasingFunction local function inCubic (t, b, c, d) return c * pow(t / d, 3) + b end --- @type EasingFunction local function outCubic(t, b, c, d) return c * (pow(t / d - 1, 3) + 1) + b end --- @type EasingFunction local function inOutCubic(t, b, c, d) t = t / d * 2 if t < 1 then return c / 2 * t * t * t + b end t = t - 2 return c / 2 * (t * t * t + 2) + b end --- @type EasingFunction local function outInCubic(t, b, c, d) if t < d / 2 then return outCubic(t * 2, b, c / 2, d) end return inCubic((t * 2) - d, b + c / 2, c / 2, d) end -- quart --- @type EasingFunction local function inQuart(t, b, c, d) return c * pow(t / d, 4) + b end --- @type EasingFunction local function outQuart(t, b, c, d) return -c * (pow(t / d - 1, 4) - 1) + b end --- @type EasingFunction local function inOutQuart(t, b, c, d) t = t / d * 2 if t < 1 then return c / 2 * pow(t, 4) + b end return -c / 2 * (pow(t - 2, 4) - 2) + b end --- @type EasingFunction local function outInQuart(t, b, c, d) if t < d / 2 then return outQuart(t * 2, b, c / 2, d) end return inQuart((t * 2) - d, b + c / 2, c / 2, d) end -- quint --- @type EasingFunction local function inQuint(t, b, c, d) return c * pow(t / d, 5) + b end --- @type EasingFunction local function outQuint(t, b, c, d) return c * (pow(t / d - 1, 5) + 1) + b end --- @type EasingFunction local function inOutQuint(t, b, c, d) t = t / d * 2 if t < 1 then return c / 2 * pow(t, 5) + b end return c / 2 * (pow(t - 2, 5) + 2) + b end --- @type EasingFunction local function outInQuint(t, b, c, d) if t < d / 2 then return outQuint(t * 2, b, c / 2, d) end return inQuint((t * 2) - d, b + c / 2, c / 2, d) end -- sine --- @type EasingFunction local function inSine(t, b, c, d) return -c * cos(t / d * (pi / 2)) + c + b end --- @type EasingFunction local function outSine(t, b, c, d) return c * sin(t / d * (pi / 2)) + b end --- @type EasingFunction local function inOutSine(t, b, c, d) return -c / 2 * (cos(pi * t / d) - 1) + b end --- @type EasingFunction local function outInSine(t, b, c, d) if t < d / 2 then return outSine(t * 2, b, c / 2, d) end return inSine((t * 2) -d, b + c / 2, c / 2, d) end -- expo --- @type EasingFunction local function inExpo(t, b, c, d) if t == 0 then return b end return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001 end --- @type EasingFunction local function outExpo(t, b, c, d) if t == d then return b + c end return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b end --- @type EasingFunction local function inOutExpo(t, b, c, d) if t == 0 then return b end if t == d then return b + c end t = t / d * 2 if t < 1 then return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005 end return c / 2 * 1.0005 * (-pow(2, -10 * (t - 1)) + 2) + b end --- @type EasingFunction local function outInExpo(t, b, c, d) if t < d / 2 then return outExpo(t * 2, b, c / 2, d) end return inExpo((t * 2) - d, b + c / 2, c / 2, d) end -- circ --- @type EasingFunction local function inCirc(t, b, c, d) return(-c * (sqrt(1 - pow(t / d, 2)) - 1) + b) end --- @type EasingFunction local function outCirc(t, b, c, d) return(c * sqrt(1 - pow(t / d - 1, 2)) + b) end --- @type EasingFunction local function inOutCirc(t, b, c, d) t = t / d * 2 if t < 1 then return -c / 2 * (sqrt(1 - t * t) - 1) + b end t = t - 2 return c / 2 * (sqrt(1 - t * t) + 1) + b end --- @type EasingFunction local function outInCirc(t, b, c, d) if t < d / 2 then return outCirc(t * 2, b, c / 2, d) end return inCirc((t * 2) - d, b + c / 2, c / 2, d) end -- elastic --- Calculate Period, Amplitude, and Shift parameters --- @param p number|nil period --- @param a number|nil amplitude --- @param c number change --- @param d number duration --- @return number p period, number a amplitude, number s shift local function calculatePAS(p,a,c,d) p, a = p or d * 0.3, a or 0 if a < abs(c) then return p, c, p / 4 end -- p, a, s return p, a, p / (2 * pi) * asin(c/a) -- p,a,s end --- @type fun(t: number, b: number, c: number, d: number, a?: number, p?: number): number local function inElastic(t, b, c, d, a, p) local s if t == 0 then return b end t = t / d if t == 1 then return b + c end p,a,s = calculatePAS(p,a,c,d) t = t - 1 return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b end --- @type fun(t: number, b: number, c: number, d: number, a?: number, p?: number): number local function outElastic(t, b, c, d, a, p) local s if t == 0 then return b end t = t / d if t == 1 then return b + c end p,a,s = calculatePAS(p,a,c,d) return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) + c + b end --- @type fun(t: number, b: number, c: number, d: number, a?: number, p?: number): number local function inOutElastic(t, b, c, d, a, p) local s if t == 0 then return b end t = t / d * 2 if t == 2 then return b + c end p,a,s = calculatePAS(p,a,c,d) t = t - 1 if t < 0 then return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b end return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p ) * 0.5 + c + b end --- @type fun(t: number, b: number, c: number, d: number, a?: number, p?: number): number local function outInElastic(t, b, c, d, a, p) if t < d / 2 then return outElastic(t * 2, b, c / 2, d, a, p) end return inElastic((t * 2) - d, b + c / 2, c / 2, d, a, p) end -- back --- @type fun(t: number, b: number, c: number, d: number, s?: number): number local function inBack(t, b, c, d, s) s = s or 1.70158 t = t / d return c * t * t * ((s + 1) * t - s) + b end --- @type fun(t: number, b: number, c: number, d: number, s?: number): number local function outBack(t, b, c, d, s) s = s or 1.70158 t = t / d - 1 return c * (t * t * ((s + 1) * t + s) + 1) + b end --- @type fun(t: number, b: number, c: number, d: number, s?: number): number local function inOutBack(t, b, c, d, s) s = (s or 1.70158) * 1.525 t = t / d * 2 if t < 1 then return c / 2 * (t * t * ((s + 1) * t - s)) + b end t = t - 2 return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b end --- @type fun(t: number, b: number, c: number, d: number, s?: number): number local function outInBack(t, b, c, d, s) if t < d / 2 then return outBack(t * 2, b, c / 2, d, s) end return inBack((t * 2) - d, b + c / 2, c / 2, d, s) end -- bounce --- @type EasingFunction local function outBounce(t, b, c, d) t = t / d if t < 1 / 2.75 then return c * (7.5625 * t * t) + b end if t < 2 / 2.75 then t = t - (1.5 / 2.75) return c * (7.5625 * t * t + 0.75) + b elseif t < 2.5 / 2.75 then t = t - (2.25 / 2.75) return c * (7.5625 * t * t + 0.9375) + b end t = t - (2.625 / 2.75) return c * (7.5625 * t * t + 0.984375) + b end --- @type EasingFunction local function inBounce(t, b, c, d) return c - outBounce(d - t, 0, c, d) + b end --- @type EasingFunction local function inOutBounce(t, b, c, d) if t < d / 2 then return inBounce(t * 2, 0, c, d) * 0.5 + b end return outBounce(t * 2 - d, 0, c, d) * 0.5 + c * .5 + b end --- @type EasingFunction local function outInBounce(t, b, c, d) if t < d / 2 then return outBounce(t * 2, b, c / 2, d) end return inBounce((t * 2) - d, b + c / 2, c / 2, d) end tween.easing = { linear = linear, inQuad = inQuad, outQuad = outQuad, inOutQuad = inOutQuad, outInQuad = outInQuad, inCubic = inCubic, outCubic = outCubic, inOutCubic = inOutCubic, outInCubic = outInCubic, inQuart = inQuart, outQuart = outQuart, inOutQuart = inOutQuart, outInQuart = outInQuart, inQuint = inQuint, outQuint = outQuint, inOutQuint = inOutQuint, outInQuint = outInQuint, inSine = inSine, outSine = outSine, inOutSine = inOutSine, outInSine = outInSine, inExpo = inExpo, outExpo = outExpo, inOutExpo = inOutExpo, outInExpo = outInExpo, inCirc = inCirc, outCirc = outCirc, inOutCirc = inOutCirc, outInCirc = outInCirc, inElastic = inElastic, outElastic = outElastic, inOutElastic = inOutElastic, outInElastic = outInElastic, inBack = inBack, outBack = outBack, inOutBack = inOutBack, outInBack = outInBack, inBounce = inBounce, outBounce = outBounce, inOutBounce = inOutBounce, outInBounce = outInBounce } -- private stuff --- Recursively copy tables from source to destination --- @generic T: table --- @param destination T --- @param keysTable T --- @param valuesTable T|nil --- @return T local function copyTables(destination, keysTable, valuesTable) valuesTable = valuesTable or keysTable local mt = getmetatable(keysTable) if mt and getmetatable(destination) == nil then setmetatable(destination, mt) end for k,v in pairs(keysTable) do if type(v) == 'table' then destination[k] = copyTables({}, v, valuesTable[k]) else destination[k] = valuesTable[k] end end return destination end --- Check subject and target recursively for valid tween parameters --- @param subject table|userdata --- @param target table --- @param path string[]|nil local function checkSubjectAndTargetRecursively(subject, target, path) path = path or {} local targetType, newPath for k,targetValue in pairs(target) do targetType, newPath = type(targetValue), copyTables({}, path) table.insert(newPath, tostring(k)) if targetType == 'number' then assert(type(subject[k]) == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' is missing from subject or isn't a number") elseif targetType == 'table' then checkSubjectAndTargetRecursively(subject[k], targetValue, newPath) else assert(targetType == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' must be a number or table of numbers") end end end --- Validate parameters for tween.new() --- @param duration number --- @param subject table|userdata --- @param target table --- @param easing function local function checkNewParams(duration, subject, target, easing) assert(type(duration) == 'number' and duration > 0, "duration must be a positive number. Was " .. tostring(duration)) local tsubject = type(subject) assert(tsubject == 'table' or tsubject == 'userdata', "subject must be a table or userdata. Was " .. tostring(subject)) assert(type(target)== 'table', "target must be a table. Was " .. tostring(target)) assert(type(easing)=='function', "easing must be a function. Was " .. tostring(easing)) checkSubjectAndTargetRecursively(subject, target) end --- Convert easing string to function or return as-is if already a function --- @param easing string|function|nil --- @return function local function getEasingFunction(easing) easing = easing or "linear" if type(easing) == 'string' then local name = easing easing = tween.easing[name] if type(easing) ~= 'function' then error("The easing function name '" .. name .. "' is invalid") end end return easing end --- Apply easing function to subject properties recursively --- @param subject table|userdata --- @param target table --- @param initial table --- @param clock number --- @param duration number --- @param easing function local function performEasingOnSubject(subject, target, initial, clock, duration, easing) local t,b,c,d for k,v in pairs(target) do if type(v) == 'table' then performEasingOnSubject(subject[k], v, initial[k], clock, duration, easing) else t,b,c,d = clock, initial[k], v - initial[k], duration subject[k] = easing(t,b,c,d) end end end --- Tween object for animating property changes --- @class Tween --- @field duration number Total duration of the tween --- @field subject table|userdata The object being tweened --- @field target table Target values for the tween --- @field easing function Easing function used --- @field clock number Current elapsed time --- @field initial table|nil Initial values (computed on first set/update) local Tween = {} local Tween_mt = {__index = Tween} --- Set the tween to a specific time --- @param clock number The time to set the tween to (must be >= 0) --- @return boolean Returns true if the tween has completed function Tween:set(clock) assert(type(clock) == 'number', "clock must be a positive number or 0") self.initial = self.initial or copyTables({}, self.target, self.subject) self.clock = clock if self.clock <= 0 then self.clock = 0 copyTables(self.subject, self.initial) elseif self.clock >= self.duration then -- the tween has expired self.clock = self.duration copyTables(self.subject, self.target) else performEasingOnSubject(self.subject, self.target, self.initial, self.clock, self.duration, self.easing) end return self.clock >= self.duration end --- Reset the tween to the beginning --- @return boolean Returns true (tween is at start, not completed) function Tween:reset() return self:set(0) end --- Update the tween by a delta time --- @param dt number Delta time to advance the tween by --- @return boolean Returns true if the tween has completed function Tween:update(dt) assert(type(dt) == 'number', "dt must be a number") return self:set(self.clock + dt) end -- Public interface --- Create a new tween object --- @param duration number Duration of the tween in seconds/ticks --- @param subject table|userdata The object whose properties will be tweened --- @param target table Target values for the properties (same structure as subject) --- @param easing string|function|nil Easing function name (string) or custom easing function (defaults to "linear") --- @return Tween function tween.new(duration, subject, target, easing) easing = getEasingFunction(easing) checkNewParams(duration, subject, target, easing) return setmetatable({ duration = duration, subject = subject, target = target, easing = easing, clock = 0 }, Tween_mt) end return tween