微信小程序使用canvas生成分享海报功能复盘

news2024/11/15 14:10:06

前言

近期需要开发一个微信小程序生成海报分享的功能。在h5一般都会直接采用 html2canvas 或者 dom2image 之类的库直接处理。但是由于小程序不具备传统意义的dom元素,所以也没有办法采用此类工具。
所以就只能一笔一笔的用 canvas 画出来了,下面对实现这个功能中遇到的问题做一个简单的复盘。

制作要求:

  • 主题切换。
  • 图片弹框展示,适应不同的手机尺寸。
  • 图片上层有弹出框展示保存图片按钮。
  • 海报内容,
    • 标题部分根据实际内容展示,可能为一行也可能为两行
    • 描述部分,最多展示四行,超出的显示成…
    • 圆角图片展示
    • 圆角虚线框

基本方案流程

  1. 预先加载好所有需要的图片。
  2. 在偏离视窗显示区域使用 canvas 绘制海报,并生成临时文件。
  3. 弹窗的图片使用 生成的临时图片。
  4. 设置图片的宽度为适应屏幕的,可通过定位或者flex来实现,图片高度根据宽度自动缩放。超出的内容滚动显示。

效果图如下:
在这里插入图片描述
在这里插入图片描述

微信canvas组件的相关问题

canvas 属于微信客户端创建的原生组件,所以需要注意一些原生组件的限制

  • 原生组件的层级是最高的,所以页面中的其他组件无论设置 z-index 为多少,都无法盖在原生组件上。
    • 后插入的原生组件可以覆盖之前的原生组件。
  • 原生组件还无法在 picker-view 中使用
  • 部分 CSS 样式无法应用于原生组件
    • 无法对原生组件设置 CSS 动画
    • 无法定义原生组件为 position: fixed
    • 不能在父级节点使用 overflow: hidden 来裁剪原生组件的显示区域

所以无法使用 canvas 绘制的图片直接用于显示。会遇到层级以及尺寸的问题。

预加载图片资源

在绘制之前我们需要先加载好图片资源并保存。

function create(){
    const img1 = preLoadImg("https:xxxx.img1", 'img1')
    const img2 = preLoadImg("https:xxxx.img2", 'img2')
    const img3 = preLoadImg("https:xxxx.img3", 'img3')

    Promise.all([img1, img2, img3]).then(res=>{
        // 开始绘制canvas
    })
}


function preLoadImg(url, taskId) {

    if(this.imageTempPath[taskId]) return Promise.resolve();
    
    if (!url) return Promise.resolve();

    url = /^https/.test(url) ? url : `https:${url}`;

    return wx.getImageInfo({src: url}).then((res)=>{
        this.imageTempPath[taskId] = res.path;
    })
}

文本处理

计算不同长度的文本绘制高度

对于不同的文本长度,可能存在占一行或者多行的情况,这个时候对于文本以下的内容绘制的 y 轴坐标会造成影响。

解决方案:先定义好每一个元素在标准情况下的坐标位置,然后对于存在可能有占据空间改变的文本,通过测量其文本宽度,计算出实际占据行数,然后出多出的 y 轴位置(diff),并在后续的元素绘制上加上这个差值。

基本思路:

  1. 测量出文本的实际绘制需要的总长度
  2. 计算出实际绘制多少行
  3. 计算实际绘制行数与默认行数的高度差

计算方法如下:

function getWordDiffDistance(
    ctx,        // canvas 上下文
    text,       // 要计算的文本
    baseline,   // 默认显示行数
    lineHeight, // 行高
    fontSize,   // 字号
    textIndent, // 首行缩进字符
    maxWidth,   // 每一行绘制的最大宽度
    maxLine     // 最大允许显示行数
) {
    // 设置上下文的字号
    ctx.setFontSize(fontSize);

    // 首行缩进的宽度
    const textIndentWidth = fontSize * textIndent;
     //实际总共能分多少行
    let allRow = Math.ceil((ctx.measureText(text).width + textIndentWidth) / maxWidth);

    allRow = Math.min(allRow, maxLine);

    return (allRow - baseline) * lineHeight;
}

ctx.measureText() 要先设置好文本属性。

