小程序里分享到朋友圈功能实现

微信放开首页小程序的入口后,小程序变得触手可及,也变得愈发有价值。
最近在开发一个小程序,小程序提供的接口其实还算丰富,有个常用的功能是分享到朋友圈,并没有直接的接口可以使用,通用的做法是绘制图片让用户保存到相册再分享,所以现在的问题是如何动态地生成图片。
先看下最终的图像

命令式
不管是前端里的canvas,还是java后端的java.awt.image,都提供了基本的绘制图像的功能,线条、文本和图片都可以绘制。
但这种方式,工作量特别大。比如椭圆,canvas倒是提供了ellipse方法可以方便地绘制椭圆,但小程序里没有这个方法,所以只能用bezierCurveTo一针一线地去缝制。
class Curve {
    constructor({x, y, radiusX, radiusY, borderRadius, position}) {
        const kappa = 4 * ((Math.sqrt(2) - 1) / 3);
        const ox = borderRadius * kappa; // control point offset horizontal
        const oy = borderRadius * kappa; // control point offset vertical
        const xm = x + borderRadius; // x-middle
        const ym = y + borderRadius; // y-middle

        switch(position) {
            case 'leftTop':

            this.startx = x;
            this.starty = ym;

            this.cp1x = x;
            this.cp1y = ym - oy;

            this.cp2x = xm - ox;
            this.cp2y = y;

            this.x = xm;
            this.y = y;

            break;

            case 'rightTop':

            this.startx = x - borderRadius;
            this.starty = y;

            this.cp1x = x - borderRadius + ox;
            this.cp1y = y;

            this.cp2x = x;
            this.cp2y = ym - oy;

            this.x = x;
            this.y = ym;

            break;

            case 'rightBottom':

            this.startx = x;
            this.starty = y - borderRadius;

            this.cp1x = x;
            this.cp1y = y - borderRadius + oy;

            this.cp2x = x - borderRadius + ox;
            this.cp2y = y;

            this.x = x - borderRadius;
            this.y = y;

            break;

            case 'leftBottom':

            this.startx = xm;
            this.starty = y;

            this.cp1x = xm - ox;
            this.cp1y = y;

            this.cp2x = x;
            this.cp2y = y - borderRadius + oy;

            this.x = x;
            this.y = y - borderRadius;

            break;

        }
        
    }
}

export const ellipse = ({x, y, radiusX, radiusY, borderRadius, ctx}) => {

    const curves = [
        new Curve({
            x: x - radiusX,
            y: y - radiusY,
            radiusX,
            radiusY,
            borderRadius,
            position: 'leftTop'
        }),
        new Curve({
            x: x + radiusX,
            y: y - radiusY,
            radiusX,
            radiusY,
            borderRadius,
            position: 'rightTop'
        }),
        new Curve({
            x: x + radiusX,
            y: y + radiusY,
            radiusX,
            radiusY,
            borderRadius,
            position: 'rightBottom'
        }),
        new Curve({
            x: x - radiusX,
            y: y + radiusY,
            radiusX,
            radiusY,
            borderRadius,
            position: 'leftBottom'
        }),
    ]

    curves.forEach((curve, index) => {
        if (index === 0) {
          ctx.moveTo(curve.startx, curve.starty);
        } else {
          ctx.lineTo(curve.startx, curve.starty);
        }
        
        ctx.bezierCurveTo(curve.cp1x, curve.cp1y, curve.cp2x, curve.cp2y, curve.x, curve.y);
    });
}


// 绘制椭圆

ellipse({
  x: 100,
  radiusX: 50,
  radiusY: 20,
  borderRadius: 12,
  ctx: wx.createCanvasContext('screenCanvas')
})
对,只是简单的绘制椭圆就这么麻烦,绘制整张图片太繁琐了。
再比如文本,因为canvas不支持多行文本,line break就需要自己做,这也是比较麻烦的事,因为每个字符占用的宽度不同。canvas倒是提供了measureText方法能衡量文本所占的宽度,但小程序里又没这方法。幸好需要绘制的文本大多是中文,可以给每个字符固定一个宽度来计算文本所需占用的宽度。
声明式
声明式的html非常适合描述UI,用chrome headless载入网页然后截个屏超级方便, Using headless Chrome as an automated screenshot  tool
我们先来看下如何只调用现有的小程序接口不依赖后端实现,寻觅下发现是可以的, Eyes Above The Waves: Drawing DOM Content To Canvas
  1. svg里的foreignObject元素可以包含html
  2. svg可以通过data url转换成图片
  3. 再通过drawImage把图片画到canvas上
找个前端模版引擎比如mustache,需要小量工作把wxml转成mustache模板,样式能直接用,然后结合数据,就能动态地生成图片。但是文字能出来了,头像图片却没有,查了下是foreignObject的限制,图片必须要转换成data url。
但图片如何转成data url呢,小程序里没有toDataURL这个方法,只有canvasGetImageData,但这个接口得到的是图像的每个像素的颜色值,没法转成图片本身的二进制数据。最后只能选择计算下头像的位置,然后用drawImage画。
okay,至此,终于能生成图片了。
但是上真机一看,图片没出来,wtf。查了下两个原因造成的
第一是,drawImage前,必须要先用downloadFile把文件下载到本地。ok,下载到本地后终于能显示了。
但是用svg表示的主体内容还是没法显示,问题出在真机上的canvas是客户端原生实现的,drawImage只能通过tempFilePath的方式去读取资源。所以这条路走通必须要依赖后端了,把svg发给后端,转成图片后再发回给前端使用。
所以结论是,如果需要生成的图片简单,命令式地绘制图片就可以了,内容多或者复杂的话,还是需要用声明式的方法。
还有个问题是,考虑到高清屏,绘制图像时需要考虑设备像素比。以iPhone 6为例,绘制设置canvas大小为目标大小的两倍,然后用css transform缩小到原来的1/2,是能实现功能的。但是到了真机就不行了,原因上面也提到了,canvas是客户端原生实现的。正确的方式是使用canvas不能直接展示给用户看,而是画好后再保存到临时文件后通过image标签缩放展示。

本文来自网易实践者社区,经作者江云唬授权发布。