├── .gitignore
├── FilesProcessor.js
├── LICENSE.md
├── PackProcessor.js
├── README.md
├── exporters
├── Cocos2d.mst
├── Css.mst
├── Egret2D.mst
├── GodotAtlas.mst
├── GodotTileset.mst
├── JsonArray.mst
├── JsonHash.mst
├── OldCss.mst
├── Phaser3.mst
├── Spine.mst
├── Starling.mst
├── UIKit.mst
├── Unity3D.mst
├── Unreal.mst
├── XML.mst
├── index.js
└── list.json
├── filters
├── Filter.js
├── Grayscale.js
├── Mask.js
└── index.js
├── index.d.ts
├── index.js
├── math
└── Rect.js
├── package.json
├── packers
├── MaxRectsBin.js
├── MaxRectsPacker.js
├── OptimalPacker.js
├── Packer.js
└── index.js
└── utils
├── TextureRenderer.js
└── Trimmer.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # ide
2 | .idea/*
3 |
4 | #node modules
5 | node_modules/*
6 |
7 | package-lock.json
--------------------------------------------------------------------------------
/FilesProcessor.js:
--------------------------------------------------------------------------------
1 | let Jimp = require("jimp");
2 | let PackProcessor = require("./PackProcessor");
3 | let TextureRenderer = require("./utils/TextureRenderer");
4 | let tinify = require("tinify");
5 | let startExporter = require("./exporters/index").startExporter;
6 |
7 | class FilesProcessor {
8 |
9 | static start(images, options, callback, errorCallback) {
10 | PackProcessor.pack(images, options,
11 | (res) => {
12 | let packResult = [];
13 | let resFiles = [];
14 | let readyParts = 0;
15 |
16 | for(let data of res) {
17 | new TextureRenderer(data, options, (renderResult) => {
18 | packResult.push({
19 | data: renderResult.data,
20 | buffer: renderResult.buffer
21 | });
22 |
23 | if(packResult.length >= res.length) {
24 | const suffix = options.suffix;
25 | let ix = options.suffixInitialValue;
26 | for(let item of packResult) {
27 | let fName = options.textureName + (packResult.length > 1 ? suffix + ix : "");
28 |
29 | FilesProcessor.processPackResultItem(fName, item, options, (files) => {
30 | resFiles = resFiles.concat(files);
31 | readyParts++;
32 | if(readyParts >= packResult.length) {
33 | callback(resFiles);
34 | }
35 | });
36 |
37 | ix++;
38 | }
39 | }
40 | });
41 | }
42 | },
43 | (error) => {
44 | if(errorCallback) errorCallback(error);
45 | });
46 | }
47 |
48 | static processPackResultItem(fName, item, options, callback) {
49 | let files = [];
50 |
51 | let pixelFormat = options.textureFormat == "png" ? "RGBA8888" : "RGB888";
52 | let mime = options.textureFormat == "png" ? Jimp.MIME_PNG : Jimp.MIME_JPEG;
53 |
54 | item.buffer.getBuffer(mime, (err, srcBuffer) => {
55 | FilesProcessor.tinifyImage(srcBuffer, options, (buffer) => {
56 | let opts = {
57 | imageName: fName + "." + options.textureFormat,
58 | imageData: buffer.toString("base64"),
59 | format: pixelFormat,
60 | textureFormat: options.textureFormat,
61 | imageWidth: item.buffer.bitmap.width,
62 | imageHeight: item.buffer.bitmap.height,
63 | removeFileExtension: options.removeFileExtension,
64 | prependFolderName: options.prependFolderName,
65 | base64Export: options.base64Export,
66 | scale: options.scale,
67 | appInfo: options.appInfo,
68 | trimMode: options.trimMode
69 | };
70 |
71 | files.push({
72 | name: fName + "." + options.exporter.fileExt,
73 | buffer: Buffer.from(startExporter(options.exporter, item.data, opts))
74 | });
75 |
76 | if(!options.base64Export) {
77 | files.push({
78 | name: fName + "." + options.textureFormat,
79 | buffer: buffer
80 | });
81 | }
82 |
83 | callback(files);
84 | });
85 | });
86 | }
87 |
88 | static tinifyImage(buffer, options, callback) {
89 | if(!options.tinify) {
90 | callback(buffer);
91 | return;
92 | }
93 |
94 | tinify.key = options.tinifyKey;
95 |
96 | tinify.fromBuffer(buffer).toBuffer(function(err, result) {
97 | if (err) throw err;
98 | callback(result);
99 | });
100 | }
101 | }
102 |
103 | module.exports = FilesProcessor;
104 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Alexander Norinchak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/PackProcessor.js:
--------------------------------------------------------------------------------
1 | let MaxRectsBinPack = require('./packers/MaxRectsBin');
2 | let OptimalPacker = require('./packers/OptimalPacker');
3 | let allPackers = require('./packers').list;
4 | let Trimmer = require('./utils/Trimmer');
5 | let TextureRenderer = require('./utils/TextureRenderer');
6 |
7 | class PackProcessor {
8 |
9 | static detectIdentical(rects) {
10 |
11 | let identical = [];
12 |
13 | for (let i = 0; i < rects.length; i++) {
14 | let rect1 = rects[i];
15 | for (let n = i + 1; n < rects.length; n++) {
16 | let rect2 = rects[n];
17 | if (rect1.image._base64 == rect2.image._base64 && identical.indexOf(rect2) < 0) {
18 | rect2.identical = rect1;
19 | identical.push(rect2);
20 | }
21 | }
22 | }
23 |
24 | for (let rect of identical) {
25 | rects.splice(rects.indexOf(rect), 1);
26 | }
27 |
28 | return {
29 | rects: rects,
30 | identical: identical
31 | }
32 | }
33 |
34 | static applyIdentical(rects, identical) {
35 | let clones = [];
36 | let removeIdentical = [];
37 |
38 | for (let item of identical) {
39 | let ix = rects.indexOf(item.identical);
40 | if (ix >= 0) {
41 | let rect = rects[ix];
42 |
43 | let clone = Object.assign({}, rect);
44 |
45 | clone.name = item.name;
46 | clone.image = item.image;
47 | clone.skipRender = true;
48 |
49 | removeIdentical.push(item);
50 | clones.push(clone);
51 | }
52 | }
53 |
54 | for (let item of removeIdentical) {
55 | identical.splice(identical.indexOf(item), 1);
56 | }
57 |
58 | for (let item of clones) {
59 | item.cloned = true;
60 | rects.push(item);
61 | }
62 |
63 | return rects;
64 | }
65 |
66 | static pack(images = {}, options = {}, onComplete = null, onError = null) {
67 |
68 | let rects = [];
69 |
70 | let padding = options.padding || 0;
71 | let extrude = options.extrude || 0;
72 |
73 | let maxWidth = 0, maxHeight = 0;
74 | let minWidth = 0, minHeight = 0;
75 |
76 | let alphaThreshold = options.alphaThreshold || 0;
77 | if (alphaThreshold > 255) alphaThreshold = 255;
78 |
79 | let names = Object.keys(images).sort();
80 |
81 | for (let key of names) {
82 | let img = images[key];
83 |
84 | maxWidth += img.width;
85 | maxHeight += img.height;
86 |
87 | if (img.width > minWidth) minWidth = img.width + padding * 2 + extrude * 2;
88 | if (img.height > minHeight) minHeight = img.height + padding * 2 + extrude * 2;
89 |
90 | rects.push({
91 | frame: { x: 0, y: 0, w: img.width, h: img.height },
92 | rotated: false,
93 | trimmed: false,
94 | spriteSourceSize: { x: 0, y: 0, w: img.width, h: img.height },
95 | sourceSize: { w: img.width, h: img.height },
96 | name: key,
97 | image: img
98 | });
99 | }
100 |
101 | let width = options.width || 0;
102 | let height = options.height || 0;
103 |
104 | if (!width) width = maxWidth;
105 | if (!height) height = maxHeight;
106 |
107 | if (options.powerOfTwo) {
108 | let sw = Math.round(Math.log(width) / Math.log(2));
109 | let sh = Math.round(Math.log(height) / Math.log(2));
110 |
111 | let pw = Math.pow(2, sw);
112 | let ph = Math.pow(2, sh);
113 |
114 | if (pw < width) pw = Math.pow(2, sw + 1);
115 | if (ph < height) ph = Math.pow(2, sh + 1);
116 |
117 | width = pw;
118 | height = ph;
119 | }
120 |
121 | if (width < minWidth || height < minHeight) {
122 | if (onError) onError({
123 | description: "Invalid size. Min: " + minWidth + "x" + minHeight
124 | });
125 | return;
126 | }
127 |
128 | if (options.allowTrim) {
129 | Trimmer.trim(rects, alphaThreshold);
130 | }
131 |
132 | for (let item of rects) {
133 | item.frame.w += padding * 2 + extrude * 2;
134 | item.frame.h += padding * 2 + extrude * 2;
135 | }
136 |
137 | let identical = [];
138 |
139 | if (options.detectIdentical) {
140 | let res = PackProcessor.detectIdentical(rects);
141 |
142 | rects = res.rects;
143 | identical = res.identical;
144 | }
145 |
146 | let getAllPackers = () => {
147 | let methods = [];
148 | for (let packerClass of allPackers) {
149 | if (packerClass !== OptimalPacker) {
150 | for (let method in packerClass.methods) {
151 | methods.push({ packerClass, packerMethod: packerClass.methods[method], allowRotation: false });
152 | if (options.allowRotation) {
153 | methods.push({ packerClass, packerMethod: packerClass.methods[method], allowRotation: true });
154 | }
155 | }
156 | }
157 | }
158 | return methods;
159 | };
160 |
161 | let packerClass = options.packer || MaxRectsBinPack;
162 | let packerMethod = options.packerMethod || MaxRectsBinPack.methods.BestShortSideFit;
163 | let packerCombos = (packerClass === OptimalPacker) ? getAllPackers() : [{ packerClass, packerMethod, allowRotation: options.allowRotation }];
164 |
165 | let optimalRes;
166 | let optimalSheets = Infinity;
167 | let optimalEfficiency = 0;
168 |
169 | let sourceArea = 0;
170 | for (let rect of rects) {
171 | sourceArea += rect.sourceSize.w * rect.sourceSize.h;
172 | }
173 |
174 | for (let combo of packerCombos) {
175 | let res = [];
176 | let sheetArea = 0;
177 |
178 | // duplicate rects if more than 1 combo since the array is mutated in pack()
179 | let _rects = packerCombos.length > 1 ? rects.map(rect => {
180 | return Object.assign({}, rect, {
181 | frame: Object.assign({}, rect.frame),
182 | spriteSourceSize: Object.assign({}, rect.spriteSourceSize),
183 | sourceSize: Object.assign({}, rect.sourceSize)
184 | });
185 | }) : rects;
186 |
187 | // duplicate identical if more than 1 combo and fix references to point to the
188 | // cloned rects since the array is mutated in applyIdentical()
189 | let _identical = packerCombos.length > 1 ? identical.map(rect => {
190 | for (let rect2 of _rects) {
191 | if (rect.identical.image._base64 == rect2.image._base64) {
192 | return Object.assign({}, rect, { identical: rect2 });
193 | }
194 | }
195 | }) : identical;
196 |
197 | while (_rects.length) {
198 | let packer = new combo.packerClass(width, height, combo.allowRotation);
199 | let result = packer.pack(_rects, combo.packerMethod);
200 |
201 | for (let item of result) {
202 | item.frame.x += padding + extrude;
203 | item.frame.y += padding + extrude;
204 | item.frame.w -= padding * 2 + extrude * 2;
205 | item.frame.h -= padding * 2 + extrude * 2;
206 | }
207 |
208 | if (options.detectIdentical) {
209 | result = PackProcessor.applyIdentical(result, _identical);
210 | }
211 |
212 | res.push(result);
213 |
214 | for (let item of result) {
215 | this.removeRect(_rects, item.name);
216 | }
217 |
218 | let { width: sheetWidth, height: sheetHeight } = TextureRenderer.getSize(result, options);
219 | sheetArea += sheetWidth * sheetHeight;
220 | }
221 |
222 | let sheets = res.length;
223 | let efficiency = sourceArea / sheetArea;
224 |
225 | if (sheets < optimalSheets || (sheets === optimalSheets && efficiency > optimalEfficiency)) {
226 | optimalRes = res;
227 | optimalSheets = sheets;
228 | optimalEfficiency = efficiency;
229 | }
230 | }
231 |
232 | if (onComplete) {
233 | onComplete(optimalRes);
234 | }
235 | }
236 |
237 | static removeRect(rects, name) {
238 | for (let i = 0; i < rects.length; i++) {
239 | if (rects[i].name == name) {
240 | rects.splice(i, 1);
241 | return;
242 | }
243 | }
244 | }
245 | }
246 |
247 | module.exports = PackProcessor;
248 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # free-tex-packer-core
2 | [](https://www.npmjs.com/package/free-tex-packer-core) \
3 | Core Free texture packer module
4 |
5 | # Install
6 |
7 | $ npm install free-tex-packer-core
8 |
9 | # Basic usage
10 | ```js
11 | let texturePacker = require("free-tex-packer-core");
12 |
13 | let images = [];
14 |
15 | images.push({path: "img1.png", contents: fs.readFileSync("./img1.png")});
16 | images.push({path: "img2.png", contents: fs.readFileSync("./img2.png")});
17 | images.push({path: "img3.png", contents: fs.readFileSync("./img3.png")});
18 |
19 | texturePacker(images, null, (files, error) => {
20 | if (error) {
21 | console.error('Packaging failed', error);
22 | } else {
23 | for(let item of files) {
24 | console.log(item.name, item.buffer);
25 | }
26 | }
27 | });
28 |
29 | ```
30 |
31 | ## Asynchronous usage
32 | ### Async/await
33 | ```js
34 | const { packAsync } = require('free-tex-packer-core');
35 |
36 | const images = [
37 | {path: "img1.png", contents: fs.readFileSync("./img1.png")},
38 | {path: "img2.png", contents: fs.readFileSync("./img2.png")},
39 | {path: "img3.png", contents: fs.readFileSync("./img3.png")}
40 | ];
41 |
42 | async function packImages() {
43 | try {
44 | const files = await packAsync(images, null);
45 | for(let item of files) {
46 | console.log(item.name, item.buffer);
47 | }
48 | }
49 | catch(error) {
50 | console.log(error);
51 | }
52 | }
53 | ```
54 | ### Promises
55 | ```js
56 | function packImages() {
57 | packAsync(images, null)
58 | .then((files) => {
59 | for(let item of files) {
60 | console.log(item.name, item.buffer);
61 | }
62 | })
63 | .catch((error) => console.log(error));
64 | }
65 | ```
66 |
67 | # Advanced usage
68 |
69 | Use packer options object
70 |
71 | ```js
72 | let texturePacker = require("free-tex-packer-core");
73 |
74 | let options = {
75 | textureName: "my-texture",
76 | width: 1024,
77 | height: 1024,
78 | fixedSize: false,
79 | padding: 2,
80 | allowRotation: true,
81 | detectIdentical: true,
82 | allowTrim: true,
83 | exporter: "Pixi",
84 | removeFileExtension: true,
85 | prependFolderName: true
86 | };
87 |
88 | let images = [];
89 |
90 | images.push({path: "img1.png", contents: fs.readFileSync("./img1.png")});
91 | images.push({path: "img2.png", contents: fs.readFileSync("./img2.png")});
92 | images.push({path: "img3.png", contents: fs.readFileSync("./img3.png")});
93 |
94 | texturePacker(images, options, (files, error) => {
95 | if (error) {
96 | console.error('Packaging failed', error);
97 | } else {
98 | for(let item of files) {
99 | console.log(item.name, item.buffer);
100 | }
101 | }
102 | });
103 | ```
104 |
105 | # Available options
106 |
107 | * `textureName` - name of output files. Default: **pack-result**
108 | * `suffix` - the suffix used for multiple sprites. Default: **-**
109 | * `suffixInitialValue` - the initial value of the suffix. Default: **0**
110 | * `width` - max single texture width. Default: **2048**
111 | * `height` - max single texture height. Default: **2048**
112 | * `fixedSize` - fix texture size. Default: **false**
113 | * `powerOfTwo` - force power of two textures sizes. Default: **false**
114 | * `padding` - spaces in pixels around images. Default: **0**
115 | * `extrude` - extrude border pixels size around images. Default: **0**
116 | * `allowRotation` - allow image rotation. Default: **true**
117 | * `detectIdentical` - allow detect identical images. Default: **true**
118 | * `allowTrim` - allow trim images. Default: **true**
119 | * `trimMode` - trim or crop. Default: **trim**
120 | * `alphaThreshold` - threshold alpha value. Default: **0**
121 | * `removeFileExtension` - remove file extensions from frame names. Default: **false**
122 | * `prependFolderName` - prepend folder name to frame names. Default: **true**
123 | * `textureFormat` - output file format (png or jpg). Default: **png**
124 | * `base64Export` - export texture as base64 string to atlas meta tag. Default: **false**
125 | * `scale` - scale size and positions in atlas. Default: **1**
126 | * `scaleMethod` - texture scaling method (BILINEAR, NEAREST_NEIGHBOR, BICUBIC, HERMITE, BEZIER). Default: **BILINEAR**
127 | * `tinify` - tinify texture using [TinyPNG](https://tinypng.com/). Default: **false**
128 | * `tinifyKey` - [TinyPNG key](https://tinypng.com/developers). Default: **""**
129 | * `packer` - type of packer (MaxRectsBin, MaxRectsPacker or OptimalPacker). Default: **MaxRectsBin**, recommended **OptimalPacker**
130 | * `packerMethod` - name of pack method (MaxRectsBin: BestShortSideFit, BestLongSideFit, BestAreaFit, BottomLeftRule, ContactPointRule. MaxRectsPacker: Smart, Square, SmartSquare, SmartArea). Default: **BestShortSideFit**
131 | * `exporter` - name of predefined exporter (JsonHash, JsonArray, Css, OldCss, Pixi, GodotAtlas, GodotTileset, PhaserHash, PhaserArray, Phaser3, XML, Starling, Cocos2d, Spine, Unreal, UIKit, Unity3D, Egret2D), or custom exporter (see below). Default: **JsonHash**
132 | * `filter` - name of bitmap filter (grayscale, mask or none). Default: **none**
133 | * `appInfo` - external app info. Required fields: url and version. Default: **null**
134 |
135 | # Custom exporter
136 |
137 | Exporter property can be object. Fields:
138 |
139 | * `fileExt` - files extension
140 | * `template` - path to template file or
141 | * `content` - content of template
142 |
143 | Free texture packer uses [mustache](http://mustache.github.io/) template engine.
144 |
145 | There are 3 objects passed to template:
146 |
147 | **rects** (Array) list of sprites for export
148 |
149 | | prop | type | description |
150 | | --- | --- | --- |
151 | | name | String | sprite name |
152 | | frame | Object | frame info (x, y, w, h, hw, hh) |
153 | | rotated | Boolean | sprite rotation flag |
154 | | trimmed | Boolean | sprite trimmed flag |
155 | | spriteSourceSize | Object | sprite source size (x, y, w, h) |
156 | | sourceSize | Object | original size (w, h) |
157 | | first | Boolean | first element in array flag |
158 | | last | Boolean | last element in array flag |
159 |
160 | **config** (Object) current export config
161 |
162 | | prop | type | description |
163 | | --- | --- | --- |
164 | | imageWidth | Number | texture width |
165 | | imageHeight | Number | texture height |
166 | | scale | Number | texture scale |
167 | | format | String | texture format |
168 | | imageName | String | texture name |
169 | | base64Export | Boolean | base64 export flag |
170 | | base64Prefix | String | prefix for base64 string |
171 | | imageData | String | base64 image data |
172 |
173 | **appInfo** (Object) application info
174 |
175 | | prop | type | description |
176 | | --- | --- | --- |
177 | | displayName | String | App name |
178 | | version | String | App version |
179 | | url | String | App url |
180 |
181 | **Template example:**
182 | ```
183 | {
184 | "frames": {
185 | {{#rects}}
186 | "{{{name}}}": {
187 | "frame": {
188 | "x": {{frame.x}},
189 | "y": {{frame.y}},
190 | "w": {{frame.w}},
191 | "h": {{frame.h}}
192 | },
193 | "rotated": {{rotated}},
194 | "trimmed": {{trimmed}},
195 | "spriteSourceSize": {
196 | "x": {{spriteSourceSize.x}},
197 | "y": {{spriteSourceSize.y}},
198 | "w": {{spriteSourceSize.w}},
199 | "h": {{spriteSourceSize.h}}
200 | },
201 | "sourceSize": {
202 | "w": {{sourceSize.w}},
203 | "h": {{sourceSize.h}}
204 | },
205 | "pivot": {
206 | "x": 0.5,
207 | "y": 0.5
208 | }
209 | }{{^last}},{{/last}}
210 | {{/rects}}
211 | },
212 | "meta": {
213 | "app": "{{{appInfo.url}}}",
214 | "version": "{{appInfo.version}}",
215 | "image": "{{config.imageName}}",
216 | "format": "{{config.format}}",
217 | "size": {
218 | "w": {{config.imageWidth}},
219 | "h": {{config.imageHeight}}
220 | },
221 | "scale": {{config.scale}}
222 | }
223 | }
224 | ```
225 |
226 | **Custom template usage example**
227 |
228 | ```js
229 | let texturePacker = require("free-tex-packer-core");
230 |
231 | let images = [];
232 |
233 | images.push({path: "img1.png", contents: fs.readFileSync("./img1.png")});
234 | images.push({path: "img2.png", contents: fs.readFileSync("./img2.png")});
235 | images.push({path: "img3.png", contents: fs.readFileSync("./img3.png")});
236 |
237 | let exporter = {
238 | fileExt: "json",
239 | template: "./MyTemplate.mst"
240 | };
241 |
242 | texturePacker(images, {exporter: exporter}, (files, error) => {
243 | if (error) {
244 | console.error('Packaging failed', error);
245 | } else {
246 | for(let item of files) {
247 | console.log(item.name, item.buffer);
248 | }
249 | }
250 | });
251 | ```
252 |
253 | # Used libs
254 |
255 | * **Jimp** - https://github.com/oliver-moran/jimp
256 | * **mustache.js** - https://github.com/janl/mustache.js/
257 | * **tinify** - https://github.com/tinify/tinify-nodejs
258 | * **MaxRectsPacker** - https://github.com/soimy/maxrects-packer
259 |
260 | ---
261 | License: MIT
262 |
--------------------------------------------------------------------------------
/exporters/Cocos2d.mst:
--------------------------------------------------------------------------------
1 | {{=<% %>=}}
2 |
3 |
4 |
5 |
6 | frames
7 |
8 | <%#rects%>
9 | <%&name%>
10 |
11 | frame
12 | {{<%frame.x%>,<%frame.y%>},{<%frame.w%>,<%frame.h%>}}
13 | offset
14 | {<% spriteSourceSize.x | offsetLeft : spriteSourceSize.w : sourceSize.w %>,<% spriteSourceSize.y | offsetRight : spriteSourceSize.h : sourceSize.h %>}
15 | rotated
16 | <<%rotated%>/>
17 | sourceColorRect
18 | {{<%spriteSourceSize.x%>,<%spriteSourceSize.y%>},{<%spriteSourceSize.w%>,<%spriteSourceSize.h%>}}
19 | sourceSize
20 | {<%sourceSize.w%>,<%sourceSize.h%>}
21 |
22 | <%/rects%>
23 |
24 | metadata
25 |
26 | format
27 | 2
28 | pixelFormat
29 | <%config.format%>
30 | premultiplyAlpha
31 |
32 | realTextureFileName
33 | <%config.imageFile%>
34 | size
35 | {<%config.imageWidth%>,<%config.imageHeight%>}
36 | textureFileName
37 | <%config.imageName%>
38 |
39 |
40 |
41 | <%={{ }}=%>
--------------------------------------------------------------------------------
/exporters/Css.mst:
--------------------------------------------------------------------------------
1 | /*
2 | ---------------------------
3 | created with {{appInfo.displayName}} v{{appInfo.version}}
4 | {{{appInfo.url}}}
5 | ---------------------------
6 | */
7 | {{#rects}}
8 | .{{{name}}} { display:inline-block;overflow:hidden;background:url({{config.imageName}}) no-repeat -{{frame.x}}px -{{frame.y}}px;{{^rotated}}width:{{frame.w}}px;height:{{frame.h}}px;{{/rotated}}{{#rotated}}width:{{frame.h}}px;height:{{frame.w}}px;transform-origin:{{frame.hw}}px {{frame.hh}}px;-moz-transform-origin:{{frame.hw}}px {{frame.hh}}px;-ms-transform-origin:{{frame.hw}}px {{frame.hh}}px;-webkit-transform-origin:{{frame.hw}}px {{frame.hh}}px;-o-transform-origin:{{frame.hw}}px {{frame.hh}}px;transform:rotate(-90deg);-moz-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-webkit-transform:rotate(-90deg);-o-transform:rotate(-90deg);{{/rotated}}{{#trimmed}}margin-left:{{spriteSourceSize.x}}px;margin-top:{{spriteSourceSize.y}}px{{/trimmed}} }
9 | {{/rects}}
--------------------------------------------------------------------------------
/exporters/Egret2D.mst:
--------------------------------------------------------------------------------
1 | {
2 | "file": "{{config.imageName}}",
3 | "frames": {
4 | {{#rects}}
5 | "{{{name}}}": {
6 | "x": {{frame.x}},
7 | "y": {{frame.y}},
8 | "w": {{frame.w}},
9 | "h": {{frame.h}},
10 | "hw": {{frame.hw}},
11 | "hh": {{frame.hh}}
12 | }{{^last}},{{/last}}
13 | {{/rects}}
14 | }
15 | }
--------------------------------------------------------------------------------
/exporters/GodotAtlas.mst:
--------------------------------------------------------------------------------
1 | {
2 | "textures": [
3 | {
4 | "image": "{{config.imageFile}}",
5 | "size": {
6 | "w": {{config.imageWidth}},
7 | "h": {{config.imageHeight}}
8 | },
9 | "sprites": [
10 | {{#rects}}
11 | {
12 | "filename": "{{{name}}}",
13 | "region": {
14 | "x": {{frame.x}},
15 | "y": {{frame.y}},
16 | "w": {{frame.w}},
17 | "h": {{frame.h}}
18 | },
19 | "margin": {
20 | "x": 0,
21 | "y": 0,
22 | "w": 0,
23 | "h": 0
24 | }
25 | }{{^last}},{{/last}}
26 | {{/rects}}
27 | ]
28 | }
29 | ],
30 | "meta": {
31 | "app": "{{{appInfo.url}}}",
32 | "version": "{{appInfo.version}}",
33 | "format": "{{config.format}}",
34 | }
35 | }
--------------------------------------------------------------------------------
/exporters/GodotTileset.mst:
--------------------------------------------------------------------------------
1 | {
2 | "textures": [
3 | {
4 | "image": "{{config.imageFile}}",
5 | "size": {
6 | "w": {{config.imageWidth}},
7 | "h": {{config.imageHeight}}
8 | },
9 | "sprites": [
10 | {{#rects}}
11 | {
12 | "filename": "{{{name}}}",
13 | "region": {
14 | "x": {{frame.x}},
15 | "y": {{frame.y}},
16 | "w": {{frame.w}},
17 | "h": {{frame.h}}
18 | },
19 | "margin": {
20 | "x": 0,
21 | "y": 0,
22 | "w": 0,
23 | "h": 0
24 | }
25 | }{{^last}},{{/last}}
26 | {{/rects}}
27 | ]
28 | }
29 | ],
30 | "meta": {
31 | "app": "{{{appInfo.url}}}",
32 | "version": "{{appInfo.version}}",
33 | "format": "{{config.format}}",
34 | }
35 | }
--------------------------------------------------------------------------------
/exporters/JsonArray.mst:
--------------------------------------------------------------------------------
1 | {
2 | "frames": [
3 | {{#rects}}
4 | {
5 | "filename": "{{{name}}}",
6 | "frame": {
7 | "x": {{frame.x}},
8 | "y": {{frame.y}},
9 | "w": {{frame.w}},
10 | "h": {{frame.h}}
11 | },
12 | "rotated": {{rotated}},
13 | "trimmed": {{trimmed}},
14 | "spriteSourceSize": {
15 | "x": {{spriteSourceSize.x}},
16 | "y": {{spriteSourceSize.y}},
17 | "w": {{spriteSourceSize.w}},
18 | "h": {{spriteSourceSize.h}}
19 | },
20 | "sourceSize": {
21 | "w": {{sourceSize.w}},
22 | "h": {{sourceSize.h}}
23 | },
24 | "pivot": {
25 | "x": 0.5,
26 | "y": 0.5
27 | }
28 | }{{^last}},{{/last}}
29 | {{/rects}}
30 | ],
31 | "meta": {
32 | "app": "{{{appInfo.url}}}",
33 | "version": "{{appInfo.version}}",
34 | "image": "{{^config.base64Export}}{{config.imageName}}{{/config.base64Export}}{{#config.base64Export}}{{{config.base64Prefix}}}{{{config.imageData}}}{{/config.base64Export}}",
35 | "format": "{{config.format}}",
36 | "size": {
37 | "w": {{config.imageWidth}},
38 | "h": {{config.imageHeight}}
39 | },
40 | "scale": {{config.scale}}
41 | }
42 | }
--------------------------------------------------------------------------------
/exporters/JsonHash.mst:
--------------------------------------------------------------------------------
1 | {
2 | "frames": {
3 | {{#rects}}
4 | "{{{name}}}": {
5 | "frame": {
6 | "x": {{frame.x}},
7 | "y": {{frame.y}},
8 | "w": {{frame.w}},
9 | "h": {{frame.h}}
10 | },
11 | "rotated": {{rotated}},
12 | "trimmed": {{trimmed}},
13 | "spriteSourceSize": {
14 | "x": {{spriteSourceSize.x}},
15 | "y": {{spriteSourceSize.y}},
16 | "w": {{spriteSourceSize.w}},
17 | "h": {{spriteSourceSize.h}}
18 | },
19 | "sourceSize": {
20 | "w": {{sourceSize.w}},
21 | "h": {{sourceSize.h}}
22 | },
23 | "pivot": {
24 | "x": 0.5,
25 | "y": 0.5
26 | }
27 | }{{^last}},{{/last}}
28 | {{/rects}}
29 | },
30 | "meta": {
31 | "app": "{{{appInfo.url}}}",
32 | "version": "{{appInfo.version}}",
33 | "image": "{{^config.base64Export}}{{config.imageName}}{{/config.base64Export}}{{#config.base64Export}}{{{config.base64Prefix}}}{{{config.imageData}}}{{/config.base64Export}}",
34 | "format": "{{config.format}}",
35 | "size": {
36 | "w": {{config.imageWidth}},
37 | "h": {{config.imageHeight}}
38 | },
39 | "scale": {{config.scale}}
40 | }
41 | }
--------------------------------------------------------------------------------
/exporters/OldCss.mst:
--------------------------------------------------------------------------------
1 | /*
2 | ---------------------------
3 | created with {{appInfo.displayName}} v{{appInfo.version}}
4 | {{{appInfo.url}}}
5 | ---------------------------
6 | */
7 | {{#rects}}
8 | .{{{name}}} { display:inline-block;overflow:hidden;background:url({{config.imageName}}) no-repeat -{{frame.x}}px -{{frame.y}}px;width:{{frame.w}}px;height:{{frame.h}}px }
9 | {{/rects}}
--------------------------------------------------------------------------------
/exporters/Phaser3.mst:
--------------------------------------------------------------------------------
1 | {
2 | "textures": [
3 | {
4 | "image": "{{^config.base64Export}}{{config.imageName}}{{/config.base64Export}}{{#config.base64Export}}{{{config.base64Prefix}}}{{{config.imageData}}}{{/config.base64Export}}",
5 | "format": "{{config.format}}",
6 | "size": {
7 | "w": {{config.imageWidth}},
8 | "h": {{config.imageHeight}}
9 | },
10 | "scale": {{config.scale}},
11 | "frames": [
12 | {{#rects}}
13 | {
14 | "filename": "{{{name}}}",
15 | "rotated": {{rotated}},
16 | "trimmed": {{trimmed}},
17 | "sourceSize": {
18 | "w": {{sourceSize.w}},
19 | "h": {{sourceSize.h}}
20 | },
21 | "spriteSourceSize": {
22 | "x": {{spriteSourceSize.x}},
23 | "y": {{spriteSourceSize.y}},
24 | "w": {{spriteSourceSize.w}},
25 | "h": {{spriteSourceSize.h}}
26 | },
27 | "frame": {
28 | "x": {{frame.x}},
29 | "y": {{frame.y}},
30 | "w": {{frame.w}},
31 | "h": {{frame.h}}
32 | }
33 | }{{^last}},{{/last}}
34 | {{/rects}}
35 | ]
36 | }
37 | ],
38 | "meta": {
39 | "app": "{{{appInfo.url}}}",
40 | "version": "{{appInfo.version}}"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/exporters/Spine.mst:
--------------------------------------------------------------------------------
1 |
2 | {{config.imageName}}
3 | size: {{config.imageWidth}},{{config.imageHeight}}
4 | format: {{config.format}}
5 | filter: Nearest,Nearest
6 | repeat: none
7 | {{#rects}}
8 | {{{name}}}
9 | rotate: {{rotated}}
10 | xy: {{frame.x}},{{frame.y}}
11 | size: {{frame.w}},{{frame.h}}
12 | orig: {{sourceSize.w}},{{sourceSize.h}}
13 | offset: 0,0
14 | index: -1
15 | {{/rects}}
--------------------------------------------------------------------------------
/exporters/Starling.mst:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | {{#rects}}
7 |
8 | {{/rects}}
9 |
--------------------------------------------------------------------------------
/exporters/UIKit.mst:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 | frames
12 |
13 | {{#rects}}
14 | {{{name}}}
15 |
16 | x{{frame.x}}
17 | y{{frame.y}}
18 | w{{frame.w}}
19 | h{{frame.h}}
20 | oX{{spriteSourceSize.x}}
21 | oY{{spriteSourceSize.y}}
22 | oW{{sourceSize.w}}
23 | oH{{sourceSize.h}}
24 |
25 | {{/rects}}
26 |
27 |
28 | meta
29 |
30 | image
31 | {{config.imageName}}
32 | width
33 | {{config.imageWidth}}
34 | height
35 | {{config.imageHeight}}
36 |
37 |
38 |
--------------------------------------------------------------------------------
/exporters/Unity3D.mst:
--------------------------------------------------------------------------------
1 | #
2 | # Sprite sheet data for Unity.
3 | #
4 | # To import these sprites into your Unity project, download "TexturePackerImporter":
5 | # https://assetstore.unity.com/packages/tools/sprite-management/texturepacker-importer-16641
6 | #
7 | # created with {{appInfo.displayName}} v{{appInfo.version}}
8 | # {{{appInfo.url}}}
9 | #
10 | :format=40300
11 | :texture={{config.imageName}}
12 | :size={{config.imageWidth}}x{{config.imageHeight}}
13 | :pivotpoints=enabled
14 | :borders=disabled
15 |
16 | {{#rects}}
17 | {{{name | escapeName}}};{{frame.x}};{{frame.y | mirror : frame.h : config.imageHeight}};{{frame.w}};{{frame.h}}; 0.5;0.5; 0;0;0;0
18 | {{/rects}}
--------------------------------------------------------------------------------
/exporters/Unreal.mst:
--------------------------------------------------------------------------------
1 | {
2 | "frames": {
3 | {{#rects}}
4 | "{{{name}}}": {
5 | "frame": {
6 | "x": {{frame.x}},
7 | "y": {{frame.y}},
8 | "w": {{frame.w}},
9 | "h": {{frame.h}}
10 | },
11 | "rotated": {{rotated}},
12 | "trimmed": {{trimmed}},
13 | "spriteSourceSize": {
14 | "x": {{spriteSourceSize.x}},
15 | "y": {{spriteSourceSize.y}},
16 | "w": {{spriteSourceSize.w}},
17 | "h": {{spriteSourceSize.h}}
18 | },
19 | "sourceSize": {
20 | "w": {{sourceSize.w}},
21 | "h": {{sourceSize.h}}
22 | },
23 | "pivot": {
24 | "x": 0.5,
25 | "y": 0.5
26 | }
27 | }{{^last}},{{/last}}
28 | {{/rects}}
29 | },
30 | "meta": {
31 | "app": "{{{appInfo.url}}}",
32 | "version": "{{appInfo.version}}",
33 | "image": "{{^config.base64Export}}{{config.imageName}}{{/config.base64Export}}{{#config.base64Export}}{{{config.base64Prefix}}}{{{config.imageData}}}{{/config.base64Export}}",
34 | "format": "{{config.format}}",
35 | "size": {
36 | "w": {{config.imageWidth}},
37 | "h": {{config.imageHeight}}
38 | },
39 | "scale": {{config.scale}},
40 | "target": "paper2d"
41 | }
42 | }
--------------------------------------------------------------------------------
/exporters/XML.mst:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 | {{#rects}}
19 |
20 | {{/rects}}
21 |
--------------------------------------------------------------------------------
/exporters/index.js:
--------------------------------------------------------------------------------
1 | let list = require("./list.json");
2 | let appInfo = require("../package.json");
3 | let mustache = require("mustache");
4 | let fs = require("fs");
5 | let path = require("path");
6 | let wax = require("@jvitela/mustache-wax");
7 |
8 | wax(mustache);
9 |
10 | mustache.Formatters = {
11 | add: (v1, v2) => {
12 | return v1 + v2;
13 | },
14 | subtract: (v1, v2) => {
15 | return v1 - v2;
16 | },
17 | multiply: (v1, v2) => {
18 | return v1 * v2;
19 | },
20 | divide: (v1, v2) => {
21 | return v1 / v2;
22 | },
23 | offsetLeft: (start, size1, size2) => {
24 | let x1 = start + size1 / 2;
25 | let x2 = size2 / 2;
26 | return x1 - x2;
27 | },
28 | offsetRight: (start, size1, size2) => {
29 | let x1 = start + size1 / 2;
30 | let x2 = size2 / 2;
31 | return x2 - x1;
32 | },
33 | mirror: (start, size1, size2) => {
34 | return size2 - start - size1;
35 | },
36 | escapeName: (name) => {
37 | return name
38 | .replace(/%/g, "%25")
39 | .replace(/#/g, "%23")
40 | .replace(/:/g, "%3A")
41 | .replace(/;/g, "%3B")
42 | .replace(/\\/g, "-")
43 | .replace(/\//g, "-");
44 | },
45 | };
46 |
47 | function getExporterByType(type) {
48 | type = type.toLowerCase();
49 |
50 | for (let item of list) {
51 | if (item.type.toLowerCase() == type) {
52 | return item;
53 | }
54 | }
55 | return null;
56 | }
57 |
58 | function prepareData(data, options) {
59 | let opt = Object.assign({}, options);
60 |
61 | opt.imageName = opt.imageName || "texture.png";
62 | opt.format = opt.format || "RGBA8888";
63 | opt.scale = opt.scale || 1;
64 | opt.base64Prefix =
65 | options.textureFormat == "png"
66 | ? "data:image/png;base64,"
67 | : "data:image/jpeg;base64,";
68 |
69 | let ret = data.map((item, index) => {
70 | let name = item.name;
71 |
72 | if (options.trimSpriteNames) {
73 | name.trim();
74 | }
75 |
76 | if (options.removeFileExtension) {
77 | let parts = name.split(".");
78 | parts.pop();
79 | name = parts.join(".");
80 | }
81 |
82 | if (!options.prependFolderName) {
83 | name = name.split("/").pop();
84 | }
85 |
86 | let frame = {
87 | x: item.frame.x,
88 | y: item.frame.y,
89 | w: item.frame.w,
90 | h: item.frame.h,
91 | hw: item.frame.w / 2,
92 | hh: item.frame.h / 2,
93 | };
94 | let spriteSourceSize = {
95 | x: item.spriteSourceSize.x,
96 | y: item.spriteSourceSize.y,
97 | w: item.spriteSourceSize.w,
98 | h: item.spriteSourceSize.h,
99 | };
100 | let sourceSize = { w: item.sourceSize.w, h: item.sourceSize.h };
101 |
102 | let trimmed = item.trimmed;
103 |
104 | if (item.trimmed && options.trimMode === "crop") {
105 | trimmed = false;
106 | spriteSourceSize.x = 0;
107 | spriteSourceSize.y = 0;
108 | sourceSize.w = spriteSourceSize.w;
109 | sourceSize.h = spriteSourceSize.h;
110 | }
111 |
112 | if (opt.scale !== 1) {
113 | frame.x *= opt.scale;
114 | frame.y *= opt.scale;
115 | frame.w *= opt.scale;
116 | frame.h *= opt.scale;
117 | frame.hw *= opt.scale;
118 | frame.hh *= opt.scale;
119 |
120 | spriteSourceSize.x *= opt.scale;
121 | spriteSourceSize.y *= opt.scale;
122 | spriteSourceSize.w *= opt.scale;
123 | spriteSourceSize.h *= opt.scale;
124 |
125 | sourceSize.w *= opt.scale;
126 | sourceSize.h *= opt.scale;
127 | }
128 |
129 | return {
130 | name: name,
131 | frame: frame,
132 | spriteSourceSize: spriteSourceSize,
133 | sourceSize: sourceSize,
134 | index: index,
135 | first: index === 0,
136 | last: index === data.length - 1,
137 | rotated: item.rotated,
138 | trimmed: trimmed,
139 | };
140 | });
141 |
142 | return { rects: ret, config: opt };
143 | }
144 |
145 | function startExporter(exporter, data, options) {
146 | let { rects, config } = prepareData(data, options);
147 | let renderOptions = {
148 | rects: rects,
149 | config: config,
150 | appInfo: options.appInfo || appInfo,
151 | };
152 |
153 | if (exporter.content) {
154 | return finishExporter(exporter, renderOptions);
155 | }
156 |
157 | let filePath;
158 | if (exporter.predefined) {
159 | filePath = path.join(__dirname, exporter.template);
160 | } else {
161 | filePath = exporter.template;
162 | }
163 |
164 | exporter.content = fs.readFileSync(filePath).toString();
165 | return finishExporter(exporter, renderOptions);
166 | }
167 |
168 | function finishExporter(exporter, renderOptions) {
169 | return mustache.render(exporter.content, renderOptions);
170 | }
171 |
172 | module.exports.getExporterByType = getExporterByType;
173 | module.exports.startExporter = startExporter;
174 | module.exports.list = list;
175 |
--------------------------------------------------------------------------------
/exporters/list.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "JsonHash",
4 | "description": "Json hash",
5 | "allowTrim": true,
6 | "allowRotation": true,
7 | "template": "JsonHash.mst",
8 | "fileExt": "json",
9 | "predefined": true
10 | },
11 | {
12 | "type": "JsonArray",
13 | "description": "Json array",
14 | "allowTrim": true,
15 | "allowRotation": true,
16 | "template": "JsonArray.mst",
17 | "fileExt": "json",
18 | "predefined": true
19 | },
20 | {
21 | "type": "XML",
22 | "description": "Plain XML format",
23 | "allowTrim": true,
24 | "allowRotation": true,
25 | "template": "XML.mst",
26 | "fileExt": "xml",
27 | "predefined": true
28 | },
29 | {
30 | "type": "Css",
31 | "description": "css format",
32 | "allowTrim": true,
33 | "allowRotation": true,
34 | "template": "Css.mst",
35 | "fileExt": "css",
36 | "predefined": true
37 | },
38 | {
39 | "type": "OldCss",
40 | "description": "old css format",
41 | "allowTrim": false,
42 | "allowRotation": false,
43 | "template": "OldCss.mst",
44 | "fileExt": "css",
45 | "predefined": true
46 | },
47 | {
48 | "type": "Pixi",
49 | "description": "pixi.js format",
50 | "allowTrim": true,
51 | "allowRotation": true,
52 | "template": "JsonHash.mst",
53 | "fileExt": "json",
54 | "predefined": true
55 | },
56 | {
57 | "type": "GodotAtlas",
58 | "description": "Godot Atlas format",
59 | "allowTrim": true,
60 | "allowRotation": true,
61 | "template": "GodotAtlas.mst",
62 | "fileExt": "tpsheet",
63 | "predefined": true
64 | },
65 | {
66 | "type": "GodotTileset",
67 | "description": "Godot Tileset format",
68 | "allowTrim": true,
69 | "allowRotation": true,
70 | "template": "GodotTileset.mst",
71 | "fileExt": "tpset",
72 | "predefined": true
73 | },
74 | {
75 | "type": "PhaserHash",
76 | "description": "Phaser (json hash)",
77 | "allowTrim": true,
78 | "allowRotation": true,
79 | "template": "JsonHash.mst",
80 | "fileExt": "json",
81 | "predefined": true
82 | },
83 | {
84 | "type": "PhaserArray",
85 | "description": "Phaser (json array)",
86 | "allowTrim": true,
87 | "allowRotation": true,
88 | "template": "JsonArray.mst",
89 | "fileExt": "json",
90 | "predefined": true
91 | },
92 | {
93 | "type": "Phaser3",
94 | "description": "Phaser 3",
95 | "allowTrim": true,
96 | "allowRotation": true,
97 | "template": "Phaser3.mst",
98 | "fileExt": "json",
99 | "predefined": true
100 | },
101 | {
102 | "type": "Cocos2d",
103 | "description": "cocos2d format",
104 | "allowTrim": true,
105 | "allowRotation": true,
106 | "template": "Cocos2d.mst",
107 | "fileExt": "plist",
108 | "predefined": true
109 | },
110 | {
111 | "type": "Unreal",
112 | "description": "UnrealEngine - Paper2d",
113 | "allowTrim": true,
114 | "allowRotation": true,
115 | "template": "Unreal.mst",
116 | "fileExt": "paper2dsprites",
117 | "predefined": true
118 | },
119 | {
120 | "type": "Starling",
121 | "description": "Starling format",
122 | "allowTrim": true,
123 | "allowRotation": true,
124 | "template": "Starling.mst",
125 | "fileExt": "xml",
126 | "predefined": true
127 | },
128 | {
129 | "type": "Spine",
130 | "description": "Spine atlas",
131 | "allowTrim": true,
132 | "allowRotation": true,
133 | "template": "Spine.mst",
134 | "fileExt": "atlas",
135 | "predefined": true
136 | },
137 | {
138 | "type": "UIKit",
139 | "description": "IOS UIKit plist",
140 | "allowTrim": true,
141 | "allowRotation": false,
142 | "template": "UIKit.mst",
143 | "fileExt": "plist",
144 | "predefined": true
145 | },
146 | {
147 | "type": "Unity3D",
148 | "description": "Unity3D sprite sheet",
149 | "allowTrim": true,
150 | "allowRotation": false,
151 | "template": "Unity3D.mst",
152 | "fileExt": "tpsheet",
153 | "predefined": true
154 | },
155 | {
156 | "type": "Egret2D",
157 | "description": "Egret2D sprite sheet",
158 | "allowTrim": false,
159 | "allowRotation": false,
160 | "template": "Egret2D.mst",
161 | "fileExt": "json",
162 | "predefined": true
163 | }
164 | ]
--------------------------------------------------------------------------------
/filters/Filter.js:
--------------------------------------------------------------------------------
1 | class Filter {
2 | constructor() {
3 | }
4 |
5 | apply(image) {
6 | return image;
7 | }
8 |
9 | static get type() {
10 | return "none";
11 | }
12 | }
13 |
14 | module.exports = Filter;
--------------------------------------------------------------------------------
/filters/Grayscale.js:
--------------------------------------------------------------------------------
1 | let Filter = require('./Filter');
2 |
3 | class Grayscale extends Filter {
4 | constructor() {
5 | super();
6 | }
7 |
8 | apply(image) {
9 | let imageData = image.bitmap;
10 |
11 | for(let i=0; i,
4 | config?: TexturePackerOptions,
5 | callback?: (files: Array<{ name: string, buffer: Buffer }>, error?: Error) => void,
6 | ): void;
7 |
8 | export function packAsync(
9 | files: Array<{ path: string; contents: Buffer }>,
10 | config?: TexturePackerOptions,
11 | ): Promise>;
12 | }
13 |
14 | /**
15 | * Trim mode for sprites
16 | *
17 | * @see TexturePackerOptions.trimMode
18 | * @see TexturePackerOptions.allowTrim
19 | */
20 | export enum TrimMode {
21 | /**
22 | * Remove transparent pixels from sides, but left original frame size
23 | *
24 | * For example:
25 | * Original sprite has size 64x64, after removing transparent pixels its real size will be reduced to 32x28,
26 | * which will be written as frame size, but original frame size will stay the same: 64x64
27 | */
28 | TRIM = 'trim',
29 | /**
30 | * Remove transparent pixels from sides, and update frame size
31 | *
32 | * For example:
33 | * Original sprite has size 64x64, after removing transparent pixels its real size will be reduced to 32x28,
34 | * which will be written as frame size, and original frame size will be reduced to the same dimensions
35 | */
36 | CROP = 'crop',
37 | }
38 |
39 | /**
40 | * Output atlas texture format
41 | *
42 | * @see TexturePackerOptions.textureFormat
43 | */
44 | export enum TextureFormat {
45 | PNG = 'png',
46 | JPG = 'jpg',
47 | }
48 |
49 | /**
50 | * Atlas packer type.
51 | * There are two implementations which could be used
52 | *
53 | * @see TexturePackerOptions.packer
54 | * @see TexturePackerOptions.packerMethod
55 | * @see MaxRectsBinMethod
56 | * @see MaxRectsPackerMethod
57 | */
58 | export enum PackerType {
59 | MAX_RECTS_BIN = 'MaxRectsBin',
60 | MAX_RECTS_PACKER = 'MaxRectsPacker',
61 | OPTIMAL_PACKER = 'OptimalPacker'
62 | }
63 |
64 | /**
65 | * MaxRectsBin packer method
66 | *
67 | * @see TexturePackerOptions.packerMethod
68 | */
69 | export enum MaxRectsBinMethod {
70 | BEST_SHORT_SIDE_FIT = 'BestShortSideFit',
71 | BEST_LONG_SIDE_FIT = 'BestLongSideFit',
72 | BEST_AREA_FIT = 'BestAreaFit',
73 | BOTTOM_LEFT_RULE = 'BottomLeftRule',
74 | CONTACT_POINT_RULE = 'ContactPointRule',
75 | }
76 |
77 | /**
78 | * MaxRectsPacker packer method
79 | *
80 | * @see TexturePackerOptions.packerMethod
81 | */
82 | export enum MaxRectsPackerMethod {
83 | SMART = 'Smart',
84 | SQUARE = 'Square',
85 | SMART_SQUARE = 'SmartSquare',
86 | SMART_AREA = 'SmartArea',
87 | SQUARE_AREA = 'SquareArea',
88 | SMART_SQUARE_AREA = 'SmartSquareArea'
89 | }
90 |
91 | /**
92 | * Packer exporter type
93 | * Predefined exporter types (supported popular formats)
94 | * Instead of predefined type you could use custom exporter
95 | *
96 | * @see TexturePackerOptions.exporter
97 | * @see PackerExporter
98 | */
99 | export enum PackerExporterType {
100 | JSON_HASH = 'JsonHash',
101 | JSON_ARRAY = 'JsonArray',
102 | CSS = 'Css',
103 | OLD_CSS = 'OldCss',
104 | PIXI = 'Pixi',
105 | PHASER_HASH = 'PhaserHash',
106 | PHASER_ARRAY = 'PhaserArray',
107 | PHASER3 = 'Phaser3',
108 | XML = 'XML',
109 | STARLING = 'Starling',
110 | COCOS2D = 'Cocos2d',
111 | SPINE = 'Spine',
112 | UNREAL = 'Unreal',
113 | UIKIT = 'UIKit',
114 | UNITY3D = 'Unity3D',
115 | }
116 |
117 | /**
118 | * Bitmap filter, applicable to output atlas texture
119 | *
120 | * @see TexturePackerOptions.filter
121 | */
122 | export enum BitmapFilterType {
123 | GRAYSCALE = 'grayscale',
124 | MASK = 'mask',
125 | NONE = 'none',
126 | }
127 |
128 | /**
129 | * Texture packer options
130 | */
131 | export interface TexturePackerOptions {
132 | /**
133 | * Name of output files.
134 | *
135 | * @default pack-result
136 | */
137 | textureName?: string;
138 |
139 | /**
140 | * Max single texture width in pixels
141 | *
142 | * @default 2048
143 | */
144 | width?: number;
145 | /**
146 | * Max single texture height in pixels
147 | *
148 | * @default 2048
149 | */
150 | height?: number;
151 | /**
152 | * Fixed texture size
153 | *
154 | * @default false
155 | */
156 | fixedSize?: boolean;
157 | /**
158 | * Force power of two textures sizes
159 | *
160 | * @default false
161 | */
162 | powerOfTwo?: boolean;
163 | /**
164 | * Spaces in pixels around images
165 | *
166 | * @default 0
167 | */
168 | padding?: number;
169 | /**
170 | * Extrude border pixels size around images
171 | *
172 | * @default 0
173 | */
174 | extrude?: number;
175 | /**
176 | * Allow image rotation
177 | * @default true
178 | */
179 | allowRotation?: boolean;
180 | /**
181 | * Allow detect identical images
182 | *
183 | * @default true
184 | */
185 | detectIdentical?: boolean;
186 | /**
187 | * Allow trim images
188 | *
189 | * @default true
190 | */
191 | allowTrim?: boolean;
192 | /**
193 | * Trim mode
194 | *
195 | * @default {@link TrimMode.TRIM}
196 | * @see {@link TrimMode}
197 | * @see {@link allowTrim}
198 | */
199 | trimMode?: TrimMode;
200 | /**
201 | * Threshold alpha value
202 | *
203 | * @default 0
204 | */
205 | alphaThreshold?: number;
206 | /**
207 | * Remove file extensions from frame names
208 | *
209 | * @default false
210 | */
211 | removeFileExtension?: boolean;
212 | /**
213 | * Prepend folder name to frame names
214 | *
215 | * @default true
216 | */
217 | prependFolderName?: boolean;
218 | /**
219 | * Output file format
220 | *
221 | * @default {@link TextureFormat.PNG}
222 | * @see {@link TextureFormat}
223 | */
224 | textureFormat?: TextureFormat;
225 | /**
226 | * Export texture as base64 string to atlas meta tag
227 | *
228 | * @default false
229 | */
230 | base64Export?: boolean;
231 | /**
232 | * Scale size and positions in atlas
233 | *
234 | * @default 1
235 | */
236 | scale?: number;
237 | /**
238 | * Texture scaling method
239 | *
240 | * @default ScaleMethod.BILINEAR
241 | */
242 | scaleMethod?: ScaleMethod;
243 | /**
244 | * "Tinify" texture using TinyPNG
245 | *
246 | * @default false
247 | */
248 | tinify?: boolean;
249 | /**
250 | * TinyPNG key
251 | *
252 | * @default empty string
253 | */
254 | tinifyKey?: string;
255 | /**
256 | * Type of packer
257 | * @see PackerType
258 | * @default {@link PackerType.MAX_RECTS_BIN}
259 | */
260 | packer?: PackerType;
261 | /**
262 | * Pack method
263 | *
264 | * @default {@link MaxRectsBinMethod.BEST_SHORT_SIDE_FIT}
265 | * @see MaxRectsBinMethod
266 | * @see MaxRectsPackerMethod
267 | */
268 | packerMethod?: MaxRectsBinMethod | MaxRectsPackerMethod;
269 | /**
270 | * Name of predefined exporter (), or custom exporter (see below)
271 | *
272 | * @default JsonHash
273 | */
274 | exporter?: PackerExporterType | PackerExporter;
275 | /**
276 | * Bitmap filter type
277 | *
278 | * @see BitmapFilterType
279 | * @default {@link BitmapFilterType.NONE}
280 | */
281 | filter?: BitmapFilterType;
282 | /**
283 | * External application info.
284 | * Required fields: url and version
285 | *
286 | * @default null
287 | */
288 | appInfo?: any;
289 | }
290 |
291 | export enum ScaleMethod {
292 | BILINEAR = 'BILINEAR',
293 | NEAREST_NEIGHBOR = 'NEAREST_NEIGHBOR',
294 | HERMITE = 'HERMITE',
295 | BEZIER = 'BEZIER',
296 | }
297 |
298 | /**
299 | * Texture packer uses {@link http://mustache.github.io/ | mustache} template engine.
300 | * Look at documentation how to create custom exporter:
301 | * {@link https://www.npmjs.com/package/free-tex-packer-core#custom-exporter}
302 | */
303 | export interface PackerExporter {
304 | /**
305 | * File extension
306 | */
307 | fileExt: string;
308 | /**
309 | * Path to template file (content could be used instead)
310 | * @see {@link content}
311 | */
312 | template?: string;
313 | /**
314 | * Template content (template path could be used instead)
315 | * @see {@link template}
316 | */
317 | content?: string;
318 | }
319 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | let getPackerByType = require("./packers/index").getPackerByType;
2 | let getExporterByType = require("./exporters/index").getExporterByType;
3 | let getFilterByType = require("./filters").getFilterByType;
4 | let FilesProcessor = require("./FilesProcessor");
5 | let appInfo = require('./package.json');
6 | let Jimp = require("jimp");
7 |
8 | function getErrorDescription(txt) {
9 | return appInfo.name + ": " + txt;
10 | }
11 |
12 | function fixPath(path) {
13 | return path.split("\\").join("/");
14 | }
15 |
16 | function loadImage(file, files) {
17 | return Jimp.read(file.contents)
18 | .then(image => {
19 | image.name = fixPath(file.path);
20 | image._base64 = file.contents.toString("base64");
21 | image.width = image.bitmap.width;
22 | image.height = image.bitmap.height;
23 | files[image.name] = image;
24 | })
25 | .catch(e => {
26 | console.error(getErrorDescription("Error reading " + file.path));
27 | });
28 | }
29 |
30 | function packAsync(images, options) {
31 | options = options || {};
32 | options = Object.assign({}, options);
33 |
34 | options.textureName = options.textureName === undefined ? "pack-result" : options.textureName;
35 | options.suffix = options.suffix === undefined ? "-" : options.suffix;
36 | options.suffixInitialValue = options.suffixInitialValue === undefined ? 0 : options.suffixInitialValue;
37 | options.width = options.width === undefined ? 2048 : options.width;
38 | options.height = options.height === undefined ? 2048 : options.height;
39 | options.powerOfTwo = !!options.powerOfTwo;
40 | options.fixedSize = options.fixedSize === undefined ? false : options.fixedSize;
41 | options.padding = options.padding === undefined ? 0 : options.padding;
42 | options.extrude = options.extrude === undefined ? 0 : options.extrude;
43 | options.allowRotation = options.allowRotation === undefined ? true : options.allowRotation;
44 | options.detectIdentical = options.detectIdentical === undefined ? true : options.detectIdentical;
45 | options.allowTrim = options.allowTrim === undefined ? true : options.allowTrim;
46 | options.trimMode = options.trimMode === undefined ? "trim" : options.trimMode;
47 | options.alphaThreshold = options.alphaThreshold === undefined ? 0 : options.alphaThreshold;
48 | options.removeFileExtension = options.removeFileExtension === undefined ? false : options.removeFileExtension;
49 | options.prependFolderName = options.prependFolderName === undefined ? true : options.prependFolderName;
50 | options.textureFormat = options.textureFormat === undefined ? "png" : options.textureFormat;
51 | options.base64Export = options.base64Export === undefined ? false : options.base64Export;
52 | options.scale = options.scale === undefined ? 1 : options.scale;
53 | options.scaleMethod = options.scaleMethod === undefined ? "BILINEAR" : options.scaleMethod;
54 | options.tinify = options.tinify === undefined ? false : options.tinify;
55 | options.tinifyKey = options.tinifyKey === undefined ? "" : options.tinifyKey;
56 | options.filter = options.filter === undefined ? "none" : options.filter;
57 |
58 | if(!options.packer) options.packer = "MaxRectsBin";
59 | if(!options.exporter) options.exporter = "JsonHash";
60 |
61 | let packer = getPackerByType(options.packer);
62 | if(!packer) {
63 | throw new Error(getErrorDescription("Unknown packer " + options.packer));
64 | }
65 |
66 | if(!options.packerMethod) {
67 | options.packerMethod = packer.defaultMethod;
68 | }
69 |
70 | let packerMethod = packer.getMethodByType(options.packerMethod);
71 | if(!packerMethod) {
72 | throw new Error(getErrorDescription("Unknown packer method " + options.packerMethod));
73 | }
74 |
75 | let exporter;
76 | if(typeof options.exporter == "string") {
77 | exporter = getExporterByType(options.exporter);
78 | }
79 | else {
80 | exporter = options.exporter;
81 | }
82 |
83 | if(!exporter.allowRotation) options.allowRotation = false;
84 | if(!exporter.allowTrim) options.allowTrim = false;
85 |
86 | if(!exporter) {
87 | throw new Error(getErrorDescription("Unknown exporter " + options.exporter));
88 | }
89 |
90 | let filter = getFilterByType(options.filter);
91 | if(!filter) {
92 | throw new Error(getErrorDescription("Unknown filter " + options.filter));
93 | }
94 |
95 | options.packer = packer;
96 | options.packerMethod = packerMethod;
97 | options.exporter = exporter;
98 | options.filter = filter;
99 |
100 | let files = {};
101 | let p = [];
102 |
103 | for(let file of images) {
104 | p.push(loadImage(file, files));
105 | }
106 |
107 | return new Promise((resolve, reject) =>
108 | Promise.all(p)
109 | .then(() => {
110 | FilesProcessor.start(files, options,
111 | (res) => resolve(res),
112 | (error) => reject(error)
113 | )
114 | })
115 | .catch((error) => reject(error))
116 | );
117 | }
118 |
119 | function pack(images, options, cb) {
120 | packAsync(images, options)
121 | .then((result) => cb(result))
122 | .catch((error) => cb(undefined, error));
123 | }
124 |
125 | module.exports = pack;
126 | module.exports.packAsync = packAsync;
127 |
--------------------------------------------------------------------------------
/math/Rect.js:
--------------------------------------------------------------------------------
1 | class Rect {
2 | constructor(x=0, y=0, width=0, height=0) {
3 | this.x = x;
4 | this.y = y;
5 | this.width = width;
6 | this.height = height;
7 | }
8 |
9 | clone() {
10 | return new Rect(this.x, this.y, this.width, this.height);
11 | }
12 |
13 | hitTest(other) {
14 | return Rect.hitTest(this, other);
15 | }
16 |
17 | static hitTest(a, b) {
18 | return a.x >= b.x && a.y >= b.y && a.x+a.width <= b.x+b.width && a.y+a.height <= b.y+b.height;
19 | }
20 | }
21 |
22 | module.exports = Rect;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "free-tex-packer-core",
3 | "displayName": "Free texture packer",
4 | "version": "0.3.4",
5 | "description": "Free texture packer core",
6 | "url": "http://github.com/odrick/free-tex-packer-core",
7 | "main": "index.js",
8 | "types": "index.d.ts",
9 | "scripts": {},
10 | "keywords": [
11 | "texture",
12 | "packer",
13 | "gulp",
14 | "gulpjs",
15 | "gulpplugin",
16 | "texturepacker",
17 | "texture-packer",
18 | "sprites",
19 | "spritesheet",
20 | "export",
21 | "sprite",
22 | "2d"
23 | ],
24 | "author": "Alexander Norinchak",
25 | "repository": {
26 | "type": "git",
27 | "url": "git+https://github.com/odrick/free-tex-packer-core"
28 | },
29 | "license": "MIT",
30 | "dependencies": {
31 | "@jvitela/mustache-wax": "^1.0.1",
32 | "jimp": "^0.2.28",
33 | "maxrects-packer": "^2.5.0",
34 | "mustache": "^2.3.0",
35 | "tinify": "^1.5.0"
36 | },
37 | "devDependencies": {
38 | "@types/node": "^13.1.0"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packers/MaxRectsBin.js:
--------------------------------------------------------------------------------
1 | let Packer = require("./Packer");
2 | let Rect = require("../math/Rect");
3 |
4 | const METHOD = {
5 | BestShortSideFit: "BestShortSideFit",
6 | BestLongSideFit: "BestLongSideFit",
7 | BestAreaFit: "BestAreaFit",
8 | BottomLeftRule: "BottomLeftRule",
9 | ContactPointRule: "ContactPointRule"
10 | };
11 |
12 | class MaxRectsBin extends Packer {
13 |
14 | constructor(width, height, allowRotate=false) {
15 | super();
16 |
17 | this.usedRectangles = [];
18 | this.freeRectangles = [];
19 |
20 | this.binWidth = width;
21 | this.binHeight = height;
22 | this.allowRotate = allowRotate;
23 |
24 | this.freeRectangles.push(new Rect(0, 0, width, height));
25 | }
26 |
27 | pack(data, method) {
28 | return this.insert2(data, method);
29 | }
30 |
31 | insert(width, height, method=METHOD.BestShortSideFit) {
32 | let newNode = new Rect();
33 | let score1 = {value:0};
34 | let score2 = {value:0};
35 |
36 | switch(method) {
37 | case METHOD.BestShortSideFit:
38 | newNode = this._findPositionForNewNodeBestShortSideFit(width, height, score1, score2);
39 | break;
40 | case METHOD.BottomLeftRule:
41 | newNode = this._findPositionForNewNodeBottomLeft(width, height, score1, score2);
42 | break;
43 | case METHOD.ContactPointRule:
44 | newNode = this._findPositionForNewNodeContactPoint(width, height, score1);
45 | break;
46 | case METHOD.BestLongSideFit:
47 | newNode = this._findPositionForNewNodeBestLongSideFit(width, height, score2, score1);
48 | break;
49 | case METHOD.BestAreaFit:
50 | newNode = this._findPositionForNewNodeBestAreaFit(width, height, score1, score2);
51 | break;
52 | }
53 |
54 | if (newNode.height === 0){
55 | return newNode;
56 | }
57 |
58 | this._placeRectangle(newNode);
59 | return newNode;
60 | }
61 |
62 | insert2(rectangles, method) {
63 | let res = [];
64 |
65 | while(rectangles.length > 0) {
66 | let bestScore1 = Infinity;
67 | let bestScore2 = Infinity;
68 | let bestRectangleIndex = -1;
69 | let bestNode = new Rect();
70 |
71 | for(let i= 0; i < rectangles.length; i++) {
72 | let score1 = {value:0};
73 | let score2 = {value:0};
74 | let newNode = this._scoreRectangle(rectangles[i].frame.w, rectangles[i].frame.h, method, score1, score2);
75 |
76 | if (score1.value < bestScore1 || (score1.value == bestScore1 && score2.value < bestScore2)) {
77 | bestScore1 = score1.value;
78 | bestScore2 = score2.value;
79 | bestNode = newNode;
80 | bestRectangleIndex = i;
81 | }
82 | }
83 |
84 | if (bestRectangleIndex == -1) {
85 | return res;
86 | }
87 |
88 | this._placeRectangle(bestNode);
89 | let rect = rectangles.splice(bestRectangleIndex, 1)[0];
90 | rect.frame.x = bestNode.x;
91 | rect.frame.y = bestNode.y;
92 |
93 | if(rect.frame.w != bestNode.width || rect.frame.h != bestNode.height) {
94 | rect.rotated = true;
95 | //rect.frame.w = bestNode.width;
96 | //rect.frame.h = bestNode.height;
97 | }
98 |
99 | res.push(rect);
100 | }
101 | return res;
102 | }
103 |
104 | _placeRectangle(node) {
105 | let numRectanglesToProcess = this.freeRectangles.length;
106 | for(let i= 0; i < numRectanglesToProcess; i++) {
107 | if (this._splitFreeNode(this.freeRectangles[i], node)) {
108 | this.freeRectangles.splice(i,1);
109 | i--;
110 | numRectanglesToProcess--;
111 | }
112 | }
113 |
114 | this._pruneFreeList();
115 | this.usedRectangles.push(node);
116 | }
117 |
118 | _scoreRectangle(width, height, method, score1, score2) {
119 | let newNode = new Rect();
120 | score1.value = Infinity;
121 | score2.value = Infinity;
122 | switch(method) {
123 | case METHOD.BestShortSideFit:
124 | newNode = this._findPositionForNewNodeBestShortSideFit(width, height, score1, score2);
125 | break;
126 | case METHOD.BottomLeftRule:
127 | newNode = this._findPositionForNewNodeBottomLeft(width, height, score1, score2);
128 | break;
129 | case METHOD.ContactPointRule:
130 | newNode = this._findPositionForNewNodeContactPoint(width, height, score1);
131 | score1.value = -score1.value;
132 | break;
133 | case METHOD.BestLongSideFit:
134 | newNode = this._findPositionForNewNodeBestLongSideFit(width, height, score2, score1);
135 | break;
136 | case METHOD.BestAreaFit:
137 | newNode = this._findPositionForNewNodeBestAreaFit(width, height, score1, score2);
138 | break;
139 | }
140 |
141 | if (newNode.height === 0) {
142 | score1.value = Infinity;
143 | score2.value = Infinity;
144 | }
145 |
146 | return newNode;
147 | }
148 |
149 | _occupancy() {
150 | let usedRectangles = this.usedRectangles;
151 | let usedSurfaceArea = 0;
152 | for(let i= 0; i < usedRectangles.length; i++) {
153 | usedSurfaceArea += usedRectangles[i].width * usedRectangles[i].height;
154 | }
155 |
156 | return usedSurfaceArea/(this.binWidth * this.binHeight);
157 | }
158 |
159 | _findPositionForNewNodeBottomLeft(width, height, bestY, bestX) {
160 | let freeRectangles = this.freeRectangles;
161 | let bestNode = new Rect();
162 |
163 | bestY.value = Infinity;
164 | let rect;
165 | let topSideY;
166 | for(let i= 0; i < freeRectangles.length; i++) {
167 | rect = freeRectangles[i];
168 | if (rect.width >= width && rect.height >= height) {
169 | topSideY = rect.y + height;
170 | if (topSideY < bestY.value || (topSideY == bestY.value && rect.x < bestX.value)) {
171 | bestNode.x = rect.x;
172 | bestNode.y = rect.y;
173 | bestNode.width = width;
174 | bestNode.height = height;
175 | bestY.value = topSideY;
176 | bestX.value = rect.x;
177 | }
178 | }
179 | if (this.allowRotate && rect.width >= height && rect.height >= width) {
180 | topSideY = rect.y + width;
181 | if (topSideY < bestY.value || (topSideY == bestY.value && rect.x < bestX.value)) {
182 | bestNode.x = rect.x;
183 | bestNode.y = rect.y;
184 | bestNode.width = height;
185 | bestNode.height = width;
186 | bestY.value = topSideY;
187 | bestX.value = rect.x;
188 | }
189 | }
190 | }
191 | return bestNode;
192 | }
193 |
194 | _findPositionForNewNodeBestShortSideFit(width, height, bestShortSideFit, bestLongSideFit){
195 | let freeRectangles = this.freeRectangles;
196 | let bestNode = new Rect();
197 |
198 | bestShortSideFit.value = Infinity;
199 |
200 | let rect,
201 | leftoverHoriz,
202 | leftoverVert,
203 | shortSideFit,
204 | longSideFit;
205 |
206 | for(let i= 0; i < freeRectangles.length; i++) {
207 | rect = freeRectangles[i];
208 | if (rect.width >= width && rect.height >= height) {
209 | leftoverHoriz = Math.abs(rect.width - width);
210 | leftoverVert = Math.abs(rect.height - height);
211 | shortSideFit = Math.min(leftoverHoriz, leftoverVert);
212 | longSideFit = Math.max(leftoverHoriz, leftoverVert);
213 |
214 | if (shortSideFit < bestShortSideFit.value || (shortSideFit == bestShortSideFit.value && longSideFit < bestLongSideFit.value)) {
215 | bestNode.x = rect.x;
216 | bestNode.y = rect.y;
217 | bestNode.width = width;
218 | bestNode.height = height;
219 | bestShortSideFit.value = shortSideFit;
220 | bestLongSideFit.value = longSideFit;
221 | }
222 | }
223 |
224 | let flippedLeftoverHoriz,
225 | flippedLeftoverVert,
226 | flippedShortSideFit,
227 | flippedLongSideFit;
228 |
229 | if (this.allowRotate && rect.width >= height && rect.height >= width) {
230 | flippedLeftoverHoriz = Math.abs(rect.width - height);
231 | flippedLeftoverVert = Math.abs(rect.height - width);
232 | flippedShortSideFit = Math.min(flippedLeftoverHoriz, flippedLeftoverVert);
233 | flippedLongSideFit = Math.max(flippedLeftoverHoriz, flippedLeftoverVert);
234 |
235 | if (flippedShortSideFit < bestShortSideFit.value || (flippedShortSideFit == bestShortSideFit.value && flippedLongSideFit < bestLongSideFit.value)) {
236 | bestNode.x = rect.x;
237 | bestNode.y = rect.y;
238 | bestNode.width = height;
239 | bestNode.height = width;
240 | bestShortSideFit.value = flippedShortSideFit;
241 | bestLongSideFit.value = flippedLongSideFit;
242 | }
243 | }
244 | }
245 |
246 | return bestNode;
247 | }
248 |
249 | _findPositionForNewNodeBestLongSideFit(width, height, bestShortSideFit, bestLongSideFit) {
250 | let freeRectangles = this.freeRectangles;
251 | let bestNode = new Rect();
252 | bestLongSideFit.value = Infinity;
253 |
254 | let rect,
255 | leftoverHoriz,
256 | leftoverVert,
257 | shortSideFit,
258 | longSideFit;
259 |
260 | for(let i= 0; i < freeRectangles.length; i++) {
261 | rect = freeRectangles[i];
262 |
263 | if (rect.width >= width && rect.height >= height) {
264 | leftoverHoriz = Math.abs(rect.width - width);
265 | leftoverVert = Math.abs(rect.height - height);
266 | shortSideFit = Math.min(leftoverHoriz, leftoverVert);
267 | longSideFit = Math.max(leftoverHoriz, leftoverVert);
268 |
269 | if (longSideFit < bestLongSideFit.value || (longSideFit == bestLongSideFit.value && shortSideFit < bestShortSideFit.value)) {
270 | bestNode.x = rect.x;
271 | bestNode.y = rect.y;
272 | bestNode.width = width;
273 | bestNode.height = height;
274 | bestShortSideFit.value = shortSideFit;
275 | bestLongSideFit.value = longSideFit;
276 | }
277 | }
278 |
279 | if (this.allowRotate && rect.width >= height && rect.height >= width) {
280 | leftoverHoriz = Math.abs(rect.width - height);
281 | leftoverVert = Math.abs(rect.height - width);
282 | shortSideFit = Math.min(leftoverHoriz, leftoverVert);
283 | longSideFit = Math.max(leftoverHoriz, leftoverVert);
284 |
285 | if (longSideFit < bestLongSideFit.value || (longSideFit == bestLongSideFit.value && shortSideFit < bestShortSideFit.value)) {
286 | bestNode.x = rect.x;
287 | bestNode.y = rect.y;
288 | bestNode.width = height;
289 | bestNode.height = width;
290 | bestShortSideFit.value = shortSideFit;
291 | bestLongSideFit.value = longSideFit;
292 | }
293 | }
294 | }
295 | return bestNode;
296 | }
297 |
298 | _findPositionForNewNodeBestAreaFit(width, height, bestAreaFit, bestShortSideFit) {
299 | let freeRectangles = this.freeRectangles;
300 | let bestNode = new Rect();
301 |
302 | bestAreaFit.value = Infinity;
303 |
304 | let rect,
305 | leftoverHoriz,
306 | leftoverVert,
307 | shortSideFit,
308 | areaFit;
309 |
310 | for(let i= 0; i < freeRectangles.length; i++) {
311 | rect = freeRectangles[i];
312 | areaFit = rect.width * rect.height - width * height;
313 |
314 | if (rect.width >= width && rect.height >= height) {
315 | leftoverHoriz = Math.abs(rect.width - width);
316 | leftoverVert = Math.abs(rect.height - height);
317 | shortSideFit = Math.min(leftoverHoriz, leftoverVert);
318 |
319 | if (areaFit < bestAreaFit.value || (areaFit == bestAreaFit.value && shortSideFit < bestShortSideFit.value)) {
320 | bestNode.x = rect.x;
321 | bestNode.y = rect.y;
322 | bestNode.width = width;
323 | bestNode.height = height;
324 | bestShortSideFit.value = shortSideFit;
325 | bestAreaFit = areaFit;
326 | }
327 | }
328 |
329 | if (this.allowRotate && rect.width >= height && rect.height >= width) {
330 | leftoverHoriz = Math.abs(rect.width - height);
331 | leftoverVert = Math.abs(rect.height - width);
332 | shortSideFit = Math.min(leftoverHoriz, leftoverVert);
333 |
334 | if (areaFit < bestAreaFit.value || (areaFit == bestAreaFit.value && shortSideFit < bestShortSideFit.value)) {
335 | bestNode.x = rect.x;
336 | bestNode.y = rect.y;
337 | bestNode.width = height;
338 | bestNode.height = width;
339 | bestShortSideFit.value = shortSideFit;
340 | bestAreaFit.value = areaFit;
341 | }
342 | }
343 | }
344 | return bestNode;
345 | }
346 |
347 | _commonIntervalLength(i1start, i1end, i2start, i2end){
348 | if (i1end < i2start || i2end < i1start){
349 | return 0;
350 | }
351 | return Math.min(i1end, i2end) - Math.max(i1start, i2start);
352 | }
353 |
354 | _contactPointScoreNode(x, y, width, height){
355 | let usedRectangles = this.usedRectangles;
356 | let score = 0;
357 |
358 | if (x == 0 || x + width === this.binWidth)
359 | score += height;
360 | if (y == 0 || y + height === this.binHeight)
361 | score += width;
362 | let rect;
363 | for(let i= 0; i < usedRectangles.length; i++) {
364 | rect = usedRectangles[i];
365 | if (rect.x == x + width || rect.x + rect.width == x)
366 | score += this._commonIntervalLength(rect.y, rect.y + rect.height, y, y + height);
367 | if (rect.y == y + height || rect.y + rect.height == y)
368 | score += this._commonIntervalLength(rect.x, rect.x + rect.width, x, x + width);
369 | }
370 | return score;
371 | }
372 |
373 | _findPositionForNewNodeContactPoint(width, height, bestContactScore) {
374 | let freeRectangles = this.freeRectangles;
375 | let bestNode = new Rect();
376 |
377 | bestContactScore.value = -1;
378 |
379 | let rect,
380 | score;
381 |
382 | for(let i= 0; i < freeRectangles.length; i++) {
383 | rect = freeRectangles[i];
384 | if (rect.width >= width && rect.height >= height) {
385 | score = this._contactPointScoreNode(rect.x, rect.y, width, height);
386 | if (score > bestContactScore.value) {
387 | bestNode.x = rect.x;
388 | bestNode.y = rect.y;
389 | bestNode.width = width;
390 | bestNode.height = height;
391 | bestContactScore = score;
392 | }
393 | }
394 | if (this.allowRotate && rect.width >= height && rect.height >= width) {
395 | score = this._contactPointScoreNode(rect.x, rect.y, height, width);
396 | if (score > bestContactScore.value) {
397 | bestNode.x = rect.x;
398 | bestNode.y = rect.y;
399 | bestNode.width = height;
400 | bestNode.height = width;
401 | bestContactScore.value = score;
402 | }
403 | }
404 | }
405 | return bestNode;
406 | }
407 |
408 | _splitFreeNode(freeNode, usedNode){
409 | let freeRectangles = this.freeRectangles;
410 | if (usedNode.x >= freeNode.x + freeNode.width || usedNode.x + usedNode.width <= freeNode.x ||
411 | usedNode.y >= freeNode.y + freeNode.height || usedNode.y + usedNode.height <= freeNode.y)
412 | return false;
413 | let newNode;
414 | if (usedNode.x < freeNode.x + freeNode.width && usedNode.x + usedNode.width > freeNode.x) {
415 | if (usedNode.y > freeNode.y && usedNode.y < freeNode.y + freeNode.height) {
416 | newNode = freeNode.clone();
417 | newNode.height = usedNode.y - newNode.y;
418 | freeRectangles.push(newNode);
419 | }
420 |
421 | if (usedNode.y + usedNode.height < freeNode.y + freeNode.height) {
422 | newNode = freeNode.clone();
423 | newNode.y = usedNode.y + usedNode.height;
424 | newNode.height = freeNode.y + freeNode.height - (usedNode.y + usedNode.height);
425 | freeRectangles.push(newNode);
426 | }
427 | }
428 |
429 | if (usedNode.y < freeNode.y + freeNode.height && usedNode.y + usedNode.height > freeNode.y) {
430 | if (usedNode.x > freeNode.x && usedNode.x < freeNode.x + freeNode.width) {
431 | newNode = freeNode.clone();
432 | newNode.width = usedNode.x - newNode.x;
433 | freeRectangles.push(newNode);
434 | }
435 |
436 | if (usedNode.x + usedNode.width < freeNode.x + freeNode.width) {
437 | newNode = freeNode.clone();
438 | newNode.x = usedNode.x + usedNode.width;
439 | newNode.width = freeNode.x + freeNode.width - (usedNode.x + usedNode.width);
440 | freeRectangles.push(newNode);
441 | }
442 | }
443 |
444 | return true;
445 | }
446 |
447 | _pruneFreeList() {
448 | let freeRectangles = this.freeRectangles;
449 | for(let i = 0;i < freeRectangles.length; i++)
450 | for(let j= i+1; j < freeRectangles.length; j++) {
451 | if (Rect.hitTest(freeRectangles[i], freeRectangles[j])) {
452 | freeRectangles.splice(i,1);
453 | break;
454 | }
455 | if (Rect.hitTest(freeRectangles[j], freeRectangles[i])) {
456 | freeRectangles.splice(j,1);
457 | }
458 | }
459 | }
460 |
461 | static get type() {
462 | return "MaxRectsBin";
463 | }
464 |
465 | static get defaultMethod() {
466 | return METHOD.BestShortSideFit;
467 | }
468 |
469 | static get methods() {
470 | return METHOD;
471 | }
472 |
473 | static getMethodProps(id="") {
474 | id = id.toLowerCase();
475 |
476 | switch(id) {
477 | case METHOD.BestShortSideFit.toLowerCase():
478 | return {name: "Best short side fit", description: "Positions the Rectangle against the short side of a free Rectangle into which it fits the best."};
479 | case METHOD.BestLongSideFit.toLowerCase():
480 | return {name: "Best long side fit", description: "Positions the Rectangle against the long side of a free Rectangle into which it fits the best."};
481 | case METHOD.BestAreaFit.toLowerCase():
482 | return {name: "Best area fit", description: "Positions the Rectangle into the smallest free Rectangle into which it fits."};
483 | case METHOD.BottomLeftRule.toLowerCase():
484 | return {name: "Bottom left rule", description: "Does the Tetris placement."};
485 | case METHOD.ContactPointRule.toLowerCase():
486 | return {name: "Contact point rule", description: "Choosest the placement where the Rectangle touches other Rectangles as much as possible."};
487 | default:
488 | throw Error("Unknown method " + id);
489 | }
490 | }
491 |
492 | static getMethodByType(type) {
493 | type = type.toLowerCase();
494 |
495 | let keys = Object.keys(METHOD);
496 |
497 | for(let name of keys) {
498 | if(type === name.toLowerCase()) return METHOD[name];
499 | }
500 |
501 | return null;
502 | }
503 | }
504 |
505 | module.exports = MaxRectsBin;
--------------------------------------------------------------------------------
/packers/MaxRectsPacker.js:
--------------------------------------------------------------------------------
1 | let MaxRectsPackerEngine = require("maxrects-packer").MaxRectsPacker;
2 | let PACKING_LOGIC = require("maxrects-packer").PACKING_LOGIC;
3 |
4 | let Packer = require("./Packer");
5 |
6 | const METHOD = {
7 | Smart: "Smart",
8 | SmartArea: "SmartArea",
9 | Square: "Square",
10 | SquareArea: "SquareArea",
11 | // SmartSquare: "SmartSquare",
12 | // SmartSquareArea: "SmartSquareArea"
13 | };
14 |
15 | class MaxRectsPacker extends Packer {
16 | constructor(width, height, allowRotate = false) {
17 | super();
18 |
19 | this.binWidth = width;
20 | this.binHeight = height;
21 | this.allowRotate = allowRotate;
22 | }
23 |
24 | pack(data, method) {
25 | let options = {
26 | smart: (method === METHOD.Smart || method === METHOD.SmartArea || method === METHOD.SmartSquare || method === METHOD.SmartSquareArea),
27 | pot: false,
28 | square: (method === METHOD.Square || method === METHOD.SquareArea || method === METHOD.SmartSquare || method === METHOD.SmartSquareArea),
29 | allowRotation: this.allowRotate,
30 | logic: (method === METHOD.Smart || method === METHOD.Square || method === METHOD.SmartSquare) ? PACKING_LOGIC.MAX_EDGE : PACKING_LOGIC.MAX_AREA
31 | };
32 |
33 | let packer = new MaxRectsPackerEngine(this.binWidth, this.binHeight, 0, options);
34 |
35 | let input = [];
36 |
37 | for (let item of data) {
38 | input.push({ width: item.frame.w, height: item.frame.h, data: item });
39 | }
40 |
41 | packer.addArray(input);
42 |
43 | let bin = packer.bins[0];
44 | let rects = bin.rects;
45 |
46 | let res = [];
47 |
48 | for (let item of rects) {
49 | item.data.frame.x = item.x;
50 | item.data.frame.y = item.y;
51 | if (item.rot) {
52 | item.data.rotated = true;
53 | }
54 | res.push(item.data);
55 | }
56 |
57 | return res;
58 | }
59 |
60 | static get type() {
61 | return "MaxRectsPacker";
62 | }
63 |
64 | static get defaultMethod() {
65 | return METHOD.Smart;
66 | }
67 |
68 | static get methods() {
69 | return METHOD;
70 | }
71 |
72 | static getMethodProps(id = '') {
73 | switch (id) {
74 | case METHOD.Smart:
75 | return { name: "Smart edge logic", description: "" };
76 | case METHOD.SmartArea:
77 | return { name: "Smart area logic", description: "" };
78 | case METHOD.Square:
79 | return { name: "Square edge logic", description: "" };
80 | case METHOD.SquareArea:
81 | return { name: "Square area logic", description: "" };
82 | case METHOD.SmartSquare:
83 | return { name: "Smart square edge logic", description: "" };
84 | case METHOD.SmartSquareArea:
85 | return { name: "Smart square area logic", description: "" };
86 | default:
87 | throw Error("Unknown method " + id);
88 | }
89 | }
90 |
91 | static getMethodByType(type) {
92 | type = type.toLowerCase();
93 |
94 | let keys = Object.keys(METHOD);
95 |
96 | for (let name of keys) {
97 | if (type === name.toLowerCase()) return METHOD[name];
98 | }
99 |
100 | return null;
101 | }
102 | }
103 |
104 | module.exports = MaxRectsPacker;
--------------------------------------------------------------------------------
/packers/OptimalPacker.js:
--------------------------------------------------------------------------------
1 | let Packer = require("./Packer");
2 |
3 | const METHOD = {
4 | Automatic: "Automatic"
5 | };
6 |
7 | class OptimalPacker extends Packer {
8 | constructor(width, height, allowRotate=false) {
9 | super();
10 | }
11 |
12 | pack(data, method) {
13 | throw new Error('OptimalPacker is a dummy and cannot be used directly');
14 | }
15 |
16 | static get type() {
17 | return "OptimalPacker";
18 | }
19 |
20 | static get defaultMethod() {
21 | return METHOD.Automatic;
22 | }
23 |
24 | static get methods() {
25 | return METHOD;
26 | }
27 |
28 | static getMethodProps(id='') {
29 | switch(id) {
30 | case METHOD.Automatic:
31 | return {name: "Automatic", description: ""};
32 | default:
33 | throw Error("Unknown method " + id);
34 | }
35 | }
36 |
37 | static getMethodByType(type) {
38 | type = type.toLowerCase();
39 |
40 | let keys = Object.keys(METHOD);
41 |
42 | for(let name of keys) {
43 | if(type === name.toLowerCase()) return METHOD[name];
44 | }
45 |
46 | return null;
47 | }
48 | }
49 |
50 | module.exports = OptimalPacker;
--------------------------------------------------------------------------------
/packers/Packer.js:
--------------------------------------------------------------------------------
1 | const METHOD = {
2 | Default: "Default"
3 | };
4 |
5 | class Packer {
6 |
7 | constructor() {
8 | }
9 |
10 | pack(data, method) {
11 | throw Error("Abstarct method. Override it.");
12 | }
13 |
14 |
15 | static get type() {
16 | return "Default";
17 | }
18 |
19 | static get defaultMethod() {
20 | return METHOD.Default;
21 | }
22 |
23 | static get methods() {
24 | return METHOD;
25 | }
26 |
27 | static getMethodProps(id=0) {
28 | return {name: "Default", description: "Default placement"};
29 | }
30 | }
31 |
32 | module.exports = Packer;
--------------------------------------------------------------------------------
/packers/index.js:
--------------------------------------------------------------------------------
1 | let MaxRectsPacker = require("./MaxRectsPacker");
2 | let MaxRectsBin = require("./MaxRectsBin");
3 | let OptimalPacker = require("./OptimalPacker");
4 |
5 | const list = [
6 | MaxRectsBin,
7 | MaxRectsPacker,
8 | OptimalPacker
9 | ];
10 |
11 | function getPackerByType(type) {
12 | type = type.toLowerCase();
13 |
14 | for(let item of list) {
15 | if(item.type.toLowerCase() === type) {
16 | return item;
17 | }
18 | }
19 | return null;
20 | }
21 |
22 | module.exports.getPackerByType = getPackerByType;
23 | module.exports.list = list;
--------------------------------------------------------------------------------
/utils/TextureRenderer.js:
--------------------------------------------------------------------------------
1 | let Jimp = require("jimp");
2 |
3 | class TextureRenderer {
4 |
5 | constructor(data, options={}, callback) {
6 | this.buffer = null;
7 | this.data = data;
8 |
9 | this.callback = callback;
10 |
11 | this.width = 0;
12 | this.height = 0;
13 |
14 | this.render(data, options);
15 | }
16 |
17 | static getSize(data, options={}) {
18 | let width = options.width || 0;
19 | let height = options.height || 0;
20 | let padding = options.padding || 0;
21 | let extrude = options.extrude || 0;
22 |
23 | if(!options.fixedSize) {
24 | width = 0;
25 | height = 0;
26 |
27 | for (let item of data) {
28 |
29 | let w = item.frame.x + item.frame.w;
30 | let h = item.frame.y + item.frame.h;
31 |
32 | if(item.rotated) {
33 | w = item.frame.x + item.frame.h;
34 | h = item.frame.y + item.frame.w;
35 | }
36 |
37 | if (w > width) {
38 | width = w;
39 | }
40 | if (h > height) {
41 | height = h;
42 | }
43 | }
44 |
45 | width += padding + extrude;
46 | height += padding + extrude;
47 | }
48 |
49 | if (options.powerOfTwo) {
50 | let sw = Math.round(Math.log(width)/Math.log(2));
51 | let sh = Math.round(Math.log(height)/Math.log(2));
52 |
53 | let pw = Math.pow(2, sw);
54 | let ph = Math.pow(2, sh);
55 |
56 | if(pw < width) pw = Math.pow(2, sw + 1);
57 | if(ph < height) ph = Math.pow(2, sh + 1);
58 |
59 | width = pw;
60 | height = ph;
61 | }
62 |
63 | return { width, height };
64 | }
65 |
66 | render(data, options={}) {
67 | let { width, height } = TextureRenderer.getSize(data, options);
68 |
69 | this.width = width;
70 | this.height = height;
71 |
72 | new Jimp(width, height, 0x0, (err, image) => {
73 | this.buffer = image;
74 |
75 | for(let item of data) {
76 | this.renderItem(item, options);
77 | }
78 |
79 | let filter = new options.filter();
80 | filter.apply(image);
81 |
82 | if(options.scale && options.scale !== 1) {
83 | let scaleMethod = Jimp.RESIZE_BILINEAR;
84 |
85 | if(options.scaleMethod === "NEAREST_NEIGHBOR") scaleMethod = Jimp.RESIZE_NEAREST_NEIGHBOR;
86 | if(options.scaleMethod === "BICUBIC") scaleMethod = Jimp.RESIZE_BICUBIC;
87 | if(options.scaleMethod === "HERMITE") scaleMethod = Jimp.RESIZE_HERMITE;
88 | if(options.scaleMethod === "BEZIER") scaleMethod = Jimp.RESIZE_BEZIER;
89 |
90 | image.resize(Math.round(width * options.scale) || 1, Math.round(height * options.scale) || 1, scaleMethod);
91 | }
92 |
93 | if(this.callback) this.callback(this);
94 | });
95 | }
96 |
97 | renderItem(item, options) {
98 | if(!item.skipRender) {
99 |
100 | let img = item.image;
101 |
102 | let dx = item.frame.x;
103 | let dy = item.frame.y;
104 | let sx = item.spriteSourceSize.x;
105 | let sy = item.spriteSourceSize.y;
106 | let sw = item.spriteSourceSize.w;
107 | let sh = item.spriteSourceSize.h;
108 | let ow = item.sourceSize.w;
109 | let oh = item.sourceSize.h;
110 |
111 | if (item.rotated) {
112 | img = img.clone();
113 | img.rotate(90);
114 |
115 | sx = item.sourceSize.h - item.spriteSourceSize.h - item.spriteSourceSize.y;
116 | sy = item.spriteSourceSize.x;
117 | sw = item.spriteSourceSize.h;
118 | sh = item.spriteSourceSize.w;
119 | ow = item.sourceSize.h;
120 | oh = item.sourceSize.w;
121 | }
122 |
123 | if(options.extrude) {
124 | let extrudeImage = img.clone();
125 |
126 | //Render corners
127 | extrudeImage.resize(1, 1);
128 | extrudeImage.blit(img, 0, 0, 0, 0, 1, 1);
129 | extrudeImage.resize(options.extrude, options.extrude);
130 | this.buffer.blit(extrudeImage, dx - options.extrude, dy - options.extrude, 0, 0, options.extrude, options.extrude);
131 |
132 | extrudeImage.resize(1, 1);
133 | extrudeImage.blit(img, 0, 0, ow-1, 0, 1, 1);
134 | extrudeImage.resize(options.extrude, options.extrude);
135 | this.buffer.blit(extrudeImage, dx + sw, dy - options.extrude, 0, 0, options.extrude, options.extrude);
136 |
137 | extrudeImage.resize(1, 1);
138 | extrudeImage.blit(img, 0, 0, 0, oh-1, 1, 1);
139 | extrudeImage.resize(options.extrude, options.extrude);
140 | this.buffer.blit(extrudeImage, dx - options.extrude, dy + sh, 0, 0, options.extrude, options.extrude);
141 |
142 | extrudeImage.resize(1, 1);
143 | extrudeImage.blit(img, 0, 0, ow-1, oh-1, 1, 1);
144 | extrudeImage.resize(options.extrude, options.extrude);
145 | this.buffer.blit(extrudeImage, dx + sw, dy + sh, 0, 0, options.extrude, options.extrude);
146 |
147 | //Render borders
148 | extrudeImage.resize(1, sh);
149 | extrudeImage.blit(img, 0, 0, 0, sy, 1, sh);
150 | extrudeImage.resize(options.extrude, sh);
151 | this.buffer.blit(extrudeImage, dx - options.extrude, dy, 0, 0, options.extrude, sh);
152 |
153 | extrudeImage.resize(1, sh);
154 | extrudeImage.blit(img, 0, 0, ow-1, sy, 1, sh);
155 | extrudeImage.resize(options.extrude, sh);
156 | this.buffer.blit(extrudeImage, dx + sw, dy, 0, 0, options.extrude, sh);
157 |
158 | extrudeImage.resize(sw, 1);
159 | extrudeImage.blit(img, 0, 0, sx, 0, sw, 1);
160 | extrudeImage.resize(sw, options.extrude);
161 | this.buffer.blit(extrudeImage, dx, dy - options.extrude, 0, 0, sw, options.extrude);
162 |
163 | extrudeImage.resize(sw, 1);
164 | extrudeImage.blit(img, 0, 0, sx, oh-1, sw, 1);
165 | extrudeImage.resize(sw, options.extrude);
166 | this.buffer.blit(extrudeImage, dx, dy + sh, 0, 0, sw, options.extrude);
167 | }
168 |
169 | this.buffer.blit(img, dx, dy, sx, sy, sw, sh);
170 | }
171 | }
172 | }
173 |
174 | module.exports = TextureRenderer;
--------------------------------------------------------------------------------
/utils/Trimmer.js:
--------------------------------------------------------------------------------
1 | class Trimmer {
2 |
3 | constructor() {
4 |
5 | }
6 |
7 | static getAlpha(data, width, x, y) {
8 | return data[((y * (width * 4)) + (x * 4)) + 3];
9 | }
10 |
11 | static getLeftSpace(data, width, height, threshold=0) {
12 | let x = 0;
13 |
14 | for(x=0; x threshold) {
17 | return x;
18 | }
19 | }
20 | }
21 |
22 | return 0;
23 | }
24 |
25 | static getRightSpace(data, width, height, threshold=0) {
26 | let x = 0;
27 |
28 | for(x=width-1; x>=0; x--) {
29 | for(let y=0; y threshold) {
31 | return width-x-1;
32 | }
33 | }
34 | }
35 |
36 | return 0;
37 | }
38 |
39 | static getTopSpace(data, width, height, threshold=0) {
40 | let y = 0;
41 |
42 | for(y=0; y threshold) {
45 | return y;
46 | }
47 | }
48 | }
49 |
50 | return 0;
51 | }
52 |
53 | static getBottomSpace(data, width, height, threshold=0) {
54 | let y = 0;
55 |
56 | for(y=height-1; y>=0; y--) {
57 | for(let x=0; x threshold) {
59 | return height-y-1;
60 | }
61 | }
62 | }
63 |
64 | return 0;
65 | }
66 |
67 | static trim(rects, threshold=0) {
68 |
69 | for(let item of rects) {
70 |
71 | let img = item.image;
72 | let data = img.bitmap.data;
73 | let spaces = {left: 0, right: 0, top: 0, bottom: 0};
74 |
75 | spaces.left = this.getLeftSpace(data, img.width, img.height, threshold);
76 |
77 | if(spaces.left !== img.width) {
78 | spaces.right = this.getRightSpace(data, img.width, img.height, threshold);
79 | spaces.top = this.getTopSpace(data, img.width, img.height, threshold);
80 | spaces.bottom = this.getBottomSpace(data, img.width, img.height, threshold);
81 |
82 | if(spaces.left > 0 || spaces.right > 0 || spaces.top > 0 || spaces.bottom > 0) {
83 | item.trimmed = true;
84 | item.spriteSourceSize.x = spaces.left;
85 | item.spriteSourceSize.y = spaces.top;
86 | item.spriteSourceSize.w = img.width-spaces.left-spaces.right;
87 | item.spriteSourceSize.h = img.height-spaces.top-spaces.bottom;
88 | }
89 | }
90 | else {
91 | item.trimmed = true;
92 | item.spriteSourceSize.x = 0;
93 | item.spriteSourceSize.y = 0;
94 | item.spriteSourceSize.w = 1;
95 | item.spriteSourceSize.h = 1;
96 | }
97 |
98 | if(item.trimmed) {
99 | item.frame.w = item.spriteSourceSize.w;
100 | item.frame.h = item.spriteSourceSize.h;
101 | }
102 | }
103 | }
104 | }
105 |
106 | module.exports = Trimmer;
--------------------------------------------------------------------------------