├── .gitignore
├── .npmignore
├── assets
├── button.png
├── rip_jpg.jpg
├── button_active.png
├── button_hover.png
├── platform_left.png
├── platform_mid.png
├── ship_jet_body.png
├── ship_jet_full.png
├── ship_jet_gun.png
├── ship_jet_head.png
├── character_evil.png
├── character_hero.png
├── platform_right.png
├── ship_enemy_body.png
├── ship_enemy_full.png
├── ship_enemy_gun.png
├── ship_enemy_wing.png
├── ship_giant_body.png
├── ship_giant_full.png
├── ship_giant_head.png
├── ship_giant_roof.png
├── ship_jet_engine.png
├── yes_no_maybe_no.gif
├── fx_particle_bomb.png
├── fx_particle_pow_01.png
├── fx_particle_pow_02.png
├── fx_particle_pow_03.png
├── fx_particle_shell.png
├── ship_giant_engine.png
├── ship_giant_floor.png
├── ship_jet_exhaust.png
├── turret_enemy_base.png
├── turret_enemy_full.png
├── turret_enemy_gun.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_smoke_01.png
├── fx_particle_smoke_02.png
├── fx_particle_smoke_03.png
├── fx_particle_engine_01.png
├── fx_particle_engine_02.png
├── fx_particle_engine_03.png
├── fx_particle_engine_04.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
├── test
├── fixtures
│ ├── 50x50.jpg
│ ├── 100x100.jpg
│ ├── 200x200.jpg
│ └── 500x500.jpg
├── index.js
└── generator.js
├── .travis.yml
├── templates
├── css.template
├── kiwi.template
├── easeljs.template
├── starling.template
├── json.template
├── jsonarray.template
└── cocos2d.template
├── example.sh
├── LICENSE
├── package.json
├── lib
├── packing
│ ├── binpacker.js
│ ├── basicpacker.js
│ ├── growingpacker.js
│ └── index.js
├── sorter
│ └── sorter.js
└── generator.js
├── README.md
└── 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 |
--------------------------------------------------------------------------------
/assets/button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/button.png
--------------------------------------------------------------------------------
/assets/rip_jpg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/rip_jpg.jpg
--------------------------------------------------------------------------------
/assets/button_active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/button_active.png
--------------------------------------------------------------------------------
/assets/button_hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/button_hover.png
--------------------------------------------------------------------------------
/assets/platform_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/platform_left.png
--------------------------------------------------------------------------------
/assets/platform_mid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/platform_mid.png
--------------------------------------------------------------------------------
/assets/ship_jet_body.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_jet_body.png
--------------------------------------------------------------------------------
/assets/ship_jet_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_jet_full.png
--------------------------------------------------------------------------------
/assets/ship_jet_gun.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_jet_gun.png
--------------------------------------------------------------------------------
/assets/ship_jet_head.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_jet_head.png
--------------------------------------------------------------------------------
/test/fixtures/50x50.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/test/fixtures/50x50.jpg
--------------------------------------------------------------------------------
/assets/character_evil.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/character_evil.png
--------------------------------------------------------------------------------
/assets/character_hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/character_hero.png
--------------------------------------------------------------------------------
/assets/platform_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/platform_right.png
--------------------------------------------------------------------------------
/assets/ship_enemy_body.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_enemy_body.png
--------------------------------------------------------------------------------
/assets/ship_enemy_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_enemy_full.png
--------------------------------------------------------------------------------
/assets/ship_enemy_gun.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_enemy_gun.png
--------------------------------------------------------------------------------
/assets/ship_enemy_wing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_enemy_wing.png
--------------------------------------------------------------------------------
/assets/ship_giant_body.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_giant_body.png
--------------------------------------------------------------------------------
/assets/ship_giant_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_giant_full.png
--------------------------------------------------------------------------------
/assets/ship_giant_head.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_giant_head.png
--------------------------------------------------------------------------------
/assets/ship_giant_roof.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_giant_roof.png
--------------------------------------------------------------------------------
/assets/ship_jet_engine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_jet_engine.png
--------------------------------------------------------------------------------
/assets/yes_no_maybe_no.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/yes_no_maybe_no.gif
--------------------------------------------------------------------------------
/test/fixtures/100x100.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/test/fixtures/100x100.jpg
--------------------------------------------------------------------------------
/test/fixtures/200x200.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/test/fixtures/200x200.jpg
--------------------------------------------------------------------------------
/test/fixtures/500x500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/test/fixtures/500x500.jpg
--------------------------------------------------------------------------------
/assets/fx_particle_bomb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_bomb.png
--------------------------------------------------------------------------------
/assets/fx_particle_pow_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_pow_01.png
--------------------------------------------------------------------------------
/assets/fx_particle_pow_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_pow_02.png
--------------------------------------------------------------------------------
/assets/fx_particle_pow_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_pow_03.png
--------------------------------------------------------------------------------
/assets/fx_particle_shell.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_shell.png
--------------------------------------------------------------------------------
/assets/ship_giant_engine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_giant_engine.png
--------------------------------------------------------------------------------
/assets/ship_giant_floor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_giant_floor.png
--------------------------------------------------------------------------------
/assets/ship_jet_exhaust.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/ship_jet_exhaust.png
--------------------------------------------------------------------------------
/assets/turret_enemy_base.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/turret_enemy_base.png
--------------------------------------------------------------------------------
/assets/turret_enemy_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/turret_enemy_full.png
--------------------------------------------------------------------------------
/assets/turret_enemy_gun.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/turret_enemy_gun.png
--------------------------------------------------------------------------------
/assets/fx_particle_boom_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_boom_01.png
--------------------------------------------------------------------------------
/assets/fx_particle_boom_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_boom_02.png
--------------------------------------------------------------------------------
/assets/fx_particle_boom_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_boom_03.png
--------------------------------------------------------------------------------
/assets/fx_particle_bullett.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_bullett.png
--------------------------------------------------------------------------------
/assets/fx_particle_crash_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_crash_01.png
--------------------------------------------------------------------------------
/assets/fx_particle_crash_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_crash_02.png
--------------------------------------------------------------------------------
/assets/fx_particle_crash_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_crash_03.png
--------------------------------------------------------------------------------
/assets/fx_particle_crash_04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_crash_04.png
--------------------------------------------------------------------------------
/assets/fx_particle_smoke_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_smoke_01.png
--------------------------------------------------------------------------------
/assets/fx_particle_smoke_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_smoke_02.png
--------------------------------------------------------------------------------
/assets/fx_particle_smoke_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_smoke_03.png
--------------------------------------------------------------------------------
/assets/fx_particle_engine_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_engine_01.png
--------------------------------------------------------------------------------
/assets/fx_particle_engine_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_engine_02.png
--------------------------------------------------------------------------------
/assets/fx_particle_engine_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_engine_03.png
--------------------------------------------------------------------------------
/assets/fx_particle_engine_04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_engine_04.png
--------------------------------------------------------------------------------
/assets/fx_particle_ratata_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_ratata_01.png
--------------------------------------------------------------------------------
/assets/fx_particle_ratata_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_ratata_02.png
--------------------------------------------------------------------------------
/assets/fx_particle_ratata_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_ratata_03.png
--------------------------------------------------------------------------------
/assets/fx_particle_ratata_04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_ratata_04.png
--------------------------------------------------------------------------------
/assets/fx_particle_ratata_05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_ratata_05.png
--------------------------------------------------------------------------------
/assets/fx_particle_ratata_06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_ratata_06.png
--------------------------------------------------------------------------------
/assets/fx_particle_ratata_07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gamefroot/Gamefroot-Texture-Packer/HEAD/assets/fx_particle_ratata_07.png
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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/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/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/starling.template:
--------------------------------------------------------------------------------
1 |
2 | {{#files}}
3 |
4 | {{/files}}
5 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Gamefroot Texture Packer
2 | ==============
3 |
4 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------