小程序通过canvas生成海报保存为图片的技巧

小程序通过canvas生成海报保存为图片的技巧

最近公司要求在小程序点击分享,要生成一张图片,可以保存在用户相册里,图片里的内容根据后台返回的数据生成,这就涉及到小程序画布的知识了,因为微信文档上,画布的Api很多已经废弃了,而且新的Api怎么用也没写的很清楚,所以就踩了很多坑。经过我查阅了大量大佬写过的文章,自己也总结一些方法,避免后面的人踩坑。

声明canvas的组件
<canvas disable-scroll="true" type="2d" id="myCanvas" canvas-id="card" style="width: 700rpx;height: 1100rpx;"></canvas>
<!--声明一个宽度为700,高度为1100的画布-->
<!--这里disable-scroll为true是为了防止画布在滚动的时候,发生穿透,导致底部内容也会跟着滚动,具体根据大家各自业务考虑是否使用-->

这里注意,属性id和属性canvas-id是有区别的,前者用于样式或者生成canvas实例,后者是调用微信api接口时需要用到,这个看后面的例子就会明白

由于我想在页面打开的时候,就开始绘制画布,因为这样在显示画布的时候内容就已经差不多渲染好了。注意如果这样做就不能做隐藏处理,因为做了隐藏处理,生成canvas实例就会报错,原因是找不到canvas这个节点。除非你是显示画布后才开始绘制画布,但是个人觉得这样会让用户体验下降,特别是绘制网络图片的时候,通常还要等个一两秒图片才绘制好。
那么,如果想要页面打开后就开始绘制画布,又不让用户察觉到画布的存在,该怎么做呢?我自己的解决方案是通过定位,将画布向左移到200%,超出可视窗口,这样用户既察觉不到画布的存在,生成canvas实例时又可以找到canvas这个节点。代码下图所示

 <canvas disable-scroll="true" type="2d" id="myCanvas" canvas-id="card" style="width: 700rpx;height: 1100rpx;left:{{canvasLeft}}">
	<cover-image src="../../utils/image/card/button.png" class="btnArea" catchtap="handleSave">保存图片</cover-image>
 </canvas>
<!--因为我想用一张图片覆盖在画布上,由于canvas是原生组件,层级最高,能覆盖他的只有cover-image和cover-view了-->
data: {
	canvasLeft: "200%"
},
//想要显示画布就把canvasLeft设置为50%即可
#myCanvas {
    border-radius: 20rpx;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

