Files
image-studio/platform/browser/brushRaster.ts
syntaxbullet ff762b8f17 feat: add inpaint region functionality and related tools
- Enhanced cursor behavior for new tools: semantic select, mask lasso, and mask rectangle.
- Updated mask edit state to include mask asset ID and kind.
- Implemented inpaint region commands for adding, applying, and removing inpaint regions.
- Introduced new operations for lasso and semantic selection tools.
- Created UI components for candidate review and inpaint region management.
- Added tests for inpaint region commands to ensure functionality.
- Updated various components to support new inpaint features and improve user experience.
2026-07-11 16:41:22 +02:00

30 lines
2.9 KiB
TypeScript

export type BrushSurface = {
readonly width: number;
readonly height: number;
readonly ready: Promise<boolean>;
readonly resource: object;
};
type InternalSurface = BrushSurface & { canvas: HTMLCanvasElement; context: CanvasRenderingContext2D };
export function createBrushSurface(width: number, height: number, source: string): BrushSurface | undefined {
const canvas = document.createElement("canvas"); canvas.width = Math.max(1, Math.round(width)); canvas.height = Math.max(1, Math.round(height));
const context = canvas.getContext("2d"); if (!context) return undefined;
const surface = { width: canvas.width, height: canvas.height, canvas, context, resource: {}, ready: Promise.resolve(false) } as InternalSurface;
(surface as { ready: Promise<boolean> }).ready = loadImage(source).then((image) => { context.clearRect(0, 0, canvas.width, canvas.height); context.drawImage(image, 0, 0, canvas.width, canvas.height); return true; }).catch(() => false);
return surface;
}
export function drawBrushSegment(surface: BrushSurface, options: { from: { x: number; y: number }; to: { x: number; y: number }; color: string; size: number; hardness: number; opacity: number; flow: number; mode: "brush" | "eraser" }) {
const context = internal(surface).context; const hardness = Math.max(0, Math.min(100, options.hardness)) / 100;
context.save(); context.globalAlpha = Math.max(0, Math.min(1, options.opacity / 100)) * Math.max(0.01, Math.min(1, options.flow / 100)); context.globalCompositeOperation = options.mode === "eraser" ? "destination-out" : "source-over"; context.strokeStyle = options.color; context.shadowColor = options.mode === "eraser" ? "rgba(0,0,0,1)" : options.color; context.shadowBlur = (1 - hardness) * options.size; context.lineWidth = options.size; context.lineCap = "round"; context.lineJoin = "round"; context.beginPath(); context.moveTo(options.from.x, options.from.y); context.lineTo(options.to.x, options.to.y); context.stroke(); context.restore();
}
export function brushSurfaceDataUrl(surface: BrushSurface) { try { return internal(surface).canvas.toDataURL("image/png"); } catch { return undefined; } }
export function brushSurfaceObjectUrl(surface: BrushSurface) { return new Promise<string | undefined>((resolve) => internal(surface).canvas.toBlob((blob) => resolve(blob ? URL.createObjectURL(blob) : undefined), "image/png")); }
export function releaseObjectUrl(source?: string) { if (source) URL.revokeObjectURL(source); }
export function scheduleFrame(callback: () => void) { return requestAnimationFrame(callback); }
export function cancelFrame(id: number) { cancelAnimationFrame(id); }
function internal(surface: BrushSurface) { return surface as InternalSurface; }
function loadImage(source: string) { return new Promise<HTMLImageElement>((resolve, reject) => { const image = new Image(); image.onload = () => resolve(image); image.onerror = () => reject(new Error("Failed to load raster layer")); image.src = source; }); }