首頁 >web前端 >js教程 >手把手帶你使用node開發一款圖集打包工具

手把手帶你使用node開發一款圖集打包工具

青灯夜游
青灯夜游轉載
2021-11-29 18:53:472317瀏覽

這篇文章就來手把手教你使用node手寫一款圖集打包工具,有一定的參考價值,希望對大家有所幫助!

手把手帶你使用node開發一款圖集打包工具

偶然發現一款很好用的跨平台圖像編解碼庫node-images.

仔細閱讀其API,就萌生了一個使用其製作精靈圖集的想法.

於是就誕生了這個工具sprites-pack-tool.

你可以在github查看

https://github.com/xdq1553/MySpritesPackTool

你可以使用npm安裝

#https://www.npmjs.com /package/sprites-pack-tool


對於精靈圖集, 我想大家都不陌生.

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

手把手帶你使用node開發一款圖集打包工具

手把手帶你使用node開發一款圖集打包工具

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

合成的圖片品質依然十分高呢.

為什麼需要使用圖集

web開發

我們在web開發, 每次在瀏覽器展示一張圖片都需要請求一次伺服器資源.

舉例, 3次請求每次4k, 和一次請求12k還是有本質區別的, 然後更多的時候一次請求並不是3 * 4k.

使用圖集能讓我們優化資源加載, 提高網站的性能.

遊戲開發

在遊戲開發中, 圖集的使用至關重要, 不管是一般幀動畫還是svga等動畫解決方案, 都不會每張圖片去請求資源.

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

其次, 遊戲場景過多, 我們一般都需要分步加載資源, 有的時候一個動畫模型, 涉及的圖片少則十來張, 多則近百張.

圖集的使用不可或缺.

下面我們就來看如何寫一款圖集打包工具.

工具設計

開發一個圖集打包工具腳本需要什麼技能.

  • node.js程式設計能力

  • 二維矩形裝箱演算法

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

  • 我們需要找到需要打包的資料夾, 可能有多個或巢狀資料夾.

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

  • 圖集的大小需要可配置

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

  • 每個資料夾打包成一個圖集, 需要考慮圖片過多的情況

  • 可能需要產生圖集所需的json文件, 記錄圖片位置資訊

開始寫腳本

腳本IO

我這裡是這樣設計.

首先我們需要一個打包物件實例MySpritePackTool, 同時支援寫入配置參數options.

/** 图集打包对象 */
const MySpritePackTool = function (opt) {
    this.options = {
        //一个文件夹图片过多或者过长 递归最大次数
        maxCount: opt.maxCount || 2,
        //需要打包图集的文件路径
        assetsPath: opt.assetsPath,
        //输出文件路径
        outPutPath: opt.outPutPath,
        //一张图集打包最大size
        maxSize: { width: 2048, height: 2048 }
    }
};

然後我們需要輸出這個物件, 可以被其他項目所引用.

module.exports = MySpritePackTool;

遍歷檔案產生節點樹

我們的輸入參數盡可能的少, 這樣就需要我們程式去遍歷資料夾.

例如, 我們有如下的目錄樹:

|--assets
   |--index
      |--img-手把手帶你使用node開發一款圖集打包工具
      |--img-手把手帶你使用node開發一款圖集打包工具
   |--login
      |--img-手把手帶你使用node開發一款圖集打包工具
   |--img-手把手帶你使用node開發一款圖集打包工具
   |--img-手把手帶你使用node開發一款圖集打包工具

我們需要每個資料夾下方打包出一張圖集.

思考: 需要什麼樣的資料結構?

##首先便於js解析, 我們約定一個物件,

每一層, 需要一個圖片資訊容器

assets;

一個所包含的圖片標識

keys;

一個資料夾名字, 也方便我們後面為圖集命名

name;

#然後每層資料夾前套相同物件;

#結構如下:

