55 lines
2.4 KiB
TypeScript
55 lines
2.4 KiB
TypeScript
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;
|
|
}
|