文本超出指定行数后显示 …

基本思路:

  1. 设置好 canvas 上下文的文字样式
  2. 通过 measureText 计算出当前文本需要绘制多少行
  3. 如果是首行且设置了首行缩进,绘制的 x 要加上缩进的宽度
  4. 然后计算出每一行要绘制的文字并进行绘制,并记录最后的截取位置
  5. 如果最后一行的实际绘制宽度大于设置的最大宽度,添加… 否则正常绘制
dealWords(options) {
    const {
        ctx,
        fontSize,
        word,
        maxWidth,
        x,
        y,
        maxLine,
        lineHeight,
        style,
        textIndent = 0,
    } = options;
    ctx.font = style || "normal 12px PingFangSC-Regular";

     //设置字体大小
    ctx.setFontSize(fontSize);

     // 首行缩进的宽度
    const textIndentWidth = fontSize * textIndent;

     //实际总共能分多少行
    let allRow = Math.ceil((ctx.measureText(word).width + textIndentWidth) / maxWidth);

     //实际能分多少行与设置的最大显示行数比,谁小就用谁做循环次数
    let count = allRow >= maxLine ? maxLine : allRow;

     //当前字符串的截断点
    let endPos = 0;
    for (let j = 0; j < count; j++) {
        let startWidth = 0;
        if (j == 0 && textIndent) startWidth = textIndentWidth;
        let rowRealMaxWidth = maxWidth - startWidth;

         //当前剩余的字符串
        let nowStr = word.slice(endPos);

         //每一行当前宽度
        let rowWid = 0;
        if (ctx.measureText(nowStr).width > rowRealMaxWidth) {
            //如果当前的字符串宽度大于最大宽度,然后开始截取
            for (let m = 0; m < nowStr.length; m++) {
                 //当前字符串总宽度
                rowWid += ctx.measureText(nowStr[m]).width;
                if (rowWid > rowRealMaxWidth) {
                    if (j === maxLine - 1) {
                    //如果是最后一行
                    ctx.fillText(
                        nowStr.slice(0, m - 1) + "...",
                        x + startWidth,
                        y + (j + 1) * lineHeight
                    ); //(j+1)*18这是每一行的高度
                    } else {
                    ctx.fillText(
                        nowStr.slice(0, m),
                        x + startWidth,
                        y + (j + 1) * lineHeight
                    );
                    }
                    endPos += m; //下次截断点
                    break;
                }
            }
        } else {
            //如果当前的字符串宽度小于最大宽度就直接输出
            ctx.fillText(nowStr.slice(0), x, y + (j + 1) * lineHeight);
        }
    }
}

绘制多行文本计算行宽的时候,空白字符可能会对最终的计算结果造成一定影响,所以可以先对其空白字符进行过滤。

图文对齐

微信小程序中通过 setTextBaseline 设置文本竖直对齐方式。可选值有 top,bottom,middle,normal;

在这里插入图片描述

图片的坐标基点为左上角坐标,所以在绘制的时候要注意 y 的起始坐标。如果有修改 文本的对齐方式,在结束的时候最好将文本竖直对齐方式设置为 normal,避免影响后续的绘制。

形状处理

绘制圆角矩形路径

使用arc()方式绘制弧线
在这里插入图片描述

// 按照canvas的弧度从 0 - 2PI 开始顺时针绘制
function drawRoundRectPathWithArc(ctx, x, y, width, height, radius) {
    ctx.beginPath();
    // 从右下角顺时针绘制,弧度从0到1/2PI
    ctx.arc(x + width - radius, y + height - radius, radius, 0, Math.PI / 2);

    // 矩形下边线
    ctx.lineTo(x + radius, y + height);

    // 左下角圆弧,弧度从1/2PI到PI
    ctx.arc(x + radius, y + height - radius, radius, Math.PI / 2, Math.PI);

    // 矩形左边线
    ctx.lineTo(x, y + radius);

    // 左上角圆弧,弧度从PI到3/2PI
    ctx.arc(x + radius, y + radius, radius, Math.PI, (Math.PI * 3) / 2);

    // 上边线
    ctx.lineTo(x + width - radius, y);

    //右上角圆弧
    ctx.arc(x + width - radius,y + radius, radius, (Math.PI * 3) / 2, Math.PI * 2);

    //右边线
    ctx.lineTo(x + width, y + height - radius);
    ctx.closePath();
}

