小程序版本:2.19.4
实现效果如图:
最近开发了一款可以合成头像的小程序应用,期间碰到了一些尴尬的问题,我这边做出总结,希望能帮广大码农避坑。
关键技术点如下:canvas贴图,wx.canvasToTempFilePath保存相册。
全部代码组织如下:
1、封装唯一的canvas获取
//页面结构
<canvas class="avatar-board" type="2d" id="avatar" canvas-id="avatar"></canvas>
//获取canvas的js段
//--data申明
canvasObj: {
with: 0,
height: 0,
initialized: false,
canvas: null,
context: null,
}
//--获取方法
getCanvas() {
return new Promise((resolve) => {
if (this.data.canvasObj.initialized) {
return resolve();
}
const query = wx.createSelectorQuery();
query
.select("#avatar") //这里是canvas的id
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node;
const context = canvas.getContext("2d");
const dpr = wx.getSystemInfoSync().pixelRatio; //获取手机dpr
canvas.width = res[0].width * dpr;
canvas.height = res[0].height * dpr;
context.scale(dpr, dpr);
const canvasObj = {
canvas,
context,
width: res[0].width,
height: res[0].height,
initialized: true,
};
this.setData({ canvasObj });
resolve();
});
});
},
2、贴图方法:
drawImage(url) {
this.getCanvas().then(() => {
var fillImg = this.data.canvasObj.canvas.createImage();
fillImg.src = url;
fillImg.onload = () => {
const scale =
this.data.canvasObj.width / Math.max(fillImg.width, fillImg.height); //计算缩放值
this.data.canvasObj.context.drawImage(
fillImg,
0,
0,
fillImg.width,
fillImg.height,
(this.data.canvasObj.width - fillImg.width * scale) / 2, //实现水平居中
(this.data.canvasObj.height - fillImg.height * scale) / 2, //实现垂直居中
fillImg.width * scale,
fillImg.height * scale
);
};
});
},
3、存图方法:
var that = this;
wx.showLoading({
title: "正在保存",
mask: true,
});
console.log(this.data.canvasObj.context);
wx.canvasToTempFilePath(
{
canvasId: "avatar",
canvas: that.data.canvasObj.canvas,
success(res) {
wx.hideLoading();
var tempFilePath = res.tempFilePath;
wx.saveImageToPhotosAlbum({
filePath: tempFilePath,
success(res) {
wx.showModal({
content: "图片已保存到相册,赶紧晒一下吧~",
showCancel: false,
confirmText: "好的",
confirmColor: "#333",
success: function (res) {
if (res.confirm) {
}
},
fail: function (res) {},
});
},
fail: function (res) {
wx.showToast({
title: res.errMsg,
icon: "none",
duration: 2000,
});
},
});
},
fail: function (res) {
console.log(res.errMsg);
wx.showToast({
title: res.errMsg,
icon: "none",
duration: 2000,
});
},
},
that
);
问题1:贴图扭曲。
解决核心代码:
const dpr = wx.getSystemInfoSync().pixelRatio;
canvas.width = res[0].width * dpr;
canvas.height = res[0].height * dpr;
context.scale(dpr, dpr);
问题2:画布存本地相册一报错(canvasToTempFilePath: fail canvas is empty)
解决方案见“存图方法”
必须吐槽一下某度全是复制粘贴的内容,找问题解决方案太费劲。
最后贴出全部代码:
js
// index.js
// 获取应用实例
const app = getApp();
Page({
data: {
url: "",
avatarUrl: "",
canvasObj: {
with: 0,
height: 0,
initialized: false,
canvas: null,
context: null,
},
optionList: [
{
text: "获取头像",
type: "auth",
},
{
text: "相册选取",
type: "album",
},
{
text: "拍照上传",
type: "camera",
},
],
value: "所有",
chooseIndex: -1,
hideFlag: true, //true-隐藏 false-显示
animationData: {}, //
},
onload(){
wx.setStorageSync('useCount', '0');
},
clearCanvas() {
return new Promise((resolve) => {
this.getCanvas().then(() => {
this.data.canvasObj.context.clearRect(
0,
0,
this.data.canvasObj.width,
this.data.canvasObj.height
);
resolve();
});
});
},
// 事件处理函数
changeAvatar(type, redraw) {
const that = this;
if (type == "auth") {
if (redraw) {
that.clearCanvas().then(() => {
that.drawImage(this.data.avatarUrl);
});
return false;
}
wx.getUserProfile({
desc: "使用头像", // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
success: (res) => {
that.clearCanvas().then(() => {
var userInfo = res.userInfo;
that.setData({
avatarUrl: userInfo.avatarUrl.replace("/132", "/0"),
});
that.drawImage(userInfo.avatarUrl.replace("/132", "/0"));
});
},
});
} else {
wx.chooseImage({
count: 1,
sizeType: ["original", "compressed"],
sourceType: [type],
success(res) {
that.clearCanvas().then(() => {
// tempFilePath可以作为 img 标签的 src 属性显示图片
const tempFilePaths = res.tempFilePaths;
that.setData({
avatarUrl: tempFilePaths,
});
that.drawImage(tempFilePaths);
});
},
});
}
},
changeTemplate(e) {
if(!this.data.avatarUrl){
return wx.showToast({
title: "请先选择头像",
icon: "none",
duration: 2000,
});
}
this.setData({
chooseIndex: e.currentTarget.dataset.idx,
url: e.currentTarget.dataset.url,
});
if (this.data.chooseIndex == -1) {
this.drawImage(this.data.url);
} else {
this.changeAvatar("auth", true);
setTimeout(()=>{
this.drawImage(this.data.url);
},200)
}
},
drawImage(url) {
this.getCanvas().then(() => {
var fillImg = this.data.canvasObj.canvas.createImage();
fillImg.src = url;
fillImg.onload = () => {
const scale =
this.data.canvasObj.width / Math.max(fillImg.width, fillImg.height);
this.data.canvasObj.context.drawImage(
fillImg,
0,
0,
fillImg.width,
fillImg.height,
(this.data.canvasObj.width - fillImg.width * scale) / 2,
(this.data.canvasObj.height - fillImg.height * scale) / 2,
fillImg.width * scale,
fillImg.height * scale
);
};
});
},
getCanvas() {
return new Promise((resolve) => {
if (this.data.canvasObj.initialized) {
return resolve();
}
const query = wx.createSelectorQuery();
query
.select("#avatar")
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node;
const context = canvas.getContext("2d");
const dpr = wx.getSystemInfoSync().pixelRatio;
canvas.width = res[0].width * dpr;
canvas.height = res[0].height * dpr;
context.scale(dpr, dpr);
const canvasObj = {
canvas,
context,
width: res[0].width,
height: res[0].height,
initialized: true,
};
this.setData({ canvasObj });
resolve();
});
});
},
// 保存图片到相册
saveShareImg() {
const useCount = wx.getStorageSync("useCount");
if(Number(useCount)+1==4){
return wx.showToast({
title: "您已超过使用次数,可分享好友解锁无限使用次数!",
icon: "none",
duration: 2000,
});
}else{
wx.setStorageSync('useCount', Number(useCount) + 1);
}
if (!this.data.canvasObj.initialized) {
return wx.showToast({
title: "没有可以保存的头像",
icon: "none",
duration: 2000,
});
}
var that = this;
wx.showLoading({
title: "正在保存",
mask: true,
});
console.log(this.data.canvasObj.context);
wx.canvasToTempFilePath(
{
canvasId: "avatar",
canvas: that.data.canvasObj.canvas,
success(res) {
wx.hideLoading();
var tempFilePath = res.tempFilePath;
wx.saveImageToPhotosAlbum({
filePath: tempFilePath,
success(res) {
wx.showModal({
content: "图片已保存到相册,赶紧晒一下吧~",
showCancel: false,
confirmText: "好的",
confirmColor: "#333",
success: function (res) {
if (res.confirm) {
}
},
fail: function (res) {},
});
},
fail: function (res) {
wx.showToast({
title: res.errMsg,
icon: "none",
duration: 2000,
});
},
});
},
fail: function (res) {
console.log(res.errMsg);
wx.showToast({
title: res.errMsg,
icon: "none",
duration: 2000,
});
},
},
that
);
},
getOption: function (e) {
var that = this;
that.setData({
value: e.currentTarget.dataset.type,
hideFlag: true,
});
this.changeAvatar(e.currentTarget.dataset.type);
},
mCancel: function () {
var that = this;
that.hideModal();
},
showModal: function () {
var that = this;
that.setData({
hideFlag: false,
});
// 创建动画实例
var animation = wx.createAnimation({
duration: 400, //动画的持续时间
timingFunction: "ease", //动画的效果 默认值是linear->匀速,ease->动画以低速开始,然后加快,在结束前变慢
});
this.animation = animation; //将animation变量赋值给当前动画
var time1 = setTimeout(function () {
that.slideIn(); //调用动画--滑入
clearTimeout(time1);
time1 = null;
}, 100);
},
// 隐藏遮罩层
hideModal: function () {
var that = this;
var animation = wx.createAnimation({
duration: 400, //动画的持续时间 默认400ms
timingFunction: "ease", //动画的效果 默认值是linear
});
this.animation = animation;
that.slideDown(); //调用动画--滑出
var time1 = setTimeout(function () {
that.setData({
hideFlag: true,
});
clearTimeout(time1);
time1 = null;
}, 220); //先执行下滑动画,再隐藏模块
},
//动画 -- 滑入
slideIn: function () {
this.animation.translateY(0).step(); // 在y轴偏移,然后用step()完成一个动画
this.setData({
//动画实例的export方法导出动画数据传递给组件的animation属性
animationData: this.animation.export(),
});
},
//动画 -- 滑出
slideDown: function () {
this.animation.translateY(300).step();
this.setData({
animationData: this.animation.export(),
});
},
onShareAppMessage: function () {
const useCount = wx.getStorageSync("useCount");
wx.setStorageSync('useCount', "100");
return {
title: '我在这里生成了好看的国庆头像,你也快来试试呀',
}
// return custom share data when useCountr share.
},
});
// [img, 0, 0, 200, 200, 0, 0, 200, 196.57142857142856]
wxml
<!--index.wxml-->
<view class="container">
<view class="bg">
<image class="img" src="../../images/bg.png"></image>
<image class="tit" src="../../images/text.png"></image>
</view>
<view class="avatar content" bindtap="showModal">
<canvas class="avatar-board" type="2d" id="avatar" canvas-id="avatar"></canvas>
</view>
<!-- <image class="icon icon-left content" src="../../images/icon-left.png"></image>
<image class="icon icon-right content" src="../../images/icon-right.png"></image> -->
<scroll-view class="list content" scroll-y="{{false}}" bounces="{{false}}" scroll-x="true">
<view class="{{ chooseIndex == 0 ? 'item active':'item' }}">
<image bindtap="changeTemplate" data-idx="0" data-url="../../images/tag1.png" src="../../images/tag1.png"></image>
<image src="../../images/choosed.png" class="choose"></image>
</view>
<view class="{{ chooseIndex == 1 ? 'item active':'item' }}">
<image bindtap="changeTemplate" data-idx="1" data-url="../../images/tag2.png" src="../../images/tag2.png"></image>
<image src="../../images/choosed.png" class="choose"></image>
</view>
<view class="{{ chooseIndex == 2 ? 'item active':'item' }}">
<image bindtap="changeTemplate" data-idx="2" data-url="../../images/tag3.png" src="../../images/tag3.png"></image>
<image src="../../images/choosed.png" class="choose"></image>
</view>
<view class="{{ chooseIndex == 3 ? 'item active':'item' }}">
<image bindtap="changeTemplate" data-idx="3" data-url="../../images/tag4.png" src="../../images/tag4.png"></image>
<image src="../../images/choosed.png" class="choose"></image>
</view>
<view class="{{ chooseIndex == 4 ? 'item active':'item' }}">
<image bindtap="changeTemplate" data-idx="4" data-url="../../images/tag5.png" src="../../images/tag5.png"></image>
<image src="../../images/choosed.png" class="choose"></image>
</view>
<view class="{{ chooseIndex == 5 ? 'item active':'item' }}">
<image bindtap="changeTemplate" data-idx="5" data-url="../../images/tag6.png" src="../../images/tag6.png"></image>
<image src="../../images/choosed.png" class="choose"></image>
</view>
<view class="{{ chooseIndex == 6 ? 'item active':'item' }}">
<image bindtap="changeTemplate" data-idx="6" data-url="../../images/tag7.png" src="../../images/tag7.png"></image>
<image src="../../images/choosed.png" class="choose"></image>
</view>
</scroll-view>
<view class="btn-box content">
<image class="btn" bindtap="showModal" mode="aspectFit" src="../../images/btn1.png"></image>
<image class="btn" bindtap="saveShareImg" mode="aspectFit" src="../../images/btn2.png"></image>
</view>
<view class="modal modal-bottom-dialog" hidden="{{hideFlag}}">
<view class="modal-cancel" bindtap="hideModal"></view>
<view class="bottom-dialog-body bottom-positon" animation="{{animationData}}">
<!-- -->
<view class='Mselect'>
<view wx:for="{{optionList}}" wx:key="unique" data-type="{{item.type}}" data-value='{{item.text}}' bindtap='getOption'>
{{item.text}}
</view>
</view>
<view></view>
<view class='Mcancel' bindtap='mCancel'>
<text>取消</text>
</view>
</view>
</view>
</view>
wxss
/**index.wxss**/
.container {
position: relative;
background-size: cover;
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-self: center;
flex-direction: column;
position: relative;
}
.container .bg {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: -1;
}
.container .bg .img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.container .bg .tit {
position: absolute;
top: 60rpx;
left: 50%;
width: 622rpx;
height: 122rpx;
transform: translateX(-50%);
}
.avatar {
margin-top: 120rpx;
width: 300rpx;
height: 300rpx;
border: 5px solid #fff;
border-radius: 10rpx;
overflow: hidden;
}
.list {
box-sizing: border-box;
width: 700rpx;
height: 238rpx;
white-space: nowrap;
background: #ffcbab;
border-radius: 20rpx;
padding: 20rpx;
overflow: hidden;
border: 4px solid rgba(255,255,255,0.3);
}
.list .item {
display: inline-block;
width: 180rpx;
height: 180rpx;
box-sizing: border-box;
background: #fff;
border-radius: 10rpx;
overflow: hidden;
}
.list .item {
position: relative;
}
.list .item .choose {
display: none;
}
.list .item.active .choose {
display: block;
width: 50rpx;
height: 50rpx;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%)
}
.list .item image {
width: 100%;
height: 100%;
object-fit: contain;
}
.list .item~.item {
margin-left: 20rpx;
}
.container .icon {
position: absolute;
bottom: 50%;
width: 40rpx;
height: 40rpx;
padding: 10rpx;
margin-bottom: -170rpx;
background: #ffcbab;
border-radius: 50%;
}
.container .icon-left {
left:4rpx;
}
.container .icon-right {
right:4rpx;
}
.avatar-board {
width: 100%;
height: 100%;
background: #fff;
}
.btn-box {
width: 700rpx;
display: flex;
justify-content:space-between;
align-items: center;
}
.btn-box .btn {
width: 360rpx;
height: 160rpx;
object-fit: contain;
color: #333;
font-size: 32rpx;
}
.content {
position: relative;
z-index: 2;
}
.arrow{
display:inline-block;
border:6px solid transparent;
border-top-color:#000;
margin-left:8px;
position:relative;
top:6rpx;
}
/* ---------------------------- */
/*模态框*/
.modal{position:fixed; top:0; right:0; bottom:0; left:0; z-index:1000;}
.modal-cancel{position:absolute; z-index:2000; top:0; right:0; bottom: 0; left:0; background:rgba(0,0,0,0.3);}
.bottom-dialog-body{width:100%; position:absolute; z-index:3000; bottom:0; left:0;background:#dfdede;}
/*动画前初始位置*/
.bottom-positon{-webkit-transform:translateY(100%);transform:translateY(100%);}
/* 底部弹出框 */
.bottom-positon{
text-align: center;
}
.Mselect{
margin-bottom: 20rpx;
}
.Mselect view{
padding: 32rpx 0;
background: #fff;
font-size: 32rpx;
}
.Mselect view:not(:last-of-type){
border-bottom: 1px solid #dfdede;
}
.Mcancel{
color: #999;
background: #fff;
padding: 26rpx 0;
}
最后希望本文能对大家在小程序图片合成方案上有所帮助。