Aseprite file importer - is that possible?

0 favourites
  • 10 posts
From the Asset Store
Excel2JSON
$1.20 USD
60% off
Convert multiple Excel files into JSON files and import them into construct3.
  • I think aseprite files have quite some data that simply gets lost on export, but would be quite useful if retained (frame speed and timings, animation names, loop/pingpong settings) not to mention how awesome it would be if I could just import the aseprite files directly instead of exporting/importing.

    The specs would be here

    github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md

    The thing is... I kinda doubt that it will be implemented officially, simply because then everyone wants their program of choice to be directly importable (.psd files, .csp files,...) so I wonder if it's possible to somehow make it happen from a user side? I don't think the addon SDK can help here, so I'm at a loss. I'd also have no idea where to start with the documentation, that's beyond my skills.

    One alternate solution would be instead to create an aseprite export script that then exports based on these specifications construct.net/en/forum/construct-3/general-discussion-7/animations-editor-new-161331 along with a json file for the data. Issue is I have no idea about LUA or aseprite scripting so... yeah. Also it would not be as convenient as a direct import.

    In general I feel like the whole exporting/importing procedere throws quite some grit into the workflow... am I alone with this? Maybe I'll write up a suggestion for some stuff at some point, but since I'm mostly doing pixelart nowadays, this importer would be really helpful already.

    Tagged:

  • The Animations editor already supports bulk importing.

    construct.net/en/make-games/manuals/construct-3/interface/animations-editor

    The basic use case is just being able to drop a folder or a zip file onto the animations pane and have an animation created. The animation is named after the folder or zip file.

    For more complex use cases, the importer supports placing a configuration file next to the files. The configuration file is a JSON file which can include several settings to be applied on the created animations.

    I haven't tried it myself, but it should be relatively easy to have Aseprite (or any other program that supports some kind of scripting) to package all the files together in a way C3 will understand.

  • Oh I know, that's what I mentioned as the alternate solution ;)

    There's still some issues I see even if I had such an exporter, but it would absolutely be a step in the right direction. At least I think I see issues, I have barely tried the bulk importer.

    I'm also not currently in the mood at all for tool dev, and as mentioned I don't know LUA (which aseprite uses) so that would involve a ton of reading, trial and error. I dunno about relatively easy...

  • Should have bolded and quoted "RELATIVELY".

    Anyway, I got interested in this and cobbled this:

    -- Get the active sprite
    local sprite = app.sprite
    
    if sprite == nil then
    	print("No active sprite found.")
    	return
    end
    
    local spriteName = string.match(sprite.filename, "[^/\\]+$")
    spriteName = spriteName:gsub("%..*$", "")
    
    -- Create a new dialog
    local dlg = Dialog("Export animation for C3")
    dlg:entry{ id="animationName", label="Animation name:", text=spriteName }
    dlg:check{ id="loop", label="Loop" }
    dlg:check{ id="pingPong", label="Ping Pong" }
    dlg:number{ id="repeatCount", label="Repeat count:", text=string.format("%i", 1), decimals=0 }
    dlg:number{ id="repeatTo", label="Repeat to:", text=string.format("%i", 0), decimals=0 }
    dlg:button{ id="ok", text="OK", focus=true }
    dlg.bounds = Rectangle(dlg.bounds.x - 50, dlg.bounds.y, dlg.bounds.width + 100, dlg.bounds.height)
    dlg:show()
    
    -- Get the data from the dialog
    local data = dlg.data
    
    -- Check if the user cancelled the dialog
    if data == nil then
    	print("Export canceled by user.")
    	return
    end
    
    if not data.ok then
    	print("Export canceled by user.")
    	return
    end
    
    -- Extract the directory path of the original sprite file
    local originalFilePath = sprite.filename
    
    local outputFolder = app.fs.joinPath(string.match(originalFilePath, "^(.-)([^\\/]-%.?([^%.\\/]*))$"), "tmp-export-for-c3")
    if outputFolder == nil then
    	print("Failed to determine output folder.")
    	return
    end
    
    -- Use the name that was set in the dialog or the file name if no name was specified in the dialog
    local defaultAnimationName = data.animationName and data.animationName or spriteName
    
    -- Construct the JSON data
    local jsonData = {
    	["use-raw-folder-names"] = true,
    	["animation"] = {
    		["name"] = defaultAnimationName,
    		["speed"] = 0,
    		["loop"] = data.loop,
    		["ping-pong"] = data.pingPong,
    		["repeat-count"] = data.repeatCount and data.repeatCount or 1,
    		["repeat-to"] = data.repeatTo and data.repeatTo or 0,
    		["frame-durations"] = {},
    		["frame-tags"] = {}
    	}
    }
    
    local frameTags = {}
    
    for i, tag in ipairs(sprite.tags) do
    	local frame = tag.fromFrame;
    	frameTags[frame.frameNumber] = tag.name
    end
    
    local fps = math.floor((1000 / (sprite.frames[1].duration * 1000)) + 0.5)
    
    jsonData["animation"]["speed"] = fps
    
    -- Flatten to merge all layers
    sprite:flatten()
    
    for i, cel in ipairs(sprite.cels) do
    	local filename = app.fs.joinPath(outputFolder, i .. ".png")
    
    	cel.image:saveAs(filename)
    
    	local lfps = math.floor((1000 / (cel.frame.duration * 1000)) + 0.5)
    
    	table.insert(jsonData["animation"]["frame-durations"], math.floor((fps / lfps) + 0.5))
    
    	local frameTag = frameTags[cel.frame.frameNumber] and frameTags[cel.frame.frameNumber] or ""
    
    	table.insert(jsonData["animation"]["frame-tags"], frameTag)
    end
    
    -- Undo the flatten done earlier
    app.command.Undo()
    
    -- Write JSON data to file
    
    local jsonFilename = app.fs.joinPath(outputFolder, "c3-import-settings.json")
    local jsonFile = io.open(jsonFilename, "w")
    if jsonFile then
    	jsonFile:write(json.encode(jsonData))
    	jsonFile:close()
    else
    	print("Failed to generate JSON file.")
    	return
    end
    
    local pathSeparator = package.config:sub(1, 1)
    -- Choose the appropriate zip command based on the operating system
    local osType = (pathSeparator == "\\") and "Windows" or "Unix-based"
    
    local zipCmd
    
    local zipFilename = app.fs.joinPath(outputFolder, defaultAnimationName .. ".zip")
    
    if osType:find("Windows") then
     -- Use PowerShell on Windows
     zipCmd = 'powershell Compress-Archive -Path "' .. outputFolder .. pathSeparator .. "*" .. '" -DestinationPath "' .. zipFilename .. '" -Force'
    else
     -- Use the zip command on Unix-based systems
     zipCmd = 'zip -r "' .. zipFilename .. '" "' .. outputFolder .. pathSeparator .. "*" ..'"'
    end
    
    -- Execute the zip command
    os.execute(zipCmd)
    
    -- Move the zip file to the new destination path
    local directoryPath, fileName = zipFilename:match("(.+)[/\\]([^/\\]+)$")
    local newZipFilePath = app.fs.joinPath(directoryPath, "..", fileName)
    
    local moveCommand
    
    if osType:find("Windows") then
     -- Windows system
     moveCommand = "move".. " " .. zipFilename .. " " .. newZipFilePath
    else
     -- Unix-like system (Linux, macOS, etc.)
     moveCommand = "mv".. " " .. zipFilename .. " " .. newZipFilePath
    end
    
    os.execute(moveCommand)
    
    -- Remove the temporary folder
    local removeCommand
    
    if osType:find("Windows") then
     -- Windows system
     removeCommand = "rmdir /s /q" .. " " .. outputFolder
    else
     -- Unix-like system (Linux, macOS, etc.)
     removeCommand = "rm -rf" .. " " .. outputFolder
    end
    
    os.execute(removeCommand)
    

    That produces a zip file with all the images and the JSON so C3 knows what to do with it.

    I found a couple of issue which can not be reconciled:

    1. There is no explicit FPS for the whole animation, instead each frame has a time in milleseconds. To fill in a value I calculate a tentative FPS from the duration of the first frame and then use that value to calculate durations for the rest of the frames which are relative to that first measure.
    2. The way Aseprite uses settings such as Loop is per tag, so you can have multiple different ones in ranges of the whole timeline. C3 just has one value for the whole animation. So instead of using the values from Aseprite, the export script just shows a dialog with the settings that can't be extracted in a way that makes sense.
    3. The export script can use the tag information, but not completely because Aseprite is more advanced that C3 in that regard. In C3 you only have a tag for each frame, that's it. In Aseprite you can define a range. To do something useful with the data, the frames where a tag range starts, get the value, while frames covered by the range, are left empty. The information could be used differently.
    4. The script flattens all the layers to export all the image information. This could be done differently, like for instance letting the script just export a specified layer.

    You can try it out and see if it's useful to you.

  • Should have bolded and quoted "RELATIVELY".

    Haha yeah. Thank you so much! I'll try it out asap.

    There is no explicit FPS for the whole animation, instead each frame has a time in milleseconds. To fill in a value I calculate a tentative FPS from the duration of the first frame and then use that value to calculate durations for the rest of the frames which are relative to that first measure.

    That is fine. That is one of the settings that I often end up changing anyway. Often I end up importing an animation and it ends up faster or slower than I have set it up in aseprite simply because in the context of the entire game it feels different. What is interesting is really the individual frame-timing!

    The way Aseprite uses settings such as Loop is per tag, so you can have multiple different ones in ranges of the whole timeline. C3 just has one value for the whole animation. So instead of using the values from Aseprite, the export script just shows a dialog with the settings that can't be extracted in a way that makes sense.

    The way I use tags is to have multiple animations in one file. In my ideal world I would export each tag from aseprite into a different animation. Here's an example from the net of how I roughly set up my files. So each tags loop settings would be per animation, and each tag would export a different animation for the same sprite.

    i.ytimg.com/vi/KzgugqLahik/maxresdefault.jpg

    The export script can use the tag information, but not completely because Aseprite is more advanced that C3 in that regard. In C3 you only have a tag for each frame, that's it. In Aseprite you can define a range. To do something useful with the data, the frames where a tag range starts, get the value, while frames covered by the range, are left empty. The information could be used differently.

    See above, I'd use the frame tag as an individual animation. Tags in the context of aseprite are used differently than in Construct. That would mean though there is no real place to actually put tags as they are used in Construct, but that's a fair tradeoff. I'll see if I can manage to adapt the script.

  • I figured a first attempt would miss the mark, specially because I am sure there are a few popular workflows that people like to use when making their animations. Just wanted to make something because it's easier to tweak an existing script than coming up with it from scratch.

    Generating a unique zip file for each tag sounds like it would fit in nicely with what the API has to offer. That would prevent using the tag information for C3 though. Fortunately I saw in the documentation of Aseprite, that you can assign custom data to frames. That custom information could be used by the exporter to generate C3 tags for each frame.

  • I figured a first attempt would miss the mark, specially because I am sure there are a few popular workflows that people like to use when making their animations. Just wanted to make something because it's easier to tweak an existing script than coming up with it from scratch.

    All good, it's a start and I can try and adapt it when I have spare time. There's no way to make a one-size-fits-all solution anyway. But having a base script to start off of is already really helpful.

    Fortunately I saw in the documentation of Aseprite, that you can assign custom data to frames. That custom information could be used by the exporter to generate C3 tags for each frame.

    That's good news. Probably also useful for adding origin/imagepoint data and such! My ideal would be that I'm able to input practically all the data of a sprite in aseprite, generate a zip (or multiple) and drag & drop it into Construct which then updates all animations with the new settings/data.

    This means some initial extra work but once it's all set up I should be able to iterate on the animations really quickly, and it's all packaged very neatly.

  • I haven't looked closely enough to the documentation or even tried anything but I think that making an extension to visually edit image points and collision polygons should be possible since there is functionality to draw custom things on a canvas in a dialog, as well as access to mouse events... it does sound like a ton of work though, so I don't think I will be doing that.

    I might do some experiments to figure out if it is possible or not.

  • Well this is gonna need some more tinkering. I've managed to update the script more to my workflow, so now it exports each tag as an individual zip. It's a bit rough but it works ok for now.

    The main issue I'm still facing is that cels are not the full image but it's basically the cropped sprite, which for animations causes the origin to go all over the place due to different individual frame sizes. I need to somehow figure out how to get the origin on a consistent spot despite that.

    I can easily get the cel.bounds and I also have the sprite width/height.

    aseprite.org/api/image

    I think that should be everything I need to math it out but my brain is not braining today. Anyway, here's the updated script for now.

    -- Get the active sprite
    local sprite = app.sprite
    
    if sprite == nil then
    	print("No active sprite found.")
    	return
    end
    
    local spriteName = string.match(sprite.filename, "[^/\\]+$")
    spriteName = spriteName:gsub("%..*$", "")
    
    
    -- Create a new dialog
    local dlg = Dialog("Export animation for C3 - this will briefly hang :)")
    dlg:button{ id="confirm", text="Confirm" }
    dlg:button{ id="cancel", text="Cancel" }
    dlg:show()
    
    local data = dlg.data
    
    if data.confirm then
    	-- Extract the directory path of the original sprite file
    	local originalFilePath = sprite.filename
    	
    	local outputFolder = app.fs.joinPath(string.match(originalFilePath, "^(.-)([^\\/]-%.?([^%.\\/]*))$"), "tmp-export-for-c3")
    	if outputFolder == nil then
    		print("Failed to determine output folder.")
    		return
    	end
    	
    	for i, tag in ipairs(sprite.tags) do
    		
    		-- Use the name that was set in the dialog or the file name if no name was specified in the dialog
    		local defaultAnimationName = tag.name
    		
    		-- Construct the JSON data
    		local jsonData = {
    			["use-raw-folder-names"] = true,
    			["animation"] = {
    				["name"] = tag.name,
    				["speed"] = 0,
    				["loop"] = false,
    				["ping-pong"] = false,
    				["repeat-count"] = tag.repeats or 1,
    				["repeat-to"] = 0,
    				["frame-durations"] = {},
    				["frame-tags"] = {},
    				["frame-image-points"] = {}
    			}
    		}
    		
    		if tag.aniDir <= 1 then
    			jsonData["animation"]["loop"] = true
    		else
    			jsonData["animation"]["ping-pong"] = true
    		end
    		
    		local fps = math.floor((1000 / (tag.fromFrame.duration * 1000)) + 0.5)
    		jsonData["animation"]["speed"] = fps
    		
    		-- Flatten to merge all layers
    		local dupe = Sprite(sprite)
    		dupe:flatten()
    		
    		local origin = {{
    			originX = 0.5,
    			originY = 0.5
    		}}
    		
    		for i, cel in ipairs(dupe.cels) do
    		
    			if cel.frameNumber >= tag.fromFrame.frameNumber and cel.frameNumber <= tag.toFrame.frameNumber then
    				local filename = app.fs.joinPath(outputFolder, i .. ".png")
    				
    				cel.image:saveAs(filename)
    				
    				--jsonData["frame-image-points"].inser = {"originX":3, "originY":3}
    				table.insert(jsonData["animation"]["frame-image-points"], origin)
    				
    				local lfps = math.floor((1000 / (cel.frame.duration * 1000)) + 0.5)
    				table.insert(jsonData["animation"]["frame-durations"], math.floor((fps / lfps) + 0.5))
    				end
    		end
    		
    		dupe:close()
    		
    		--app.command.Undo()
    		
    		-- Write JSON data to file
    		local jsonFilename = app.fs.joinPath(outputFolder, "c3-import-settings.json")
    		local jsonFile = io.open(jsonFilename, "w")
    		if jsonFile then
    			jsonFile:write(json.encode(jsonData))
    			jsonFile:close()
    		else
    			print("Failed to generate JSON file.")
    			return
    		end
    		
    		local pathSeparator = package.config:sub(1, 1)
    		-- Choose the appropriate zip command based on the operating system
    		local osType = (pathSeparator == "\\") and "Windows" or "Unix-based"
    		
    		local zipCmd
    		
    		local zipFilename = app.fs.joinPath(outputFolder, defaultAnimationName .. ".zip")
    		
    		if osType:find("Windows") then
    		-- Use PowerShell on Windows
    		zipCmd = 'powershell Compress-Archive -Path "' .. outputFolder .. pathSeparator .. "*" .. '" -DestinationPath "' .. zipFilename .. '" -Force'
    		else
    		-- Use the zip command on Unix-based systems
    		zipCmd = 'zip -r "' .. zipFilename .. '" "' .. outputFolder .. pathSeparator .. "*" ..'"'
    		end
    		
    		-- Execute the zip command
    		os.execute(zipCmd)
    		
    		-- Move the zip file to the new destination path
    		local directoryPath, fileName = zipFilename:match("(.+)[/\\]([^/\\]+)$")
    		local newZipFilePath = app.fs.joinPath(directoryPath, "..", fileName)
    		
    		local moveCommand
    		
    		if osType:find("Windows") then
    		-- Windows system
    		moveCommand = "move".. " " .. zipFilename .. " " .. newZipFilePath
    		else
    		-- Unix-like system (Linux, macOS, etc.)
    		moveCommand = "mv".. " " .. zipFilename .. " " .. newZipFilePath
    		end
    		
    		os.execute(moveCommand)
    		
    		-- Remove the temporary folder
    		local removeCommand
    		
    		if osType:find("Windows") then
    		-- Windows system
    		removeCommand = "rmdir /s /q" .. " " .. outputFolder
    		else
    		-- Unix-like system (Linux, macOS, etc.)
    		removeCommand = "rm -rf" .. " " .. outputFolder
    		end
    		
    		os.execute(removeCommand)
    		
    	end
    end
  • Try Construct 3

    Develop games in your browser. Powerful, performant & highly capable.

    Try Now Construct 3 users don't see these ads
  • You might be able to get around that by creating a temporary image, rather than using the one from the cel.

    aseprite.org/api/image

    I am thinking of creating a new image, making it the size of the frame so it includes all the empty space, and then use image.drawImage to paste the original one in the correct position. Not sure where to take the "correct" position from though, but it should be there somewhere.

    The position should be Cel.position

    aseprite.org/api/cel

Jump to:
Active Users
There are 1 visitors browsing this topic (0 users and 1 guests)