Files
Pien-Studio/apps/web/hooks/use-face-blur-workflow.ts
T

142 lines
5.1 KiB
TypeScript
Raw Normal View History

2026-05-10 03:06:54 +07:00
import React from "react";
2026-05-21 00:02:12 +07:00
import type { FaceBlurMethod, Layer, 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
};
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-21 00:02:12 +07:00
const { selectedLayer, faceDetectionsLayerId, faceDetections, setLayerEffect, removeLayerEffect } = options;
2026-05-10 03:06:54 +07:00
const [blurMethod, setBlurMethod] = React.useState<FaceBlurMethod>("gaussian");
const [blurAmount, setBlurAmount] = React.useState(14);
const [censorColor, setCensorColor] = React.useState("#111111");
const [selectedFaceIndices, setSelectedFaceIndices] = React.useState<number[]>([]);
const [faceBlurPreview, setFaceBlurPreview] = React.useState<FaceBlurPreview | null>(null);
2026-05-21 00:02:12 +07:00
const hasDetectableSelection = Boolean(selectedLayer && selectedLayer.type === "raster" && faceDetectionsLayerId === selectedLayer.id);
const faceBlurEffect = React.useMemo(
() => selectedLayer?.effects.find((e): e is Extract<LayerEffect, { kind: "face-blur" }> => e.kind === "face-blur") ?? null,
[selectedLayer?.effects],
);
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-10 03:06:54 +07:00
if (faceDetectionsLayerId !== selectedLayer.id || faceDetections.length === 0) return [];
const indexSet = new Set(indices);
return faceDetections
.filter((_, index) => indexSet.has(index))
.map((face) => {
const baseWidth = Math.max(1, selectedLayer.width ?? face.sourceWidth);
const baseHeight = Math.max(1, selectedLayer.height ?? face.sourceHeight);
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],
);
const toggleFaceIndex = React.useCallback((index: number) => {
setSelectedFaceIndices((prev) => (prev.includes(index) ? prev.filter((item) => item !== index) : [...prev, index]));
}, []);
const clearBlur = React.useCallback(() => {
2026-05-21 00:02:12 +07:00
if (!selectedLayer) return;
removeLayerEffect(selectedLayer.id, "face-blur");
2026-05-10 03:06:54 +07:00
setFaceBlurPreview(null);
2026-05-21 00:02:12 +07:00
}, [selectedLayer, removeLayerEffect]);
2026-05-10 03:06:54 +07:00
const blurFaces = React.useCallback(
(indices: number[]) => {
2026-05-21 00:02:12 +07:00
if (!selectedLayer || selectedLayer.type !== "raster" || !selectedLayer.sourceUri) return;
2026-05-10 03:06:54 +07:00
if (faceDetectionsLayerId !== selectedLayer.id || faceDetections.length === 0) return;
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,
});
setSelectedFaceIndices([]);
setFaceBlurPreview(null);
},
2026-05-21 00:02:12 +07:00
[blurAmount, blurMethod, buildBlurRegions, censorColor, faceDetections.length, faceDetectionsLayerId, selectedLayer, setLayerEffect],
2026-05-10 03:06:54 +07:00
);
React.useEffect(() => {
if (!hasDetectableSelection) {
setSelectedFaceIndices((prev) => (prev.length === 0 ? prev : []));
setFaceBlurPreview(null);
return;
}
2026-05-21 00:02:12 +07:00
if (faceBlurEffect) {
2026-05-10 03:06:54 +07:00
setSelectedFaceIndices((prev) => (prev.length === 0 ? prev : []));
return;
}
setSelectedFaceIndices((prev) => {
const next = faceDetections.map((_, i) => i);
if (prev.length === next.length && prev.every((value, index) => value === next[index])) return prev;
return next;
});
2026-05-21 00:02:12 +07:00
}, [faceDetections, hasDetectableSelection, faceBlurEffect]);
2026-05-10 03:06:54 +07:00
React.useEffect(() => {
2026-05-21 00:02:12 +07:00
if (!hasDetectableSelection || !selectedLayer || selectedLayer.type !== "raster") {
2026-05-10 03:06:54 +07:00
setFaceBlurPreview(null);
return;
}
if (selectedFaceIndices.length === 0) {
setFaceBlurPreview(null);
return;
}
setFaceBlurPreview({
layerId: selectedLayer.id,
2026-05-21 00:02:12 +07:00
effects: [{
kind: "face-blur",
enabled: true,
method: blurMethod,
amount: blurAmount,
regions: buildBlurRegions(selectedFaceIndices),
censorColor,
}],
2026-05-10 03:06:54 +07:00
});
}, [blurAmount, blurMethod, buildBlurRegions, censorColor, hasDetectableSelection, selectedFaceIndices, selectedLayer]);
return {
blurMethod,
setBlurMethod,
blurAmount,
setBlurAmount,
censorColor,
setCensorColor,
selectedFaceIndices,
faceBlurPreview,
toggleFaceIndex,
clearBlur,
blurFaces,
};
}