微信小程序 - 龙骨图集拆分

2023-12-18 22:15

本文主要是介绍微信小程序 - 龙骨图集拆分,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

微信小程序 - 龙骨图集拆分

  • 注意
  • 目录结构
  • 演示动画
  • 废话一下
    • 业务逻辑
    • 注意点
    • 龙骨JSON图集结构
  • 源码分享
    • dragonbones-split.js
    • dragonbones-split.json
    • dragonbones-split.wxml
    • dragonbones-split.wxss
    • imgUtil.js
  • 参考资料

注意

只支持了JSON版本

目录结构

在这里插入图片描述

演示动画

Spine播放器1.5.0_PC端

Spine播放器1.5.1_移动端

废话一下

这是 SpinePlayer 2D骨骼动画播放器 - 微信小程序版 工具箱中的一个功能。
功能很简单,80% 的代码都在处理交互逻辑,以及移动端PC端兼容方面的问题。

业务逻辑

  1. 读取JSON文件和PNG图片。
  2. 解析JSON得到所有小图在图集(PNG图片)中的 x,y 坐标和高宽。
  3. 将PNG图集绘制到2d画布,然后使用 canvasToTempFilePath 逐个截取区域,保存为小图。
  4. 最后将所有小图打包为 zip 供用户保存即可。

注意点

  1. 为了保证截取图片的清晰度,画布尺寸需要用图片大小乘以设备像素比
  2. 图片填充完整,再截图。否则会空白。所以会成两步操作比较稳妥。当然也可以自己控制延时自动调用,一气呵成。
  3. 因为 2d 画布不便于直接显示,所以使用一个 image 组件来实现预览。
    3.1. 方法是将 PNG 读取为 base64 赋给 image 组件 <image src="{{textureBase64}}" />
    3.2. 读取 PNGbase64 就这句 fs.readFileSync(‘临时图片路径’, 'base64') 当然用的时候还要拼接一下头,详情看源码吧。

龙骨JSON图集结构

可以看出结构非常简单,直接读 SubTexture 数组,遍历它进行截取就可以了。

{"imagePath": "body_tex.png","width": 1024,"SubTexture": [{"height": 472,"y": 1,"width": 295,"name": "body/a_arm_L","x": 720}略。。。],"name": "body","height": 1024
}

源码分享

dragonbones-split.js

