只支持了JSON版本
Spine播放器1.5.0_PC端
Spine播放器1.5.1_移动端
这是 SpinePlayer 2D骨骼动画播放器 - 微信小程序版 工具箱中的一个功能。
功能很简单,80%
的代码都在处理交互逻辑,以及移动端PC端兼容方面的问题。
画布尺寸
需要用图片大小
乘以设备像素比
。PNG
读取为 base64
赋给 image
组件 <image src="{{textureBase64}}" />
PNG
为 base64
就这句 fs.readFileSync(‘临时图片路径’, 'base64')
当然用的时候还要拼接一下头,详情看源码吧。可以看出结构非常简单,直接读 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
}
// 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数据。包含拆分出的小图地址 tempFilePath
shareZipFile: { name: '', path: ''}, // 最终生成的ZIP
jsonValue: '', // 文本框内容(PC 端用于获取 JSON)
// parseInt('0011', 2) === 3
status: 0, // 工作状态:0000 初始,0001 有图,0010 有JSON,0100 已拆图,1000 已ZIP
isPC: 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的相关信息。点击预览时会用到它的 path
this.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 组件 src
subTextureList: json.SubTexture, // 更新页面上的 subTexture 列表
status: 2, // 已选图,JSON完成解析
},()=>{
wx.hideLoading();
});
})
}).catch(err => {
console.log(err);
wx.showToast({ title: '图集选择失败', icon: 'error' });
this.setData(
{
textureBase64: '', // 更新 image 组件 src
subTextureList: [], // 更新页面上的 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 message
await promises.then(res => {
let texture = res[0];
console.log(texture);
this.setData({
texture ,
textureBase64: imgUtil.imageToBase64(texture.path, texture.type),
status: 1,
// 重选图片后,清空已选的JSON
jsonValue: '', // 清除 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; // 设置图片 src
resolve();
});
});
},
/**
* 解析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) => {},
})
}
})
{
"usingComponents": {}
}
<!--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>选择龙骨PNG、JSON</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>
/* 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;
}
const fs = wx.getFileSystemManager();
/**
* 选择图集(PNG和JSON一对)
* @param {object} options
*/
function chooseAtlas(_options = {}){
const defaultOptions = { count: 2 };
let options = { ...defaultOptions, ..._options };
// 选择 PNG、JSON
let 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')){ // JSON
return 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){
// 解析JSON
let 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,
}