이 글은 node를 사용하여 앨범 패키징 도구를 작성하는 방법을 단계별로 설명합니다. 이 글이 모든 사람에게 도움이 되기를 바랍니다.
우연히 매우 유용한 크로스 플랫폼 이미지 인코딩 및 디코딩 라이브러리인 node-images를 발견했습니다.
API를 주의 깊게 읽은 후 이를 사용하여 elf 아틀라스를 만드는 아이디어를 생각해 냈습니다. .
그래서 이 도구 sprites-pack-tool
이 탄생했습니다.sprites-pack-tool
.
你可以在github查看
https://github.com/xdq1553/MySpritesPackTool
你可以使用npm安装
https://www.npmjs.com/package/sprites-pack-tool
对于精灵图集, 我想大家都不陌生.
比如把下面的几张图片合成一张.
这一张图集就是我用本文介绍的工具打包合成的.
合成的图片品质依然十分高呢.
我们在web开发中, 每次在浏览器展示一张图片都需要请求一次服务器资源.
举个例子, 3次请求每次4k, 和一次请求12k还是有本质区别的, 然后更多的时候一次请求并不是3 * 4k.
使用图集能让我们优化资源加载, 提高网站的性能.
在游戏开发中, 图集的使用至关重要, 不管是一般帧动画还是svga等动画解决方案, 都不会每张图片去请求资源.
更多的时候, 我们都是打包成图集, 而图集打包工具texturepacker
更是大行其道.
其次, 游戏场景过多, 我们一般都需要分步加载资源, 有的时候一个动画模型, 涉及的图片少则十来张, 多则近百张.
图集的使用不可或缺.
下面我们就来看下如何编写一款图集打包工具.
开发一个图集打包工具脚本需要什么技能.
node.js编程能力
二维矩形装箱算法
然后我们思考如何去打包一张图集.
我们需要找到需要打包的文件夹, 可能有多个或者嵌套文件夹.
图集是多张散图拼合而成.
图集的大小需要可配置
尽可能的压缩图集空间, 使每张图紧密贴合
每个文件夹打包成一个图集, 需要考虑图片过多的情况
可能需要生成图集所需要的json文件, 记录图片位置信息
我这里是这样设计.
首先我们需要一个打包对象实例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-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다. |--img-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다. |--login |--img-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다. |--img-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다. |--img-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다.
我们需要每个文件夹下打包出一张图集.
思考: 需要什么样的数据结构?
首先便于js解析, 我们约定一个对象,
每一层, 需要一个图片信息容器assets
;
一个所包含的图片标识keys
;
一个文件夹名字, 也方便我们后面为图集命名name
;
然后每层文件夹前套相同对象;
结构如下:
{ assets: [ { id: 'assets/img-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다.', width: 190, height: 187 }, ... ], name: 'assets', keys: 'img-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다.,img-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다.,', index: { assets: [ { id: 'assets/index/img-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다.', width: 190, height: 187 }, ... ], name: 'index', keys: 'img-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다.,img-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다.,' }, login: { assets: [ { id: 'assets/login/img-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다.', width: 190, height: 187 } ], name: 'index', keys: 'img-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다.,' }, }
不难发现, 我们已经可以得到需要打包的所有文件和文件夹.
那么用程序如何实现呢?
主要用到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
github에서 볼 수 있습니다🎜🎜https://github.com/xdq1553/MySpritesPackTool🎜🎜볼 수 있습니다 npm을 사용하여 설치🎜🎜 https://www.npmjs.com/package/sprites-pack-tool🎜
texturepacker
가 더욱 인기가 있습니다. .🎜🎜두 번째로, 게임 장면이 너무 많고 일반적으로 리소스를 단계별로 로드해야 합니다. 때로는 애니메이션 모델에 적게는 12장, 많게는 100장의 사진이 포함됩니다. 🎜🎜🎜사진 사용. 🎜🎜🎜작성 방법을 살펴보겠습니다. 아틀라스 패키징 도구입니다.🎜MySpritePackTool
가 필요합니다. 구성 매개변수 옵션
작성도 지원합니다.🎜/** * 确定宽高 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 = [];
`${obj['name'] + (count ? "-" + count : '')}`🎜아틀라스를 패키징해야 합니다. 🎜🎜🎜생각하기: 어떤 종류의 데이터 구조가 필요한가?🎜🎜🎜우선 js 파싱에 편리하고 객체에 동의합니다.🎜 🎜각 레이어에는 이미지 정보 컨테이너 자산이 필요합니다 ; 🎜🎜포함된 이미지 식별자
keys
🎜🎜나중에 앨범을 만들 때에도 편리한 폴더 이름 이름 name
;🎜🎜그런 다음 각 폴더 레이어 앞에 동일한 개체를 배치합니다.🎜🎜🎜구조는 다음과 같습니다.🎜🎜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]); } } }🎜패키징해야 하는 모든 파일과 폴더를 이미 얻을 수 있다는 것을 찾는 것은 어렵지 않습니다.🎜 🎜🎜 그럼 어떻게 프로그램으로 구현할까요?🎜🎜🎜주로 nodejs의 🎜fs 모듈🎜을 사용하여 폴더를 재귀적으로 동작시켜 필요한 노드 트리를 출력합니다.🎜🎜주의할 점은 작업할 때 사진인지 폴더인지 판단해야 한다는 점입니다. 🎜
assets: [ { maxArea: { width: 180,height: 340 }, atlas: [ { id: 'assets/index/img-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다.', width: 190, height: 187, x: 0, y: 0 } ], fileName: 'assets' }, ... ]🎜이 방법으로 필요한 노드 트리를 얻을 수 있습니다.🎜
너비
와 높이
두 가지 정보가 있는데, 실제로는 직사각형입니다.🎜我们现在所要做的就是把这些不同面积的矩形放到一个具有最大长宽的大矩形中.
跳开图片, 从矩形放置入手
二维矩形装箱算法有不少, 我这里选用一种比较简单的.
首先得到一个具有最大长宽的矩形盒子.
我们先放入一个矩形A, 这样子, 剩余区域就有两块: 矩形A的右边和矩形A的下边.
然后我们继续放入矩形B, 可以先右再下, 然后基于矩形B又有两块空白空间.
依次类推, 我们就可以将合适的矩形全部放入.
举个例子
把左边的散装矩形放入右边的矩形框中, 可以得到:
可以看到, 我们节省了很多空间, 矩形排列紧凑.
如果用代码实现, 是怎么样的呢?
/** * 确定宽高 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['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-노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다.', 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]); } } }
这样子, 我们就大功告成了,
运行测试一下, 可以得到如下的图集:
效果还不错.
安装
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
Reference
node-images: https://github.com/zhangyuanwei/node-images
spritesheet: https: //github.com/krzysztof-o/spritesheet.js
이 기사는 피상적입니다. 여러분 모두 주저하지 말고 댓글과 좋아요를 남겨주세요~
원본 주소: https://juejin.cn /post/7035809483666227230
저자: 어린 시절부터 곰이었습니다
노드 관련 지식을 더 보려면 nodejs 튜토리얼 을 방문하세요! !
위 내용은 노드를 사용하여 아틀라스 패키징 도구를 개발하는 방법을 단계별로 안내합니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!