feat: implement inpainting functionality with mask handling

- Added `createMaskedPixelReplacementSource` function to handle pixel replacement using inpainting.
- Introduced `buildInpaintBundle` to prepare inpainting data including mask generation and validation.
- Created utility functions for mask operations such as `applyMaskedContentModeToRgba`, `expandRectWithinBounds`, and others for mask manipulation.
- Developed tests for inpainting preparation and mask raster utilities to ensure functionality and correctness.
- Implemented mask raster operations including inversion, feathering, blurring, and more.
This commit is contained in:
syntaxbullet
2026-07-05 09:35:16 +02:00
parent 6e5d58a638
commit f5c610dac5
35 changed files with 2285 additions and 242 deletions

View File

@@ -520,7 +520,8 @@ function createMaskVisualizationProgram(gl: WebGL2RenderingContext): MaskVisuali
in vec2 v_texCoord;
out vec4 outColor;
void main() {
float maskAlpha = texture(u_mask, v_texCoord).a;
vec4 maskColor = texture(u_mask, v_texCoord);
float maskAlpha = maskColor.a * dot(maskColor.rgb, vec3(0.2126, 0.7152, 0.0722));
if (u_mode == 0) {
float value = step(0.5, maskAlpha);
outColor = vec4(value, value, value, 1.0);
@@ -642,7 +643,9 @@ function createMaskRevealPreviewProgram(gl: WebGL2RenderingContext) {
out vec4 outColor;
void main() {
vec4 color = texture(u_image, v_texCoord);
float hiddenMaskAlpha = 1.0 - texture(u_mask, v_maskTexCoord).a;
vec4 maskColor = texture(u_mask, v_maskTexCoord);
float maskAlpha = maskColor.a * dot(maskColor.rgb, vec3(0.2126, 0.7152, 0.0722));
float hiddenMaskAlpha = 1.0 - maskAlpha;
float previewAlpha = clamp(hiddenMaskAlpha * u_opacity, 0.0, 1.0);
outColor = vec4(color.rgb * previewAlpha, color.a * previewAlpha);
}`,
@@ -693,7 +696,8 @@ function createMaskedProgram(gl: WebGL2RenderingContext) {
out vec4 outColor;
void main() {
vec4 color = texture(u_image, v_texCoord);
float maskAlpha = texture(u_mask, v_maskTexCoord).a;
vec4 maskColor = texture(u_mask, v_maskTexCoord);
float maskAlpha = maskColor.a * dot(maskColor.rgb, vec3(0.2126, 0.7152, 0.0722));
float alpha = color.a * maskAlpha;
outColor = vec4(color.rgb * maskAlpha, alpha);
}`,

View File

@@ -1,7 +1,9 @@
import type { Asset } from "@core/asset";
import type { ImageDocument } from "@core/document";
import type { Rect } from "@core/geometry";
import type { Layer } from "@core/layer";
import { createDocumentReadIndex, forEachLayerBackToFront, resolveIndexedLayerBounds, type DocumentReadIndex } from "@editor/document-indexes";
import type { EditorState, MaskViewMode, ViewportState } from "@editor/state";
import type { EditorState, GenerationCandidate, MaskViewMode, ViewportState } from "@editor/state";
import { clearScreenRect } from "./clear-rect";
import type { ImageTextureRenderer } from "./image-textures";
import { documentRectToScreenRect } from "./screen-rect";
@@ -14,15 +16,21 @@ const maskRevealPreviewOpacity = 0.28;
export function renderLayers(context: WebGlRendererContext, document: ImageDocument, editor: EditorState, imageTextureRenderer: ImageTextureRenderer) {
const documentIndex = createDocumentReadIndex(document);
const generationCandidate = selectedGenerationCandidate(editor);
for (const artboard of document.artboards) {
if (!artboard.visible) continue;
const clipRect = documentRectToScreenRect(context.canvas, artboard.bounds, editor.viewport);
const maskLayerIds = documentIndex.maskLayerIdsByArtboardId.get(artboard.id) ?? emptyLayerIds;
forEachLayerBackToFront(artboard.layers, (layer) => renderLayer(context, documentIndex, editor, layer, imageTextureRenderer, clipRect, maskLayerIds));
if (generationCandidate?.placement.artboardId === artboard.id) renderGenerationCandidatePreview(context, editor, generationCandidate, imageTextureRenderer, clipRect);
}
}
export function generationCandidatePreviewAssets(editor: EditorState): Asset[] {
return editor.generation.candidates.map((candidate) => generationCandidateAsset(candidate));
}
function renderLayer(
context: WebGlRendererContext,
documentIndex: DocumentReadIndex,
@@ -102,6 +110,44 @@ function isIsolatedMaskView(mode: MaskViewMode) {
return mode === "blackWhite" || mode === "alpha" || mode === "overlay";
}
function renderGenerationCandidatePreview(
context: WebGlRendererContext,
editor: EditorState,
candidate: GenerationCandidate,
imageTextureRenderer: ImageTextureRenderer,
clipRect: ScreenRect,
) {
const rect = documentRectToScreenRect(context.canvas, generationCandidateBounds(candidate), editor.viewport);
imageTextureRenderer.render(generationCandidateAsset(candidate), rect, clipRect);
}
function selectedGenerationCandidate(editor: EditorState): GenerationCandidate | undefined {
return editor.generation.candidates.find((candidate) => candidate.id === editor.generation.selectedCandidateId) ?? editor.generation.candidates[0];
}
function generationCandidateAsset(candidate: GenerationCandidate): Asset {
return {
id: generationCandidateAssetId(candidate.id),
name: candidate.placement.layerName,
mimeType: candidate.mimeType,
source: candidate.source,
intrinsicSize: candidate.intrinsicSize,
};
}
function generationCandidateAssetId(candidateId: string) {
return `generation-candidate:${candidateId}`;
}
function generationCandidateBounds(candidate: GenerationCandidate): Rect {
return {
x: candidate.placement.transform.position.x,
y: candidate.placement.transform.position.y,
w: candidate.intrinsicSize.w * candidate.placement.transform.scale.x,
h: candidate.intrinsicSize.h * candidate.placement.transform.scale.y,
};
}
function assetWithBrushStrokePreview<TAsset extends ImageDocument["assets"][number] | undefined>(asset: TAsset, editor: EditorState): TAsset {
if (!asset || editor.brushStrokePreview?.assetId !== asset.id) return asset;
return { ...asset, source: editor.brushStrokePreview.source } as TAsset;

View File

@@ -4,7 +4,7 @@ import { renderArtboard } from "./artboard";
import { createBrushPreviewRenderer } from "./brush-preview";
import { createCheckerboardRenderer } from "./checkerboard";
import { createImageTextureRenderer } from "./image-textures";
import { renderLayers } from "./layers";
import { generationCandidatePreviewAssets, renderLayers } from "./layers";
import { renderSelectionOverlay } from "./selection";
import { renderTransformControls } from "./transform-controls";
import type { WebGlRendererContext } from "./types";
@@ -62,7 +62,7 @@ export function createRenderer(canvas: HTMLCanvasElement, backend: RendererBacke
for (const artboard of frame.document.artboards) {
if (artboard.visible) renderArtboard(rendererContext, artboard, frame.editor.viewport, checkerboardRenderer);
}
imageTextureRenderer.syncAssets(frame.document.assets);
imageTextureRenderer.syncAssets([...frame.document.assets, ...generationCandidatePreviewAssets(frame.editor)]);
renderLayers(rendererContext, frame.document, frame.editor, imageTextureRenderer);
if (!frame.editor.maskEdit) {