使用arcTo()方式绘制弧线

function drawRoundRectPathWithArcTo(ctx, x, y, width, height, radius) {
    ctx.beginPath();

    // 上边线
    ctx.lineTo(x + width - radius, y);

    // 右上弧线
    ctx.arcTo(x + width, y, x + width, y + radius, radius)

    //右边线
    ctx.lineTo(x + width, y + height - radius);


    // 从右下角顺时针绘制,弧度从0到1/2PI
    ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius)

    // 矩形下边线
    ctx.lineTo(x + radius, y + height);

    // 左下角圆弧,弧度从1/2PI到PI
    ctx.arcTo(x, y + height, x, y +height -radius, radius)

    // 矩形左边线
    ctx.lineTo(x, y + radius);

    // 左上角圆弧,弧度从PI到3/2PI
    ctx.arcTo(x,y, x+ radius, y, radius)

    
    ctx.closePath();
}

背景色填充

function fillRoundRectPath(ctx, x, y, width, height, radius, color){
    ctx.save();
    this.drawRoundRectPathWithArc(ctx, x, y, width, height, radius);
    ctx.setFillStyle(color);
    ctx.fill();
    ctx.restore();
}

图片填充

function drawRoundRectImg(ctx, x, y, width, height, radius, img) {
    if(!img) return
    ctx.save();
    this.drawRoundRectPathWithArc(ctx, x, y, width, height, radius);
    // 剪切  原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
    ctx.clip();
    ctx.drawImage(img, x, y, width, height);
    ctx.restore();
}

虚线框

function strokeRoundRectPath(ctx, x, y, width, height, radius) {
    this.drawRoundRectPathWithArc(ctx, x, y, width, height, radius);
    ctx.strokeStyle = "#DDDDDD";
    ctx.lineWidth = 0.5;
    ctx.setLineDash([6, 5]);
    ctx.stroke();
}

生成临时图片

wx.canvasToTempFilePath(Object object, Object this)

把当前画布指定区域的内容导出生成指定大小的图片。在 draw() 回调里调用该方法才能保证图片导出成功。

ctx.draw(false, async () => {
    // canvas画布转成图片并返回图片地址
    const { tempFilePath } = await wx.canvasToTempFilePath(
        {
            x: 0,       // 指定的画布区域的左上角横坐标	
            y: 0,       // 指定的画布区域的左上角纵坐标
            width: posterImg_width,     // 指定的画布区域的宽度	
            height: posterImg_height,   // 指定的画布区域的高度
            destWidth: posterImg_width * pixelRatio, // 输出的图片的宽度 导出大小为 canvas 的 pixelRatio 倍
            destHeight: posterImg_height * pixelRatio, // 输出的图片的高度 
            canvasId: "posterCanvas",
        },
        this
    );
    this.posterTempFilePath = tempFilePath;
});

不同像素手机的显示适配问题

由于只是一张图片的展示,所以显示适配的问题久很好解决。

  • 设置图片父层容器的侧边距,使容器自动撑开。
  • 图片宽度设置为 width:100%, 设置 mode="widthFix"让图片自动缩放。

微信本地保存临时图片

function savePoster(tempFilePath) {
    wx.saveImageToPhotosAlbum({
        filePath: tempFilePath,
    }).then(()=> {
        wx.showToast({
            title: "保存成功", // 提示的内容,
            icon: "success", // 图标,
            duration: 2000, // 延迟时间,
            mask: true, // 显示透明蒙层,防止触摸穿透,
        });
    },
    (err) => {
        wx.showToast({
            title: "保存失败", // 提示的内容,
            icon: "none", // 图标,
            duration: 2000, // 延迟时间,
            mask: true, // 显示透明蒙层,防止触摸穿透,
        });
    },);
}

主题切换

通过替换不同的背景图片来切换不同的主题。

参考文章

说说如何使用 Canvas 绘制弧线与曲线

canvas生成分享海报

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/187548.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(文末有彩蛋,不看白不看)兑现一下之前答应读者的事

