
Keyless

--ai generated by kaede meow okay hi
local FPS = 24
local FRAME_FOLDER = "autodraw"
local WHITE_AS_TRANSPARENT = true
local WHITE_THRESHOLD = 250
local ALPHA_THRESHOLD = 16
local PIXEL_BLOCK = 1
local STABLE_FRAMES = 3
local MAX_FRAMES = 500
local WIDTH = 96
local HEIGHT = 72
local MAX_ENCODED_BYTES = 150000
local AssetService = game:GetService("AssetService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local allowedFps = {
[2] = true,
[4] = true,
[6] = true,
[8] = true,
[12] = true,
[24] = true,
}
assert(allowedFps[FPS], "FPS must be one of: 2, 4, 6, 8, 12, 24")
if not isfolder(FRAME_FOLDER) then
makefolder(FRAME_FOLDER)
error(("Created Workspace/%s. Add 1.png, 2.png, ... and run the script again."):format(FRAME_FOLDER))
end
local framePaths = {}
local highestFrame = 0
local hasFramesAboveLimit = false
for _, path in ipairs(listfiles(FRAME_FOLDER)) do
local fileName = path:match("([^/\\]+)$")
local digits = fileName and fileName:lower():match("^(%d+)%.png$")
local index = digits and tonumber(digits) or nil
if index and index >= 1 then
if index <= MAX_FRAMES then
if framePaths[index] then
error(("Duplicate frame number %d: %s and %s"):format(index, framePaths[index], path))
end
framePaths[index] = path
highestFrame = math.max(highestFrame, index)
else
hasFramesAboveLimit = true
end
end
end
assert(highestFrame > 0, ("No numbered PNGs found. Add Workspace/%s/1.png (or 001.png) first."):format(FRAME_FOLDER))
for index = 1, highestFrame do
assert(framePaths[index], ("Missing frame %d before a later PNG. Frame numbers must be consecutive."):format(index))
end
local frameCount = highestFrame
if hasFramesAboveLimit then
warn(("The game allows only %d frames; later PNGs will be ignored."):format(MAX_FRAMES))
end
local palette = {}
local stableOutput = {}
local pendingOutput = {}
local pendingCounts = {}
local processedFrameCount = 0
local function quantizedColor(red, green, blue)
-- 4 red levels x 4 green levels x 3 blue levels = at most 48 colors.
-- WebTown's animation format supports a maximum global palette of 52 colors.
local quantizedRed = math.clamp(math.floor(red / 85 + 0.5) * 85, 0, 255)
local quantizedGreen = math.clamp(math.floor(green / 85 + 0.5) * 85, 0, 255)
local quantizedBlue = math.clamp(math.floor(blue / 127.5 + 0.5) * 127.5, 0, 255)
quantizedBlue = math.floor(quantizedBlue + 0.5)
local key = quantizedRed * 65536 + quantizedGreen * 256 + quantizedBlue
local color = palette[key]
if not color then
color = Color3.fromRGB(quantizedRed, quantizedGreen, quantizedBlue)
palette[key] = color
end
return color
end
local function loadFrame(path)
local assetId = getcustomasset(path, true)
local ok, imageOrError = pcall(
AssetService.CreateEditableImageAsync,
AssetService,
Content.fromUri(assetId)
)
assert(ok, ("Could not decode %s: %s"):format(path, tostring(imageOrError)))
local image = imageOrError
local imageWidth = math.floor(image.Size.X + 0.5)
local imageHeight = math.floor(image.Size.Y + 0.5)
if imageWidth ~= WIDTH or imageHeight ~= HEIGHT then
image:Destroy()
error(("%s is %dx%d; every frame must be exactly %dx%d."):format(
path,
imageWidth,
imageHeight,
WIDTH,
HEIGHT
))
end
local pixelBuffer = image:ReadPixelsBuffer(Vector2.zero, image.Size)
local grid = {}
processedFrameCount += 1
for blockY = 0, HEIGHT - 1, PIXEL_BLOCK do
for blockX = 0, WIDTH - 1, PIXEL_BLOCK do
local redTotal = 0
local greenTotal = 0
local blueTotal = 0
local alphaTotal = 0
local sampleCount = 0
for sampleY = blockY, math.min(blockY + PIXEL_BLOCK - 1, HEIGHT - 1) do
for sampleX = blockX, math.min(blockX + PIXEL_BLOCK - 1, WIDTH - 1) do
local offset = (sampleY * WIDTH + sampleX) * 4
redTotal += buffer.readu8(pixelBuffer, offset)
greenTotal += buffer.readu8(pixelBuffer, offset + 1)
blueTotal += buffer.readu8(pixelBuffer, offset + 2)
alphaTotal += buffer.readu8(pixelBuffer, offset + 3)
sampleCount += 1
end
end
local red = math.floor(redTotal / sampleCount + 0.5)
local green = math.floor(greenTotal / sampleCount + 0.5)
local blue = math.floor(blueTotal / sampleCount + 0.5)
local alpha = math.floor(alphaTotal / sampleCount + 0.5)
local transparent = alpha < ALPHA_THRESHOLD
if WHITE_AS_TRANSPARENT and red >= WHITE_THRESHOLD and green >= WHITE_THRESHOLD and blue >= WHITE_THRESHOLD then
transparent = true
end
local desired = not transparent and quantizedColor(red, green, blue) or false
for outputY = blockY, math.min(blockY + PIXEL_BLOCK - 1, HEIGHT - 1) do
for outputX = blockX, math.min(blockX + PIXEL_BLOCK - 1, WIDTH - 1) do
local outputIndex = outputY * WIDTH + outputX + 1
local output = desired
if processedFrameCount > 1 then
output = stableOutput[outputIndex]
if desired == output then
pendingOutput[outputIndex] = nil
pendingCounts[outputIndex] = nil
elseif desired == pendingOutput[outputIndex] then
local count = (pendingCounts[outputIndex] or 0) + 1
pendingCounts[outputIndex] = count
if count >= STABLE_FRAMES then
output = desired
pendingOutput[outputIndex] = nil
pendingCounts[outputIndex] = nil
end
else
pendingOutput[outputIndex] = desired
pendingCounts[outputIndex] = 1
end
end
stableOutput[outputIndex] = output
if output then
grid[outputIndex] = output
end
end
end
end
end
image:Destroy()
return grid
end
print(("[AutoDraw] Loading %d frame(s) at %dx%d..."):format(frameCount, WIDTH, HEIGHT))
local importedFrames = table.create(frameCount)
for index = 1, frameCount do
importedFrames[index] = loadFrame(framePaths[index])
if index % 10 == 0 or index == frameCount then
print(("[AutoDraw] Loaded %d/%d"):format(index, frameCount))
task.wait()
end
end
local AnimData = (require :: any)(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("AnimData"))
local encoded = AnimData.encode(importedFrames, FPS, nil)
assert(encoded, "The imported animation contains no visible pixels.")
assert(#encoded <= MAX_ENCODED_BYTES, ("Animation is too complex: %d KB / %d KB game limit. Reduce frame count or image detail."):format(
math.ceil(#encoded / 1000),
math.floor(MAX_ENCODED_BYTES / 1000)
))
local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
local webTown = playerGui:FindFirstChild("WebTown96")
local blocker = webTown and webTown:FindFirstChild("FlipbookBlocker")
local studio = blocker and blocker:FindFirstChild("flipbook.exe - Animation Studio")
assert(studio, "Open flipbook.exe - Animation Studio before running this script.")
local plusButton
local speedButton
for _, child in ipairs(studio:GetChildren()) do
if child:IsA("TextButton") then
local label = child:FindFirstChild("Label")
local text = label and label:IsA("TextLabel") and label.Text or child.Text
if text == "+" then
plusButton = child
elseif text:find("Speed:", 1, true) then
speedButton = child
end
end
end
assert(plusButton, "Could not locate the studio's add-frame button. The game UI may have changed.")
local plusConnections = getconnections(plusButton.MouseButton1Click)
assert(#plusConnections > 0 and typeof(plusConnections[1].Function) == "function", "Could not inspect the studio frame controls.")
local plusUpvalues = debug.getupvalues(plusConnections[1].Function)
local currentFrames = plusUpvalues[1]
local undoStack = plusUpvalues[6]
local rebuildTimeline = plusUpvalues[7]
local showFrame = plusUpvalues[8]
assert(type(currentFrames) == "table", "Could not access the studio's live frame table.")
assert(type(rebuildTimeline) == "function" and type(showFrame) == "function", "Could not access the studio refresh functions.")
-- Update the studio's shared table in place so all of its existing callbacks keep working.
table.clear(currentFrames)
for index, grid in ipairs(importedFrames) do
currentFrames[index] = grid
end
if type(undoStack) == "table" then
table.clear(undoStack)
end
if speedButton then
local speedConnections = getconnections(speedButton.MouseButton1Click)
if #speedConnections > 0 and typeof(speedConnections[1].Function) == "function" then
-- The studio's speed callback captures the live FPS value as its third upvalue.
debug.setupvalue(speedConnections[1].Function, 3, FPS)
local label = speedButton:FindFirstChild("Label")
if label and label:IsA("TextLabel") then
label.Text = ("⏱ Speed: %d fps"):format(FPS)
end
end
end
rebuildTimeline()
showFrame(1)
print(("[AutoDraw] Imported %d frame(s), %d KB encoded, %d fps. Click Done! to save."):format(
frameCount,
math.ceil(#encoded / 1000),
FPS
))No comments yet for this script.