├── assets ├── hero.png └── timeline.png ├── gif-sequencing.aseprite-extension ├── .gitignore ├── extension ├── package.json ├── plugin.lua ├── export.lua └── gif-sequencing.lua ├── LICENSE └── README.md /assets/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fletchmakes/gif-sequencing/HEAD/assets/hero.png -------------------------------------------------------------------------------- /assets/timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fletchmakes/gif-sequencing/HEAD/assets/timeline.png -------------------------------------------------------------------------------- /gif-sequencing.aseprite-extension: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fletchmakes/gif-sequencing/HEAD/gif-sequencing.aseprite-extension -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Other ignores 2 | **.vscode 3 | 4 | # Compiled Lua sources 5 | luac.out 6 | 7 | # luarocks build files 8 | *.src.rock 9 | *.zip 10 | *.tar.gz 11 | 12 | # Object files 13 | *.o 14 | *.os 15 | *.ko 16 | *.obj 17 | *.elf 18 | 19 | # Precompiled Headers 20 | *.gch 21 | *.pch 22 | 23 | # Libraries 24 | *.lib 25 | *.a 26 | *.la 27 | *.lo 28 | *.def 29 | *.exp 30 | 31 | # Shared objects (inc. Windows DLLs) 32 | *.dll 33 | *.so 34 | *.so.* 35 | *.dylib 36 | 37 | # Executables 38 | *.exe 39 | *.out 40 | *.app 41 | *.i*86 42 | *.x86_64 43 | *.hex 44 | 45 | -------------------------------------------------------------------------------- /extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gif-sequencing", 3 | "displayName": "Gif Sequencing", 4 | "description": "Export your sprite as a gif with the tags in a custom order!", 5 | "version": "1.2", 6 | "author": { "name": "David Fletcher", 7 | "email": "david.login@aol.com", 8 | "url": "https://github.com/david-fletcher/gif-sequencing" }, 9 | "contributors": [ ], 10 | "publisher": "David Fletcher", 11 | "license": "MIT", 12 | "categories": [ "Scripts" ], 13 | "contributes": { 14 | "scripts": [ 15 | { "path": "./plugin.lua" } 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 David Fletcher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gif Sequencing 2 | ![hero image](./assets/hero.png) 3 | 4 | An Aseprite extension that allows you to export your sprite as a gif with the tags in a custom order! 5 | 6 | ## How to Use Gif Sequencing 7 | 8 | 1. Download this extension by visiting the releases page! (Double click the extension once downloaded to install) 9 | 2. In Aseprite, select `File > Export Gif Sequence` 10 | 3. Use the dialog windows to construct and export a custom gif sequence! 11 | 12 | ## What is a Gif Sequence? 13 | 14 | A gif sequence is simply another way to export your Aseprite animation as a .gif file. However, this plugin will allow you to build up your animation based on the timeline tags. This allows you to use more flexibility when creating your animations. In the image below, the timeline only has 1 set of frames for idle, walking, and running, but the gif sequence is built in such a way that the .gif fully expresses what the animation will look like within your project. 15 | 16 | ![timeline image](./assets/timeline.png) 17 | ![build sequence](https://media.giphy.com/media/kKrdE8Ovd7la66asJz/giphy-downsized-large.gif) 18 | 19 | ## Sequence Presets 20 | 21 | Gif Sequence supports the use of presets. Simply save the current sequence as a preset by clicking the checkbox and entering a name for the sequence, then clicking the save button. Now, whenever you load Gif Sequencing, you can select a preset from the dropdown at the top of the window and it will automagically populate the sequence for you! Want to edit a preset you already have defined? Simply save it again under the same name! 22 | 23 | ## Credits 24 | 25 | This extension was commissioned by [@2dchaos](https://twitter.com/2dchaos) on Twitter! Thank you! 26 | 27 | As an advocate of open-source software, feel free to suggest edits, or just fork this repository and make your own! The license on this software is open for commercial and private use. This extension will remain free forever; however, if you'd like to buy me a coffee, you can do so here: 28 | 29 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/L3L766S5F) 30 | -------------------------------------------------------------------------------- /extension/plugin.lua: -------------------------------------------------------------------------------- 1 | -- MIT License 2 | 3 | -- Copyright (c) 2021 David Fletcher 4 | 5 | -- Permission is hereby granted, free of charge, to any person obtaining a copy 6 | -- of this software and associated documentation files (the "Software"), to deal 7 | -- in the Software without restriction, including without limitation the rights 8 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | -- copies of the Software, and to permit persons to whom the Software is 10 | -- furnished to do so, subject to the following conditions: 11 | 12 | -- The above copyright notice and this permission notice shall be included in all 13 | -- copies or substantial portions of the Software. 14 | 15 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | -- SOFTWARE. 22 | 23 | function init(plugin) 24 | print("Aseprite is initializing Gif Sequencing") 25 | 26 | ----------------------------------- 27 | -- PLUGIN COMMANDS 28 | ----------------------------------- 29 | -- A plugin command is simply a menu element that, when clicked, runs the function passed to onclick. 30 | -- In this file, we specify menu element definitions, but abstract away their implementations to different Lua scripts. 31 | -- This is done for organizational purposes, and to ease the iterative development of scripts. With this structure, 32 | -- we can develop the script file independently of the plugin definition, using Aseprite's scripting engine 33 | -- to run scripts as we build out our functionality, then wrap the finalized scripts in a plugin. 34 | ----------------------------------- 35 | -- We use the parameter to init, plugin, to access the preferences table. This table will be saved on exit and restored 36 | -- upon re-entry, so that you can easily save any user-defined preferences you need to. You will notice that we use 37 | -- loadfile() to create a Lua chunk based on our script, and then pass the preferences table to the script as an argument. 38 | -- The script can modify that table, which will then be saved automagically by Aseprite's API. 39 | ----------------------------------- 40 | plugin:newCommand { 41 | id="gif-sequencing", 42 | title="Export Gif Sequence", 43 | group="file_export", 44 | onclick=function() 45 | local executable = app.fs.joinPath(app.fs.userConfigPath, "extensions", "gif-sequencing", "gif-sequencing.lua") 46 | -- load the lua script into a Lua chunk, then execute it with the parameter plugin.preferences 47 | loadfile(executable)(plugin.preferences) 48 | end 49 | } 50 | end 51 | 52 | function exit(plugin) 53 | print("Aseprite is closing Gif Sequencing") 54 | end -------------------------------------------------------------------------------- /extension/export.lua: -------------------------------------------------------------------------------- 1 | -- MIT License 2 | 3 | -- Copyright (c) 2021 David Fletcher 4 | 5 | -- Permission is hereby granted, free of charge, to any person obtaining a copy 6 | -- of this software and associated documentation files (the "Software"), to deal 7 | -- in the Software without restriction, including without limitation the rights 8 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | -- copies of the Software, and to permit persons to whom the Software is 10 | -- furnished to do so, subject to the following conditions: 11 | 12 | -- The above copyright notice and this permission notice shall be included in all 13 | -- copies or substantial portions of the Software. 14 | 15 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | -- SOFTWARE. 22 | 23 | local sequence = ... 24 | 25 | ----------------------------------- 26 | -- HELPER METHODS 27 | ----------------------------------- 28 | local function readAllTags(sprite) 29 | local tags = {} 30 | 31 | -- main loop where we compile all of the info we need to essentially copy / paste 32 | for _,tag in ipairs(sprite.tags) do 33 | -- snag the name from the tag to use as a dictionary reference when we're reading the sequence later 34 | tags[tag.name] = {} 35 | 36 | -- loop through all of the frames within this tag and copy the image information from each cel 37 | tags[tag.name].frames = {} 38 | -- we should be flattened at this point, so grabbing the active layer is a cheap operation to get what we want (the layer:cel() function) 39 | local layer = app.activeLayer 40 | for f=tag.fromFrame.frameNumber,tag.toFrame.frameNumber do 41 | local cel = layer:cel(f) 42 | if (cel ~= nil) then 43 | table.insert(tags[tag.name].frames, { 44 | pixels=layer:cel(f).image, 45 | position=layer:cel(f).position 46 | }) 47 | else 48 | table.insert(tags[tag.name].frames, { 49 | pixels=nil, 50 | position=nil 51 | }) 52 | end 53 | end 54 | end 55 | 56 | return tags 57 | end 58 | 59 | local function buildSequence(sprite, sequence, tags) 60 | -- we should be flattened at this point, so grabbing the active layer is a cheap operation to get what we want (the layer:cel() function) 61 | local layer = app.activeLayer 62 | 63 | -- loop through the sequence and start building up the frames 64 | for _,seq_tag in ipairs(sequence.tags) do 65 | -- loop through the repetitions on this tag 66 | for tag=1,seq_tag.repetitions do 67 | -- loop through the frames on this tag 68 | for _,image in ipairs(tags[seq_tag.name].frames) do 69 | local frame = sprite:newEmptyFrame(#sprite.frames+1) 70 | if (image.pixels ~= nil) then 71 | sprite:newCel(layer, frame, image.pixels, image.position) 72 | end 73 | end 74 | end 75 | end 76 | end 77 | 78 | local function deleteAllTags(sprite) 79 | for tag=1,#sprite.tags do 80 | sprite:deleteTag(sprite.tags[1]) 81 | end 82 | end 83 | 84 | local function deleteAllFramesInRange(sprite, min, max) 85 | for f=min,max do 86 | sprite:deleteFrame(sprite.frames[min]) 87 | end 88 | end 89 | 90 | local function deleteAllHiddenLayers(sprite) 91 | for _,layer in ipairs(sprite.layers) do 92 | if (not layer.isVisible) then 93 | sprite:deleteLayer(layer) 94 | end 95 | end 96 | end 97 | 98 | local function exportAsGif() 99 | -- it says "save a copy", but really this opens the file export menu 100 | app.command.SaveFileCopyAs { ["useUI"]=true } 101 | end 102 | 103 | ----------------------------------- 104 | -- EXECUTE MAIN LOGIC 105 | ----------------------------------- 106 | 107 | -- origSprite is the reference to the user's original file. DO NOT MODIFY 108 | -- tempSprite is the reference to the file we will be modifying heavily before exporting, then abandoning 109 | local origSprite = app.activeSprite 110 | local tempSprite = Sprite(origSprite) 111 | 112 | -- delete all of the invisible layers because they are not needed, then flatten 113 | deleteAllHiddenLayers(tempSprite) 114 | tempSprite:flatten() 115 | 116 | -- read all of the tags into a large table that we can pull info from later 117 | local tags = readAllTags(tempSprite) 118 | local lastFrameOfOriginalSprite = #tempSprite.frames 119 | 120 | -- start putting the sequence back together in the specified order 121 | -- appended at the back of the sprite; original frames will be deleted after we've built the sequence 122 | buildSequence(tempSprite, sequence, tags) 123 | 124 | -- delete all of the tags so there isn't any confusion (this step isn't strictly necessary, but it was 125 | -- helpful during debugging to keep me from getting confused) 126 | deleteAllTags(tempSprite) 127 | 128 | -- we've built the sequence appended as frames at the end of the timeline; let's now delete the original 129 | -- frames to leave behind JUST the sequence desired 130 | deleteAllFramesInRange(tempSprite, 1, lastFrameOfOriginalSprite) 131 | 132 | -- finally ready to export! 133 | exportAsGif() 134 | 135 | -- close the temporary sprite to finalize the process 136 | tempSprite:close() 137 | -------------------------------------------------------------------------------- /extension/gif-sequencing.lua: -------------------------------------------------------------------------------- 1 | -- MIT License 2 | 3 | -- Copyright (c) 2021 David Fletcher 4 | 5 | -- Permission is hereby granted, free of charge, to any person obtaining a copy 6 | -- of this software and associated documentation files (the "Software"), to deal 7 | -- in the Software without restriction, including without limitation the rights 8 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | -- copies of the Software, and to permit persons to whom the Software is 10 | -- furnished to do so, subject to the following conditions: 11 | 12 | -- The above copyright notice and this permission notice shall be included in all 13 | -- copies or substantial portions of the Software. 14 | 15 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | -- SOFTWARE. 22 | 23 | ----------------------------------- 24 | -- USER DEFINED PREFERENCES 25 | ----------------------------------- 26 | -- In this file, we receive a reference to the table plugin.preferences (from ./plugin.lua). 27 | -- In Lua, when a parameter is a table, it is passed by reference. 28 | -- This allows us to mutate the parameter, and the original object will be mutated as well; 29 | -- Thus, we do not need to have a "return value" from this script 30 | ----------------------------------- 31 | -- If you need to stage edits to the preferences table, but don't want them saved in certain situations, 32 | -- consider creating a deep copy of the preferences table at the start of your script, 33 | -- editing the copy, and if you'd like to save your changes, deep copy the edited copy you made, and 34 | -- save it back to the original preferences table; an implementation of a deep copy function 35 | -- can be found in ./helpers.lua 36 | ----------------------------------- 37 | local prefs = ... 38 | if (prefs == nil) then 39 | prefs = { 40 | presets = {} 41 | } 42 | end 43 | 44 | ----------------------------------- 45 | -- CREATE CONFIRM DIALOG 46 | ----------------------------------- 47 | -- param: str - a string value that will be displayed to the user (usually a question if they'd like to confirm an action) 48 | -- returns: true, if the user clicked "Confirm"; nil, if the user clicked "Cancel" or closed the dialog 49 | ----------------------------------- 50 | -- This function creates a simple dialog window that will display a message, along with two buttons: "Cancel" and "Confirm". 51 | -- You can observe whether or not the user closed the dialog in this way: 52 | -- local confirmed = create_confirm("Would you like to continue?") 53 | -- if (not confirmed) then 54 | -- end 55 | ----------------------------------- 56 | local function create_confirm(str) 57 | local confirm = Dialog("Confirm?") 58 | 59 | confirm:label { 60 | id="text", 61 | text=str 62 | } 63 | 64 | confirm:button { 65 | id="cancel", 66 | text="Cancel", 67 | onclick=function() 68 | confirm:close() 69 | end 70 | } 71 | 72 | confirm:button { 73 | id="confirm", 74 | text="Confirm", 75 | onclick=function() 76 | confirm:close() 77 | end 78 | } 79 | 80 | -- show to grab centered coordinates 81 | confirm:show{ wait=true } 82 | 83 | return confirm.data.confirm 84 | end 85 | 86 | ----------------------------------- 87 | -- CREATE ERROR DIALOG 88 | ----------------------------------- 89 | -- param: table - a table of string values that will be displayed to the user (usually an error message) 90 | -- param: dialog - the Dialog object (if any) that spawned this error message 91 | -- param: exit - a boolean value; 0 to keep dialog open, and 1 to close dialog 92 | ----------------------------------- 93 | -- This function creates a simple dialog window that will alert the user of an error that occured. 94 | -- If you call this from a Dialog function, you can optionally close the dialog to end user interaction 95 | -- and prevent further errors (making the user try again). 96 | ----------------------------------- 97 | local function create_error(table, dialog, exit) 98 | app.alert{title="There was an error.", text=table} 99 | if (exit == 1) then dialog:close() end 100 | end 101 | 102 | ----------------------------------- 103 | -- DEEPCOPY FUNCTION (from http://lua-users.org/wiki/CopyTable) 104 | ----------------------------------- 105 | -- param: orig - any Lua value (table or otherwise) that should be copied and returned 106 | -- returns: an exact copy of the original value 107 | ----------------------------------- 108 | -- This function is extremely useful if you need to stage edits to a table, but 109 | -- roll them back if there was an error or if the user cancelled the action. 110 | -- Create a copy of the table you want to edit, edit the copy, and then do nothing 111 | -- if the changes should be ignored; but if the changes should be saved, deep copy the 112 | -- edited copy again, and set that equal to the original value you were trying to edit. 113 | ----------------------------------- 114 | local function deepcopy(orig) 115 | -- http://lua-users.org/wiki/CopyTable 116 | local orig_type = type(orig) 117 | local copy 118 | 119 | if orig_type == 'table' then 120 | copy = {} 121 | for orig_key, orig_value in next, orig, nil do 122 | copy[deepcopy(orig_key)] = deepcopy(orig_value) 123 | end 124 | 125 | setmetatable(copy, deepcopy(getmetatable(orig))) 126 | else -- number, string, boolean, etc 127 | copy = orig 128 | end 129 | 130 | return copy 131 | end 132 | 133 | ----------------------------------- 134 | -- HELPER FUNCTIONS 135 | ----------------------------------- 136 | local function getListOfTags() 137 | local result = {} 138 | 139 | local sprite = app.activeSprite 140 | for _,t in ipairs(sprite.tags) do 141 | table.insert(result, t.name) 142 | end 143 | 144 | return result 145 | end 146 | 147 | local function getNumberList(max) 148 | local result = {} 149 | 150 | for i=1,max do 151 | table.insert(result, tostring(i)) 152 | end 153 | 154 | return result 155 | end 156 | 157 | local function getPresetNameList(presets) 158 | local pNames = { 159 | "None" 160 | } 161 | 162 | local map = {} 163 | 164 | for i,p in pairs(presets) do 165 | table.insert(pNames, p.preset) 166 | map[p.preset] = i 167 | end 168 | 169 | return pNames, map 170 | end 171 | 172 | local function validatePresetHasNecessaryTags(sequence) 173 | -- if the sequence is nil, just pass back true (probably a "None" preset) 174 | if (sequence == nil) then return true end 175 | 176 | local tagNamesMissing = {} 177 | 178 | local foundAll = true 179 | local foundOne = false 180 | for _,seq_t in pairs(sequence.tags) do 181 | foundOne = false 182 | for _,spr_t in ipairs(app.activeSprite.tags) do 183 | if (seq_t.name == spr_t.name) then 184 | foundOne = true 185 | break 186 | end 187 | end 188 | 189 | if (not foundOne) then 190 | table.insert(tagNamesMissing, seq_t.name) 191 | foundAll = false 192 | end 193 | end 194 | 195 | return foundAll, tagNamesMissing 196 | end 197 | 198 | ----------------------------------- 199 | -- WINDOW FACTORIES 200 | ----------------------------------- 201 | local function reorderTagsWindow(sequence, exit) 202 | local dialog = Dialog("Reorder Tags") 203 | 204 | local function refresh(dlg) 205 | dlg:close() 206 | reorderTagsWindow(sequence, exit):show{ wait=true } 207 | end 208 | 209 | -- build the list of tags that can be re-ordered 210 | local numberList = getNumberList(#sequence) 211 | 212 | for idx,tag in ipairs(sequence) do 213 | dialog:combobox { 214 | id="tag"..idx, 215 | label=idx..": "..tag.name.." x"..tag.repetitions, 216 | option=tostring(idx), 217 | options=numberList, 218 | onchange=function() 219 | table.remove(sequence, idx) 220 | table.insert(sequence, tonumber(dialog.data["tag"..idx]), tag) 221 | refresh(dialog) 222 | end 223 | } 224 | end 225 | 226 | dialog:separator { 227 | id="actions", 228 | text="Actions" 229 | } 230 | 231 | dialog:button { 232 | id="cancel", 233 | text="Cancel", 234 | onclick=function() 235 | dialog:close() 236 | end 237 | } 238 | 239 | dialog:button { 240 | id="save", 241 | text="Save", 242 | onclick=function() 243 | exit.action = "save" 244 | dialog:close() 245 | end 246 | } 247 | 248 | return dialog 249 | end 250 | 251 | local function editTagWindow(tag) 252 | local dialog = Dialog("Edit Tag") 253 | 254 | dialog:separator { 255 | id="details", 256 | text="Tag Details" 257 | } 258 | 259 | dialog:combobox { 260 | id="tag", 261 | label="Tag", 262 | option="", 263 | options=getListOfTags() 264 | } 265 | 266 | dialog:number { 267 | id="repetitions", 268 | label="Repetitions", 269 | number=0, 270 | decimals=0 271 | } 272 | 273 | dialog:separator { 274 | id="actions", 275 | text="Actions" 276 | } 277 | 278 | dialog:button { 279 | id="cancel", 280 | text="Cancel", 281 | onclick=function() 282 | dialog:close() 283 | end 284 | } 285 | 286 | dialog:button { 287 | id="save", 288 | text="Save", 289 | onclick=function() 290 | dialog:close() 291 | end 292 | } 293 | 294 | -- force the dialog to render the incoming data 295 | dialog:modify { 296 | id="tag", 297 | option=tag.name 298 | } 299 | 300 | dialog:modify { 301 | id="repetitions", 302 | text=tag.repetitions 303 | } 304 | 305 | return dialog 306 | end 307 | 308 | local function mainWindow(sequence, presets, exit) 309 | local dialog = Dialog("Export Gif Sequence") 310 | 311 | local function refresh(dlg) 312 | dlg:close() 313 | mainWindow(sequence, presets, exit):show{ wait=true } 314 | end 315 | 316 | local presetNames, presetMap = getPresetNameList(presets) 317 | 318 | dialog:separator { 319 | id="preset_sep", 320 | text="Presets" 321 | } 322 | 323 | dialog:combobox { 324 | id="presets", 325 | options=presetNames, 326 | option=sequence.preset, 327 | onchange=function() 328 | local isValid, tagNamesMissing = validatePresetHasNecessaryTags(presets[presetMap[dialog.data.presets]]) 329 | 330 | if (not isValid) then 331 | local message = {"You cannot use this preset since the current document does not have all of the tags needed for this preset.", "Missing tags:"} 332 | for _,name in ipairs(tagNamesMissing) do 333 | table.insert(message, name) 334 | end 335 | create_error(message, nil, 0) 336 | refresh(dialog) 337 | return 338 | end 339 | 340 | if (dialog.data.presets == "None") then 341 | sequence = { 342 | preset="None", 343 | tags={} 344 | } 345 | else 346 | sequence = deepcopy(presets[presetMap[dialog.data.presets]]) 347 | end 348 | refresh(dialog) 349 | end 350 | } 351 | 352 | dialog:button { 353 | id="delete_preset", 354 | text="Delete Selected Preset", 355 | enabled=(sequence.preset ~= "None"), 356 | onclick=function() 357 | table.remove(presets, presetMap[dialog.data.presets]) 358 | sequence = { 359 | preset="None", 360 | tags={} 361 | } 362 | refresh(dialog) 363 | end 364 | } 365 | 366 | dialog:separator { 367 | id="sequence", 368 | text="Sequence" 369 | } 370 | 371 | if (#sequence.tags == 0) then 372 | dialog:label { 373 | id="noitems", 374 | text="There are currently no tags in the sequence." 375 | } 376 | else 377 | for idx,tag in pairs(sequence.tags) do 378 | dialog:button { 379 | id="tag_"..idx, 380 | text=tag.name.." x"..tag.repetitions, 381 | onclick=function() 382 | local original = tag 383 | local edit = editTagWindow({ name=original.name, repetitions=original.repetitions }):show{ wait=true } 384 | 385 | if (edit.data.save) then 386 | local edited_tag = { name=edit.data.tag, repetitions=edit.data.repetitions } 387 | table.remove(sequence.tags, idx) 388 | table.insert(sequence.tags, idx, edited_tag) 389 | 390 | refresh(dialog) 391 | end 392 | end 393 | } 394 | 395 | dialog:button { 396 | id="tag_delete_"..idx, 397 | text="Remove Tag", 398 | onclick=function() 399 | local confirm = create_confirm("Are you sure you'd like to delete this tag?") 400 | if (confirm) then 401 | table.remove(sequence.tags, idx) 402 | refresh(dialog) 403 | end 404 | end 405 | } 406 | 407 | dialog:newrow() 408 | end 409 | end 410 | 411 | dialog:separator { 412 | id="actions", 413 | text="Actions" 414 | } 415 | 416 | dialog:button { 417 | id="add_tag", 418 | text="Add Another Tag", 419 | onclick=function() 420 | local tag_list = getListOfTags() 421 | local edit = editTagWindow({ name=tag_list[1], repetitions=1 }) 422 | edit:show{ wait=true } 423 | 424 | if (edit.data.save) then 425 | local new_tag = { name=edit.data.tag, repetitions=edit.data.repetitions } 426 | table.insert(sequence.tags, new_tag) 427 | 428 | refresh(dialog) 429 | end 430 | end 431 | } 432 | 433 | dialog:button { 434 | id="reorder_tags", 435 | text="Reorder Tags", 436 | onclick=function() 437 | local copy = deepcopy(sequence.tags) 438 | local exit_reorder = { action=nil } 439 | 440 | reorderTagsWindow(copy, exit_reorder):show{ wait=true } 441 | if (exit_reorder.action == "save") then 442 | sequence.tags = deepcopy(copy) 443 | refresh(dialog) 444 | end 445 | end 446 | } 447 | 448 | dialog:button { 449 | id="clear_all", 450 | text="Clear All Tags", 451 | onclick=function() 452 | sequence.tags = {} 453 | refresh(dialog) 454 | end 455 | } 456 | 457 | dialog:check { 458 | id="save_as", 459 | selected=false, 460 | text="Save Sequence As Preset", 461 | onclick=function() 462 | if (dialog.data.save_as) then 463 | exit.preset = true 464 | dialog:modify { 465 | id="preset_name", 466 | visible=true, 467 | focus=true 468 | } 469 | dialog:modify { 470 | id="save_preset", 471 | visible=true 472 | } 473 | else 474 | exit.preset = nil 475 | dialog:modify { 476 | id="preset_name", 477 | visible=false 478 | } 479 | dialog:modify { 480 | id="save_preset", 481 | visible=false 482 | } 483 | end 484 | end 485 | } 486 | 487 | dialog:entry { 488 | id="preset_name", 489 | text="New Preset", 490 | visible=false 491 | } 492 | 493 | dialog:button { 494 | id="save_preset", 495 | text="Save Preset", 496 | visible=false, 497 | onclick=function() 498 | local copy = deepcopy(sequence) 499 | copy.preset = dialog.data.preset_name 500 | 501 | if (presetMap[copy.preset] ~= nil) then 502 | -- just overwrite the changes, don't create a new entry 503 | presets[presetMap[copy.preset]] = copy 504 | else 505 | -- save a new entry 506 | table.insert(presets, copy) 507 | end 508 | 509 | sequence = deepcopy(copy) 510 | refresh(dialog) 511 | end 512 | } 513 | 514 | dialog:button { 515 | id="export", 516 | text="Export Sequence", 517 | onclick=function() 518 | exit.action = "export" 519 | exit.sequence = deepcopy(sequence) 520 | dialog:close() 521 | end 522 | } 523 | 524 | return dialog 525 | end 526 | 527 | ----------------------------------- 528 | -- EXECUTE MAIN LOGIC 529 | ----------------------------------- 530 | 531 | -- first check to see if any tags exist in the sprite 532 | local numTags = #getListOfTags() 533 | if (numTags == 0) then 534 | create_error({"You cannot use this option if there are no tags defined."}, nil, 0) 535 | else 536 | local sequence = { 537 | preset="None", 538 | tags={} 539 | } 540 | 541 | if (prefs.presets == nil) then 542 | prefs.presets = {} 543 | end 544 | 545 | if (prefs.last_open ~= nil) then 546 | -- validate that the sequence we are loading is valid. if it's not, continue loading a blank sequence instead 547 | local isValid, tagNamesMissing = validatePresetHasNecessaryTags(prefs.last_open) 548 | if (isValid) then 549 | sequence = deepcopy(prefs.last_open) 550 | end 551 | end 552 | 553 | local exit = { action=nil, sequence=nil } 554 | mainWindow(sequence, prefs.presets, exit):show{ wait=true } 555 | 556 | if (exit.action == "export") then 557 | -- DEVELOPMENT VERSION 558 | -- local exportChunk = app.fs.joinPath(app.fs.userConfigPath, "scripts", "export.lua") 559 | -- PRODUCTION VERSION 560 | local exportChunk = app.fs.joinPath(app.fs.userConfigPath, "extensions", "gif-sequencing", "export.lua") 561 | -- load the lua script into a Lua chunk, then execute it with the parameter plugin.preferences 562 | loadfile(exportChunk)(exit.sequence) 563 | end 564 | 565 | prefs.last_open = deepcopy(exit.sequence) 566 | end 567 | --------------------------------------------------------------------------------