refactor(*): 代码质量 - 优化前后端实现并完善协议与画布验证
This commit is contained in:
54
web/src/lib/canvas/image-mask-document.ts
Normal file
54
web/src/lib/canvas/image-mask-document.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
export type MaskPoint = readonly [number, number];
|
||||
export type MaskStroke = { diameter: number; erase: boolean; points: MaskPoint[] };
|
||||
|
||||
/** A stroke document is the source of truth; previews and exported masks are separate renders. */
|
||||
export function paintMaskStrokes(canvas: HTMLCanvasElement, strokes: readonly MaskStroke[], color: string): CanvasRenderingContext2D {
|
||||
const brush = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!brush) throw new Error("无法创建遮罩画布");
|
||||
brush.clearRect(0, 0, canvas.width, canvas.height);
|
||||
brush.lineCap = "round";
|
||||
brush.lineJoin = "round";
|
||||
brush.fillStyle = brush.strokeStyle = color;
|
||||
for (const stroke of strokes) {
|
||||
const first = stroke.points[0];
|
||||
if (!first) continue;
|
||||
brush.globalCompositeOperation = stroke.erase ? "destination-out" : "source-over";
|
||||
brush.lineWidth = stroke.diameter;
|
||||
const path = new Path2D();
|
||||
if (stroke.points.length === 1) {
|
||||
path.arc(first[0], first[1], stroke.diameter / 2, 0, Math.PI * 2);
|
||||
brush.fill(path);
|
||||
} else {
|
||||
path.moveTo(first[0], first[1]);
|
||||
for (const [x, y] of stroke.points.slice(1)) path.lineTo(x, y);
|
||||
brush.stroke(path);
|
||||
}
|
||||
}
|
||||
brush.globalCompositeOperation = "source-over";
|
||||
return brush;
|
||||
}
|
||||
|
||||
export function invertSelectionAlpha(selection: Uint8ClampedArray): Uint8ClampedArray {
|
||||
if (selection.length % 4 !== 0) throw new RangeError("遮罩像素数据无效");
|
||||
let selected = false;
|
||||
const result = new Uint8ClampedArray(selection.length).fill(255);
|
||||
for (let pixel = 0; pixel < selection.length / 4; pixel++) {
|
||||
if (selection[pixel * 4 + 3] === 0) continue;
|
||||
selected = true;
|
||||
result[pixel * 4 + 3] = 0;
|
||||
}
|
||||
if (!selected) throw new Error("请先涂抹需要修改的区域");
|
||||
return result;
|
||||
}
|
||||
|
||||
export function exportEditMask(width: number, height: number, strokes: readonly MaskStroke[]): string {
|
||||
const surface = document.createElement("canvas");
|
||||
Object.assign(surface, { width, height });
|
||||
const brush = paintMaskStrokes(surface, strokes, "black");
|
||||
const pixels = brush.getImageData(0, 0, width, height);
|
||||
pixels.data.set(invertSelectionAlpha(pixels.data));
|
||||
brush.putImageData(pixels, 0, 0);
|
||||
const png = surface.toDataURL("image/png");
|
||||
if (!png.startsWith("data:image/png")) throw new Error("遮罩导出失败");
|
||||
return png;
|
||||
}
|
||||
Reference in New Issue
Block a user