{
  assets: [
    {
      id: 'assets/img-手把手帶你使用node開發一款圖集打包工具',
      width: 190,
      height: 187
    },
    ...
  ],
  name: 'assets',
  keys: 'img-手把手帶你使用node開發一款圖集打包工具,img-手把手帶你使用node開發一款圖集打包工具,',
  index: {
    assets: [
        {
            id: 'assets/index/img-手把手帶你使用node開發一款圖集打包工具',
            width: 190,
            height: 187
        },
        ...
    ],
    name: 'index',
    keys: 'img-手把手帶你使用node開發一款圖集打包工具,img-手把手帶你使用node開發一款圖集打包工具,'
  },
  login: {
    assets: [
        {
            id: 'assets/login/img-手把手帶你使用node開發一款圖集打包工具',
            width: 190,
            height: 187
        }
    ],
    name: 'index',
    keys: 'img-手把手帶你使用node開發一款圖集打包工具,'
  },
}

不難發現, 我們已經可以得到需要打包的所有檔案和資料夾.

那麼用程式如何實作呢?

主要用到nodejs的

fs模組來遞歸操作資料夾, 並輸出所需的節點樹.

注意在書寫的時候需要判斷是圖片還是資料夾.

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);
                //判断存在文件 同时是文件夹系统
                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}不存在!`);
    }
}

這樣子我們就可以得到我們所需要的節點樹了.

#獲取新的圖集位置資訊

我們對資料夾的操作已經完成, 這時候我們就需要思考.

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

散圖有兩個訊息, 一個

width和一個height, 其實就是一個矩形.

我们现在所要做的就是把这些不同面积的矩形放到一个具有最大长宽的大矩形中.


跳开图片, 从矩形放置入手

二维矩形装箱算法有不少, 我这里选用一种比较简单的.

首先得到一个具有最大长宽的矩形盒子.

我们先放入一个矩形A, 这样子, 剩余区域就有两块: 矩形A的右边矩形A的下边.

手把手帶你使用node開發一款圖集打包工具

然后我们继续放入矩形B, 可以先右再下, 然后基于矩形B又有两块空白空间.

手把手帶你使用node開發一款圖集打包工具

依次类推, 我们就可以将合适的矩形全部放入.

举个例子

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

手把手帶你使用node開發一款圖集打包工具

可以看到, 我们节省了很多空间, 矩形排列紧凑.

如果用代码实现, 是怎么样的呢?

/** 
 * 确定宽高 w h
 * 空白区域先放一个, 剩下的寻找右边和下边
 * 是否有满足右边的, 有则 放入 无则 继续遍历
 * 是否有满足下边的, 有则 放入 无则 继续遍历
 */
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;
            }
        }

    }

    /** 找到可以放入的节点 */
    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) {
        //已经使用过的 删除 
        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;
    }
}

使用递归, 代码量很少, 但是功能强大.

但是有一个问题, 如果超出定长定宽, 或者一个矩形装不完, 我们的算法是不会放入到大矩形中的.

这样子就有点不满足我们的图集打包思路了.

所以我们还需要将这个算法改进一下;

加入两个变量, 一个记录使用的总的区域, 一个记录未被装入的矩形.

//记录使用的总的区域
this.usedArea = { width: 0, height: 0 };
//记录未被装入的矩形
this.levelBlocks = [];

详细代码可以查看源码中的packing.

当然, 这里只是最简单的一种二维装箱算法

还有一种加强版的装箱算法, 我放在源码里了, 这里就不赘述了, 原理基本一致

现在, 我们已经可以将矩形合适的装箱了, 那怎么使用去处理成图集呢?

定义一个dealImgsPacking方法, 继续去处理我们的节点树.

这里用到了我们的配置项maxCount, 就是为了一张图集装不完, 多打出几张图集的作用.

然后我们打包出来的图集命名使用文件夹 + 当前是第几张的形式.

`${obj[&#39;name&#39;] + (count ? "-" + count : &#39;&#39;)}`

具体方法如下:

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[&#39;name&#39;] + (count ? "-" + count : &#39;&#39;)}`
            };
            newBlocks = packer1.levelBlocks;
            obj["assets"].push(sheets1);
            count++;
        }
    }
    for (let item in obj) {
        if (obj[item].hasOwnProperty("assets")) {
            this.dealImgsPacking(obj[item]);
        }
    }
}

