小程序canvas绘图

背景

由于小程序本身不具备分享到朋友圈的能力,所以我们要采用曲线救国的方式来实现。即生成“分享海报”后,由用户自主保存,再去朋友圈分享。
下面是最终的实现效果:

807868352568046452.jpg | center | 250x351

又是图片合成,又是保存图片,看来是免不了使用canvas了,还是有点小慌(canvas菜鸡一枚)。中途也是遇到不少的问题。下面一起来看看吧~

欢乐踩坑之旅

踩坑之前提供一下,微信的文档,读完文档,走遍天下:小程序cavas文档
下面的几点,乍一看还是感觉很简单的。我当然也是这么想的,然后就差点没爬出来(调试时间花的比较多,总体难度还好)。
接下来,就一起把这些坑都踩平吧。

画布适配

首先介绍一下小程序canvas的特点:

  • canvas元素大小可以使用小程序特有的rpx单位,但是内部绘制的时候,单位是px。所以需要获取设备的屏幕宽度自行计算。方法也比较简单;
  • canvas元素在真机上不可以使用css3动画效果;
  • canvas元素在真机上的层级最高(无法实现在上面做一个自定义的弹窗)。
利用比例换算适配各种屏幕:

wx.getSystemInfo({
    success: res => {
        if (res.errMsg === 'getSystemInfo:ok') {
            let width = res.windowWidth
            this.setData({
                system: res.system.indexOf('iOS') > -1 ? 'ios' :'android', // 判断系统,为什么要用之后会说明
                canvasRatio: width / 375, // 之后用于其他内容的比例转换
                canvasWidth: res / 375 * this.data.canvasWidth, // 画布宽度
                canvasHeight:res / 375 * this.data.canvasHeight // 画布高度
            })
        } else {
            // 失败,一般不会进这里
        }
    },
    fail: res => {
        // 失败,一般不会进这里
    }
})

农历年计算

这个问题难点在于不了解农历和公历之间的换算规则(感觉两者没有什么关系啊= =)。有兴趣的上网搜索一下,很容易能找到,方法就不贴了,相当长。

绘制文本

写文本会有问题吗?
多行文本不是可以自动换行吗?
文本溢出显示省略号,不是直接用-webkit-line-clamp就好了吗?
文本加粗直接一个bold不就搞定了吗?
在canvas中,还真有问题,上面的想法都不能实现。因为只提供了这样一个API:CanvasContext.fillText(string text, number x, number y, number maxWidth)
可以看到,这个API只能设置填充的文本,开始填充的x轴距离,y轴距离,绘制文本的总宽度。
也就是说,我们除了设置要填充哪些字,从哪里开始绘制,绘制到哪里结束以外,其他的都束手无策。
可想而知,多行文本、文字过多、过少都没有办法处理,因此需要写一个方法进行处理。

思路:

  1. 小程序中还提供了一个很好的API:Object CanvasContext.measureText(string text),使用这个接口可以量出当前传入的文本所占的宽度,有了这个数据,我们就可以通过循环叠加每个字的方式来判断一行是否占满,占满则记为一组,如果超过了设置的显示行数,则回退一个字改为“…”,这样同时实现了换行和溢出省略号;
  2. 用多行文本需要设置行高,关系到下一行从哪里开始画;
  3. 加粗的实现采用,左右偏移0.5px进行绘制,看起来就有加粗的效果了。

下面是实现的方案:

/**
 * @func 绘制多行文本内容
 * @params {object} ctx canvas画布
 * @params {string} config.text 文本
 * @params {number} config.line 行数
 * @params {number} config.fontsize 字号
 * @params {string} config.color 颜色
 * @params {number} congig.X 画布 x 位置
 * @params {number} config.Y 画布 y 位置
 * @params {number} config.paddingRight 右边的距离
 * @params {boolean} config.bold 是否加粗
 * @returns {number} 画完的高度
 */
