1
0
mirror of https://github.com/brues-code/TwitchEmotes.git synced 2026-09-22 15:46:58 +00:00
Files
TwitchEmotes/TwitchEmotesAnimator.lua
Brues ce8038ed03 Pack animation sheets as tall strips
ClassicAPI's texture dimension gate lifts the two limits the sheet layout was
built around. A sheet skinnier than 16:1 drawing nothing was never a client
rule -- VanillaHelpers grew the texture recycle pool to 6x6 but left the index
stride at 5, so a 32x1024 strip collided with a 64x32 bucket and was handed
back the wrong texture. And power-of-two is no longer required, since the gate
grows the decode scratch to fit.

So a run wraps into columns only when it outgrows the scratch at 32 frames, and
a sheet is sized to its frames exactly: 20 frames is a 32x640 strip rather than
64x1024. The 128 frame budget is unchanged, and older sheets still play -- the
animator reads the column count off the sheet.
2026-09-14 13:58:06 -05:00

142 lines
6.1 KiB
Lua

-- Animated emotes are 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:left:right: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
-- Frames run row-major across however many columns the sheet is wide, since a
-- run longer than 32 frames outgrows the 1024px decode scratch as one strip.
-- Older sheets are packed more densely than that: before ClassicAPI's texture
-- dimension gate a sheet skinnier than 16:1 drew nothing, so runs wrapped at 16
-- frames. Deriving the column count from the sheet handles either.
local function GetFrameRect(animdata, framenum)
local cols = math.floor(animdata.imageWidth / animdata.frameWidth)
if cols < 1 then cols = 1 end
local left = (framenum % cols) * animdata.frameWidth
local top = math.floor(framenum / cols) * animdata.frameHeight
return left, left + animdata.frameWidth, top, top + animdata.frameHeight
end
-- Crop a Texture (not inline text) to its animdata's current frame.
local function CropToCurrentFrame(tex, animdata)
local left, right, top, bottom = GetFrameRect(animdata, GetCurrentFrameNum(animdata))
tex:SetTexCoord(left / animdata.imageWidth, right / animdata.imageWidth,
top / animdata.imageHeight, bottom / animdata.imageHeight)
end
-- The crop bounds are texel offsets, so emit them as integers rather than
-- letting a float from the frame maths stringify into the payload.
local function BuildFrameString(imagepath, animdata, framenum, w, h)
local left, right, top, bottom = GetFrameRect(animdata, framenum)
return ("|T%s:%d:%d:0:0:%d:%d:%d:%d:%d:%d|t"):format(imagepath, w, h,
animdata.imageWidth, animdata.imageHeight,
left, right, top, bottom)
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)