大家过年好呀&#xff01;&#xff08;我&#xff09;明天就要开工上班啦&#xff01;在假期的最后一天踢了会球&#xff0c;简单吃点东西&#xff0c;晚上来兑现答应一位读者的事情。开整&#xff01; 前情提要 这篇文章缘起于「开发者」技术交流群中一位读者遇到的问题&…

《树上的男爵》坚持解释不清的理想是疏离,还是自由?

《树上的男爵》坚持解释不清的理想是疏离&#xff0c;还是自由&#xff1f; 伊塔洛卡尔维诺&#xff0c;意大利作家&#xff0c;后现代主义派。 卡尔维诺为“寓言式奇幻文学的大师”。&#xff08;评论家赫伯特密特甘评&#xff09; 吴正仪 译 许多年来&#xff0c;我为一些连我…

【3】SpringBoot基础

//从事微服务开发工作 SpringBoot提供一种快速使用Spring的方式 1、自动化 2、设置多个starter配置依赖比Maven直接管理更便捷 3、内置服务器 总结&#xff1a;自动配置&#xff0c;起步依赖&#xff0c;辅助功能 2.6.11版本的说明书 Spring Boot Reference Documentation…

第九层(8):STL之set/multiset

文章目录前情回顾set/multiset概念区别构造函数赋值函数大小操作函数交换函数插入函数删除函数查找函数统计函数为什么set不可以插入重复数据pair数组pair数组创建怎么样去改变set容器的排序规则下一座石碑&#x1f389;welcome&#x1f389; ✒️博主介绍&#xff1a;一名大一…

智公网:一级消防师要掌握的知识点!

一、建筑物的耐火等级 1、建筑物的耐火等级分为四级&#xff0c;一、二、三、四级。 2、节点缝隙或金属承重构件节点的外露部位&#xff0c;应做防火保护层。 3、民用建筑的耐火等级、层数、长度和面积&#xff0c;一二级最大防火分区的长度250m。多层建筑最大允许建筑面积2…

【gcc/g++/gdb/cmake】命令

文章目录参考资料一、gcc/g/make1 gcc和g的区别2 gcc/g与make区别3 make与cmake区别二、开发环境搭建1 编译器&#xff0c;调试器&#xff0c;CMake安装2 GCC编译器2.1 编译过程2.1.1 -E 预处理-Pre-Processing .i文件2.1.2 -S 编译-Compiling .s文件2.1.3 -C 汇编-Assembling …

软件工程(二)——需求工程、统一建模语言UML

目录 一、需求获取 二、需求分析 1.结构化需求分析 2.面向对象分析方法OOA 三、UML 41视图 四、UML 图 (1)用例图 (2)类图、对象图 (3)顺序图 &#xff08;4&#xff09;活动图 &#xff08;5&#xff09;状态图 &#xff08;6&#xff09;通信图 软件需求指用户对系…

【科研】ET-BERT代码分析

0. 数据集 论文使用的TLS1.3数据集是从 2021 年 3 月到 2021 年 7 月在中国科技网 (CSTNET) 上收集的。 如果要使用自己的数据集的话&#xff0c;需要检查数据格式是否与datasets/cstnet-tls1.3/目录下一致&#xff0c;并且在data_process/目录下指定数据集路径。 用于微调的…

Pomotroid 使用指南:一款高颜值 PC 端番茄时钟

文章首发于个人公众号&#xff1a;「阿拉平平」 番茄工作法作为一套高效易行的时间管理方案&#xff0c;是由意大利人弗朗西斯科西里洛于 1992 年创立的。作者发明的初衷源自于自身严重的拖延症&#xff0c;于是他找来一个厨房用的番茄计时器&#xff0c;调到 10 分钟&#xff…

游戏SDK(二)框架设计

前言 根据上一篇游戏SDK&#xff08;一&#xff09; 客户端整体架构&#xff0c;介绍了游戏SDK 及 游戏SDK的需求分析。根据需求分析&#xff0c;对游戏SDK的设计分为3大块&#xff1a; 客户端&#xff1a;接口统一&#xff0c;做好逻辑转发和处理。具体渠道具体实现&#xf…

