aboutsummaryrefslogtreecommitdiff
path: root/.config/awesome/quarrel/animation
diff options
context:
space:
mode:
authordelta <darkussdelta@gmail.com>2025-07-04 00:38:29 +0200
committerdelta <darkussdelta@gmail.com>2025-07-04 00:38:29 +0200
commitb3530d7c4a102935fa26498a160ee1dc6c1e9c03 (patch)
treed7751206a694bc5de2d6b34b0c077cfcd1855798 /.config/awesome/quarrel/animation
parentdf75ec5ed5e3848c497f0439acb43ec9246ad3e7 (diff)
:3
Diffstat (limited to '.config/awesome/quarrel/animation')
-rw-r--r--.config/awesome/quarrel/animation/bezier.lua89
-rw-r--r--.config/awesome/quarrel/animation/init.lua249
-rw-r--r--.config/awesome/quarrel/animation/subscribable/init.lua47
-rw-r--r--.config/awesome/quarrel/animation/tween/init.lua554
4 files changed, 939 insertions, 0 deletions
diff --git a/.config/awesome/quarrel/animation/bezier.lua b/.config/awesome/quarrel/animation/bezier.lua
new file mode 100644
index 0000000..a505e48
--- /dev/null
+++ b/.config/awesome/quarrel/animation/bezier.lua
@@ -0,0 +1,89 @@
+local gtable = require "gears.table"
+
+-- port of https://github.com/WebKit/WebKit/blob/da934454c84ac2dcbf9fca9e5f4ac2644ef25d72/Source/WebCore/platform/graphics/UnitBezier.h
+
+local bezier = {}
+
+function bezier:sample_x(t)
+ -- `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
+ return ((self.ax * t + self.bx) * t + self.cx) * t
+end
+
+function bezier:sample_y(t)
+ return ((self.ay * t + self.by) * t + self.cy) * t
+end
+
+function bezier:sample_derivative_x(t)
+ return (3.0 * self.ax * t + 2.0 * self.bx) * t + self.cx
+end
+
+function bezier:solve_x(x, epsilon)
+ local x2, d2
+ local t2 = x
+
+ -- First try a few iterations of Newton's method -- normally very fast.
+ for _ = 1, 8 do
+ x2 = self:sample_x(t2) - x
+ if math.abs(x2) < epsilon then
+ return t2
+ end
+ d2 = self:sample_derivative_x(t2)
+ if math.abs(d2) < 1e-6 then
+ break
+ end
+ t2 = t2 - x2 / d2
+ end
+
+ -- Fall back to the bisection method for reliability.
+ local t0 = 0
+ local t1 = 1
+ t2 = x
+
+ if t2 < t0 then
+ return t0
+ end
+ if t2 > t1 then
+ return t1
+ end
+
+ while t0 < t1 do
+ x2 = self:sample_x(t2)
+ if math.abs(x2 - x) < epsilon then
+ return t2
+ end
+ if x > x2 then
+ t0 = t2
+ else
+ t1 = t2
+ end
+ t2 = (t1 - t0) * 0.5 + t0
+ end
+
+ -- Failure.
+ return t2
+end
+
+function bezier:solve(x, epsilon)
+ return self:sample_y(self:solve_x(x, epsilon))
+end
+
+local function new(x1, y1, x2, y2)
+ local obj = gtable.crush({}, bezier)
+
+ -- Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1).
+ obj.cx = 3.0 * x1
+ obj.bx = 3.0 * (x2 - x1) - obj.cx
+ obj.ax = 1.0 - obj.cx - obj.bx
+
+ obj.cy = 3.0 * y1
+ obj.by = 3.0 * (y2 - y1) - obj.cy
+ obj.ay = 1.0 - obj.cy - obj.by
+
+ return obj
+end
+
+return setmetatable(bezier, {
+ __call = function(_, ...)
+ return new(...)
+ end,
+})
diff --git a/.config/awesome/quarrel/animation/init.lua b/.config/awesome/quarrel/animation/init.lua
new file mode 100644
index 0000000..c57b01a
--- /dev/null
+++ b/.config/awesome/quarrel/animation/init.lua
@@ -0,0 +1,249 @@
+-------------------------------------------
+-- @author https://github.com/Kasper24
+-- @copyright 2021-2025 Kasper24
+-------------------------------------------
+local GLib = require("lgi").GLib
+local gobject = require "gears.object"
+local gpcall = require "gears.protected_call"
+local gtable = require "gears.table"
+local gtimer = require "gears.timer"
+local subscribable = require "quarrel.animation.subscribable"
+-- local qconsts = require "quarrel.consts"
+local qtween = require "quarrel.animation.tween"
+-- local qbezier = require "quarrel.animation.bezier"
+local ipairs = ipairs
+local table = table
+local pairs = pairs
+
+local animation_manager = {}
+
+-- local easing_bezier = qbezier(0.2, 0, 0, 1)
+-- animation_manager.consts = qconsts.protect(function()
+-- local duration = 0.3
+-- return {
+-- DURATION = duration,
+-- INTRO = duration / 4,
+-- EASING = function(t, b, c, d)
+-- local epsilon = 1000 / d
+-- return c * easing_bezier:solve(t/d, epsilon) + b
+-- end
+-- }
+-- end)
+
+local animation = {}
+
+local function second_to_micro(sec)
+ return sec * 1000000
+end
+
+local function framerate_tomilli(framerate)
+ return 1000 / framerate
+end
+
+local function on_no_running_animations(self, callback)
+ gtimer.start_new(0.1, function()
+ if #self._private.animations <= 0 then
+ callback()
+ return false
+ else
+ local has_non_looped_anim = false
+ for _, animation in ipairs(self._private.animations) do
+ if animation.loop == false then
+ has_non_looped_anim = true
+ end
+ end
+
+ if has_non_looped_anim == false then
+ callback()
+ return false
+ end
+ end
+
+ return true
+ end)
+end
+
+local function animation_loop(self)
+ self._private.source_id = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ framerate_tomilli(self._private.framerate),
+ function()
+ for index, animation in ipairs(self._private.animations) do
+ if animation.state == true then
+ -- compute delta time
+ local time = GLib.get_monotonic_time()
+ local delta = time - animation.last_elapsed
+ animation.last_elapsed = time
+
+ -- If pos is true, the animation has ended
+ local pos = gpcall(animation.tween.update, animation.tween, delta)
+ if pos == true then
+ -- Loop the animation, don't end it.
+ -- Useful for widgets like the spinning cicle
+ if animation.loop == true then
+ animation.tween:reset()
+ else
+ animation.state = false
+
+ -- Snap to end
+ animation.pos = animation.tween.target
+
+ gpcall(animation.emit_signal, animation, "update", animation.pos)
+ gpcall(animation.fire, animation, animation.pos)
+
+ gpcall(animation.emit_signal, animation, "ended", animation.pos)
+ gpcall(animation.ended.fire, animation, animation.pos)
+
+ table.remove(self._private.animations, index)
+ end
+ -- Animation in process, keep updating
+ else
+ animation.pos = pos
+
+ gpcall(animation.emit_signal, animation, "update", animation.pos)
+ gpcall(animation.fire, animation, animation.pos)
+ end
+ else
+ table.remove(self._private.animations, index)
+ end
+ end
+
+ -- call again the function after cooldown
+ return true
+ end
+ )
+end
+
+function animation:set(args)
+ args = args or {}
+
+ -- Awestoer/Rubbto compatibility
+ -- I'd rather this always be a table, but Awestore/Rubbto
+ -- except the :set() method to have 1 number value parameter
+ -- used to set the target
+ local is_table = type(args) == "table"
+ local initial = is_table and (args.pos or self.pos) or self.pos
+ local subject = is_table and (args.subject or self.subject) or self.subject
+ local target = is_table and (args.target or self.target) or args
+ local duration = is_table and (args.duration or self.duration) or self.duration
+ local easing = is_table and (args.easing or self.easing) or self.easing
+
+ if self.tween == nil or self.reset_on_stop == true then
+ self.tween = qtween.new {
+ initial = initial,
+ subject = subject,
+ target = target,
+ duration = second_to_micro(duration),
+ easing = easing,
+ }
+ end
+
+ if self._private.anim_manager._private.instant and self.override_instant ~= true then
+ self.pos = self.tween.target
+ self:fire(self.pos)
+ self:emit_signal("update", self.pos)
+
+ self.state = false
+ self.ended:fire(self.pos)
+ self:emit_signal("ended", self.pos)
+ return
+ end
+
+ if self._private.anim_manager._private.animations[self.index] == nil then
+ table.insert(self._private.anim_manager._private.animations, self)
+ end
+
+ self.state = true
+ self.last_elapsed = GLib.get_monotonic_time()
+
+ self.started:fire()
+ self:emit_signal "started"
+end
+
+-- Rubato compatibility
+function animation:abort()
+ self.state = false
+end
+
+function animation:stop()
+ self.state = false
+end
+
+function animation:initial()
+ return self._private.initial
+end
+
+function animation_manager:set_instant(value)
+ if value == true and self._private.instant == false then
+ on_no_running_animations(self, function()
+ -- GLib.source_remove(self._private.source_id)
+ self._private.instant = true
+ end)
+ elseif self._private.instant == true then
+ self._private.instant = false
+ -- animation_loop(self)
+ end
+end
+
+function animation_manager:set_framerate(value)
+ self._private.framerate = value
+ -- if self._private.instant == false then
+ on_no_running_animations(self, function()
+ GLib.source_remove(self._private.source_id)
+ animation_loop(self)
+ end)
+ -- end
+end
+
+function animation_manager:new(args)
+ args = args or {}
+
+ args.pos = args.pos or 0
+ args.subject = args.subject or nil
+ args.target = args.target or nil
+ args.duration = args.duration or 0
+ args.easing = args.easing or nil
+ args.loop = args.loop or false
+ args.signals = args.signals or {}
+ args.update = args.update or nil
+ args.reset_on_stop = args.reset_on_stop == nil and true or args.reset_on_stop
+
+ -- Awestoer/Rubbto compatibility
+ args.subscribed = args.subscribed or nil
+ local ret = subscribable()
+ ret.started = subscribable()
+ ret.ended = subscribable()
+ if args.subscribed ~= nil then
+ ret:subscribe(args.subscribed)
+ end
+
+ for sig, sigfun in pairs(args.signals) do
+ ret:connect_signal(sig, sigfun)
+ end
+ if args.update ~= nil then
+ ret:connect_signal("update", args.update)
+ end
+
+ gtable.crush(ret, args, true)
+ gtable.crush(ret, animation, true)
+
+ ret._private = {}
+ ret._private.anim_manager = self
+ ret._private.initial = args.pos
+ -- Can't have it private for rubato compatibility
+ ret.state = false
+
+ return ret
+end
+
+local instance = gobject {}
+gtable.crush(instance, animation_manager, true)
+
+instance._private = {}
+instance._private.animations = {}
+instance._private.instant = false
+instance._private.framerate = 60
+
+animation_loop(instance)
+
+return instance
diff --git a/.config/awesome/quarrel/animation/subscribable/init.lua b/.config/awesome/quarrel/animation/subscribable/init.lua
new file mode 100644
index 0000000..9542704
--- /dev/null
+++ b/.config/awesome/quarrel/animation/subscribable/init.lua
@@ -0,0 +1,47 @@
+-------------------------------------------
+-- @author https://github.com/Kasper24
+-- @copyright 2021-2025 Kasper24
+-- Adopted from rubato
+-------------------------------------------
+local gobject = require "gears.object"
+
+-- Kidna copying awesotre's stores on a surface level for added compatibility
+local function subscribable(args)
+ local ret = gobject {}
+ local subscribed = {}
+
+ -- Subscrubes a function to the object so that it's called when `fire` is
+ -- Calls subscribe_callback if it exists as well
+ function ret:subscribe(func)
+ local id = tostring(func):gsub("function: ", "")
+ subscribed[id] = func
+
+ if self.subscribe_callback then
+ self.subscribe_callback(func)
+ end
+ end
+
+ -- Unsubscribes a function and calls unsubscribe_callback if it exists
+ function ret:unsubscribe(func)
+ if not func then
+ subscribed = {}
+ else
+ local id = tostring(func):gsub("function: ", "")
+ subscribed[id] = nil
+ end
+
+ if self.unsubscribe_callback then
+ self.unsubscribe_callback(func)
+ end
+ end
+
+ function ret:fire(...)
+ for _, func in pairs(subscribed) do
+ func(...)
+ end
+ end
+
+ return ret
+end
+
+return subscribable
diff --git a/.config/awesome/quarrel/animation/tween/init.lua b/.config/awesome/quarrel/animation/tween/init.lua
new file mode 100644
index 0000000..5f4ce51
--- /dev/null
+++ b/.config/awesome/quarrel/animation/tween/init.lua
@@ -0,0 +1,554 @@
+-------------------------------------------
+-- @author https://github.com/Kasper24
+-- @copyright 2021-2025 Kasper24
+-------------------------------------------
+-- easing
+-- Adapted from https://github.com/EmmanuelOga/easing. See LICENSE.txt for credits.
+-- For all easing functions:
+-- t = time == how much time has to pass for the tweening to complete
+-- b = begin == starting property value
+-- c = change == ending - beginning
+-- d = duration == running time. How much time has passed *right now*
+-- local Color = require "external.lua-color"
+local gobject = require "gears.object"
+local gtable = require "gears.table"
+local tostring = tostring
+local assert = assert
+local table = table
+local pairs = pairs
+local error = error
+
+local type = type
+
+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.
+ ]],
+}
+
+local pow, sin, cos, pi, sqrt, abs, asin = math.pow, math.sin, math.cos, math.pi, math.sqrt, math.abs, math.asin
+
+-- linear
+local function linear(t, b, c, d)
+ return c * t / d + b
+end
+
+-- quad
+local function inQuad(t, b, c, d)
+ return c * pow(t / d, 2) + b
+end
+local function outQuad(t, b, c, d)
+ t = t / d
+ return -c * t * (t - 2) + b
+end
+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
+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
+local function inCubic(t, b, c, d)
+ return c * pow(t / d, 3) + b
+end
+local function outCubic(t, b, c, d)
+ return c * (pow(t / d - 1, 3) + 1) + b
+end
+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
+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
+local function inQuart(t, b, c, d)
+ return c * pow(t / d, 4) + b
+end
+local function outQuart(t, b, c, d)
+ return -c * (pow(t / d - 1, 4) - 1) + b
+end
+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
+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
+local function inQuint(t, b, c, d)
+ return c * pow(t / d, 5) + b
+end
+local function outQuint(t, b, c, d)
+ return c * (pow(t / d - 1, 5) + 1) + b
+end
+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
+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
+local function inSine(t, b, c, d)
+ return -c * cos(t / d * (pi / 2)) + c + b
+end
+local function outSine(t, b, c, d)
+ return c * sin(t / d * (pi / 2)) + b
+end
+local function inOutSine(t, b, c, d)
+ return -c / 2 * (cos(pi * t / d) - 1) + b
+end
+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
+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
+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
+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
+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
+local function inCirc(t, b, c, d)
+ return (-c * (sqrt(1 - pow(t / d, 2)) - 1) + b)
+end
+local function outCirc(t, b, c, d)
+ return (c * sqrt(1 - pow(t / d - 1, 2)) + b)
+end
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+local function inBounce(t, b, c, d)
+ return c - outBounce(d - t, 0, c, d) + b
+end
+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 * 0.5 + b
+end
+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 interface
+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
+
+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
+
+local function checkNewParams(initial, duration, subject, target, easing)
+ -- assert(type(initial) == 'number' and duration > 0, "duration must be a positive number. Was " .. tostring(duration))
+ -- assert(type(duration) == 'number' and duration > 0, "duration must be a positive number. Was " .. tostring(duration))
+ assert(type(easing) == "function", "easing must be a function. Was " .. tostring(easing))
+
+ if subject and target then
+ 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))
+ checkSubjectAndTargetRecursively(subject, target)
+ end
+end
+
+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
+
+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
+
+-- local function performEasingOnColor(initial, target, clock, duration, easing)
+-- initial = Color(initial)
+-- target = Color(target)
+--
+-- local r = easing(clock, initial.r, target.r - initial.r, duration)
+-- local g = easing(clock, initial.g, target.g - initial.g, duration)
+-- local b = easing(clock, initial.b, target.b - initial.b, duration)
+-- local a = easing(clock, initial.a, target.a - initial.a, duration)
+--
+-- return tostring(Color { r = r, g = g, b = b, a = a })
+-- end
+
+local function performEasing(table, initial, target, clock, duration, easing)
+ if type(target) == "table" then
+ local t, b, c, d
+ for k, target in pairs(target) do
+ if type(target) == "table" then
+ table[k] = {}
+ performEasing(table[k], initial[k], target, clock, duration, easing)
+ -- elseif type(target) == "string" and target:sub(1, 1) == "#" then
+ -- table[k] = performEasingOnColor(initial[k], target, clock, duration, easing)
+ else
+ t, b, c, d = clock, initial[k], target - initial[k], duration
+ table[k] = easing(t, b, c, d)
+ end
+ end
+
+ return table
+ -- elseif type(target) == "string" and target:sub(1, 1) == "#" then
+ -- return performEasingOnColor(initial, target, clock, duration, easing)
+ else
+ local t, b, c, d = clock, initial, target - initial, duration
+ return easing(t, b, c, d)
+ end
+end
+
+-- Public interface
+local Tween = {}
+
+function Tween:set(clock)
+ assert(type(clock) == "number", "clock must be a positive number or 0")
+
+ if self.subject and self.initial == 0 then
+ self.initial = copyTables({}, self.target, self.subject)
+ end
+
+ self.clock = clock
+
+ if self.clock <= 0 then
+ self.clock = 0
+ if self.subject then
+ copyTables(self.subject, self.initial)
+ end
+ elseif self.clock >= self.duration then -- the tween has expired
+ self.clock = self.duration
+
+ if self.subject then
+ copyTables(self.subject, self.target)
+ end
+ else
+ if self.subject then
+ performEasingOnSubject(self.subject, self.target, self.initial, self.clock, self.duration, self.easing)
+ else
+ local pos = {}
+ return performEasing(pos, self.initial, self.target, self.clock, self.duration, self.easing)
+ end
+ end
+
+ return self.clock >= self.duration
+end
+
+function Tween:update(dt)
+ assert(type(dt) == "number", "dt must be a number")
+ return self:set(self.clock + dt)
+end
+
+function Tween:reset()
+ return self:set(0)
+end
+
+function tween.new(args)
+ args = args or {}
+
+ args.initial = args.initial or 0
+ args.subject = args.subject or nil
+ args.target = args.target or nil
+ args.duration = args.duration or 0
+ args.easing = args.easing or nil
+
+ args.easing = getEasingFunction(args.easing)
+ checkNewParams(args.initial, args.duration, args.subject, args.target, args.easing)
+
+ local ret = gobject {}
+ ret.clock = 0
+
+ gtable.crush(ret, args, true)
+ gtable.crush(ret, Tween, true)
+
+ return ret
+end
+
+return tween