微信小程序实现合成头像

2022-09-29  本文已影响0人  Eason_0cce

小程序版本:2.19.4
实现效果如图:

Screenshot_20220929_180134_com.tencent.mm.jpg

最近开发了一款可以合成头像的小程序应用,期间碰到了一些尴尬的问题,我这边做出总结,希望能帮广大码农避坑。

关键技术点如下: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;
 }

最后希望本文能对大家在小程序图片合成方案上有所帮助。

上一篇下一篇

猜你喜欢

热点阅读