- 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.
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
import { Cursor, Eraser, Feather, Hand, PaintBrush, DropHalf, MagicWand, Sparkle, Polygon, Rectangle, Selection } from "@phosphor-icons/react";
|
|
import { commandIds } from "@commands/ids";
|
|
import type { AppStore } from "@editor/store";
|
|
import type { InteractionMode, OperationId, ToolId } from "@editor/tools";
|
|
import { availableOperationIds, availableToolIds } from "@editor/tools";
|
|
import { isOperationWorkspacePanel, type WorkspacePanel } from "@editor/state";
|
|
import { labelForTool } from "./toolLabels";
|
|
|
|
export type ToolOverlayProps = {
|
|
activeTool: ToolId;
|
|
interactionMode: InteractionMode;
|
|
panel: WorkspacePanel;
|
|
inpaintMaskEditing: boolean;
|
|
dispatch: AppStore["dispatch"];
|
|
};
|
|
|
|
export function ToolOverlay({ activeTool, interactionMode, panel, inpaintMaskEditing, dispatch }: ToolOverlayProps) {
|
|
return (
|
|
<nav
|
|
aria-label="Tools"
|
|
className="app-surface pointer-events-auto flex w-11 flex-col items-center gap-1 rounded-xl p-1 text-white"
|
|
>
|
|
{availableToolIds.filter((tool) => inpaintMaskEditing || (tool !== "semanticSelect" && tool !== "maskLasso" && tool !== "maskRectangle")).map((tool) => {
|
|
const active = !isOperationWorkspacePanel(panel) && isToolHighlighted(tool, activeTool, interactionMode);
|
|
const Icon = iconForTool(tool);
|
|
|
|
return (
|
|
<button
|
|
key={tool}
|
|
type="button"
|
|
aria-label={labelForTool(tool)}
|
|
aria-pressed={active}
|
|
title={labelForTool(tool)}
|
|
className={buttonClass(active)}
|
|
onClick={() => dispatch(commandIds.toolSetActive, { tool })}
|
|
>
|
|
<Icon size={18} weight={active ? "fill" : "regular"} />
|
|
</button>
|
|
);
|
|
})}
|
|
<div className="my-0.5 h-px w-6 bg-white/10" aria-hidden="true" />
|
|
{availableOperationIds.map((operation) => {
|
|
const active = panel === operation;
|
|
const Icon = iconForOperation(operation);
|
|
return (
|
|
<button
|
|
key={operation}
|
|
type="button"
|
|
aria-label={labelForOperation(operation)}
|
|
aria-pressed={active}
|
|
title={labelForOperation(operation)}
|
|
className={buttonClass(active)}
|
|
onClick={() => dispatch(commandIds.workspaceSetPanel, { panel: active ? "none" : operation })}
|
|
>
|
|
<Icon size={18} weight={active ? "fill" : "regular"} />
|
|
</button>
|
|
);
|
|
})}
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
function iconForTool(tool: ToolId) {
|
|
switch (tool) {
|
|
case "brush":
|
|
return PaintBrush;
|
|
case "eraser":
|
|
return Eraser;
|
|
case "feather":
|
|
return Feather;
|
|
case "magicWand":
|
|
return MagicWand;
|
|
case "semanticSelect":
|
|
return Selection;
|
|
case "maskLasso":
|
|
return Polygon;
|
|
case "maskRectangle":
|
|
return Rectangle;
|
|
case "pan":
|
|
return Hand;
|
|
case "select":
|
|
return Cursor;
|
|
}
|
|
}
|
|
|
|
function iconForOperation(operation: OperationId) {
|
|
return operation === "generate" ? Sparkle : DropHalf;
|
|
}
|
|
|
|
function labelForOperation(operation: OperationId) {
|
|
return operation === "generate" ? "Generate" : "Chroma key";
|
|
}
|
|
|
|
function isToolHighlighted(tool: ToolId, activeTool: ToolId, interactionMode: InteractionMode) {
|
|
if (interactionMode.type === "temporary-pan") return tool === "pan";
|
|
return activeTool === tool;
|
|
}
|
|
|
|
function buttonClass(active: boolean) {
|
|
const base = "inline-flex size-9 items-center justify-center rounded-lg text-xs font-medium transition focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/50";
|
|
return active ? `${base} bg-sky-300 text-slate-950` : `${base} text-white/55 hover:bg-white/[0.07] hover:text-white`;
|
|
}
|