// packageTools/pages/dragonbones-split/dragonbones-split.js
const fileUtil = require('../../../utils/fileUtil.js');
const imgUtil = require('./imgUtil.js');
const pixelRatio = wx.getSystemInfoSync().pixelRatio; // 设备像素比
let canvas, ctx;
let globalData = getApp().globalData;
let dbsplit = globalData.PATH.dbsplit;
Page({/*** 页面的初始数据*/data: {canvasWidth: 300,                     // 画布宽canvasHeight: 150,                    // 画布高texture: {},                          // 龙骨图集PNG图片信息 { path, width, height, orientation, type }textureBase64: '',                    // 龙骨图集PNG图片的 Base64 编码subTextureList:[],                    // 龙骨图集JSON数据。包含拆分出的小图地址 tempFilePathshareZipFile: { name: '', path: ''},  // 最终生成的ZIPjsonValue: '',                        // 文本框内容(PC 端用于获取 JSON)// parseInt('0011', 2) === 3status: 0,                            // 工作状态:0000 初始,0001 有图,0010 有JSON,0100 已拆图,1000 已ZIPisPC: false,},/*** 生命周期函数--监听页面加载*/onLoad(options) {// 获取画布wx.createSelectorQuery().select('#myCanvas') // 在 WXML 中填入的 id.fields({ node: true, size: true }).exec((res) => {canvas = res[0].node;           // Canvas 对象ctx = canvas.getContext('2d');  // 渲染上下文});// 创建工作目录fileUtil.mkdir(dbsplit); this.setData({ isPC: globalData.systemInfo.isPC });// 清理场地fileUtil.clearDirSync(globalData.PATH.dbsplit);},/*** 粘贴龙骨图集 JSON 的文本框发生变化*/onChange(event) {try {if(event.detail.value === undefined){return;}let json = imgUtil.parseJSON(event.detail.value);this.data.shareZipFile = { name: `${json.name}.zip`, path: `${dbsplit}/${json.name}.zip`}; // zip路径打包时用this.setData({ jsonValue: event.detail.value,status: 2,subTextureList: json.SubTexture,});} catch (err) {console.log(err);this.setData({ jsonValue: '',status: 1, // this.data.status & 13, // parseInt('1101', 2)subTextureList: [],});wx.showToast({ title: 'JSON格式有误', icon: 'error' })}},/*** 选择龙骨图集PNG、JSON*/async choosePNGJSON(e){console.log('选择龙骨图集PNG、JSON');wx.showLoading({ title: '选择图集' });imgUtil.chooseAtlas({ count: 2}).then(res => {if(res.length != 2){wx.showToast({ title: '文件数量异常!', icon: 'error' });return;}let texture, json;if(res[0].type === 'png'){[texture, json] = res;}else{[json, texture] = res;}wx.showLoading({ title: '解析图集' });this.data.texture = texture; // 更新图集PNG的相关信息。点击预览时会用到它的 paththis.data.shareZipFile = { name: `${json.name}.zip`, path: `${dbsplit}/${json.name}.zip`}; // zip路径打包时用// 图集PNG填充画布this.fillCanvasWithImage(texture).then(()=>{// 填充完成后,在下一个时间片段更新数据this.setData({textureBase64: imgUtil.imageToBase64(texture.path, texture.type), // 更新 image 组件 srcsubTextureList: json.SubTexture,                                  // 更新页面上的 subTexture 列表status: 2,                                                        // 已选图,JSON完成解析},()=>{wx.hideLoading();});})}).catch(err => {console.log(err);wx.showToast({ title: '图集选择失败', icon: 'error' });this.setData({textureBase64: '',              // 更新 image 组件 srcsubTextureList: [],             // 更新页面上的 subTexture 列表status: 0,});}).finally(()=>{wx.hideLoading() })},/*** 选择龙骨图集PNG文件*/async choosePNG(e){console.log('选择龙骨图集PNG文件');let whereFrom = globalData.systemInfo.isPC ? 'media' : 'message';let promises = imgUtil.chooseImg({ count: 1, whereFrom }); // media messageawait promises.then(res => {let texture = res[0];console.log(texture);this.setData({ texture , textureBase64: imgUtil.imageToBase64(texture.path, texture.type),status: 1,// 重选图片后,清空已选的JSONjsonValue: '',        // 清除 JSON 数据subTextureList: []    // 清除 解析后的 JSON 数据}, res => {this.fillCanvasWithImage(texture); // 填充画布});}).catch(err => {console.log(err);wx.showToast({ title: '选择图片失败', icon: 'error' });this.setData({ texture: {} , textureBase64: '',status: 0, // this.data.status & 14,});});},/*** 将图片绘制到画布*/fillCanvasWithImage(imageInfo){let { path, width, height, orientation, type } = imageInfo;// 按图片大小更新画布尺寸canvas.width = width * pixelRatio;canvas.height = height * pixelRatio;ctx.scale(pixelRatio, pixelRatio);return new Promise((resolve, reject)=>{// 更新画布 渲染宽高。完成后,绘制图片到画布this.setData({ canvasWidth: width, canvasHeight: height}, res=> {const image = canvas.createImage(); // 创建图片对象image.onload = () => ctx.drawImage(image, 0, 0); // 图片加载完成,在回调中将其绘制到画布image.src = path; // 设置图片 srcresolve();});});},/*** 解析JSON并拆分图集*/async parseJsonAndSplitIMG(e){console.log('解析JSON并拆分图集');if(this.data.status < 1){wx.showToast({ title: '请选择图片', icon: 'error'});return;}if(this.data.status < 2){wx.showToast({ title: '请提供JSON', icon: 'error'});return;}this.splitIMG();},/*** 拆分龙骨图集PNG文件*/async splitIMG(e){console.log('拆分龙骨图集PNG文件');let pArr = this.data.subTextureList.map(subTexture => {return new Promise((resolve, reject)=> {let { x, y, width, height, } = subTexture;wx.canvasToTempFilePath({x, y, width, height, canvas,destWidth: width,destHeight: height,fileType: 'png',success: res => {console.log(res.tempFilePath);subTexture.tempFilePath = res.tempFilePath;resolve(subTexture);},fail: reject});});});Promise.all(pArr).then(async res => {await this.creatZip(res);this.setData({ status: 3, }); // 更新状态,完成拆图}).catch(err => {this.setData({ status: 2, });});},/*** 将拆好的小图打包为 ZIP*/async creatZip(subTextureList){console.log('将拆好的小图打包为 ZIP');try {// 图片列表let fileList = subTextureList.map(subTexture => ({ name: subTexture.name, path: subTexture.tempFilePath}));// 创建压缩包await fileUtil.zip(fileList, this.data.shareZipFile.path, progress => { wx.showLoading({ title: progress.msg, });if(progress.percent == 100){setTimeout(wx.hideLoading, 200);}});// 更新状态console.log(this.data.shareZipFile.path);this.setData({subTextureList});} catch (err) {console.error(err)wx.showToast({ icon: 'error', title: '打包失败' });this.setData({shareZipFile: {}});} finally {wx.hideLoading();}},/*** 将拆分后的文件打包导出*/saveIMG(e){console.log('将拆分后的文件打包导出');console.log(this.data.subTextureList);if(this.data.status < 3){wx.showToast({ title: '尚未拆图', icon: 'error' });return;}// 如果是电脑端,否则是手机端if(globalData.systemInfo.platform == 'windows' || globalData.systemInfo.platform == 'mac'// || globalData.systemInfo.platform == 'devtools'){wx.saveFileToDisk({filePath: this.data.shareZipFile.path,success: console.log,fail: console.error});} else {wx.shareFileMessage({filePath: this.data.shareZipFile.path,fileName: this.data.shareZipFile.name,success: console.log,fail: console.error,complete: console.log});}},async previewTexture(e){if(!!this.data.texture.path == false && globalData.systemInfo.isPC){await this.choosePNG();return;}wx.previewImage({urls: [this.data.texture.path],success: (res) => {},fail: (res) => {},complete: (res) => {},})},previewSubTexture(e){if(this.data.status < 3){wx.showToast({ title: '尚未拆分', icon:"error" });return;}wx.previewImage({urls: this.data.subTextureList.map(obj => obj.tempFilePath),current: e.currentTarget.dataset.texturePath,showmenu: true,success: (res) => {},fail: (res) => {},complete: (res) => {},})}
})

dragonbones-split.json

{"usingComponents": {}
}

dragonbones-split.wxml

<!--packageTools/pages/dragonbones-split/dragonbones-split.wxml-->
<!-- 大家好我是笨笨,笨笨的笨,笨笨的笨,谢谢!https://blog.csdn.net/jx520/ -->
<view class="main-container bg-60" ><view class="top-container poem-container poem-h bg-24" style="min-height: 200rpx;"><view></view><view>无根翡翠顺江涛, 有尾鱼虾逆水潮。</view><view>行宿天涯本无路, 去留飘渺也逍遥。</view></view><view class="top-container scroll-y home-top" disabled><image class="texture chessboard" src="{{textureBase64}}" bind:tap="previewTexture"/><view wx:for="{{subTextureList}}" wx:key="name"bindtap="previewSubTexture" data-texture-name="{{item.name}}" data-texture-path="{{item.tempFilePath}}"class="sub-texture-list {{status < 3 ? 'disabled' : 'splited'}}"><view class="sub-texture-row"> <view>{{item.name}}</view><view>{{item.width}} x {{item.height}}</view></view></view><view class="sub-texture-list" wx:if="{{isPC}}"><textarea class="json-area" auto-height	maxlength="-1" placeholder="请在此处粘贴龙骨图集的 JSON 内容" value="{{jsonValue}}"bindblur="onChange" bindlinechange="onChange" bindconfirm="onChange" bindinput="onChange" /></view><canvas id="myCanvas" type="2d" class="canvas2d" style="width: {{canvasWidth}}px; height: {{canvasHeight}}px;" /></view><view class="bottom-container"><!-- 移动端 --><view wx:if="{{isPC == false}}"><view class="but-row"><view class="button button-large" bindtap="choosePNGJSON"><view>选择龙骨PNGJSON</view></view><view class="button button-large {{status < 2 ? 'disabled' : ''}}" bindtap="parseJsonAndSplitIMG"><view>拆分图集</view></view></view><view class="but-row"><view class="button button-large {{status < 3 ? 'disabled' : ''}}" bindtap="saveIMG"><view>转发到聊天</view></view></view></view><!-- PC--><view wx:if="{{isPC}}"><view class="but-row"><view class="button button-large" bindtap="choosePNG"><view wx:if="{{textureBase64 === ''}}">选择图片</view><view wx:if="{{textureBase64 != ''}}">重选图片</view></view><view class="button button-large {{status < 2 ? 'disabled' : ''}}" bindtap="parseJsonAndSplitIMG"><view>拆分图集</view></view></view><view class="but-row"><view class="button button-large {{status < 3 ? 'disabled' : ''}}" bindtap="saveIMG"><view>保存</view></view></view></view><view class="gb-img"></view></view>
</view>

dragonbones-split.wxss

/* packageTools/pages/dragonbones-split/dragonbones-split.wxss */
@import '/common/wxss/player-page-common.wxss';.button{display: flex;flex-direction: column;justify-content: center;align-items: center;margin: 10rpx;padding: 10rpx;color: rgb(236, 236, 236);box-sizing: border-box;
}
.button-large{display: flex;justify-content: center;text-align: center;font-size: large;width: auto;height: 100%;max-height: 50%;box-sizing: border-box;flex-grow: 1;border-radius: 10px;background-color: rgb(102, 102, 102);border-top: 1px solid rgb(112, 112, 112);border-bottom: 2px solid rgb(90, 90, 90);
}.but-row {display: flex;
}
.sub-texture-row {display: flex;align-items: center;padding: 0 20rpx;border-bottom: rgb(102, 102, 102) solid 1px;box-sizing: border-box;
}
.sub-texture-row>view {margin: 4px;
}
.sub-texture-row :nth-child(1) {width: 70%;word-wrap: break-word;border-right: #666 solid 1px;
}
.sub-texture-row :nth-child(2) {margin-left: 5px;width: 30%;height: 100%;
}.texture {padding: 6px;width: 100%;border: rgb(63, 63, 63) solid 2px;box-sizing: border-box;
}
.canvas2d {position: absolute;right: 100vw;
}
.sub-texture-list {display: flex;flex-direction: column;padding: 5rpx 20rpx;
}
.json-area {background-color: #000;border-radius: 10rpx;width: 100%;padding: 20rpx;box-sizing: border-box;font-size: x-small;min-height: 430rpx;
}.splited {color: chartreuse;
}

imgUtil.js

const fs = wx.getFileSystemManager();/*** 选择图集(PNG和JSON一对)* @param {object} options */
function chooseAtlas(_options = {}){const defaultOptions = { count: 2 };let options = { ...defaultOptions, ..._options };// 选择 PNG、JSONlet promise = wx.chooseMessageFile(options).then(res => res.tempFiles) .then(tempFiles => {return tempFiles.map(tempFilePath => {if(tempFilePath.type === 'image'){ // 图片return wx.getImageInfo({ src: tempFilePath.path }).then(res => {let { path, width, height, orientation, type } =  res;let imageInfo = { path, width, height, orientation, type };return imageInfo;});}else if(tempFilePath.type === 'file' && tempFilePath.path.toLowerCase().endsWith('.json')){ // JSONreturn parseJSON(fs.readFileSync(tempFilePath.path, 'utf-8'));}else{return null;}}).filter(obj => obj != null);}).catch(err=> {console.log(err);return [];});// 全部完成再返回return promise.then(promiseArr => {return Promise.all(promiseArr).then(res => res);})
}/*** 选择图片* @param {object} options */
function chooseImg(_options = {}){const defaultOptions = {count: 9, mediaType: ['image'],whereFrom: 'message' , // 从何处选取。合法值:message media};let options = { ...defaultOptions, ..._options };let promise;// 根据参数中给的选择方式,调用对应方法。switch (options.whereFrom) {case 'media':promise = wx.chooseMedia(options).then(res => res.tempFiles.map( data => data.tempFilePath) ).catch(err=> {console.log(err);return [];});break;default:promise = wx.chooseMessageFile(options).then(res => res.tempFiles.map( data => data.path) ).catch(err=> {console.log(err);return [];});break;}// 对选择的图片,获取信息。构建好对象返回return promise.then(tempFiles => {return tempFiles.map(tempFilePath => {return wx.getImageInfo({ src: tempFilePath }).then(res => {let { path, width, height, orientation, type } =  res;let imageInfo = { path, width, height, orientation, type };return imageInfo;});});}).then(promiseArr => { // 全部完成再返回return Promise.all(promiseArr).then(res => res);});
}/*** 从 tempFilePath 以 base64 格式读取文件内容* @param {*} tempFilePath * @param {*} type 图片类型是提前通过 getImageInfo 获取的*/
function imageToBase64(tempFilePath, type) {let data = fs.readFileSync(tempFilePath, 'base64');return `data:image/${type};base64,${data}`;
}/*** 解析龙骨图集的JSON* @param {string} dbJsonTxt 龙骨图集的JSON*/
function parseJSON(dbJsonTxt){// 解析JSONlet json = JSON.parse(dbJsonTxt);// 从 SubTexture 中取出图片名// 判断是否有重复,如果重复就用完整路径名,否则:就直接用图片名let arr = json.SubTexture.map(st => st.name.substr(st.name.lastIndexOf('/')+1));// { "x": 2, "y": 2, "width": 554, "height": 140, "name": "weapon_hand_r"}arr = json.SubTexture.map(subTexture => {if(arr.length !== new Set(arr).size){subTexture.name = `${subTexture.name.replace(/\//g, '-')}.png`;}else{subTexture.name = `${subTexture.name.substr(subTexture.name.lastIndexOf('/')+1)}.png`;}return subTexture;});console.log(arr);json.SubTexture = arr;json.type = 'json';return json;
}// /**
//  * 选择JSON。(PC端的弹窗竟然不支持输入功能。此方法没用上)
//  * @param {object} options 
//  */
// function chooseJSON(_options = {}){
//   const defaultOptions = {
//     count: 1,
//     whereFrom: 'modal' , // 从何处选取。合法值: modal message
//   };
//   let options = { ...defaultOptions, ..._options };
//   let promise;
//   // 根据参数中给的选择方式,调用对应方法。
//   switch (options.whereFrom) {
//     case 'modal':
//       promise = wx.showModal({
//           title: '龙骨图集JSON',
//           placeholderText: '请输入龙骨图集JSON内容',
//           confirmText: '解析',
//           editable: true,
//         }).then(res => {
//           if (res.confirm && res.errMsg === "showModal:ok" ) {
//             console.log(res.content);
//             return res.content;
//           } else if (res.cancel) {
//             console.log('用户点击取消')
//             return Promise.reject();
//           }
//         });
//       break;
//     default:
//       promise = wx.chooseMessageFile(options)
//         .then(res => res.tempFiles.map( data => fs.readFileSync(data.path, 'utf-8')))
//         .catch(err=> {
//           console.log(err);
//           return '';
//         });
//       break;
//   }
//   return promise;
// }module.exports = {chooseAtlas,chooseImg,imageToBase64,parseJSON,
}

参考资料

笑虾:微信小程序 - 创建 ZIP 压缩包
笑虾:微信小程序 - 文件工具类 fileUtil.js

这篇关于微信小程序 - 龙骨图集拆分的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/509942

相关文章

java向微信服务号发送消息的完整步骤实例

《java向微信服务号发送消息的完整步骤实例》:本文主要介绍java向微信服务号发送消息的相关资料,包括申请测试号获取appID/appsecret、关注公众号获取openID、配置消息模板及代码... 目录步骤1. 申请测试系统2. 公众号账号信息3. 关注测试号二维码4. 消息模板接口5. Java测试

Python基于微信OCR引擎实现高效图片文字识别

《Python基于微信OCR引擎实现高效图片文字识别》这篇文章主要为大家详细介绍了一款基于微信OCR引擎的图片文字识别桌面应用开发全过程,可以实现从图片拖拽识别到文字提取,感兴趣的小伙伴可以跟随小编一... 目录一、项目概述1.1 开发背景1.2 技术选型1.3 核心优势二、功能详解2.1 核心功能模块2.

python编写朋克风格的天气查询程序

《python编写朋克风格的天气查询程序》这篇文章主要为大家详细介绍了一个基于Python的桌面应用程序,使用了tkinter库来创建图形用户界面并通过requests库调用Open-MeteoAPI... 目录工具介绍工具使用说明python脚本内容如何运行脚本工具介绍这个天气查询工具是一个基于 Pyt

Ubuntu设置程序开机自启动的操作步骤

《Ubuntu设置程序开机自启动的操作步骤》在部署程序到边缘端时,我们总希望可以通电即启动我们写好的程序,本篇博客用以记录如何在ubuntu开机执行某条命令或者某个可执行程序,需要的朋友可以参考下... 目录1、概述2、图形界面设置3、设置为Systemd服务1、概述测试环境:Ubuntu22.04 带图

Python程序打包exe,单文件和多文件方式

《Python程序打包exe,单文件和多文件方式》:本文主要介绍Python程序打包exe,单文件和多文件方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录python 脚本打成exe文件安装Pyinstaller准备一个ico图标打包方式一(适用于文件较少的程

Python程序的文件头部声明小结

《Python程序的文件头部声明小结》在Python文件的顶部声明编码通常是必须的,尤其是在处理非ASCII字符时,下面就来介绍一下两种头部文件声明,具有一定的参考价值,感兴趣的可以了解一下... 目录一、# coding=utf-8二、#!/usr/bin/env python三、运行Python程序四、

如何基于Python开发一个微信自动化工具

《如何基于Python开发一个微信自动化工具》在当今数字化办公场景中,自动化工具已成为提升工作效率的利器,本文将深入剖析一个基于Python的微信自动化工具开发全过程,有需要的小伙伴可以了解下... 目录概述功能全景1. 核心功能模块2. 特色功能效果展示1. 主界面概览2. 定时任务配置3. 操作日志演示

Redis迷你版微信抢红包实战

《Redis迷你版微信抢红包实战》本文主要介绍了Redis迷你版微信抢红包实战... 目录1 思路分析1.1hCckRX 流程1.2 注意点①拆红包:二倍均值算法②发红包:list③抢红包&记录:hset2 代码实现2.1 拆红包splitRedPacket2.2 发红包sendRedPacket2.3 抢

无法启动此程序因为计算机丢失api-ms-win-core-path-l1-1-0.dll修复方案

《无法启动此程序因为计算机丢失api-ms-win-core-path-l1-1-0.dll修复方案》:本文主要介绍了无法启动此程序,详细内容请阅读本文,希望能对你有所帮助... 在计算机使用过程中,我们经常会遇到一些错误提示,其中之一就是"api-ms-win-core-path-l1-1-0.dll丢失

SpringBoot后端实现小程序微信登录功能实现

《SpringBoot后端实现小程序微信登录功能实现》微信小程序登录是开发者通过微信提供的身份验证机制,获取用户唯一标识(openid)和会话密钥(session_key)的过程,这篇文章给大家介绍S... 目录SpringBoot实现微信小程序登录简介SpringBoot后端实现微信登录SpringBoo