/** * 导入所需的工具函数和依赖 */ import { isNull } from "@/cool"; import { generateFrame } from "./qrcode"; import type { ClQrcodeMode } from "../../types"; /** * 二维码生成配置选项接口 * 定义了生成二维码所需的所有参数 */ export type QrcodeOptions = { ecc: string; // 纠错级别,可选 L/M/Q/H,纠错能力依次增强 text: string; // 二维码内容,要编码的文本 size: number; // 二维码尺寸,单位px foreground: string; // 前景色,二维码数据点的颜色 background: string; // 背景色,二维码背景的颜色 padding: number; // 内边距,二维码四周留白的距离 logo: string; // logo图片地址,可以在二维码中心显示logo logoSize: number; // logo尺寸,logo图片的显示大小 mode: ClQrcodeMode; // 二维码样式模式,支持矩形、圆形、线条、小方块 pdColor: string | null; // 定位点颜色,三个角上定位图案的颜色,为null时使用前景色 pdRadius: number; // 定位图案圆角半径,为0时绘制直角矩形 }; /** * 获取当前设备的像素比 * 用于处理高分屏显示 * @returns 设备像素比 */ function getRatio() { // #ifdef APP || MP-WEIXIN return uni.getWindowInfo().pixelRatio; // App和小程序环境 // #endif // #ifdef H5 return window.devicePixelRatio; // H5环境 // #endif } /** * 绘制圆角矩形 * 兼容不同平台的圆角矩形绘制方法 * @param ctx Canvas上下文 * @param x 矩形左上角x坐标 * @param y 矩形左上角y坐标 * @param width 矩形宽度 * @param height 矩形高度 * @param radius 圆角半径 */ function drawRoundedRect( ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number ) { if (radius <= 0) { // 圆角半径为0时直接绘制矩形 ctx.fillRect(x, y, width, height); return; } // 限制圆角半径不超过矩形的一半 const maxRadius = Math.min(width, height) / 2; const r = Math.min(radius, maxRadius); ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + width - r, y); ctx.arcTo(x + width, y, x + width, y + r, r); ctx.lineTo(x + width, y + height - r); ctx.arcTo(x + width, y + height, x + width - r, y + height, r); ctx.lineTo(x + r, y + height); ctx.arcTo(x, y + height, x, y + height - r, r); ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r); ctx.closePath(); ctx.fill(); } /** * 绘制定位图案 * 绘制7x7的定位图案,包含外框、内框和中心点 * @param ctx Canvas上下文 * @param startX 定位图案起始X坐标 * @param startY 定位图案起始Y坐标 * @param px 单个像素点大小 * @param pdColor 定位图案颜色 * @param background 背景颜色 * @param radius 圆角半径 */ function drawPositionPattern( ctx: CanvasRenderingContext2D, startX: number, startY: number, px: number, pdColor: string, background: string, radius: number ) { const patternSize = px * 7; // 定位图案总尺寸 7x7 // 绘制外层边框 (7x7) ctx.fillStyle = pdColor; drawRoundedRect(ctx, startX, startY, patternSize, patternSize, radius); // 绘制内层空心区域 (5x5) ctx.fillStyle = background; const innerStartX = startX + px; const innerStartY = startY + px; const innerSize = px * 5; const innerRadius = Math.max(0, radius - px); // 内层圆角适当减小 drawRoundedRect(ctx, innerStartX, innerStartY, innerSize, innerSize, innerRadius); // 绘制中心实心区域 (3x3) ctx.fillStyle = pdColor; const centerStartX = startX + px * 2; const centerStartY = startY + px * 2; const centerSize = px * 3; const centerRadius = Math.max(0, radius - px * 2); // 中心圆角适当减小 drawRoundedRect(ctx, centerStartX, centerStartY, centerSize, centerSize, centerRadius); } /** * 绘制二维码到Canvas上下文 * 主要的二维码绘制函数,处理不同平台的兼容性 * @param context Canvas上下文对象 * @param options 二维码配置选项 */ export function drawQrcode(context: CanvasContext, options: QrcodeOptions) { // 获取2D绘图上下文 const ctx = context.getContext("2d")!; if (isNull(ctx)) return; // 获取设备像素比,用于高清适配 const ratio = getRatio(); // App和小程序平台的画布初始化 // #ifdef APP || MP-WEIXIN const c1 = ctx.canvas; // 清空画布并设置尺寸 ctx.clearRect(0, 0, c1.offsetWidth, c1.offsetHeight); c1.width = c1.offsetWidth * ratio; c1.height = c1.offsetHeight * ratio; // #endif // #ifdef APP ctx.reset(); // #endif // H5平台的画布初始化 // #ifdef H5 const c2 = context as HTMLCanvasElement; c2.width = c2.offsetWidth * ratio; c2.height = c2.offsetHeight * ratio; // #endif // 缩放画布以适配高分屏 ctx.scale(ratio, ratio); // 生成二维码数据矩阵 const frame = generateFrame(options.text, options.ecc); const points = frame.frameBuffer; // 点阵数据 const width = frame.width; // 矩阵宽度 // 计算二维码内容区域大小(减去四周的padding) const contentSize = options.size - options.padding * 2; // 计算每个数据点的实际像素大小 const px = contentSize / width; // 二维码内容的起始位置(考虑padding) const offsetX = options.padding; const offsetY = options.padding; // 绘制整个画布背景 ctx.fillStyle = options.background; ctx.fillRect(0, 0, options.size, options.size); /** * 判断坐标点是否在定位图案区域内 * 二维码三个角上的定位图案是7x7的方块 * @param i 横坐标 * @param j 纵坐标 * @param width 二维码宽度 * @returns 是否是定位点 */ function isPositionDetectionPattern(i: number, j: number, width: number): boolean { // 判断三个角的定位图案(7x7) if (i < 7 && j < 7) return true; // 左上角 if (i > width - 8 && j < 7) return true; // 右上角 if (i < 7 && j > width - 8) return true; // 左下角 return false; } /** * 判断坐标点是否在Logo区域内(包含缓冲区) * @param i 横坐标 * @param j 纵坐标 * @param width 二维码宽度 * @param logoSize logo尺寸(像素) * @param px 单个数据点像素大小 * @returns 是否在logo区域内 */ function isInLogoArea( i: number, j: number, width: number, logoSize: number, px: number ): boolean { if (logoSize <= 0) return false; // 计算logo在矩阵中占用的点数 const logoPoints = Math.ceil(logoSize / px); // 添加缓冲区,logo周围额外空出1个点的距离 const buffer = 1; const totalLogoPoints = logoPoints + buffer * 2; // 计算logo区域在矩阵中的中心位置 const centerI = Math.floor(width / 2); const centerJ = Math.floor(width / 2); // 计算logo区域的边界 const halfSize = Math.floor(totalLogoPoints / 2); const minI = centerI - halfSize; const maxI = centerI + halfSize; const minJ = centerJ - halfSize; const maxJ = centerJ + halfSize; // 判断当前点是否在logo区域内 return i >= minI && i <= maxI && j >= minJ && j <= maxJ; } // 先绘制定位图案 const pdColor = options.pdColor ?? options.foreground; const radius = options.pdRadius; // 绘制三个定位图案 // 左上角 (0, 0) drawPositionPattern(ctx, offsetX, offsetY, px, pdColor, options.background, radius); // 右上角 (width-7, 0) drawPositionPattern( ctx, offsetX + (width - 7) * px, offsetY, px, pdColor, options.background, radius ); // 左下角 (0, width-7) drawPositionPattern( ctx, offsetX, offsetY + (width - 7) * px, px, pdColor, options.background, radius ); // 点的间距,用于圆形和小方块模式 const dot = px * 0.1; // 遍历绘制数据点(跳过定位图案区域和logo区域) for (let i = 0; i < width; i++) { for (let j = 0; j < width; j++) { if (points[j * width + i] > 0) { // 跳过定位图案区域 if (isPositionDetectionPattern(i, j, width)) { continue; } // 跳过logo区域(包含缓冲区) if (options.logo != "" && isInLogoArea(i, j, width, options.logoSize, px)) { continue; } // 绘制数据点 ctx.fillStyle = options.foreground; const x = offsetX + px * i; const y = offsetY + px * j; // 根据不同模式绘制数据点 switch (options.mode) { case "line": // 线条模式 - 绘制水平线条 ctx.fillRect(x, y, px, px / 2); break; case "circular": // 圆形模式 - 绘制圆点 ctx.beginPath(); const rx = x + px / 2 - dot; const ry = y + px / 2 - dot; ctx.arc(rx, ry, px / 2 - dot, 0, 2 * Math.PI); ctx.fill(); ctx.closePath(); break; case "rectSmall": // 小方块模式 - 绘制小一号的方块 ctx.fillRect(x + dot, y + dot, px - dot * 2, px - dot * 2); break; default: // 默认实心方块模式 ctx.fillRect(x, y, px, px); } } } } // 绘制 Logo if (options.logo != "") { let img: Image; // 微信小程序环境创建图片 // #ifdef MP-WEIXIN || APP-HARMONY img = context.createImage(); // #endif // 其他环境创建图片 // #ifndef MP-WEIXIN || APP-HARMONY img = new Image(options.logoSize, options.logoSize); // #endif // 设置图片源并在加载完成后绘制 img.src = options.logo; img.onload = () => { drawLogo(ctx, options, img); }; } } /** * 在二维码中心绘制Logo * 在二维码中心位置绘制Logo图片,并添加白色背景 * @param ctx Canvas上下文 * @param options 二维码配置 * @param img Logo图片对象 */ function drawLogo(ctx: CanvasRenderingContext2D, options: QrcodeOptions, img: Image) { ctx.save(); // 保存当前绘图状态 // 计算二维码内容区域的中心位置(考虑padding) const contentSize = options.size - options.padding * 2; const contentCenterX = options.padding + contentSize / 2; const contentCenterY = options.padding + contentSize / 2; // 添加额外的背景边距,与数据点避让区域保持一致 const backgroundPadding = 6; // 背景比logo大6px const backgroundSize = options.logoSize + backgroundPadding * 2; // 绘制白色背景作为Logo的底色(稍大于logo) ctx.fillStyle = "#fff"; const backgroundX = contentCenterX - backgroundSize / 2; const backgroundY = contentCenterY - backgroundSize / 2; ctx.fillRect(backgroundX, backgroundY, backgroundSize, backgroundSize); // 获取图片信息后绘制Logo uni.getImageInfo({ src: options.logo, success: (imgInfo) => { // 计算logo的精确位置 const logoX = contentCenterX - options.logoSize / 2; const logoY = contentCenterY - options.logoSize / 2; // 绘制Logo图片,四周留出3px边距 // #ifdef APP-HARMONY ctx.drawImage( img, logoX + 3, logoY + 3, options.logoSize - 6, options.logoSize - 6, 0, 0, imgInfo.width, imgInfo.height ); // #endif // #ifndef APP-HARMONY ctx.drawImage(img, logoX + 3, logoY + 3, options.logoSize - 6, options.logoSize - 6); // #endif ctx.restore(); // 恢复之前的绘图状态 }, fail(err) { console.error(err); } }); }