下图所示是新 Canvas 2D 实例的代码,还在用wx.createCanvasContext的小伙伴请停止使用,因为官方已经废除了。(注意,使用微信开发工具的真机调试画布会报错,这是微信开发工具的bug,如果要看画布在真机上的效果,请用预览的功能
在这里插入图片描述

 draw() {
     const query = wx.createSelectorQuery() // 创建节点查询器 query
     query.select('#myCanvas')//获取canvas节点,这里是id不是canvas-id
         .fields({ node: true, size: true })
         .exec((res) => {
             const canvas = res[0].node
             this.data.canvasNode = canvas//保存canvas实例
             const ctx = canvas.getContext('2d')
             const dpr = wx.getSystemInfoSync().pixelRatio //获取设备像素比
             canvas.width = res[0].width * dpr//设置画布的宽度
             canvas.height = res[0].height * dpr//设置画布的高度
             ctx.scale(dpr, dpr)//在调用后,之后创建的路径其横纵坐标会被缩放,这里建议设计图统一以750px的为准,这样像素就不用换算,可以直接写入
             
             //<--这里是绘制画布内容的代码-->
      })
  },


绘制画布内容
绘制文本
//绘制字体大小为24px(12px),颜色为#999的文本
const rpx = wx.getSystemInfoSync().windowWidth / 750 //这里是为了内容能在不同设备自适应
ctx.font = `${24 * rpx}px sans-serif`;//设置当前字体样式
ctx.fillStyle = '#999999'//设置填充色,这里建议颜色不要缩写
ctx.fillText('佣金比例:', 269 * rpx, 252 * rpx);//在画布上绘制被填充的文本,ctx.fillText(要绘制的文本, x坐标, y坐标)

//绘制字体大小为34px(17px),颜色为红色,粗体的文本
const rpx = wx.getSystemInfoSync().windowWidth / 750 //这里是为了内容能在不同设备自适应
ctx.font = `bold ${34 * rpx}px sans-serif`;
ctx.fillStyle = 'red'
ctx.fillText('佣金比例:', 379 * rpx, 210 * rpx);

//绘制自动换行文本(这里借鉴的是别人的写法,是哪个大佬写的我忘了,因为大佬用的是旧版的写法和自己的业务需要的原因,所以自己改了点代码)
//这里我写在util.js里面,方便调用。
let dealWords = function (options, rpx) {
  options.ctx.fillStyle = options.fontColor//设置字体颜色
  options.ctx.font = options.fontSize;//设置字体大小
  var allRow = Math.ceil(options.ctx.measureText(options.word).width / options.maxWidth);//实际总共能分多少行
  var count = allRow >= options.maxLine ? options.maxLine : allRow;//实际能分多少行与设置的最大显示行数比,谁小就用谁做循环次数
  var endPos = 0;//当前字符串的截断点
  for (var j = 0; j < count; j++) {
    var nowStr = options.word.slice(endPos);//当前剩余的字符串
    var rowWid = 0;//每一行当前宽度    
    if (options.ctx.measureText(nowStr).width > options.maxWidth) {//如果当前的字符串宽度大于最大宽度,然后开始截取
      for (var m = 0; m < nowStr.length; m++) {
        rowWid += options.ctx.measureText(nowStr[m]).width;//当前字符串总宽度
        if (rowWid > options.maxWidth) {
          if (j === options.maxLine - 1) { //如果是最后一行
            options.ctx.fillText(nowStr.slice(0, m - 1) + '...', options.x, options.y + (j + 1) * options.lineheight * rpx);    //(j+1)*18这是每一行的高度        
          } else {
            options.ctx.fillText(nowStr.slice(0, m), options.x, options.y + (j + 1) * options.lineheight * rpx);
          }
          endPos += m;//下次截断点
          break;
        }
      }
    } else {//如果当前的字符串宽度小于最大宽度就直接输出
      options.ctx.fillText(nowStr.slice(0), options.x, options.y + (j + 1) * options.lineheight * rpx);
    }
  }
}
module.exports = {
  dealWords
}

//调用的方法
const utils = require('../../utils/util.js')
const rpx = wx.getSystemInfoSync().windowWidth / 750 //这里是为了内容能在不同设备自适应
utils.dealWords({
	ctx: ctx, //画布上下文
	fontSize: `${30 * rpx}px sans-serif`, //设置当前字体样式
	fontColor: '#333333',//设置填充色
	word: '哈哈哈哈', //需要处理的文字
	lineheight: 40,//行高的配置
	maxWidth: 384 * rpx, //一行文字最大宽度
	x: 270 * rpx, //文字在x轴要显示的位置
	y: 39 * rpx, //文字在y轴要显示的位置
	maxLine: 3 //文字最多显示的行数
}, rpx)


绘制图片
//绘制图片的封装方法,支持生成普通图片、水印图片(通过设置透明度)、圆形图片(常用于小程序二维码一物一码的场景下)
let drawImage = function (canvas, ctx, imgsrc, x, y, width, height, isAlpha,globalAlpha,isRange) {
  return new Promise((resolve, reject) => {
    let img = canvas.createImage()
    img.src = imgsrc
    img.onload = function () {
      ctx.save();//保存绘图上下文。与ctx.restore()配对
      //是否绘制透明度图片
      if (isAlpha) {
        ctx.globalAlpha = globalAlpha //设置透明度。范围 0-1,0 表示完全透明,1 表示完全不透明
      } else {
        ctx.globalAlpha = "1"
      }
      //是否绘制圆形图片
      if (isRange) { 
      	//开始创建一个路径。
        ctx.beginPath();
        //创建一条弧线。创建一个圆可以指定起始弧度为 0,终止弧度为 2 * Math.PI。
        //ctx.arc(圆心的x坐标,圆心的y坐标,圆的半径,起始弧度,终止弧度,弧度的方向是否是逆时针)
        ctx.arc(width / 2 + x, height / 2 + y, width / 2, 0, Math.PI * 2, false);
        //从原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内(不能访问画布上的其他区域)。
        //可以在使用 clip 方法前通过使用 save 方法对当前画布区域进行保存,并在以后的任意时间通过restore方法对其进行恢复。
        ctx.clip();
      }
      //绘制图像到画布,ctx.drawImage(图片实例, x坐标, y坐标, 宽度, 高度);
      //网络图片要通过 getImageInfo / downloadFile 先下载,代码后文会写
      ctx.drawImage(img, x, y, width, height);
      ctx.restore();//恢复之前保存的绘图上下文。与ctx.save()配对
      resolve()
    }
    img.onerror = (err) => {
      console.log(err)
      reject(err)
    }
  })
}
module.exports = {
  drawImage 
}

//调用的方法
const utils = require('../../utils/util.js')
const rpx = wx.getSystemInfoSync().windowWidth / 750 //这里是为了内容能在不同设备自适应
//绘制宽度为172px,高度为74px,透明度0.2的图片
utils.drawImage(canvas, ctx, '../../utils/image/card/logo.png', 489 * rpx, 71 * rpx, 172 * rpx, 74 * rpx, true,0.2,false)

//绘制网路图片
//网络图片要通过wx.getImageInfo获取图片信息。网络图片需先去小程序平台配置download域名才能生效。
wx.getImageInfo({
    src: '此处为网路图片的路径',
    success: function (res) {
    //res.path 网络图片转为本地的临时路径
    //图片压缩,由于有些网络图片体积太大,影响绘图速度,所以可以适当的压缩下
        wx.compressImage({
            src: res.path, 
            quality: 1, // 压缩质量,范围0~100,数值越小,质量越低,压缩率越高(仅对jpg有效)
            complete: function (res) {
            //res.tempFilePath是压缩后的路径
            //utils.drawImage为上文绘制图片的封装方法
                utils.drawImage(canvas, ctx, res.tempFilePath, 49 * rpx, 55 * rpx, 200 * rpx, 200 * rpx)
            }
        })
    }
})

//在画布上生成微信小程序二维码,一物一码的业务场景下
//这里我们服务端调用的是wxacode.getUnlimited,接口会返回二维码的图片Buffer,我们需要做的是把图片buff转换成小程序本地路径
//要注意小程序请求服务端接口时,responseType要设置为arraybuffer

//获取全局唯一的文件管理器
let fileManager = wx.getFileSystemManager()
//wx.env.USER_DATA_PATH 获取本地用户文件目录的路径
//filePath  自定义的本地临时路径
let filePath = wx.env.USER_DATA_PATH + '/inner.jpg'
var that = this
//获取到的二维码的图片Buffer写入本地临时路径
fileManager.writeFile({
    filePath: filePath,//本地临时路径
    encoding: 'binary',//编码方式,二进制
    data: data,//这里为后台接口返回的图片Buffer
    success: function (res) {
    	//由于业务需求,需要绘制圆形的二维码,但是后台给的图片是直角的,所以需要自己绘制成圆形
        utils.drawImage(canvas, ctx, filePath, 531 * rpx, 929 * rpx, 150 * rpx, 150 * rpx, false, true)
    },
    fail: function (res) {
        console.log(res)
    },
});


其他小技巧
//绘制矩形图案,可支持圆角(借鉴大佬的),代码就不解释了,大家自行研究
//参数r为圆角的像素,color为矩形的填充色
let roundRectColor = function (ctx, x, y, w, h, r, color) { 
  if (w < 2 * r) { r = w / 2; }
  if (h < 2 * r) { r = h / 2; }

  ctx.beginPath();
  ctx.fillStyle = color;

  ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5);
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.lineTo(x + w, y + r);

  ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2);
  ctx.lineTo(x + w, y + h - r);
  ctx.lineTo(x + w - r, y + h);

  ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5);
  ctx.lineTo(x + r, y + h);
  ctx.lineTo(x, y + h - r);

  ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI);
  ctx.lineTo(x, y + r);
  ctx.lineTo(x + r, y);

  ctx.fill();
  ctx.closePath();
}
module.exports = {
  roundRectColor
}

