feat: add feather brush tool with adjustable settings and blending functionality

- Implemented feather brush tool in the brushRaster module, allowing for feathered edges in brush strokes.
- Added new FeatherControls component for UI adjustments of feather settings including size, radius, strength, and smoothing.
- Updated brush preview logic to accommodate feather tool alongside existing brush and eraser tools.
- Enhanced layer rendering to support feather mask previews and interactions.
- Introduced blending logic for feathered strokes to mix blurred mask values with original pixels.
- Added unit tests for feather blending functionality and tool keybindings.
- Updated cursor handling to reflect feather tool usage.
This commit is contained in:
syntaxbullet
2026-07-11 22:08:25 +02:00
parent 95043dfbdd
commit 38565382c3
27 changed files with 491 additions and 65 deletions

View File

@@ -2,8 +2,9 @@ import { useMemo } from "react";
import type { AppStore } from "@editor/store";
import type { ImageDocument } from "@core/document";
import type { GenerationState, MaskViewMode, SelectionState, ViewportState } from "@editor/state";
import type { BrushSettings, ChromaKeySettings, GenerateSettings, MagicWandSettings, OperationId, ToolId } from "@editor/tools";
import type { BrushSettings, ChromaKeySettings, FeatherSettings, GenerateSettings, MagicWandSettings, OperationId, ToolId } from "@editor/tools";
import { BrushControls } from "./bottom-controls/BrushControls";
import { FeatherControls } from "./bottom-controls/FeatherControls";
import { ChromaKeyControls } from "./bottom-controls/ChromaKeyControls";
import { MagicWandControls } from "./bottom-controls/MagicWandControls";
import { GenerateActionControls } from "./bottom-controls/GenerateActionControls";
@@ -26,6 +27,7 @@ export type BottomControlsIslandProps = {
activeTool: ToolId;
operation?: OperationId;
brushSettings: BrushSettings;
featherSettings: FeatherSettings;
generateSettings: GenerateSettings;
generation: GenerationState;
chromaKeySettings: ChromaKeySettings;
@@ -41,7 +43,7 @@ export type BottomControlsIslandProps = {
documentActions: DocumentActions;
};
export function BottomControlsIsland({ document, selection, viewport, visible, action, activeTool, operation, brushSettings, generateSettings, generation, chromaKeySettings, magicWandSettings, editingMask = false, maskKind, maskViewMode = "composite", transformBounds, transformTarget, brushHint, dispatch, generationWorkflow, documentActions }: BottomControlsIslandProps) {
export function BottomControlsIsland({ document, selection, viewport, visible, action, activeTool, operation, brushSettings, featherSettings, generateSettings, generation, chromaKeySettings, magicWandSettings, editingMask = false, maskKind, maskViewMode = "composite", transformBounds, transformTarget, brushHint, dispatch, generationWorkflow, documentActions }: BottomControlsIslandProps) {
const documentIndex = useMemo(() => createDocumentReadIndex(document), [document]);
const selectedLayerInfo = selection.layerIds.length === 1 && selection.layerIds[0] ? documentIndex.layerInfoById.get(selection.layerIds[0]) : undefined;
const zoomPercent = Math.round(viewport.zoom * 100);
@@ -59,10 +61,12 @@ export function BottomControlsIsland({ document, selection, viewport, visible, a
<GenerateActionControls settings={generateSettings} generation={generation} dispatch={dispatch} workflow={generationWorkflow} generate={documentActions.generate} />
) : operation === "chromaKey" ? (
<ChromaKeyControls document={document} selection={selection} settings={chromaKeySettings} dispatch={dispatch} />
) : (activeTool === "brush" || activeTool === "eraser") && brushHint ? (
) : (activeTool === "brush" || activeTool === "eraser" || activeTool === "feather") && brushHint ? (
<BrushHint tool={activeTool} hint={brushHint} />
) : activeTool === "brush" || activeTool === "eraser" ? (
<BrushControls tool={activeTool} settings={brushSettings} editingMask={editingMask} maskKind={maskKind} maskViewMode={maskViewMode} dispatch={dispatch} />
) : activeTool === "feather" ? (
<FeatherControls settings={featherSettings} editingMask={editingMask} dispatch={dispatch} />
) : activeTool === "magicWand" ? (
<MagicWandControls settings={magicWandSettings} dispatch={dispatch} />
) : activeTool === "semanticSelect" ? (
@@ -80,7 +84,7 @@ export function BottomControlsIsland({ document, selection, viewport, visible, a
);
}
function BrushHint({ tool, hint }: { tool: "brush" | "eraser"; hint: string }) {
function BrushHint({ tool, hint }: { tool: "brush" | "eraser" | "feather"; hint: string }) {
return (
<div className="flex items-center gap-3 px-4 text-center">
<span className="rounded bg-white/[0.06] px-2 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.12em] text-white/45">{tool}</span>