├── .vscode └── settings.json ├── Automatic Snapshot.lua ├── Command Palette.lua ├── LICENSE ├── Open Time Lapse.lua ├── README.md └── Take Snapshot.lua /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Lua.diagnostics.disable": [ 3 | "lowercase-global" 4 | ], 5 | "Lua.diagnostics.globals": [ 6 | "Dialog", 7 | "app", 8 | "Image" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /Automatic Snapshot.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Record v3.x - Automatic Snapshot Controls 3 | License: MIT 4 | Website: https://sprngr.itch.io/aseprite-record 5 | Source: https://github.com/sprngr/aseprite-record 6 | ]] 7 | 8 | --[[ 9 | Record Core Library 10 | ]] 11 | 12 | error_messages = { 13 | invalid_api_version = "This script requires Aseprite v1.2.30 or newer. Please update Aseprite to continue.", 14 | no_active_sprite = "No active sprite is available.", 15 | save_required = "A sprite must be saved before you are able to run this script.", 16 | snapshot_required = "You need to take at least one snapshot to load a time lapse.", 17 | } 18 | 19 | local _auto_snap_delay = 1 20 | 21 | -- Utility functions 22 | function check_api_version() 23 | if app.apiVersion < 15 then 24 | show_error(error_messages.invalid_api_version) 25 | return false 26 | else 27 | return true 28 | end 29 | end 30 | 31 | function show_error(error_msg) 32 | local error_dialog = Dialog("Error") 33 | error_dialog 34 | :label { text = error_msg } 35 | :newrow() 36 | :button { text = "Close", onclick = function() error_dialog:close() end } 37 | error_dialog:show() 38 | end 39 | 40 | local function get_active_frame_number() 41 | local frame = app.activeFrame 42 | if frame == nil then 43 | return 1 44 | else 45 | return frame 46 | end 47 | end 48 | 49 | RecordingContext = {} 50 | 51 | function RecordingContext:get_recording_index() 52 | local path = self.index_file 53 | if not app.fs.isFile(path) then 54 | return 0 55 | end 56 | local file = io.open(path, "r") 57 | assert(file) 58 | local contents = file:read() 59 | io.close(file) 60 | return tonumber(contents) 61 | end 62 | 63 | function RecordingContext:set_recording_index(index) 64 | local path = self.index_file 65 | local file = io.open(path, "w") 66 | assert(file) 67 | file:write("" .. index) 68 | io.close(file) 69 | end 70 | 71 | function RecordingContext.new(sprite) 72 | local self = {} 73 | setmetatable(self, { __index = RecordingContext }) 74 | self.sprite_file_name = app.fs.fileTitle(sprite.filename) 75 | self.sprite_file_path = app.fs.filePath(sprite.filename) 76 | self:_init_directory() 77 | self.index_file = app.fs.joinPath(self.record_directory_path, "_index.txt") 78 | self:_promote_v2_to_v3() 79 | return self 80 | end 81 | 82 | function RecordingContext:_init_directory() 83 | local dir_name_legacy = self.sprite_file_name .. "_record" 84 | local dir_path_legacy = app.fs.joinPath(self.sprite_file_path, dir_name_legacy) 85 | if app.fs.isDirectory(dir_path_legacy) then 86 | -- For 2.x Target Directory Backwards Compatibility 87 | self._is_legacy_recording = true 88 | self.record_directory_name = dir_name_legacy 89 | else 90 | self._is_legacy_recording = false 91 | self.record_directory_name = self.sprite_file_name .. "__record" 92 | end 93 | 94 | self.record_directory_path = app.fs.joinPath(self.sprite_file_path, self.record_directory_name) 95 | end 96 | 97 | function RecordingContext:_promote_v2_to_v3() 98 | if not self._is_legacy_recording then 99 | return -- Is not v2.x 100 | end 101 | if app.fs.isFile(self.index_file) then 102 | return -- Is v2.x, but already has promoted structure 103 | end 104 | -- 2.x Add Missing Index File for Forward Compatibility 105 | local is_index_set = false 106 | local current_index = 0 107 | while not is_index_set do 108 | if not app.fs.isFile(self:get_recording_image_path(current_index)) then 109 | is_index_set = true 110 | self.context:set_recording_index(current_index) 111 | else 112 | current_index = current_index + 1 113 | end 114 | end 115 | end 116 | 117 | function RecordingContext:get_recording_image_path(index) 118 | if not app.fs.isDirectory(self.record_directory_path) then 119 | app.fs.makeDirectory(self.record_directory_path) 120 | end 121 | 122 | return app.fs.joinPath(self.record_directory_path, self.sprite_file_name .. "_" .. index .. ".png") 123 | end 124 | 125 | Snapshot = {} 126 | 127 | function Snapshot:_initialize(sprite) 128 | self.auto_snap_enabled = false 129 | self.auto_snap_delay = _auto_snap_delay 130 | self.auto_snap_increment = 0 131 | self.context = nil 132 | 133 | -- Instance of Aseprite Sprite object 134 | -- https://github.com/aseprite/api/blob/master/api/sprite.md#sprite 135 | self.sprite = nil 136 | if sprite then 137 | self.sprite = sprite 138 | self.context = RecordingContext.new(sprite) 139 | end 140 | end 141 | 142 | function Snapshot:_increment_recording_index() 143 | local index = self.context:get_recording_index(self) + 1 144 | self.context:set_recording_index(index) 145 | end 146 | 147 | function Snapshot:get_recording_image_path(index) 148 | return self.context:get_recording_image_path(index) 149 | end 150 | 151 | function Snapshot.new() 152 | local self = {} 153 | setmetatable(self, { __index = Snapshot }) 154 | self:_initialize(nil) 155 | return self 156 | end 157 | 158 | function Snapshot:is_active() 159 | if not self:is_valid() then 160 | return false 161 | end 162 | return self.auto_snap_enabled 163 | end 164 | 165 | function Snapshot:is_valid() 166 | if self.sprite then 167 | return true 168 | end 169 | return false 170 | end 171 | 172 | function Snapshot:reset() 173 | self:_initialize(nil) 174 | end 175 | 176 | function Snapshot:auto_save() 177 | if not self.auto_snap_enabled then 178 | return 179 | end 180 | if not self.sprite then 181 | return 182 | end 183 | 184 | self.auto_snap_increment = self.auto_snap_increment + 1 185 | if self.auto_snap_increment < self.auto_snap_delay then 186 | return 187 | end 188 | self.auto_snap_increment = 0 189 | self:save() 190 | end 191 | 192 | function Snapshot:_get_current_image() 193 | local image = Image(self.sprite.width, self.sprite.height, self.sprite.colorMode) 194 | image:drawSprite(self.sprite, get_active_frame_number()) 195 | return image 196 | end 197 | 198 | function Snapshot:_get_saved_image_content(index) 199 | if index < 0 then 200 | return nil 201 | end 202 | local path = self:get_recording_image_path(index) 203 | if not app.fs.isFile(path) then 204 | return nil 205 | end 206 | local file = io.open(path, "rb") 207 | assert(file) 208 | local content = file:read("a") 209 | io.close(file) 210 | return content 211 | end 212 | 213 | function Snapshot:save() 214 | local image = self:_get_current_image() 215 | local index = self.context:get_recording_index() 216 | local path = self:get_recording_image_path(index) 217 | 218 | image:saveAs{ 219 | filename = path, 220 | palette = self.sprite.palettes[1] 221 | } 222 | 223 | local image_changed = true 224 | local prev_content = self:_get_saved_image_content(index - 1) 225 | if prev_content ~= nil then 226 | local curr_content = self:_get_saved_image_content(index) 227 | assert(curr_content ~= nil) 228 | if prev_content == curr_content then 229 | image_changed = false 230 | end 231 | end 232 | 233 | if image_changed then 234 | self:_increment_recording_index() 235 | end 236 | end 237 | 238 | function Snapshot:set_sprite(sprite) 239 | if not app.fs.isFile(sprite.filename) then 240 | return show_error(error_messages.save_required) 241 | end 242 | 243 | if (not self.sprite or self.sprite ~= sprite) then 244 | self:_initialize(sprite) 245 | end 246 | end 247 | 248 | function Snapshot:update_sprite() 249 | local sprite = app.activeSprite 250 | if not sprite then 251 | return show_error(error_messages.no_active_sprite) 252 | end 253 | self:set_sprite(sprite) 254 | end 255 | 256 | --[[ 257 | End Record Core Library 258 | ]] 259 | 260 | local snapshot = Snapshot.new() 261 | 262 | local function take_auto_snapshot() 263 | snapshot:auto_save() 264 | end 265 | 266 | local function disable_auto_snapshot(dialog) 267 | snapshot.auto_snap_enabled = false 268 | if not snapshot:is_valid() then 269 | return 270 | end 271 | 272 | snapshot.sprite.events:off(take_auto_snapshot) 273 | 274 | dialog:modify { 275 | id = "status", 276 | text = "OFF" 277 | } 278 | dialog:modify { 279 | id = "toggle", 280 | text = "Start" 281 | } 282 | end 283 | 284 | local function enable_auto_snapshot(dialog) 285 | snapshot:update_sprite() 286 | 287 | snapshot.auto_snap_enabled = snapshot:is_valid() 288 | if not snapshot.auto_snap_enabled then 289 | return 290 | end 291 | 292 | snapshot.auto_snap_increment = 0 293 | snapshot.sprite.events:on("change", take_auto_snapshot) 294 | 295 | dialog:modify { 296 | id = "status", 297 | text = "RUNNING" 298 | } 299 | dialog:modify { 300 | id = "toggle", 301 | text = "Stop" 302 | } 303 | dialog:modify { 304 | id = "target", 305 | text = snapshot.context.sprite_file_name 306 | } 307 | end 308 | 309 | if check_api_version() then 310 | local main_dialog = Dialog { 311 | title = "Record - Auto Snapshot", 312 | onclose = function() 313 | snapshot:reset() 314 | end 315 | } 316 | 317 | main_dialog:label { 318 | id = "target", 319 | label = "Target:", 320 | text = "" 321 | } 322 | main_dialog:label { 323 | id = "status", 324 | label = "Status:", 325 | text = "OFF" 326 | } 327 | main_dialog:number { 328 | id = "delay", 329 | label = "Action Delay:", 330 | focus = true, 331 | text = tostring(snapshot.auto_snap_delay), 332 | onchange = function() 333 | _auto_snap_delay = main_dialog.data.delay 334 | snapshot.auto_snap_delay = _auto_snap_delay 335 | snapshot.auto_snap_increment = 0 336 | end 337 | } 338 | main_dialog:separator {} 339 | main_dialog:button { 340 | id = "toggle", 341 | text = "Start", 342 | onclick = function() 343 | if snapshot:is_active() then 344 | disable_auto_snapshot(main_dialog) 345 | else 346 | enable_auto_snapshot(main_dialog) 347 | end 348 | end 349 | } 350 | main_dialog:show { wait = false } 351 | end 352 | -------------------------------------------------------------------------------- /Command Palette.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Record v3.x - Command Palette 3 | License: MIT 4 | Website: https://sprngr.itch.io/aseprite-record 5 | Source: https://github.com/sprngr/aseprite-record 6 | ]] 7 | 8 | --[[ 9 | Record Core Library 10 | ]] 11 | 12 | error_messages = { 13 | invalid_api_version = "This script requires Aseprite v1.2.30 or newer. Please update Aseprite to continue.", 14 | no_active_sprite = "No active sprite is available.", 15 | save_required = "A sprite must be saved before you are able to run this script.", 16 | snapshot_required = "You need to take at least one snapshot to load a time lapse.", 17 | } 18 | 19 | -- Utility functions 20 | function check_api_version() 21 | if app.apiVersion < 15 then 22 | show_error(error_messages.invalid_api_version) 23 | return false 24 | else 25 | return true 26 | end 27 | end 28 | 29 | function show_error(error_msg) 30 | local error_dialog = Dialog("Error") 31 | error_dialog 32 | :label { text = error_msg } 33 | :newrow() 34 | :button { text = "Close", onclick = function() error_dialog:close() end } 35 | error_dialog:show() 36 | end 37 | 38 | local function get_active_frame_number() 39 | local frame = app.activeFrame 40 | if frame == nil then 41 | return 1 42 | else 43 | return frame 44 | end 45 | end 46 | 47 | RecordingContext = {} 48 | 49 | function RecordingContext:get_recording_index() 50 | local path = self.index_file 51 | if not app.fs.isFile(path) then 52 | return 0 53 | end 54 | local file = io.open(path, "r") 55 | assert(file) 56 | local contents = file:read() 57 | io.close(file) 58 | return tonumber(contents) 59 | end 60 | 61 | function RecordingContext:set_recording_index(index) 62 | local path = self.index_file 63 | local file = io.open(path, "w") 64 | assert(file) 65 | file:write("" .. index) 66 | io.close(file) 67 | end 68 | 69 | function RecordingContext.new(sprite) 70 | local self = {} 71 | setmetatable(self, { __index = RecordingContext }) 72 | self.sprite_file_name = app.fs.fileTitle(sprite.filename) 73 | self.sprite_file_path = app.fs.filePath(sprite.filename) 74 | self:_init_directory() 75 | self.index_file = app.fs.joinPath(self.record_directory_path, "_index.txt") 76 | self:_promote_v2_to_v3() 77 | return self 78 | end 79 | 80 | function RecordingContext:_init_directory() 81 | local dir_name_legacy = self.sprite_file_name .. "_record" 82 | local dir_path_legacy = app.fs.joinPath(self.sprite_file_path, dir_name_legacy) 83 | if app.fs.isDirectory(dir_path_legacy) then 84 | -- For 2.x Target Directory Backwards Compatibility 85 | self._is_legacy_recording = true 86 | self.record_directory_name = dir_name_legacy 87 | else 88 | self._is_legacy_recording = false 89 | self.record_directory_name = self.sprite_file_name .. "__record" 90 | end 91 | 92 | self.record_directory_path = app.fs.joinPath(self.sprite_file_path, self.record_directory_name) 93 | end 94 | 95 | function RecordingContext:_promote_v2_to_v3() 96 | if not self._is_legacy_recording then 97 | return -- Is not v2.x 98 | end 99 | if app.fs.isFile(self.index_file) then 100 | return -- Is v2.x, but already has promoted structure 101 | end 102 | -- 2.x Add Missing Index File for Forward Compatibility 103 | local is_index_set = false 104 | local current_index = 0 105 | while not is_index_set do 106 | if not app.fs.isFile(self:get_recording_image_path(current_index)) then 107 | is_index_set = true 108 | self.context:set_recording_index(current_index) 109 | else 110 | current_index = current_index + 1 111 | end 112 | end 113 | end 114 | 115 | function RecordingContext:get_recording_image_path(index) 116 | if not app.fs.isDirectory(self.record_directory_path) then 117 | app.fs.makeDirectory(self.record_directory_path) 118 | end 119 | 120 | return app.fs.joinPath(self.record_directory_path, self.sprite_file_name .. "_" .. index .. ".png") 121 | end 122 | 123 | Snapshot = {} 124 | 125 | function Snapshot:_initialize(sprite) 126 | self.auto_snap_enabled = false 127 | self.auto_snap_delay = 1 128 | self.auto_snap_increment = 0 129 | self.context = nil 130 | 131 | -- Instance of Aseprite Sprite object 132 | -- https://github.com/aseprite/api/blob/master/api/sprite.md#sprite 133 | self.sprite = nil 134 | if sprite then 135 | self.sprite = sprite 136 | self.context = RecordingContext.new(sprite) 137 | end 138 | end 139 | 140 | function Snapshot:_increment_recording_index() 141 | local index = self.context:get_recording_index(self) + 1 142 | self.context:set_recording_index(index) 143 | end 144 | 145 | function Snapshot:get_recording_image_path(index) 146 | return self.context:get_recording_image_path(index) 147 | end 148 | 149 | function Snapshot.new() 150 | local self = {} 151 | setmetatable(self, { __index = Snapshot }) 152 | self:_initialize(nil) 153 | return self 154 | end 155 | 156 | function Snapshot:is_active() 157 | if not self:is_valid() then 158 | return false 159 | end 160 | return self.auto_snap_enabled 161 | end 162 | 163 | function Snapshot:is_valid() 164 | if self.sprite then 165 | return true 166 | end 167 | return false 168 | end 169 | 170 | function Snapshot:reset() 171 | self:_initialize(nil) 172 | end 173 | 174 | function Snapshot:auto_save() 175 | if not self.auto_snap_enabled then 176 | return 177 | end 178 | if not self.sprite then 179 | return 180 | end 181 | 182 | self.auto_snap_increment = self.auto_snap_increment + 1 183 | if self.auto_snap_increment < self.auto_snap_delay then 184 | return 185 | end 186 | self.auto_snap_increment = 0 187 | self:save() 188 | end 189 | 190 | function Snapshot:_get_current_image() 191 | local image = Image(self.sprite.width, self.sprite.height, self.sprite.colorMode) 192 | image:drawSprite(self.sprite, get_active_frame_number()) 193 | return image 194 | end 195 | 196 | function Snapshot:_get_saved_image_content(index) 197 | if index < 0 then 198 | return nil 199 | end 200 | local path = self:get_recording_image_path(index) 201 | if not app.fs.isFile(path) then 202 | return nil 203 | end 204 | local file = io.open(path, "rb") 205 | assert(file) 206 | local content = file:read("a") 207 | io.close(file) 208 | return content 209 | end 210 | 211 | function Snapshot:save() 212 | local image = self:_get_current_image() 213 | local index = self.context:get_recording_index() 214 | local path = self:get_recording_image_path(index) 215 | image:saveAs{ 216 | filename = path, 217 | palette = self.sprite.palettes[1] 218 | } 219 | 220 | local image_changed = true 221 | local prev_content = self:_get_saved_image_content(index - 1) 222 | if prev_content ~= nil then 223 | local curr_content = self:_get_saved_image_content(index) 224 | assert(curr_content ~= nil) 225 | if prev_content == curr_content then 226 | image_changed = false 227 | end 228 | end 229 | 230 | if image_changed then 231 | self:_increment_recording_index() 232 | end 233 | end 234 | 235 | function Snapshot:set_sprite(sprite) 236 | if not app.fs.isFile(sprite.filename) then 237 | return show_error(error_messages.save_required) 238 | end 239 | 240 | if (not self.sprite or self.sprite ~= sprite) then 241 | self:_initialize(sprite) 242 | end 243 | end 244 | 245 | function Snapshot:update_sprite() 246 | local sprite = app.activeSprite 247 | if not sprite then 248 | return show_error(error_messages.no_active_sprite) 249 | end 250 | self:set_sprite(sprite) 251 | end 252 | 253 | --[[ 254 | End Record Core Library 255 | ]] 256 | 257 | local snapshot = Snapshot.new() 258 | 259 | local function take_snapshot() 260 | snapshot:update_sprite() 261 | if not snapshot:is_valid() then 262 | return 263 | end 264 | snapshot:save() 265 | end 266 | 267 | local function open_time_lapse() 268 | snapshot:update_sprite() 269 | if not snapshot:is_valid() then 270 | return 271 | end 272 | 273 | local path = snapshot:get_recording_image_path(0) 274 | if app.fs.isFile(path) then 275 | app.command.OpenFile { filename = path } 276 | else 277 | show_error(error_messages.snapshot_required) 278 | end 279 | end 280 | 281 | if check_api_version() then 282 | local main_dialog = Dialog { 283 | title = "Record - Command Palette" 284 | } 285 | 286 | -- Creates the main dialog box 287 | main_dialog:button { 288 | text = "Take Snapshot", 289 | onclick = function() 290 | take_snapshot() 291 | end 292 | } 293 | main_dialog:button { 294 | text = "Open Time Lapse", 295 | onclick = function() 296 | open_time_lapse() 297 | end 298 | } 299 | main_dialog:show { wait = false } 300 | end 301 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2020 Michael Springer @sprngr 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /Open Time Lapse.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Record v3.x - Open Time Lapse 3 | License: MIT 4 | Website: https://sprngr.itch.io/aseprite-record 5 | Source: https://github.com/sprngr/aseprite-record 6 | ]] 7 | 8 | --[[ 9 | Record Core Library 10 | ]] 11 | 12 | error_messages = { 13 | invalid_api_version = "This script requires Aseprite v1.2.30 or newer. Please update Aseprite to continue.", 14 | no_active_sprite = "No active sprite is available.", 15 | save_required = "A sprite must be saved before you are able to run this script.", 16 | snapshot_required = "You need to take at least one snapshot to load a time lapse.", 17 | } 18 | 19 | -- Utility functions 20 | function check_api_version() 21 | if app.apiVersion < 15 then 22 | show_error(error_messages.invalid_api_version) 23 | return false 24 | else 25 | return true 26 | end 27 | end 28 | 29 | function show_error(error_msg) 30 | local error_dialog = Dialog("Error") 31 | error_dialog 32 | :label { text = error_msg } 33 | :newrow() 34 | :button { text = "Close", onclick = function() error_dialog:close() end } 35 | error_dialog:show() 36 | end 37 | 38 | local function get_active_frame_number() 39 | local frame = app.activeFrame 40 | if frame == nil then 41 | return 1 42 | else 43 | return frame 44 | end 45 | end 46 | 47 | RecordingContext = {} 48 | 49 | function RecordingContext:get_recording_index() 50 | local path = self.index_file 51 | if not app.fs.isFile(path) then 52 | return 0 53 | end 54 | local file = io.open(path, "r") 55 | assert(file) 56 | local contents = file:read() 57 | io.close(file) 58 | return tonumber(contents) 59 | end 60 | 61 | function RecordingContext:set_recording_index(index) 62 | local path = self.index_file 63 | local file = io.open(path, "w") 64 | assert(file) 65 | file:write("" .. index) 66 | io.close(file) 67 | end 68 | 69 | function RecordingContext.new(sprite) 70 | local self = {} 71 | setmetatable(self, { __index = RecordingContext }) 72 | self.sprite_file_name = app.fs.fileTitle(sprite.filename) 73 | self.sprite_file_path = app.fs.filePath(sprite.filename) 74 | self:_init_directory() 75 | self.index_file = app.fs.joinPath(self.record_directory_path, "_index.txt") 76 | self:_promote_v2_to_v3() 77 | return self 78 | end 79 | 80 | function RecordingContext:_init_directory() 81 | local dir_name_legacy = self.sprite_file_name .. "_record" 82 | local dir_path_legacy = app.fs.joinPath(self.sprite_file_path, dir_name_legacy) 83 | if app.fs.isDirectory(dir_path_legacy) then 84 | -- For 2.x Target Directory Backwards Compatibility 85 | self._is_legacy_recording = true 86 | self.record_directory_name = dir_name_legacy 87 | else 88 | self._is_legacy_recording = false 89 | self.record_directory_name = self.sprite_file_name .. "__record" 90 | end 91 | 92 | self.record_directory_path = app.fs.joinPath(self.sprite_file_path, self.record_directory_name) 93 | end 94 | 95 | function RecordingContext:_promote_v2_to_v3() 96 | if not self._is_legacy_recording then 97 | return -- Is not v2.x 98 | end 99 | if app.fs.isFile(self.index_file) then 100 | return -- Is v2.x, but already has promoted structure 101 | end 102 | -- 2.x Add Missing Index File for Forward Compatibility 103 | local is_index_set = false 104 | local current_index = 0 105 | while not is_index_set do 106 | if not app.fs.isFile(self:get_recording_image_path(current_index)) then 107 | is_index_set = true 108 | self.context:set_recording_index(current_index) 109 | else 110 | current_index = current_index + 1 111 | end 112 | end 113 | end 114 | 115 | function RecordingContext:get_recording_image_path(index) 116 | if not app.fs.isDirectory(self.record_directory_path) then 117 | app.fs.makeDirectory(self.record_directory_path) 118 | end 119 | 120 | return app.fs.joinPath(self.record_directory_path, self.sprite_file_name .. "_" .. index .. ".png") 121 | end 122 | 123 | Snapshot = {} 124 | 125 | function Snapshot:_initialize(sprite) 126 | self.auto_snap_enabled = false 127 | self.auto_snap_delay = 1 128 | self.auto_snap_increment = 0 129 | self.context = nil 130 | 131 | -- Instance of Aseprite Sprite object 132 | -- https://github.com/aseprite/api/blob/master/api/sprite.md#sprite 133 | self.sprite = nil 134 | if sprite then 135 | self.sprite = sprite 136 | self.context = RecordingContext.new(sprite) 137 | end 138 | end 139 | 140 | function Snapshot:_increment_recording_index() 141 | local index = self.context:get_recording_index(self) + 1 142 | self.context:set_recording_index(index) 143 | end 144 | 145 | function Snapshot:get_recording_image_path(index) 146 | return self.context:get_recording_image_path(index) 147 | end 148 | 149 | function Snapshot.new() 150 | local self = {} 151 | setmetatable(self, { __index = Snapshot }) 152 | self:_initialize(nil) 153 | return self 154 | end 155 | 156 | function Snapshot:is_active() 157 | if not self:is_valid() then 158 | return false 159 | end 160 | return self.auto_snap_enabled 161 | end 162 | 163 | function Snapshot:is_valid() 164 | if self.sprite then 165 | return true 166 | end 167 | return false 168 | end 169 | 170 | function Snapshot:reset() 171 | self:_initialize(nil) 172 | end 173 | 174 | function Snapshot:auto_save() 175 | if not self.auto_snap_enabled then 176 | return 177 | end 178 | if not self.sprite then 179 | return 180 | end 181 | 182 | self.auto_snap_increment = self.auto_snap_increment + 1 183 | if self.auto_snap_increment < self.auto_snap_delay then 184 | return 185 | end 186 | self.auto_snap_increment = 0 187 | self:save() 188 | end 189 | 190 | function Snapshot:_get_current_image() 191 | local image = Image(self.sprite.width, self.sprite.height, self.sprite.colorMode) 192 | image:drawSprite(self.sprite, get_active_frame_number()) 193 | return image 194 | end 195 | 196 | function Snapshot:_get_saved_image_content(index) 197 | if index < 0 then 198 | return nil 199 | end 200 | local path = self:get_recording_image_path(index) 201 | if not app.fs.isFile(path) then 202 | return nil 203 | end 204 | local file = io.open(path, "rb") 205 | assert(file) 206 | local content = file:read("a") 207 | io.close(file) 208 | return content 209 | end 210 | 211 | function Snapshot:save() 212 | local image = self:_get_current_image() 213 | local index = self.context:get_recording_index() 214 | local path = self:get_recording_image_path(index) 215 | image:saveAs{ 216 | filename = path, 217 | palette = self.sprite.palettes[1] 218 | } 219 | 220 | local image_changed = true 221 | local prev_content = self:_get_saved_image_content(index - 1) 222 | if prev_content ~= nil then 223 | local curr_content = self:_get_saved_image_content(index) 224 | assert(curr_content ~= nil) 225 | if prev_content == curr_content then 226 | image_changed = false 227 | end 228 | end 229 | 230 | if image_changed then 231 | self:_increment_recording_index() 232 | end 233 | end 234 | 235 | function Snapshot:set_sprite(sprite) 236 | if not app.fs.isFile(sprite.filename) then 237 | return show_error(error_messages.save_required) 238 | end 239 | 240 | if (not self.sprite or self.sprite ~= sprite) then 241 | self:_initialize(sprite) 242 | end 243 | end 244 | 245 | function Snapshot:update_sprite() 246 | local sprite = app.activeSprite 247 | if not sprite then 248 | return show_error(error_messages.no_active_sprite) 249 | end 250 | self:set_sprite(sprite) 251 | end 252 | 253 | --[[ 254 | End Record Core Library 255 | ]] 256 | 257 | if check_api_version() then 258 | local sprite = app.activeSprite 259 | if sprite and app.fs.isFile(sprite.filename) then 260 | local context = RecordingContext.new(sprite) 261 | local path = context:get_recording_image_path(0) 262 | if app.fs.isFile(path) then 263 | app.command.OpenFile { filename = path } 264 | else 265 | return show_error(error_messages.snapshot_required) 266 | end 267 | else 268 | return show_error(error_messages.save_required) 269 | end 270 | end 271 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Record for Aseprite 2 | 3 | An Aseprite utility script for recording snapshots in app to build time lapses. 4 | 5 | ## How to Install 6 | 7 | It is recommended to go to the itch page for the tool to get the latest stable release: https://sprngr.itch.io/aseprite-record 8 | 9 | If you would like to live on the edge and pull down the source code, you can clone this repo and copy that directory into your Aseprite scripts directory. 10 | 11 | ## Scripts 12 | 13 | (I don't like that the script files have a space in their names, but it makes it look so much better in the Aseprite menus.) 14 | 15 | ### Automatic Snapshot 16 | 17 | This option will open up a dialog box that provides functionality take snapshots on an interval. 18 | 19 | It requires there to be an active & saved sprite in order to run. The interval at which saves happen is based on [sprite change events](https://github.com/aseprite/api/blob/main/api/sprite.md#spriteevents) - when changes are made to the sprite, including undo/redo actions. 20 | 21 | The interval is configurable in the dialog, lower numbers means more frequent snapshots. If you change the active sprite in the app, automatic snapshots will keep a cached reference to the target sprite until you target a new one with the dialog. Usage of the Command Palette and Take Snapshot command can be used in parallel while this is running. 22 | 23 | ### Command Palette 24 | 25 | This option will open up a dialog box to leave up in your editor, giving you access to the functionality to take a snapshot & open the time lapse for the current sprite if any snapshots are saved for it. 26 | 27 | The functions of each button are described in detail below and are available as single actions that can be mapped to a keyboard shortcut. 28 | 29 | ### Take Snapshot 30 | 31 | This option saves a flattened png copy of the visible layers of the current sprite. It is saved to a sibling folder named `__record`. Each file will be saved with an incrementing count appended to the end of it. No modifications to your work are performed by this script, it only creates new files. 32 | 33 | ### Open Time Lapse 34 | 35 | This will open the Aseprite dialog asking if you wish to load all sequenced files related as a gif. If you accept, it will load it as a cool time lapse of all your snapshots saved for the current sprite. 36 | 37 | ## Contributing 38 | 39 | If any contributions are made, please be sure the code added/modified aligns with the [LuaRocks Lua Style Guide](https://github.com/luarocks/lua-style-guide). -------------------------------------------------------------------------------- /Take Snapshot.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Record v3.x - Take Snapshot 3 | License: MIT 4 | Website: https://sprngr.itch.io/aseprite-record 5 | Source: https://github.com/sprngr/aseprite-record 6 | ]] 7 | 8 | --[[ 9 | Record Core Library 10 | ]] 11 | 12 | error_messages = { 13 | invalid_api_version = "This script requires Aseprite v1.2.30 or newer. Please update Aseprite to continue.", 14 | no_active_sprite = "No active sprite is available.", 15 | save_required = "A sprite must be saved before you are able to run this script.", 16 | snapshot_required = "You need to take at least one snapshot to load a time lapse.", 17 | } 18 | 19 | -- Utility functions 20 | function check_api_version() 21 | if app.apiVersion < 15 then 22 | show_error(error_messages.invalid_api_version) 23 | return false 24 | else 25 | return true 26 | end 27 | end 28 | 29 | function show_error(error_msg) 30 | local error_dialog = Dialog("Error") 31 | error_dialog 32 | :label { text = error_msg } 33 | :newrow() 34 | :button { text = "Close", onclick = function() error_dialog:close() end } 35 | error_dialog:show() 36 | end 37 | 38 | local function get_active_frame_number() 39 | local frame = app.activeFrame 40 | if frame == nil then 41 | return 1 42 | else 43 | return frame 44 | end 45 | end 46 | 47 | RecordingContext = {} 48 | 49 | function RecordingContext:get_recording_index() 50 | local path = self.index_file 51 | if not app.fs.isFile(path) then 52 | return 0 53 | end 54 | local file = io.open(path, "r") 55 | assert(file) 56 | local contents = file:read() 57 | io.close(file) 58 | return tonumber(contents) 59 | end 60 | 61 | function RecordingContext:set_recording_index(index) 62 | local path = self.index_file 63 | local file = io.open(path, "w") 64 | assert(file) 65 | file:write("" .. index) 66 | io.close(file) 67 | end 68 | 69 | function RecordingContext.new(sprite) 70 | local self = {} 71 | setmetatable(self, { __index = RecordingContext }) 72 | self.sprite_file_name = app.fs.fileTitle(sprite.filename) 73 | self.sprite_file_path = app.fs.filePath(sprite.filename) 74 | self:_init_directory() 75 | self.index_file = app.fs.joinPath(self.record_directory_path, "_index.txt") 76 | self:_promote_v2_to_v3() 77 | return self 78 | end 79 | 80 | function RecordingContext:_init_directory() 81 | local dir_name_legacy = self.sprite_file_name .. "_record" 82 | local dir_path_legacy = app.fs.joinPath(self.sprite_file_path, dir_name_legacy) 83 | if app.fs.isDirectory(dir_path_legacy) then 84 | -- For 2.x Target Directory Backwards Compatibility 85 | self._is_legacy_recording = true 86 | self.record_directory_name = dir_name_legacy 87 | else 88 | self._is_legacy_recording = false 89 | self.record_directory_name = self.sprite_file_name .. "__record" 90 | end 91 | 92 | self.record_directory_path = app.fs.joinPath(self.sprite_file_path, self.record_directory_name) 93 | end 94 | 95 | function RecordingContext:_promote_v2_to_v3() 96 | if not self._is_legacy_recording then 97 | return -- Is not v2.x 98 | end 99 | if app.fs.isFile(self.index_file) then 100 | return -- Is v2.x, but already has promoted structure 101 | end 102 | -- 2.x Add Missing Index File for Forward Compatibility 103 | local is_index_set = false 104 | local current_index = 0 105 | while not is_index_set do 106 | if not app.fs.isFile(self:get_recording_image_path(current_index)) then 107 | is_index_set = true 108 | self.context:set_recording_index(current_index) 109 | else 110 | current_index = current_index + 1 111 | end 112 | end 113 | end 114 | 115 | function RecordingContext:get_recording_image_path(index) 116 | if not app.fs.isDirectory(self.record_directory_path) then 117 | app.fs.makeDirectory(self.record_directory_path) 118 | end 119 | 120 | return app.fs.joinPath(self.record_directory_path, self.sprite_file_name .. "_" .. index .. ".png") 121 | end 122 | 123 | Snapshot = {} 124 | 125 | function Snapshot:_initialize(sprite) 126 | self.auto_snap_enabled = false 127 | self.auto_snap_delay = 1 128 | self.auto_snap_increment = 0 129 | self.context = nil 130 | 131 | -- Instance of Aseprite Sprite object 132 | -- https://github.com/aseprite/api/blob/master/api/sprite.md#sprite 133 | self.sprite = nil 134 | if sprite then 135 | self.sprite = sprite 136 | self.context = RecordingContext.new(sprite) 137 | end 138 | end 139 | 140 | function Snapshot:_increment_recording_index() 141 | local index = self.context:get_recording_index(self) + 1 142 | self.context:set_recording_index(index) 143 | end 144 | 145 | function Snapshot:get_recording_image_path(index) 146 | return self.context:get_recording_image_path(index) 147 | end 148 | 149 | function Snapshot.new() 150 | local self = {} 151 | setmetatable(self, { __index = Snapshot }) 152 | self:_initialize(nil) 153 | return self 154 | end 155 | 156 | function Snapshot:is_active() 157 | if not self:is_valid() then 158 | return false 159 | end 160 | return self.auto_snap_enabled 161 | end 162 | 163 | function Snapshot:is_valid() 164 | if self.sprite then 165 | return true 166 | end 167 | return false 168 | end 169 | 170 | function Snapshot:reset() 171 | self:_initialize(nil) 172 | end 173 | 174 | function Snapshot:auto_save() 175 | if not self.auto_snap_enabled then 176 | return 177 | end 178 | if not self.sprite then 179 | return 180 | end 181 | 182 | self.auto_snap_increment = self.auto_snap_increment + 1 183 | if self.auto_snap_increment < self.auto_snap_delay then 184 | return 185 | end 186 | self.auto_snap_increment = 0 187 | self:save() 188 | end 189 | 190 | function Snapshot:_get_current_image() 191 | local image = Image(self.sprite.width, self.sprite.height, self.sprite.colorMode) 192 | image:drawSprite(self.sprite, get_active_frame_number()) 193 | return image 194 | end 195 | 196 | function Snapshot:_get_saved_image_content(index) 197 | if index < 0 then 198 | return nil 199 | end 200 | local path = self:get_recording_image_path(index) 201 | if not app.fs.isFile(path) then 202 | return nil 203 | end 204 | local file = io.open(path, "rb") 205 | assert(file) 206 | local content = file:read("a") 207 | io.close(file) 208 | return content 209 | end 210 | 211 | function Snapshot:save() 212 | local image = self:_get_current_image() 213 | local index = self.context:get_recording_index() 214 | local path = self:get_recording_image_path(index) 215 | image:saveAs{ 216 | filename = path, 217 | palette = self.sprite.palettes[1] 218 | } 219 | 220 | local image_changed = true 221 | local prev_content = self:_get_saved_image_content(index - 1) 222 | if prev_content ~= nil then 223 | local curr_content = self:_get_saved_image_content(index) 224 | assert(curr_content ~= nil) 225 | if prev_content == curr_content then 226 | image_changed = false 227 | end 228 | end 229 | 230 | if image_changed then 231 | self:_increment_recording_index() 232 | end 233 | end 234 | 235 | function Snapshot:set_sprite(sprite) 236 | if not app.fs.isFile(sprite.filename) then 237 | return show_error(error_messages.save_required) 238 | end 239 | 240 | if (not self.sprite or self.sprite ~= sprite) then 241 | self:_initialize(sprite) 242 | end 243 | end 244 | 245 | function Snapshot:update_sprite() 246 | local sprite = app.activeSprite 247 | if not sprite then 248 | return show_error(error_messages.no_active_sprite) 249 | end 250 | self:set_sprite(sprite) 251 | end 252 | 253 | --[[ 254 | End Record Core Library 255 | ]] 256 | 257 | if check_api_version() then 258 | local sprite = app.activeSprite 259 | if sprite and app.fs.isFile(sprite.filename) then 260 | local snapshot = Snapshot.new() 261 | snapshot:set_sprite(sprite) 262 | snapshot:save() 263 | else 264 | return show_error(error_messages.save_required) 265 | end 266 | end 267 | --------------------------------------------------------------------------------