- Adjusted button styles across various components for consistency and better UX. - Enhanced layout of action controls to utilize whitespace more effectively. - Updated slider styles for a more modern appearance and improved usability. - Refined input fields and labels for better accessibility and readability. - Introduced new app surface styles for a cohesive design across the application. - Added tests for canvas cursor behavior to ensure correct cursor display during operations.
85 lines
4.0 KiB
TypeScript
85 lines
4.0 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import type { Asset } from "@core/asset";
|
|
import type { AppStore } from "@editor/store";
|
|
import { analyzeMask, runMaskOperation, type MaskAnalysis, type MaskRasterOperation } from "@operations/masks/rasterActions";
|
|
|
|
export function MaskStatus({ asset }: { asset: Asset }) {
|
|
const [analysis, setAnalysis] = useState<MaskAnalysis>();
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
void analyzeMask(asset)
|
|
.then((nextAnalysis) => {
|
|
if (!cancelled) setAnalysis(nextAnalysis);
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setAnalysis(undefined);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [asset.source, asset.intrinsicSize.w, asset.intrinsicSize.h]);
|
|
|
|
if (!analysis) return <span className="rounded bg-white/[0.04] px-2 py-0.5 text-[0.68rem] text-sky-100/40">Reading</span>;
|
|
return (
|
|
<span className="inline-flex min-w-0 items-center gap-1.5 rounded-md bg-white/[0.04] px-1.5 py-1 text-[0.68rem] text-sky-100/60">
|
|
<img src={analysis.thumbnail} alt="" className="h-7 w-10 rounded-md bg-black/30 object-cover ring-1 ring-white/10" />
|
|
<span className="whitespace-nowrap">Reveal {formatPercent(analysis.coverage)}</span>
|
|
<span className="whitespace-nowrap text-sky-100/45">Inpaint {formatPercent(analysis.hiddenCoverage)}</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function MaskOperationButtons({ maskLayerId, maskAsset, dispatch }: { maskLayerId: string; maskAsset: Asset; dispatch: AppStore["dispatch"] }) {
|
|
return (
|
|
<>
|
|
<MaskOperationButton label="Invert" title="Invert mask" maskLayerId={maskLayerId} maskAsset={maskAsset} operation={{ type: "invert" }} dispatch={dispatch} />
|
|
<MaskOperationButton label="White" title="Fill mask white" maskLayerId={maskLayerId} maskAsset={maskAsset} operation={{ type: "fill", fill: "white" }} dispatch={dispatch} />
|
|
<MaskOperationButton label="Black" title="Fill mask black" maskLayerId={maskLayerId} maskAsset={maskAsset} operation={{ type: "fill", fill: "black" }} dispatch={dispatch} />
|
|
<MaskOperationButton label="Clear" title="Clear mask" maskLayerId={maskLayerId} maskAsset={maskAsset} operation={{ type: "fill", fill: "clear" }} dispatch={dispatch} />
|
|
<MaskOperationButton label="Feather" title="Feather mask edge" maskLayerId={maskLayerId} maskAsset={maskAsset} operation={{ type: "feather", radius: 3 }} dispatch={dispatch} />
|
|
<MaskOperationButton label="Expand" title="Expand mask" maskLayerId={maskLayerId} maskAsset={maskAsset} operation={{ type: "expand", radius: 3 }} dispatch={dispatch} />
|
|
<MaskOperationButton label="Contract" title="Contract mask" maskLayerId={maskLayerId} maskAsset={maskAsset} operation={{ type: "contract", radius: 3 }} dispatch={dispatch} />
|
|
<MaskOperationButton label="Blur" title="Blur mask edge" maskLayerId={maskLayerId} maskAsset={maskAsset} operation={{ type: "blur", radius: 2 }} dispatch={dispatch} />
|
|
<MaskOperationButton label="Clean" title="Despeckle mask" maskLayerId={maskLayerId} maskAsset={maskAsset} operation={{ type: "despeckle", strength: 8 }} dispatch={dispatch} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
function MaskOperationButton({
|
|
label,
|
|
title,
|
|
maskLayerId,
|
|
maskAsset,
|
|
operation,
|
|
dispatch,
|
|
}: {
|
|
label: string;
|
|
title: string;
|
|
maskLayerId: string;
|
|
maskAsset: Asset;
|
|
operation: MaskRasterOperation;
|
|
dispatch: AppStore["dispatch"];
|
|
}) {
|
|
const [busy, setBusy] = useState(false);
|
|
return (
|
|
<button
|
|
type="button"
|
|
className={maskActionButtonClass()}
|
|
disabled={busy}
|
|
title={title}
|
|
onClick={() => {
|
|
setBusy(true);
|
|
void runMaskOperation(maskLayerId, maskAsset, operation, dispatch)
|
|
.finally(() => setBusy(false));
|
|
}}
|
|
>
|
|
{busy ? "..." : label}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
|
|
function maskActionButtonClass() { return "rounded-md bg-white/[0.05] px-2 py-1 text-[0.68rem] font-semibold text-white/55 transition hover:bg-white/[0.09] hover:text-white disabled:pointer-events-none disabled:opacity-35"; }
|
|
function formatPercent(value: number) { return `${Math.round(value * 100)}%`; }
|