背景:
在使用某些支持webgl的圖形庫(eg:PIXI.js,fabric.js)場景中,如果加載的紋理超過webgl可處理的最大紋理限制,會導致渲染的紋理缺失,甚至無法顯示。
方案
實現圖片自動壓縮算法,自動獲取 webgl 支持的最大紋理大小,設置一個壓縮比率,循環壓縮圖片的像素,直到小于最大紋理限制。
返回 canvas,方便第三方庫繼續處理,如果需要 image,可自行調用canvas方法轉換成image。
注意:如果不需要像素處理,刪除處理像素相關的代碼即可。
vim imageHelp.ts
/**** @param imgStr image base64 | url* @param ratio 壓縮比率* @returns 壓縮后的canvas對象,獲取image 使用 canvas.convertToBlob()*/
export async function compressImage(options: { imgStr: string; ratio?: number; negate?: 0 | 1 }) {const { imgStr, ratio = 0.5, negate = 0 } = optionsconst isInverted = negate == 1 // 底色是否反黑色// 2. 添加錯誤處理if (!imgStr) throw new Error('Invalid image source')if (ratio <= 0 || ratio > 1) throw new Error('Invalid compression ratio')try {const img = await loadImage(imgStr)const maxTextureSize = getMaxTextureSize()// 5. 優化尺寸計算邏輯const { width, height } = calculateTargetSize(img, ratio, maxTextureSize)// 6. 使用 OffscreenCanvas 提升性能const { canvas, ctx } = createCanvasContext(width, height)ctx.drawImage(img, 0, 0, width, height)// 7. 添加 Worker 終止邏輯防止內存泄漏const worker = new CanvasWorker()const cleanup = () => worker.terminate()return await new Promise<{ canvas: OffscreenCanvas; width: number; height: number }>((resolve, reject) => {worker.onmessage = (e) => {try {// imageData.data.buffer 所有權已轉移,無法更新數據 imageData.data.buffer// 重新構建 ImageData 對象const updatedImageData = new ImageData(new Uint8ClampedArray(e.data.buffer),canvas.width,canvas.height)// 將修改后的圖像數據放回畫布ctx.putImageData(updatedImageData, 0, 0)cleanup()if (width > maxTextureSize || height > maxTextureSize) {// 壓縮后的圖像需要縮放,保持原圖像的視覺大小ctx.scale(1 / ratio, 1 / ratio)}resolve({canvas,width,height,})} catch (error) {cleanup()reject(error)}}worker.onerror = (error) => {cleanup()reject(error)}// 8. 優化數據傳輸const imageData = ctx.getImageData(0, 0, width, height)// 傳遞圖像數據給worker,第二個參數是一個Transferable對象,可以將數據從一個線程傳遞到另一個線程,而不是復制worker.postMessage({buffer: imageData.data.buffer,targetColor: isInverted ? [0, 0, 0, 255] : [255, 255, 255, 255],tolerance: 50, // 添加顏色容差參數},[imageData.data.buffer])})} catch (error) {throw new Error(`Image processing failed: ${error?.message}`)}
}
function getMaxTextureSize(): number {const gl = document.createElement('canvas').getContext('webgl')return gl ? gl.getParameter(gl.MAX_TEXTURE_SIZE) : 4096 // 默認值
}function calculateTargetSize(img: { width: number; height: number },ratio: number,maxSize: number
) {let width = img.widthlet height = img.height// 壓縮圖像像素while (width > maxSize || height > maxSize) {width *= ratioheight *= ratio}return {width,height,}
}function createCanvasContext(width: number, height: number) {const canvas = new OffscreenCanvas(width, height)canvas.width = widthcanvas.height = heightreturn {canvas,ctx: canvas.getContext('2d')!,}
}
vim canvas.worker.ts
self.onmessage = (event) => {const { buffer, targetColor, isInverted } = event.data// 轉換為 Uint8ClampedArray 進行像素級別的處理const data = new Uint8ClampedArray(buffer);// 遍歷每個像素for(let i = 0; i < data.length; i += 4) {const r = data[i]; // 紅色通道const g = data[i + 1]; // 綠色通道const b = data[i + 2]; // 藍色通道// 檢查該像素是否為需要刪除的顏色if(r === targetColor[0] && g === targetColor[1] && b === targetColor[2]) {// 將黑色像素設置為透明data[i + 3] = 0; // Alpha通道設置為0}// 反轉顏色if(isInverted) {data[i] = 255 - data[i]data[i + 1] = 255 - data[i + 1]data[i + 2] = 255 - data[i + 2]}}self.postMessage(data)
}