id-scanner-lib
Version:
Browser-based ID card, QR code, and face recognition scanner with liveness detection
171 lines (156 loc) • 4.28 kB
text/typescript
/**
* @file Canvas 对象池
* @description 提供 OffscreenCanvas/HTMLCanvasElement 的复用机制,减少内存分配和 GC 压力
* @module core/utils/canvas-pool
*/
/**
* Canvas 池条目
*/
export interface PooledCanvas {
canvas: OffscreenCanvas | HTMLCanvasElement;
width: number;
height: number;
inUse: boolean;
}
interface PoolEntry {
canvas: OffscreenCanvas | HTMLCanvasElement;
width: number;
height: number;
inUse: boolean;
lastUsed: number;
}
/**
* Canvas 对象池
*
* 复用 Canvas 元素,避免频繁创建和销毁导致的内存抖动
*
* @example
* ```typescript
* const pool = new CanvasPool();
* const acquired = pool.acquire(640, 480);
* // 使用 canvas 进行绘制...
* pool.release(acquired);
* ```
*/
export class CanvasPool {
private _pool: PoolEntry[] = [];
private _maxSize: number = 4;
private _useOffscreen: boolean;
/**
* 创建 Canvas 池
* @param maxSize 最大池大小
* @param useOffscreen 是否使用 OffscreenCanvas
*/
constructor(maxSize: number = 4, useOffscreen: boolean = true) {
this._maxSize = maxSize;
this._useOffscreen = useOffscreen && typeof OffscreenCanvas !== 'undefined';
}
/**
* 从池中获取 Canvas
*
* 1. 查找尺寸匹配的可用 canvas(允许 10% 误差)
* 2. 无匹配则创建新的
* 3. 超出 _maxSize 则回收最旧的 inUse=false 的
* 4. 返回 PooledCanvas,标记 inUse=true
*
* @param width 宽度
* @param height 高度
*/
acquire(width: number, height: number): PooledCanvas {
// 1. 查找尺寸匹配的可用 canvas(允许 10% 误差)
const matched = this._pool.find(p =>
!p.inUse &&
Math.abs(p.width - width) / width <= 0.1 &&
Math.abs(p.height - height) / height <= 0.1
);
if (matched) {
matched.inUse = true;
matched.lastUsed = Date.now();
return {
canvas: matched.canvas,
width: matched.width,
height: matched.height,
inUse: true,
};
}
// 2. 无匹配则创建新的
let canvas: OffscreenCanvas | HTMLCanvasElement;
if (this._useOffscreen) {
canvas = new OffscreenCanvas(width, height);
} else {
// 在非 OffscreenCanvas 环境(如 Node.js 测试)回退到 HTMLCanvasElement
if (typeof document !== 'undefined') {
canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
} else {
// 无法创建 canvas
throw new Error('CanvasPool: 无法创建 canvas (OffscreenCanvas 不可用,且 document 不存在)');
}
}
const entry: PoolEntry = {
canvas,
width,
height,
inUse: true,
lastUsed: Date.now(),
};
// 3. 超出 _maxSize 则回收最旧的 inUse=false 的
if (this._pool.length >= this._maxSize) {
const oldestIdx = this._findOldestAvailableIndex();
if (oldestIdx !== -1) {
this._pool.splice(oldestIdx, 1);
}
}
this._pool.push(entry);
return {
canvas: entry.canvas,
width: entry.width,
height: entry.height,
inUse: true,
};
}
/**
* 归还 Canvas 到池中
* @param pooled 要归还的 PooledCanvas
*/
release(pooled: PooledCanvas): void {
const entry = this._pool.find(p => p.canvas === pooled.canvas);
if (entry) {
entry.inUse = false;
entry.lastUsed = Date.now();
}
}
/**
* 清空池
*/
clear(): void {
this._pool = [];
}
/**
* 获取池统计信息
*/
getStats(): { total: number; inUse: number; available: number } {
return {
total: this._pool.length,
inUse: this._pool.filter(p => p.inUse).length,
available: this._pool.filter(p => !p.inUse).length,
};
}
/**
* 查找最旧的可用条目索引
*/
private _findOldestAvailableIndex(): number {
let oldestIdx = -1;
let oldestTime = Infinity;
for (let i = 0; i < this._pool.length; i++) {
if (!this._pool[i].inUse && this._pool[i].lastUsed < oldestTime) {
oldestTime = this._pool[i].lastUsed;
oldestIdx = i;
}
}
return oldestIdx;
}
}
/** 全局共享的 Canvas 池实例 */
export const globalCanvasPool = new CanvasPool();