- 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.
47 lines
3.1 KiB
TypeScript
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"}`;
|
|
}
|