实现一个小程序分享图 wxml2canvas

news2024/11/20 11:36:49

我们经常会遇上动态生成海报的需求,而在小程序中,生成图片非Canvas莫属。但是在实际工作当中,为了追求效率,我们会不可避免地去使用一些JS插件,而 wxml-to-canvas 就是一款官方推荐且非常优秀的插件,它可以轻松地帮你将HTML代码转换成Canvas,进而生成可保存分享的图片。

但是wxml-to-canvas是通过静态模板和样式绘制 canvas ,进而导出图片,需要单独写一份静态模板用于编译,对于很多场景还是有些限制,比如有时需要将图文混排的富文本内容生成分享图,对于这种长度不定,内容动态变化的图片生成需求,直接利用官方的canvas接口绘制是十分困难的,包括但不限于文字换行、表情文字图片混排、文字加粗、子标题等元素都需要一一绘制

我们的目标是实现一个通过wxml节点标记,收集元素从而进行编译转换,仅依赖wxml直出需要绘制的canvas进而快速实现图片分享,为此学习了 wxml2canvas npm包,实现了基础功能的wxml2canvas

  • 此文暂不讨论图片类型的wxmlcanvas

先看结果🌰

我们针对简单的demo进行处理,包含了对块级元素行内元素背景色、等简单样式的转换,达到 wxml->canvas->image 的一次性处理,且不需要重复书写静态代码模板进行编译

下面让我们一步步探求如何实现这个tiny版的wxml2canvas

实现

小程序提供了如下特性,可供我们便捷使用:

  • measureText接口能直接测量出文本的宽度;
  • SelectorQuery可以查询到节点对应的computedStyle。

同时小程序也存在一些弊端,比如:

  • canvas属于原生组件,在移动端会置于最顶层;
  • 通过SelectorQuery只能拿到节点的style,而无法获取文本节点的内容

所以我们第一步获取元素就面临两个问题:

1.如何获取需要转成canvas的元素
2.如何拿到获取元素的对应属性(样式、节点、内容…)

当获取到待收集的元素后,就可以将元素绘制到指定的canvas上,也就实现了wxml2canvas

所以初始化时,需要传入如下参数

/**
 * element:需要渲染的canvas节点
 * class:查找所有类名为exc-c的节点,并进行加入绘制队列
 * limit:限定相对位置 
 *
 **/
wxml2Canvas({element: "over-canvas",options: {class: ".exc-c",limit: ".limit-r",},
}) 

在wxml中需要绘制的元素需添加 exc-c 类名,方便对元素进行查找,并且如果需要限定相对位置,也需要在父级添加 limit-r 类名,例如,一个文本的位置(left, top) = (50, 80),class为panel的节点的位置为(left, top) = (20, 40),则文本canvas上实际绘制的位置(x, y) = (50 - 20, 80 -40) = (30, 40)。如果不传入limit,则以实际的位置(x, y) = (50, 80) 绘制

对于 wxml 而言,如有需要渲染的文本,也需要将文本内容通过 data-text="xxx" 属性的方式进行挂载,因为 SelectorQuery 无法获取文本节点的内容但是可以获取到节点的 dataset 属性,从而拿到文本内容

wxml 代码如下:

 <view class="content">Wxml:</view><view class="-box limit-r"><view class="-line exc-c" data-type="inline-text" data-text="这是{{txt}}">这是{{txt}}</view><view class="-line -blue exc-c" data-type="inline-text" data-text="这是蓝色行内文字试试">这是蓝色行内文字试试</view><view class="-line exc-c" data-type="inline-text" data-text="这是换行的行内行内行内行内行内行内行内行内行内行行内行内内文字">这是换行的行内行内行内行内行内行行内行内内行内行内行内行内文字</view><view class="-line -red exc-c" data-type="inline-text" data-text="这是红色行内红色行内红色行内红色行内文字试试">这是红色行内红色行内红色行内红色行内文字试试</view><view class="-il exc-c" data-type="text" data-text="这是无边距文字">这是无边距文字</view><view class="-il-2 exc-c" data-type="text" data-text="这是margin文字">这是margin文字</view><view class="-il-3 exc-c" data-type="text" data-text="这是padding文字">这是padding文字</view><view class="-il-4 exc-c" data-type="text" data-text="这是居中有背景文字">这是居中有背景文字</view><view class="-il-5 exc-c" data-type="text" data-text="这是背景文字" data-background="rgba(255, 0, 0, 0.4)" data-padding="0 0 0 0">这是背景文字</view></view><view class="content">Canvas:</view><canvas canvas-id="over-canvas" class="-box"></canvas> 

上面是需要配置的代码,那下面让我看看如何具体实现:

1.配置基础的样式 如:字体大小、颜色、边距,并返回一个函数用来追加配置
2.解析元素 拿到所有需要绘制的元素以及相对的父级元素
3.按照是否涉及换行进行分类绘制 (块级元素 / 行内元素)
4.全部绘制结束时进行 resolve

const Wxml2Canvas = config => {return new Promise(async (resolve, reject) => {const appendSet = setInit(config); // 配置基础const [render, limit] = await getWxml(config.options); // 获取待绘制元素、限制区域const [block, inlineTmp] = sortListByTop(render); // 划分块级元素 \ 行内元素appendSet({ limit }); // 追加配置Promise.all([...drawB(block), drawL(inlineTmp)]).then(resolve).catch(reject);});
}; 

初始化

在上面代码中,通过 setInit 设置 canvas 的基本属性,并将可配置的默认参数(宽度、字体大小、字体颜色…)以及 ctx 维护成一个对外暴露的公共对象,最后返回一个函数,供使用者更新配置

const DEFAULT_CONFIG = {width: 340,TOP: "top",FONT_SIZE: "14",PADDING: "0 0 0 0",FONT_COL: "#454545",SHADOW_COL: "#ffffff",background: "#ffffff",font: "14px PingFang SC",STROKECOLOR: "white",
};

export const CACHE_INFO = {};

export const setInit = (config = {}) => {const info = { ...DEFAULT_CONFIG, ...config };const { background, font, width, height, TOP, STROKECOLOR } = info;CACHE_INFO.options = info;CACHE_INFO.ctx = wx.createCanvasContext(info.element, info._this);CACHE_INFO.ctx.font = font;CACHE_INFO.ctx.setTextBaseline(TOP);CACHE_INFO.ctx.setStrokeStyle(STROKECOLOR); // 设置基本样式drawRectToCanvas(0, 0, width, height, { fill: background });return arg => Object.keys(arg).forEach(i => (CACHE_INFO.options[i] = arg[i]));
}; 

获取元素

getWxml 中,通过小程序提供的 createSelectorQuery 获取到需要绘制元素的节点信息,同时也获取父级相对元素的节点信息,作为 limit 界定绘制元素的边界

/**
 * @param { object } item 待处理的节点
 * @returns array 解析后的节点信息
 */
export const getWxml = item => {const { options } = CACHE_INFO;const { _this, width } = options;const query = _this? wx.createSelectorQuery().in(_this): wx.createSelectorQuery();const render = new Promise(resolve => {query.selectAll(item.class).fields({dataset: true,size: true,rect: true,computedStyle: COMPUT_STYLE,},res => resolve(res)).exec();});const limit = new Promise(resolve => {if (!item.limit) resolve({ top: 0, width });query.select(item.limit).fields({dataset: true,size: true,rect: true,},res => resolve(res)).exec();});return Promise.all([render, limit]);
}; 

其中 COMPUT_STYLE 规定了需要查找的元素样式名称:

export const COMPUT_STYLE = ["width","height","font","fontSize","fontFamily","fontWeight","fontStyle","textAlign","color","lineHeight","border","borderColor","borderStyle","borderWidth","verticalAlign","boxShadow","background","backgroundColor","backgroundImage","backgroundPosition","backgroundSize","paddingLeft","paddingTop","paddingRight","paddingBottom",
]; 

获取到相对父级元素,将获取的节点作为边界信息更新到配置中

appendSet({ limit }); // 追加配置 

获取到的元素可以分为两类 即:

  • 涉及文字换行的元素 (行内元素)
  • 不涉及文字换行的元素 (块级元素)

我们可以通过在书写 wxml 时,通过事先声明 data-type="inline-text" ,这样获取到节点之后,可以通过 dataset 获取元素的类型,如果为行内元素时,需要将行内元素通过高度进行分层,将同一行的元素进行归类

/**
 * @param { array } list 待处理的节点
 * @returns array
 */
export const sortListByTop = list => {const [arrBlock, arrLine, lineTemp] = [[], [], {}];list.forEach(i => {if (i.dataset.type && i.dataset.type.indexOf("inline") == -1) {arrBlock.push(i);} else {arrLine.push(i);}});arrLine.forEach(i => {lineTemp[i.top] = lineTemp[i.top] || [];lineTemp[i.top].push(i);});return [arrBlock, lineTemp];
}; 

处理元素

通过 drawWxmlBlockdrawWxmlInline 分别对元素进行处理和绘制,在此之前还需要 drawAfter 方法将节点信息(文字、位置…) + 元素的样式信息(padding、width…) 转化为对应的 canvas 位置信息:

drawAfter:

/**
 * 返回节点的真实位置
 *
 * @param { object } el 需要渲染的节点
 * @param {*} leftOffset 从左侧开始绘制的起点
 * @param {*} maxWidth 一行文本的最大宽度
 * @returns 返回canvas位置
 */
const drawAfter = (el, leftOffset, maxWidth) => {const { options } = CACHE_INFO;const { left: limitLeft = 0, top: limitTop = 0 } = options.limit;const leftFix = +el.dataset.left || 0;const topFix = +el.dataset.top || 0;el.width = transferNum(el.width);el.height = transferNum(el.height);el.left = transferNum(el.left) - limitLeft + leftFix;el.top = transferNum(el.top) - limitTop + topFix;let padding = el.dataset.padding || options.PADDING;if (typeof padding === "string") {padding = transferPadding(padding);}const paddingTop = +el.paddingTop.replace("px", "") + +padding[0];const paddingRight = +el.paddingRight.replace("px", "") + +padding[1];const paddingBottom = +el.paddingBottom.replace("px", "") + +padding[2];const paddingLeft = +el.paddingLeft.replace("px", "") + +padding[3];el.padding = [paddingTop, paddingRight, paddingBottom, paddingLeft];const text = el.dataset.text || "";el.background = el.dataset.background || el.backgroundColor;return {text,x: leftOffset || el.left,y: el.top,originX: el.left,...(leftOffset && { leftOffset }),...(maxWidth && { maxWidth }),};
}; 

drawWxmlBlock:

对于块级元素,将转为 canvas 位置信息的节点依次绘制,通过 Promise 将绘制完成的结果进行返回

/**
 * 绘制块级元素
 * @param { array } block 需要绘制的块级元素
 */
export const drawWxmlBlock = (block = []) => {return block.map(el =>new Promise((resolve, reject) => {const textData = drawAfter(el);drawText(textData, el, "text", resolve, reject,);}));
}; 

drawWxmlInline:

对于行内元素,首先通过同一个行元素的左右边距计算出一行的最大宽度,用于换行,并且记录距离左侧的边距 leftOffset ,每次绘制完更新下一次绘制的起点,从上次结束的位置继续绘制,全部绘制完成后进行 resolve (有些贪心算法的影子)

/**
 * 绘制行内元素
 * @param { object } inline 需要绘制的行内元素
 */
export const drawWxmlInline = (inline = {}) => {let leftOffset = 0;return new Promise(resolve => {let maxWidth = 0;let minLeft = Infinity;let maxRight = -Infinity;Object.keys(inline).forEach(top => {inline[top].forEach(el => {minLeft = Math.min(el.left, minLeft);maxRight = Math.max(el.right, maxRight);});});// 找出同一top下的最小left和最大right,得到最大的宽度,用于换行maxWidth = Math.ceil(maxRight - minLeft);Object.keys(inline).forEach(top => {inline[top].forEach(el => {const textData = drawAfter(el, leftfOfset, maxWidth);const drawRes = drawText(textData, el, "inline");leftOffset = drawRes.leftOffset; // 每次绘制从上次结束地方开始});});resolve();});
}; 

渲染节点

无论是块级元素还是行内元素都是通过 drawText 方法最终渲染节点到指定的 canvas 中,只不过传参会发生变化,块级元素 每次绘制都是独立的,所以每次绘制成功之后会触发 resolve 的回调,但是行内元素,每次绘制需要返回当前绘制结束之后的定位,以此作为下次绘制的起始位置。

块级元素:

/**
 * 绘制文字 
 * @param { object } textData 节点位置
 * @param { object } el 节点信息
 * @param { string } type 渲染节点类型
 * @param { function } resolve 成功时候抛出
 * @param { function } reject 失败时候抛出
 */
const DrawTxt = (textData, el, type, resolve, reject) => {const { ctx, options } = CACHE_INFO;try {...[x, y] = setTxtAlign(textData, el, textWidth, width);ctx.fillText(textData.text, x, y);ctx.draw(true);resolve();} catch (e) {reject && reject(e);}
}; 

以上对于块级元素时,我们只需要结合 textAlignpadding 就可以计算出 x 的值,通过 行数 * 行高padding 计算出 y 的值,最后通过 ctx.fillText 进行绘制

行内元素:

/**
 * 绘制文字 
 * @param { object } textData 节点位置
 * @param { object } el 节点信息
 */
const DrawTxt = (textData, el) => {const { ctx, options } = CACHE_INFO;...// 元素换行的情况:const maxw = textData.maxWidth; // 最大宽度let lineNum = Math.max(Math.floor(textWidth / maxw), 1); // 最大行数const singleLength = Math.floor(text.length / lineNum); // 每行字数const widthOffset = textData.leftOffset - textData.originX; // 计算真实的左边距let [endIdx, fSingle, fsWidth] = getTextSingleLine(text, maxw, singleLength, 0, widthOffset);[x, y] = setTxtAlign(textData, el, fsWidth);ctx.fillText(fSingle, x, y); // 绘制leftOffset = x + fsWidth; // 更新左边距topOffset = y; // 更新右边距...ctx.draw(true);return { leftOffset, topOffset }
}; 

上面行内元素,需要关心的是 getTextSingleLine 方法,即计算截取换行文字的索引和位置,通过 offset 矫正每行的真实文本数,如果 真实文本距离 + 左边距 > 一行最大长度 则不断进行截取,并且更新一行的文本字数,直到不超过一行最大长度时,返回截取的文字索引、截取的文字、截取后的文本长度

getTextSingleLine:

/**
 * 当文本超过宽度时,计算每一行应该绘制的文本
 *
 * @param {*} text 需要绘制的文字
 * @param {*} width 一行最大长度
 * @param {*} singleLength 每行实际字数
 * @param {*} currentIndex 文字的起始位置索引
 * @param {*} widthOffset 左边距
 */
export const getTextSingleLine = ( text,width,singleLength,currentIndex = 0,widthOffset = 0 ) => {let offset = 0;let endIndex = currentIndex + singleLength + offset;let single = text.substring(currentIndex, endIndex);let singleWidth = measureWidth(single);while (Math.round(widthOffset + singleWidth) > width) {offset -= 1;endIndex = currentIndex + singleLength + offset;single = text.substring(currentIndex, endIndex);singleWidth = measureWidth(single);}return [endIndex, single, singleWidth];
}; 

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

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

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

相关文章

图文详解Ansible中的变量及加密

文章目录一、变量命名二、变量级别三、.变量设定和使用方式1.在playbook中直接定义变量2.在文件中定义变量3.使用变量4.设定主机变量和清单变量5.目录设定变量6.用命令覆盖变量7.使用数组设定变量8.注册变量9.事实变量10.魔法变量四、JINJA2模板五、 Ansible的加密控制练习1.用…

I2C总线应用测试程序

参考链接&#xff1a;I2c协议 Linux I2C应用编程开发 问题背景 在工作中需要测试I2C总线的传输稳定性&#xff0c;需写一个测试程序通过读写从设备寄存器的值来验证数据传输稳定性。 站在cpu的角度来看&#xff0c;操作I2C外设实际上就是通过控制cpu中挂载该I2C外设的I2C控制…

yunUI组件库解析:图片上传与排序组件yImgPro

yunUI是笔者开源的微信小程序功能库。目前其中包含了一些复杂的功能组件。方便使用。未来它将分为组件、样式、js三者合为一体&#xff0c;但分别提供。 本文所用代码皆来源于组件库中的yImgPro组件。详细代码可至github查看。地址&#xff1a; yunUI 。 npm地址&#xff1a;yu…

Bing+ChatGPT 对传统搜索引擎的降维打击

早些时候申请了新版 Bing 的内测资格&#xff0c;终于收到了通过的邮件。 一天的体验之后&#xff0c;我的感受是&#xff1a;当新版 Bing 具备了 ChatGPT 的聊天能力之后&#xff0c;它的能力不论是对传统搜索引擎&#xff0c;还是 ChatGPT 自身&#xff0c;都将是降维打击。 …

LeetCode 237. 删除链表中的节点

原题链接 难度&#xff1a;middle\color{orange}{middle}middle 题目描述 有一个单链表的 headheadhead&#xff0c;我们想删除它其中的一个节点 nodenodenode。 给你一个需要删除的节点 nodenodenode 。你将 无法访问 第一个节点 headheadhead。 链表的所有值都是 唯一的&…

IoT 边缘集群基于 Kubernetes Events 的告警通知实现(二):进一步配置

上一篇文章 IoT 边缘集群基于 Kubernetes Events 的告警通知实现 目标 告警恢复通知 - 经过评估无法实现原因: 告警和恢复是单独完全不相关的事件, 告警是 Warning 级别, 恢复是 Normal 级别, 要开启恢复, 就会导致所有 Normal Events 都会被发送, 这个数量是很恐怖的; 而且…

【重排重绘】从输入url到浏览器展示页面发生了什么?

目录步骤如下&#xff1a;一、用户在浏览器搜索栏中输入url地址二、浏览器解析域名得到服务器ip地址浏览器解析域名得到服务器ip地址有哪些过程&#xff1f;三、TCP三次握手建立客户端和服务器的连接四、客户端发送HTTP请求获取服务器端的静态资源五、服务器发送HTTP响应报文给…

程序员深度体验一周ChatGPT发现竟然....

程序员深度体验一周ChatGPT发现竟然… 周一打卡上班&#xff0c;老板凑到我跟前&#xff1a;“小李啊&#xff0c;这周有个新需求交给你做一下&#xff0c;给我们的API管理平台新增一个智能Mock的功能…”。我条件反射般的差点脱口而出&#xff1a;“这个需求做不了…”。不过…

【软件测试】资深测试总结的几个自动化测试点,提升跨越一大步......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 自动化的软件测试与…

PostgreSQL查询引擎——SELECT STATEMENTS SelectStmt

SelectStmt: select_no_parens %prec UMINUS| select_with_parens %prec UMINUS select_with_parens:( select_no_parens ) { $$ $2; }| ( select_with_parens ) { $$ $2; } 该规则返回单个SelectStmt节点或它们的树&#xff0c;表示集合操作树(set-operation tree…

JAVA线程池的使用

一、池化思想和JAVA线程池 池化是很重要的思想&#xff1b;池化的好处是提供缓冲和统一的管理。这个笔者在本人的数据库连接池的博客中已经提到过了&#xff08;JAVA常用数据库连接池_王者之路001的博客-CSDN博客 &#xff09;。 线程池是另一种池化思想的运用&#xff0c;把…

MySQL 派生表产生关联索引auto_key0导致SQL非常的慢

相同的SQL在maridb运行0.5秒&#xff0c;在MySQL8.0.26中运行要19秒 官方MySQL在处理子查时&#xff0c;优化器有个优化参数derived_merge&#xff0c;MySQL7开启添加&#xff0c;默认on.很多情况可以自动优化派生表&#xff0c;避免创建临时索引auto_key0和生成临时表数据做…

C++入门:函数重载

目录 一. 函数重载的概念和分类 1.1 什么是函数重载 1.2 函数重载的分类 1.3 关于函数重载的几点注意事项 二. C实现函数重载的底层逻辑&#xff08;为什么C可以实现函数重载而C语言不能&#xff09; 2.1 编译器编译程序的过程 2.2 为什么C可以实现函数重载而C语言不能 …

内网安装管家婆软件如何实现外网访问?内网穿透的几种方案教程

管家婆软件从网络架构上分两种版本&#xff1a;web&#xff08;浏览器http端口&#xff09;访问的版本和客户端&#xff08;211固定端口sqlserver数据库&#xff09;访问的版本。公司库管经常用仓库登录管家婆&#xff0c;一旦需要在公司外部登陆访问管家婆客户端&#xff0c;就…

微信中如何接入机器人才比较正常

大家好,我是雄雄,欢迎关注微信公众号:雄雄的小课堂。 前言 为什么会有这个话题?大家都知道最近有个AI机器人很火,那就是AI机器人,关于它的介绍,大家可以自行百度去,我这边就不多介绍了。 好多人嫌网页版玩的不过瘾,就把这个机器人接入到了QQ上,接入到了钉钉上,TG …

Go语言基础知识学习笔记

环境准备 下载安装Golang&#xff1a;https://golang.google.cn/dl/ 因为国外下载速度较慢&#xff0c;我们需要配置国内代理 # 开启包管理工具 go env -w GO111MODULEon # 设置代理 go env -w GOPROXYhttps://goproxy.cn,direct # 设置不走 proxy 的私有仓库&#xff0c;多…

Ajax?阿贾克斯?

一、Ajax简介 AJAX Asynchronous JavaScript and XML&#xff08;异步的 JavaScript 和 XML&#xff09;。 AJAX 不是新的编程语言&#xff0c;而是一种使用现有标准的创新方法。 AJAX 最大的优点是在不重新加载整个页面的情况下&#xff0c;可以与服务器交换数据并更新部分网…

供应链挑战迎刃而解!桑迪亚国家实验室使出“量子杀手锏”

桑迪亚国家实验室的科学家Alicia Magann&#xff08;右&#xff09;&#xff0c;Kenneth Rudinger&#xff08;左上&#xff09;&#xff0c;Mohan Sarovar&#xff08;左下&#xff09;和Matthew Grace&#xff08;未附图&#xff09;开发了基于反馈的量子优化算法&#xff08…

“太极”如何利用混部资源,助力腾讯广告降本增效

编者按&#xff1a;近年来&#xff0c; 随着大模型在NLP领域横扫各种大数据磅单取得巨大成功之后&#xff0c;大数据加大模型成为了AI领域建模的标准范式。搜索、广告、推荐的建模也不例外&#xff0c;动辄千亿参数&#xff0c;上T大小的模型成为各大预估场景的标配&#xff0c…

人脸识别——景联文科技提供3D头模数据采集业务!

“拿起手机刷脸解锁、上下班考勤、支付订单&#xff0c;刷脸已极大地便利了我们的生活。清华大学新闻学院教授沈阳表示&#xff0c;中国人平均每天要暴露在各种摄像头下超过500次。人脸识别已成了我们生活中重要的一部分。由于2D人脸识别容易受到姿态、表情、光照等因素影响&am…