import type { UiAssetExtractionMark } from './ImageCanvasUiAssetExtractionModel'; function assertCanvasSupport() { if (typeof document === 'undefined') { throw new Error('当前环境不支持图片编辑'); } } function loadImageElement(source: string) { return new Promise((resolve, reject) => { const image = new Image(); image.crossOrigin = 'anonymous'; image.onload = () => resolve(image); image.onerror = () => reject(new Error('图片读取失败')); image.src = source; }); } function createRasterCanvas(width: number, height: number) { assertCanvasSupport(); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const context = canvas.getContext('2d'); if (!context) { throw new Error('当前浏览器不支持图片编辑'); } return { canvas, context }; } function resolveImageSize(image: HTMLImageElement) { return { width: Math.max(1, image.naturalWidth || image.width || 1), height: Math.max(1, image.naturalHeight || image.height || 1), }; } function prepareRedSelectionStroke(context: CanvasRenderingContext2D) { context.strokeStyle = '#ff2b2b'; context.lineWidth = 6; context.lineCap = 'round'; context.lineJoin = 'round'; } function resolveMarkLabelPoint(mark: UiAssetExtractionMark) { if (mark.tool === 'brush') { return mark.points[0] ?? { x: 0, y: 0 }; } return { x: Math.min(mark.x, mark.x + mark.width), y: Math.min(mark.y, mark.y + mark.height), }; } function drawRedSelectionIndexLabel( context: CanvasRenderingContext2D, mark: UiAssetExtractionMark, index: number, ) { const point = resolveMarkLabelPoint(mark); const radius = 18; const x = Math.max( radius + 2, Math.min(point.x, context.canvas.width - radius - 2), ); const y = Math.max( radius + 2, Math.min(point.y, context.canvas.height - radius - 2), ); context.save(); context.fillStyle = '#ff2b2b'; context.strokeStyle = '#ffffff'; context.lineWidth = 4; context.beginPath(); context.arc(x, y, radius, 0, Math.PI * 2); context.fill(); context.stroke(); context.fillStyle = '#ffffff'; context.font = '900 24px sans-serif'; context.textAlign = 'center'; context.textBaseline = 'middle'; context.fillText(String(index + 1), x, y + 1); context.restore(); } export async function renderUiDesignAssetExtractionMarkedImage({ source, marks, showIndexLabels = false, }: { source: string; marks: UiAssetExtractionMark[]; showIndexLabels?: boolean; }) { if (!marks.length) { throw new Error('请至少框选一个素材'); } const image = await loadImageElement(source); const { width, height } = resolveImageSize(image); const { canvas, context } = createRasterCanvas(width, height); context.drawImage(image, 0, 0, width, height); prepareRedSelectionStroke(context); for (const mark of marks) { context.beginPath(); if (mark.tool === 'rect') { context.rect( Math.min(mark.x, mark.x + mark.width), Math.min(mark.y, mark.y + mark.height), Math.abs(mark.width), Math.abs(mark.height), ); } else if (mark.tool === 'ellipse') { const width = Math.abs(mark.width); const height = Math.abs(mark.height); context.ellipse( Math.min(mark.x, mark.x + mark.width) + width / 2, Math.min(mark.y, mark.y + mark.height) + height / 2, width / 2, height / 2, 0, 0, Math.PI * 2, ); } else if (mark.tool === 'brush') { const [firstPoint, ...restPoints] = mark.points; if (!firstPoint) { continue; } context.moveTo(firstPoint.x, firstPoint.y); restPoints.forEach((point) => { context.lineTo(point.x, point.y); }); context.stroke(); continue; } context.stroke(); } if (showIndexLabels) { marks.forEach((mark, index) => drawRedSelectionIndexLabel(context, mark, index), ); } return canvas.toDataURL('image/png'); }