auto animation script
Key Required

auto animation script

WebTownkaidechannnkaidechannn 174 views 353 executesUpdated Aug 10, 2026
Unrated
00
Lua Script•270 lines (9992 chars)
--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
))

Features & Information

Place pngs into workspace/autodraw (use ffmpeg to split an video into 001.png, 002.png etc.) (must be zero padded) Open up the animation menu in the game Run script and post!

Community Comments (0)

No comments yet for this script.

More Scripts for WebTown