1
0
mirror of https://github.com/brues-code/TwitchEmotes.git synced 2026-09-21 23:26:57 +00:00
Files
TwitchEmotes/TwitchEmotesAnimator.lua
T
Brues fdff71922e Add animated emote support
Animate multi-frame sprite-sheet emotes (e.g. :meow:) using ClassicAPI's
extended |T texcoord-crop escape, across every context.

- TwitchEmotes_animation_metadata (per-sheet frame layout) added to
  Emotes.lua; animated emotes' base entries cropped to frame 0 so any
  un-animated context shows a clean first frame, not the whole sheet.
- TwitchEmotesAnimator.lua: ~30fps ticker advancing the current frame in
  chat lines, the autocomplete popup, dropdown menus, chat bubbles, and
  the stats screen (featured icons + ranked lists).
- Re-converted sprite sheets to uncompressed, unflipped, native
  dimensions (1.12 cannot decode RLE TGA); fixed Clap.tga which was a
  single 32x32 frame.
2026-09-02 16:30:12 -05:00

127 lines
5.2 KiB
Lua

-- Animated emotes are vertical sprite sheets; TwitchEmotes_animation_metadata
-- (Emotes.lua) maps each sheet's texture path to its frame layout. ClassicAPI's
-- inline-texture renderer supports the extended
-- |Tpath:w:h:0:0:imgW:imgH:0:frameW:top:bottom|t form (sprite-sheet crop), so we
-- animate by re-SetText-ing each visible FontString ~30fps with the current
-- frame's crop. On 1.12 a ScrollingMessageFrame's visible lines are real
-- CSimpleFontStrings exposed as the ChatFrame's FontString regions, so they can
-- be rewritten in place — the same way the retail TwitchEmotesAnimator does it.
local TWITCHEMOTES_T = 0
local sinceUpdate = 0
local function GetCurrentFrameNum(animdata)
if animdata.pingpong then
local v = math.floor((TWITCHEMOTES_T * animdata.framerate) % ((animdata.nFrames * 2) - 1))
if v > animdata.nFrames then
v = animdata.nFrames - (v % animdata.nFrames)
end
return v
end
return math.floor((TWITCHEMOTES_T * animdata.framerate) % animdata.nFrames)
end
-- Crop a Texture (not inline text) to its animdata's current frame.
local function CropToCurrentFrame(tex, animdata)
local top = GetCurrentFrameNum(animdata) * animdata.frameHeight / animdata.imageHeight
tex:SetTexCoord(0, 1, top, top + animdata.frameHeight / animdata.imageHeight)
end
local function BuildFrameString(imagepath, animdata, framenum, w, h)
local top = framenum * animdata.frameHeight
local bottom = top + animdata.frameHeight
return "|T" .. imagepath .. ":" .. w .. ":" .. h .. ":0:0:" ..
animdata.imageWidth .. ":" .. animdata.imageHeight .. ":0:" ..
animdata.frameWidth .. ":" .. top .. ":" .. bottom .. "|t"
end
-- escape the pattern-magic chars that can appear in an emote escape (paths use
-- '+' and '-'); '.' is left as-is to match the retail animator.
local function escpattern(s)
return (s:gsub("%+", "%%+"):gsub("%-", "%%-"))
end
-- Rewrite every animated emote escape in the fontstring to its current frame.
local function UpdateFontString(fontstring, w, h)
local txt = fontstring:GetText()
if txt == nil then return end
for emoteStr in txt:gmatch("(|TInterface\\AddOns\\TwitchEmotes\\Emotes.-|t)") do
local imagepath = emoteStr:match("|T(Interface\\AddOns\\TwitchEmotes\\Emotes.-%.tga).-|t")
local animdata = imagepath and TwitchEmotes_animation_metadata[imagepath]
if animdata then
local framenum = GetCurrentFrameNum(animdata)
txt = txt:gsub(escpattern(emoteStr), BuildFrameString(imagepath, animdata, framenum, w, h))
fontstring:SetText(txt)
end
end
end
local anim = CreateFrame("Frame")
anim:SetScript("OnUpdate", function(self, elapsed)
TWITCHEMOTES_T = TWITCHEMOTES_T + elapsed
sinceUpdate = sinceUpdate + elapsed
if sinceUpdate < 0.033 then return end -- ~30fps
sinceUpdate = 0
for i = 1, (NUM_CHAT_WINDOWS or 10) do
local cf = _G["ChatFrame" .. i]
if cf and cf:IsShown() then
for j = 1, cf:GetNumRegions() do
local region = select(j, cf:GetRegions())
if region and region:GetObjectType() == "FontString" then
UpdateFontString(region, 28, 28)
end
end
end
end
-- Autocomplete suggestion icons are Textures, not inline text: crop each
-- animated one's current frame via SetTexCoord.
local ac = TwitchEmotesACPopup
if ac and ac:IsShown() and ac.btns then
for _, b in ipairs(ac.btns) do
if b:IsShown() and b.animdata then
CropToCurrentFrame(b.ico, b.animdata)
end
end
end
-- Open dropdown menus: emote rows are inline-text FontStrings.
for level = 1, (UIDROPDOWNMENU_MAXLEVELS or 3) do
local list = _G["DropDownList" .. level]
if list and list:IsShown() then
for i = 1, (UIDROPDOWNMENU_MAXBUTTONS or 40) do
local btn = _G["DropDownList" .. level .. "Button" .. i]
if btn and btn:IsShown() then
local fs = btn:GetFontString()
if fs then UpdateFontString(fs, 28, 28) end
end
end
end
end
-- Chat bubbles: our own overlay FontString carries the emote text.
if C_ChatBubbles then
for _, bubble in ipairs(C_ChatBubbles.GetAllChatBubbles()) do
local ov = bubble.tweOverlay
if ov and ov:IsShown() then
UpdateFontString(ov, 28, 28)
end
end
end
-- Stats screen: featured emotes are Textures; the sent/seen lists are
-- inline-text FontString regions of the frame.
local sw = TwitchEmotesStatsFrame
if sw and sw:IsShown() then
if sw.topSentTex and sw.topSentTex.animdata then CropToCurrentFrame(sw.topSentTex, sw.topSentTex.animdata) end
if sw.topSeenTex and sw.topSeenTex.animdata then CropToCurrentFrame(sw.topSeenTex, sw.topSeenTex.animdata) end
for j = 1, sw:GetNumRegions() do
local region = select(j, sw:GetRegions())
if region and region:GetObjectType() == "FontString" then
UpdateFontString(region, 16, 16)
end
end
end
end)