206 lines
6.1 KiB
TypeScript
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);
|
|
},
|
|
};
|
|
}
|