美文网首页
微信小程序实现合成头像

微信小程序实现合成头像

作者: Eason_0cce | 来源:发表于2022-09-29 17:45 被阅读0次

    小程序版本: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;
     }
    
    

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

    相关文章

      网友评论

          本文标题:微信小程序实现合成头像

          本文链接:https://www.haomeiwen.com/subject/djekartx.html