Files

208 lines
5.7 KiB
TypeScript
Raw Permalink Normal View History

2026-05-10 03:06:54 +07:00
import React from "react";
2026-05-31 03:03:49 +07:00
import {
getLayerRuntimeSource,
type FaceBlurMethod,
type Layer,
type LayerEffect,
} from "@pien-studio/types";
2026-05-10 03:06:54 +07:00
import type { FaceDetectionOverlay } from "./use-face-detection";
type FaceBlurPreview = {
layerId: string;
2026-05-21 00:02:12 +07:00
effects: LayerEffect[];
2026-05-10 03:06:54 +07:00
};
2026-05-24 21:05:21 +07:00
type FaceSelectionState = {
key: string;
indices: number[];
};
2026-05-10 03:06:54 +07:00
type UseFaceBlurWorkflowOptions = {
selectedLayer: Layer | null;
faceDetectionsLayerId: string | null;
faceDetections: FaceDetectionOverlay[];
2026-05-21 00:02:12 +07:00
setLayerEffect: (layerId: string, effect: LayerEffect) => void;
removeLayerEffect: (layerId: string, kind: LayerEffect["kind"]) => void;
2026-05-10 03:06:54 +07:00
};
export function useFaceBlurWorkflow(options: UseFaceBlurWorkflowOptions) {
2026-05-31 03:03:49 +07:00
const {
selectedLayer,
faceDetectionsLayerId,
faceDetections,
setLayerEffect,
removeLayerEffect,
} = options;
const [blurMethod, setBlurMethod] =
React.useState<FaceBlurMethod>("gaussian");
2026-05-10 03:06:54 +07:00
const [blurAmount, setBlurAmount] = React.useState(14);
const [censorColor, setCensorColor] = React.useState("#111111");
2026-05-31 03:03:49 +07:00
const [faceSelection, setFaceSelection] =
React.useState<FaceSelectionState | null>(null);
const hasDetectableSelection = Boolean(
selectedLayer &&
selectedLayer.type === "raster" &&
faceDetectionsLayerId === selectedLayer.id,
);
2026-05-21 00:02:12 +07:00
const faceBlurEffect = React.useMemo(
2026-05-31 03:03:49 +07:00
() =>
selectedLayer?.effects.find(
(e): e is Extract<LayerEffect, { kind: "face-blur" }> =>
e.kind === "face-blur",
) ?? null,
2026-05-21 00:02:12 +07:00
[selectedLayer?.effects],
);
2026-05-10 03:06:54 +07:00
2026-05-24 21:05:21 +07:00
const selectionKey = `${selectedLayer?.id ?? "none"}:${faceDetectionsLayerId ?? "none"}:${faceDetections.length}:${faceBlurEffect ? "effect" : "empty"}`;
const defaultSelectedFaceIndices = React.useMemo(() => {
if (!hasDetectableSelection || faceBlurEffect) return [];
return faceDetections.map((_, index) => index);
}, [faceDetections, faceBlurEffect, hasDetectableSelection]);
2026-05-31 03:03:49 +07:00
const selectedFaceIndices =
faceSelection?.key === selectionKey
? faceSelection.indices
: defaultSelectedFaceIndices;
2026-05-24 21:05:21 +07:00
2026-05-10 03:06:54 +07:00
const buildBlurRegions = React.useCallback(
(indices: number[]) => {
2026-05-21 00:02:12 +07:00
if (!selectedLayer || selectedLayer.type !== "raster") return [];
2026-05-31 03:03:49 +07:00
if (
faceDetectionsLayerId !== selectedLayer.id ||
faceDetections.length === 0
)
return [];
2026-05-10 03:06:54 +07:00
const indexSet = new Set(indices);
return faceDetections
.filter((_, index) => indexSet.has(index))
.map((face) => {
2026-05-31 03:03:49 +07:00
const baseWidth = Math.max(1, selectedLayer.width);
const baseHeight = Math.max(1, selectedLayer.height);
2026-05-10 03:06:54 +07:00
const scaleX = face.sourceWidth / baseWidth;
const scaleY = face.sourceHeight / baseHeight;
return {
x: Math.max(0, Math.floor(face.x * scaleX)),
y: Math.max(0, Math.floor(face.y * scaleY)),
width: Math.max(1, Math.floor(face.width * scaleX)),
height: Math.max(1, Math.floor(face.height * scaleY)),
sourceWidth: face.sourceWidth,
sourceHeight: face.sourceHeight,
censorColor,
};
});
},
[censorColor, faceDetections, faceDetectionsLayerId, selectedLayer],
);
2026-05-31 03:03:49 +07:00
const toggleFaceIndex = React.useCallback(
(index: number) => {
setFaceSelection((prev) => {
const current =
prev?.key === selectionKey
? prev.indices
: defaultSelectedFaceIndices;
const indices = current.includes(index)
? current.filter((item) => item !== index)
: [...current, index];
return { key: selectionKey, indices };
});
},
[defaultSelectedFaceIndices, selectionKey],
);
2026-05-10 03:06:54 +07:00
const clearBlur = React.useCallback(() => {
2026-05-21 00:02:12 +07:00
if (!selectedLayer) return;
removeLayerEffect(selectedLayer.id, "face-blur");
2026-05-24 21:05:21 +07:00
setFaceSelection({ key: selectionKey, indices: [] });
}, [removeLayerEffect, selectedLayer, selectionKey]);
2026-05-10 03:06:54 +07:00
const blurFaces = React.useCallback(
(indices: number[]) => {
2026-05-31 03:03:49 +07:00
if (
!selectedLayer ||
selectedLayer.type !== "raster" ||
!getLayerRuntimeSource(selectedLayer)
)
return;
if (
faceDetectionsLayerId !== selectedLayer.id ||
faceDetections.length === 0
)
return;
2026-05-10 03:06:54 +07:00
const regions = buildBlurRegions(indices);
2026-05-21 00:02:12 +07:00
setLayerEffect(selectedLayer.id, {
kind: "face-blur",
enabled: true,
2026-05-10 03:06:54 +07:00
method: blurMethod,
amount: blurAmount,
regions,
censorColor,
});
2026-05-24 21:05:21 +07:00
setFaceSelection({ key: selectionKey, indices: [] });
2026-05-10 03:06:54 +07:00
},
2026-05-31 03:03:49 +07:00
[
blurAmount,
blurMethod,
buildBlurRegions,
censorColor,
faceDetections.length,
faceDetectionsLayerId,
selectedLayer,
selectionKey,
setLayerEffect,
],
2026-05-10 03:06:54 +07:00
);
2026-05-24 21:05:21 +07:00
const faceBlurPreview = React.useMemo<FaceBlurPreview | null>(() => {
2026-05-31 03:03:49 +07:00
if (
!hasDetectableSelection ||
!selectedLayer ||
selectedLayer.type !== "raster"
) {
2026-05-24 21:05:21 +07:00
return null;
2026-05-10 03:06:54 +07:00
}
if (selectedFaceIndices.length === 0) {
2026-05-24 21:05:21 +07:00
return null;
2026-05-10 03:06:54 +07:00
}
2026-05-24 21:05:21 +07:00
return {
2026-05-10 03:06:54 +07:00
layerId: selectedLayer.id,
2026-05-31 03:03:49 +07:00
effects: [
{
kind: "face-blur",
enabled: true,
method: blurMethod,
amount: blurAmount,
regions: buildBlurRegions(selectedFaceIndices),
censorColor,
},
],
2026-05-24 21:05:21 +07:00
};
2026-05-31 03:03:49 +07:00
}, [
blurAmount,
blurMethod,
buildBlurRegions,
censorColor,
hasDetectableSelection,
selectedFaceIndices,
selectedLayer,
]);
2026-05-10 03:06:54 +07:00
return {
blurMethod,
setBlurMethod,
blurAmount,
setBlurAmount,
censorColor,
setCensorColor,
selectedFaceIndices,
faceBlurPreview,
toggleFaceIndex,
clearBlur,
blurFaces,
};
}