//引用方法
const utils = require('../../utils/util.js')
const rpx = wx.getSystemInfoSync().windowWidth / 750 //这里是为了内容能在不同设备自适应

//绘制X坐标为25px,y坐标为25px,宽度为650px,高度为880px,圆角为10px,填充色为白色的矩形
utils.roundRectColor(ctx, 25 * rpx, 25 * rpx, 650 * rpx, 880 * rpx, 10 * rpx, "#ffffff");

//绘制填充色为渐变色的矩形
//创建一个线性的渐变颜色,ctx.createLinearGradient(起点的 x 坐标,起点的 y 坐标,终点的 x 坐标,终点的 y 坐标)
const grd = ctx.createLinearGradient(0, 0, 0, 1100* rpx)
//添加颜色的渐变点,范围 0-1
grd.addColorStop(0, '#FDB862')
grd.addColorStop(1, '#FB4631')
utils.roundRectColor(ctx, 0, 0, 700 * rpx, 1100 * rpx, 20 * rpx, grd);

小程序通过canvas生成海报保存为图片的技巧的技巧我知道的就只有这些了,还有很多技巧我可能还没发现,也请各位大佬指教了。代码不全是我自己写的,有很大一部分也是拿其他大佬的代码改的,我只是个卑微的搬运工罢了

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值