drawText (ctx, config) {
    let tempContent = '' // 临时文本,用于存储拼接的字符
    let row = [] // 临时数组,用于存储换行的内容
    let lineHeight = config.lineHeight ? config.lineHeight : config.fontsize * 1.7 // 行高
    ctx.setFontSize(config.fontsize) // 这会影响到计算宽度,必须先设置
    ctx.setFillStyle(config.color)
    for (let i = 0; i < config.text.length; i++) {
        if (ctx.measureText(tempContent).width < this.data.canvasWidth - config.X - config.paddingRight - lineHeight) { // 未超出宽度继续拼接
            tempContent += config.text[i]
        } else {
            i--
            row.push(tempContent) // 记录换行
            tempContent = ''
        }
    }
    row.push(tempContent)
    if (row.length > config.line) { // 此时说明有多行
        let rowCut = row.slice(0, config.line) // 截取需保留的行数
        let rowPart = rowCut[config.line - 1] // 取最后一行(这一行需要加省略号)
        let lastLine = ''
        let empty = []
        for (let i = 0; i < rowPart.length; i++) {
            if (ctx.measureText(lastLine).width < this.data.canvasWidth - lineHeight - config.X) { // 宽度预留省略号的位置
                lastLine += rowPart[i]
            } else {
                break
            }
        }
        empty.push(lastLine)
        let group = empty[0] + '...'
        rowCut.splice(config.line - 1, 1, group)
        row = rowCut // 此时 row就是每行的文本
    }
    for (let b = 0; b < row.length; b++) {
        ctx.fillText(row[b], config.X, config.Y + lineHeight + b * lineHeight, this.data.canvasWidth) // 写入文字内容
        if (config.bold) {
            ctx.fillText(row[b], config.X + .5, config.Y + lineHeight + b * lineHeight, this.data.canvasWidth)
            ctx.fillText(row[b], config.X - .5, config.Y + lineHeight + b * lineHeight, this.data.canvasWidth)
        }
    }
}

小程序码获取展示

小程序码获取展示并没有什么问题,需要注意的是要记得把downFile中添加相应的域名,千万不要忘记。

正文图片显示

正文图片是从文章中正则抽取出来的第一张图,图片大小、宽高比均不确定。这就需要一个显示的策略。为了保证相对好的效果,采用的方案是保证短边显示出来,并取长边正中的部分。如下图:

image.png | center | 412x238

具体实现见下列代码:

/**
 * @func 取图片绘制区域
 * @params {object} ctx 画布
 * @params {object} imgInfo 图片信息,包含图片路径、宽、高等
 * @params {number} x 绘制区域 x
 * @returns {number} y 绘制区域 y
 */
drawImageArea (ctx, imgInfo,x, y) {
    let imageWidth = 155 * this.data.canvasRatio // 画布中图片宽度
    let imageHeight = 111 * this.data.canvasRatio // 画布中图片高度
    let actWidth = 0 // 实际宽度
    let actHeight = 0 // 实际高度
    let sx = 0 // 起始点 sx
    let sy = 0 // 起始点 sy
    let dw = imageWidth / imgInfo.width  // 画布与图片的宽度比例
    let dh = imageHeight / imgInfo.height // 画布与图片的高度比例
    if (dw < dh) { // 根据宽高比切高度
        actWidth = imageWidth / dh
        actHeight = imgInfo.height
        sx = (imgInfo.width - actWidth) / 2
        sy = 0
    } else {
        actWidth = imgInfo.width
        actHeight = imageHeight / dw
        sx = 0
        sy = (imgInfo.height - actHeight) / 2
    }
    if (actWidth < 2000 && actHeight < 2000 || this.data.system === 'ios') {
        ctx.drawImage(imgInfo.path, sx, sy, actWidth, actHeight, x, y, imageWidth, imageHeight)
    }
}

正本图片来源

由于图片的来源是外部的链接,要解析图片就必须用wx.getImageInfo获取,而获取图片的前提是域名加在downFile列表中。所以说,这个需求在弱小的前端手中就无法实现了。
想到的实现方案是把图片地址取出来给后端,由后端转存到自己的oss上进行访问。搞定。

总结

总体看下来,其实没有很难的地方,具体实现起来可能没有看起来这么轻松。有空的时候都可以试试看,实现一下这个还算比较常见的需求。


 上一篇
TypeScript学习(一) TypeScript学习(一)
我是从去年开始使用TypeScript的,刚开始用的时候也是因为项目使用的ts,所以也是糊里糊涂的用着,一直没系统整理学习过,所以正好借此机会,整理一下,加深理解。 一、概念 首先呢,它是跟javaScript一样属于一种脚本语言; 其次
2018年11月07日
下一篇 
正则常用字符含义 正则常用字符含义
正则常用字符含义 字符 含义 ? 匹配前一项0次或1次 等价于{0,1} + 匹配前一项1次或多次 等价于{1,} * 匹配前一项0次或多次 等价于{0,} . 除换行符和其他Unicode行终止符之外的任意字符
2018年10月25日
  目录