Node 實(shí)戰(zhàn) | 我用百來行代碼實(shí)現(xiàn)了一個圖集打包工具
大廠技術(shù)??高級前端??Node進(jìn)階
點(diǎn)擊上方?程序員成長指北,關(guān)注公眾號
回復(fù)1,加入高級Node交流群
作者:起小就些熊 (作者授權(quán)轉(zhuǎn)載)
原文鏈接:https://juejin.cn/post/7035809483666227230
前言
偶然發(fā)現(xiàn)一款很好用的跨平臺圖像編解碼庫node-images.
仔細(xì)閱讀其API, 就萌生了一個使用其制作精靈圖集的想法.
于是就誕生了這個工具sprites-pack-tool.
你
可以在github查看:https://github.com/xdq1553/MySpritesPackTool
你可以使用npm安裝:https://www.npmjs.com/package/sprites-pack-tool
對于精靈圖集, 我想大家都不陌生.
比如把下面的幾張圖片合成一張.

得到一張圖:
這一張圖集就是我用本文介紹的工具打包合成的.
合成的圖片品質(zhì)依然十分高呢.
為什么需要使用圖集
web開發(fā)
我們在web開發(fā)中, 每次在瀏覽器展示一張圖片都需要請求一次服務(wù)器資源.
舉個例子, 3次請求每次4k, 和一次請求12k還是有本質(zhì)區(qū)別的, 然后更多的時候一次請求并不是3 * 4k.
使用圖集能讓我們優(yōu)化資源加載, 提高網(wǎng)站的性能.
游戲開發(fā)
在游戲開發(fā)中, 圖集的使用至關(guān)重要, 不管是一般幀動畫還是svga等動畫解決方案, 都不會每張圖片去請求資源.
更多的時候, 我們都是打包成圖集, 而圖集打包工具texturepacker更是大行其道.
其次, 游戲場景過多, 我們一般都需要分步加載資源, 有的時候一個動畫模型, 涉及的圖片少則十來張, 多則近百張.
圖集的使用不可或缺.
下面我們就來看下如何編寫一款圖集打包工具.
工具設(shè)計
開發(fā)一個圖集打包工具腳本需要什么技能.
nodejs編程能力二維矩形裝箱算法
然后我們思考如何去打包一張圖集.
我們需要找到需要打包的文件夾, 可能有多個或者嵌套文件夾. 圖集是多張散圖拼合而成. 圖集的大小需要可配置 盡可能的壓縮圖集空間, 使每張圖緊密貼合 每個文件夾打包成一個圖集, 需要考慮圖片過多的情況 可能需要生成圖集所需要的json文件, 記錄圖片位置信息
開始編寫腳本
腳本IO
我這里是這樣設(shè)計.
首先我們需要一個打包對象實(shí)例MySpritePackTool, 同時支持寫入配置參數(shù)options.
/**?圖集打包對象?*/
const?MySpritePackTool?=?function?(opt)?{
????this.options?=?{
????????//一個文件夾圖片過多或者過長?遞歸最大次數(shù)
????????maxCount:?opt.maxCount?||?2,
????????//需要打包圖集的文件路徑
????????assetsPath:?opt.assetsPath,
????????//輸出文件路徑
????????outPutPath:?opt.outPutPath,
????????//一張圖集打包最大size
????????maxSize:?{?width:?2048,?height:?2048?}
????}
};
然后我們需要輸出這個對象, 可以被其他項(xiàng)目所引用.
module.exports?=?MySpritePackTool;
遍歷文件生成節(jié)點(diǎn)樹
我們的輸入?yún)?shù)盡可能的少, 這樣就需要我們程序去遍歷文件夾.
例如, 我們有如下的目錄樹:
|--assets
???|--index
??????|--img-3.png
??????|--img-4.png
???|--login
??????|--img-5.png
???|--img-1.png
???|--img-2.png
我們需要每個文件夾下打包出一張圖集.
思考: 需要什么樣的數(shù)據(jù)結(jié)構(gòu)?
首先便于js解析, 我們約定一個對象,
每一層, 需要一個圖片信息容器assets;
一個所包含的圖片標(biāo)識keys;
一個文件夾名字, 也方便我們后面為圖集命名name;
然后每層文件夾前套相同對象;
結(jié)構(gòu)如下:
{
??assets:?[
????{
??????id:?'assets/img-1.png',
??????width:?190,
??????height:?187
????},
????...
??],
??name:?'assets',
??keys:?'img-1.png,img-2.png,',
??index:?{
????assets:?[
????????{
????????????id:?'assets/index/img-3.png',
????????????width:?190,
????????????height:?187
????????},
????????...
????],
????name:?'index',
????keys:?'img-3.png,img-4.png,'
??},
??login:?{
????assets:?[
????????{
????????????id:?'assets/login/img-5.png',
????????????width:?190,
????????????height:?187
????????}
????],
????name:?'index',
????keys:?'img-5.png,'
??},
}
不難發(fā)現(xiàn), 我們已經(jīng)可以得到需要打包的所有文件和文件夾.
那么用程序如何實(shí)現(xiàn)呢?
主要用到nodejs的fs模塊來遞歸操作文件夾, 并輸出所需要的節(jié)點(diǎn)樹.
注意在書寫的時候需要判斷是圖片還是文件夾.
MySpritePackTool.prototype.findAllFiles?=?function?(obj,?rootPath)?{
????let?nodeFiles?=?[];
????if?(fs.existsSync(rootPath))?{
????????//獲取所有文件名
????????nodeFiles?=?fs.readdirSync(rootPath);
????????//組裝對象
????????let?nameArr?=?rootPath.split('/');
????????obj["assets"]?=?[];
????????obj["name"]?=?nameArr[nameArr.length?-?1];
????????obj["keys"]?=?"";
????????nodeFiles.forEach(item?=>?{
????????????//判斷不是圖片路徑
????????????if?(!/(.png)|(.jpe?g)$/.test(item))?{
????????????????let?newPath?=?path.join(rootPath,?item);
????????????????//判斷存在文件?同時是文件夾系統(tǒng)
????????????????if?(fs.existsSync(newPath)?&&?fs.statSync(newPath).isDirectory())?{
????????????????????//?console.log("獲得新的地址",?newPath);
????????????????????obj[item]?=?{};
????????????????????this.findAllFiles(obj[item],?newPath);
????????????????}?else?{
????????????????????console.log(`文件路徑:?${newPath}不存在!`);
????????????????}
????????????}?else?{
????????????????console.log(`圖片路徑:?${item}`);
????????????????obj["keys"]?+=?item?+?",";
????????????????let?params?=?{};
????????????????params["id"]?=?path.resolve(rootPath,?`./${item}`);
????????????????//獲得圖片寬高
????????????????params["width"]?=?images(path.resolve(rootPath,?`./${item}`)).width();
????????????????params["height"]?=?images(path.resolve(rootPath,?`./${item}`)).height();
????????????????obj["assets"].push(params);
????????????}
????????})
????}?else?{
????????console.log(`文件路徑:?${rootPath}不存在!`);
????}
}
這樣子我們就可以得到我們所需要的節(jié)點(diǎn)樹了.
獲取新的圖集位置信息
我們對文件夾的操作已經(jīng)完成, 這時候我們就需要思考.
如何把這些零散的圖片打包到一張圖片上.
散圖有兩個信息, 一個width和一個height, 其實(shí)就是一個矩形.
我們現(xiàn)在所要做的就是把這些不同面積的矩形放到一個具有最大長寬的大矩形中.跳開圖片, 從矩形放置入手
二維矩形裝箱算法有不少, 我這里選用一種比較簡單的.
首先得到一個具有最大長寬的矩形盒子.
我們先放入一個矩形A, 這樣子, 剩余區(qū)域就有兩塊: 矩形A的右邊和矩形A的下邊.
然后我們繼續(xù)放入矩形B, 可以先右再下, 然后基于矩形B又有兩塊空白空間.
依次類推, 我們就可以將合適的矩形全部放入.
舉個例子
把左邊的散裝矩形放入右邊的矩形框中, 可以得到:
可以看到, 我們節(jié)省了很多空間, 矩形排列緊湊.
如果用代碼實(shí)現(xiàn), 是怎么樣的呢?
/**?
?*?確定寬高?w?h
?*?空白區(qū)域先放一個,?剩下的尋找右邊和下邊
?*?是否有滿足右邊的,?有則?放入?無則?繼續(xù)遍歷
?*?是否有滿足下邊的,?有則?放入?無則?繼續(xù)遍歷
?*/
const?Packer?=?function?(w,?h)?{
????this.root?=?{?x:?0,?y:?0,?width:?w,?height:?h?};
????//?/**?匹配所有的方格?*/
????Packer.prototype.fit?=?function?(blocks)?{
????????let?node;
????????for?(let?i?=?0;?i?????????????let?block?=?blocks[i];
????????????node?=?this.findNode(this.root,?block.width,?block.height);
????????????if?(node)?{
????????????????let?fit?=?this.findEmptyNode(node,?block.width,?block.height);
????????????????block.x?=?fit.x;
????????????????block.y?=?fit.y;
????????????????block.fit?=?fit;
????????????}
????????}
????}
????/**?找到可以放入的節(jié)點(diǎn)?*/
????Packer.prototype.findNode?=?function?(node,?w,?h)?{
????????if?(node.used)?{
????????????return?this.findNode(node.rightArea,?w,?h)?||?this.findNode(node.downArea,?w,?h);
????????}?else?if?(node.width?>=?w?&&?node.height?>=?h)?{
????????????return?node;
????????}?else?{
????????????return?null;
????????}
????}
????/**?找到空位?*/
????Packer.prototype.findEmptyNode?=?function?(node,?w,?h)?{
????????//已經(jīng)使用過的?刪除?
????????node.used?=?true;
????????//右邊空間
????????node.rightArea?=?{
????????????x:?node.x?+?w,
????????????y:?node.y,
????????????width:?node.width?-?w,
????????????height:?h
????????};
????????//下方空位
????????node.downArea?=?{
????????????x:?node.x,
????????????y:?node.y?+?h,
????????????width:?node.width,
????????????height:?node.height?-?h
????????}
????????return?node;
????}
}
使用遞歸, 代碼量很少, 但是功能強(qiáng)大.
但是有一個問題,如果超出定長定寬, 或者一個矩形裝不完, 我們的算法是不會放入到大矩形中的. 這樣子就有點(diǎn)不滿足我們的圖集打包思路了.
所以我們還需要將這個算法改進(jìn)一下;
加入兩個變量, 一個記錄使用的總的區(qū)域, 一個記錄未被裝入的矩形.
//記錄使用的總的區(qū)域
this.usedArea?=?{?width:?0,?height:?0?};
//記錄未被裝入的矩形
this.levelBlocks?=?[];
詳細(xì)代碼可以查看源碼中的packing.
當(dāng)然, 這里只是最簡單的一種二維裝箱算法
還有一種加強(qiáng)版的裝箱算法, 我放在源碼里了, 這里就不贅述了, 原理基本一致
現(xiàn)在, 我們已經(jīng)可以將矩形合適的裝箱了, 那怎么使用去處理成圖集呢?
定義一個dealImgsPacking方法, 繼續(xù)去處理我們的節(jié)點(diǎn)樹.
這里用到了我們的配置項(xiàng)maxCount, 就是為了一張圖集裝不完, 多打出幾張圖集的作用.
然后我們打包出來的圖集命名使用文件夾 + 當(dāng)前是第幾張的形式.
`${obj['name']?+?(count???"-"?+?count?:?'')}`
具體方法如下:
MySpritePackTool.prototype.dealImgsPacking?=?function?(obj)?{
????let?count?=?0;
????if?(obj.hasOwnProperty("assets"))?{
????????let?newBlocks?=?obj["assets"];
????????obj["assets"]?=?[];
????????while?(newBlocks.length?>?0?&&?count?this.options.maxCount)?{
????????????let?packer1?=?new?Packer(this.options.maxSize.width,?this.options.maxSize.height);
????????????packer1.fit(newBlocks);
????????????let?sheets1?=?{
????????????????maxArea:?packer1.usedArea,
????????????????atlas:?newBlocks,
????????????????fileName:?`${obj['name']?+?(count???"-"?+?count?:?'')}`
????????????};
????????????newBlocks?=?packer1.levelBlocks;
????????????obj["assets"].push(sheets1);
????????????count++;
????????}
????}
????for?(let?item?in?obj)?{
????????if?(obj[item].hasOwnProperty("assets"))?{
????????????this.dealImgsPacking(obj[item]);
????????}
????}
}
通過這個方法我們改造了之前的節(jié)點(diǎn)樹;
將之前節(jié)點(diǎn)樹中的assest變?yōu)榱艘粋€數(shù)組, 每個數(shù)組元素代表一張圖集信息.
結(jié)構(gòu)如下:
??assets:?[
????{?
????????maxArea:?{?width:?180,height:?340?},?
????????atlas:?[
????????????{
????????????????id:?'assets/index/img-3.png',
????????????????width:?190,
????????????????height:?187,
????????????????x:?0,
????????????????y:?0
????????????}
????????],?
????????fileName:?'assets'?},
????????...
??]
我們可以清晰的得到, 打包之后的圖集, 最大寬高是maxArea, 每張圖寬高位置信息是atlas,以及圖集名稱fileName.
接下來, 就是最后一步了, 繪制新的圖片, 并輸出圖片文件.
注意
我們在使用打包算法的時候, 可以先進(jìn)行一下基于圖片大小的排序 這樣以來打包出來的圖集會留白更小
圖集打包并輸出
這里圖集的繪制和輸出均是使用了node-images的API;
遍歷之前得到的節(jié)點(diǎn)樹, 首先繪制一張maxArea大小的空白圖像.
images(item["maxArea"].width,?item["maxArea"].height)
然后遍歷一張圖集所需要的圖片信息, 將每一張圖片繪制到空白圖像上.
//繪制空白圖像
let?newSprites?=?images(item["maxArea"].width,?item["maxArea"].height);
//繪制圖集
imgObj.forEach(it?=>?{
????newSprites.draw(images(it["id"]),?it["x"],?it["y"]);
});
然后繪制完一張圖集輸出一張.
newSprites.save(`${this.options.outPutPath}/${item['fileName']}.png`);
最后對節(jié)點(diǎn)樹遞歸調(diào)用, 繪制出所有的圖集.
具體代碼如下:
MySpritePackTool.prototype.drawImages?=?function?(obj)?{
????let?count?=?0;
????if?(obj.hasOwnProperty("assets"))?{
????????//打包出一個或者多個圖集
????????let?imgsInfo?=?obj["assets"];
????????imgsInfo.forEach(item?=>?{
????????????if?(item.hasOwnProperty("atlas"))?{
????????????????let?imgObj?=?item["atlas"];
????????????????//?console.log("8888",imgObj)
????????????????//繪制一張透明圖像
????????????????let?newSprites?=?images(item["maxArea"].width,?item["maxArea"].height);
????????????????imgObj.forEach(it?=>?{
????????????????????newSprites.draw(images(it["id"]),?it["x"],?it["y"]);
????????????????});
????????????????newSprites.save(`${this.options.outPutPath}/${item['fileName']}.png`);
????????????????count++;
????????????}
????????})
????}
????for?(let?item?in?obj)?{
????????if?(obj[item].hasOwnProperty("assets"))?{
????????????this.drawImages(obj[item]);
????????}
????}
}
這樣子, 我們就大功告成了,
運(yùn)行測試一下, 可以得到如下的圖集:
效果還不錯.
如何使用
安裝
npm?i?sprites-pack-tool
使用
const?MySpritePackTool?=?require("sprites-pack-tool");
const?path?=?require("path");
/**?打包最多遞歸次數(shù)?*/
const?MAX_COUNT?=?2;
//需要合成的圖集的路徑
const?assetsPath?=?path.resolve(__dirname,?"./assets");
/**?圖集打包工具配置*/
const?mySpritePackTool?=?new?MySpritePackTool({
????//一個文件夾圖片過多或者過長?遞歸最大次數(shù)
????maxCount:?MAX_COUNT,
????//需要打包圖集的文件路徑
????assetsPath:?assetsPath,
????//輸出文件路徑
????outPutPath:?path.resolve(__dirname,?"./res"),
????//一張圖集打包最大size
????maxSize:?{?width:?2048,height:?2048}
});
/**?圖集打包?*/
mySpritePackTool.Pack2Sprite();
展望
當(dāng)然, 這個工具只是初版, 后續(xù)還會繼續(xù)優(yōu)化并增加新的功能.
算法可以繼續(xù)優(yōu)化, 現(xiàn)在留白也挺多. 文件夾操作,可以優(yōu)化. 比如寫入圖片可以每個文件夾下一張圖集. 增加更多配置項(xiàng), 比如開啟圖片壓縮 增加json文件 ...
我大致看了下, 市場上有幾款圖集打包工具, 要么基于texturePacker, 要么基于imagemagick;
使用這倆應(yīng)用開放的API也是可以打包圖集的, 效果品質(zhì)可能更好.
但是你還得額外安裝一個應(yīng)用.
同樣的, 你也可以使用webpack的一些loader或者plugins. 目的都是打包圖集.
本文介紹的工具比較輕量, 但是可堪一用, 同時開箱即可.
后文
有一陣子沒有寫文章了, 這個周末偶然想寫一個這樣的工具, 就付諸實(shí)踐了, 結(jié)果還不錯.
如果你還有更好的方式, 可以留下你的評論, 感激不盡~.
歡迎大家拍磚指正, 筆者功力尚淺, 如有不當(dāng)之處請斧正.
源碼
你可以在github查看:https://github.com/xdq1553/MySpritesPackTool
你可以使用npm安裝:https://www.npmjs.com/package/sprites-pack-tool
參考
node-images:https://github.com/zhangyuanwei/node-images
spritesheet:https://github.com/krzysztof-o/spritesheet.js
文章粗淺, 望諸位不吝您的評論和點(diǎn)贊~
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
???“分享、點(diǎn)贊、在看” 支持一波??
