美文网首页
小程序之Canvas图片文字绘制长图

小程序之Canvas图片文字绘制长图

作者: 笑谈红尘乱离人 | 来源:发表于2020-02-15 22:32 被阅读0次

    这几天在做一个绘制长图的功能, 试了很多方法均无法有效的实现, 经过长时间折腾, 总算搞了出来. 期间走了很多坑, 一次次的重试以及换方法, 几近崩溃, 我觉得很有必要把这个艰难过程记录下来. 实现起来确实有点繁琐, 我想不出更好的方案来代替, 如哪位朋友有更简单或者更正宗更高效的实现方式, 还望私信或评论告知.

    需求是这样的, 就像写信一样, 有标题, 有正文, 还有署名以及日期. 不同的是加了图片, 图片数量不超过九张; 另外, 还需要一张图片信纸作为整封信的背景图. 因此整理一下写信所需元素:

    • title标题
    • content正文内容
    • images所选图片 (非必须)
    • backgroundImage信纸背景图

    之前有过了解, 基本上前端语言都会提供相对较底层的绘制技术, 微信小程序提供了CanvasContextAPI 用于实现各种绘制功能, 基本上可以满足日常需求. 点击查看微信小程序CanvasContext相关API

    最难的就是绘制文字!!!

    简直是天坑!!!

    简直是神坑!!!

    简直是巨坑!!!

    我以前并没使用过绘制文字的 API , 看着文档描述的看起来很简单. 先不说里面的坑, 开始写代码, wxml 文件需要创建一个画布, 这里的宽高单位是px:

    <canvas canvas-id="canvasId" style="width:{{canvasWidth}}px;height:{{canvasHeight}}px;background:red;position:fixed;top:{{canvasTop}}px;" ></canvas>
    

    画布 Canvas 的widthheight

    默认 width 为300px, height 为150px, 显然不是想要的

    • width: 根据需求选用屏幕的宽度, 这里有个坑值得注意, 如果直接设置为wx.getSystemInfoSync().windowWidth, 绘制出的图片很模糊, 质量差, 一看就是分辨率不够. 因此宽度需要设置的比屏幕宽度大, 可以是其的两倍或三倍大小. 做iOS开发的时候知道在不同机型有可能使用两倍图, 如iPhone8; 而iPhone8 Plus则使用三倍图. 查看 API 得知wx.getSystemInfoSync().pixelRatio即为设备屏幕的像素比, 因此设置:
    let info = wx.getSystemInfoSync(); // 设备系统信息
    this.setData({ canvasWidth: info.windowWidth * info.pixelRatio }); // 画布的宽度
    
    • height: 因为正文内容是未知的, 图片数量未知, 每张图片宽高也不一样, 所以画布高度就很难确定, 需要计算文字的高度, 为了不让图片变形以及图片宽度不超过画布, 需等比例缩小图片. 最后累加起来得到所有图片的高度. 从上至下绘制的时, 文字与图片、图片和图片之间有一定的间隔, 间隔大小也需乘以设备屏幕的像素比

    艰难的绘制文字

    先设置canvasHeight为300px :

    this.setData({ canvasHeight: 300 })
    

    试着绘制一小段文字, 代码如下:

    this.data.content = '今天的天气真不错阳光明媚';
    let ctx = wx.createCanvasContext('canvasId');
    // 绘制画布背景色
    ctx.setFillStyle('#95B095');
    ctx.fillRect(0, 0, this.data.canvasWidth, this.data.canvasHeight);
    // 开始绘制文字
    let fontSize = 17 * this.data.scale;
    ctx.setFontSize(fontSize);
    ctx.setFillStyle('#00000');
    let x = 0;
    let y = fontSize + 5;
    ctx.fillText(this.data.content, x, y - 5);
    ctx.draw();
    

    通过wx.canvasToTempFilePath()预览图片后如下图(所有截图的灰色部分代表模拟器外区域, 黑色代表模拟器, 浅绿色代表画布):

    图1 --

    看起来挺好的, 没什么问题, 但当正文内容很长的时候, 会发现显示的不完整, 没有换行. 有可能是缺少一个参数导致, 没有设置绘制文字的最大宽度导致的, 因此改写绘制代码如下:

    ctx.fillText(this.data.content, x, y - 5, this.data.canvasWidth);
    

    整段文字被压缩在画布范围内了, 被挤变形, 不忍直视, 效果如下:

    图2 --

    查了一圈官方文档也没有找到解决方法, 网上找了点资料大多说是遍历字符串, 计算每个字的宽度, 累加起来如果大于画布宽度则换行, 一个字一个字的绘制, 看着似乎有效, 于是按照这个方法来实现一遍. 开始使用for循环去遍历, 最开始遍历字符串时没啥问题, 后来试多几次遇到一个小问题, 当文字内容中有emoji表情()的时候, 绘制不出表情字符, 原因是遍历的时候就无法获取表情字符. 于是后面改用for...of来遍历, 才没出现此问题. 代码如下:

    this.data.content = '今天的天气真不错阳光明媚,非常适合去郊游,哥儿们几个去不去呢,大家也好久没聚在一起聊聊了,非常难得的机会';
    let ctx = wx.createCanvasContext('canvasId');
    // 设置画布背景色
    ctx.setFillStyle('#95B095');
    ctx.fillRect(0, 0, this.data.canvasWidth, this.data.canvasHeight);
    // 开始绘制文字
    let fontSize = 17 * this.data.scale;
    ctx.setFontSize(fontSize);
    ctx.setFillStyle('#FFFFFF');
    let x = 0;
    let y = fontSize + 5;
    let rowWidth = 0.0;
    let i = 0;
    for (let str of this.data.content) {
        let width = ctx.measureText(str).width; 
        rowWidth += width;
        if (rowWidth >= this.data.canvasWidth) { // 换行
            x = 0;
            i = 0;
            y += fontSize;
            rowWidth = 0.0;
        } else { // 本行
            if (i != 0) {
                x += width;
            }
        }
        ctx.fillText(str, x, y, this.data.canvasWidth);
        I++;
     }
    ctx.draw();
    

    效果如下:

    图3 --

    似乎完美解决之前的问题, 需求就要实现了, 但有时候就是比较衰, 也可能是上天帮自己, 每次输入不同文字去绘制, 突然有一次我切换为系统双拼输入法时, 不小心输入了一个英文字母a, 发现绘制不出来, 仔细一看原来是与前面一个汉字重叠了, 如下图:

    图4 --

    后面又试了一些数字、英文符号, 全部会出现重叠的情况, 这个问题至今无法解决. 最后仔细思考后, 觉得是单独绘制数字、英文字母、英文符号才会出现此问题, 于是想着如果不单独绘制应该不会出现, 但是又想如果不单独绘制那还能怎么绘制, 因为整体绘制所有文字是没办法换行的, 之前就试过了.

    经过连胜六局王者荣耀后猛的来了灵感, 为何不一行一行的绘制呢, 之前实践过的整体绘制无法换行, 如果截取一部分, 也就是一个小整体, 应该是没问题的, 但是要怎么实现呢. 苦思冥想后有了思路, 同样采用遍历的方法, 不同的是先声明一个字符串数组来表示需要绘制的行字符串(数组转字符串). 遍历整个字符串时, 计算每个字符的宽度并累加起来, 同时此字符添加到字符串数组里, 当累加后的宽度大于等于画布宽度的时候就说明要换行了, 实现如下:

    this.data.content = '今天的天气真不错aha阳光明媚,非常适合去郊游,哥儿几个233去不去,大家也好久没聚在了,非,常&难.得k的(["/机会';
    let ctx = wx.createCanvasContext('canvasId');
    // 设置画布背景色 今天的天气真不错阳光明媚,非常适合去郊游,哥儿几个去不去,大家也好久没聚在了,非常难得的机会
    ctx.setFillStyle('#95B095');
    ctx.fillRect(0, 0, this.data.canvasWidth, this.data.canvasHeight);
    // 开始绘制文字
    let fontSize = 17 * this.data.scale;
    ctx.setFontSize(fontSize);
    ctx.setFillStyle('#FFFFFF');
    let x = 0;
    let y = fontSize;
    let rowWidth = 0.0;
    let strs = [];
    for (let str of this.data.content) {
        let width = ctx.measureText(str).width;
        if ((rowWidth + width) >= this.data.canvasWidth) {
            // 需要换行了, 先把这行绘制了
            let drawStr = strs.join('');
            ctx.fillText(drawStr, x, y);
            // 这行绘制完了, 清空
            strs.length = 0;
            rowWidth = 0.0;
            // 下一行了, y坐标增加
            y += fontSize;
        }
        // 不需要换行, 不需要绘制, 字符添加到数组, 累加宽度
        strs.push(str);
        rowWidth += width;
     }
     if (strs.length > 0) {
        // 剩余的最后一行
        let drawStr = strs.join('');
        ctx.fillText(drawStr, x, y);
     }
    ctx.draw();
    

    效果很好, 请看图:

    图5 --

    看来万事大吉了, 很是欣慰, 看着绘制出的完美文字, 心里甚是高兴. 但手指也没停, 依然在不断的输入文字点击立即去绘制, 然而当在输入框里输入换行时, 绘制出来的却没有换行, 而是以空格代替. 顿时心里哇凉哇凉的, 还是不够完美, 也庆幸自己测试次数多, 及早发现问题. 其实这个换行也好解决, 遍历的时候判断字符是否是换行符, 只需要修改上述的循环内的代码即可:

    for (let str of this.data.content) {
        let width = ctx.measureText(str).width;
        // 判断有没有换行符
        let isLineFeed = false;
        if (str === '\n') {
            isLineFeed = true;
        }
        if ((rowWidth + width) >= this.data.canvasWidth || isLineFeed) {
            // 需要换行了, 先把这行绘制了
            let drawStr = strs.join('');
            ctx.fillText(drawStr, x, y);
            // 这行绘制完了, 清空
            strs.length = 0;
            rowWidth = 0.0;
            // 下一行了, y坐标增加
            y += fontSize;
        }
        // 不需要换行, 不需要绘制, 字符添加到数组, 累加宽度
        if (!isLineFeed) {
            strs.push(str);
            rowWidth += width;
        }
     }
    

    如下效果:

    图6 --

    信纸绘制

    先来看一下最终实现的效果图:

    图7 --

    要实现最终效果还需要信纸作为背景, 设计师给的背景图尺寸为1035*2745, 宽度基本足够. 但当文字特别多, 或者图片有九张的时候, 这高度肯定就不够了, 只能靠从上至下平铺多张, 至于多少张可以计算出来.

    期间还有很多很多细节比较繁琐, 花费相当多的时间去打磨, 去验证, 这里就不一一赘述. 此外, 在写这篇文章的时候, 偶然发现新的官方Canvas组件已经可以使用, 以后有机会再来研究吧.

    2020-05-31补充: 请戳这里去看源码

    相关文章

      网友评论

          本文标题:小程序之Canvas图片文字绘制长图

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