├── .gitattributes ├── Frog.aseprite ├── README.md └── Stacked Sprite Visualizer.lua /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /Frog.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jontopielski/aseprite-stacked-sprite-visualizer/3ddfd2ee59fe56364d4fd2a583f17119d22876c9/Frog.aseprite -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **An Aseprite script that visualizes stacked sprites** 2 | 3 | ![StackedSpriteScareCrow](https://user-images.githubusercontent.com/6269590/236551162-79dc3ad5-bac4-4e3e-a610-6e6c43bbdf2d.gif) 4 | 5 | Take this spritesheet, containing each 'slice' of a 3-dimensional frog character:\ 6 | ![image](https://user-images.githubusercontent.com/6269590/236532036-0f9980dd-4f6c-4d27-b88d-1faab9232f91.png) 7 | 8 | Displaying the frog character as a series of individual frames:\ 9 | ![image](https://user-images.githubusercontent.com/6269590/236532290-0ea5d4a7-f30b-423f-acf2-9ca14627e14c.png) 10 | 11 | Open the Stacked Sprite Visualizer and prepare to generate the stack:\ 12 | ![image](https://user-images.githubusercontent.com/6269590/236533028-03cca2c9-e022-437e-8635-eee12f1a62ba.png) 13 | 14 | Generate the stack!\ 15 | ![SpinningFrog](https://user-images.githubusercontent.com/6269590/236532908-6c21ddb7-52a4-4d73-a23e-4b8611435498.gif) 16 | 17 | Installation Steps: 18 | 1. Make sure you're on Aseprite version 1.3+ 19 | 2. Download this repository (Click Code->Download ZIP) 20 | 3. In Aseprite, find your scripts folder via File->Scripts->Open Scripts Folder 21 | 4. Copy "Stacked Sprites Visualizer.lua" into the Aseprite Scripts folder 22 | 5. In Aseprite, reload scripts via File->Scripts->Rescan Scripts Folder 23 | 6. You should now see the Visualizer in File->Scripts 24 | 25 | Notes: 26 | - I've included an example stacked sprite frog to play around with 27 | - Try to make each frame size the same width and height (ex: 32x32 rather than 32x24) 28 | - This script hasn't been tested very much, so please keep backups of your work just in case 29 | -------------------------------------------------------------------------------- /Stacked Sprite Visualizer.lua: -------------------------------------------------------------------------------- 1 | local dlg = Dialog("Stacked Sprite Visualizer") -- dialog window 2 | local frameCount = 8 -- total number of frames in the final stack 3 | local saveName = "StackedSprite" -- generated stack file name 4 | local stackFinalFrame = 1 -- the frame we set to active after we generate 5 | local originalStartingFrame = app.activeFrame.frameNumber -- the frame the base sprite is currently on 6 | local fakeEmptyColor = app.pixelColor.rgba(255, 255, 255, 1) -- a barely noticeable "empty" pixel 7 | local emptyFrames = 0 -- the total number of empty frames we populate and then undo at the end 8 | local celCount = 0 -- the number of frames in the bsae sprite 9 | local startSprite = app.activeSprite -- a reference to the base sprite 10 | local destSprite = nil -- a reference to the destination sprite (generated stack) 11 | local lastBuiltSprite = nil -- a reference to the last base sprite we generated on 12 | local allSpriteFrames = {} -- a table that holds all the rotated animations 13 | 14 | -- resets the changing state variables 15 | function resetState() 16 | stackFinalFrame = 1 17 | emptyFrames = 0 18 | celCount = 0 19 | -- check if we're focusing on the base sprite or stack 20 | if app.activeSprite.filename == saveName and lastBuiltSprite == nil then 21 | startSprite = app.activeSprite 22 | elseif app.activeSprite.filename == saveName then 23 | startSprite = lastBuiltSprite 24 | app.activeSprite = startSprite 25 | else 26 | startSprite = app.activeSprite 27 | originalStartingFrame = app.activeFrame.frameNumber 28 | end 29 | destSprite = nil 30 | allSpriteFrames = {} 31 | end 32 | 33 | -- empty cels must be filled or they aren't indexable 34 | function replaceAllEmptyFramesWithFakePixel() 35 | for i,frame in ipairs(app.activeSprite.frames) do 36 | app.activeFrame = i 37 | if not app.activeImage then 38 | emptyFrames = emptyFrames + 1 39 | local addedCel = app.activeSprite:newCel(app.activeLayer, i) 40 | local addedImage = Image(app.activeSprite.width, app.activeSprite.height) 41 | addedImage:drawPixel(0, 0, fakeEmptyColor) 42 | addedCel.image = addedImage 43 | end 44 | end 45 | end 46 | 47 | -- celCount refers to the number of frames in the base sprite 48 | function getCelCount() 49 | local totalCels = 0 50 | for i,cel in ipairs(app.activeLayer.cels) do 51 | totalCels = totalCels + 1 52 | end 53 | return totalCels 54 | end 55 | 56 | -- if a stack file exists, modify it so it looks like the base sprite. otherwise, duplicate the base sprite 57 | function setupDestinationSprite() 58 | for i,sprite in ipairs(app.sprites) do 59 | if sprite.filename == saveName then 60 | destSprite = sprite 61 | app.activeSprite = destSprite 62 | stackFinalFrame = app.activeFrame 63 | local midpoint = Point(startSprite.width / 2, startSprite.height / 2) 64 | midpoint = Point(0, 0) 65 | app.command.CanvasSize{ ui=false, bounds=Rectangle(midpoint.x, midpoint.y, startSprite.width, startSprite.height) } 66 | 67 | local existingCelCount = 0 68 | for i,cel in ipairs(destSprite.cels) do 69 | existingCelCount = existingCelCount + 1 70 | end 71 | app.activeFrame = 1 72 | for i=1, existingCelCount - 1 do 73 | app.command.RemoveFrame() 74 | end 75 | for i=1, celCount-1 do 76 | app.command.NewFrame() 77 | end 78 | 79 | for j,startCel in ipairs(startSprite.cels) do 80 | local destCel = destSprite.cels[j] 81 | destCel.image = Image(startCel.image) 82 | destCel.position = startCel.position 83 | end 84 | app.activeFrame = 1 85 | end 86 | end 87 | 88 | if destSprite == nil then 89 | destSprite = Sprite(app.activeSprite) 90 | destSprite.filename = saveName 91 | end 92 | end 93 | 94 | -- prepare the destination sprite with empty frames for duplicated animations 95 | function addEmptyFrames() 96 | for i=1, celCount*(frameCount-1) do 97 | app.command.NewFrame() 98 | end 99 | end 100 | 101 | -- make a bunch of replicas of the base spritesheet, to be rotated later 102 | function duplicateSpritesheetToNewFrames() 103 | for i=1, celCount do 104 | local baseIndex = celCount * (frameCount-1) + i 105 | local baseCel = destSprite.cels[baseIndex] 106 | for j=1, frameCount-1 do 107 | local copyIndex = ((j - 1) * celCount) + i 108 | local copyCel = destSprite.cels[copyIndex] 109 | copyCel.image = Image(baseCel.image) 110 | copyCel.position = baseCel.position 111 | end 112 | end 113 | end 114 | 115 | -- rotate all of the duplicate sprite animations 116 | function rotateSprites() 117 | for i=1, frameCount do 118 | local angleRotation = (360/frameCount) * (i - 1) 119 | for j=1, celCount do 120 | local nextFrameIndex = ((i-1) * celCount) + j 121 | app.activeFrame = nextFrameIndex 122 | app.command.MaskAll() 123 | app.command.Rotate{target="mask", angle=angleRotation} 124 | if app.activeImage:isEmpty() then 125 | local fillerCel = app.activeSprite:newCel(app.activeLayer, nextFrameIndex) 126 | local fillerPixelImage = Image(app.activeSprite.width, app.activeSprite.height) 127 | fillerPixelImage:drawPixel(0, 0, fakeEmptyColor) 128 | fillerCel.image = fillerPixelImage 129 | end 130 | end 131 | end 132 | end 133 | 134 | -- need to make the canvas taller to accommodate for the y-offset 135 | function expandSpriteHeight() 136 | app.command.CanvasSize{ ui=false, top=celCount } 137 | end 138 | 139 | -- put every rotated animation in a table 140 | function populateSpritesheet() 141 | for i,currentCel in ipairs(destSprite.cels) do 142 | allSpriteFrames[i] = currentCel.image 143 | end 144 | end 145 | 146 | -- make a bunch of copies of the rotated animations 147 | function duplicateLayers() 148 | for i=1, celCount-1 do 149 | app.command.DuplicateLayer() 150 | end 151 | end 152 | 153 | -- move each rotated animation into its own layer 154 | function mapSpriteFramesToLayers() 155 | for currentFrame=1, frameCount do 156 | for currentLayer=1, celCount do 157 | app.activeLayer = destSprite.layers[currentLayer] 158 | app.activeFrame = currentFrame 159 | local spritesheetIndex = (currentFrame-1) * celCount + currentLayer 160 | local spritesheetImage = allSpriteFrames[spritesheetIndex] 161 | app.activeCel.image = spritesheetImage 162 | app.activeCel.position = Point(spritesheetImage.cel.position.x, spritesheetImage.cel.position.y - currentLayer) 163 | end 164 | end 165 | end 166 | 167 | -- count the number of frames in the final sprite 168 | function getTotalDestFrames() 169 | local totalDestFrames = 0 170 | for i,frame in ipairs(app.activeSprite.frames) do 171 | totalDestFrames = totalDestFrames + 1 172 | end 173 | return totalDestFrames 174 | end 175 | 176 | -- remove all frames except for the final animation 177 | function removeExcessFrames() 178 | local destFrameCount = getTotalDestFrames() 179 | app.activeFrame = frameCount + 1 180 | for i=1, destFrameCount-frameCount do 181 | app.command.RemoveFrame() 182 | end 183 | end 184 | 185 | -- undo those empty pixels we added to the base sprite 186 | function undoStartingSprite() 187 | app.activeSprite = startSprite 188 | lastBuiltSprite = startSprite 189 | for i=1, emptyFrames*2 do 190 | app.command.Undo() 191 | end 192 | app.command.Undo() -- undos flattening 193 | app.activeFrame = originalStartingFrame 194 | end 195 | 196 | -- focus on either the generated stack or back to the base sprite 197 | function setFinalSpriteFocus() 198 | if dlg.data.autoplayAnimation then 199 | app.activeSprite = destSprite 200 | app.command.PlayAnimation() 201 | end 202 | if not dlg.data.openStack then 203 | app.activeSprite = startSprite 204 | else 205 | app.activeSprite = destSprite 206 | end 207 | end 208 | 209 | -- flatten the layers, set frame durations, and set the active frame 210 | function setupDestFrames() 211 | destSprite:flatten() 212 | for i,frame in ipairs(destSprite.frames) do 213 | frame.duration = (dlg.data.frameDuration * 50) / 1000.0 214 | end 215 | if dlg.data.alwaysResetFrame then 216 | app.activeFrame = 1 217 | else 218 | app.activeFrame = stackFinalFrame 219 | end 220 | end 221 | 222 | -- generates the stacked sprite 223 | function generateStack() 224 | resetState() 225 | startSprite:flatten() 226 | replaceAllEmptyFramesWithFakePixel() 227 | celCount = getCelCount() 228 | setupDestinationSprite() 229 | addEmptyFrames() 230 | duplicateSpritesheetToNewFrames() 231 | rotateSprites() 232 | expandSpriteHeight() 233 | populateSpritesheet() 234 | duplicateLayers() 235 | mapSpriteFramesToLayers() 236 | removeExcessFrames() 237 | setupDestFrames() 238 | 239 | app.command.DeselectMask() 240 | 241 | undoStartingSprite() 242 | setFinalSpriteFocus() 243 | end 244 | 245 | -- sets the frameCount variable when changed in the dialog 246 | function updateFrameCount() 247 | frameCount = powOfTwo(dlg.data.frameCount) 248 | end 249 | 250 | -- recursive 2^x function 251 | function powOfTwo(power) 252 | if power == 0 then 253 | return 1 254 | end 255 | return 2 * powOfTwo(power - 1) 256 | end 257 | 258 | -- creates the dialog and waits for input 259 | function main() 260 | dlg:separator{ text="Parameters" } 261 | dlg:slider{ id="frameCount", label="Frame count (2^x)", min=0, max=6, value=3, onchange=updateFrameCount } 262 | dlg:slider{ id="frameDuration", label="Frame duration (*50ms)", min=1, max=8, value=4 } 263 | dlg:separator{ text="Render" } 264 | dlg:check{ id="alwaysResetFrame", label="Start at first frame", selected=false, focus=false } 265 | dlg:check{ id="autoplayAnimation", label="Autoplay animation", selected=true, focus=false } 266 | dlg:check{ id="openStack", label="Open file", selected=true, focus=false } 267 | dlg:check{ id="focusPlaceholder", label="Focus placeholder", selected=true, visible=false, focus=true } 268 | dlg:button{ text="Generate Stack", onclick=generateStack, hexpand=true, focus=false } 269 | dlg:show{ wait=false } 270 | end 271 | 272 | main() 273 | --------------------------------------------------------------------------------