--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 ))