export interface PooledCanvas { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D; inUse: boolean; lastUsed: number; } export class CanvasPool { private static pool: PooledCanvas[] = []; private static maxPoolSize = 20; private static readonly MIN_CANVAS_SIZE = 256; private static readonly MAX_CANVAS_SIZE = 4096; public static acquire(width: number, height: number): PooledCanvas { // Try to find a suitable canvas from the pool const suitable = this.pool.find( (item) => !item.inUse && item.canvas.width >= width && item.canvas.height >= height && item.canvas.width <= width * 1.5 && item.canvas.height <= height * 1.5 ); if (suitable) { suitable.inUse = true; suitable.lastUsed = Date.now(); // Clear and resize if needed suitable.canvas.width = width; suitable.canvas.height = height; suitable.ctx.clearRect(0, 0, width, height); return suitable; } // Create new canvas if pool not full if (this.pool.length < this.maxPoolSize) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d', { alpha: true, desynchronized: true, }) as CanvasRenderingContext2D; canvas.width = Math.min(Math.max(width, this.MIN_CANVAS_SIZE), this.MAX_CANVAS_SIZE); canvas.height = Math.min(Math.max(height, this.MIN_CANVAS_SIZE), this.MAX_CANVAS_SIZE); const pooledCanvas: PooledCanvas = { canvas, ctx, inUse: true, lastUsed: Date.now(), }; this.pool.push(pooledCanvas); return pooledCanvas; } // Evict and reuse least recently used canvas const lru = this.pool .filter((item) => !item.inUse) .sort((a, b) => a.lastUsed - b.lastUsed)[0]; if (lru) { lru.canvas.width = width; lru.canvas.height = height; lru.ctx.clearRect(0, 0, width, height); lru.inUse = true; lru.lastUsed = Date.now(); return lru; } // Fallback: create temporary canvas (shouldn't normally happen) const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; canvas.width = width; canvas.height = height; return { canvas, ctx, inUse: true, lastUsed: Date.now(), }; } public static release(pooledCanvas: PooledCanvas) { if (this.pool.includes(pooledCanvas)) { pooledCanvas.inUse = false; // Clear canvas to free memory pooledCanvas.ctx.clearRect(0, 0, pooledCanvas.canvas.width, pooledCanvas.canvas.height); } } public static releaseAll() { for (const item of this.pool) { item.inUse = false; item.ctx.clearRect(0, 0, item.canvas.width, item.canvas.height); } } public static destroy() { for (const item of this.pool) { item.canvas.width = 0; item.canvas.height = 0; } this.pool = []; } public static getStats() { return { poolSize: this.pool.length, maxPoolSize: this.maxPoolSize, inUse: this.pool.filter((item) => item.inUse).length, available: this.pool.filter((item) => !item.inUse).length, }; } public static adjustPoolSize(newSize: number) { if (newSize < this.pool.length) { // Remove excess canvases const toRemove = this.pool.length - newSize; const removed = this.pool .filter((item) => !item.inUse) .slice(0, toRemove); for (const item of removed) { const index = this.pool.indexOf(item); if (index > -1) { this.pool.splice(index, 1); } } } this.maxPoolSize = newSize; } }