593 lines
20 KiB
Lua
593 lines
20 KiB
Lua
local Bit = require("modules.loveanimate.libs.Bit")
|
|
local Classic = require("modules.loveanimate.libs.Classic")
|
|
|
|
local function intToRGB(int)
|
|
return
|
|
Bit.band(Bit.rshift(int, 16), 0xFF) / 255,
|
|
Bit.band(Bit.rshift(int, 8), 0xFF) / 255,
|
|
Bit.band(int, 0xFF) / 255,
|
|
Bit.band(Bit.rshift(int, 24), 0xFF) / 255
|
|
end
|
|
|
|
---
|
|
--- @class love.animate.AnimateAtlas
|
|
---
|
|
local AnimateAtlas = Classic:extend()
|
|
|
|
local json = require("modules.loveanimate.libs.Json")
|
|
require("modules.loveanimate.libs.StringUtil")
|
|
local evilJson = require("modules.json")
|
|
local evilFiles = require"modules.files"
|
|
|
|
function AnimateAtlas:constructor()
|
|
self.frame = 0
|
|
self.symbol = ""
|
|
self.playing = false
|
|
|
|
--- @protected
|
|
self._curSymbol = nil
|
|
|
|
--- @protected
|
|
self._frameTimer = 0
|
|
|
|
--- @protected
|
|
self._rotatedAtlasSpriteTextures = {} --- @type table<string, love.Image>
|
|
|
|
--- @protected
|
|
self._colorTransformShader = love.graphics.newShader([[
|
|
extern vec4 colorOffset;
|
|
extern vec4 colorMultiplier;
|
|
|
|
vec4 effect(vec4 color, Image tex, vec2 texCoords, vec2 screenCoords) {
|
|
vec4 finalColor = Texel(tex, texCoords) * color;
|
|
finalColor += colorOffset;
|
|
return finalColor * colorMultiplier;
|
|
}
|
|
]])
|
|
self:setColorOffset(0, 0, 0, 0)
|
|
self:setColorMultiplier(1, 1, 1, 1)
|
|
end
|
|
|
|
function AnimateAtlas:setColorOffset(r, g, b, a)
|
|
self._colorTransformShader:send("colorOffset", {r, g, b, a})
|
|
end
|
|
|
|
function AnimateAtlas:setColorMultiplier(r, g, b, a)
|
|
self._colorTransformShader:send("colorMultiplier", {r, g, b, a})
|
|
end
|
|
|
|
local colorTransforms = {
|
|
brightness = function(self, colorTransform)
|
|
local brightness = colorTransform["brightness"]
|
|
self:setColorOffset(brightness, brightness, brightness, 0)
|
|
self:setColorMultiplier(
|
|
1 - math.abs(brightness),
|
|
1 - math.abs(brightness),
|
|
1 - math.abs(brightness),
|
|
1
|
|
)
|
|
end,
|
|
tint = function(self, colorTransform)
|
|
local tintColor = tonumber("0xFF" + colorTransform["tintColor"]:sub(2))
|
|
local tintR, tintG, tintB = intToRGB(tintColor)
|
|
|
|
local multiplier = colorTransform["tintMultiplier"]
|
|
self:setColorOffset(
|
|
tintR * multiplier,
|
|
tintG * multiplier,
|
|
tintB * multiplier,
|
|
0
|
|
)
|
|
self:setColorMultiplier(
|
|
1 - multiplier,
|
|
1 - multiplier,
|
|
1 - multiplier,
|
|
1
|
|
)
|
|
end,
|
|
alpha = function(self, colorTransform)
|
|
local alphaMultiplier = colorTransform["alphaMultiplier"]
|
|
self:setColorMultiplier(1, 1, 1, alphaMultiplier)
|
|
end,
|
|
advanced = function(self, colorTransform)
|
|
self:setColorOffset(
|
|
colorTransform["redOffset"],
|
|
colorTransform["greenOffset"],
|
|
colorTransform["blueOffset"],
|
|
colorTransform["AlphaOffset"]
|
|
)
|
|
self:setColorMultiplier(
|
|
colorTransform["RedMultiplier"],
|
|
colorTransform["greenMultiplier"],
|
|
colorTransform["blueMultiplier"],
|
|
colorTransform["alphaMultiplier"]
|
|
)
|
|
end
|
|
}
|
|
|
|
--- Load the atlas from folder
|
|
--- @param folder string
|
|
---
|
|
function AnimateAtlas:load(folder)
|
|
for key, value in pairs(self._rotatedAtlasSpriteTextures) do
|
|
value:release()
|
|
self._rotatedAtlasSpriteTextures[key] = nil
|
|
end
|
|
self.timeline = {}
|
|
self.timeline.data = json.decode(love.filesystem.read("string", folder .. "/" .. "Animation.json"))
|
|
self.timeline.optimized = self.timeline.data.AN ~= nil
|
|
|
|
self.spritemaps = {}
|
|
for _, item in ipairs(love.filesystem.getDirectoryItems(folder)) do
|
|
if string.startsWith(item, "spritemap") and string.endsWith(item, ".json") then
|
|
local data = evilJson.parse(evilFiles.read_file(folder.."/"..item))
|
|
local texture = love.graphics.newImage(folder .. "/" .. string.sub(item, 1, #item - 5) .. ".png")
|
|
table.insert(self.spritemaps, { data = data, texture = texture })
|
|
end
|
|
end
|
|
self.libraries = {}
|
|
if self.timeline.data.SD ~= nil or self.timeline.data.SYMBOL_DICTIONARY ~= nil then
|
|
-- regular adobe format
|
|
local optimized = self.timeline.data.SD ~= nil
|
|
local symbolDictionary = self.timeline.data[optimized and "SD" or "SYMBOL_DICTIONARY"]
|
|
|
|
local symbols = symbolDictionary[optimized and "S" or "Symbols"]
|
|
for i = 1, #symbols do
|
|
local symbol = symbols[i]
|
|
|
|
local symbolName = symbol[optimized and "SN" or "SYMBOL_name"]
|
|
local data = symbol[optimized and "TL" or "TIMELINE"]
|
|
|
|
self.libraries[symbolName] = { data = data, optimized = data.L ~= nil }
|
|
end
|
|
else
|
|
-- bta format
|
|
for _, item in ipairs(love.filesystem.getDirectoryItems(folder .. "/LIBRARY")) do
|
|
if string.endsWith(item, ".json") then
|
|
local data = json.decode(love.filesystem.read("string", folder .. "/LIBRARY/" .. item))
|
|
self.libraries[string.sub(item, 1, #item - 5)] = { data = data, optimized = data.L ~= nil }
|
|
end
|
|
end
|
|
end
|
|
if #self.spritemaps < 1 then
|
|
error("Couldn't find any spritemaps for folder path '" .. folder .. "'")
|
|
return
|
|
end
|
|
if love.filesystem.getInfo(folder .. "/metadata.json", "file") ~= nil then
|
|
self.framerate = json.decode(love.filesystem.read("string", folder .. "/metadata.json"))[self.timeline.optimized and "FRT" or "framerate"]
|
|
else
|
|
local optimized = self.timeline.data.FRT ~= nil
|
|
local hasFramerate = self.timeline.data.FRT ~= nil or self.timeline.data.framerate ~= nil
|
|
self.framerate = hasFramerate and (optimized and self.timeline.data.FRT or self.timeline.data.framerate) or 24
|
|
print(self.framerate)
|
|
end
|
|
print("Loaded at " .. self.framerate .. " frames per second")
|
|
end
|
|
|
|
---
|
|
--- @param symbol string?
|
|
---
|
|
function AnimateAtlas:play(symbol)
|
|
self.frame = 0
|
|
self.symbol = symbol or ""
|
|
|
|
self.playing = true
|
|
self._frameTimer = 0.0
|
|
end
|
|
|
|
function AnimateAtlas:stop()
|
|
self.playing = false
|
|
self._frameTimer = 0.0
|
|
end
|
|
|
|
function AnimateAtlas:resume()
|
|
self.playing = true
|
|
end
|
|
|
|
function AnimateAtlas:getTimelineLength(timeline)
|
|
local optimized = timeline.optimized == true or timeline.L ~= nil
|
|
if timeline.data then
|
|
timeline = timeline.data[optimized and "AN" or "ANIMATION"][optimized and "TL" or "TIMELINE"]
|
|
end
|
|
local longest = 0
|
|
local timelineLayers = timeline[optimized and "L" or "LAYERS"]
|
|
for i = #timelineLayers, 1, -1 do
|
|
local layer = timelineLayers[i]
|
|
local layerFrames = layer[optimized and "FR" or "Frames"]
|
|
if layerFrames == nil then
|
|
goto continue
|
|
end
|
|
|
|
local keyframe = layerFrames[#layerFrames]
|
|
if keyframe ~= nil then
|
|
local length = keyframe[optimized and "I" or "index"] + keyframe[optimized and "DU" or "duration"]
|
|
if length > longest then
|
|
longest = length
|
|
end
|
|
end
|
|
::continue::
|
|
end
|
|
|
|
return longest
|
|
end
|
|
|
|
function AnimateAtlas:getLength()
|
|
local optimized = self.timeline.optimized == true or self.timeline.L ~= nil
|
|
return self:getTimelineLength(self.timeline.data[optimized and "AN" or "ANIMATION"][optimized and "TL" or "TIMELINE"])
|
|
end
|
|
|
|
local function matrix2D(matrix)
|
|
return "column", -- OKAY MAKE SURE THIS IS HERE LOL
|
|
matrix[1], -- a
|
|
matrix[2], -- b
|
|
0, 0,
|
|
matrix[3], -- c
|
|
matrix[4], -- d
|
|
0, 0, 0, 0, 1, 0,
|
|
matrix[5], -- tx
|
|
matrix[6], -- ty
|
|
0, 1
|
|
end
|
|
|
|
local function matrix3D(matrix, optimized)
|
|
if optimized then
|
|
return "column",
|
|
matrix[1], -- a
|
|
matrix[2], -- b
|
|
matrix[3], matrix[4],
|
|
matrix[5], -- c
|
|
matrix[6], -- d
|
|
matrix[7], matrix[8], matrix[9], matrix[10], matrix[11], matrix[12],
|
|
matrix[13], -- tx
|
|
matrix[14], -- ty
|
|
matrix[15], matrix[16]
|
|
end
|
|
|
|
return "column",
|
|
matrix["m00"], matrix["m01"], matrix["m02"], matrix["m03"],
|
|
matrix["m10"], matrix["m11"], matrix["m12"], matrix["m13"],
|
|
matrix["m20"], matrix["m21"], matrix["m22"], matrix["m23"],
|
|
matrix["m30"], matrix["m31"], matrix["m32"], matrix["m33"]
|
|
end
|
|
|
|
local function renderSymbol(self, symbol, frame, index, matrix, colorTransform, optimized)
|
|
local symbolName = symbol[optimized and "SN" or "SYMBOL_name"]
|
|
|
|
-- get the symbol's first frame
|
|
local firstFrame = symbol[optimized and "FF" or "firstFrame"]
|
|
if firstFrame == nil then
|
|
firstFrame = 0
|
|
end
|
|
|
|
local frameIndex = firstFrame -- get the frame index we want to possibly render
|
|
frameIndex = frameIndex + (frame - index)
|
|
|
|
local symbolType = symbol[optimized and "ST" or "symbolType"]
|
|
if symbolType == "movieclip" or symbolType == "MC" then
|
|
-- movie clips can only display first frame
|
|
frameIndex = 0
|
|
end
|
|
local loopMode = symbol[optimized and "LP" or "loop"]
|
|
|
|
local library = self.libraries[symbolName]
|
|
local symbolTimeline = library.data
|
|
|
|
local length = self:getTimelineLength(symbolTimeline)
|
|
|
|
if loopMode == "loop" or loopMode == "LP" then
|
|
-- wrap around back to 0
|
|
if frameIndex < 0 then
|
|
frameIndex = length - 1
|
|
end
|
|
while frameIndex > length - 1 do
|
|
frameIndex = frameIndex - length
|
|
end
|
|
|
|
elseif loopMode == "playonce" or loopMode == "PO" then
|
|
-- stop at last frame
|
|
if frameIndex < 0 then
|
|
frameIndex = 0
|
|
if self.disappear then self.object.alpha = 0 end
|
|
self.object.position = Vector2(10000, 10000)
|
|
end
|
|
if frameIndex > length - 1 then
|
|
frameIndex = length - 1
|
|
if self.disappear then self.object.alpha = 0 end
|
|
self.object.position = Vector2(10000, 10000)
|
|
end
|
|
|
|
elseif loopMode == "singleframe" or loopMode == "SF" then
|
|
-- stop at first frame
|
|
frameIndex = firstFrame
|
|
end
|
|
|
|
local is3DMatrix = symbol[optimized and "M3D" or "Matrix3D"] ~= nil
|
|
local symbolMatrix = love.math.newTransform()
|
|
|
|
local symbolMatrixRaw
|
|
if is3DMatrix then
|
|
symbolMatrixRaw = symbol[optimized and "M3D" or "Matrix3D"]
|
|
else
|
|
symbolMatrixRaw = symbol[optimized and "MX" or "Matrix"]
|
|
end
|
|
|
|
symbolMatrix:setMatrix((is3DMatrix and matrix3D or matrix2D)(symbolMatrixRaw, optimized))
|
|
|
|
-- TODO: is this shit even working correctly??
|
|
local symbolColor = symbol[optimized and "C" or "color"]
|
|
if symbolColor and not colorTransform then
|
|
colorTransform = symbolColor
|
|
end
|
|
if colorTransform and symbolColor then
|
|
for key, value in pairs(colorTransform) do
|
|
if type(value) == "number" then
|
|
if string.endsWith(key, "Offset") then
|
|
-- is offset
|
|
colorTransform[key] = value + symbolColor[key]
|
|
else
|
|
-- assume multiplier
|
|
colorTransform[key] = value * symbolColor[key]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
self:drawTimeline(symbolTimeline, frameIndex, matrix:clone():apply(symbolMatrix), colorTransform)
|
|
end
|
|
|
|
local function renderSprite(self, sprite, spritemap, spriteMatrix, matrix, colorTransform)
|
|
-- store thecolor transform mode somewhere
|
|
local colorTransformMode = colorTransform and colorTransform.mode or nil
|
|
if not colorTransformMode then
|
|
colorTransformMode = "none"
|
|
end
|
|
--- @type "brightness"|"tint"|"alpha"|"advanced"|"none"
|
|
colorTransformMode = colorTransformMode:lower()
|
|
|
|
local texture = spritemap.texture --- @type love.Image
|
|
local quad = love.graphics.newQuad(sprite.x, sprite.y, sprite.w, sprite.h, texture:getWidth(), texture:getHeight())
|
|
|
|
local drawMatrix = matrix * spriteMatrix
|
|
if sprite.rotated then
|
|
drawMatrix:translate(0,sprite.w)
|
|
drawMatrix:rotate(-math.pi/2)
|
|
end
|
|
local lastShader = love.graphics.getShader()
|
|
if colorTransform ~= nil then
|
|
love.graphics.setShader(self._colorTransformShader)
|
|
self:setColorOffset(0, 0, 0, 0)
|
|
self:setColorMultiplier(1, 1, 1, 1)
|
|
|
|
if(type(colorTransforms[colorTransformMode]) == "function") then
|
|
colorTransforms[colorTransformMode](self, colorTransform)
|
|
end
|
|
end
|
|
|
|
love.graphics.draw(texture, quad, drawMatrix)
|
|
|
|
if colorTransform ~= nil then
|
|
love.graphics.setShader(lastShader)
|
|
end
|
|
end
|
|
|
|
local function renderAtlasSprite(self, atlasSprite, matrix, colorTransform, optimized)
|
|
local name = atlasSprite[optimized and "N" or "name"]
|
|
local is3DMatrix = atlasSprite[optimized and "M3D" or "Matrix3D"] ~= nil
|
|
|
|
local spriteMatrixRaw = nil
|
|
if is3DMatrix then
|
|
spriteMatrixRaw = atlasSprite[optimized and "M3D" or "Matrix3D"]
|
|
else
|
|
spriteMatrixRaw = atlasSprite[optimized and "MX" or "Matrix"]
|
|
end
|
|
local spriteMatrix = love.math.newTransform()
|
|
spriteMatrix:setMatrix((is3DMatrix and matrix3D or matrix2D)(spriteMatrixRaw, optimized))
|
|
|
|
local spritemaps = self.spritemaps
|
|
for l = 1, #spritemaps do
|
|
local spritemap = spritemaps[l]
|
|
local sprites = spritemap.data.ATLAS.SPRITES
|
|
for z = 1, #sprites do
|
|
local sprite = sprites[z].SPRITE
|
|
|
|
if sprite.name == name then
|
|
renderSprite(self, sprite, spritemap, spriteMatrix, matrix, colorTransform)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function renderKeyFrame(self, keyframe, frame, matrix, colorTransform, optimized)
|
|
local index = keyframe[optimized and "I" or "index"]
|
|
local duration = keyframe[optimized and "DU" or "duration"]
|
|
|
|
if not (frame >= index and frame < index + duration) then
|
|
return false
|
|
end
|
|
local elements = keyframe[optimized and "E" or "elements"]
|
|
|
|
for k = 1, #elements do
|
|
local element = elements[k]
|
|
|
|
local symbol = element[optimized and "SI" or "SYMBOL_Instance"]
|
|
local atlasSprite = element[optimized and "ASI" or "ATLAS_SPRITE_instance"]
|
|
|
|
if symbol then
|
|
self._curSymbol = { data = symbol, index = index }
|
|
renderSymbol(self, symbol, frame, index, matrix, colorTransform, optimized)
|
|
elseif atlasSprite then
|
|
renderAtlasSprite(self, atlasSprite, matrix, colorTransform, optimized)
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local maskShader = love.graphics.newShader[[
|
|
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {
|
|
float alpha = Texel(texture, texture_coords).a;
|
|
if (alpha == 0.0) {
|
|
// a discarded pixel wont be applied as the stencil.
|
|
discard;
|
|
}
|
|
return vec4(alpha);
|
|
}
|
|
]]
|
|
|
|
---
|
|
--- @param timeline table
|
|
--- @param frame integer
|
|
--- @param matrix love.Transform
|
|
---
|
|
function AnimateAtlas:drawTimeline(timeline, frame, matrix, colorTransform)
|
|
local optimized = timeline.L ~= nil
|
|
local timelineLayers = timeline[optimized and "L" or "LAYERS"]
|
|
local namesToLayers = {}
|
|
|
|
for i = #timelineLayers, 1, -1 do
|
|
local layer = timelineLayers[i]
|
|
namesToLayers[layer[optimized and "LN" or "Layer_name"]] = layer
|
|
end
|
|
|
|
for i = #timelineLayers, 1, -1 do
|
|
local layer = timelineLayers[i]
|
|
local keyframes = layer[optimized and "FR" or "Frames"]
|
|
local layerType = layer[optimized and "LT" or "Layer_type"]
|
|
local clippedBy = layer[optimized and "CB" or "Clipped_by"]
|
|
|
|
if layerType ~= nil then
|
|
goto continue
|
|
end
|
|
|
|
if clippedBy ~= nil then
|
|
love.graphics.clear(false, true, false)
|
|
love.graphics.setStencilState("replace", "always", 1)
|
|
|
|
local maskLayer = namesToLayers[clippedBy]
|
|
local maskKeyframes = maskLayer[optimized and "FR" or "Frames"]
|
|
love.graphics.setShader(maskShader)
|
|
for j = 1, #maskKeyframes do
|
|
if renderKeyFrame(self, maskKeyframes[j], frame, matrix, nil, optimized) then
|
|
break
|
|
end
|
|
end
|
|
love.graphics.setShader()
|
|
|
|
love.graphics.setStencilState("keep", "greater", 0)
|
|
end
|
|
|
|
for j = 1, #keyframes do
|
|
if renderKeyFrame(self, keyframes[j], frame, matrix, colorTransform, optimized) then
|
|
break
|
|
end
|
|
end
|
|
|
|
if clippedBy ~= nil then
|
|
love.graphics.clear(false, true, false)
|
|
love.graphics.setStencilState()
|
|
end
|
|
::continue::
|
|
end
|
|
end
|
|
|
|
function AnimateAtlas:getSymbolTimeline(symbol)
|
|
if not symbol then
|
|
symbol = ""
|
|
end
|
|
local timeline = self.libraries[self.symbol]
|
|
if not timeline then
|
|
timeline = self.timeline
|
|
else
|
|
timeline = timeline.data
|
|
end
|
|
return timeline
|
|
end
|
|
|
|
function AnimateAtlas:update(dt)
|
|
if self.framerate <= 0.0 or not self.playing then
|
|
return
|
|
end
|
|
self._frameTimer = self._frameTimer + dt
|
|
while self._frameTimer >= 1.0 / self.framerate do
|
|
self.frame = self.frame + 1
|
|
self._frameTimer = self._frameTimer - 1.0 / self.framerate
|
|
end
|
|
|
|
local length = self:getTimelineLength(self:getSymbolTimeline(self.symbol))
|
|
if self.frame > length then
|
|
if self._curSymbol then
|
|
local optimized = self._curSymbol.data.ST ~= nil
|
|
local symbolName = self._curSymbol.data[optimized and "SN" or "SYMBOL_name"]
|
|
|
|
local firstFrame = self._curSymbol.data[optimized and "FF" or "firstFrame"]
|
|
if firstFrame == nil then
|
|
firstFrame = 0
|
|
end
|
|
local frameIndex = firstFrame -- get the frame index we want to possibly render
|
|
frameIndex = frameIndex + (self.frame - self._curSymbol.index)
|
|
|
|
local symbolType = self._curSymbol.data[optimized and "ST" or "symbolType"]
|
|
if symbolType == "movieclip" or symbolType == "MC" then
|
|
-- movie clips can only display first frame
|
|
frameIndex = 0
|
|
end
|
|
local loopMode = self._curSymbol.data[optimized and "LP" or "loop"]
|
|
|
|
local library = self.libraries[symbolName]
|
|
local symbolTimeline = library.data
|
|
|
|
local length = self:getTimelineLength(symbolTimeline)
|
|
|
|
if loopMode == "loop" or loopMode == "LP" then
|
|
-- wrap around back to 0
|
|
if frameIndex < 0 then
|
|
frameIndex = length - 1
|
|
end
|
|
if frameIndex > length - 1 then
|
|
frameIndex = 0
|
|
end
|
|
elseif loopMode == "playonce" or loopMode == "PO" then
|
|
-- stop at last frame
|
|
if frameIndex < 1 then
|
|
frameIndex = 1
|
|
end
|
|
if frameIndex > length - 4 then
|
|
frameIndex = length - 4
|
|
end
|
|
|
|
elseif loopMode == "singleframe" or loopMode == "SF" then
|
|
-- stop at first frame
|
|
frameIndex = firstFrame
|
|
end
|
|
self.frame = frameIndex
|
|
else
|
|
self.frame = length - 1
|
|
end
|
|
end
|
|
end
|
|
|
|
function AnimateAtlas:draw(x, y, r, sx, sy, ox, oy)
|
|
r = r or 0.0 -- rotation (radians)
|
|
sx = sx or 1.0 -- scale x
|
|
sy = sy or 1.0 -- scale y
|
|
ox = ox or 0.0 -- origin x
|
|
oy = oy or 0.0 -- origin y
|
|
|
|
local identity = love.math.newTransform()
|
|
identity:translate(x, y)
|
|
|
|
identity:translate(ox, oy)
|
|
identity:rotate(r)
|
|
identity:scale(sx, sy)
|
|
identity:translate(-ox, -oy)
|
|
|
|
local timeline = self:getSymbolTimeline(self.symbol)
|
|
if timeline.data then
|
|
timeline = timeline.data[timeline.optimized and "AN" or "ANIMATION"][timeline.optimized and "TL" or "TIMELINE"]
|
|
end
|
|
self._curSymbol = nil
|
|
self:drawTimeline(timeline, self.frame, identity, nil)
|
|
end
|
|
|
|
return AnimateAtlas |