├── LICENSE ├── README.md ├── atlas.png ├── main.lua ├── packer.lua ├── serial.lua └── test_images ├── pipo-nekonin001.png ├── pipo-nekonin002.png ├── pipo-nekonin003.png ├── pipo-nekonin004.png ├── pipo-nekonin005.png ├── pipo-nekonin006.png ├── pipo-nekonin007.png ├── pipo-nekonin008.png ├── pipo-nekonin009.png ├── pipo-nekonin010.png ├── pipo-nekonin011.png ├── pipo-nekonin012.png ├── pipo-nekonin013.png ├── pipo-nekonin014.png ├── pipo-nekonin015.png ├── pipo-nekonin016.png ├── pipo-nekonin017.png ├── pipo-nekonin018.png ├── pipo-nekonin019.png └── pipo-nekonin020.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Pedro Lucas 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 | ## explanation and methods 2 | - packer have a method called sort that can be overrided, but i recommend to keep as default (max side) 3 | - after genAtlas exec, packer.atlas will be a canvas 4 | - save methods use lua io.open as default, if are necessary to change it, you can replace these functions 5 | 6 | | function | param | return | 7 | |--|--|--| 8 | | add | image: **Image**, tag: **string** (optional) | 9 | | genAtlas | | | 10 | | getRect | tag: **string** | x: **int**, y: **int**, w: **int**, h: **int** | 11 | | getQuad | tag: **string** | quad: **Quad** | 12 | | serialize | | serial: **string** | 13 | | saveAtlas | path: **string** | error: **error** | 14 | | saveSerial | path: **string** | error: **error** | 15 | 16 | ### code example 17 | ```lua 18 | local packer = require "packer" 19 | 20 | function love.load() 21 | local files = love.filesystem.getDirectoryItems("test_images/") 22 | 23 | for _, file in ipairs(files) do 24 | local fileName = "test_images/" .. file 25 | 26 | packer:add(love.graphics.newImage(fileName), fileName) 27 | end 28 | 29 | packer:genAtlas() 30 | packer:saveAtlas("atlas.png") 31 | packer:saveSerial("serial.lua") 32 | end 33 | 34 | function love.draw() 35 | love.graphics.draw(packer.atlas, 0, 0) 36 | end 37 | ``` 38 | 39 | ### credits 40 | - [test images](https://pipoya.itch.io/pipoya-free-rpg-character-sprites-nekonin) 41 | - [article about binary packing](https://codeincomplete.com/articles/bin-packing/) 42 | - [cripboy](https://github.com/cripboy) 43 | -------------------------------------------------------------------------------- /atlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/atlas.png -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | local packer = require "packer" 2 | 3 | function love.load() 4 | local files = love.filesystem.getDirectoryItems("test_images/") 5 | 6 | for _, file in ipairs(files) do 7 | local fileName = "test_images/" .. file 8 | 9 | packer:add(love.graphics.newImage(fileName), fileName) 10 | end 11 | 12 | packer:genAtlas() 13 | packer:saveAtlas("atlas.png") 14 | packer:saveSerial("serial.lua") 15 | end 16 | 17 | function love.draw() 18 | love.graphics.draw(packer.atlas, 0, 0) 19 | end -------------------------------------------------------------------------------- /packer.lua: -------------------------------------------------------------------------------- 1 | -- created by: github.com/cripboy 2 | -- references: https://codeincomplete.com/articles/bin-packing/ 3 | local packer = {} 4 | packer.blocks = {} 5 | packer.dictionary = {} 6 | 7 | function packer.sort(a, b) 8 | return math.min(a.w, a.h) > math.min(b.w, b.h) 9 | end 10 | 11 | function packer:add(image, tag) 12 | assert(image:type() == "Image", "Packer only acepts images") 13 | table.insert(self.blocks, { 14 | image = image, 15 | x = 0, 16 | y = 0, 17 | w = image:getWidth(), 18 | h = image:getHeight() 19 | }) 20 | 21 | -- a tag is necessary to recovery this image 22 | -- position after joined into the atlas 23 | if tag then 24 | self.dictionary[tag] = self.blocks[#self.blocks] 25 | end 26 | end 27 | 28 | local function newNode(x, y, w, h) 29 | return {x = x, y = y, w = w, h = h, used = false, right = nil, down = nil} 30 | end 31 | 32 | local function findNode(node, w, h) 33 | -- node filled 34 | if node.used then 35 | return findNode(node.right, w, h) or findNode(node.down, w, h) 36 | 37 | -- you can place it here 38 | elseif (w <= node.w) and (h <= node.h) then 39 | return node 40 | 41 | -- no space to this shape 42 | else 43 | return nil 44 | end 45 | end 46 | 47 | -- fill this node and generated two more 48 | -- OBS: down and right can be w = 0 and h = 0 49 | local function splitNode(node, w, h) 50 | node.used = true 51 | node.right = newNode(node.x + w, node.y, node.w - w, h) 52 | node.down = newNode(node.x, node.y + h, node.w, node.h - h) 53 | end 54 | 55 | -- wrap the old node in a new one 56 | local function growRight(node, w, h) 57 | local growedNode = newNode(node.x, node.y, node.w + w, node.h) 58 | growedNode.used = true 59 | growedNode.down = node 60 | growedNode.right = newNode(node.w, node.y, w, node.h) 61 | 62 | return growedNode 63 | end 64 | 65 | -- wrap the old node in a new one 66 | local function growDown(node, w, h) 67 | local growedNode = newNode(node.x, node.y, node.w, node.h + h) 68 | growedNode.used = true 69 | growedNode.down = newNode(node.x, node.h, node.w, h) 70 | growedNode.right = node 71 | 72 | return growedNode 73 | end 74 | 75 | -- choose wich direction are better to growup 76 | local function growNode(node, w, h) 77 | local canGrowRight = h <= node.h 78 | local canGrowDown = w <= node.w 79 | local shouldGrowRight = canGrowRight and node.h >= node.w + w 80 | local shouldGrowDown = canGrowDown and node.w >= node.h + h 81 | 82 | if shouldGrowRight then 83 | return growRight(node, w, h) 84 | elseif shouldGrowDown then 85 | return growDown(node, w, h) 86 | elseif canGrowRight then 87 | return growRight(node, w, h) 88 | elseif canGrowDown then 89 | return growDown(node, w, h) 90 | end 91 | end 92 | 93 | function packer:genAtlas() 94 | assert(#self.blocks > 0, "You can't generate an atlas without images") 95 | 96 | -- sorting (standard sort) 97 | table.sort(self.blocks, self.sort) 98 | 99 | -- initial size 100 | local initialNode = newNode(0, 0, self.blocks[1].w, self.blocks[1].h) 101 | 102 | -- put blocks into the initialNode and future nodes 103 | for _, block in ipairs(self.blocks) do 104 | -- try to find nearest not used node 105 | local node = findNode(initialNode, block.w, block.h) 106 | 107 | -- need to growup 108 | if not node then 109 | initialNode = growNode(initialNode, block.w, block.h) 110 | node = findNode(initialNode, block.w, block.h) 111 | end 112 | 113 | splitNode(node, block.w, block.h) 114 | block.x = node.x 115 | block.y = node.y 116 | end 117 | 118 | -- make the atlas 119 | self.atlas = love.graphics.newCanvas(initialNode.w, initialNode.h) 120 | love.graphics.setCanvas(self.atlas) 121 | love.graphics.setColor(1, 1, 1) 122 | 123 | for _, block in ipairs(self.blocks) do 124 | love.graphics.draw(block.image, block.x, block.y) 125 | end 126 | 127 | love.graphics.setCanvas() 128 | end 129 | 130 | function packer:getBlock(tag) 131 | assert(self.dictionary[tag], "Invalid tag " .. tag) 132 | return self.dictionary[tag] 133 | end 134 | 135 | function packer:getRect(tag) 136 | local block = self:getBlock(tag) 137 | return block.x, block.y, block.w, block.h 138 | end 139 | 140 | -- we don't need generate all quads at same time 141 | function packer:getQuad(tag) 142 | local block = self:getBlock(tag) 143 | 144 | if not block.quad then 145 | block.quad = love.graphics.newQuad( 146 | block.x, 147 | block.y, 148 | block.w, 149 | block.h, 150 | self.atlas) 151 | end 152 | 153 | return block.quad 154 | end 155 | 156 | -- dirty serialization, but works ~fast~ fine ¯\_(ツ)_/¯ 157 | function packer:serialize() 158 | assert(self.atlas, "Generate an atlas before the serial") 159 | local content = "return {\n" 160 | 161 | for key, block in pairs(self.dictionary) do 162 | content = content .. ' ["' .. key .. '"]' .. " = {" 163 | content = content .. "x = " .. block.x .. "," 164 | content = content .. " y = " .. block.y .. "," 165 | content = content .. " w = " .. block.w .. "," 166 | content = content .. " h = " .. block.h .. "},\n" 167 | end 168 | 169 | return content .. "}" 170 | end 171 | 172 | function packer:saveAtlas(dir) 173 | local imageFile, err = io.open(dir, "wb") 174 | 175 | if imageFile then 176 | local imgData = self.atlas:newImageData() 177 | local fileData = imgData:encode("png") 178 | 179 | imageFile:write(fileData:getString()) 180 | imageFile:close() 181 | end 182 | 183 | return err 184 | end 185 | 186 | function packer:saveSerial(dir) 187 | local serialFile, err = io.open(dir, "w") 188 | 189 | if serialFile then 190 | serialFile:write(self:serialize()) 191 | serialFile:close() 192 | end 193 | 194 | return err 195 | end 196 | 197 | return packer 198 | -------------------------------------------------------------------------------- /serial.lua: -------------------------------------------------------------------------------- 1 | return { 2 | ["test_images/pipo-nekonin016.png"] = {x = 288, y = 128, w = 96, h = 128}, 3 | ["test_images/pipo-nekonin020.png"] = {x = 96, y = 384, w = 96, h = 128}, 4 | ["test_images/pipo-nekonin019.png"] = {x = 96, y = 256, w = 96, h = 128}, 5 | ["test_images/pipo-nekonin018.png"] = {x = 0, y = 256, w = 96, h = 128}, 6 | ["test_images/pipo-nekonin017.png"] = {x = 288, y = 0, w = 96, h = 128}, 7 | ["test_images/pipo-nekonin007.png"] = {x = 0, y = 384, w = 96, h = 128}, 8 | ["test_images/pipo-nekonin004.png"] = {x = 480, y = 0, w = 96, h = 128}, 9 | ["test_images/pipo-nekonin006.png"] = {x = 480, y = 128, w = 96, h = 128}, 10 | ["test_images/pipo-nekonin005.png"] = {x = 480, y = 256, w = 96, h = 128}, 11 | ["test_images/pipo-nekonin002.png"] = {x = 384, y = 128, w = 96, h = 128}, 12 | ["test_images/pipo-nekonin008.png"] = {x = 288, y = 256, w = 96, h = 128}, 13 | ["test_images/pipo-nekonin001.png"] = {x = 0, y = 0, w = 96, h = 128}, 14 | ["test_images/pipo-nekonin003.png"] = {x = 384, y = 0, w = 96, h = 128}, 15 | ["test_images/pipo-nekonin009.png"] = {x = 384, y = 256, w = 96, h = 128}, 16 | ["test_images/pipo-nekonin010.png"] = {x = 192, y = 256, w = 96, h = 128}, 17 | ["test_images/pipo-nekonin011.png"] = {x = 96, y = 128, w = 96, h = 128}, 18 | ["test_images/pipo-nekonin012.png"] = {x = 0, y = 128, w = 96, h = 128}, 19 | ["test_images/pipo-nekonin013.png"] = {x = 192, y = 0, w = 96, h = 128}, 20 | ["test_images/pipo-nekonin014.png"] = {x = 96, y = 0, w = 96, h = 128}, 21 | ["test_images/pipo-nekonin015.png"] = {x = 192, y = 128, w = 96, h = 128}, 22 | } -------------------------------------------------------------------------------- /test_images/pipo-nekonin001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin001.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin002.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin003.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin004.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin005.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin006.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin007.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin008.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin009.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin009.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin010.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin011.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin011.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin012.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin012.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin013.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin013.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin014.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin014.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin015.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin015.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin016.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin016.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin017.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin017.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin018.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin018.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin019.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin019.png -------------------------------------------------------------------------------- /test_images/pipo-nekonin020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elloramir/packer/bc48a0772716ed627453d8812f8e5d3f3308a39a/test_images/pipo-nekonin020.png --------------------------------------------------------------------------------