【高并发】- 生产级系统搭建 - 4

前言 关于高并发系统中&#xff0c;当前比较热门的还是属于“秒杀”系统&#xff0c;前面章节在整理了“秒杀”系统的相关设计概念后&#xff0c;本章节&#xff0c;来讲解扣减库存相关的业务逻辑。 1 库存的那些事 一般电商网站中&#xff0c;购买流程一般都是这样的&#xff…

多线程之Callable接口、ReentrantLock、信号量 Semaphore以及CountDownLatch

目录&#xff1a;一、Callable接口Callable的用法小结二、ReentrantLockReentrantLock 的用法ReentrantLock 和 synchronized 的区别&#xff1f;为什么有了 synchronized 还需要 juc(java.util.concurrent) 下的 lock&#xff1f;三、信号量 Semaphore如何理解信号量&#xff…

Python接口测试实战4(下) - 框架完善:用例基类,用例标签,重新运行上次失败用例

本节内容使用用例基类自定义TestSuitecollect-only的实现testlist的实现用例tags的实现rerun-fails的实现命令行参数的使用更简单的用例编写使用用例基类因为每条用例都需要从excel中读取数据&#xff0c;解析数据&#xff0c;发送请求&#xff0c;断言响应结果&#xff0c;我们…

阿里云ECS学习笔记1

ECS&#xff1a;弹性计算服务。CPU可以热插、内存可变大变小、硬盘可以增加~ 注册&#xff1a; 在企业中&#xff0c;应该以企业的身份进行注册&#xff0c;而不是以个人身份进行注册。 手机号&#xff1a;行政部门专门管理注册的账号资源的&#xff0c;而不使用个人或者老板…

【可解释性机器学习】解释基于XGBoost对泰坦尼克号数据集的预测过程和结果

解释基于XGBoost对泰坦尼克号数据集的预测过程和结果1. 训练数据2. 简单的 XGBoost 分类器3. 解释重量4. 解释预测5. 添加文本特性参考资料本文介绍如何分析XGBoost分类器的预测&#xff08; eli5也支持 XGBoost和大多数 scikit-learn树集成的回归&#xff09;。 我们将使用 Ti…

【数据结构】8.5 归并排序

文章目录相邻两个有序子序列的归并归并排序算法归并排序算法分析基本思想 将两个或两个以上的有序子序列归并为一个有序序列。在内部排序中&#xff0c;通常采用的是2-路归并排序。 即&#xff1a;将两个位置相邻的有序子序列 R[l…m] 和 R[m1…n] 归并为一个有序序列 R[l…n]…

1个寒假能学会多少网络安全技能?

现在可以看到很多标题都声称三个月内就可以转行网络安全领域&#xff0c;并且成为月入15K的网络工程师。那么&#xff0c;这个寒假的时间能学多少网络安全知识&#xff1f;是否能入门网络安全工程师呢&#xff1f; 答案是肯定的。 虽然网络完全知识是一门广泛的学科&#xff…

在线支付系列【9】微信支付之申请微信公众号

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 文章目录前言申请微信公众号前言 由于微信支付的产品体系全部搭载于微信的社交体系之上&#xff0c;所以直连商户或服务商接入微信支付之前&#xff0c;都需要有一个微信社交载体&#xff0c;该载体对应…

天啦撸~ChatGPT通过国际软件测试工程师(ISTQB)认证了~

天啦撸&#xff01;目前最火的AI应用ChatGPT通过ISTQB认证了~ 近期&#xff0c;国外的一位工程师&#xff0c;放出了他用ChatGPT通过认证的相关信息。 ChatGPT相信大家都知道是什么了&#xff0c;ISTQB相信很多测试小伙伴也不陌生&#xff0c;而且很多考证的小伙伴也对此梦寐以…

Linux之网络性能测试工具netperf实践

一、netperf简介 Netperf是一种网络性能的测量工具&#xff0c;主要针对基于TCP或UDP的传输。Netperf根据应用的不同&#xff0c;可以进行不同模式的网络性能测试&#xff0c;即批量数据传输&#xff08;bulk data transfer&#xff09;模式和请求/应答&#xff08;request/rep…