Files
image-studio/view/bottom-controls/BrushControls.tsx

105 lines
4.5 KiB
TypeScript

import { Eraser, PaintBrush } from "@phosphor-icons/react";
import { commandIds } from "@commands/ids";
import type { AppStore } from "@editor/store";
import type { MaskViewMode } from "@editor/state";
import type { BrushSettings, ToolId } from "@editor/tools";
import { BottomControlColorPicker } from "./ColorPicker";
import { BottomControlDivider } from "./Divider";
import { BottomControlSelectMenu, type BottomControlSelectOption } from "./SelectMenu";
import { BottomControlSlider } from "./Slider";
import { bottomControlFieldClass, bottomControlIconSlotClass, bottomControlLabelClass, bottomControlMenuClass } from "./styles";
export type BrushControlsProps = {
tool: Extract<ToolId, "brush" | "eraser">;
settings: BrushSettings;
editingMask?: boolean;
maskViewMode?: MaskViewMode;
dispatch: AppStore["dispatch"];
};
const maskViewModeOptions = [
{ value: "composite", label: "Image" },
{ value: "blackWhite", label: "B/W" },
{ value: "alpha", label: "Alpha" },
{ value: "overlay", label: "Overlay" },
] satisfies readonly BottomControlSelectOption<MaskViewMode>[];
export function BrushControls({ tool, settings, editingMask = false, maskViewMode = "composite", dispatch }: BrushControlsProps) {
return (
<div className={bottomControlMenuClass()}>
<span className={bottomControlIconSlotClass()} title={editingMask ? `Mask ${tool === "eraser" ? "hide" : "reveal"}` : tool === "eraser" ? "Eraser" : "Brush"}>
{tool === "eraser" ? <Eraser size={24} weight="regular" /> : <PaintBrush size={24} weight="regular" />}
</span>
<BottomControlDivider />
{editingMask ? (
<>
<button type="button" className={maskModeButtonClass(tool === "brush")} aria-pressed={tool === "brush"} onClick={() => dispatch(commandIds.toolSetActive, { tool: "brush" })}>Reveal</button>
<button type="button" className={maskModeButtonClass(tool === "eraser")} aria-pressed={tool === "eraser"} onClick={() => dispatch(commandIds.toolSetActive, { tool: "eraser" })}>Hide</button>
<BottomControlDivider />
</>
) : null}
{tool === "brush" && !editingMask ? (
<label className={bottomControlFieldClass()}>
<span className={bottomControlLabelClass()}>Color</span>
<BottomControlColorPicker
value={settings.color}
aria-label="Brush color"
onValueChange={(color) => dispatch(commandIds.toolSetBrushSettings, { color })}
/>
</label>
) : null}
<label className={bottomControlFieldClass()}>
<span className={bottomControlLabelClass()}>Size</span>
<BottomControlSlider
min={1}
max={200}
value={settings.size}
className="w-48"
aria-label={`${tool} size`}
onValueChange={(size) => dispatch(commandIds.toolSetBrushSettings, { size })}
/>
<span className="w-12 text-right text-base text-white">{Math.round(settings.size)}</span>
</label>
<BottomControlDivider />
<label className={bottomControlFieldClass()}>
<span className={bottomControlLabelClass()}>Hard</span>
<BottomControlSlider
min={0}
max={100}
value={settings.hardness}
className="w-48"
aria-label={`${tool} hardness`}
onValueChange={(hardness) => dispatch(commandIds.toolSetBrushSettings, { hardness })}
/>
<span className="w-12 text-right text-base text-white">{Math.round(settings.hardness)}</span>
</label>
{editingMask ? (
<>
<BottomControlDivider />
<label className={bottomControlFieldClass()}>
<span className={bottomControlLabelClass()}>View</span>
<BottomControlSelectMenu
value={maskViewMode}
options={maskViewModeOptions}
aria-label="Mask view mode"
onValueChange={(mode) => dispatch(commandIds.toolSetMaskViewMode, { mode })}
/>
</label>
<BottomControlDivider />
<button
type="button"
className="rounded-full bg-white px-5 py-2 text-base font-medium text-black transition hover:bg-white/90"
onClick={() => dispatch(commandIds.toolExitMaskEdit, undefined)}
>
Done
</button>
</>
) : null}
</div>
);
}
function maskModeButtonClass(active: boolean) {
return `rounded-full px-4 py-2 text-base font-medium transition ${active ? "bg-white text-black" : "bg-white/10 text-white hover:bg-white/15"}`;
}