手把手带你使用node开发一款图集打包工具

本篇文章就来手把手教你使用node手写一款图集打包工具,有一定的参考价值,希望对大家有所帮助!

手把手带你使用node开发一款图集打包工具

偶然发现一款很好用的跨平台图像编解码库node-images.

仔细阅读其API, 就萌生了一个使用其制作精灵图集的想法.

于是就诞生了这个工具sprites-pack-tool.

你可以在github查看https://github.com/xdq1553/MySpritesPackTool你可以使用npm安装https://www.npmjs.com/package/sprites-pack-tool

对于精灵图集, 我想大家都不陌生.

比如把下面的几张图片合成一张.

1.png

2.png

这一张图集就是我用本文介绍的工具打包合成的.

合成的图片品质依然十分高呢.

为什么需要使用图集

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-3.png      |--img-4.png   |--login      |--img-5.png   |--img-1.png   |--img-2.png

我们需要每个文件夹下打包出一张图集.

思考: 需要什么样的数据结构?

首先便于js解析, 我们约定一个对象,

每一层, 需要一个图片信息容器assets;

一个所包含的图片标识keys;

一个文件夹名字, 也方便我们后面为图集命名name;

然后每层文件夹前套相同对象;

结构如下:

{  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,'  },}

不难发现, 我们已经可以得到需要打包的所有文件和文件夹.

那么用程序如何实现呢?

主要用到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的下边.

3.png

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

4.png

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

举个例子

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

LuckyCola工具库 LuckyCola工具库

LuckyCola工具库是您工作学习的智能助手,提供一系列AI驱动的工具,旨在为您的生活带来便利与高效。

LuckyCola工具库 19 查看详情 LuckyCola工具库

5.png

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

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

/**  * 确定宽高 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 = 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['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]);        }    }}

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

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

结构如下:

  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.

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

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

图集打包并输出

这里图集的绘制和输出均是使用了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['fileName']}.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['fileName']}.png`);                count++;            }        })    }    for (let item in obj) {        if (obj[item].hasOwnProperty("assets")) {            this.drawImages(obj[item]);        }    }}

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

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

6.png

7.png

效果还不错.

如何使用

安装

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开发一款图集打包工具的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/555532.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
Linux日志安全审计怎么做
上一篇 2025年11月9日 22:18:45
最全面详细的Composer安装配置教程(win/linux)
下一篇 2025年11月9日 22:18:45

相关推荐

  • JavaScript 高效判断页面所有复选框状态的技巧与实践

    本文旨在提供一套高效且专业的javascript方法,用于判断网页中所有复选框的选中状态。我们将探讨如何利用`array.some()`快速确定是否有未选中的复选框(进而判断是否全部选中),以及如何使用`array.filter()`统计选中和未选中的复选框数量。通过优化dom元素选择和数组操作,提…

    2026年5月10日
    000
  • OSMnx中interpolate_points函数详解及街道细分与图构建实践

    本文详细介绍了osmnx库中`utils_geo.interpolate_points`函数的使用方法,特别是其返回的python生成器类型。我们将学习如何处理生成器输出,并提供一个完整的教程,演示如何利用此函数将现有街道几何体细分为更小的线段,进而构建一个精细化的网络图,以支持更细粒度的空间分析。…

    2026年5月10日
    000
  • 如何安全有效地从外部网页获取HTML元素数据并应用于自身页面

    本教程旨在解决如何在不同域名下,通过javascript获取并使用另一个网页的html元素数据。文章将深入探讨同源策略的限制,并提供两种主要解决方案:使用` 在现代Web开发中,有时我们需要从外部网站获取特定的HTML内容或属性值,并将其整合到我们自己的网页中。例如,从XYZ.COM/B.html页…

    2026年5月10日
    000
  • JS如何操作HTML元素_DOM编程核心方法【教程】

    必须掌握操作HTML元素的核心DOM方法:一、通过ID获取单个元素;二、通过类名获取元素集合;三、通过标签名获取元素集合;四、通过CSS选择器获取元素;五、为元素绑定事件监听器;六、创建并插入新元素;七、替换或删除现有元素。 如果您希望使用JavaScript动态修改网页内容、响应用户交互或构建交互…

    2026年5月10日
    000
  • Golang如何提升TCP长连接处理效率_Golang TCP长连接处理性能优化实践详解

    答案:通过非阻塞I/O、单Goroutine双工模型、sync.Pool对象复用、TCP_NODELAY优化及高效心跳管理,结合系统调优,可显著提升Golang百万级TCP长连接处理效率。 在高并发网络服务场景中,TCP长连接的处理效率直接影响系统的吞吐能力和资源消耗。Golang凭借其轻量级Gor…

    2026年5月10日
    000
  • 使用SMTP.js发送邮件:客户端集成、常见问题与最佳实践指南

    本文深入探讨了使用SMTP.js库在前端发送邮件时可能遇到的问题,特别是与Elastic Email集成时的挑战。我们将分析代码中常见的异步处理错误、条件函数定义陷阱,并提供修正后的代码示例和最佳实践。重点强调了正确处理Promise链、确保函数可访问性以及客户端邮件发送的安全考量,帮助开发者构建更…

    2026年5月10日
    000
  • 如何在不暴露密钥的情况下,在客户端创建 Stripe Payment Link

    本文介绍了在纯静态网站环境下,如何利用 Stripe Payment Link 实现商品售卖,并着重讨论了在不暴露 Stripe 密钥的前提下,客户端创建 Payment Link 的可行性。分析了直接在客户端使用密钥的风险,并提出了预先生成 Payment Link 或使用后端服务动态生成 Pay…

    2026年5月10日
    000
  • Windows用Prettier一键格式化乱码HTML代码

    首先确保HTML文件保存为UTF-8编码,使用文本编辑器另存为UTF-8格式;其次在命令行执行chcp 65001切换至UTF-8代码页后再运行Prettier;接着在VS Code中设置files.encoding为utf8并启用files.autoGuessEncoding;最后可通过Node.…

    2026年5月10日
    000
  • php怎么截取网页_php抓取网页内容的几种方法

    file_get_contents适用于静态页抓取,但受限于allow_url_fopen且无法执行JS;2. cURL支持自定义请求头、Cookie等,适合处理复杂HTTP请求;3. Guzzle作为现代PHP项目推荐方案,具备良好扩展性与异步支持;4. 动态渲染内容需借助Puppeteer或Se…

    2026年5月10日
    000
  • html函数如何实现动态内容显示 html函数在网页交互中的核心应用

    JavaScript函数通过操作DOM实现动态内容更新与交互,如显示时间、实时搜索、增删元素及加载数据,使网页具备动态功能。 HTML 本身没有“函数”的概念,它是一种标记语言,用于定义网页结构。真正实现动态内容显示和交互功能的是 JavaScript。通常所说的“HTML函数”其实是 JavaSc…

    2026年5月10日
    000
  • HTMLAMP怎么做_加速移动页面实现教程

    答案:HTML AMP通过规范标签、禁用自定义JS、引入AMP JS库和缓存技术提升移动页面加载速度,需遵循AMP HTML标准并验证有效性,有助于SEO但非万能,未来将更开放并与PWA等融合。 HTML AMP 旨在加速移动页面加载速度,提升用户体验。简单来说,它通过限制某些 HTML 功能,并采…

    2026年5月10日
    000
  • 理解PHP服务器端请求与浏览器开发者工具的限制

    当PHP脚本使用file_get_contents等函数发起服务器端请求时,这些请求直接在服务器上执行,而非通过浏览器。因此,浏览器开发者工具的网络活动面板无法捕获和显示这些内部的服务器间通信,因为它仅监控浏览器自身发出的网络请求,对服务器内部处理过程无感知。 客户端请求与服务器端请求的本质区别 在…

    2026年5月10日
    100
  • JavaScript DOM操作:点击关联元素获取目标文本内容的教程

    本教程详细介绍了如何通过JavaScript处理用户点击事件,并结合DOM的 closest() 和 querySelector() 方法,从复杂的HTML结构中准确获取目标元素的文本内容。文章强调了使用 addEventListener() 进行事件绑定、避免重复ID以及高效DOM遍历的最佳实践,…

    2026年5月10日
    000
  • 前端构建优化:利用常量折叠提升应用性能

    本文深入探讨了一种在构建阶段执行部分源代码以进行优化的技术——常量折叠(Constant Folding)。通过在编译时预计算表达式并替换为最终结果,该技术显著减少了运行时开销,提升了应用性能。文章将详细解释其工作原理、优势,并探讨其在现代前端构建工具中的应用与配置,旨在帮助开发者实现更高效的代码优…

    2026年5月10日
    000
  • JavaScript数据结构实现_javascript算法基础

    JavaScript中常用数据结构包括栈、链表和字典:1. 栈利用数组的push和pop实现LIFO,适用于括号匹配;2. 链表由节点组成,插入删除高效,适合频繁修改场景;3. 字典用对象实现键值对存储,常用于频率统计;4. 二分查找在有序数组中以O(log n)效率查找目标值,需数组已排序。掌握这…

    2026年5月10日
    000
  • JavaScript井字棋(Tic-Tac-Toe)核心交互逻辑实现教程

    本教程详细介绍了如何使用javascript实现井字棋(tic-tac-toe)游戏的核心交互逻辑。内容涵盖了如何遍历并为棋盘上的每个方格添加点击事件监听器,实现玩家x和o的交替落子,以及重置游戏状态的功能。通过提供的html、css和javascript代码示例,读者可以快速理解并构建一个基础的井…

    2026年5月10日
    000
  • 全栈JS代码怎么结构化_全栈JavaScript项目代码结构与规范指南

    采用分层+功能划分的目录结构,明确分离前后端代码;2. 遵循单一职责原则,路由、控制器、服务与模型各司其职;3. 统一命名规范并集成ESLint+Prettier保证代码风格一致;4. 使用环境变量管理配置,通过脚本实现自动化构建与并发启动服务。 全栈JavaScript项目涉及前端、后端、数据库交…

    2026年5月10日
    000
  • JavaScript模块加载机制_JavaScript代码组织规范

    现代前端推荐使用ES Modules,通过import和export实现静态依赖管理,配合合理目录结构与命名规范提升可维护性,注意浏览器与Node.js的运行差异。 JavaScript 的模块加载机制和代码组织规范是现代前端开发中的核心基础。随着项目规模扩大,良好的模块化设计能提升代码可维护性、复…

    2026年5月10日
    000
  • c++怎么实现一个静态代码分析工具_C++代码质量与静态分析工具开发

    静态代码分析工具通过解析源码构建AST,利用Clang框架实现未使用变量检测,结合ASTMatchers进行规则匹配,最终生成警告信息。 静态代码分析工具可以在不运行程序的前提下,检测出潜在的语法错误、编码规范问题、内存泄漏风险等。在C++中开发一个简单的静态分析工具,核心思路是解析源码并构建抽象语…

    2026年5月10日
    000
  • JavaScript模块化是什么_ES6模块和CommonJS有什么区别呢

    JavaScript模块化将代码拆分为独立可复用单元,ES6模块(import/export,编译时加载、实时引用)与CommonJS(require/module.exports,运行时加载、值拷贝)核心区别在于设计目标和运行机制不同。 JavaScript模块化是把代码按功能拆分成独立、可复用的…

    2026年5月10日
    000

发表回复

登录后才能评论
关注微信