Files
doodle-mini/miniprogram/platform/canvas-adapter.ts
T
2026-03-27 17:31:52 +08:00

206 lines
6.1 KiB
TypeScript

/**
* 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<WechatMiniprogram.Image>;
toDataURL(type?: 'png' | 'jpg', quality?: number): Promise<string>;
toBlob(): Promise<unknown>;
toTempFilePath(options?: {
quality?: number;
fileType?: 'png' | 'jpg';
}): Promise<string>;
}
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<CanvasInitResult> {
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<string> {
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<WechatMiniprogram.Image> {
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);
},
};
}