通过这个方法我们改造了之前的节点树;

将之前节点树中的assest变为了一个数组, 每个数组元素代表一张图集信息.

结构如下:

  assets: [
    { 
        maxArea: { width: 180,height: 340 }, 
        atlas: [
            {
                id: &#39;assets/index/img-手把手帶你使用node開發一款圖集打包工具&#39;,
                width: 190,
                height: 187,
                x: 0,
                y: 0
            }
        ], 
        fileName: &#39;assets&#39; },
        ...
  ]

我们可以清晰的得到, 打包之后的图集, 最大宽高是maxArea, 每张图宽高位置信息是atlas,以及图集名称fileName.

接下来, 就是最后一步了, 绘制新的图片, 并输出图片文件.

注意
我们在使用打包算法的时候, 可以先进行一下基于图片大小的排序
这样以来打包出来的图集会留白更小

图集打包并输出

这里图集的绘制和输出均是使用了node-images的API;

遍历之前得到的节点树, 首先绘制一张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[&#39;fileName&#39;]}.png`);

最后对节点树递归调用, 绘制出所有的图集.

具体代码如下:

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[&#39;fileName&#39;]}.png`);
                count++;
            }
        })
    }

    for (let item in obj) {
        if (obj[item].hasOwnProperty("assets")) {
            this.drawImages(obj[item]);
        }
    }

}

这样子, 我们就大功告成了,

运行测试一下, 可以得到如下的图集:

手把手帶你使用node開發一款圖集打包工具

手把手帶你使用node開發一款圖集打包工具

效果还不错.

如何使用

安装

npm i sprites-pack-tool

使用

const MySpritePackTool = require("sprites-pack-tool");
const path = require("path");
/** 打包最多递归次数 */
const MAX_COUNT = 2;
//需要合成的图集的路径
const assetsPath = path.resolve(__dirname, "./assets");

/** 图集打包工具配置*/
const mySpritePackTool = new MySpritePackTool({
    //一个文件夹图片过多或者过长 递归最大次数
    maxCount: MAX_COUNT,
    //需要打包图集的文件路径
    assetsPath: assetsPath,
    //输出文件路径
    outPutPath: path.resolve(__dirname, "./res"),
    //一张图集打包最大size
    maxSize: { width: 2048,height: 2048}
});
/** 图集打包 */
mySpritePackTool.Pack2Sprite();

展望

当然, 这个工具只是初版, 后续还会继续优化并增加新的功能.

  • 算法可以继续优化, 现在留白也挺多.

  • 文件夹操作,可以优化. 比如写入图片可以每个文件夹下一张图集.

  • 增加更多配置项, 比如开启图片压缩

  • 增加json文件

  • ...

我大致看了下, 市场上有几款图集打包工具, 要么基于texturePacker, 要么基于imagemagick;

使用这俩应用开放的API也是可以打包图集的, 效果品质可能更好.

但是你还得额外安装一个应用.

同样的, 你也可以使用webpack的一些loader或者plugins. 目的都是打包图集.

本文介紹的工具比較輕量, 但是可堪一用, 同時開箱即可.

後文

有一陣子沒有寫文章了, 這個週末偶然想寫一個這樣的工具, 就付諸實踐了, 結果還不錯.

如果你還有更好的方式, 可以留下你的評論, 感激不盡~.

歡迎大家拍磚指正, 筆者功力尚淺, 如有不當之處請斧正.

#源

你可以在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

#文章粗淺, 望諸位不吝您的評論與按讚~

#原文網址:https://juejin.cn/post/7035809483666227230

作者:起小就些熊

更多node相關知識,請造訪:nodejs 教學! !

以上是手把手帶你使用node開發一款圖集打包工具的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除