/** * Canvas 平台适配器 * 封装微信小程序 Canvas API,为未来 Web 平台迁移做准备 */ type QueryHost = | WechatMiniprogram.Page.TrivialInstance | WechatMiniprogram.Component.TrivialInstance; type Canvas2DContextLike = WechatMiniprogram.RenderingContext & { scale: (x: number, y: number) => void; }; export interface CanvasInitResult { canvas: WechatMiniprogram.Canvas; ctx: Canvas2DContextLike; width: number; height: number; dpr: number; } /** * 与架构文档 ICanvasAdapter 对齐的小程序实现 */ export interface ICanvasAdapter { getCanvas(): WechatMiniprogram.Canvas; getContext(): Canvas2DContextLike; getSize(): { width: number; height: number; dpr: number }; setSize(width: number, height: number): void; loadImage(src: string): Promise; toDataURL(type?: 'png' | 'jpg', quality?: number): Promise; toBlob(): Promise; toTempFilePath(options?: { quality?: number; fileType?: 'png' | 'jpg'; }): Promise; } function createQuery(host?: QueryHost): WechatMiniprogram.SelectorQuery { if (host && typeof host.createSelectorQuery === 'function') { return host.createSelectorQuery(); } return wx.createSelectorQuery(); } function getDpr(): number { try { return Math.max(1, wx.getSystemInfoSync().pixelRatio || 1); } catch { return 1; } } function ensureAspectRatio(aspectRatio: number): number { if (!Number.isFinite(aspectRatio) || aspectRatio <= 0) return 1; return aspectRatio; } /** * 初始化 Canvas 节点 * @param selector Canvas 选择器 (如 '#canvasContent') * @param wrapperSelector 容器选择器 (如 '#canvasWrapper') * @param aspectRatio 宽高比 (width / height) * @param host 可选:页面/组件实例,组件内调用建议传入 this */ export function initCanvas( selector: string, wrapperSelector: string, aspectRatio: number, host?: QueryHost, ): Promise { return new Promise((resolve, reject) => { const ratio = ensureAspectRatio(aspectRatio); createQuery(host) .select(wrapperSelector) .boundingClientRect((rect) => { if (!rect || !rect.width) { reject(new Error('Canvas wrapper not found')); return; } const boxWidth = rect.width; const boxHeight = boxWidth / ratio; createQuery(host) .select(selector) .fields({ node: true, size: true }) .exec((res) => { if (!res || !res[0] || !res[0].node) { reject(new Error('Canvas node not found')); return; } const canvas = res[0].node as WechatMiniprogram.Canvas; const ctx = canvas.getContext( '2d', ) as Canvas2DContextLike; const dpr = getDpr(); canvas.width = boxWidth * dpr; canvas.height = boxHeight * dpr; ctx.scale(dpr, dpr); resolve({ canvas, ctx, width: boxWidth, height: boxHeight, dpr, }); }); }) .exec(); }); } /** * Canvas 导出为临时文件路径 */ export function canvasToTempFilePath( canvas: WechatMiniprogram.Canvas, options?: { quality?: number; fileType?: 'png' | 'jpg' }, ): Promise { return new Promise((resolve, reject) => { wx.canvasToTempFilePath({ canvas, fileType: options?.fileType || 'png', quality: options?.quality ?? 1, success: (res) => resolve(res.tempFilePath), fail: (err) => reject(err), }); }); } /** * 加载图片到 Canvas */ export function loadImage( canvas: WechatMiniprogram.Canvas, src: string, ): Promise { return new Promise((resolve, reject) => { const img = canvas.createImage(); img.onload = () => resolve(img); img.onerror = (err) => reject(err); img.src = src; }); } /** * 通过初始化结果创建微信 Canvas 适配器实例 */ export function createWxCanvasAdapter(init: CanvasInitResult): ICanvasAdapter { let width = init.width; let height = init.height; let dpr = init.dpr; let scaled = true; const applyScale = () => { if (!scaled) { init.ctx.scale(dpr, dpr); scaled = true; } }; return { getCanvas() { return init.canvas; }, getContext() { return init.ctx; }, getSize() { return { width, height, dpr }; }, setSize(nextWidth: number, nextHeight: number) { width = Math.max(1, nextWidth); height = Math.max(1, nextHeight); dpr = getDpr(); init.canvas.width = width * dpr; init.canvas.height = height * dpr; scaled = false; applyScale(); }, loadImage(src: string) { return loadImage(init.canvas, src); }, async toDataURL(type = 'png', quality = 1) { const maybeCanvas = init.canvas as unknown as { toDataURL?: (mimeType?: string, q?: number) => string; }; if (typeof maybeCanvas.toDataURL === 'function') { const mimeType = type === 'jpg' ? 'image/jpeg' : 'image/png'; return maybeCanvas.toDataURL(mimeType, quality); } throw new Error('toDataURL is not supported in current runtime'); }, async toBlob() { throw new Error('toBlob is not supported in WeChat Mini Program'); }, toTempFilePath(options) { return canvasToTempFilePath(init.canvas, options); }, }; }