/** * Utilitários de captura de canvas 3D — M9.3 * * Funções determinísticas (sem dependência de React) para: * - Extrair data URL de um canvas 2D/WebGL * - Redimensionar a imagem para uma largura máxima (preservando aspect ratio) * - Validar formato/qualidade * * Funciona com qualquer HTMLCanvasElement (incluindo R3F, Konva, D3). * Para canvas WebGL, o browser exige que `preserveDrawingBuffer: true` * seja passado ao `getContext('webgl2')` OU que a captura seja feita * imediatamente após o frame renderizado. Como R3F usa o loop de * animação do `useFrame`, a captura dentro do mesmo frame funciona. * * Dica: para WebGL, chamar `gl.flush()` ou renderizar um frame extra * antes de `toDataURL` evita canvas em branco. */ export interface CaptureOptions { /** Formato de saída. Padrão: 'png' */ format?: 'png' | 'jpeg' | 'webp'; /** Qualidade JPEG/WebP (0–1). Ignorado para PNG. Padrão: 0.92 */ quality?: number; /** Largura máxima do PNG final (px). 0 = sem redimensionamento */ maxWidth?: number; /** Altura máxima do PNG final (px). 0 = sem limite */ maxHeight?: number; } /** * Converte um HTMLCanvasElement em data URL. * * Para PNG, o segundo argumento é ignorado. Para JPEG/WebP, `quality` * controla a compressão (1 = sem perda, 0 = máxima compressão). */ export function canvasToDataURL( canvas: HTMLCanvasElement, format: 'png' | 'jpeg' | 'webp' = 'png', quality = 0.92, ): string { if (!canvas) throw new Error('canvas é null'); const mime = `image/${format}`; return canvas.toDataURL(mime, quality); } /** * Captura e opcionalmente redimensiona a imagem do canvas. * * Usa um canvas 2D temporário para escalar, preservando a proporção. * Retorna a data URL final pronta para嵌入 em `` ou PDF. */ export async function captureCanvasImage( canvas: HTMLCanvasElement, options: CaptureOptions = {}, ): Promise { const { format = 'png', quality = 0.92, maxWidth = 0, maxHeight = 0 } = options; const srcW = canvas.width; const srcH = canvas.height; let outW = srcW; let outH = srcH; if (maxWidth > 0 && maxHeight > 0) { const ratio = Math.min(maxWidth / srcW, maxHeight / srcH); outW = Math.round(srcW * ratio); outH = Math.round(srcH * ratio); } else if (maxWidth > 0) { outW = Math.min(maxWidth, srcW); outH = Math.round((outW / srcW) * srcH); } else if (maxHeight > 0) { outH = Math.min(maxHeight, srcH); outW = Math.round((outH / srcH) * srcW); } if (outW === srcW && outH === srcH) { return canvasToDataURL(canvas, format, quality); } const off = document.createElement('canvas'); off.width = outW; off.height = outH; const ctx = off.getContext('2d'); if (!ctx) throw new Error('Não foi possível criar contexto 2D'); ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; ctx.drawImage(canvas, 0, 0, outW, outH); return off.toDataURL(`image/${format}`, quality); } /** * Faz o download da imagem capturada. */ export function downloadImage(dataUrl: string, filename: string): void { const link = document.createElement('a'); link.setAttribute('href', dataUrl); link.setAttribute('download', filename); document.body.appendChild(link); link.click(); document.body.removeChild(link); } /** * Estima o tamanho da data URL em KB (útil para preview). */ export function estimateDataUrlSizeKB(dataUrl: string): number { const commaIdx = dataUrl.indexOf(','); if (commaIdx < 0) return 0; const base64 = dataUrl.slice(commaIdx + 1); return Math.round((base64.length * 3) / 4 / 1024); } /** * Converte data URL em Blob (útil para upload ou PDF embed). */ export async function dataURLtoBlob(dataUrl: string): Promise { const res = await fetch(dataUrl); return res.blob(); }