├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── assets ├── button.png ├── button_active.png ├── button_hover.png ├── character_evil.png ├── character_hero.png ├── fx_particle_bomb.png ├── fx_particle_boom_01.png ├── fx_particle_boom_02.png ├── fx_particle_boom_03.png ├── fx_particle_bullett.png ├── fx_particle_crash_01.png ├── fx_particle_crash_02.png ├── fx_particle_crash_03.png ├── fx_particle_crash_04.png ├── fx_particle_engine_01.png ├── fx_particle_engine_02.png ├── fx_particle_engine_03.png ├── fx_particle_engine_04.png ├── fx_particle_pow_01.png ├── fx_particle_pow_02.png ├── fx_particle_pow_03.png ├── fx_particle_ratata_01.png ├── fx_particle_ratata_02.png ├── fx_particle_ratata_03.png ├── fx_particle_ratata_04.png ├── fx_particle_ratata_05.png ├── fx_particle_ratata_06.png ├── fx_particle_ratata_07.png ├── fx_particle_shell.png ├── fx_particle_smoke_01.png ├── fx_particle_smoke_02.png ├── fx_particle_smoke_03.png ├── platform_left.png ├── platform_mid.png ├── platform_right.png ├── rip_jpg.jpg ├── ship_enemy_body.png ├── ship_enemy_full.png ├── ship_enemy_gun.png ├── ship_enemy_wing.png ├── ship_giant_body.png ├── ship_giant_engine.png ├── ship_giant_floor.png ├── ship_giant_full.png ├── ship_giant_head.png ├── ship_giant_roof.png ├── ship_jet_body.png ├── ship_jet_engine.png ├── ship_jet_exhaust.png ├── ship_jet_full.png ├── ship_jet_gun.png ├── ship_jet_head.png ├── turret_enemy_base.png ├── turret_enemy_full.png ├── turret_enemy_gun.png └── yes_no_maybe_no.gif ├── example.sh ├── index.js ├── lib ├── generator.js ├── packing │ ├── basicpacker.js │ ├── binpacker.js │ ├── growingpacker.js │ └── index.js └── sorter │ └── sorter.js ├── package.json ├── templates ├── cocos2d.template ├── css.template ├── easeljs.template ├── json.template ├── jsonarray.template ├── kiwi.template └── starling.template └── test ├── fixtures ├── 100x100.jpg ├── 200x200.jpg ├── 500x500.jpg └── 50x50.jpg ├── generator.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | example 2 | node_modules 3 | npm-debug.log 4 | .idea 5 | *.iml 6 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | .git* 3 | npm-debug.log 4 | .idea 5 | *.iml 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.10" 5 | - "0.12" 6 | 7 | # whitelisted branches 8 | branches: 9 | only: 10 | - kiwi 11 | - master -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Krzysztof Opałka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gamefroot Texture Packer 2 | ============== 3 | 4 | [![Build Status](https://travis-ci.org/Gamefroot/Gamefroot-Texture-Packer.svg?branch=kiwi)](https://travis-ci.org/Gamefroot/Gamefroot-Texture-Packer) 5 | 6 | Generate high quality texture atlases in Node.js, developed for [Gamefroot.com](http://gamefroot.com) and [Kiwi.js](http://www.kiwijs.org/). 7 | 8 | ###Features### 9 | * Use growing-binpacking to optimise your texture memory 10 | * Trim, scale and pad assets to further make use of space 11 | * Add gutter (bleed) to your images to avoid nasty join lines 12 | * Add a maximum width/height to conform to platform limitations 13 | * Generate as many atlases as you need with a single command 14 | * Use texture groups to ensure optimal run-time performance 15 | 16 | ###Supported spritesheet formats### 17 | * Kiwi.js 18 | * Starling / Sparrow 19 | * JSON (i.e. PIXI.js) 20 | * Easel.js 21 | * cocos2d 22 | * CSS (new!) 23 | 24 | ###Usage### 25 | 1. **Command Line** 26 | ```bash 27 | $ gf-pack assets/*.png 28 | ``` 29 | Options: 30 | ```bash 31 | $ gf-pack 32 | Usage: gf-pack [options] 33 | 34 | Options: 35 | -f, --format format of spritesheet (kiwi, starling, sparrow, json, pixi.js, easel.js, cocos2d) [default: ""] 36 | --cf, --customFormat path to external format template [default: ""] 37 | -n, --name name of generated spritesheet [default: "spritesheet"] 38 | -p, --path path to export directory [default: "."] 39 | -w, --width The maximum width of the generated image(s), required for binpacking, optional for other algorithms [default: 999999] 40 | -h, --height The maximum height of the generated image(s), required for binpacking, optional for other algorithms [default: 999999] 41 | --fullpath include path in file name [default: false] 42 | --prefix prefix for image paths [default: ""] 43 | --trim removes transparent whitespaces around images [default: false] 44 | --square texture should be a square with dimensions max(width,height) [default: false] 45 | --powerOfTwo texture width and height should be rounded up to the nearest power of two [default: false] 46 | --validate check algorithm returned data [default: false] 47 | --scale percentage scale [default: "100%"] 48 | --fuzz percentage fuzz factor (usually value of 1% is a good choice) [default: ""] 49 | --algorithm packing algorithm: growing-binpacking (default), binpacking (requires w and h options), vertical or horizontal [default: "growing-binpacking"] 50 | --padding padding between images in spritesheet [default: 0] 51 | --sort Sort method: maxside (default), area, width or height [string] [default: "maxside"] 52 | --maxAtlases maximum number of texture atlases that will be outputted [default: 0] 53 | --gutter the number of pixels to bleed the image edge, gutter is added to padding value [default: 0] 54 | --group allows you to specify a group of assets that must be included in the same atlas, make sure to use quotes around file paths [default: []] 55 | --resizeWidth resizes all source images to a specific width [default: 0] 56 | --resizeHeight resizes all source images to a specific height [default: 0] 57 | ``` 58 | 2. **Node.js** 59 | ```javascript 60 | var packer = require('gamefroot-texture-packer'); 61 | 62 | packer('assets/*.png', {format: 'kiwi'}, function (err) { 63 | if (err) throw err; 64 | 65 | console.log('spritesheet successfully generated'); 66 | }); 67 | ``` 68 | 69 | ###Installation### 70 | 1. Install [ImageMagick](http://www.imagemagick.org/) 71 | 2. ```npm install gamefroot-texture-packer -g``` 72 | 73 | ###Test### 74 | 75 | mocha test 76 | 77 | 78 | -------------- 79 | This library is based on the foundation work of [Spritesheet.js](https://github.com/krzysztof-o/spritesheet.js) 80 | -------------------------------------------------------------------------------- /assets/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/button.png -------------------------------------------------------------------------------- /assets/button_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/button_active.png -------------------------------------------------------------------------------- /assets/button_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/button_hover.png -------------------------------------------------------------------------------- /assets/character_evil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/character_evil.png -------------------------------------------------------------------------------- /assets/character_hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/character_hero.png -------------------------------------------------------------------------------- /assets/fx_particle_bomb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_bomb.png -------------------------------------------------------------------------------- /assets/fx_particle_boom_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_boom_01.png -------------------------------------------------------------------------------- /assets/fx_particle_boom_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_boom_02.png -------------------------------------------------------------------------------- /assets/fx_particle_boom_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_boom_03.png -------------------------------------------------------------------------------- /assets/fx_particle_bullett.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_bullett.png -------------------------------------------------------------------------------- /assets/fx_particle_crash_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_crash_01.png -------------------------------------------------------------------------------- /assets/fx_particle_crash_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_crash_02.png -------------------------------------------------------------------------------- /assets/fx_particle_crash_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_crash_03.png -------------------------------------------------------------------------------- /assets/fx_particle_crash_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_crash_04.png -------------------------------------------------------------------------------- /assets/fx_particle_engine_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_engine_01.png -------------------------------------------------------------------------------- /assets/fx_particle_engine_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_engine_02.png -------------------------------------------------------------------------------- /assets/fx_particle_engine_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_engine_03.png -------------------------------------------------------------------------------- /assets/fx_particle_engine_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_engine_04.png -------------------------------------------------------------------------------- /assets/fx_particle_pow_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_pow_01.png -------------------------------------------------------------------------------- /assets/fx_particle_pow_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_pow_02.png -------------------------------------------------------------------------------- /assets/fx_particle_pow_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_pow_03.png -------------------------------------------------------------------------------- /assets/fx_particle_ratata_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_ratata_01.png -------------------------------------------------------------------------------- /assets/fx_particle_ratata_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_ratata_02.png -------------------------------------------------------------------------------- /assets/fx_particle_ratata_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_ratata_03.png -------------------------------------------------------------------------------- /assets/fx_particle_ratata_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_ratata_04.png -------------------------------------------------------------------------------- /assets/fx_particle_ratata_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_ratata_05.png -------------------------------------------------------------------------------- /assets/fx_particle_ratata_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_ratata_06.png -------------------------------------------------------------------------------- /assets/fx_particle_ratata_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_ratata_07.png -------------------------------------------------------------------------------- /assets/fx_particle_shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_shell.png -------------------------------------------------------------------------------- /assets/fx_particle_smoke_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_smoke_01.png -------------------------------------------------------------------------------- /assets/fx_particle_smoke_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_smoke_02.png -------------------------------------------------------------------------------- /assets/fx_particle_smoke_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/fx_particle_smoke_03.png -------------------------------------------------------------------------------- /assets/platform_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/platform_left.png -------------------------------------------------------------------------------- /assets/platform_mid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/platform_mid.png -------------------------------------------------------------------------------- /assets/platform_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/platform_right.png -------------------------------------------------------------------------------- /assets/rip_jpg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/rip_jpg.jpg -------------------------------------------------------------------------------- /assets/ship_enemy_body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_enemy_body.png -------------------------------------------------------------------------------- /assets/ship_enemy_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_enemy_full.png -------------------------------------------------------------------------------- /assets/ship_enemy_gun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_enemy_gun.png -------------------------------------------------------------------------------- /assets/ship_enemy_wing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_enemy_wing.png -------------------------------------------------------------------------------- /assets/ship_giant_body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_giant_body.png -------------------------------------------------------------------------------- /assets/ship_giant_engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_giant_engine.png -------------------------------------------------------------------------------- /assets/ship_giant_floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_giant_floor.png -------------------------------------------------------------------------------- /assets/ship_giant_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_giant_full.png -------------------------------------------------------------------------------- /assets/ship_giant_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_giant_head.png -------------------------------------------------------------------------------- /assets/ship_giant_roof.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_giant_roof.png -------------------------------------------------------------------------------- /assets/ship_jet_body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_jet_body.png -------------------------------------------------------------------------------- /assets/ship_jet_engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_jet_engine.png -------------------------------------------------------------------------------- /assets/ship_jet_exhaust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_jet_exhaust.png -------------------------------------------------------------------------------- /assets/ship_jet_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_jet_full.png -------------------------------------------------------------------------------- /assets/ship_jet_gun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_jet_gun.png -------------------------------------------------------------------------------- /assets/ship_jet_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/ship_jet_head.png -------------------------------------------------------------------------------- /assets/turret_enemy_base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/turret_enemy_base.png -------------------------------------------------------------------------------- /assets/turret_enemy_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/turret_enemy_full.png -------------------------------------------------------------------------------- /assets/turret_enemy_gun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/turret_enemy_gun.png -------------------------------------------------------------------------------- /assets/yes_no_maybe_no.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/assets/yes_no_maybe_no.gif -------------------------------------------------------------------------------- /example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -R example 4 | mkdir -p example 5 | 6 | node index.js -p example/kiwi -f kiwi --algorithm growing-binpacking assets/platform*.png --trim --gutter 4 7 | 8 | # node index.js -p example/json -f json --trim --padding 10 assets/*.png 9 | # node index.js -p example/json_50% -f json --trim --padding 10 --scale 50% assets/*.png 10 | # node index.js -p example/starling_sparrow -f starling --trim assets/*.png 11 | # node index.js -p example/easel_js -f easel.js --trim assets/*.png 12 | # node index.js -p example/cocos2d -f cocos2d --trim assets/*.png 13 | # node index.js -p example/css -f css --trim assets/*.png 14 | 15 | # node ../index.js --name vertical --algorithm vertical --trim assets/*.png 16 | # node ../index.js --name horizontal --algorithm horizontal --trim assets/*.png 17 | # node ../index.js --name growing-binpacking --algorithm growing-binpacking --trim assets/*.png 18 | # node ../index.js --name binpacking --algorithm binpacking --width 1000 --height 1000 --trim assets/*.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var generator = require('./lib/generator'); 3 | var async = require('async'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var glob = require('glob'); 7 | var optimist = require('optimist'); 8 | 9 | module.exports = generate; 10 | 11 | var FORMATS = { 12 | 'json': {template: 'json.template', extension: 'json', trim: false}, 13 | 'jsonarray': {template: 'jsonarray.template', extension: 'json', trim: false}, 14 | 'pixi.js': {template: 'json.template', extension: 'json', trim: true}, 15 | 'starling': {template: 'starling.template', extension: 'xml', trim: true}, 16 | 'sparrow': {template: 'starling.template', extension: 'xml', trim: true}, 17 | 'easel.js': {template: 'easeljs.template', extension: 'json', trim: false}, 18 | 'cocos2d': {template: 'cocos2d.template', extension: 'plist', trim: false}, 19 | 'css': {template: 'css.template', extension: 'css', trim: false}, 20 | 'kiwi': {template: 'kiwi.template', extension: 'json', trim: false} 21 | }; 22 | 23 | if (!module.parent) { 24 | var argv = optimist.usage('Usage: $0 [options] ') 25 | .options('f', { 26 | alias: 'format', 27 | describe: 'format of spritesheet (kiwi, starling, sparrow, json, pixi.js, easel.js, cocos2d)', 28 | default: '' 29 | }) 30 | .options('cf', { 31 | alias: 'customFormat', 32 | describe: 'path to external format template', 33 | default: '' 34 | }) 35 | .options('n', { 36 | alias: 'name', 37 | describe: 'name of generated spritesheet', 38 | default: 'spritesheet' 39 | }) 40 | .options('p', { 41 | alias: 'path', 42 | describe: 'path to export directory', 43 | default: '.' 44 | }) 45 | .options('w', { 46 | alias: 'width', 47 | describe: 'The maximum width of the generated image(s), required for binpacking, optional for other algorithms', 48 | default: 999999 49 | }) 50 | .options('h', { 51 | alias: 'height', 52 | describe: 'The maximum height of the generated image(s), required for binpacking, optional for other algorithms', 53 | default: 999999 54 | }) 55 | .options('fullpath', { 56 | describe: 'include path in file name', 57 | default: false, 58 | boolean: true 59 | }) 60 | .options('prefix', { 61 | describe: 'prefix for image paths', 62 | default: "" 63 | }) 64 | .options('trim', { 65 | describe: 'removes transparent whitespaces around images', 66 | default: false, 67 | boolean: true 68 | }) 69 | .options('square', { 70 | describe: 'texture should be a square with dimensions max(width,height)', 71 | default: false, 72 | boolean: true 73 | }) 74 | .options('powerOfTwo', { 75 | describe: 'texture width and height should be rounded up to the nearest power of two', 76 | default: false, 77 | boolean: true 78 | }) 79 | .options('validate', { 80 | describe: 'check algorithm returned data', 81 | default: false, 82 | boolean: true 83 | }) 84 | .options('scale', { 85 | describe: 'percentage scale', 86 | default: '100%' 87 | }) 88 | .options('fuzz', { 89 | describe: 'percentage fuzz factor (usually value of 1% is a good choice)', 90 | default: '' 91 | }) 92 | .options('algorithm', { 93 | describe: 'packing algorithm: growing-binpacking (default), binpacking (requires w and h options), vertical or horizontal', 94 | default: 'growing-binpacking' 95 | }) 96 | .options('padding', { 97 | describe: 'padding between images in spritesheet', 98 | default: 0 99 | }) 100 | .options('sort', { 101 | describe: 'Sort method: maxside (default), area, width or height', 102 | default: 'maxside' 103 | }) 104 | .options('maxAtlases', { 105 | describe: 'maximum number of texture atlases that will be outputted', 106 | default: 0 107 | }) 108 | .options('gutter', { 109 | describe: 'the number of pixels to bleed the image edge, gutter is added to padding value', 110 | default: 0 111 | }) 112 | .options('group', { 113 | describe: 'allows you to specify a group of assets that must be included in the same atlas, make sure to use quotes around file paths', 114 | default: [] 115 | }) 116 | .options('resizeWidth', { 117 | describe: 'resizes all source images to a specific width', 118 | default: 0 119 | }) 120 | .options('resizeHeight', { 121 | describe: 'resizes all source images to a specific height', 122 | default: 0 123 | }) 124 | .demand(1) 125 | .argv; 126 | 127 | if (argv._.length == 0) { 128 | optimist.showHelp(); 129 | return; 130 | } 131 | generate(argv._, argv, function (err) { 132 | if (err) throw err; 133 | console.log('Spritesheet successfully generated'); 134 | }); 135 | } 136 | 137 | /** 138 | * generates spritesheet 139 | * @param {string} files pattern of files images files 140 | * @param {string[]} files paths to image files 141 | * @param {object} options 142 | * @param {string} options.format format of spritesheet (starling, sparrow, json, pixi.js, easel.js, cocos2d, kiwi) 143 | * @param {string} options.customFormat external format template 144 | * @param {string} options.name name of the generated spritesheet 145 | * @param {string} options.path path to the generated spritesheet 146 | * @param {string} options.width maximum width of the generated image(s) 147 | * @param {string} options.height maximum height of the generated image(s) 148 | * @param {string} options.prefix prefix for image paths (css format only) 149 | * @param {boolean} options.fullpath include path in file name 150 | * @param {boolean} options.trim removes transparent whitespaces around images 151 | * @param {boolean} options.square texture should be square 152 | * @param {boolean} options.powerOfTwo texture's size (both width and height) should be a power of two 153 | * @param {string} options.algorithm packing algorithm: growing-binpacking (default), binpacking (requires passing width and height options), vertical or horizontal 154 | * @param {number} options.padding padding between images in spritesheet 155 | * @param {string} options.sort Sort method: maxside (default), area, width, height or none 156 | * @param {number} options.maxAtlases the maximum number of texture atlases that will be generated 157 | * @param {number} options.gutter the amount to bleed the edges of images in spritesheet 158 | * @param {function} callback 159 | */ 160 | function generate(files, options, callback) { 161 | files = Array.isArray(files) ? files : glob.sync(files); 162 | if (files.length == 0) return callback(new Error('no files specified')); 163 | 164 | options = options || {}; 165 | if (Array.isArray(options.format)) { 166 | options.format = options.format.map(function(x){return FORMATS[x]}); 167 | } 168 | else if (options.format || !options.customFormat) { 169 | options.format = [FORMATS[options.format] || FORMATS['json']]; 170 | } 171 | options.name = options.name || 'spritesheet'; 172 | options.spritesheetName = options.name; 173 | options.path = path.resolve(options.path || '.'); 174 | options.fullpath = options.hasOwnProperty('fullpath') ? options.fullpath : false; 175 | options.square = options.hasOwnProperty('square') ? options.square : false; 176 | options.powerOfTwo = options.hasOwnProperty('powerOfTwo') ? options.powerOfTwo : false; 177 | options.extension = options.hasOwnProperty('extension') ? options.extension : options.format[0].extension; 178 | options.trim = options.hasOwnProperty('trim') ? options.trim : options.format[0].trim; 179 | options.algorithm = options.hasOwnProperty('algorithm') ? options.algorithm : 'growing-binpacking'; 180 | options.sort = options.hasOwnProperty('sort') ? options.sort : 'maxside'; 181 | options.padding = options.hasOwnProperty('padding') ? parseInt(options.padding, 10) : 0; 182 | options.prefix = options.hasOwnProperty('prefix') ? options.prefix : ''; 183 | options.maxAtlases = options.hasOwnProperty('maxAtlases') ? options.maxAtlases : 0; 184 | options.gutter = options.hasOwnProperty('gutter') ? parseInt(options.gutter, 10) : 0; 185 | options.resizeWidth = options.hasOwnProperty('resizeWidth') ? parseInt(options.resizeWidth, 10) : 0; 186 | options.resizeHeight = options.hasOwnProperty('resizeHeight') ? parseInt(options.resizeHeight, 10) : 0; 187 | 188 | var fileHash = {}; 189 | files = files.map(function (item, index) { 190 | var resolvedItem = path.resolve(item); 191 | var name = ""; 192 | if (options.fullpath) { 193 | name = item.substring(0, item.lastIndexOf(".")); 194 | } 195 | else { 196 | name = options.prefix + resolvedItem.substring(resolvedItem.lastIndexOf(path.sep) + 1, resolvedItem.lastIndexOf('.')); 197 | } 198 | fileHash[resolvedItem] = { 199 | index: index, 200 | path: resolvedItem, 201 | name: name, 202 | extension: path.extname(resolvedItem) 203 | }; 204 | return fileHash[resolvedItem]; 205 | }); 206 | 207 | if (options.group){ 208 | options.group = Array.isArray(options.group) ? options.group : [options.group]; 209 | options.groups = []; 210 | options.group.forEach(function(groupFiles){ 211 | groupFiles = Array.isArray(groupFiles) ? groupFiles : glob.sync(groupFiles); 212 | var groupItems = []; 213 | groupFiles.forEach(function(item){ 214 | var groupId = options.groups.length; 215 | var resolvedItem = path.resolve(item); 216 | if (fileHash.hasOwnProperty(resolvedItem)) { 217 | var fileInfo = fileHash[resolvedItem]; 218 | fileInfo.group = groupId; 219 | groupItems.push(fileInfo); 220 | } 221 | }); 222 | options.groups.push(groupItems); 223 | }); 224 | } 225 | 226 | if (!fs.existsSync(options.path) && options.path !== '') fs.mkdirSync(options.path); 227 | 228 | async.waterfall([ 229 | function (callback) { 230 | generator.trimImages(files, options, callback); 231 | }, 232 | function (callback) { 233 | generator.resizeImages(files, options, callback); 234 | }, 235 | function (files, callback) { 236 | generator.getImagesSizes(files, options, callback); 237 | }, 238 | function (files, callback) { 239 | generator.determineCanvasSize(files, options, callback); 240 | }, 241 | function (options, callback) { 242 | var n = 0; 243 | var ow = options.width; 244 | var oh = options.height; 245 | var baseName = options.name; 246 | async.each(options.atlases, function(atlas, done){ 247 | options.name = atlas.name = baseName + '-' + (++n); 248 | options.width = atlas.width; 249 | options.height = atlas.height; 250 | generator.generateImage(atlas.files, options, done); 251 | }, callback); 252 | options.name = baseName; 253 | options.width = ow; 254 | options.height = oh; 255 | }, 256 | function (callback) { 257 | var n = 0; 258 | var ow = options.width; 259 | var oh = options.height; 260 | var baseName = options.name; 261 | async.each(options.atlases, function(atlas, done){ 262 | options.name = baseName + '-' + (++n); 263 | options.width = atlas.width; 264 | options.height = atlas.height; 265 | generator.generateData(atlas.files, options, done); 266 | }, callback); 267 | options.name = baseName; 268 | options.width = ow; 269 | options.height = oh; 270 | } 271 | ], 272 | callback); 273 | } 274 | -------------------------------------------------------------------------------- /lib/generator.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | var fs = require('fs'); 3 | var Mustache = require('mustache'); 4 | var async = require('async'); 5 | var os = require('os'); 6 | var path = require('path'); 7 | 8 | var packing = require('./packing/'); 9 | var sorter = require('./sorter/sorter.js'); 10 | 11 | /** 12 | * Generate temporary trimmed image files 13 | * @param {string[]} files 14 | * @param {object} options 15 | * @param {boolean} options.trim is trimming enabled 16 | * @param callback 17 | */ 18 | exports.trimImages = function (files, options, callback) { 19 | if (!options.trim) return callback(null); 20 | 21 | var i = 0; 22 | async.eachSeries(files, function (file, next) { 23 | file.originalPath = file.path; 24 | i++; 25 | file.path = path.join(os.tmpDir(), 'spritesheet_js_' + (new Date()).getTime() + '_image_' + i + '.png'); 26 | 27 | var scale = options.scale && (options.scale !== '100%') ? ' -resize ' + options.scale : ''; 28 | var fuzz = options.fuzz ? ' -fuzz ' + options.fuzz : ''; 29 | //have to add 1px transparent border because imagemagick does trimming based on border pixel's color 30 | exec('convert' + scale + ' ' + fuzz + ' -define png:exclude-chunks=date "' + file.originalPath + '" -bordercolor transparent -border 1 -trim "' + file.path + '"', next); 31 | }, callback); 32 | }; 33 | 34 | /** 35 | * Resizes the images to the given cellWidth and cellHeight 36 | * @param {string[]} files 37 | * @param {object} options 38 | * @param {number} options.resizeWidth The width each image should be resized to 39 | * @param {number} options.resizeHeight The height each image should be resized to 40 | * @param {function} callback in the form f( err, files ) 41 | */ 42 | exports.resizeImages = function (files, options, callback) { 43 | var i = 0; 44 | if (options.resizeWidth || options.resizeHeight) { 45 | async.eachSeries(files, function (file, next) { 46 | var resizeWidth = options.resizeWidth; 47 | var resizeHeight = options.resizeHeight; 48 | var srcPath = file.path; 49 | if (!file.originalPath) { 50 | file.originalPath = file.path; 51 | } 52 | file.path = path.join(os.tmpDir(), 'spritesheet_js_' + (new Date()).getTime() + '_image_' + i + '.png'); 53 | // Build the resize command, we want one of the following 54 | // - convert {src} -resize {width}\! {dest} Only width specified 55 | // - convert {src} -resize x{height}\! {dest} Only height specified 56 | // - convert {src} -resize {width}x{height}\! {dest} Width + Height specified 57 | var cmd = 'convert "' + srcPath + '" -resize '; 58 | if ( resizeWidth ){ 59 | cmd += resizeWidth; 60 | } 61 | if ( resizeHeight ){ 62 | cmd += 'x' + resizeHeight; 63 | } 64 | cmd += '\\! "' + file.path + '"'; 65 | exec( cmd, next); 66 | i++; 67 | }, function( err ){ 68 | callback( err, files ); 69 | }); 70 | } else { 71 | process.nextTick(function(){ 72 | callback( undefined, files ); 73 | }); 74 | } 75 | }; 76 | 77 | /** 78 | * Iterates through given files and gets its size 79 | * @param {string[]} files 80 | * @param {object} options 81 | * @param {boolean} options.trim is trimming enabled 82 | * @param {function} callback 83 | */ 84 | exports.getImagesSizes = function (files, options, callback) { 85 | if (!options.padding) options.padding = 0; 86 | if (!options.gutter) options.gutter = 0; 87 | var filePaths = files.map(function (file) { 88 | return '"' + file.path + '"'; 89 | }); 90 | 91 | let sizes = []; 92 | 93 | // Get file data one by one, as a very large number of simultaneous files 94 | // can blow out `identify` via `stdout maxBuffer exceeded`. 95 | function getNextFileData () { 96 | if ( !filePaths.length ) { 97 | // Complete sequence. 98 | 99 | // Remove images that are frames of a single image. 100 | // This reduces the `sizes` array back to a 1:1 mapping with files. 101 | let lastFilePath; 102 | sizes = sizes.filter( function( size ){ 103 | let filePath = size.substring( 0, size.indexOf( "[" ) ); 104 | let uniqueFilePath = filePath !== lastFilePath; 105 | lastFilePath = filePath; 106 | return uniqueFilePath; 107 | }); 108 | 109 | // Set file options from image analysis. 110 | sizes.forEach( ( item, i ) => { 111 | let size = item.match( / ([0-9]+)x([0-9]+) / ); 112 | let file = files[ i ]; 113 | let padding = options.padding * 2; 114 | let gutter = options.gutter * 2; 115 | file.width = parseInt( size[ 1 ], 10 ) + padding + gutter; 116 | file.height = parseInt( size[ 2 ], 10 ) + padding + gutter; 117 | file.area = file.width * file.height; 118 | file.trimmed = false; 119 | 120 | if ( options.trim ) { 121 | let rect = item.match( 122 | / ([0-9]+)x([0-9]+)[\+\-]([0-9]+)[\+\-]([0-9]+) / ); 123 | file.trim = {}; 124 | file.trim.x = parseInt( rect[ 3 ], 10 ) - 1; 125 | file.trim.y = parseInt( rect[ 4 ], 10 ) - 1; 126 | file.trim.width = parseInt( rect[ 1 ], 10 ) - 2; 127 | file.trim.height = parseInt( rect[ 2 ], 10 ) - 2; 128 | 129 | file.trimmed = 130 | file.trim.width !== file.width - padding - gutter || 131 | file.trim.height !== file.height - padding - gutter; 132 | } 133 | } ); 134 | 135 | return callback( null, files ); 136 | } 137 | 138 | // Continue sequence. 139 | let filePath = filePaths.shift(); 140 | exec( "identify " + filePath, ( err, stdout ) => { 141 | if ( err ) { 142 | return callback( err ); 143 | } 144 | sizes.push( stdout ) 145 | getNextFileData(); 146 | } ); 147 | } 148 | 149 | getNextFileData(); 150 | }; 151 | 152 | /** 153 | * Determines texture size using selected algorithm 154 | * @param {object[]} files 155 | * @param {object} options 156 | * @param {object} options.algorithm (growing-binpacking, binpacking, vertical, horizontal) 157 | * @param {object} options.square canvas width and height should be equal 158 | * @param {object} options.powerOfTwo canvas width and height should be power of two 159 | * @param {function} callback 160 | */ 161 | exports.determineCanvasSize = function (files, options, callback) { 162 | files.forEach(function (item) { 163 | item.w = item.width; 164 | item.h = item.height; 165 | }); 166 | options.excludedFiles = []; 167 | var filesCopy = files.concat(); 168 | 169 | // sort files based on the choosen options.sort method 170 | sorter.run(options.sort, filesCopy); 171 | // Pack groups into 'blocks' 172 | packing.blockGroups(options.algorithm, filesCopy, options); 173 | // Sort files again now that groups are blocked 174 | sorter.run(options.sort, filesCopy); 175 | // Apply the square and powerOfTwo options to the size 176 | // to ensure that the packing takes these values into account 177 | applySquareAndPowerConstraints(options, options.square, options.powerOfTwo); 178 | // Pack the sizes 179 | packing.pack(options.algorithm, filesCopy, options); 180 | // If we are using an alogirithm like growing binpacking, it 181 | // will dynamically adjust it's size, so we reapply the 182 | // square and powerOfTwo options here 183 | options.atlases.forEach(function(atlas){ 184 | applySquareAndPowerConstraints(atlas, options.square, options.powerOfTwo); 185 | }); 186 | 187 | callback(null, options); 188 | }; 189 | 190 | function applySquareAndPowerConstraints(options, square, powerOfTwo){ 191 | if (square) { 192 | options.width = options.height = Math.max(options.width, options.height); 193 | } 194 | if (powerOfTwo) { 195 | options.width = roundToPowerOfTwo(options.width); 196 | options.height = roundToPowerOfTwo(options.height); 197 | } 198 | }; 199 | 200 | /** 201 | * generates texture data file 202 | * @param {object[]} files 203 | * @param {object} options 204 | * @param {string} options.path path to image file 205 | * @param {function} callback 206 | */ 207 | exports.generateImage = function (files, options, callback) { 208 | if (!options.padding) options.padding = 0; 209 | if (!options.gutter) options.gutter = 0; 210 | var outputPath = options.path + '/' + options.name + '.png'; 211 | 212 | var content = { x:0,y:0, w:0,h:0 }; 213 | var command = []; 214 | var extraSize = String('"' + outputPath + '"').length + 1; 215 | var commandSize = extraSize; 216 | 217 | // The maximum number of characters allowed in a single command. 218 | // This is used to help avoid E2BIG errors. 219 | // If we were really fancy we could auto-detect this from the environment 220 | // using `getconf ARG_MAX`. 221 | // Larger numbers = better performance, but less safe. 222 | // Ideally you want to use about half the maximum system allowance, 223 | // to be on the safe side. 224 | var MAX_COMMAND_SIZE = 8000; 225 | 226 | function addOperation( op ){ 227 | command.push(op); 228 | commandSize += op.length + 1; // Plus one for a trailing space 229 | } 230 | addOperation('convert -define png:exclude-chunks=date -limit memory 512MiB -quality 0% -size ' + options.width + 'x' + options.height + ' xc:none'); 231 | 232 | async.eachSeries(files, function (file, callbackAsync) { 233 | content.x = file.x + options.padding + options.gutter; 234 | content.y = file.y + options.padding + options.gutter; 235 | 236 | if (options.gutter) { 237 | content.w = file.width - options.padding*2 - options.gutter*2; 238 | content.h = file.height - options.padding*2 - options.gutter*2; 239 | // Add the gutters left, top, right, bottom 240 | addOperation(' \\( "' + file.path + '" -crop 1x'+content.h+'+0+0 +repage -sample '+options.gutter+'x'+content.h+'\\! \\) -geometry +' + (file.x + options.padding ) + '+' + content.y + ' -composite'); 241 | addOperation(' \\( "' + file.path + '" -crop '+content.w+'x1+0+0 +repage -sample '+content.w+'x'+options.gutter+'\\! \\) -geometry +' + content.x + '+' + (file.y + options.padding) + ' -composite'); 242 | addOperation(' \\( "' + file.path + '" -crop 1x'+content.h+'+'+(content.w-1)+'+0 +repage -sample '+options.gutter+'x'+content.h+'\\! \\) -geometry +' + (content.x + content.w) + '+' + content.y + ' -composite'); 243 | addOperation(' \\( "' + file.path + '" -crop '+content.w+'x1+0+'+(content.h-1)+' +repage -sample '+content.w+'x'+options.gutter+'\\! \\) -geometry +' + content.x + '+' + (content.y + content.h) + ' -composite'); 244 | // Add the gutters topleft, topright, bottomleft, bottomright 245 | var gxg = options.gutter+'x'+options.gutter; 246 | addOperation(' \\( "' + file.path + '" -crop 1x1+0+0 +repage -sample '+gxg+'\\! \\) -geometry +' + (file.x + options.padding ) + '+' + (file.y + options.padding) + ' -composite'); 247 | addOperation(' \\( "' + file.path + '" -crop 1x1+'+(content.w-1)+'+0 +repage -sample '+gxg+'\\! \\) -geometry +' + (content.x + content.w) + '+' + (file.y + options.padding) + ' -composite'); 248 | addOperation(' \\( "' + file.path + '" -crop 1x1+0+'+(content.w-1)+'+0 +repage -sample '+gxg+'\\! \\) -geometry +' + (file.x + options.padding ) + '+' + (content.y + content.h) + ' -composite'); 249 | addOperation(' \\( "' + file.path + '" -crop 1x1+'+(content.w-1)+'+'+(content.h-1)+' +repage -sample '+gxg+'\\! \\) -geometry +' + (content.x + content.w) + '+' + (content.y + content.h) + ' -composite'); 250 | } 251 | // Actual image 252 | addOperation('"' + file.path + '" -geometry +' + content.x + '+' + content.y + ' -composite'); 253 | 254 | // Perform the command right away if it's getting too complicated 255 | // otherwise move onto the next file 256 | if (commandSize > MAX_COMMAND_SIZE) { 257 | command.push('"' + outputPath + '"'); 258 | var cmd = command.join(' '); 259 | command = ['convert "' + outputPath + '"']; 260 | commandSize = command[0].length + extraSize; 261 | exec(cmd, callbackAsync); 262 | } else { 263 | callbackAsync(); 264 | } 265 | 266 | }, function ( err ) { 267 | if ( err ) { 268 | return callback( err ); 269 | } 270 | 271 | if ( command.length ) { 272 | // Once we're done, execute the final operations. 273 | command.push( '"' + outputPath + '"' ); 274 | var cmd = command.join( " " ); 275 | exec( cmd, function ( err ) { 276 | if ( err ) { 277 | return callback( err ); 278 | } 279 | unlinkTempFiles( files ); 280 | callback( null ); 281 | } ); 282 | } else { 283 | // We ran out of ops on the last pass. 284 | unlinkTempFiles( files ); 285 | callback( null ); 286 | } 287 | } ); 288 | }; 289 | 290 | function unlinkTempFiles(files) { 291 | files.forEach(function (file) { 292 | if (file.originalPath && file.originalPath !== file.path) { 293 | fs.unlinkSync(file.path.replace(/\\ /g, ' ')); 294 | } 295 | }); 296 | } 297 | 298 | /** 299 | * generates texture data file 300 | * @param {object[]} files 301 | * @param {object} options 302 | * @param {string} options.path path to data file 303 | * @param {string} options.dataFile data file name 304 | * @param {function} callback 305 | */ 306 | exports.generateData = function (files, options, callback) { 307 | if (!options.padding) options.padding = 0; 308 | if (!options.gutter) options.gutter = 0; 309 | var formats = (Array.isArray(options.customFormat) ? options.customFormat : [options.customFormat]).concat(Array.isArray(options.format) ? options.format : [options.format]); 310 | formats.forEach(function(format, i){ 311 | if (!format) return; 312 | var path = typeof format === 'string' ? format : __dirname + '/../templates/' + format.template; 313 | var templateContent = fs.readFileSync(path, 'utf-8'); 314 | 315 | // sort files based on the choosen options.sort method 316 | sorter.run(options.sort, files); 317 | 318 | options.files = files; 319 | options.files[options.files.length - 1].isLast = true; 320 | options.files.forEach(function (item, i) { 321 | item.width -= options.padding * 2 + options.gutter * 2; 322 | item.height -= options.padding * 2 + options.gutter * 2; 323 | item.x += options.padding + options.gutter; 324 | item.y += options.padding + options.gutter; 325 | 326 | item.index = i; 327 | if (item.trim) { 328 | item.trim.frameX = -item.trim.x; 329 | item.trim.frameY = -item.trim.y; 330 | item.trim.offsetX = Math.floor(Math.abs(item.trim.x + item.width / 2 - item.trim.width / 2)); 331 | item.trim.offsetY = Math.floor(Math.abs(item.trim.y + item.height / 2 - item.trim.height / 2)); 332 | } 333 | item.cssName = item.name || ""; 334 | item.cssName = item.cssName.replace("_hover", ":hover"); 335 | item.cssName = item.cssName.replace("_active", ":active"); 336 | }); 337 | 338 | var result = Mustache.render(templateContent, options); 339 | function findPriority(property) { 340 | var value = options[property]; 341 | var isArray = Array.isArray(value); 342 | if (isArray) { 343 | return i < value.length ? value[i] : format[property] || value[0]; 344 | } 345 | return format[property] || value; 346 | } 347 | fs.writeFile(findPriority('path') + '/' + findPriority('name') + '.' + findPriority('extension'), result, callback); 348 | }); 349 | }; 350 | 351 | /** 352 | * Rounds a given number to to next number which is power of two 353 | * @param {number} value number to be rounded 354 | * @return {number} rounded number 355 | */ 356 | function roundToPowerOfTwo(value) { 357 | if (typeof value != 'number') return undefined; 358 | var powers = 2; 359 | while (value > powers) { 360 | powers *= 2; 361 | } 362 | 363 | return powers; 364 | } 365 | -------------------------------------------------------------------------------- /lib/packing/basicpacker.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var Packer = function(direction,w,h){ 4 | this.init(direction,w,h); 5 | }; 6 | 7 | Packer.HORIZONTAL = 'horizontal'; 8 | 9 | Packer.VERTICAL = 'vertical'; 10 | 11 | Packer.prototype = { 12 | 13 | init: function(direction,w,h){ 14 | if (direction != Packer.HORIZONTAL && direction != Packer.VERTICAL) { 15 | throw new Error('Invalid direction provided, "'+direction+'"'); 16 | } 17 | this.direction = direction; 18 | this.maxWidth = w || 9999999; 19 | this.maxHeight = h || 9999999; 20 | }, 21 | 22 | fit: function(blocks){ 23 | switch (this.direction) { 24 | case Packer.HORIZONTAL: this.fitHorizontal(blocks); break; 25 | case Packer.VERTICAL: this.fitVertical(blocks); break; 26 | } 27 | }, 28 | 29 | fitHorizontal: function(blocks){ 30 | var self = this; 31 | this.width = 0; 32 | this.height = 0; 33 | var x = 0; 34 | var y = 0; 35 | var maxWidth = 0; 36 | var maxHeight = 0; 37 | var lastItem 38 | blocks.forEach(function (item) { 39 | if (x + item.w > self.maxWidth) { 40 | maxWidth = Math.max(maxWidth, x); 41 | x = 0; 42 | y += maxHeight; 43 | maxHeight = 0; 44 | } 45 | if (y + item.h < self.maxHeight) { 46 | maxHeight = Math.max(maxHeight, item.h); 47 | item.fit = { x:x, y:y, w:item.w, h:item.h }; 48 | x += item.w; 49 | } 50 | }); 51 | this.width = maxWidth; 52 | this.height = y + maxHeight; 53 | }, 54 | 55 | fitVertical: function(blocks){ 56 | var self = this; 57 | this.width = 0; 58 | this.height = 0; 59 | var x = 0; 60 | var y = 0; 61 | var maxWidth = 0; 62 | var maxHeight = 0; 63 | var lastItem 64 | blocks.forEach(function (item) { 65 | if (y + item.h > self.maxHeight) { 66 | maxHeight = Math.max(maxHeight, y) 67 | y = 0; 68 | x += maxWidth; 69 | maxWidth = 0; 70 | } 71 | if (x + item.w < self.maxWidth) { 72 | maxWidth = Math.max(maxWidth, item.w); 73 | item.fit = { x:x, y:y, w:item.w, h:item.h }; 74 | y += item.h; 75 | } 76 | }); 77 | this.width = x + maxWidth; 78 | this.height = maxHeight; 79 | } 80 | }; 81 | 82 | module.exports = Packer; -------------------------------------------------------------------------------- /lib/packing/binpacker.js: -------------------------------------------------------------------------------- 1 | // Derived from file here 2 | // https://github.com/jsmarkus/node-bin-packing/blob/master/js/packer.js 3 | 4 | var Packer = exports.Packer = function(w, h) { 5 | this.init(w, h); 6 | }; 7 | 8 | Packer.prototype = { 9 | 10 | init: function(w, h) { 11 | if (typeof w != 'number' || typeof h != 'number') { 12 | throw new Error('Invalid dimensions specified "'+w+' x '+h+'"'); 13 | } 14 | this.root = { x: 0, y: 0, w: w, h: h }; 15 | }, 16 | 17 | fit: function(blocks) { 18 | var n, node, block; 19 | for (n = 0; n < blocks.length; n++) { 20 | block = blocks[n]; 21 | if (node = this.findNode(this.root, block.w, block.h)) 22 | block.fit = this.splitNode(node, block.w, block.h); 23 | } 24 | }, 25 | 26 | findNode: function(root, w, h) { 27 | if (root.used) 28 | return this.findNode(root.right, w, h) || this.findNode(root.down, w, h); 29 | else if ((w <= root.w) && (h <= root.h)) 30 | return root; 31 | else 32 | return null; 33 | }, 34 | 35 | splitNode: function(node, w, h) { 36 | node.used = true; 37 | node.down = { x: node.x, y: node.y + h, w: node.w, h: node.h - h }; 38 | node.right = { x: node.x + w, y: node.y, w: node.w - w, h: h }; 39 | return node; 40 | } 41 | 42 | } 43 | 44 | module.exports = Packer; 45 | -------------------------------------------------------------------------------- /lib/packing/growingpacker.js: -------------------------------------------------------------------------------- 1 | // Derived from file here 2 | // https://github.com/jsmarkus/node-bin-packing/blob/master/js/packer.growing.js 3 | 4 | var GrowingPacker = function(w,h) { 5 | this.init(w,h); 6 | }; 7 | 8 | GrowingPacker.prototype = { 9 | 10 | init: function(w,h) { 11 | this.maxWidth = w || 9999999999999; 12 | this.maxHeight = h || 9999999999999; 13 | }, 14 | 15 | fit: function(blocks) { 16 | var n, node, block, len = blocks.length; 17 | var w = len > 0 ? blocks[0].w : 0; 18 | var h = len > 0 ? blocks[0].h : 0; 19 | this.root = { x: 0, y: 0, w: w, h: h }; 20 | for (n = 0; n < len ; n++) { 21 | block = blocks[n]; 22 | if (node = this.findNode(this.root, block.w, block.h)) 23 | block.fit = this.splitNode(node, block.w, block.h); 24 | else 25 | block.fit = this.growNode(block.w, block.h); 26 | } 27 | }, 28 | 29 | findNode: function(root, w, h) { 30 | if (root.used) 31 | return this.findNode(root.right, w, h) || this.findNode(root.down, w, h); 32 | else if ((w <= root.w) && (h <= root.h)) 33 | return root; 34 | else 35 | return null; 36 | }, 37 | 38 | splitNode: function(node, w, h) { 39 | node.used = true; 40 | node.down = { x: node.x, y: node.y + h, w: node.w, h: node.h - h }; 41 | node.right = { x: node.x + w, y: node.y, w: node.w - w, h: h }; 42 | return node; 43 | }, 44 | 45 | growNode: function(w, h) { 46 | var canGrowDown = (w <= this.root.w && this.root.h + h < this.maxHeight); 47 | var canGrowRight = (h <= this.root.h && this.root.w + w < this.maxWidth); 48 | 49 | var shouldGrowRight = canGrowRight && (this.root.h >= (this.root.w + w)); // attempt to keep square-ish by growing right when height is much greater than width 50 | var shouldGrowDown = canGrowDown && (this.root.w >= (this.root.h + h)); // attempt to keep square-ish by growing down when width is much greater than height 51 | 52 | if (shouldGrowRight) 53 | return this.growRight(w, h); 54 | else if (shouldGrowDown) 55 | return this.growDown(w, h); 56 | else if (canGrowRight) 57 | return this.growRight(w, h); 58 | else if (canGrowDown) 59 | return this.growDown(w, h); 60 | else 61 | return null; // need to ensure sensible root starting size to avoid this happening 62 | }, 63 | 64 | growRight: function(w, h) { 65 | this.root = { 66 | used: true, 67 | x: 0, 68 | y: 0, 69 | w: this.root.w + w, 70 | h: this.root.h, 71 | down: this.root, 72 | right: { x: this.root.w, y: 0, w: w, h: this.root.h } 73 | }; 74 | if (node = this.findNode(this.root, w, h)) 75 | return this.splitNode(node, w, h); 76 | else 77 | return null; 78 | }, 79 | 80 | growDown: function(w, h) { 81 | this.root = { 82 | used: true, 83 | x: 0, 84 | y: 0, 85 | w: this.root.w, 86 | h: this.root.h + h, 87 | down: { x: 0, y: this.root.h, w: this.root.w, h: h }, 88 | right: this.root 89 | }; 90 | if (node = this.findNode(this.root, w, h)) 91 | return this.splitNode(node, w, h); 92 | else 93 | return null; 94 | } 95 | 96 | } 97 | 98 | module.exports = GrowingPacker; -------------------------------------------------------------------------------- /lib/packing/index.js: -------------------------------------------------------------------------------- 1 | var BinPacker = require('./binpacker'); 2 | var GrowingPacker = require('./growingpacker'); 3 | var BasicPacker = require('./basicpacker'); 4 | 5 | var algorithms = { 6 | 'binpacking': binpackingStrict, 7 | 'growing-binpacking': growingBinpacking, 8 | 'horizontal': horizontal, 9 | 'vertical': vertical 10 | }; 11 | exports.pack = function (algorithm, files, options) { 12 | algorithm = algorithm || 'growing-binpacking'; 13 | var remainingFiles = files.concat(); 14 | options.atlases = []; 15 | while (!options.maxAtlases || options.atlases.length < options.maxAtlases) { 16 | // Details for this texture group 17 | var group = { 18 | width: options.width, 19 | height: options.height 20 | }; 21 | // Perform the fit 22 | algorithms[algorithm](remainingFiles, group); 23 | 24 | // Find out which files were fit 25 | var insertedFiles = []; 26 | var i = remainingFiles.length; 27 | while (--i >= 0) { 28 | var item = remainingFiles[i]; 29 | if (item.fit) { 30 | item.x = item.fit.x; 31 | item.y = item.fit.y; 32 | delete item.fit; 33 | delete item.w; 34 | delete item.h; 35 | 36 | if (item.files) { 37 | // If this is a group, add all of the groups files 38 | item.files.forEach(function(file){ 39 | file.x = file.fit.x + item.x; 40 | file.y = file.fit.y + item.y; 41 | delete file.fit; 42 | delete file.w; 43 | delete file.h; 44 | insertedFiles.push(file); 45 | }); 46 | } else { 47 | // Otherwise just add the single file 48 | insertedFiles.push(item); 49 | } 50 | remainingFiles.splice(i,1); 51 | } 52 | } 53 | 54 | // If we didn't insert any files, don't continue 55 | if (insertedFiles.length == 0) { 56 | break; 57 | } 58 | // Otherwise add another texture group to the result 59 | else { 60 | group.files = insertedFiles; 61 | options.atlases.push(group); 62 | } 63 | } 64 | 65 | // If we stopped before all the files were packed 66 | // We either need to throw an error or make a record 67 | // of said files 68 | if (remainingFiles.length > 0) { 69 | remainingFiles.forEach(function(file){ 70 | options.excludedFiles.push(file); 71 | }); 72 | if (options.validate) { 73 | throw new Error("Can't fit all textures in given dimensions"); 74 | } 75 | } 76 | }; 77 | 78 | exports.blockGroups = function(algorithm, files, options) { 79 | algorithm = algorithm || 'growing-binpacking'; 80 | var groups = []; 81 | files.forEach(function(file){ 82 | if (file.hasOwnProperty('group')) { 83 | while (groups.length-1 < file.group) { 84 | groups.push([]); 85 | } 86 | groups[file.group].push(file); 87 | } 88 | }); 89 | for (var i = 0; i < groups.length; i++) { 90 | var group = groups[i]; 91 | if (group.length > 0) { 92 | var block = { width:options.width, height:options.height }; 93 | algorithms[algorithm](group, block); 94 | block.w = block.width; 95 | block.h = block.height; 96 | block.group = i; 97 | block.files = []; 98 | 99 | var didFit = true; 100 | group.forEach(function(file){ 101 | files.splice(files.indexOf(file),1); 102 | if (!file.fit) { 103 | didFit = false; 104 | options.excludedFiles.push(file); 105 | } 106 | else { 107 | block.files.push(file); 108 | } 109 | }); 110 | if (didFit) { 111 | files.push(block); 112 | } 113 | } 114 | } 115 | }; 116 | 117 | function growingBinpacking(files, group) { 118 | var packer = new GrowingPacker(group.width,group.height); 119 | packer.fit(files); 120 | group.width = packer.root.w; 121 | group.height = packer.root.h; 122 | }; 123 | 124 | function binpackingStrict(files, group) { 125 | var packer = new BinPacker(group.width, group.height); 126 | packer.fit(files); 127 | group.width = packer.root.w; 128 | group.height = packer.root.h; 129 | }; 130 | 131 | function horizontal(files, group) { 132 | var packer = new BasicPacker(BasicPacker.HORIZONTAL, group.width, group.height); 133 | packer.fit(files); 134 | group.width = packer.width; 135 | group.height = packer.height; 136 | } 137 | 138 | function vertical(files, group) { 139 | var packer = new BasicPacker(BasicPacker.VERTICAL, group.width, group.height); 140 | packer.fit(files); 141 | group.width = packer.width; 142 | group.height = packer.height; 143 | }; -------------------------------------------------------------------------------- /lib/sorter/sorter.js: -------------------------------------------------------------------------------- 1 | // based on: http://codeincomplete.com/posts/2011/5/7/bin_packing/example/ 2 | 3 | var Sorters = { 4 | w: function (a, b) { 5 | return b.w - a.w; 6 | }, 7 | h: function (a, b) { 8 | return b.h - a.h; 9 | }, 10 | a: function (a, b) { 11 | return b.area - a.area; 12 | }, 13 | max: function (a, b) { 14 | return Math.max(b.w, b.h) - Math.max(a.w, a.h); 15 | }, 16 | min: function (a, b) { 17 | return Math.min(b.w, b.h) - Math.min(a.w, a.h); 18 | } 19 | }; 20 | 21 | var MultiSorters = { 22 | height: function (a, b) { 23 | return msort(a, b, ['h', 'w']); 24 | }, 25 | width: function (a, b) { 26 | return msort(a, b, ['w', 'h']); 27 | }, 28 | area: function (a, b) { 29 | return msort(a, b, ['a', 'h', 'w']); 30 | }, 31 | maxside: function (a, b) { 32 | return msort(a, b, ['max', 'min', 'h', 'w']); 33 | } 34 | }; 35 | 36 | exports.run = function (method, files) { 37 | if (method != 'none') { 38 | var filter = MultiSorters[method]; 39 | if (filter) { 40 | files.sort(filter); 41 | } 42 | } 43 | }; 44 | 45 | function msort(a, b, criteria) { /* sort by multiple criteria */ 46 | 47 | var diff, n; 48 | 49 | for (n = 0; n < criteria.length; n++) { 50 | 51 | diff = Sorters[criteria[n]](a, b); 52 | 53 | if (diff !== 0) { 54 | return diff; 55 | } 56 | } 57 | 58 | return 0; 59 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gamefroot-texture-packer", 3 | "version": "1.1.2", 4 | "description": "Tool for generating high quality texture atlases from a list of input files, backed by gamefroot.com and kiwi.js.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "bin": { 10 | "gf-pack": "./index.js" 11 | }, 12 | "dependencies": { 13 | "async": "^1.4.0", 14 | "glob": "^5.0.14", 15 | "mustache": "^2.1.3", 16 | "optimist": "~0.6.0" 17 | }, 18 | "devDependencies": { 19 | "expect": "^1.8.0", 20 | "mocha": "*" 21 | }, 22 | "scripts": { 23 | "test": "mocha test" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git://github.com/Gamefroot/Gamefroot-Texture-Packer.git" 28 | }, 29 | "keywords": [ 30 | "kiwi.js", 31 | "gamefroot", 32 | "pack", 33 | "spritesheet", 34 | "texture", 35 | "starling", 36 | "sparrow", 37 | "pixi.js", 38 | "json", 39 | "cocos2d", 40 | "easel.js", 41 | "generator" 42 | ], 43 | "license": "MIT", 44 | "bugs": { 45 | "url": "https://github.com/Gamefroot/Gamefroot-Texture-Packer/issues" 46 | }, 47 | "homepage": "https://github.com/Gamefroot/Gamefroot-Texture-Packer" 48 | } 49 | -------------------------------------------------------------------------------- /templates/cocos2d.template: -------------------------------------------------------------------------------- 1 | {{=<% %>=}} 2 | 3 | 4 | 5 | 6 | frames 7 | 8 | <%#files%> 9 | <%#trimmed%> 10 | <%name%><%extension%> 11 | {{<%x%>,<%y%>},{<%width%>,<%height%>}} 12 | offset 13 | {<%trim.offsetX%>,<%trim.offsetY%>} 14 | rotated 15 | 16 | sourceColorRect 17 | {{<%trim.x%>,<%trim.y%>},{<%trim.width%>,<%trim.height%>}} 18 | sourceSize 19 | {<%trim.width%>,<%trim.height%>} 20 | <%/trimmed%> 21 | <%^trimmed%> 22 | <%name%><%extension%> 23 | {{<%x%>,<%y%>},{<%width%>,<%height%>}} 24 | offset 25 | {0,0} 26 | rotated 27 | 28 | sourceColorRect 29 | {{0,0},{<%width%>,<%height%>}} 30 | sourceSize 31 | {<%width%>,<%height%>} 32 | <%/trimmed%> 33 | <%/files%> 34 | 35 | metadata 36 | 37 | format 38 | 2 39 | realTextureFileName 40 | <%name%>.png 41 | size 42 | {<%width%>,<%height%>} 43 | textureFileName 44 | <%name%>.png 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /templates/css.template: -------------------------------------------------------------------------------- 1 | {{#files}} 2 | .{{{cssName}}} { 3 | width: {{width}}px; 4 | height: {{height}}px; 5 | background: url("{{{prefix}}}{{{spritesheetName}}}.png") -{{x}}px -{{y}}px; 6 | } 7 | 8 | {{/files}} -------------------------------------------------------------------------------- /templates/easeljs.template: -------------------------------------------------------------------------------- 1 | { 2 | "images": ["{{{name}}}.png"], 3 | "frames": [ 4 | {{#files}} 5 | [{{x}}, {{y}}, {{width}}, {{height}}]{{^isLast}},{{/isLast}} //{{{name}}} 6 | {{/files}} 7 | ], 8 | "animations": { 9 | {{#files}} 10 | "{{{name}}}":[{{index}}]{{^isLast}},{{/isLast}} 11 | {{/files}} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /templates/json.template: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "image": "{{{name}}}.png", 4 | "size": {"w":{{width}},"h":{{height}}}, 5 | "scale": "1" 6 | }, 7 | "frames": { 8 | {{#files}} 9 | "{{{name}}}{{extension}}": 10 | { 11 | {{#trimmed}} 12 | "frame": {"x":{{x}},"y":{{y}},"w":{{width}},"h":{{height}}}, 13 | "rotated": false, 14 | "trimmed": true, 15 | "spriteSourceSize": {"x":{{trim.x}},"y":{{trim.y}},"w":{{width}},"h":{{height}}}, 16 | "sourceSize": {"w":{{trim.width}},"h":{{trim.height}}} 17 | {{/trimmed}} 18 | {{^trimmed}} 19 | "frame": {"x":{{x}},"y":{{y}},"w":{{width}},"h":{{height}}}, 20 | "rotated": false, 21 | "trimmed": false, 22 | "spriteSourceSize": {"x":0,"y":0,"w":{{width}},"h":{{height}}}, 23 | "sourceSize": {"w":{{width}},"h":{{height}}} 24 | {{/trimmed}} 25 | }{{^isLast}},{{/isLast}} 26 | {{/files}} 27 | } 28 | } -------------------------------------------------------------------------------- /templates/jsonarray.template: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "image": "{{{name}}}.png", 4 | "size": {"w":{{width}},"h":{{height}}}, 5 | "scale": "1" 6 | }, 7 | "frames": [ 8 | {{#files}} 9 | { 10 | {{#trimmed}} 11 | "filename": "{{{name}}}{{extension}}", 12 | "frame": {"x":{{x}},"y":{{y}},"w":{{width}},"h":{{height}}}, 13 | "rotated": false, 14 | "trimmed": true, 15 | "spriteSourceSize": {"x":{{trim.x}},"y":{{trim.y}},"w":{{width}},"h":{{height}}}, 16 | "sourceSize": {"w":{{trim.width}},"h":{{trim.height}}} 17 | {{/trimmed}} 18 | {{^trimmed}} 19 | "filename": "{{{name}}}{{extension}}", 20 | "frame": {"x":{{x}},"y":{{y}},"w":{{width}},"h":{{height}}}, 21 | "rotated": false, 22 | "trimmed": false, 23 | "spriteSourceSize": {"x":0,"y":0,"w":{{width}},"h":{{height}}}, 24 | "sourceSize": {"w":{{width}},"h":{{height}}} 25 | {{/trimmed}} 26 | }{{^isLast}},{{/isLast}} 27 | {{/files}} 28 | ] 29 | } -------------------------------------------------------------------------------- /templates/kiwi.template: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{{name}}}", 3 | "cells": [ 4 | {{#files}} 5 | { 6 | "x": {{x}}, 7 | "y": {{y}}, 8 | "w": {{width}}, 9 | "h": {{height}}, 10 | "name": "{{{name}}}" 11 | }{{^isLast}},{{/isLast}} 12 | {{/files}} 13 | ] 14 | } -------------------------------------------------------------------------------- /templates/starling.template: -------------------------------------------------------------------------------- 1 | 2 | {{#files}} 3 | 4 | {{/files}} 5 | -------------------------------------------------------------------------------- /test/fixtures/100x100.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/test/fixtures/100x100.jpg -------------------------------------------------------------------------------- /test/fixtures/200x200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/test/fixtures/200x200.jpg -------------------------------------------------------------------------------- /test/fixtures/500x500.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/test/fixtures/500x500.jpg -------------------------------------------------------------------------------- /test/fixtures/50x50.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/f0df9e31bccfc84c2feddfb17bd1a6bfcc7251a5/test/fixtures/50x50.jpg -------------------------------------------------------------------------------- /test/generator.js: -------------------------------------------------------------------------------- 1 | var generator = require('../lib/generator'); 2 | var expect = require('expect'); 3 | var fs = require('fs'); 4 | 5 | describe('generator', function () { 6 | 7 | describe('getImagesSizes', function () { 8 | it('should return image sizes', function (done) { 9 | var FILES = [ 10 | {path: __dirname + '/fixtures/50x50.jpg', width: 50, height: 50}, 11 | {path: __dirname + '/fixtures/100x100.jpg', width: 100, height: 100}, 12 | {path: __dirname + '/fixtures/200x200.jpg', width: 200, height: 200}, 13 | {path: __dirname + '/fixtures/500x500.jpg', width: 500, height: 500} 14 | ]; 15 | 16 | generator.getImagesSizes(FILES, {padding: 0}, function (err, files) { 17 | expect(err).toBe(null); 18 | 19 | expect(files[0].width).toEqual(50); 20 | expect(files[0].height).toEqual(50); 21 | 22 | expect(files[1].width).toEqual(100); 23 | expect(files[1].height).toEqual(100); 24 | 25 | expect(files[2].width).toEqual(200); 26 | expect(files[2].height).toEqual(200); 27 | 28 | expect(files[3].width).toEqual(500); 29 | expect(files[3].height).toEqual(500); 30 | 31 | done(); 32 | }); 33 | }); 34 | }); 35 | 36 | describe('resizeImages', function (){ 37 | it('should resize images', function ( done ) { 38 | var FILES = [ 39 | {path: __dirname + '/fixtures/50x50.jpg', width: 50, height: 50}, 40 | {path: __dirname + '/fixtures/100x100.jpg', width: 100, height: 100}, 41 | {path: __dirname + '/fixtures/200x200.jpg', width: 200, height: 200}, 42 | {path: __dirname + '/fixtures/500x500.jpg', width: 500, height: 500} 43 | ]; 44 | 45 | var testWidth = 50; 46 | var testHeight = 50; 47 | generator.resizeImages(FILES, { resizeWidth:50, resizeHeight:50 }, function ( err, files ){ 48 | expect(err).toBe(null); 49 | 50 | generator.getImagesSizes( files, {padding: 0}, function ( err, files ) { 51 | expect(err).toBe(null); 52 | 53 | files.forEach( function( file, i ) { 54 | expect(file.width).toEqual( testWidth ); 55 | expect(file.height).toEqual( testHeight ); 56 | }); 57 | 58 | done(); 59 | }); 60 | }); 61 | }); 62 | }); 63 | 64 | describe('determineCanvasSize', function () { 65 | var FILES = [ 66 | {path: __dirname + '/fixtures/500x500.jpg', width: 500, height: 500}, 67 | {path: __dirname + '/fixtures/200x200.jpg', width: 200, height: 200}, 68 | {path: __dirname + '/fixtures/100x100.jpg', width: 100, height: 100}, 69 | {path: __dirname + '/fixtures/50x50.jpg', width: 50, height: 50} 70 | ]; 71 | 72 | it('should return square canvas', function (done) { 73 | var options = {format:'kiwi', algorithm:'growing-binpacking', square: true, powerOfTwo: false}; 74 | generator.determineCanvasSize(FILES, options, function (err) { 75 | expect(err).toBe(null); 76 | expect(options.atlases.length).toEqual(1,"number of groups should be one"); 77 | expect(options.atlases[0].width).toEqual(options.atlases[0].height); 78 | 79 | done(); 80 | }); 81 | }); 82 | 83 | it('should return power of two', function (done) { 84 | var options = {square: false, powerOfTwo: true}; 85 | generator.determineCanvasSize(FILES, options, function (err) { 86 | expect(err).toBe(null); 87 | expect(options.atlases.length).toEqual(1, "number of groups should be one"); 88 | expect(options.atlases[0].width).toEqual(1024); 89 | expect(options.atlases[0].height).toEqual(512); 90 | done(); 91 | }); 92 | }); 93 | 94 | it('should create multiple texture groups', function (done) { 95 | var options = {square: false, powerOfTwo: false, width:500, height:500}; 96 | generator.determineCanvasSize(FILES, options, function (err) { 97 | expect(err).toBe(null); 98 | expect(options.atlases.length).toBeMoreThan(1); 99 | // TODO check json and image created for each group 100 | done(); 101 | }); 102 | }); 103 | 104 | it('should create limit texture groups', function (done) { 105 | var options = {square: false, powerOfTwo: false, width:500, height:500, maxAtlases:1}; 106 | generator.determineCanvasSize(FILES, options, function (err) { 107 | expect(err).toBe(null); 108 | expect(options.atlases.length).toEqual(1); 109 | expect(options.excludedFiles.length).toBeMoreThan(0); 110 | done(); 111 | }); 112 | }); 113 | 114 | var FILES_GROUPED = [ 115 | {path: __dirname + '/fixtures/500x500.jpg', width: 500, height: 500, group:0}, 116 | {path: __dirname + '/fixtures/200x200.jpg', width: 200, height: 200, group:0}, 117 | {path: __dirname + '/fixtures/100x100.jpg', width: 100, height: 100, group:0}, 118 | {path: __dirname + '/fixtures/50x50.jpg', width: 50, height: 50, group:0} 119 | ]; 120 | 121 | it('should group images together that have the same group specified', function(done){ 122 | var options = {width:500, height:500}; 123 | generator.determineCanvasSize(FILES_GROUPED, options, function (err){ 124 | expect(err).toBe(null); 125 | // Because the files must be grouped together and can not all fit 126 | expect(options.atlases.length).toEqual(0); 127 | expect(options.excludedFiles.length).toBeMoreThan(0); 128 | done(); 129 | }); 130 | }); 131 | 132 | after(function(){ 133 | try { 134 | for (var i = 1; i <= 4; i++) { 135 | fs.unlinkSync(__dirname + '/test-'+i+'.png'); 136 | fs.unlinkSync(__dirname + '/test-'+i+'.json'); 137 | } 138 | } catch(e){ } 139 | }); 140 | }); 141 | 142 | describe('generateImage', function () { 143 | var FILES = [ 144 | {path: __dirname + '/fixtures/50x50.jpg', width: 50, height: 50, x: 0, y: 0}, 145 | {path: __dirname + '/fixtures/100x100.jpg', width: 100, height: 100, x: 0, y: 0}, 146 | {path: __dirname + '/fixtures/200x200.jpg', width: 200, height: 200, x: 0, y: 0}, 147 | {path: __dirname + '/fixtures/500x500.jpg', width: 500, height: 500, x: 0, y: 0} 148 | ]; 149 | 150 | it('should generate image file', function (done) { 151 | var options = {width: 100, height: 100, path: __dirname + '/', name: 'test', padding: 0}; 152 | generator.generateImage(FILES, options, function (err) { 153 | expect(err).toBe(null); 154 | expect(fs.existsSync(__dirname + '/test.png')).toExist(); 155 | done(); 156 | }); 157 | }); 158 | 159 | after(function(){ 160 | fs.unlinkSync(__dirname + '/test.png'); 161 | }); 162 | }); 163 | 164 | describe('generateData', function () { 165 | var FILES = [ 166 | {path: __dirname + '/fixtures/50x50.jpg', width: 50, height: 50, x: 0, y: 0}, 167 | {path: __dirname + '/fixtures/100x100.jpg', width: 100, height: 100, x: 0, y: 0}, 168 | {path: __dirname + '/fixtures/200x200.jpg', width: 200, height: 200, x: 0, y: 0}, 169 | {path: __dirname + '/fixtures/500x500.jpg', width: 500, height: 500, x: 0, y: 0} 170 | ]; 171 | 172 | it('should generate data file', function (done) { 173 | var options = {path: __dirname + '/', name: 'test', format: {extension: 'json', template: 'json.template', padding: 0}}; 174 | generator.generateData(FILES, options, function (err) { 175 | expect(err).toBe(null); 176 | expect(fs.existsSync(__dirname + '/test.json')).toExist(); 177 | done(); 178 | }); 179 | }); 180 | 181 | after(function(){ 182 | fs.unlinkSync(__dirname + '/test.json'); 183 | }); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var spritesheet = require('..'); 2 | var assert = require('assert'); 3 | var expect = require('expect'); 4 | var fs = require('fs'); 5 | 6 | var FORMAT = {extension: 'json', template: 'json.template'}; 7 | 8 | describe('Texture Packing', function () { 9 | 10 | describe('with given pattern of files', function () { 11 | it('should generate xml file', function (done) { 12 | spritesheet(__dirname + '/fixtures/*', {name: 'test', path: __dirname, format: FORMAT}, function (err) { 13 | expect(err).toBe(null); 14 | expect(fs.existsSync(__dirname + '/test-1.json')).toExist(); 15 | done(); 16 | }); 17 | }); 18 | 19 | it('should generate png file', function (done) { 20 | spritesheet(__dirname + '/fixtures/*', {name: 'test', path: __dirname, format: FORMAT}, function (err) { 21 | expect(err).toBe(null); 22 | expect(fs.existsSync(__dirname + '/test-1.png')).toExist(); 23 | done(); 24 | }); 25 | }); 26 | 27 | after(function(){ 28 | try { 29 | for (var i = 1; i <= 4; i++) { 30 | fs.unlinkSync(__dirname + '/test-'+i+'.png'); 31 | fs.unlinkSync(__dirname + '/test-'+i+'.json'); 32 | } 33 | } catch(e){ } 34 | }); 35 | }); 36 | 37 | describe('with given array of files', function () { 38 | it('should generate xml file', function (done) { 39 | spritesheet([__dirname + '/fixtures/100x100.jpg'], {name: 'test', path: __dirname, format: FORMAT}, function (err) { 40 | expect(err).toBe(null); 41 | expect(fs.existsSync(__dirname + '/test-1.json')).toExist(); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should generate png file', function (done) { 47 | spritesheet([__dirname + '/fixtures/100x100.jpg'], {name: 'test', path: __dirname, format: FORMAT}, function (err) { 48 | expect(err).toBe(null); 49 | expect(fs.existsSync(__dirname + '/test-1.png')).toExist(); 50 | done(); 51 | }); 52 | }); 53 | 54 | after(function(){ 55 | try { 56 | for (var i = 1; i <= 4; i++) { 57 | fs.unlinkSync(__dirname + '/test-'+i+'.png'); 58 | fs.unlinkSync(__dirname + '/test-'+i+'.json'); 59 | } 60 | } catch(e){ } 61 | }); 62 | }); 63 | 64 | }); 65 | 66 | --------------------------------------------------------------------------------