写在前面
最近收到一个有关微信小程序的需求:
要求某个场景下,点击一个按钮,生成一个宣传海报,此海报可以转发给朋友、收藏、保存到相册。通过扫描海报上的二维码可以直达某个网址/小程序。
将此功能封装成组件后,写此文章记录一下,方便后续再用到的时候查阅。
1. 实现思路与构想
- 模块化设计
整个海报生成分享功能采用了模块化设计,将不同功能封装在独立的文件中。例如,painter 负责绘图,downloader 负责资源下载,pen 提供绘图工具类,qrcode 生成二维码,gradient 处理渐变色,sha1 和 util 提供工具函数。这种设计使得代码易于维护和扩展。
- 异步处理与状态管理
为了保证用户体验,所有耗时操作(如图片下载、绘图等)都采用异步处理。同时,通过 wx.showLoading 和 wx.hideLoading 来显示加载提示,提升用户体验。
- 性能优化
- **LRU 缓存: **downloader.js 中实现了 LRU 缓存策略,确保图片资源的高效加载,减少重复下载。
- **渐变色与二维码生成: **gradient.js 和 qrcode.js 提供了高效的渐变色和二维码生成算法,确保绘图性能。
- **多层画布: **painter.wxml 中使用了多个 canvas 标签,分别用于背景、底部、全局、顶部和前台绘图,避免了单个画布的性能瓶颈。
- 用户交互与反馈
- **用户交互: **painter.js 中处理了用户的点击、拖动等交互事件,提供了丰富的交互体验。
- **错误处理: **在绘图过程中,通过 onImgErr 和 onImgOK 回调函数处理绘图成功和失败的情况,提供及时的用户反馈。
2. 目录结构即文件概述
--components 组件库
--shareBox 海报生成组件
--index.js
--index.json
--index.wxml
--index.wxss
--pages 业务页面
--index 首页
--index.js
--index.json
--index.wxml
--index.wxss
--painter 绘图组件
--lib 辅助库文件
--downloader.js
--gradient.js
--pen.js
--qrcode.js
--sha1.js
--util.js
--painter.js
--painter.json
--painter.wxml
--painter.wxss
- index.js 和 index.wxml
- index.js: 这是海报生成组件的核心逻辑文件。它定义了一个自定义组件,负责处理用户交互和数据绑定。主要功能包括:
- 监听属性变化(如是否开始绘图、用户信息等)。
- 调用 handleStartDrawImg 方法启动绘图过程。
- 处理绘图成功 (onImgOK) 和失败 (onImgErr) 的回调。
- index.wxml: 定义了组件的结构,使用了 painter 组件来绘制海报,并通过 palette 属性传递绘图配置。
- painter.js 和 painter.wxml
- painter.js: 这是绘图组件的核心逻辑文件,负责实际的绘图操作。它实现了以下功能:
- 初始化画布并设置样式。
- 下载图片资源并进行缓存管理。
- 处理用户交互(如点击、拖动等)。
- 触发绘图完成事件并返回结果。
- painter.wxml: 定义了绘图组件的结构,包含多个 canvas 标签用于不同的绘图层(背景、底部、全局、顶部、前台),并通过事件绑定处理用户交互。
- 辅助库文件
- downloader.js: 负责下载图片资源并进行本地缓存管理。提供了 LRU 缓存策略,确保图片资源的高效加载。
- gradient.js: 实现渐变色的生成,支持线性渐变和径向渐变。
- pen.js: 提供绘图工具类,封装了具体的绘图逻辑,如绘制矩形、文本、图片等。
- qrcode.js: 实现二维码的生成,支持多种纠错级别。
- sha1.js: 提供 SHA-1 哈希算法,用于生成唯一的文件名或标识符。
- util.js: 提供一些通用工具函数,如 URL 验证、对象深比较等。
- 文件调用关系
- index.js 调用 painter:
- 当 isCanDraw 属性变为 true 时,触发 handleStartDrawImg 方法。
- handleStartDrawImg 方法中设置了 imgDraw 数据,并通过 setData 更新视图,触发 painter 组件的绘制。
- painter.js 内部调用:
- painter 组件监听 palette 属性的变化,当有新的绘图配置时,调用 startPaint 方法。
- startPaint 方法中下载图片资源并调用 Pen 类进行实际绘图。
- Pen 类在绘图过程中会根据配置调用 gradient.js、qrcode.js 等辅助库来处理特定类型的元素。
- 辅助库之间的调用:
- downloader.js 被 painter.js 调用来下载图片资源。
- gradient.js 和 qrcode.js 被 pen.js 调用来处理渐变色和二维码的绘制。
- sha1.js 和 util.js 提供了工具函数,被多个文件调用以增强功能。
3. 关键方法剖析
- handleStartDrawImg (index.js)
handleStartDrawImg() {
wx.showLoading({
title: '生成中'
})
this.setData({
imgDraw: {
width: '750rpx',
height: '1334rpx',
background: 'https://example.com/staticimages/wechat/poster-bg.png',
views: [
// 各种视图元素配置
]
}
})
}
- 功能: 当用户触发开始绘图时,显示加载提示,并设置 imgDraw 数据来启动绘图过程。
- 流程:
- 使用 wx.showLoading 显示加载提示。
- 调用 this.setData 更新组件的数据,其中 imgDraw 包含了画布尺寸、背景图片和多个视图元素(如文本、图片等)的配置。
- 重要性: 这是整个绘图流程的起点,负责初始化绘图配置并更新视图。
- startPaint (painter.js)
startPaint() {
this.initScreenK();
this.downloadImages(this.properties.palette).then((palette) => {
const { width, height } = palette;
if (!width || !height) {
console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
return;
}
// 生成图片时,根据设置的像素值重新绘制
this.canvasWidthInPx = width.toPx();
if (this.properties.widthPixels) {
setStringPrototype(this.screenK, this.properties.widthPixels / this.canvasWidthInPx)
this.canvasWidthInPx = this.properties.widthPixels
}
this.canvasHeightInPx = height.toPx();
this.setData({
photoStyle: `width:${this.canvasWidthInPx}px;height:${this.canvasHeightInPx}px;`,
});
this.photoContext || (this.photoContext = wx.createCanvasContext('photo', this));
new Pen(this.photoContext, palette).paint(() => {
this.saveImgToLocal();
});
setStringPrototype(this.screenK, this.properties.scaleRatio);
});
}
- 功能: 初始化屏幕比例,下载图片资源,并调用 Pen 类进行实际绘图。完成后保存图片到本地。
- 流程:
- 调用 initScreenK 初始化屏幕比例。
- 使用 downloadImages 下载所有需要的图片资源。
- 设置画布宽度和高度,并创建 photoContext。
- 创建 Pen 实例并调用 paint 方法开始绘图。
- 绘图完成后调用 saveImgToLocal 将图片保存到本地。
- 关键点:
- 异步处理: 使用 Promise 确保图片下载完成后才开始绘图。
- 画布配置: 根据传入的配置动态调整画布大小。
- 绘图工具类: 使用 Pen 类封装具体的绘图逻辑,提高代码复用性和可维护性。
- download (downloader.js)
download(url, lru) {
return new Promise((resolve, reject) => {
if (!(url && util.isValidUrl(url))) {
resolve(url);
return;
}
const fileName = getFileName(url);
if (!lru) {
// 无 LRU 情况下直接判断临时文件是否存在
wx.getFileInfo({
filePath: fileName,
success: () => {
resolve(url);
},
fail: () => {
downloadFile(url, lru).then((path) => {
resolve(path);
}, () => {
reject();
});
},
})
return
}
const file = getFile(fileName);
if (file) {
// 检查文件是否正常,不正常需要重新下载
wx.getSavedFileInfo({
filePath: file[KEY_PATH],
success: (res) => {
resolve(file[KEY_PATH]);
},
fail: (error) => {
downloadFile(url, lru).then((path) => {
resolve(path);
}, () => {
reject();
});
},
});
} else {
downloadFile(url, lru).then((path) => {
resolve(path);
}, () => {
reject();
});
}
});
}
- **功能: **下载图片资源,支持 LRU 缓存策略,确保图片资源的高效加载。
- 流程:
- 验证 URL 的有效性。
- 如果未启用 LRU 缓存,则检查临时文件是否存在,存在则直接返回路径,否则下载文件。
- 如果启用了 LRU 缓存,则检查缓存文件是否存在且有效,无效则重新下载。
- 使用 wx.getSavedFileInfo 和 wx.downloadFile 分别检查和下载文件。
- 关键点:
- LRU 缓存: 提供高效的缓存管理,减少重复下载。
- 错误处理: 对于无效或不存在的文件,提供合理的错误处理机制。
- 异步操作: 使用 Promise 处理异步操作,确保流程顺畅。
- doGradient (gradient.js)
doGradient(bg, width, height, ctx) {
if (bg.startsWith('linear')) {
linearEffect(width, height, bg, ctx);
} else if (bg.startsWith('radial')) {
radialEffect(width, height, bg, ctx);
}
}
- **功能: **根据背景配置生成渐变色效果,支持线性渐变和径向渐变。
- 流程:
- 判断背景配置类型(线性渐变或径向渐变)。
- 根据类型调用相应的函数 (linearEffect 或 radialEffect) 来生成渐变色。
- 关键点:
- 条件分支: 根据背景配置选择不同的渐变生成方式。
- 封装性: 将具体实现封装在 linearEffect 和 radialEffect 函数中,提高了代码的可读性和可维护性。
- draw (pen.js)
draw(callback, isMoving, movingCache) {
this.style = {
width: this.data.width.toPx(),
height: this.data.height.toPx(),
};
if (isMoving) {
this.isMoving = true
this.movingCache = movingCache
}
this._background();
for (const view of this.data.views) {
this._drawAbsolute(view);
}
this.ctx.draw(false, () => {
callback && callback(this.callbackInfo);
});
}
- **功能: **负责具体的绘图操作,包括背景绘制、元素绘制等。
- 流程:
- 计算画布的实际宽度和高度。
- 如果有移动操作,更新状态和缓存。
- 调用 _background 方法绘制背景。
- 遍历 views 数组,调用 _drawAbsolute 方法绘制每个元素。
- 完成绘图后调用 this.ctx.draw 并执行回调函数。
- 关键点:
- 状态管理: 通过 isMoving 和 movingCache 管理绘图状态。
- 遍历绘制: 使用循环遍历 views 数组,逐个绘制元素。
- 回调机制: 绘图完成后执行回调函数,通知外部绘图完成。
- genframe (qrcode.js)
function genframe(instring) {
var x, y, k, t, v, i, j, m;
// 找到最小版本号
do {
version++;
k = (ecclevel - 1) * 4 + (version - 1) * 16;
neccblk1 = eccblocks[k++];
neccblk2 = eccblocks[k++];
datablkw = eccblocks[k++];
eccblkwid = eccblocks[k];
k = datablkw * (neccblk1 + neccblk2) + neccblk2 - 3 + (version <= 9);
if (t <= k)
break;
} while (version < 40);
// 初始化数据结构
width = 17 + 4 * version;
// 插入定位符、校准图案、单黑点、定时器间隙、格式信息区、定时行/列、版本块等固定部分
// ...
// 将字符串转换为位流,并计算最大字符串长度
v = strinbuf.length;
// 填充到末尾的填充模式
i = v + 3 - (version < 10);
while (i < x) {
strinbuf[i++] = 0xec;
if (i == x) break;
strinbuf[i++] = 0x11;
}
// 计算并附加 ECC
// ...
// 打包位流到帧,避免掩码区域
// ...
// 应用掩码并评估图像质量
// ...
// 添加最终的掩码/ECC级别字节
// ...
return qrframe;
}
- **功能: **生成二维码,包括计算版本、插入固定部分、转换字符串为位流、计算 ECC、应用掩码、评估图像质量等步骤。
- 流程:
- 版本计算: 找到适合输入字符串的最小版本号。
- 初始化: 初始化数据结构和画布。
- 固定部分: 插入定位符、校准图案、单黑点、定时器间隙、格式信息区、定时行/列、版本块等固定部分。
- 位流转换: 将输入字符串转换为位流,并填充到最大长度。
- ECC 计算: 计算并附加纠错码(ECC)。
- 打包位流: 将位流打包到帧中,避免掩码区域。
- 掩码与评估: 应用掩码并评估图像质量,选择最佳掩码。
- 添加元数据: 添加最终的掩码和 ECC 级别字节。
- 关键点:
- 复杂度高: 二维码生成涉及多个步骤,每个步骤都有详细的逻辑。
- 错误纠正: 计算并附加 ECC 确保二维码的鲁棒性。
- 优化选择: 通过评估图像质量选择最佳掩码,提升二维码的识别率。
- getFileName (downloader.js)
function getFileName(url) {
if (util.isDataUrl(url)) {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(url) || [];
const fileName = `${sha1.hex_sha1(bodyData)}.${format}`;
return fileName;
} else {
return url;
}
}
- **功能: **根据 URL 获取文件名,支持 Base64 编码的图片。
- 流程:
- 检查 URL 是否为 Base64 编码的图片。
- 如果是 Base64 编码,提取图片格式和内容,使用 SHA-1 生成唯一文件名。
- 如果不是 Base64 编码,直接返回原始 URL。
- 关键点:
- 正则表达式: 使用正则表达式匹配 Base64 编码的图片格式和内容。
- SHA-1 哈希: 使用 SHA-1 生成唯一文件名,确保文件名的唯一性和安全性。
- utf16to8 (qrcode.js)
utf16to8: function (str) {
var out, i, len, c;
out = "";
len = str.length;
for (i = 0; i < len; i++) {
c = str.charCodeAt(i);
if ((c >= 0x0001) && (c <= 0x007F)) {
out += str.charAt(i);
} else if (c > 0x07FF) {
out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F));
out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F));
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
} else {
out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F));
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
}
}
return out;
}
- **功能: **将 UTF-16 编码的字符串转换为 UTF-8 编码的字符串,支持中文字符。
- 流程:
- 遍历字符串中的每个字符,根据字符编码范围进行转换。
- 对于 ASCII 字符(0x0001 到 0x007F),直接保留原字符。
- 对于多字节字符(大于 0x07FF),使用 UTF-8 编码规则进行转换。
- 关键点:
- 字符编码转换: 精确处理不同范围的字符编码,确保转换正确。
- UTF-8 规则: 遵循 UTF-8 编码规则,确保中文字符的正确表示。
4. 生成不同类型的海报
4.1 背景纯色的海报
handleStartDrawImg() {
wx.showLoading({ title: '生成中' });
this.setData({
imgDraw: {
width: '750rpx',
height: '1334rpx',
background: '#FFFFFF',
views: [
{
type: 'text',
text: '欢迎使用小程序',
css: {
top: '50rpx',
left: '50rpx',
fontSize: '32rpx',
color: '#333333'
}
},
{
type: 'rect',
css: {
top: '100rpx',
left: '50rpx',
width: '650rpx',
height: '200rpx',
backgroundColor: '#F0F0F0',
borderRadius: '16rpx'
}
}
]
}
});
}
4.2 带有二维码的海报
handleStartDrawImg() {
wx.showLoading({ title: '生成中' });
this.setData({
imgDraw: {
width: '750rpx',
height: '1334rpx',
background: '#FFFFFF',
views: [
{
type: 'text',
text: '欢迎使用小程序',
css: {
top: '50rpx',
left: '50rpx',
fontSize: '32rpx',
color: '#333333'
}
},
{
type: 'qrcode',
content: 'https://example.com',
css: {
top: '100rpx',
left: '50rpx',
width: '200rpx',
height: '200rpx',
color: '#000000',
background: '#FFFFFF'
}
}
]
}
});
}
4.3 背景是图片的海报
handleStartDrawImg() {
wx.showLoading({ title: '生成中' });
this.setData({
imgDraw: {
width: '750rpx',
height: '1334rpx',
background: 'https://example.com/poster-bg.png',
views: [
{
type: 'text',
text: '欢迎使用小程序',
css: {
top: '50rpx',
left: '50rpx',
fontSize: '32rpx',
color: '#FFFFFF'
}
},
{
type: 'image',
url: 'https://example.com/logo.png',
css: {
top: '100rpx',
left: '50rpx',
width: '100rpx',
height: '100rpx'
}
}
]
}
});
}
5. 实战代码及效果
5.1 代码
业务页面代码:
- index.json
{
"usingComponents": {
"share-box": "/components/shareBox/index"
}
}
- index.wxml
<share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" imageBase64="{{imageBase64}}" posterImgBase64="{{posterImgBase64}}" bind:initData="handleClose" />
- index.js
// index.js
// 获取应用实例
const app = getApp()
Page({
data: {
motto: 'Hello World',
userInfo: {},
poster: app.imgUrl+'poster/poster.png',
isCanDraw: false,//海报标志
},
//海报开始
handleClose() {
this.setData({
isCanDraw: !this.data.isCanDraw
})
},
//简单的调用,没有什么参数传递
getUserInfoAndPosterSimply(e) {
wx.getUserProfile({
desc: "获取您的头像昵称信息",
success: res => {
const { userInfo = {} } = res
this.setData({
userInfo,
imageBase64:'data:image/png;base64,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',
posterImgBase64: 'https://gitee.com/anarkhao/anarkh-we-chat-mini-program-project/raw/master/images/poster/head_icon.png',
isCanDraw: true // 开始绘制海报图
})
},
fail: err => {
console.log(err)
}
})
},
//较为复杂的调用,从后台获取微信小程序头像图片imageBase64,posterImgBase64为海报的head图片(这里只是说明可以用图片进行传递)
getUserInfoAndPosterComplex(e) {
var that = this;
wx.getUserProfile({
desc: "获取您的头像昵称信息",
success: res => {
wx.request({
url: 'https://example.com/wechat/getwxacodeunlimit?azb010='+that.data.azb010,
data: {},
header:app.getHeader(),
method: "POST",
success: function (resbak) {
if(resbak.data.flag=='1'){
const { userInfo = {} } = res
that.setData({
userInfo,
isCanDraw: true, // 开始绘制海报图
imageBase64: 'data:image/jpeg;base64,'+resbak.data.imageBase64,
posterImgBase64: that.data.posterImgBase64
})
}else{
wx.hideLoading();
wx.showToast({
title: '生成海报失败',
icon: 'none',
duration: 2000
});
}
}
})
},
fail: err => {
console.log(err)
}
})
},
//海报结束
// 事件处理函数
bindViewTap() {
wx.navigateTo({
url: '../logs/logs'
})
},
onLoad() {
if (wx.getUserProfile) {
this.setData({
canIUseGetUserProfile: true
})
}
}
})
组件shareBox:
- index.js
Component({
properties: {
// 是否开始绘图
isCanDraw: {
type: Boolean,
value: false,
observer(newVal) {
newVal && this.handleStartDrawImg()
}
},
// 用户头像昵称信息
userInfo: {
type: Object,
value: {
avatarUrl: '',
nickName: ''
}
},
imageBase64: String,
posterImgBase64: String
},
data: {
imgDraw: {}, // 绘制图片的大对象
sharePath: '' // 生成的分享图
},
methods: {
handleStartDrawImg() {
wx.showLoading({
title: '生成中'
})
this.setData({
imgDraw: {
width: '750rpx',
height: '1334rpx',
background: '#B2E2F2',//也可以放图片
views: [
{
type: 'image',
url: this.data.posterImgBase64 || 'https://example.com/default.png',
css: {
top: '32rpx',
left: '30rpx',
right: '32rpx',
width: '688rpx',
height: '420rpx',
borderRadius: '16rpx'
},
},
{
type: 'image',
url: 'https://gitee.com/anarkhao/anarkh-we-chat-mini-program-project/raw/master/images/headphoto/head_photo.png',
css: {
top: '404rpx',
left: '328rpx',
width: '96rpx',
height: '96rpx',
borderWidth: '6rpx',
borderColor: '#FFF',
borderRadius: '96rpx'
}
},
{
type: 'text',
// text: this.data.userInfo.nickName || 'Anarkh_Lee',
text: 'Anarkh_Lee',
css: {
top: '532rpx',
fontSize: '28rpx',
left: '375rpx',
align: 'center',
color: '#3c3c3c'
}
},
{
type: 'text',
text: `邀请您观看我的个人博客`,
css: {
top: '576rpx',
left: '375rpx',
align: 'center',
fontSize: '28rpx',
color: '#3c3c3c'
}
},
{
type: 'text',
text: `欢迎来到Anarkh-Lee的个人博客系统`,
css: {
top: '644rpx',
left: '375rpx',
maxLines: 1,
align: 'center',
fontWeight: 'bold',
fontSize: '40rpx',
color: '#3c3c3c'
}
},
{
type: 'image',
url: this.data.imageBase64,//这是一个二维码
css: {
top: '834rpx',
left: '470rpx',
width: '200rpx',
height: '200rpx'
}
},
{
type: 'text',
text: `长按二维码观看博客`,
css: {
top: '915rpx',
left: '250rpx',
align: 'center',
fontSize: '28rpx',
color: '#3c3c3c'
}
}
]
}
})
},
onImgErr(e) {
wx.hideLoading()
wx.showToast({
title: '生成分享图失败,请刷新页面重试'
})
//通知外部绘制完成,重置isCanDraw为false
this.triggerEvent('initData')
},
onImgOK(e) {
wx.hideLoading()
// 展示分享图
wx.showShareImageMenu({
path: e.detail.path,
fail: err => {
console.log(err)
}
})
//通知外部绘制完成,重置isCanDraw为false
this.triggerEvent('initData')
}
}
})
- 获取微信头像的后端代码
@PostMapping(value="getwxacodeunlimit",produces = MediaType.APPLICATION_JSON_VALUE)
public JSONObject getwxacodeunlimit(@RequestParam(value="azb010") String azb010) throws Exception {
JSONObject result=new JSONObject();
RestTemplate restTemplate=new RestTemplate();
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
JSONObject object = new JSONObject();
object.put("scene", azb010);
object.put("page", "pages/xxx/xxx/xxx/xxxx");
object.put("auto_color", true);
object.put("env_version","release");
object.put("check_path",false);
String str=object.toString();
ResponseEntity<byte[]> responseEntity = restTemplate.postForEntity( "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token="+miniCommonUtil.getAccessTokenTest(), str,byte[].class);
String type = responseEntity.getHeaders().getContentType().getType();
System.out.println("类型:"+type);
if("image".equals(type)){
System.out.println("图片");
//返回地是图片
byte[] fileData = responseEntity.getBody();
String appletCode = Base64.encodeBase64String(fileData);
System.out.println(appletCode);
result.put("flag", "1");
result.put("imageBase64",appletCode);
}else{
System.out.println("json");
System.out.println(new String(responseEntity.getBody()));
result.put("flag", "0");
}
return result;
}
5.2 步骤
- 初始化和配置:
- handleStartDrawImg 方法被调用,显示加载提示。
- 使用 this.setData 更新 imgDraw 数据,包含画布尺寸、背景渐变色和多个视图元素的配置。
- 下载图片资源:
- painter.js 中的 startPaint 方法被调用。
- downloadImages 方法下载所有需要的图片资源,使用 downloader.js 进行 LRU 缓存管理。
- 设置画布:
- 根据配置设置画布的实际宽度和高度。
- 创建 photoContext 用于绘图。
- 绘制背景:
- Pen 类中的 _background 方法被调用。
- 使用 gradient.js 中的 doGradient 方法生成渐变色背景。
- 绘制视图元素:
- 遍历 views 数组,调用 _drawAbsolute 方法绘制每个元素。
- 图片绘制:
- 使用 _drawAbsImage 方法绘制图片。
- 如果图片是 Base64 编码,使用 downloader.js 中的 download 方法进行处理。
- 文本绘制:
- 使用 _fillAbsText 方法绘制文本。
- 如果设置了背景颜色,使用 _doBackground 方法绘制文本背景。
- 二维码绘制:
- 使用 _drawQRCode 方法绘制二维码。
- 使用 qrcode.js 中的 draw 方法生成二维码。
- 完成绘图:
- 调用 this.ctx.draw 方法完成绘图。
- 绘图完成后调用 saveImgToLocal 方法将图片保存到本地。
- 隐藏加载提示,触发 onImgOK 回调函数,通知外部绘图完成。
5.3 详细说明
- 背景设置:
- 使用背景色#B2E2F2。
- 图片元素:
- Logo 图片:
- type: ‘image’
- url: 图片的 URL。
- css: 设置图片的位置、大小和圆角。
- 用户头像:
- type: ‘image’
- url: 用户头像的 Base64 编码或 URL。
- css: 设置图片的位置、大小和圆角。
- 文本元素:
- 用户昵称:
- type: ‘text’
- text: 用户昵称或默认文本。
- css: 设置文本的位置、字体大小、对齐方式和颜色。
- 邀请文本:
- type: ‘text’
- text: 邀请文本。
- css: 设置文本的位置、字体大小、对齐方式和颜色。
- 平台名称:
- type: ‘text’
- text: 平台名称。
- css: 设置文本的位置、字体大小、对齐方式、字体粗细和颜色。
- 二维码文本:
- type: ‘text’
- text: 二维码提示文本。
- css: 设置文本的位置、字体大小、对齐方式和颜色。
- 二维码元素:
- 二维码:
- type: ‘qrcode’
- content: 二维码内容(如直播链接)。
- css: 设置二维码的位置、大小、背景颜色和二维码颜色。
5.4 关键函数在海报中的作用
- handleStartDrawImg (index.js):
- 初始化并设置 imgDraw 数据,包含画布尺寸、背景渐变色和多个视图元素的配置。
- 更新视图,触发 painter 组件的绘制。
- startPaint (painter.js):
- 初始化屏幕比例。
- 下载所有需要的图片资源,使用 downloader.js 进行 LRU 缓存管理。
- 设置画布宽度和高度,并创建 photoContext。
- 创建 Pen 实例并调用 paint 方法开始绘图。
- 绘图完成后调用 saveImgToLocal 将图片保存到本地。
- download (downloader.js):
- 下载图片资源,支持 LRU 缓存策略,确保图片资源的高效加载。
- 处理 Base64 编码的图片,生成唯一文件名。
- doGradient (gradient.js):
- 根据背景配置生成渐变色效果,支持线性渐变。
- 使用 sha1.js 生成唯一文件名,确保文件名的唯一性和安全性。
- draw (pen.js):
- 负责具体的绘图操作,包括背景绘制、元素绘制等。
- 根据配置调用 _background 方法绘制背景。
- 遍历 views 数组,调用 _drawAbsolute 方法绘制每个元素。
- 完成绘图后调用 this.ctx.draw 并执行回调函数。
- genframe (qrcode.js):
- 生成二维码,包括计算版本、插入固定部分、转换字符串为位流、计算 ECC、应用掩码、评估图像质量等步骤。
- 使用 sha1.js 生成唯一文件名,确保文件名的唯一性和安全性。
- _drawAbsImage (pen.js):
- 绘制图片元素。
- 如果图片是 Base64 编码,使用 downloader.js 中的 download 方法进行处理。
- 根据配置设置图片的位置、大小和圆角。
- _fillAbsText (pen.js):
- 绘制文本元素。
- 如果设置了背景颜色,使用 _doBackground 方法绘制文本背景。
- 根据配置设置文本的位置、字体大小、对齐方式和颜色。
- _drawQRCode (pen.js):
- 绘制二维码元素。
- 使用 qrcode.js 中的 draw 方法生成二维码。
- 根据配置设置二维码的位置、大小、背景颜色和二维码颜色。
5.5 效果
没有美工设计,太丑了哈哈哈
6. 代码地址
https://gitee.com/anarkhao/anarkh-we-chat-mini-program-project
7. 写在最后
通过上述配置和流程解析,我们可以看到:
- 模块化设计: 各个方法专注于特定功能,减少了代码耦合,提高了可维护性。
- 异步处理: 大量使用 Promise 和异步操作,确保流程顺畅,提升了用户体验。
- 性能优化: 通过 LRU 缓存、渐变色生成、二维码生成等技术手段,优化了绘图性能。
- 用户交互: 提供了丰富的用户交互功能,如点击、拖动等,增强了用户友好性。
这些方法共同协作,实现了从用户交互到绘图生成再到结果展示的完整流程,确保了海报生成分享功能的高效稳定运行。通过不同的配置,可以生成各种复杂的海报,包括单纯的海报、带有二维码的海报、背景是图片的海报、背景是渐变色的海报等。