├── .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 | [![Stats](https://nodei.co/npm/free-tex-packer-core.png?downloads=true&stars=true)](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; --------------------------------------------------------------------------------