071faa482c
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
148 lines
4.0 KiB
TypeScript
148 lines
4.0 KiB
TypeScript
import type { UiAssetExtractionMark } from './ImageCanvasUiAssetExtractionModel';
|
|
|
|
function assertCanvasSupport() {
|
|
if (typeof document === 'undefined') {
|
|
throw new Error('当前环境不支持图片编辑');
|
|
}
|
|
}
|
|
|
|
function loadImageElement(source: string) {
|
|
return new Promise<HTMLImageElement>((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');
|
|
}
|