溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何使用node開發(fā)一款圖集打包工具

發(fā)布時(shí)間:2021-11-30 09:44:06 來(lái)源:億速云 閱讀:129 作者:小新 欄目:web開發(fā)

這篇文章主要為大家展示了“如何使用node開發(fā)一款圖集打包工具”,內(nèi)容簡(jiǎn)而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領(lǐng)大家一起研究并學(xué)習(xí)一下“如何使用node開發(fā)一款圖集打包工具”這篇文章吧。

比如把下面的幾張圖片合成一張.

如何使用node開發(fā)一款圖集打包工具

如何使用node開發(fā)一款圖集打包工具

這一張圖集就是我用本文介紹的工具打包合成的.

合成的圖片品質(zhì)依然十分高呢.

為什么需要使用圖集

web開發(fā)

我們?cè)趙eb開發(fā)中, 每次在瀏覽器展示一張圖片都需要請(qǐng)求一次服務(wù)器資源.

舉個(gè)例子, 3次請(qǐng)求每次4k, 和一次請(qǐng)求12k還是有本質(zhì)區(qū)別的, 然后更多的時(shí)候一次請(qǐng)求并不是3 * 4k.

使用圖集能讓我們優(yōu)化資源加載, 提高網(wǎng)站的性能.

游戲開發(fā)

在游戲開發(fā)中, 圖集的使用至關(guān)重要, 不管是一般幀動(dòng)畫還是svga等動(dòng)畫解決方案, 都不會(huì)每張圖片去請(qǐng)求資源.

更多的時(shí)候, 我們都是打包成圖集, 而圖集打包工具texturepacker更是大行其道.

其次, 游戲場(chǎng)景過(guò)多, 我們一般都需要分步加載資源, 有的時(shí)候一個(gè)動(dòng)畫模型, 涉及的圖片少則十來(lái)張, 多則近百?gòu)?

圖集的使用不可或缺.

下面我們就來(lái)看下如何編寫一款圖集打包工具.

工具設(shè)計(jì)

開發(fā)一個(gè)圖集打包工具腳本需要什么技能.

  • node.js編程能力

  • 二維矩形裝箱算法

然后我們思考如何去打包一張圖集.

  • 我們需要找到需要打包的文件夾, 可能有多個(gè)或者嵌套文件夾.

  • 圖集是多張散圖拼合而成.

  • 圖集的大小需要可配置

  • 盡可能的壓縮圖集空間, 使每張圖緊密貼合

  • 每個(gè)文件夾打包成一個(gè)圖集, 需要考慮圖片過(guò)多的情況

  • 可能需要生成圖集所需要的json文件, 記錄圖片位置信息

開始編寫腳本

腳本IO

我這里是這樣設(shè)計(jì).

首先我們需要一個(gè)打包對(duì)象實(shí)例MySpritePackTool, 同時(shí)支持寫入配置參數(shù)options.

/** 圖集打包對(duì)象 */
const MySpritePackTool = function (opt) {
    this.options = {
        //一個(gè)文件夾圖片過(guò)多或者過(guò)長(zhǎng) 遞歸最大次數(shù)
        maxCount: opt.maxCount || 2,
        //需要打包圖集的文件路徑
        assetsPath: opt.assetsPath,
        //輸出文件路徑
        outPutPath: opt.outPutPath,
        //一張圖集打包最大size
        maxSize: { width: 2048, height: 2048 }
    }
};

