Files
image-studio/view/bottom-controls/FeatherControls.tsx
syntaxbullet 38565382c3 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.
2026-07-11 22:08:25 +02:00

47 lines
3.1 KiB
TypeScript

import { Feather } from "@phosphor-icons/react";
import { commandIds } from "@commands/ids";
import type { AppStore } from "@editor/store";
import type { FeatherSettings } from "@editor/tools";
import { BottomControlDivider } from "./Divider";
import { BottomControlSlider } from "./Slider";
import { bottomControlFieldClass, bottomControlIconSlotClass, bottomControlLabelClass, bottomControlMenuClass } from "./styles";
export function FeatherControls({ settings, editingMask, dispatch }: { settings: FeatherSettings; editingMask: boolean; dispatch: AppStore["dispatch"] }) {
return (
<div className={bottomControlMenuClass()}>
<span className={bottomControlIconSlotClass()} title="Feather mask edges"><Feather size={24} weight="regular" /></span>
<BottomControlDivider />
<span className="whitespace-nowrap text-xs font-medium text-white/65">{editingMask ? "Feather mask" : "Feather layer mask"}</span>
<BottomControlDivider />
<FeatherSlider label="Size" min={1} max={400} value={settings.size} onValueChange={(size) => dispatch(commandIds.toolSetFeatherSettings, { size })} />
<BottomControlDivider />
<FeatherSlider label="Radius" min={1} max={128} value={settings.radius} onValueChange={(radius) => dispatch(commandIds.toolSetFeatherSettings, { radius })} />
<BottomControlDivider />
<FeatherSlider label="Strength" min={1} max={100} value={settings.strength} suffix="%" onValueChange={(strength) => dispatch(commandIds.toolSetFeatherSettings, { strength })} />
<BottomControlDivider />
<FeatherSlider label="Smooth" min={0} max={100} value={settings.smoothing} suffix="%" onValueChange={(smoothing) => dispatch(commandIds.toolSetFeatherSettings, { smoothing })} />
<button type="button" className={toggleClass(settings.pressureSize)} aria-pressed={settings.pressureSize} title="Use pen pressure for feather brush size" onClick={() => dispatch(commandIds.toolSetFeatherSettings, { pressureSize: !settings.pressureSize })}>Pressure</button>
{editingMask ? (
<>
<BottomControlDivider />
<button type="button" className="rounded-md bg-sky-300 px-3 py-1.5 text-xs font-semibold text-slate-950 transition hover:bg-sky-200" onClick={() => dispatch(commandIds.toolExitMaskEdit, undefined)}>Done</button>
</>
) : null}
</div>
);
}
function FeatherSlider({ label, min, max, value, suffix = "", onValueChange }: { label: string; min: number; max: number; value: number; suffix?: string; onValueChange: (value: number) => void }) {
return (
<label className={bottomControlFieldClass()}>
<span className={bottomControlLabelClass()}>{label}</span>
<BottomControlSlider min={min} max={max} value={value} className="min-w-10 w-[clamp(2.5rem,8vw,6.5rem)] shrink" aria-label={`Feather ${label.toLowerCase()}`} onValueChange={onValueChange} />
<span className="w-10 text-right text-xs font-medium text-white/85">{Math.round(value)}{suffix}</span>
</label>
);
}
function toggleClass(active: boolean) {
return `rounded-md px-3 py-1.5 text-xs font-medium transition ${active ? "bg-sky-300 text-slate-950" : "bg-white/[0.06] text-white/70 hover:bg-white/10"}`;
}