- Refactor LayersSheet component to support mask editing state and improve layer visibility handling. - Introduce functions to collect mask layer IDs and count display layers excluding masks. - Update BrushControls to include mask view mode options and a Done button for exiting mask editing. - Modify brush session handling to support brush previews when editing masks. - Implement a new BrushPreviewRenderer for rendering brush strokes with visual feedback. - Add document geometry utilities for transforming points and resolving layer bounds. - Ensure proper cleanup of brush preview on pointer leave and other interactions.
90 lines
3.6 KiB
TypeScript
90 lines
3.6 KiB
TypeScript
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 { BottomControlDivider } from "./Divider";
|
|
import { bottomControlLabelClass } from "./styles";
|
|
|
|
export type BrushControlsProps = {
|
|
tool: Extract<ToolId, "brush" | "eraser">;
|
|
settings: BrushSettings;
|
|
editingMask?: boolean;
|
|
maskViewMode?: MaskViewMode;
|
|
dispatch: AppStore["dispatch"];
|
|
};
|
|
|
|
export function BrushControls({ tool, settings, editingMask = false, maskViewMode = "composite", dispatch }: BrushControlsProps) {
|
|
return (
|
|
<div className="flex w-full items-center justify-center gap-2 tabular-nums">
|
|
<span className="px-2 font-medium text-white/85">{editingMask ? `Mask · ${tool === "eraser" ? "Hide" : "Reveal"}` : tool === "eraser" ? "Eraser" : "Brush"}</span>
|
|
<BottomControlDivider />
|
|
{tool === "brush" && !editingMask ? (
|
|
<label className="flex items-center gap-1.5">
|
|
<span className={bottomControlLabelClass()}>Color</span>
|
|
<input
|
|
type="color"
|
|
className="h-7 w-8 cursor-pointer rounded border-0 bg-transparent p-0"
|
|
value={settings.color}
|
|
aria-label="Brush color"
|
|
onChange={(event) => dispatch(commandIds.toolSetBrushSettings, { color: event.target.value })}
|
|
/>
|
|
</label>
|
|
) : null}
|
|
<label className="flex items-center gap-1.5">
|
|
<span className={bottomControlLabelClass()}>Size</span>
|
|
<input
|
|
type="range"
|
|
min={1}
|
|
max={200}
|
|
value={settings.size}
|
|
className="w-24 accent-white"
|
|
aria-label={`${tool} size`}
|
|
onChange={(event) => dispatch(commandIds.toolSetBrushSettings, { size: Number(event.target.value) })}
|
|
/>
|
|
<span className="w-8 text-right text-white">{Math.round(settings.size)}</span>
|
|
</label>
|
|
<BottomControlDivider />
|
|
<label className="flex items-center gap-1.5">
|
|
<span className={bottomControlLabelClass()}>Hard</span>
|
|
<input
|
|
type="range"
|
|
min={0}
|
|
max={100}
|
|
value={settings.hardness}
|
|
className="w-24 accent-white"
|
|
aria-label={`${tool} hardness`}
|
|
onChange={(event) => dispatch(commandIds.toolSetBrushSettings, { hardness: Number(event.target.value) })}
|
|
/>
|
|
<span className="w-8 text-right text-white">{Math.round(settings.hardness)}</span>
|
|
</label>
|
|
{editingMask ? (
|
|
<>
|
|
<BottomControlDivider />
|
|
<label className="flex items-center gap-1.5">
|
|
<span className={bottomControlLabelClass()}>View</span>
|
|
<select
|
|
className="h-7 rounded-full border border-white/10 bg-black/70 px-2 text-white outline-none"
|
|
value={maskViewMode}
|
|
aria-label="Mask view mode"
|
|
onChange={(event) => dispatch(commandIds.toolSetMaskViewMode, { mode: event.target.value as MaskViewMode })}
|
|
>
|
|
<option value="composite">Image</option>
|
|
<option value="blackWhite">B/W</option>
|
|
<option value="alpha">Alpha</option>
|
|
<option value="overlay">Overlay</option>
|
|
</select>
|
|
</label>
|
|
<BottomControlDivider />
|
|
<button
|
|
type="button"
|
|
className="rounded-full bg-white px-3 py-1 font-medium text-black transition hover:bg-white/90"
|
|
onClick={() => dispatch(commandIds.toolExitMaskEdit, undefined)}
|
|
>
|
|
Done
|
|
</button>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|