然后我們需要輸出這個(gè)對(duì)象, 可以被其他項(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

我們需要每個(gè)文件夾下打包出一張圖集.

思考: 需要什么樣的數(shù)據(jù)結(jié)構(gòu)?

首先便于js解析, 我們約定一個(gè)對(duì)象,

每一層, 需要一個(gè)圖片信息容器assets;

一個(gè)所包含的圖片標(biāo)識(shí)keys;

一個(gè)文件夾名字, 也方便我們后面為圖集命名name;

然后每層文件夾前套相同對(duì)象;

結(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模塊來(lái)遞歸操作文件夾, 并輸出所需要的節(jié)點(diǎn)樹.

注意在書寫的時(shí)候需要判斷是圖片還是文件夾.

MySpritePackTool.prototype.findAllFiles = function (obj, rootPath) {
    let nodeFiles = [];
    if (fs.existsSync(rootPath)) {
        //獲取所有文件名
        nodeFiles = fs.readdirSync(rootPath);

        //組裝對(duì)象
        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);
                //判斷存在文件 同時(shí)是文件夾系統(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)樹了.

獲取新的圖集位置信息

我們對(duì)文件夾的操作已經(jīng)完成, 這時(shí)候我們就需要思考.

如何把這些零散的圖片打包到一張圖片上.

散圖有兩個(gè)信息, 一個(gè)width和一個(gè)height, 其實(shí)就是一個(gè)矩形.

我們現(xiàn)在所要做的就是把這些不同面積的矩形放到一個(gè)具有最大長(zhǎng)寬的大矩形中.


跳開圖片, 從矩形放置入手

二維矩形裝箱算法有不少, 我這里選用一種比較簡(jiǎn)單的.

首先得到一個(gè)具有最大長(zhǎng)寬的矩形盒子.

我們先放入一個(gè)矩形A, 這樣子, 剩余區(qū)域就有兩塊: 矩形A的右邊矩形A的下邊.

如何使用node開發(fā)一款圖集打包工具

然后我們繼續(xù)放入矩形B, 可以先右再下, 然后基于矩形B又有兩塊空白空間.

如何使用node開發(fā)一款圖集打包工具

依次類推, 我們就可以將合適的矩形全部放入.

舉個(gè)例子

把左邊的散裝矩形放入右邊的矩形框中, 可以得到:

如何使用node開發(fā)一款圖集打包工具

可以看到, 我們節(jié)省了很多空間, 矩形排列緊湊.

如果用代碼實(shí)現(xiàn), 是怎么樣的呢?

/** 
 * 確定寬高 w h
 * 空白區(qū)域先放一個(gè), 剩下的尋找右邊和下邊
 * 是否有滿足右邊的, 有則 放入 無(wú)則 繼續(xù)遍歷
 * 是否有滿足下邊的, 有則 放入 無(wú)則 繼續(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 < blocks.length; 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)使用過(guò)的 刪除 
        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)大.

但是有一個(gè)問(wèn)題, 如果超出定長(zhǎng)定寬, 或者一個(gè)矩形裝不完, 我們的算法是不會(huì)放入到大矩形中的.

這樣子就有點(diǎn)不滿足我們的圖集打包思路了.

所以我們還需要將這個(gè)算法改進(jìn)一下;

加入兩個(gè)變量, 一個(gè)記錄使用的總的區(qū)域, 一個(gè)記錄未被裝入的矩形.

//記錄使用的總的區(qū)域
this.usedArea = { width: 0, height: 0 };
//記錄未被裝入的矩形
this.levelBlocks = [];

詳細(xì)代碼可以查看源碼中的packing.

當(dāng)然, 這里只是最簡(jiǎn)單的一種二維裝箱算法

還有一種加強(qiáng)版的裝箱算法, 我放在源碼里了, 這里就不贅述了, 原理基本一致

現(xiàn)在, 我們已經(jīng)可以將矩形合適的裝箱了, 那怎么使用去處理成圖集呢?

定義一個(gè)dealImgsPacking方法, 繼續(xù)去處理我們的節(jié)點(diǎn)樹.

這里用到了我們的配置項(xiàng)maxCount, 就是為了一張圖集裝不完, 多打出幾張圖集的作用.

然后我們打包出來(lái)的圖集命名使用文件夾 + 當(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]);
        }
    }
}

通過(guò)這個(gè)方法我們改造了之前的節(jié)點(diǎn)樹;

將之前節(jié)點(diǎn)樹中的assest變?yōu)榱艘粋€(gè)數(shù)組, 每個(gè)數(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.

接下來(lái), 就是最后一步了, 繪制新的圖片, 并輸出圖片文件.

注意
我們?cè)谑褂么虬惴ǖ臅r(shí)候, 可以先進(jìn)行一下基于圖片大小的排序
這樣以來(lái)打包出來(lái)的圖集會(huì)留白更小

圖集打包并輸出

這里圖集的繪制和輸出均是使用了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`);

最后對(duì)節(jié)點(diǎn)樹遞歸調(diào)用, 繪制出所有的圖集.

具體代碼如下:

MySpritePackTool.prototype.drawImages = function (obj) {
    let count = 0;
    if (obj.hasOwnProperty("assets")) {
        //打包出一個(gè)或者多個(gè)圖集
        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)行測(cè)試一下, 可以得到如下的圖集:

如何使用node開發(fā)一款圖集打包工具

如何使用node開發(fā)一款圖集打包工具

效果還不錯(cuò).

如何使用

安裝

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({
    //一個(gè)文件夾圖片過(guò)多或者過(guò)長(zhǎng) 遞歸最大次數(shù)
    maxCount: MAX_COUNT,
    //需要打包圖集的文件路徑
    assetsPath: assetsPath,
    //輸出文件路徑
    outPutPath: path.resolve(__dirname, "./res"),
    //一張圖集打包最大size
    maxSize: { width: 2048,height: 2048}
});
/** 圖集打包 */
mySpritePackTool.Pack2Sprite();

以上是“如何使用node開發(fā)一款圖集打包工具”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI