├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── gulpfile.js
├── package.json
├── screenshots
├── donate_wechat.png
├── screenshot_basic.jpg
├── screenshot_canvas.jpg
├── screenshot_component.jpg
├── screenshot_font.jpg
├── screenshot_iconfont.jpg
├── screenshot_multiple.jpg
├── screenshot_overflow.jpg
├── screenshot_package.jpg
├── screenshot_snapshot.jpg
├── screenshot_snapshot2.jpg
├── screenshot_snapshot3.jpg
└── screenshot_video.jpg
├── src
├── canvas.js
├── constants.js
├── element.js
├── gradient.js
├── index.js
├── index.json
├── index.wxml
└── index.wxss
└── tools
├── build.js
├── config.js
└── demo
├── app.js
├── app.json
├── app.wxss
├── images
├── U3e6ny.jpg
└── qrcode.png
├── pages
├── basic
│ ├── index.js
│ ├── index.json
│ └── index.wxml
├── canvas
│ ├── index.js
│ ├── index.json
│ └── index.wxml
├── component
│ ├── comp.js
│ ├── comp.json
│ ├── comp.wxml
│ ├── comp.wxss
│ ├── index.js
│ ├── index.json
│ └── index.wxml
├── font
│ ├── index.js
│ ├── index.json
│ └── index.wxml
├── iconfont
│ ├── index.js
│ ├── index.json
│ ├── index.wxml
│ └── index.wxss
├── index
│ ├── index.js
│ ├── index.json
│ └── index.wxml
├── multiple
│ ├── index.js
│ ├── index.json
│ └── index.wxml
├── overflow
│ ├── index.js
│ ├── index.json
│ └── index.wxml
├── package
│ ├── comp.js
│ ├── comp.json
│ ├── comp.wxml
│ ├── comp.wxss
│ ├── index.js
│ ├── index.json
│ └── index.wxml
├── snapshot-2
│ ├── index.js
│ ├── index.json
│ ├── index.wxml
│ └── index.wxss
├── snapshot-3
│ ├── index.js
│ ├── index.json
│ ├── index.wxml
│ └── index.wxss
├── snapshot
│ ├── index.js
│ ├── index.json
│ ├── index.wxml
│ └── index.wxss
└── video
│ ├── index.js
│ ├── index.json
│ └── index.wxml
├── project.config.json
├── sitemap.json
└── utils
└── defer.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | miniprogram_dev
2 | miniprogram_dist
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "node": true
6 | },
7 | "extends": [
8 | "airbnb-base"
9 | ],
10 | "parserOptions": {
11 | "ecmaVersion": 12,
12 | "sourceType": "module"
13 | },
14 | "globals": {
15 | "App": true,
16 | "Page": true,
17 | "Component": true,
18 | "Behavior": true,
19 | "wx": true,
20 | "getApp": true,
21 | "getCurrentPages": true
22 | },
23 | "rules": {
24 | "import/no-extraneous-dependencies": "off",
25 | "no-plusplus": "off",
26 | "no-continue": "off",
27 | "linebreak-style": "off",
28 | "no-multi-assign": "off",
29 | "no-underscore-dangle": "off",
30 | "no-param-reassign": "off",
31 | "no-await-in-loop": "off",
32 | "no-fallthrough": "off",
33 | "no-nested-ternary": "off"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | miniprogram_dev
2 | miniprogram_dist
3 | node_modules
4 | package-lock.json
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 ChrisChan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # wxml2canvas-2d
2 |
3 | 基于微信小程序 2D Canvas 的画布组件,根据给定 WXML 结构以及 CSS 样式快速转换成 Canvas 元素,以用于生成海报图片分享等操作。所见即所得(bushi
4 |
5 | ## 安装
6 |
7 | ### npm
8 |
9 | 使用 npm 构建前,请先阅读微信官方的 [npm 支持](https://developers.weixin.qq.com/miniprogram/dev/devtools/npm.html)。
10 |
11 | ```bash
12 | # 通过 npm 安装
13 | npm i wxml2canvas-2d -S --production
14 | ```
15 |
16 | ### 构建 npm 包
17 |
18 | 打开微信开发者工具,点击 **工具** -> **构建 npm**,并勾选 **使用 npm 模块** 选项,构建完成后,即可引入组件。
19 |
20 | ## 使用
21 |
22 | 1. 在页面配置中引入 `wxml2canvas-2d` ;
23 | ```json
24 | {
25 | "usingComponents": {
26 | "wxml2canvas": "wxml2canvas-2d"
27 | }
28 | }
29 | ```
30 | 2. 在页面中编写 wxml 结构,将要生成画布内容的**根节点**用名为 `wxml2canvas-container` 的样式类名称标记,将该根节点内部**需要生成画布内容的节点**用名为 `wxml2canvas-item` 的样式类名称标记(文字类节点需在对应节点声明 `data-text` 属性,并传入文字内容)。上述两个样式类名称可以自定义,只需将对应名称传入 `wxml2canvas-2d` 组件的对应属性参数即可;
31 | ```html
32 |
33 |
34 | 测试标题
35 |
36 | 测试内容,长文本。。
37 |
38 |
39 |
40 | ```
41 | 3. 补充各个节点样式;
42 | ```css
43 | /* pages/index/index.wxss */
44 | .box { /* 根节点(容器)的样式 */ background: white; }
45 | .title { /* 标题的样式 */ }
46 | .image { /* 图片的样式 */ }
47 | .content { /* 内容的样式 */ }
48 | ```
49 | 4. 依据 wxml 结构以及 css 样式,生成画布内容,并将生成结果导出;
50 | ```javascript
51 | // pages/index/index.js
52 | Page({
53 | async generateSharingCard() {
54 | const canvas = this.selectComponent('#wxml2canvas');
55 | await canvas.draw();
56 | const filePath = await canvas.toTempFilePath();
57 | wx.previewImage({
58 | urls: [filePath],
59 | });
60 | },
61 | });
62 | ```
63 | 5. 更多使用方式以及注意事项参考 [API](#api) 以及 [其他](#其他),或克隆此仓库查看更多示例;
64 |
65 | > **PS**:使用字体时,请注意在**生成画布内容前** [**加载对应的字体文件**](https://developers.weixin.qq.com/miniprogram/dev/api/ui/font/wx.loadFontFace.html);部分平台如 Windows 可能不支持画布使用自定义字体(小程序基础库 [v3.6.6](https://developers.weixin.qq.com/miniprogram/dev/framework/release/#v3-6-6-2024-11-12) 及以上已修复);离屏画布模式下,大部分设备均不支持画布使用自定义字体(小程序基础库 [v3.8.7](https://developers.weixin.qq.com/miniprogram/dev/framework/release/#v3-8-7-2025-05-27) 及以上已修复)。
66 |
67 | ## API
68 |
69 | ### 组件参数
70 |
71 | ||类型|说明|默认值|
72 | |:-|:-|:-|:-|
73 | |container-class|String|根节点(容器)样式类名称|wxml2canvas-container|
74 | |item-class|String|内部节点样式类名称|wxml2canvas-item|
75 | |scale|Number|画布缩放比例|1|
76 | |offscreen|Boolean|是否使用离屏画布|false|
77 |
78 | ### 外部样式类
79 |
80 | ||说明|
81 | |:-|:-|
82 | |box-class|Canvas 节点样式类名称|
83 |
84 | ### 组件方法
85 |
86 |
87 |
88 | |
89 | 说明 |
90 | 参数 |
91 |
92 |
93 | 属性 |
94 | 默认值 |
95 | 说明 |
96 |
97 |
98 | draw(page?: PageObject, component?: ComponentObject) |
99 | 绘制画布内容 |
100 | page |
101 | 当前页面实例 |
102 | 组件所在页面实例 |
103 |
104 |
105 | component |
106 | - |
107 | 组件所在组件实例 |
108 |
109 |
110 | toTempFilePath(original?: Boolean) |
111 | 导出画布至临时路径 |
112 | original |
113 | true |
114 | 是否使用实机渲染尺寸 true:各设备像素比不同,导出结果尺寸不同 false:以 750px 为标准,与 WXSS 一致 |
115 |
116 |
117 | toDataURL() |
118 | 导出画布至 Data URI |
119 | - |
120 |
121 |
122 | getImageData() |
123 | 提取画布的像素数据 |
124 | - |
125 |
126 |
127 |
128 | > **PS**:iOS、Mac 与 Windows 平台在**离屏画布模式**(offscreen 为 true)下使用 `wx.canvasToTempFilePath` 导出时会[报错](https://developers.weixin.qq.com/community/search?query=fail%2520invalid%2520viewId)(小程序基础库 [v3.7.1](https://developers.weixin.qq.com/miniprogram/dev/framework/release/#v3-7-1-2024-11-26) 及以上已修复),可以使用 `Canvas.toDataURL` 搭配 `FileSystemManager.saveFile` 保存导出的图片
129 |
130 | ### 其他
131 |
132 |
133 | WXML 组件支持情况
134 |
135 |
136 | > 仅能获取组件自身的样式内容,无法获取组件的伪元素等样式内容
137 |
138 |
139 |
140 | 名称 |
141 | 说明 |
142 |
143 |
144 | view |
145 | 视图容器,支持 |
146 |
147 |
148 | text |
149 | 文本,支持 |
150 |
151 |
152 | button |
153 | 按钮,支持 |
154 |
155 |
156 | image |
157 | 图片,支持 |
158 |
159 |
160 | video |
161 | 视频,支持 |
162 |
163 |
164 | canvas |
165 | 画布,支持 |
166 |
167 |
168 |
169 |
170 |
171 | CSS 属性支持情况
172 |
173 |
174 | > 基础定位布局相关属性 left、width、padding、margin 等均支持
175 |
176 |
177 |
178 | 属性 |
179 | 说明 |
180 |
181 |
182 | background |
183 | 背景,支持渐变图案 |
184 |
185 |
186 | background-color |
187 | 背景颜色,支持 |
188 |
189 |
190 | background-image |
191 | 背景图像,支持 |
192 |
193 |
194 | background-position |
195 | background-position-x |
196 | 背景图像水平方向的位置,支持 |
197 |
198 |
199 | background-position-y |
200 | 背景图像垂直方向的位置,支持 |
201 |
202 |
203 | background-size |
204 | 背景图像的大小,支持 |
205 |
206 |
207 | background-repeat |
208 | 背景图像的重复方式,暂不支持 space 和 round |
209 |
210 |
211 | background-clip |
212 | 背景图像的延伸方式,支持 |
213 |
214 |
215 | border |
216 | border-width |
217 | 边框宽度,支持 |
218 |
219 |
220 | border-style |
221 | 边框样式,暂仅支持 solid、dashed 和 double |
222 |
223 |
224 | border-color |
225 | 边框颜色,支持 |
226 |
227 |
228 | opacity |
229 | 透明度,支持 |
230 |
231 |
232 | box-shadow |
233 | 阴影,暂仅支持单一阴影 |
234 |
235 |
236 | border-radius |
237 | 圆角,支持 |
238 |
239 |
240 | font-family |
241 | 字体,支持 |
242 |
243 |
244 | font-size |
245 | 字体大小,支持 |
246 |
247 |
248 | font-weight |
249 | 字重,支持 |
250 |
251 |
252 | text-align |
253 | 文本对齐,支持 |
254 |
255 |
256 | line-height |
257 | 行高,支持 |
258 |
259 |
260 | text-overflow |
261 | 文字溢出处理,支持 |
262 |
263 |
264 | color |
265 | 文字颜色,支持 |
266 |
267 |
268 | text-indent |
269 | 首行缩进,支持 |
270 |
271 |
272 | text-shadow |
273 | 文字阴影,支持 |
274 |
275 |
276 | direction |
277 | 文本方向,支持 |
278 |
279 |
280 | letter-spacing |
281 | 字符间距,部分平台支持:Windows |
282 |
283 |
284 | word-spacing |
285 | 单词间距,部分平台支持:Windows |
286 |
287 |
288 | filter |
289 | 滤镜效果,部分平台支持:Windows |
290 |
291 |
292 | transform |
293 | 二维变换,支持 |
294 |
295 |
296 | transform-origin |
297 | 变形原点,支持 |
298 |
299 |
300 | text-decoration |
301 | text-decoration-line |
302 | 文本装饰类型,支持 |
303 |
304 |
305 | text-decoration-style |
306 | 文本装饰样式,暂仅支持 solid、dashed 和 double |
307 |
308 |
309 | text-decoration-color |
310 | 文本装饰颜色,支持 |
311 |
312 |
313 |
314 |
315 | TODOs
316 |
317 |
318 | - [x] 支持 `background-image` 等背景图片样式
319 | - [x] 支持 `background-image` 基础属性设置
320 | - [x] 支持 `background-clip` 延伸范围
321 | - [ ] 支持渐变类 `Gradients`
322 | - [x] 支持 `linear-gradient` 线性渐变
323 | - [x] 支持 `radial-gradient` 径向渐变
324 | - [x] 支持 `conic-gradient` 锥形渐变
325 | - [ ] 支持多重 `Gradients` 渐变
326 | - [ ] 支持渐变类 `Gradients` 插值提示(*大脑烧烤中...*)
327 | - [ ] 支持多重 `background`,多重 `box-shadow`
328 | - [x] 支持多重 `background-image`
329 | - [ ] 支持多重 `box-shadow`
330 | - [x] 支持 `CSS Transforms` 相关属性
331 | - [ ] 支持 `CSS Writing Modes` 相关属性(*大脑烧烤中...*)
332 | - [x] 支持 `text-indent`、`text-shadow` 等文字样式
333 | - [x] 支持 `filter` 滤镜效果
334 | - [x] 支持 `video` 标签
335 | - [x] 支持 `canvas` 标签
336 | - [x] 支持渲染自定义组件
337 | - [x] 支持渲染 iconfont 矢量图标
338 |
339 |
340 | 使用注意
341 |
342 |
343 | - 微信新版 Canvas 2D 的画布有宽高分别最大不能超过 4096px 的限制,此 repo 绘制画布时会将画布大小根据设备像素比(dpr)进行放大,使用时请注意避免容器的宽高大于 4096px / dpr
344 | - 尽管微信新版 Canvas 2D 接口采用同步的方式绘制 Canvas 元素,但在部分机型或平台上调用 wx.canvasToTempFilePath 时,也可能绘制过程尚未完成,所以使用过程中尽可能延迟或分步骤调用 wx.canvasToTempFilePath 进行导出图片的操作
345 | - 绘制文字元素时,各机型和各平台对于 font-size、font-weight、line-height 的实际表现与 CSS 中的表现有细微不同,取决于元素的 font-family,建议为文字设置固定的 line-height
346 | - Image 元素的 src 支持:绝对路径、网络地址、临时路径、本地路径以及 base64 Data URI,暂不支持相对路径,无法根据相对路径定位图片资源地址
347 | - 组件方法中的 draw 方法,允许传入 page 与 component 两个参数。当未传入 page 时,默认使用 getCurrentPages 中的最后一个页面实例,即当前页面实例。若此组件位于另一组件内,需传入 component 参数,支持仅传入 component 参数,即:draw(page, component) 与 draw(component) 两种传参方式
348 | - 绘制元素的阴影时,阴影的透明度将随着背景色的透明度等比改变,未设置背景色时,元素的阴影将会不可见,所以绘制元素的阴影时,请尽量设置该元素的背景色为不透明的实色,若无设置,此 repo 在绘制该元素的阴影前会自动设置为纯黑色背景
349 | - 绘制文字的阴影时,阴影的透明度将随着文字颜色的透明度等比改变,所以绘制文字的阴影时,请尽量设置该元素的文字颜色为不透明的实色
350 | - 绘制渐变图案时,请尽量在 CSS 中将渐变的色标按位置正序顺序依次书写,支持使用负值(径向渐变除外),暂未处理色标位置错乱情况下的表现形式,暂不支持控制渐变进程的插值提示
351 | - 设置渐变背景图案时,请尽量避免使用 black、white 等名词形式描述颜色,部分 iOS 设备不会自动转换颜色内容,难以匹配并识别颜色(目前发现部分 iOS 设备中,红色不管以任何形式描述,结果均显示为 red,暂时已处理,且仅处理颜色为 red 的情况)
352 | - 开启离屏画布模式时,部分平台在绘制图片时,由 Canvas.createImage 创建的图片元素,相同的 src 只触发一次 onload 回调,目前只能避免对同一图片重复绘制
353 |
354 |
355 | 开发注意
356 |
357 |
358 | - 微信新版 Canvas 2D 接口基本与 Web Canvas API 对齐,但仍有部分 API 存在差异,随着微信版本或基础库更新,或许会提高相应 API 的支持度
359 | - iOS 平台对于 Path2D 的支持度不足,此 repo 已去除 Path2D 的相关应用,转而使用普通路径,相对应的路径生成次数会增多,绘制时长有所增加,但不多
360 | - 部分 iOS 平台使用 CanvasContext.ellipse 以及 Path2D.ellipse 时,其中的参数 rotation 旋转角度所使用的角度单位不同:iOS 使用角度值,macOS 平台未知,其余使用弧度值
361 | - 绘制文字元素时,各机型和各平台对于 font-size、font-weight、line-height 的实际表现与 CSS 中的表现有细微不同,此 repo 暂时使用常量比例进行换算对齐,未彻底解决
362 | - 绘制元素的边框暂时只支持 solid、dashed 和 double 三种样式,其中 dashed 样式的边框使用 CanvasContext.setLineDash 实现,各机型和各平台的边框虚线间距表现均有差异,此 repo 暂时使用与边框宽度等比的间距表现虚线边框
363 | - 微信新版 Canvas API 目前不支持绘制椭圆形径向渐变图案,此 repo 使用 CanvasContext.scale 对圆形径向渐变图案进行放大或缩小,以实现椭圆形径向渐变图案,而在 closest-corner 与 farthest-corner 模式下的椭圆形径向渐变中,目前还未找出 CSS 在绘制椭圆形径向渐变图案时的长轴与短轴的大小的计算规则,暂时使用常量比例进行换算对齐,未彻底解决
364 | - 锥形渐变图案目前仅微信开发者工具以及 Windows 平台支持,开发工具上锥形渐变角度的 0° 基准与 CSS 表现一致(即 12 点钟方向),起始角度参数的角度单位为弧度,Windows 平台上的 0° 基准为 3 点钟方向,起始角度参数的角度单位为角度,iOS 和 Android 均不支持 CanvasContext.createConicGradient API,macOS 平台未知
365 |
366 |
367 | 更新日志
368 |
369 |
370 | - **v1.3.8 (2025-08-09)**
371 | 1. `F` 修复 transform 表现错误
372 | - **v1.3.7 (2025-08-08)**
373 | 1. `A` 新增 支持绘制 iconfont 矢量图标
374 | - **v1.3.6 (2025-08-06)**
375 | 1. `F` 修复 部分情况下文字缺失
376 | - **v1.3.5 (2025-07-30)**
377 | 1. `F` 修复 部分情况下文字错乱
378 | - **v1.3.4 (2025-07-08)**
379 | 1. `A` 新增 支持绘制自定义组件
380 | - **v1.3.3 (2025-07-08)**
381 | 1. `A` 新增 支持绘制元素 canvas
382 | - **v1.3.2 (2025-07-07)**
383 | 1. `A` 新增 支持绘制样式 text-decoration、text-decoration-color、text-decoration-line、text-decoration-style (solid、dashed、double)
384 | - **v1.3.1 (2025-05-27)**
385 | 1. `U` 更新 兼容部分情况圆角表现差异
386 | - **v1.3.0 (2025-04-28)**
387 | 1. `A` 新增 支持绘制样式 border-left、border-right、border-top、border-bottom
388 | 2. `A` 新增 支持绘制样式 border-style (double)
389 | - **v1.2.5 (2025-04-26)**
390 | 1. `U` 更新 兼容部分设备字体表现差异
391 | - **v1.2.4 (2025-04-21)**
392 | 1. `U` 更新 优化绘制流程
393 | 2. `A` 新增 支持绘制元素 video [详情](https://github.com/ChrisChan13/wxml2canvas-2d/issues/20)
394 | - **v1.2.3 (2025-04-01)**
395 | 1. `F` 修复 text-overflow 表现错误 [详情](https://github.com/ChrisChan13/wxml2canvas-2d/issues/17)
396 | - **v1.2.2 (2025-03-18)**
397 | 1. `U` 更新 优化文字绘制流程
398 | 2. `F` 修复 Number 类型文字绘制报错 [详情](https://github.com/ChrisChan13/wxml2canvas-2d/issues/14)
399 | - **v1.2.1 (2025-02-25)**
400 | 1. `A` 新增 支持导出 ImageData (像素点数据)
401 | 2. `U` 更新 优化文字绘制流程
402 | 3. `A` 新增 支持绘制样式 direction [详情](https://github.com/ChrisChan13/wxml2canvas-2d/issues/13)
403 | - **v1.2.0 (2025-02-18)**
404 | 1. `A` 新增 支持绘制样式 transform、transform-origin [详情](https://github.com/ChrisChan13/wxml2canvas-2d/issues/4)
405 | - **v1.1.8 (2025-01-22)**
406 | 1. `F` 修复 line-height 过高时表现错误 [详情](https://juejin.cn/post/7439556363104600079#comment)
407 | - **v1.1.7 (2025-01-21)**
408 | 1. `F` 修复 组件嵌套于组件时绘制报错 [详情](https://developers.weixin.qq.com/community/develop/article/doc/0000eae9008c484fe262362c66b013?jumpto=comment&commentid=00024297c4c28081a9b2672a1654)
409 | - **v1.1.6 (2025-01-14)**
410 | 1. `F` 修复 组件嵌套于组件时绘制报错 [详情](https://developers.weixin.qq.com/community/develop/article/doc/0000eae9008c484fe262362c66b013?jumpto=comment&commentid=00024297c4c28081a9b2672a1654)
411 | - **v1.1.5 (2024-11-27)**
412 | 1. `A` 修复 iOS 平台 border-radius 表现错误 (iOS 角度单位与其他平台对齐) [详情](https://github.com/ChrisChan13/wxml2canvas-2d/issues/11)
413 | - **v1.1.4 (2024-11-18)**
414 | 1. `U` 更新 优化部分常量变量设置
415 | - **v1.1.3 (2024-11-16)**
416 | 1. `A` 新增 支持离屏画布模式
417 | 2. `A` 新增 支持导出 DataURI (Base64 编码)
418 | - **v1.1.2 (2024-11-14)**
419 | 1. `F` 修复 text-align 表现错误
420 | - **v1.1.1 (2024-11-14)**
421 | 1. `A` 新增 支持绘制样式 filter (仅 Windows 支持)
422 | - **v1.1.0 (2024-11-11)**
423 | 1. `U` 更新 优化绘制流程
424 | 2. `A` 新增 支持绘制样式 letter-spacing (仅 Windows 支持)、word-spacing (仅 Windows 支持)
425 | - **v1.0.10 (2024-11-11)**
426 | 1. `A` 新增 支持绘制样式 text-shadow [详情](https://github.com/ChrisChan13/wxml2canvas-2d/issues/10)
427 | - **v1.0.9 (2024-11-01)**
428 | 1. `A` 新增 支持绘制换行符 [详情](https://github.com/ChrisChan13/wxml2canvas-2d/issues/9)
429 | 2. `F` 修复 单行文字 text-overflow 表现错误
430 | 3. `A` 新增 支持绘制样式 text-indent
431 | - **v1.0.8 (2024-07-02)**
432 | 1. `U` 更新 优化节点信息查询逻辑
433 | 2. `U` 更新 兼容部分设备字体表现差异 [详情](https://github.com/ChrisChan13/wxml2canvas-2d/issues/7)
434 | - **v1.0.7 (2024-04-22)**
435 | 1. `A` 新增 支持绘制样式 background-clip
436 | - **v1.0.6 (2024-04-19)**
437 | 1. `F` 修复 Windows 平台画布缩放错误 [详情](https://github.com/ChrisChan13/wxml2canvas-2d/issues/5)
438 | 2. `A` 新增 支持导出时统一尺寸
439 | - **v1.0.5 (2024-04-16)**
440 | 1. `A` 新增 支持绘制样式 radial-gradient
441 | 2. `A` 新增 支持绘制样式 conic-gradient (仅 Windows 支持)
442 | - **v1.0.4 (2024-04-11)**
443 | 1. `U` 更新 修改元素的盒子模型绘制逻辑
444 | - **v1.0.3 (2024-04-11)**
445 | 1. `F` 修复 绘制背景图报错
446 | - **v1.0.2 (2024-04-10)**
447 | 1. `A` 新增 支持绘制样式 background-image、background-size、background-repeat、background-position [详情](https://github.com/ChrisChan13/wxml2canvas-2d/issues/1)
448 | 2. `U` 更新 优化 Gradient 对象创建逻辑
449 | - **v1.0.1 (2024-03-16)**
450 | 1. `F` 修复 iOS 平台表现错误 (iOS 不支持 Path2D) [详情](https://github.com/ChrisChan13/wxml2canvas-2d/issues/3)
451 | 2. `A` 新增 支持绘制样式 linear-gradient
452 | - **v1.0.0 (2023-12-19)**
453 | 1. `A` 新增 支持绘制元素 image、view、text、button
454 | 2. `A` 新增 支持绘制样式 定位相关属性、padding、background-color、opacity、border-radius
455 | 3. `A` 新增 支持绘制样式 font-weight、font-size、font-family、text-align、line-height、text-overflow、color
456 | 4. `A` 新增 支持绘制样式 box-shadow (单个阴影)
457 | 5. `A` 新增 支持绘制样式 border (四边一致)、border-width、border-color
458 | 6. `A` 新增 支持绘制样式 border-style (dashed 和 solid)
459 | 7. `A` 新增 支持绘制内容缩放
460 | 8. `A` 新增 支持导出 tempFile 临时文件
461 |
462 |
463 | ## FAQ
464 |
465 |
466 | 如何绘制自定义组件?
467 |
468 |
469 | `wxml2canvas-2d` 支持绘制自定义组件,自定义组件内也可以使用其他自定义组件。
470 | 1. 自定义组件的元素节点需要声明 `id` 以及 `data-component` 属性,当然 **样式类** `wxml2canvas-item` 也不可缺少。请确保 `id` 在文档中不重复,`data-component` 为 `Boolean` 类型,只需声明即为 `true` 值。
471 | 2. 自定义组件内的元素节点与页面内的元素节点无异,为需要渲染的元素节点用样式类 `wxml2canvas-item` 标记即可。
472 | 3. 自定义组件 `slot` 插槽内的元素节点与页面内的元素节点无异,同上。
473 | 4. 支持渲染自定义组件内的子自定义组件,为子自定义组件进行如上同样的设置即可。
474 |
475 | 参考如下:
476 | ```html
477 |
478 |
479 | 测试标题
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 | 测试内容,长文本。。
488 |
489 |
490 | ```
491 |
492 |
493 | 如何绘制 iconfont 矢量图标?
494 |
495 |
496 | `wxml2canvas-2d` 支持绘制 iconfont 矢量图标。与自定义字体类似,生成画布内容前需加载对应的矢量图标字体文件。此外,还需搭配 `data-icon` 属性传入对应图标的十六进制 Unicode 码,该码与 CSS 中对应的矢量图标字符码相同。
497 |
498 | 参考如下:
499 | ```html
500 |
501 |
502 |
503 |
504 | 测试标题
505 |
506 |
507 | ```
508 | ```css
509 | @font-face {
510 | font-family: 'iconfont';
511 | src: url('data:font/ttf;charset=utf-8;base64,XXXXXXXXXXXXX') format('truetype');
512 | /* 其他样式 */
513 | }
514 | [class^="icon-"], [class*=" icon-"] {
515 | font-family: 'iconfont' !important;
516 | /* 其他样式 */
517 | }
518 | .icon-title::before {
519 | content: '\e996';
520 | }
521 | ```
522 | ```javascript
523 | Page({
524 | async generateSharingCard() {
525 | await wx.loadFontFace({
526 | family: 'iconfont',
527 | // 可以为 https 链接或者 Data URL
528 | source: 'data:font/ttf;charset=utf-8;base64,XXXXXXXXXXXXX',
529 | scopes: ['native'],
530 | });
531 | // 导出画布
532 | // ...
533 | },
534 | });
535 | ```
536 |
537 |
538 | 如何同时截取多个不同节点的图片?
539 |
540 |
541 | 当需要同时截取页面上不同节点多张不同图片的时候,可以用多个 `wxml2canvas-2d` 组件,各自为 `container-class` 以及 `item-class` 自定义不同的样式类名,并在对应节点的 `class` 中体现,如:
542 | ```html
543 |
544 |
545 | 测试标题
546 |
547 | 测试内容,长文本。。
548 |
549 |
550 |
551 |
552 | 测试标题
553 |
554 | 测试内容,长文本。。
555 |
556 |
557 |
558 |
559 |
560 |
561 | ```
562 | ```javascript
563 | Page({
564 | // 同时截取节点一与节点二的图片
565 | async captureAllNodes() {
566 | const filePaths = await Promise.all(
567 | this.captureNodeScreenshot('#canvas_1'),
568 | this.captureNodeScreenshot('#canvas_2'),
569 | );
570 | },
571 | async captureNodeScreenshot(id) {
572 | const canvas = this.selectComponent(id);
573 | await canvas.draw();
574 | const filePath = await canvas.toTempFilePath();
575 | return filePath;
576 | },
577 | });
578 | ```
579 |
580 |
581 | Error: set height/width out of range: xxxx > xxxx
582 |
583 |
584 | 此问题为微信对 2D Canvas 的高度/宽度限制,在不同设备中具体的限制大小有所不同,根据设备的像素比,以 4096 为基数的倍数作为限制大小。
585 | 若触发了此类限制,且由 `wxml2canvas-2d` 组件渲染的内容相对固定,即宽高变化不大,可以传入组件参数 `scale` 对画布进行缩小。比较好的方案为根据设备的像素比按比例缩放,但需注意缩小后导出图片的质量有所降低。
586 | 若需要渲染的内容过多,且宽高无法确定,推荐将内容分段渲染,使用多个 `wxml2canvas-2d` 组件渲染不同段落的内容,最后将所有导出的图片,使用第三方库合成图片,或将所有图片按序拼接在一个旧版 Canvas 画布中并导出。
587 |
588 |
589 | Error: The height/width xxxx has exceeded the limit 4096
590 |
591 |
592 | 此问题与上一个问题相同,均为微信对 2D Canvas 的高度/宽度限制,区别在于此类报错信息存在于旧基础库版本中,该限制为固定高度限制。参考上一问题。
593 |
594 |
595 | TypeError: Cannot read property 'draw' of null
596 |
597 |
598 | 此问题一般是由于调用 `draw` 方法时,`wxml2canvas-2d` 组件实例不存在于当前页面中。请检查页面 JSON 配置文件,是否配置了 `wxml2canvas-2d` 组件,以及页面中是否编写了 `` 节点。
599 |
600 |
601 | TypeError: Cannot read property 'width' of undefined
602 |
603 |
604 | 此问题一般是由于将 `wxml2canvas-2d` 组件封装于另一组件内,而调用 `draw` 方法时,没有将组件实例传入,导致查询不到 `wxml2canvas-2d` 节点。请参考“如何在自定义组件中使用 wxml2canvas-2d 组件”。
605 |
606 |
607 | 如何在自定义组件中使用 wxml2canvas-2d 组件?
608 |
609 |
610 | 将 `wxml2canvas-2d` 组件封装于自定义组件中时,由于小程序的节点查询方法需要传入对应的组件实例,所以 `draw` 方法支持传入页面或组件的实例。传参方式有:
611 | ```javascript
612 | // 一、默认使用当前页面实例,即不传参数
613 | Page({
614 | captureNodeScreenshot() {
615 | const canvas = this.selectComponent('#wxml2canvas');
616 | await canvas.draw();
617 | },
618 | });
619 |
620 | // 二、传入页面实例,调用另一个页面的方法
621 | Page({
622 | captureNodeScreenshot() {
623 | /** 上一个页面的页面实例 */
624 | const page = getCurrentPages().slice(-2)[0]
625 | const canvas = page.selectComponent('#wxml2canvas');
626 | await canvas.draw(page);
627 | },
628 | });
629 |
630 | // 三、传入组件实例,位于自定义组件内时必传
631 | Component({
632 | methods: {
633 | captureNodeScreenshot() {
634 | const canvas = this.selectComponent('#wxml2canvas');
635 | await canvas.draw(this);
636 | },
637 | },
638 | });
639 |
640 | // 四、待绘制节点位于组件内,传入组件实例
641 | Page({
642 | captureNodeScreenshot() {
643 | const component = this.selectComponent('#yourComponent');
644 | const canvas = this.selectComponent('#wxml2canvas');
645 | await canvas.draw(this, component);
646 | },
647 | });
648 | ```
649 |
650 |
651 | 为什么文本内容截图出来不一样?
652 |
653 |
654 | 关于文本内容,不同设备有不同的默认字体、行高、字重等影响文字在界面中表现的因素,而在将文字绘制于画布中时,这些差异也会被放大。因此,若画布渲染与界面渲染之间有细微的差异,属于正常现象,适当设置文字的字体、行高、字重等样式可以减少此类差异。
655 |
656 |
657 | 文本内容被截取、溢出缩略不正确、换行结果不正确?
658 |
659 |
660 | 上一个问题“为什么文本内容截图出来不一样?”中提到了不同设备之间文字的表现差异,这是其中一个对于此问题很大的影响因素,具体分为以下几种情况:
661 |
662 | 1. 文字未能渲染完整,末端发生了截取:`wxml2canvas-2d` 组件会获取元素在界面中渲染的宽高,并将渲染范围限制在该宽高范围内,超出的部分将不会渲染。由于界面与画布的文字表现存在差异,有可能出现界面上文字所占宽高小于画布上文字所占宽高,导致溢出部分被截取。
663 | 2. 文字缩略位置不一致或没有正常缩略:与情况 1 相似,界面与画布的表现差异影响了文字所占空间的大小,从而使缩略位置产生偏差。而没有正常缩略的情况与情况 3 相似,参考情况 3。
664 | 3. 多行文字没有换行或单行文字产生换行:不同语言的文字存在不同的分词规则,从而决定其文字在界面上的表现,如英文单词会在行内空间不足时提前换行以确保单词完整显示等等。`wxml2canvas-2d` 组件使用 `Intl.Segmenter` 处理分词,但该 API 支持范围有限。在不支持 `Intl.Segmenter` 的设备上将会调用简单的 polyfill 来模拟分词,该 polyfill 分词规则简单,因此误判率高,从而对换行结果产生了影响。
665 |
666 | 上述情况 1 的问题虽已经过计算优化,但仍无法覆盖所有语言文字字符组合的情况。情况 3 中 polyfill 的分词规则与 空格符(/x20)以及一部分英文标点字符相关,若分词规则有误,很大可能是由于文本中有大量的中英文数字或空格等字符的混合内容。若文本中空格较多,画布绘制与界面表现差距太大,可以尝试将 空格(/x20)替换为 空格(/xa0),此举将绕过部分 polyfill 的分词过滤。
667 |
668 |
669 | 为什么部分设备圆角绘制不正确?
670 |
671 |
672 | 这个问题目前仅在部分 iOS 设备中发现过,由于圆角使用了 `CanvasContext.ellipse` API 来绘制,而部分 iOS 设备的 `CanvasContext.ellipse` 方法实现不同,其中一个角度参数的描述单位不同,iOS 使用了角度为单位,而其他设备是正常的弧度单位。出现该问题的 iOS 设备范围暂时无法准确界定,无法得到有效的修复,实际过程中可以减少椭圆形圆角的使用,采用圆形圆角代替,避免出现该问题。
673 |
674 |
675 | 为什么单词间距和字符间距不生效?
676 |
677 |
678 | 单词间距(word-spacing)和字符间距(letter-spacing)目前发现仅在开发工具和 Windows 设备上有效,其他设备设置了对应的 Canvas 样式后没有起到任何效果。实际过程中尽量避免单词间距和字符间距的设置,否则可能会导致文字占用空间变小,绘制时产生截取。若必须控制间距,可将文字内容拆分为单词/字符,为每个单词/字符设置 margin 样式。
679 |
680 |
681 | 如何在 uni-app 与 Taro 中使用?
682 |
683 |
684 | `wxml2canvas-2d` 组件可以在 uni-app 与 Taro 中使用,但跨平台的支持度有限,目前只支持微信小程序平台。
685 | 1. 在 uni-app 中使用:参考 [小程序自定义组件支持](https://uniapp.dcloud.net.cn/tutorial/miniprogram-subject.html)。
686 | 2. 在 Taro 中使用:参考 [Taro 使用原生模块](https://docs.taro.zone/docs/hybrid)。
687 |
688 | 需要注意的是,Taro 对于小程序 dataset 的模拟是在小程序的逻辑层实现的,并没有真正在模板设置这个属性。`wxml2canvas-2d` 组件渲染文本内容时需要对应的节点设置 `data-text` 属性,而 Taro 会忽略该属性,导致 `wxml2canvas-2d` 组件读取不到文本内容。Taro 提供了属性注入的方案,参考 [模板属性 dataset](https://docs.taro.zone/docs/vue-overall/#dataset)。
689 |
690 |
691 | 如何在 skyline 渲染引擎中使用?
692 |
693 |
694 | 非常抱歉,`wxml2canvas-2d` 目前无法在小程序 skyline 引擎中使用,因 skyline 引擎无法获取 `computedStyle`,导致无法在画布中绘制对应的样式。
695 |
696 |
697 | ## Demo
698 |
699 | 克隆本仓库,运行 `npm i & npm run dev`,将 miniprogram_dev 文件夹导入微信开发者工具
700 |
701 | ## 效果预览
702 |
703 |
704 |
705 |
706 | 基础示例
707 | |
708 |
709 | 绘制视频节点示例
710 | |
711 |
712 | 绘制 Canvas 节点示例
713 | |
714 |
715 |
716 |
717 |
718 | |
719 |
720 |
721 | |
722 |
723 |
724 | |
725 |
726 |
727 |
728 |
729 |
730 | 自定义字体示例
731 | |
732 |
733 | IconFont 图标示例
734 | |
735 |
736 | 并发绘制示例
737 | |
738 |
739 |
740 |
741 |
742 | |
743 |
744 |
745 | |
746 |
747 |
748 | |
749 |
750 |
751 |
752 |
753 |
754 | 自定义组件内示例
755 | |
756 |
757 | 绘制自定义组件示例
758 | |
759 |
760 | 绘制超长节点示例
761 | |
762 |
763 |
764 |
765 |
766 | |
767 |
768 |
769 | |
770 |
771 |
772 | |
773 |
774 |
775 |
776 |
777 |
778 | 完整页面截图示例
779 | |
780 |
781 | 完整页面截图示例-2
782 | |
783 |
784 | 完整页面截图示例-3
785 | |
786 |
787 |
788 |
789 |
790 | |
791 |
792 |
793 | |
794 |
795 |
796 | |
797 |
798 |
799 |
800 | ## 支持
801 |
802 | 如果这个项目对您有所帮助,或者您希望支持我的持续开发,欢迎通过以下方式进行赞赏。
803 |
804 | 您的支持就是我保持更新的最大动力!
805 |
806 | |微信赞赏码|
807 | |:---:|
808 | |
|
809 | |感谢您的慷慨赞赏!|
810 |
811 | > 请在赞赏时留言备注您的 GitHub ID 或昵称,我会铭记于心!
812 |
813 | ## Star History
814 |
815 | [](https://star-history.com/#ChrisChan13/wxml2canvas-2d&Date)
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 | const clean = require('gulp-clean');
3 |
4 | const config = require('./tools/config');
5 | require('./tools/build');
6 |
7 | const cleanPath = (path) => gulp.src(path, {
8 | read: false, allowEmpty: true,
9 | }).pipe(clean());
10 |
11 | gulp.task('clean', gulp.series(
12 | () => cleanPath(config.distPath),
13 | (done) => {
14 | if (!config.isDev) return done();
15 | return cleanPath(config.demoDist);
16 | },
17 | ));
18 |
19 | gulp.task('default', gulp.series('build'));
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wxml2canvas-2d",
3 | "version": "1.3.8",
4 | "description": "基于微信小程序 2D Canvas 的画布组件,根据给定 WXML 结构以及 CSS 样式快速转换成 Canvas 元素",
5 | "main": "miniprogram_dist/index.js",
6 | "miniprogram": "miniprogram_dist",
7 | "scripts": {
8 | "dev": "gulp dev --dev",
9 | "clean-dev": "gulp clean --dev",
10 | "clean": "gulp clean",
11 | "build": "gulp"
12 | },
13 | "keywords": [
14 | "wxml2canvas",
15 | "canvas",
16 | "miniprogram"
17 | ],
18 | "files": ["miniprogram_dist"],
19 | "author": "chrischan",
20 | "license": "MIT",
21 | "repository": "https://github.com/ChrisChan13/wxml2canvas-2d.git",
22 | "devDependencies": {
23 | "eslint": "^7.32.0",
24 | "eslint-config-airbnb-base": "^14.2.1",
25 | "eslint-plugin-import": "^2.24.2",
26 | "gulp": "^4.0.2",
27 | "gulp-clean": "^0.4.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/screenshots/donate_wechat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisChan13/wxml2canvas-2d/99c432e9791d8a98ceff32aa4490ec8f30e2a0fb/screenshots/donate_wechat.png
--------------------------------------------------------------------------------
/screenshots/screenshot_basic.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisChan13/wxml2canvas-2d/99c432e9791d8a98ceff32aa4490ec8f30e2a0fb/screenshots/screenshot_basic.jpg
--------------------------------------------------------------------------------
/screenshots/screenshot_canvas.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisChan13/wxml2canvas-2d/99c432e9791d8a98ceff32aa4490ec8f30e2a0fb/screenshots/screenshot_canvas.jpg
--------------------------------------------------------------------------------
/screenshots/screenshot_component.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisChan13/wxml2canvas-2d/99c432e9791d8a98ceff32aa4490ec8f30e2a0fb/screenshots/screenshot_component.jpg
--------------------------------------------------------------------------------
/screenshots/screenshot_font.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisChan13/wxml2canvas-2d/99c432e9791d8a98ceff32aa4490ec8f30e2a0fb/screenshots/screenshot_font.jpg
--------------------------------------------------------------------------------
/screenshots/screenshot_iconfont.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisChan13/wxml2canvas-2d/99c432e9791d8a98ceff32aa4490ec8f30e2a0fb/screenshots/screenshot_iconfont.jpg
--------------------------------------------------------------------------------
/screenshots/screenshot_multiple.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisChan13/wxml2canvas-2d/99c432e9791d8a98ceff32aa4490ec8f30e2a0fb/screenshots/screenshot_multiple.jpg
--------------------------------------------------------------------------------
/screenshots/screenshot_overflow.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisChan13/wxml2canvas-2d/99c432e9791d8a98ceff32aa4490ec8f30e2a0fb/screenshots/screenshot_overflow.jpg
--------------------------------------------------------------------------------
/screenshots/screenshot_package.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisChan13/wxml2canvas-2d/99c432e9791d8a98ceff32aa4490ec8f30e2a0fb/screenshots/screenshot_package.jpg
--------------------------------------------------------------------------------
/screenshots/screenshot_snapshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisChan13/wxml2canvas-2d/99c432e9791d8a98ceff32aa4490ec8f30e2a0fb/screenshots/screenshot_snapshot.jpg
--------------------------------------------------------------------------------
/screenshots/screenshot_snapshot2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisChan13/wxml2canvas-2d/99c432e9791d8a98ceff32aa4490ec8f30e2a0fb/screenshots/screenshot_snapshot2.jpg
--------------------------------------------------------------------------------
/screenshots/screenshot_snapshot3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisChan13/wxml2canvas-2d/99c432e9791d8a98ceff32aa4490ec8f30e2a0fb/screenshots/screenshot_snapshot3.jpg
--------------------------------------------------------------------------------
/screenshots/screenshot_video.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisChan13/wxml2canvas-2d/99c432e9791d8a98ceff32aa4490ec8f30e2a0fb/screenshots/screenshot_video.jpg
--------------------------------------------------------------------------------
/src/canvas.js:
--------------------------------------------------------------------------------
1 | import {
2 | DEFAULT_LINE_HEIGHT, FONT_SIZE_OFFSET,
3 | SYS_DPR, RPX_RATIO, LINE_BREAK_SYMBOL,
4 | IS_MOBILE, VIDEO_POSTER_MODES,
5 | POSITIONS, DOUBLE_LINE_RATIO,
6 | } from './constants';
7 | import { drawGradient } from './gradient';
8 |
9 | /**
10 | * 拆分文本
11 | * @param {String} text 文本内容
12 | * @returns {Array} 文本字符
13 | */
14 | const segmentText = (text) => {
15 | // 使用内置的 Intl.Segmenter API 进行拆分,安卓设备不支持
16 | if (typeof Intl !== 'undefined' && Intl.Segmenter) {
17 | const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
18 | return Array.from(segmenter.segment(text)).map((item) => item.segment);
19 | }
20 | return Array.from(text);
21 | };
22 |
23 | /**
24 | * 获取单词长度中位数
25 | * @param {Array} segments 单词数组
26 | * @return {Number} 单词长度中位数
27 | */
28 | const getSegmentLengthMedian = (segments) => {
29 | const words = segments.filter((segment) => segment.isWord);
30 | const size = words.length;
31 | const lengths = words.map((segment) => segment.value.length).sort((a, b) => a - b);
32 | if (size % 2 === 1) {
33 | return lengths[Math.floor(size / 2)];
34 | }
35 | return (lengths[size / 2 - 1] + lengths[size / 2]) / 2;
36 | };
37 |
38 | /**
39 | * 拆分文本为单词与符号
40 | * @param {String} text 文本内容
41 | * @returns {Array} 单词与符号数组
42 | */
43 | const segmentTextIntoWords = (text) => {
44 | /** 分隔符号计数 */
45 | let delimitersCount = 0;
46 | /** 单词计数 */
47 | let wordsCount = 0;
48 | /** 是否由单词组成 */
49 | let isWordBased = false;
50 | /** 单词与符号数组 */
51 | let segments = [];
52 | // 使用内置的 Intl.Segmenter API 进行拆分,安卓设备不支持
53 | if (typeof Intl !== 'undefined' && Intl.Segmenter) {
54 | const segmenter = new Intl.Segmenter(undefined, { granularity: 'word' });
55 | segments = Array.from(segmenter.segment(text)).map((item) => {
56 | if (!item.isWordLike) delimitersCount += 1;
57 | else wordsCount += 1;
58 | return {
59 | value: item.segment,
60 | isWord: item.isWordLike,
61 | };
62 | });
63 | } else {
64 | if (typeof text !== 'string') text = text.toString();
65 | /** 分隔符号匹配 */
66 | const delimiters = text.matchAll(/[,.!?; ]/g);
67 | let delimiter = delimiters.next();
68 | /** 连续分隔符号计数 */
69 | let consecutiveNonWord = 0;
70 | let lastIndex = 0;
71 | while (!delimiter.done) {
72 | const word = text.slice(lastIndex, delimiter.value.index);
73 | if (word) {
74 | // 单独处理换行符,不记入分隔符号
75 | if (new RegExp(`${LINE_BREAK_SYMBOL}`).test(word)) {
76 | // eslint-disable-next-line no-loop-func
77 | word.split(LINE_BREAK_SYMBOL).map((item) => {
78 | segments.push({
79 | value: item,
80 | isWord: true,
81 | }, {
82 | value: LINE_BREAK_SYMBOL,
83 | isWord: false,
84 | });
85 | wordsCount += 1;
86 | return item;
87 | });
88 | segments.splice(-1, 1);
89 | } else {
90 | segments.push({
91 | value: word,
92 | isWord: true,
93 | });
94 | wordsCount += 1;
95 | }
96 | consecutiveNonWord = 0;
97 | }
98 | segments.push({
99 | value: delimiter.value[0],
100 | isWord: false,
101 | });
102 | // 连续的分隔符号只计一次
103 | if (consecutiveNonWord === 0) {
104 | delimitersCount += 1;
105 | }
106 | consecutiveNonWord += 1;
107 | lastIndex = delimiter.value.index + delimiter.value[0].length;
108 | delimiter = delimiters.next();
109 | }
110 | if (lastIndex < text.length) {
111 | segments.push({
112 | value: text.slice(lastIndex),
113 | isWord: true,
114 | });
115 | wordsCount += 1;
116 | }
117 | }
118 | /**
119 | * 判断是否由单词组成
120 | *
121 | * 1. 单词数量超过 1 个
122 | * 2. 单词长度中位数不超过 13
123 | * 3. 分隔符号占比超过 30%
124 | */
125 | isWordBased = wordsCount > 1 && getSegmentLengthMedian(segments) <= 13
126 | && delimitersCount / (wordsCount + delimitersCount) > 0.3;
127 | if (!isWordBased) {
128 | segments = segmentText(text).map((item) => ({
129 | value: item,
130 | isWord: true,
131 | }));
132 | }
133 | return segments;
134 | };
135 |
136 | /**
137 | * 获取画布对象
138 | * @param {ComponentObject} component 组件实例对象
139 | * @param {String} selector 选择器
140 | * @returns {Promise