Files
DuvelCorp 566f3f4925 Add Animations feature + fix own-pet tooltip false positive
Animations (all optional, default on, per-effect toggles):
- New Options -> Animations tab with master switch + per-effect checkboxes
- Embed PizzaSauce animation library (api/PizzaSauce.lua)
- fx-celebrate: firework + banner when a pet learns a new ability/rank,
  triggered from core-scan-beasttraining pet-learn hook
- Hunter's Book: slide + fade on open, results list fade on tab switch
- Options window: slide + fade on open
- All new user-facing strings keyed (ANIM_* namespace)

Fixes:
- Tooltip #19: own-pet lines (mood/loyalty/happiness/XP) no longer
  misfire on party members / targets. Removed the fragile title-name
  fallback in MTH_TT_IsTooltipOnOwnPet; ownership now requires positive
  unit identity.

Temp (remove before release): /mth fx and /mth fxdebug diagnostics.
2026-09-10 14:41:26 +02:00

1348 lines
39 KiB
Lua

-- PizzaSauce: Smooth, declarative animation library for WoW 1.12
local _G = getfenv(0)
local lib = {}
lib.version = "0.1.1"
_G["PizzaSauce"] = lib
lib._types = {}
lib._easing = {}
lib._active = {}
lib._pending = {}
lib._pendingN = 0
lib._count = 0
lib._finBuf = {}
lib._frame = CreateFrame("Frame", nil, UIParent)
lib._frame:Hide()
local lastTime = GetTime()
local function activate(anim)
if anim._active then return end
anim._active = true
lib._count = lib._count + 1
lib._active[anim] = true
if lib._count == 1 then
lastTime = GetTime()
lib._frame:Show()
end
end
local function deactivate(anim)
if not anim._active then return end
anim._active = false
lib._count = lib._count - 1
lib._active[anim] = nil
if lib._count <= 0 then
lib._count = 0
if lib._pendingN == 0 then lib._frame:Hide() end
end
end
lib._frame:SetScript("OnUpdate", function()
local now = GetTime()
local dt = now - lastTime
lastTime = now
if dt <= 0 then return end
-- Process pending auto-play animations
local pendingN = lib._pendingN
if pendingN > 0 then
lib._pendingN = 0
for i = 1, pendingN do
local anim = lib._pending[i]
lib._pending[i] = nil
if anim._autoplay then anim:Play() end
end
end
-- Hide driver if nothing to do
if lib._count == 0 then
lib._frame:Hide()
return
end
-- Tick all active, collect finished into scratch buffer.
-- Can't finish inline because _finish() callbacks may create new animations,
-- which would modify _active while we're iterating it.
local finN = 0
for anim in pairs(lib._active) do
if anim:_tick(dt, now) then
finN = finN + 1
lib._finBuf[finN] = anim
end
end
for i = 1, finN do
lib._finBuf[i]:_finish()
lib._finBuf[i] = nil
end
end)
lib._activate = activate
lib._deactivate = deactivate
function lib._schedulePending(anim)
anim._autoplay = true
lib._pendingN = lib._pendingN + 1
lib._pending[lib._pendingN] = anim
lastTime = GetTime()
lib._frame:Show()
end
function lib:RegisterType(name, handlers)
lib._types[name] = handlers
end
function lib:RegisterEasing(name, fn)
lib._easing[name] = fn
end
function lib:RegisterHelper(name, fn)
if lib[name] then
DEFAULT_CHAT_FRAME:AddMessage(
"PizzaSauce: RegisterHelper('" .. name .. "') would overwrite an existing method")
return
end
lib[name] = fn
end
local E = lib._easing
local pi = math.pi
local sin = math.sin
local cos = math.cos
local pow = math.pow
local sqrt = math.sqrt
E["linear"] = function(t) return t end
E["swing"] = function(t) return 0.5 - cos(t * pi) / 2 end
-- Quad (power of 2)
E["inQuad"] = function(t) return t * t end
E["outQuad"] = function(t) return t * (2 - t) end
E["inOutQuad"] = function(t)
if t < 0.5 then return 2 * t * t end
return -1 + (4 - 2 * t) * t
end
-- Cubic (power of 3)
E["inCubic"] = function(t) return t * t * t end
E["outCubic"] = function(t) local u = t - 1; return u * u * u + 1 end
E["inOutCubic"] = function(t)
if t < 0.5 then return 4 * t * t * t end
local u = 2 * t - 2; return (u * u * u + 2) / 2
end
-- Quart (power of 4)
E["inQuart"] = function(t) return t * t * t * t end
E["outQuart"] = function(t) local u = t - 1; return 1 - u * u * u * u end
E["inOutQuart"] = function(t)
if t < 0.5 then return 8 * t * t * t * t end
local u = t - 1; return 1 - 8 * u * u * u * u
end
-- Quint (power of 5)
E["inQuint"] = function(t) return t * t * t * t * t end
E["outQuint"] = function(t) local u = t - 1; return 1 + u * u * u * u * u end
E["inOutQuint"] = function(t)
if t < 0.5 then return 16 * t * t * t * t * t end
local u = t - 1; return 1 + 16 * u * u * u * u * u
end
-- Sine
E["inSine"] = function(t) return 1 - cos(t * pi / 2) end
E["outSine"] = function(t) return sin(t * pi / 2) end
E["inOutSine"] = function(t) return 0.5 - cos(t * pi) / 2 end
-- Expo
E["inExpo"] = function(t)
if t == 0 then return 0 end
return pow(2, 10 * (t - 1))
end
E["outExpo"] = function(t)
if t == 1 then return 1 end
return 1 - pow(2, -10 * t)
end
E["inOutExpo"] = function(t)
if t == 0 or t == 1 then return t end
if t < 0.5 then return pow(2, 20 * t - 10) / 2 end
return (2 - pow(2, -20 * t + 10)) / 2
end
-- Circ
E["inCirc"] = function(t) return 1 - sqrt(1 - t * t) end
E["outCirc"] = function(t) local u = t - 1; return sqrt(1 - u * u) end
E["inOutCirc"] = function(t)
if t < 0.5 then return (1 - sqrt(1 - 4 * t * t)) / 2 end
local u = 2 * t - 2; return (sqrt(1 - u * u) + 1) / 2
end
-- Elastic
E["inElastic"] = function(t)
if t == 0 or t == 1 then return t end
return -pow(2, 10 * t - 10) * sin((t * 10 - 10.75) * (2 * pi) / 3)
end
E["outElastic"] = function(t)
if t == 0 or t == 1 then return t end
return pow(2, -10 * t) * sin((t * 10 - 0.75) * (2 * pi) / 3) + 1
end
E["inOutElastic"] = function(t)
if t == 0 or t == 1 then return t end
if t < 0.5 then
return -pow(2, 20 * t - 10) * sin((20 * t - 11.125) * (2 * pi) / 4.5) / 2
end
return pow(2, -20 * t + 10) * sin((20 * t - 11.125) * (2 * pi) / 4.5) / 2 + 1
end
-- Back
E["inBack"] = function(t)
local s = 1.70158
return t * t * ((s + 1) * t - s)
end
E["outBack"] = function(t)
local s = 1.70158
local u = t - 1
return u * u * ((s + 1) * u + s) + 1
end
E["inOutBack"] = function(t)
local s = 1.70158 * 1.525
if t < 0.5 then
return (2 * t) * (2 * t) * ((s + 1) * (2 * t) - s) / 2
end
local u = 2 * t - 2
return (u * u * ((s + 1) * u + s) + 2) / 2
end
-- Bounce
E["outBounce"] = function(t)
if t < 1 / 2.75 then
return 7.5625 * t * t
elseif t < 2 / 2.75 then
t = t - 1.5 / 2.75
return 7.5625 * t * t + 0.75
elseif t < 2.5 / 2.75 then
t = t - 2.25 / 2.75
return 7.5625 * t * t + 0.9375
else
t = t - 2.625 / 2.75
return 7.5625 * t * t + 0.984375
end
end
E["inBounce"] = function(t) return 1 - E["outBounce"](1 - t) end
E["inOutBounce"] = function(t)
if t < 0.5 then return (1 - E["outBounce"](1 - 2 * t)) / 2 end
return (1 + E["outBounce"](2 * t - 1)) / 2
end
local function lerp(a, b, t) return a + (b - a) * t end
local function rotCorner(a)
local r = math.rad(a)
return 0.5 + math.cos(r) / 1.4142, 0.5 + math.sin(r) / 1.4142
end
local function applyRotation(texture, angle)
local a = -angle
local ULx, ULy = rotCorner(a + 225)
local LLx, LLy = rotCorner(a + 135)
local URx, URy = rotCorner(a - 45)
local LRx, LRy = rotCorner(a + 45)
texture:SetTexCoord(ULx, ULy, LLx, LLy, URx, URy, LRx, LRy)
end
lib:RegisterType("alpha", {
init = function(target, from)
return from or target:GetAlpha()
end,
apply = function(target, from, to, t)
target:SetAlpha(lerp(from, to, t))
end,
})
lib:RegisterType("scale", {
init = function(target, from, to, tween)
tween._baseW = target:GetWidth()
tween._baseH = target:GetHeight()
return from or 1.0
end,
apply = function(target, from, to, t, tween)
local s = lerp(from, to, t)
target:SetWidth(tween._baseW * s)
target:SetHeight(tween._baseH * s)
end,
})
lib:RegisterType("size", {
init = function(target, from)
return from or { target:GetWidth(), target:GetHeight() }
end,
apply = function(target, from, to, t)
target:SetWidth(lerp(from[1], to[1], t))
target:SetHeight(lerp(from[2], to[2], t))
end,
})
lib:RegisterType("position", {
init = function(target, from, to, tween)
local point, rel, relPoint, x, y = target:GetPoint(1)
tween._point = point or "CENTER"
tween._rel = rel
tween._relPoint = relPoint or "CENTER"
return from or { x or 0, y or 0 }
end,
apply = function(target, from, to, t, tween)
target:ClearAllPoints()
target:SetPoint(tween._point, tween._rel, tween._relPoint,
lerp(from[1], to[1], t), lerp(from[2], to[2], t))
end,
})
lib:RegisterType("color", {
init = function(target, from)
return from or { 1, 1, 1, 1 }
end,
apply = function(target, from, to, t)
target:SetVertexColor(
lerp(from[1], to[1], t),
lerp(from[2], to[2], t),
lerp(from[3], to[3], t),
lerp(from[4] or 1, to[4] or 1, t))
end,
})
lib:RegisterType("rotation", {
init = function(target, from)
return from or 0
end,
apply = function(target, from, to, t)
applyRotation(target, lerp(from, to, t))
end,
})
lib:RegisterType("custom", {
init = function(target, from, to, tween)
if from ~= nil then return from end
if tween._getter then return tween._getter(target) end
return 0
end,
apply = function(target, from, to, t, tween)
if tween._setter then
tween._setter(target, lerp(from, to, t))
end
end,
})
local function resolveEasing(easing)
if type(easing) == "function" then return easing end
return lib._easing[easing] or lib._easing["inOutQuad"]
end
local Tween = {}
function Tween:Play()
self._elapsed = 0
self._started = false
self._done = false
lib._activate(self)
end
function Tween:Cancel()
if not self._active then return end
lib._deactivate(self)
self._done = true
if self._onCancel then self:_onCancel() end
end
function Tween:IsPlaying()
return self._active == true
end
function Tween:_start()
local handler = lib._types[self._type]
if handler and handler.init then
self._from = handler.init(self._target, self._from, self._to, self)
end
self._started = true
if self._onStart then self:_onStart() end
end
function Tween:_tick(dt)
self._elapsed = self._elapsed + dt
if self._elapsed < self._delay then return false end
if not self._started then self:_start() end
local t = self._duration > 0
and math.min((self._elapsed - self._delay) / self._duration, 1)
or 1
local handler = lib._types[self._type]
if handler and handler.apply and self._target then
handler.apply(self._target, self._from, self._to, resolveEasing(self._easing)(t), self)
end
if self._onUpdate then self:_onUpdate(t) end
return t >= 1
end
function Tween:_finish()
lib._deactivate(self)
self._done = true
-- Apply one final time at exactly t=1 -- the last _tick may have landed
-- slightly before 1.0 depending on frame timing.
local handler = lib._types[self._type]
if handler and handler.apply and self._target then
handler.apply(self._target, self._from, self._to, 1, self)
end
if handler and handler.cleanup then
handler.cleanup(self._target, self)
end
if self._onFinish then self:_onFinish() end
end
function Tween:SetFrom(val) self._from = val end
function Tween:SetTo(val) self._to = val end
function Tween:SetDuration(val) self._duration = val end
function Tween:SetDelay(val) self._delay = val end
function Tween:SetTarget(val) self._target = val end
function Tween:_reset()
self._elapsed = 0
self._started = false
self._done = false
end
local TweenMT = { __index = Tween }
local KNOWN_OPTS = {
type = true, from = true, to = true, duration = true, easing = true,
delay = true, onStart = true, onUpdate = true, onFinish = true,
onCancel = true, getter = true, setter = true, defer = true,
}
function lib:Tween(target, opts)
local tween = setmetatable({
_target = target,
_type = opts.type or "alpha",
_from = opts.from,
_to = opts.to,
_duration = opts.duration or 0.3,
_easing = opts.easing or "inOutQuad",
_delay = opts.delay or 0,
_onStart = opts.onStart,
_onUpdate = opts.onUpdate,
_onFinish = opts.onFinish,
_onCancel = opts.onCancel,
_getter = opts.getter,
_setter = opts.setter,
}, TweenMT)
-- Forward extra opts to the tween for custom type handlers.
-- Internal fields use underscore prefix, so user params (vx, radius, etc.)
-- are accessible as tween.vx, tween.radius without collision.
for k, v in pairs(opts) do
if not KNOWN_OPTS[k] then
tween[k] = v
end
end
if not opts.defer then
lib._schedulePending(tween)
end
return tween
end
local Sequence = {}
function Sequence:Play()
self._index = 1
self._loopCount = 0
self._elapsed = 0
self._done = false
self:_resetChildren()
lib._activate(self)
end
function Sequence:Cancel()
if not self._active then return end
local child = self._children[self._index]
if child and child._started and child.Cancel then child:Cancel() end
lib._deactivate(self)
self._done = true
if self._onCancel then self:_onCancel() end
end
function Sequence:IsPlaying()
return self._active == true
end
function Sequence:_tick(dt)
self._elapsed = self._elapsed + dt
if self._elapsed < self._delay then return false end
local child = self._children[self._index]
if not child then return true end
if not child._started then child:_start() end
if child:_tick(dt) then
child:_finish()
self._index = self._index + 1
if self._index > table.getn(self._children) then
return self:_handleLoop()
end
end
return false
end
function Sequence:_finish()
lib._deactivate(self)
self._done = true
if self._onFinish then self:_onFinish() end
end
function Sequence:_start()
self._started = true
end
function Sequence:_reset()
self._index = 1
self._loopCount = 0
self._elapsed = 0
self._done = false
self._started = false
self:_resetChildren()
end
function Sequence:_resetChildren()
for i = 1, table.getn(self._children) do
self._children[i]:_reset()
end
end
function Sequence:_handleLoop()
self._loopCount = self._loopCount + 1
if self._loop ~= 0 and self._loopCount >= self._loop then
return true
end
if self._yoyo then
local c = self._children
local n = table.getn(c)
for i = 1, math.floor(n / 2) do
c[i], c[n - i + 1] = c[n - i + 1], c[i]
end
end
self._index = 1
self:_resetChildren()
return false
end
local SequenceMT = { __index = Sequence }
function lib:Sequence(children, opts)
opts = opts or {}
for i = 1, table.getn(children) do
children[i]._autoplay = false
end
local seq = setmetatable({
_children = children,
_index = 1,
_loop = opts.loop or 1,
_yoyo = opts.yoyo or false,
_loopCount = 0,
_delay = opts.delay or 0,
_elapsed = 0,
_onFinish = opts.onFinish,
_onCancel = opts.onCancel,
_done = false,
}, SequenceMT)
if not opts.defer then
lib._schedulePending(seq)
end
return seq
end
local Group = {}
function Group:Play()
self._loopCount = 0
self._elapsed = 0
self._done = false
self:_resetChildren()
lib._activate(self)
end
function Group:Cancel()
if not self._active then return end
for i = 1, table.getn(self._children) do
local child = self._children[i]
if child._active or child._started then
if child.Cancel then child:Cancel() end
end
end
lib._deactivate(self)
self._done = true
if self._onCancel then self:_onCancel() end
end
function Group:IsPlaying()
return self._active == true
end
function Group:_tick(dt)
self._elapsed = self._elapsed + dt
if self._elapsed < self._delay then return false end
local allDone = true
for i = 1, table.getn(self._children) do
local child = self._children[i]
if not child._done then
if not child._started then child:_start() end
if child:_tick(dt) then
child:_finish()
else
allDone = false
end
end
end
if allDone then
return self:_handleLoop()
end
return false
end
function Group:_handleLoop()
self._loopCount = self._loopCount + 1
if self._loop ~= 0 and self._loopCount >= self._loop then
return true
end
if self._yoyo then
local c = self._children
local n = table.getn(c)
for i = 1, math.floor(n / 2) do
c[i], c[n - i + 1] = c[n - i + 1], c[i]
end
end
self:_resetChildren()
return false
end
function Group:_finish()
lib._deactivate(self)
self._done = true
if self._onFinish then self:_onFinish() end
end
function Group:_start()
self._started = true
end
function Group:_reset()
self._loopCount = 0
self._elapsed = 0
self._done = false
self._started = false
self:_resetChildren()
end
function Group:_resetChildren()
for i = 1, table.getn(self._children) do
self._children[i]:_reset()
end
end
local GroupMT = { __index = Group }
function lib:Group(children, opts)
opts = opts or {}
for i = 1, table.getn(children) do
children[i]._autoplay = false
end
local grp = setmetatable({
_children = children,
_loop = opts.loop or 1,
_yoyo = opts.yoyo or false,
_loopCount = 0,
_delay = opts.delay or 0,
_elapsed = 0,
_onFinish = opts.onFinish,
_onCancel = opts.onCancel,
_done = false,
}, GroupMT)
if not opts.defer then
lib._schedulePending(grp)
end
return grp
end
-- Directional offsets for slide animations
local directions = {
LEFT = function(d) return { -d, 0 } end,
RIGHT = function(d) return { d, 0 } end,
UP = function(d) return { 0, d } end,
DOWN = function(d) return { 0,-d } end,
}
local function dirOffset(dir, dist)
local fn = directions[string.upper(dir or "LEFT")]
return fn and fn(dist) or { 0, 0 }
end
local function getAnchorPos(frame)
local _, _, _, x, y = frame:GetPoint(1)
return { x or 0, y or 0 }
end
local EMPTY = {}
local function opts(o)
if type(o) == "table" then return o end
return EMPTY
end
-- ============================================================
-- Alpha
-- ============================================================
function lib:FadeTo(target, toAlpha, duration, easing, o)
o = opts(o)
return lib:Tween(target, {
type = "alpha", to = toAlpha,
duration = duration, easing = easing,
delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer,
})
end
function lib:FadeIn(target, duration, easing, o)
o = opts(o)
return lib:Tween(target, {
type = "alpha", from = 0, to = 1,
duration = duration, easing = easing,
onStart = function()
target:SetAlpha(0)
target:Show()
if o.onStart then o.onStart() end
end,
delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer,
})
end
function lib:FadeOut(target, duration, easing, o)
o = opts(o)
local onFinish = o.onFinish
return lib:FadeTo(target, 0, duration, easing, {
delay = o.delay, onCancel = o.onCancel, defer = o.defer,
onFinish = function() target:Hide(); if onFinish then onFinish() end end,
})
end
function lib:Flash(target, count, duration, o)
o = opts(o)
count = count or 3
duration = duration or 0.6
local half = duration / count / 2
local steps = {}
for i = 1, count do
table.insert(steps, lib:Tween(target, { type = "alpha", to = 0, duration = half, easing = "inQuad", defer = true }))
table.insert(steps, lib:Tween(target, { type = "alpha", to = 1, duration = half, easing = "outQuad", defer = true }))
end
return lib:Sequence(steps, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer })
end
function lib:Breathe(target, minAlpha, maxAlpha, duration, o)
o = opts(o)
duration = duration or 1.6
local half = duration / 2
return lib:Sequence({
lib:Tween(target, { type = "alpha", from = minAlpha, to = maxAlpha, duration = half, easing = "inOutQuad", defer = true }),
lib:Tween(target, { type = "alpha", from = maxAlpha, to = minAlpha, duration = half, easing = "inOutQuad", defer = true }),
}, { loop = 0, delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer })
end
-- ============================================================
-- Scale
-- ============================================================
function lib:ScaleIn(target, duration, easing, o)
o = opts(o)
return lib:Tween(target, {
type = "scale", from = 0, to = 1,
duration = duration or 0.3, easing = easing or "outBack",
onStart = function()
target:Show()
if o.onStart then o.onStart() end
end,
delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer,
})
end
function lib:ScaleOut(target, duration, easing, o)
o = opts(o)
local onFinish = o.onFinish
return lib:Tween(target, {
type = "scale", from = 1, to = 0,
duration = duration or 0.3, easing = easing or "inBack",
onStart = o.onStart,
onFinish = function() target:Hide(); if onFinish then onFinish() end end,
onCancel = o.onCancel, delay = o.delay, defer = o.defer,
})
end
function lib:Pulse(target, scale, duration, o)
o = opts(o)
scale = scale or 1.3
local half = (duration or 0.4) / 2
local baseW, baseH = target:GetWidth(), target:GetHeight()
local function set(f, s)
f:SetWidth(baseW * s)
f:SetHeight(baseH * s)
end
return lib:Sequence({
lib:Tween(target, { type = "custom", from = 1.0, to = scale, duration = half, easing = "outQuad", setter = set, defer = true }),
lib:Tween(target, { type = "custom", from = scale, to = 1.0, duration = half, easing = "inQuad", setter = set, defer = true }),
}, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer })
end
function lib:Rubber(target, scale, duration, o)
o = opts(o)
return lib:Tween(target, {
type = "scale", from = scale or 1.3, to = 1.0,
duration = duration or 0.5, easing = "outElastic",
delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer,
})
end
function lib:Flip(target, axis, duration, o)
o = opts(o)
local quarter = (duration or 0.8) / 4
local horizontal = axis ~= "y"
local base = horizontal and target:GetWidth() or target:GetHeight()
local set = horizontal
and function(f, v) f:SetWidth(v) end
or function(f, v) f:SetHeight(v) end
-- Find child textures to mirror via SetTexCoord
local textures = {}
if target.GetTexture then
table.insert(textures, target)
else
local children = { target:GetRegions() }
for i = 1, table.getn(children) do
if children[i].GetTexture then table.insert(textures, children[i]) end
end
end
local function mirror()
for i = 1, table.getn(textures) do
if horizontal then
textures[i]:SetTexCoord(1, 0, 0, 1)
else
textures[i]:SetTexCoord(0, 1, 1, 0)
end
end
end
local function unmirror()
for i = 1, table.getn(textures) do
textures[i]:SetTexCoord(0, 1, 0, 1)
end
end
local shrink = function(f, v) set(f, v) end
return lib:Sequence({
-- 0°→90°: shrink to invisible
lib:Tween(target, { type = "custom", from = base, to = 0.01, duration = quarter, easing = "inQuad", setter = shrink, defer = true }),
-- 90°→180°: mirror and expand (showing flipped content)
lib:Tween(target, { type = "custom", from = 0.01, to = base, duration = quarter, easing = "outQuad", setter = shrink, defer = true,
onStart = function() mirror() end }),
-- 180°→270°: shrink again
lib:Tween(target, { type = "custom", from = base, to = 0.01, duration = quarter, easing = "inQuad", setter = shrink, defer = true }),
-- 270°→360°: unmirror and expand back to original
lib:Tween(target, { type = "custom", from = 0.01, to = base, duration = quarter, easing = "outQuad", setter = shrink, defer = true,
onStart = function() unmirror() end }),
}, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer })
end
-- ============================================================
-- Position
-- ============================================================
function lib:Move(target, from, to, duration, easing, o)
o = opts(o)
return lib:Tween(target, {
type = "position", from = from, to = to,
duration = duration or 0.3, easing = easing,
delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer,
})
end
function lib:MoveTo(target, to, duration, easing, o)
o = opts(o)
return lib:Tween(target, {
type = "position", to = to,
duration = duration, easing = easing,
delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer,
})
end
function lib:SlideIn(target, direction, distance, duration, easing, o)
o = opts(o)
local dest = getAnchorPos(target)
local offset = dirOffset(direction, distance or 100)
local from = { dest[1] - offset[1], dest[2] - offset[2] }
return lib:Group({
lib:Tween(target, {
type = "position", from = from, to = dest, duration = duration or 0.3, easing = easing or "outCubic",
onStart = function()
target:SetAlpha(0)
target:Show()
if o.onStart then o.onStart() end
end,
defer = true,
}),
lib:Tween(target, {
type = "alpha",
from = 0,
to = 1,
duration = (duration or 0.3) * 0.6,
easing = "outQuad",
defer = true,
}),
}, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer })
end
function lib:SlideOut(target, direction, distance, duration, easing, o)
o = opts(o)
local from = getAnchorPos(target)
local offset = dirOffset(direction, distance or 100)
local dest = { from[1] + offset[1], from[2] + offset[2] }
local onFinish = o.onFinish
return lib:Group({
lib:Tween(target, { type = "position", from = from, to = dest, duration = duration or 0.3, easing = easing or "inCubic", defer = true }),
lib:Tween(target, { type = "alpha", to = 0, duration = duration or 0.3, easing = "inQuad", defer = true }),
}, {
delay = o.delay, onCancel = o.onCancel, defer = o.defer,
onFinish = function() target:Hide(); if onFinish then onFinish() end end,
})
end
function lib:Bounce(target, height, count, duration, o)
o = opts(o)
height = height or 30
count = count or 3
duration = duration or 0.6
local pos = getAnchorPos(target)
local steps = {}
for i = 1, count do
local h = height * (1 - (i - 1) / count) -- decay
table.insert(steps, lib:Tween(target, {
type = "position", from = pos, to = { pos[1], pos[2] + h },
duration = duration / count / 2, easing = "outQuad", defer = true,
}))
table.insert(steps, lib:Tween(target, {
type = "position", from = { pos[1], pos[2] + h }, to = pos,
duration = duration / count / 2, easing = "inQuad", defer = true,
}))
end
return lib:Sequence(steps, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer })
end
function lib:Shake(target, intensity, duration, o)
o = opts(o)
intensity = intensity or 5
duration = duration or 0.3
local pos = getAnchorPos(target)
local steps = {}
local stepDur = 0.03
local numSteps = math.floor(duration / stepDur)
for i = 1, numSteps do
local decay = 1 - (i - 1) / numSteps
local dx = (math.random() * intensity * 2 - intensity) * decay
local dy = (math.random() * intensity * 2 - intensity) * decay
table.insert(steps, lib:Tween(target, {
type = "position", to = { pos[1] + dx, pos[2] + dy },
duration = stepDur, easing = "linear", defer = true,
}))
end
-- Snap back to original position
table.insert(steps, lib:Tween(target, {
type = "position", to = pos,
duration = stepDur, easing = "linear", defer = true,
}))
return lib:Sequence(steps, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer })
end
function lib:FlyTo(frame, targetFrame, duration, easing, o)
o = opts(o)
local toX = targetFrame:GetLeft() + targetFrame:GetWidth() / 2 - UIParent:GetWidth() / 2
local toY = targetFrame:GetBottom() + targetFrame:GetHeight() / 2 - UIParent:GetHeight() / 2
return lib:Tween(frame, {
type = "position", to = { toX, toY },
duration = duration or 0.5, easing = easing or "inOutQuad",
delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer,
})
end
-- ============================================================
-- Rotation
-- ============================================================
function lib:Spin(texture, degreesPerSecond, o)
o = opts(o)
local dps = degreesPerSecond or 90
return lib:Sequence({
lib:Tween(texture, {
type = "rotation",
from = 0,
to = dps > 0 and 360 or -360,
duration = 360 / math.abs(dps),
easing = "linear",
defer = true,
}),
}, { loop = 0, delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer })
end
-- ============================================================
-- Color
-- ============================================================
function lib:Color(target, from, to, duration, easing, o)
o = opts(o)
return lib:Tween(target, {
type = "color", from = from, to = to,
duration = duration or 0.3, easing = easing,
delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer,
})
end
function lib:ColorFlash(texture, r, g, b, duration, o)
o = opts(o)
duration = duration or 0.4
local half = duration / 2
return lib:Sequence({
lib:Tween(texture, { type = "color", to = { r, g, b, 1 }, duration = half, easing = "outQuad", defer = true }),
lib:Tween(texture, { type = "color", from = { r, g, b, 1 }, to = { 1, 1, 1, 1 }, duration = half, easing = "inQuad", defer = true }),
}, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer })
end
-- ============================================================
-- Size
-- ============================================================
function lib:Size(target, from, to, duration, easing, o)
o = opts(o)
return lib:Tween(target, {
type = "size", from = from, to = to,
duration = duration or 0.3, easing = easing,
delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer,
})
end
function lib:Morph(target, to, duration, easing, o)
o = opts(o)
return lib:Tween(target, {
type = "size", to = to,
duration = duration or 0.3, easing = easing,
delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer,
})
end
-- ============================================================
-- Value
-- ============================================================
function lib:Progress(statusbar, toValue, duration, easing, o)
o = opts(o)
return lib:Tween(statusbar, {
type = "custom",
to = toValue,
duration = duration or 0.4,
easing = easing or "outCubic",
getter = function(bar) return bar:GetValue() end,
setter = function(bar, val) bar:SetValue(val) end,
delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer,
})
end
-- ============================================================
-- Text
-- ============================================================
function lib:Typewriter(fontstring, text, duration, o)
o = opts(o)
local len = string.len(text)
return lib:Tween(fontstring, {
type = "custom",
from = 0, to = len,
duration = duration or len * 0.05,
easing = "linear",
setter = function(fs, val)
fs:SetText(string.sub(text, 1, math.floor(val)))
end,
delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer,
})
end
-- ============================================================
-- Clipping (Reveal / Conceal)
-- ============================================================
-- Progressively reveal a texture from a direction by re-anchoring
-- to one edge, animating width/height, and shifting tex coords.
local clipAnchors = {
LEFT = { "TOPLEFT", "TOPLEFT" },
RIGHT = { "TOPRIGHT", "TOPRIGHT" },
UP = { "TOPLEFT", "TOPLEFT" },
DOWN = { "BOTTOMLEFT", "BOTTOMLEFT" },
}
local function clipTween(target, direction, duration, easing, from, to, o)
o = opts(o)
direction = string.upper(direction or "LEFT")
duration = duration or 0.3
local parent = target:GetParent()
local baseW = target._clipBaseW or parent:GetWidth()
local baseH = target._clipBaseH or parent:GetHeight()
target._clipBaseW = baseW
target._clipBaseH = baseH
local horizontal = (direction == "LEFT" or direction == "RIGHT")
local reversed = (direction == "RIGHT" or direction == "UP")
local anchor = clipAnchors[direction]
target:ClearAllPoints()
target:SetPoint(anchor[1], parent, anchor[2], 0, 0)
return lib:Tween(target, {
type = "custom", from = from, to = to,
duration = duration, easing = easing or "outQuad",
setter = function(tex, t)
if horizontal then
tex:SetWidth(math.max(baseW * t, 0.01))
tex:SetHeight(baseH)
if reversed then
tex:SetTexCoord(1 - t, 1, 0, 1)
else
tex:SetTexCoord(0, t, 0, 1)
end
else
tex:SetWidth(baseW)
tex:SetHeight(math.max(baseH * t, 0.01))
if reversed then
tex:SetTexCoord(0, 1, 1 - t, 1)
else
tex:SetTexCoord(0, 1, 0, t)
end
end
end,
onFinish = function()
target:SetTexCoord(0, 1, 0, 1)
target:ClearAllPoints()
target:SetAllPoints(parent)
if o.onFinish then o.onFinish() end
end,
onCancel = o.onCancel, delay = o.delay, defer = o.defer,
})
end
function lib:Reveal(target, direction, duration, easing, o)
target:Show()
return clipTween(target, direction, duration, easing, 0, 1, o)
end
function lib:Conceal(target, direction, duration, easing, o)
o = opts(o)
local onFinish = o.onFinish
return clipTween(target, direction, duration, easing, 1, 0, {
delay = o.delay, onCancel = o.onCancel, defer = o.defer,
onFinish = function() target:Hide(); if onFinish then onFinish() end end,
})
end
-- WipeIn/WipeOut: texture slides in from a direction while being clipped.
-- Unlike Reveal/Conceal where the image is stationary and a window opens,
-- here the image physically moves into view (like sliding out from under a cover).
-- WipeIn/WipeOut: the image slides into/out of view while being clipped.
-- The texture stays full-size (no stretching) -- we clip by shrinking the
-- visible region and offsetting tex coords so the revealed portion matches
-- the correct part of the image.
--
-- For "LEFT" (image slides in from the left edge):
-- anchor at TOPLEFT, width = baseW * t, texCoord left = 1-t
-- → at t=0 nothing visible; at t=1 full image
local wipeAnchors = {
LEFT = "TOPLEFT",
RIGHT = "TOPRIGHT",
UP = "TOPLEFT",
DOWN = "BOTTOMLEFT",
}
local function wipeTween(target, direction, duration, easing, from, to, o)
o = opts(o)
direction = string.upper(direction or "LEFT")
duration = duration or 0.3
local parent = target:GetParent()
local baseW = target._clipBaseW or parent:GetWidth()
local baseH = target._clipBaseH or parent:GetHeight()
target._clipBaseW = baseW
target._clipBaseH = baseH
local horizontal = (direction == "LEFT" or direction == "RIGHT")
local anchor = wipeAnchors[direction]
target:ClearAllPoints()
target:SetPoint(anchor, parent, anchor, 0, 0)
return lib:Tween(target, {
type = "custom", from = from, to = to,
duration = duration, easing = easing or "outQuad",
setter = function(tex, t)
if horizontal then
tex:SetWidth(math.max(baseW * t, 0.01))
tex:SetHeight(baseH)
if direction == "LEFT" then
tex:SetTexCoord(1 - t, 1, 0, 1)
else
tex:SetTexCoord(0, t, 0, 1)
end
else
tex:SetWidth(baseW)
tex:SetHeight(math.max(baseH * t, 0.01))
if direction == "DOWN" then
tex:SetTexCoord(0, 1, 1 - t, 1)
else
tex:SetTexCoord(0, 1, 0, t)
end
end
end,
onFinish = function()
target:SetTexCoord(0, 1, 0, 1)
target:ClearAllPoints()
target:SetAllPoints(parent)
if o.onFinish then o.onFinish() end
end,
onCancel = o.onCancel, delay = o.delay, defer = o.defer,
})
end
local wipeInFlip = { LEFT="RIGHT", RIGHT="LEFT", UP="DOWN", DOWN="UP" }
function lib:WipeIn(target, direction, duration, easing, o)
target:Show()
local dir = wipeInFlip[string.upper(direction or "LEFT")] or direction
return wipeTween(target, dir, duration, easing, 0, 1, o)
end
function lib:WipeOut(target, direction, duration, easing, o)
o = opts(o)
local onFinish = o.onFinish
return wipeTween(target, direction, duration, easing, 1, 0, {
delay = o.delay, onCancel = o.onCancel, defer = o.defer,
onFinish = function() target:Hide(); if onFinish then onFinish() end end,
})
end
-- ============================================================
-- Meta / Utility
-- ============================================================
function lib:Stagger(targets, animFn, delay, o)
o = opts(o)
delay = delay or 0.08
local children = {}
for i = 1, table.getn(targets) do
local anim = animFn(targets[i], i)
anim._autoplay = false
if i > 1 then
table.insert(children, lib:Sequence({ lib:Delay(delay * (i - 1)), anim }, { defer = true }))
else
table.insert(children, anim)
end
end
return lib:Group(children, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer })
end
function lib:Delay(duration)
-- A no-op tween that just waits. Uses a dummy target-less approach.
local dummy = lib._frame
return lib:Tween(dummy, {
type = "alpha",
from = dummy:GetAlpha(),
to = dummy:GetAlpha(),
duration = duration or 1,
defer = true,
})
end
function lib:Run(fn, o)
o = opts(o)
return lib:Tween(nil, {
type = "custom", from = 0, to = 0, duration = 0,
onStart = fn, delay = o.delay, defer = o.defer,
})
end
function lib:Stop(target)
for anim in pairs(lib._active) do
if anim._target == target then
anim:Cancel()
elseif anim._children then
lib:_stopInChildren(anim, anim, target)
end
end
-- Also cancel pending (not yet started) animations for this target
for i = 1, lib._pendingN do
local anim = lib._pending[i]
if anim then
if anim._target == target then
anim._autoplay = false
elseif anim._children then
if lib:_hasTargetInChildren(anim, target) then
anim._autoplay = false
end
end
end
end
end
function lib:_hasTargetInChildren(parent, target)
for i = 1, table.getn(parent._children) do
local child = parent._children[i]
if child._target == target then return true end
if child._children and lib:_hasTargetInChildren(child, target) then return true end
end
return false
end
function lib:_stopInChildren(root, parent, target)
for i = 1, table.getn(parent._children) do
local child = parent._children[i]
if child._target == target then
root:Cancel()
return
elseif child._children then
lib:_stopInChildren(root, child, target)
if not root._active then return end
end
end
end