mirror of
https://github.com/YuzuZensai/Pien-Studio.git
synced 2026-09-02 14:18:35 +00:00
✨ feat: layer effects, bucket fill, simple brush
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import type { Layer } from "@pien-studio/types";
|
||||
import { getToolDefinition } from "@pien-studio/editor-core";
|
||||
|
||||
type Viewport = {
|
||||
x: number;
|
||||
@@ -10,7 +11,7 @@ type Viewport = {
|
||||
type UseCanvasInteractionsOptions = {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
tool: "pointer" | "hand" | "face";
|
||||
tool: string;
|
||||
onSelectLayer: (id: string | null) => void;
|
||||
onMoveLayer: (id: string, x: number, y: number) => void;
|
||||
onMoveLayerEnd?: (id: string, x: number, y: number) => void;
|
||||
@@ -21,6 +22,10 @@ type UseCanvasInteractionsOptions = {
|
||||
onInteractionStart?: () => void;
|
||||
onInteractionEnd?: () => void;
|
||||
onContextMenu?: (x: number, y: number) => void;
|
||||
onFillLayer?: (layerId: string, x: number, y: number) => void;
|
||||
onBrushStrokeStart?: (layerId: string, x: number, y: number, layerWidth: number, layerHeight: number) => void;
|
||||
onBrushStrokeMove?: (layerId: string, x: number, y: number) => void;
|
||||
onBrushStrokeEnd?: (layerId: string) => void;
|
||||
};
|
||||
|
||||
const MIN_SCALE = 0.1;
|
||||
@@ -43,6 +48,10 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
|
||||
onInteractionStart,
|
||||
onInteractionEnd,
|
||||
onContextMenu,
|
||||
onFillLayer,
|
||||
onBrushStrokeStart,
|
||||
onBrushStrokeMove,
|
||||
onBrushStrokeEnd,
|
||||
} = options;
|
||||
const [viewport, setViewport] = React.useState<Viewport>({ x: 0, y: 0, scale: 1 });
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
@@ -86,6 +95,8 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
|
||||
startRotation: number;
|
||||
lastRotation: number;
|
||||
} | null>(null);
|
||||
const brushRef = React.useRef<{ id: string; lastX: number; lastY: number; rect: DOMRect } | null>(null);
|
||||
|
||||
const pinchRef = React.useRef<{
|
||||
active: boolean;
|
||||
initialPinchPx: number;
|
||||
@@ -237,10 +248,18 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
|
||||
return;
|
||||
}
|
||||
if (e.button !== 0) return;
|
||||
if (tool !== "hand" && tool !== "face" && !isSpacePan) return;
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
isPanning.current = true;
|
||||
lastPos.current = { x: e.clientX, y: e.clientY };
|
||||
const toolDef = getToolDefinition(tool);
|
||||
if (toolDef?.interactionMode !== "select" && !isSpacePan) {
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
isPanning.current = true;
|
||||
lastPos.current = { x: e.clientX, y: e.clientY };
|
||||
return;
|
||||
}
|
||||
if (isSpacePan) {
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
isPanning.current = true;
|
||||
lastPos.current = { x: e.clientX, y: e.clientY };
|
||||
}
|
||||
}
|
||||
|
||||
function onContainerPointerMove(e: React.PointerEvent<HTMLDivElement>) {
|
||||
@@ -251,7 +270,17 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
|
||||
setViewport((vp) => ({ ...vp, x: vp.x + dx, y: vp.y + dy }));
|
||||
return;
|
||||
}
|
||||
if (tool === "pointer" && rotateRef.current && onRotateLayer) {
|
||||
if (brushRef.current && onBrushStrokeMove) {
|
||||
const { rect } = brushRef.current;
|
||||
const localX = (e.clientX - rect.left) / viewport.scale;
|
||||
const localY = (e.clientY - rect.top) / viewport.scale;
|
||||
brushRef.current.lastX = localX;
|
||||
brushRef.current.lastY = localY;
|
||||
onBrushStrokeMove(brushRef.current.id, localX, localY);
|
||||
return;
|
||||
}
|
||||
const toolDef = getToolDefinition(tool);
|
||||
if (toolDef?.allowsLayerRotate && rotateRef.current && onRotateLayer) {
|
||||
const { centerX, centerY, startAngle, startRotation, id } = rotateRef.current;
|
||||
const currentAngle = Math.atan2(e.clientY - centerY, e.clientX - centerX);
|
||||
const delta = currentAngle - startAngle;
|
||||
@@ -260,7 +289,7 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
|
||||
onRotateLayer(id, nextRotation);
|
||||
return;
|
||||
}
|
||||
if (tool === "pointer" && resizeRef.current && onResizeLayer) {
|
||||
if (toolDef?.allowsLayerResize && resizeRef.current && onResizeLayer) {
|
||||
const dx = (e.clientX - resizeRef.current.startX) / viewport.scale;
|
||||
const dy = (e.clientY - resizeRef.current.startY) / viewport.scale;
|
||||
const corner = resizeRef.current.corner;
|
||||
@@ -315,7 +344,7 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (tool === "pointer" && dragRef.current) {
|
||||
if (toolDef?.allowsLayerDrag && dragRef.current) {
|
||||
const dx = (e.clientX - dragRef.current.startEventX) / viewport.scale;
|
||||
const dy = (e.clientY - dragRef.current.startEventY) / viewport.scale;
|
||||
const nextX = dragRef.current.startLayerX + dx;
|
||||
@@ -362,6 +391,10 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
|
||||
const { id, lastRotation } = rotateRef.current;
|
||||
if (typeof lastRotation === "number") onRotateLayerEnd(id, lastRotation);
|
||||
}
|
||||
if (brushRef.current && onBrushStrokeEnd) {
|
||||
onBrushStrokeEnd(brushRef.current.id);
|
||||
brushRef.current = null;
|
||||
}
|
||||
dragRef.current = null;
|
||||
resizeRef.current = null;
|
||||
resizeMovePendingRef.current = null;
|
||||
@@ -372,6 +405,24 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
|
||||
function onLayerPointerDown(e: React.PointerEvent<HTMLDivElement>, layer: Layer) {
|
||||
if (isSpacePan) return;
|
||||
e.stopPropagation();
|
||||
const toolDef = getToolDefinition(tool);
|
||||
if (toolDef?.interactionMode === "paint") {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const localX = (e.clientX - rect.left) / viewport.scale;
|
||||
const localY = (e.clientY - rect.top) / viewport.scale;
|
||||
onSelectLayer(layer.id);
|
||||
if (tool === "brush" && onBrushStrokeStart) {
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
brushRef.current = { id: layer.id, lastX: localX, lastY: localY, rect: e.currentTarget.getBoundingClientRect() };
|
||||
const lw = layer.width ?? Math.round(200 * layer.scale);
|
||||
const lh = layer.height ?? Math.round(150 * layer.scale);
|
||||
onBrushStrokeStart(layer.id, localX, localY, lw, lh);
|
||||
beginInteraction();
|
||||
} else if (onFillLayer) {
|
||||
onFillLayer(layer.id, localX, localY);
|
||||
}
|
||||
return;
|
||||
}
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
dragRef.current = {
|
||||
id: layer.id,
|
||||
@@ -387,7 +438,7 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
|
||||
}
|
||||
|
||||
function onResizeHandleDown(e: React.PointerEvent<HTMLButtonElement>, layer: Layer, corner: string) {
|
||||
if (tool !== "pointer" || !onResizeLayer) return;
|
||||
if (!getToolDefinition(tool)?.allowsLayerResize || !onResizeLayer) return;
|
||||
e.stopPropagation();
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
rotateRef.current = null;
|
||||
@@ -411,7 +462,7 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
|
||||
}
|
||||
|
||||
function onRotateHandleDown(e: React.PointerEvent<HTMLButtonElement>, layer: Layer) {
|
||||
if (tool !== "pointer" || !onRotateLayer) return;
|
||||
if (!getToolDefinition(tool)?.allowsLayerRotate || !onRotateLayer) return;
|
||||
e.stopPropagation();
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
resizeRef.current = null;
|
||||
|
||||
@@ -32,8 +32,13 @@ export function useEditorBindings() {
|
||||
removeSelectedLayer: s.removeSelectedLayer,
|
||||
addLayerByType: s.addLayerByType,
|
||||
moveSelectedLayerOrder: s.moveSelectedLayerOrder,
|
||||
addCanvasSizedLayer: s.addCanvasSizedLayer,
|
||||
importImageFromFile: s.importImageFromFile,
|
||||
setImageLayerFaceBlur: s.setImageLayerFaceBlur,
|
||||
updateImageLayerSource: s.updateImageLayerSource,
|
||||
setLayerEffect: s.setLayerEffect,
|
||||
removeLayerEffect: s.removeLayerEffect,
|
||||
setLayerVisible: s.setLayerVisible,
|
||||
setEffectEnabled: s.setEffectEnabled,
|
||||
setCanvasSize: s.setCanvasSize,
|
||||
setTool: s.setTool,
|
||||
undo: s.undo,
|
||||
|
||||
@@ -2,14 +2,20 @@ import React from "react";
|
||||
|
||||
type UseEditorShortcutsOptions = {
|
||||
onSave: () => void;
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
onCopy: () => void;
|
||||
onCut: () => void;
|
||||
onPaste: () => void;
|
||||
onPaste: (e?: ClipboardEvent) => void;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
function isEditableTarget(e: Event) {
|
||||
return e.target instanceof HTMLElement && /^(input|textarea|select)$/i.test(e.target.tagName);
|
||||
}
|
||||
|
||||
export function useEditorShortcuts(options: UseEditorShortcutsOptions) {
|
||||
const { onSave, onCopy, onCut, onPaste, onDelete } = options;
|
||||
const { onSave, onUndo, onRedo, onCopy, onCut, onPaste, onDelete } = options;
|
||||
|
||||
React.useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
@@ -19,31 +25,57 @@ export function useEditorShortcuts(options: UseEditorShortcutsOptions) {
|
||||
onSave();
|
||||
return;
|
||||
}
|
||||
if (e.key.toLowerCase() === "c") {
|
||||
if (e.key.toLowerCase() === "z" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onCopy();
|
||||
onUndo();
|
||||
return;
|
||||
}
|
||||
if (e.key.toLowerCase() === "x") {
|
||||
if (e.key.toLowerCase() === "z" && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onCut();
|
||||
onRedo();
|
||||
return;
|
||||
}
|
||||
if (e.key.toLowerCase() === "v") {
|
||||
if (e.key.toLowerCase() === "y") {
|
||||
e.preventDefault();
|
||||
onPaste();
|
||||
onRedo();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "Delete" || e.key === "Backspace") {
|
||||
if (!(e.target instanceof HTMLElement) || /^(input|textarea|select)$/i.test(e.target.tagName)) return;
|
||||
if (isEditableTarget(e)) return;
|
||||
e.preventDefault();
|
||||
onDelete();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCopy(e: ClipboardEvent) {
|
||||
if (isEditableTarget(e)) return;
|
||||
e.preventDefault();
|
||||
onCopy();
|
||||
}
|
||||
|
||||
function handleCut(e: ClipboardEvent) {
|
||||
if (isEditableTarget(e)) return;
|
||||
e.preventDefault();
|
||||
onCut();
|
||||
}
|
||||
|
||||
function handlePaste(e: ClipboardEvent) {
|
||||
if (isEditableTarget(e)) return;
|
||||
e.preventDefault();
|
||||
onPaste(e);
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [onCopy, onCut, onDelete, onPaste, onSave]);
|
||||
window.addEventListener("copy", handleCopy);
|
||||
window.addEventListener("cut", handleCut);
|
||||
window.addEventListener("paste", handlePaste);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("copy", handleCopy);
|
||||
window.removeEventListener("cut", handleCut);
|
||||
window.removeEventListener("paste", handlePaste);
|
||||
};
|
||||
}, [onCopy, onCut, onDelete, onPaste, onRedo, onSave, onUndo]);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { Layer } from "@pien-studio/types";
|
||||
import type { Layer, LayerEffect } from "@pien-studio/types";
|
||||
import { useFaceBlurWorkflow } from "./use-face-blur-workflow";
|
||||
|
||||
function makeImageLayer(overrides: Partial<Layer> = {}): Layer {
|
||||
return {
|
||||
id: "layer-1",
|
||||
type: "image",
|
||||
type: "raster",
|
||||
sourceUri: "data:image/png;base64,abc",
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
effects: [],
|
||||
visible: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("useFaceBlurWorkflow", () => {
|
||||
it("selects all detected faces by default when no existing face blur", async () => {
|
||||
const setImageLayerFaceBlur = vi.fn();
|
||||
const setLayerEffect = vi.fn();
|
||||
const removeLayerEffect = vi.fn();
|
||||
const selectedLayer = makeImageLayer();
|
||||
const faceDetections = [
|
||||
{ x: 1, y: 2, width: 10, height: 12, label: "a", sourceWidth: 100, sourceHeight: 100 },
|
||||
@@ -31,28 +34,37 @@ describe("useFaceBlurWorkflow", () => {
|
||||
selectedLayer,
|
||||
faceDetectionsLayerId: "layer-1",
|
||||
faceDetections,
|
||||
setImageLayerFaceBlur,
|
||||
setLayerEffect,
|
||||
removeLayerEffect,
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedFaceIndices).toEqual([0, 1]);
|
||||
expect(result.current.faceBlurPreview?.regions).toHaveLength(2);
|
||||
const faceBlurEffect = result.current.faceBlurPreview?.effects.find((e) => e.kind === "face-blur");
|
||||
expect(faceBlurEffect?.kind === "face-blur" ? faceBlurEffect.regions : undefined).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps selection empty when selected image already has blur", async () => {
|
||||
const setImageLayerFaceBlur = vi.fn();
|
||||
const selectedLayer = makeImageLayer({
|
||||
faceBlur: { method: "gaussian", amount: 14, regions: [{ x: 0, y: 0, width: 4, height: 4 }] },
|
||||
});
|
||||
const setLayerEffect = vi.fn();
|
||||
const removeLayerEffect = vi.fn();
|
||||
const faceBlurEffect: LayerEffect = {
|
||||
kind: "face-blur",
|
||||
enabled: true,
|
||||
method: "gaussian",
|
||||
amount: 14,
|
||||
regions: [{ x: 0, y: 0, width: 4, height: 4 }],
|
||||
};
|
||||
const selectedLayer = makeImageLayer({ effects: [faceBlurEffect] });
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useFaceBlurWorkflow({
|
||||
selectedLayer,
|
||||
faceDetectionsLayerId: "layer-1",
|
||||
faceDetections: [{ x: 1, y: 2, width: 10, height: 12, label: "a", sourceWidth: 100, sourceHeight: 100 }],
|
||||
setImageLayerFaceBlur,
|
||||
setLayerEffect,
|
||||
removeLayerEffect,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -63,7 +75,8 @@ describe("useFaceBlurWorkflow", () => {
|
||||
});
|
||||
|
||||
it("toggles face selection and applies blur to selected regions", async () => {
|
||||
const setImageLayerFaceBlur = vi.fn();
|
||||
const setLayerEffect = vi.fn();
|
||||
const removeLayerEffect = vi.fn();
|
||||
const selectedLayer = makeImageLayer();
|
||||
const faceDetections = [
|
||||
{ x: 1, y: 2, width: 10, height: 12, label: "a", sourceWidth: 100, sourceHeight: 100 },
|
||||
@@ -75,7 +88,8 @@ describe("useFaceBlurWorkflow", () => {
|
||||
selectedLayer,
|
||||
faceDetectionsLayerId: "layer-1",
|
||||
faceDetections,
|
||||
setImageLayerFaceBlur,
|
||||
setLayerEffect,
|
||||
removeLayerEffect,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -93,10 +107,11 @@ describe("useFaceBlurWorkflow", () => {
|
||||
result.current.blurFaces(result.current.selectedFaceIndices);
|
||||
});
|
||||
|
||||
expect(setImageLayerFaceBlur).toHaveBeenCalledTimes(1);
|
||||
expect(setImageLayerFaceBlur).toHaveBeenCalledWith(
|
||||
expect(setLayerEffect).toHaveBeenCalledTimes(1);
|
||||
expect(setLayerEffect).toHaveBeenCalledWith(
|
||||
"layer-1",
|
||||
expect.objectContaining({
|
||||
kind: "face-blur",
|
||||
method: "gaussian",
|
||||
amount: 14,
|
||||
regions: expect.arrayContaining([
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
import React from "react";
|
||||
import type { FaceBlurMethod, Layer } from "@pien-studio/types";
|
||||
import type { FaceBlurMethod, Layer, LayerEffect } from "@pien-studio/types";
|
||||
import type { FaceDetectionOverlay } from "./use-face-detection";
|
||||
|
||||
type FaceBlurPreview = {
|
||||
layerId: string;
|
||||
method: FaceBlurMethod;
|
||||
amount: number;
|
||||
regions: { x: number; y: number; width: number; height: number; sourceWidth: number; sourceHeight: number; censorColor?: string }[];
|
||||
censorColor?: string;
|
||||
effects: LayerEffect[];
|
||||
};
|
||||
|
||||
type UseFaceBlurWorkflowOptions = {
|
||||
selectedLayer: Layer | null;
|
||||
faceDetectionsLayerId: string | null;
|
||||
faceDetections: FaceDetectionOverlay[];
|
||||
setImageLayerFaceBlur: (layerId: string, faceBlur: Layer["faceBlur"] | undefined) => void;
|
||||
setLayerEffect: (layerId: string, effect: LayerEffect) => void;
|
||||
removeLayerEffect: (layerId: string, kind: LayerEffect["kind"]) => void;
|
||||
};
|
||||
|
||||
export function useFaceBlurWorkflow(options: UseFaceBlurWorkflowOptions) {
|
||||
const { selectedLayer, faceDetectionsLayerId, faceDetections, setImageLayerFaceBlur } = options;
|
||||
const { selectedLayer, faceDetectionsLayerId, faceDetections, setLayerEffect, removeLayerEffect } = options;
|
||||
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);
|
||||
const hasDetectableSelection = Boolean(selectedLayer && selectedLayer.type === "image" && faceDetectionsLayerId === selectedLayer.id);
|
||||
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],
|
||||
);
|
||||
|
||||
const buildBlurRegions = React.useCallback(
|
||||
(indices: number[]) => {
|
||||
if (!selectedLayer || selectedLayer.type !== "image") return [];
|
||||
if (!selectedLayer || selectedLayer.type !== "raster") return [];
|
||||
if (faceDetectionsLayerId !== selectedLayer.id || faceDetections.length === 0) return [];
|
||||
const indexSet = new Set(indices);
|
||||
return faceDetections
|
||||
@@ -57,17 +60,19 @@ export function useFaceBlurWorkflow(options: UseFaceBlurWorkflowOptions) {
|
||||
}, []);
|
||||
|
||||
const clearBlur = React.useCallback(() => {
|
||||
if (!selectedLayer || selectedLayer.type !== "image") return;
|
||||
setImageLayerFaceBlur(selectedLayer.id, undefined);
|
||||
if (!selectedLayer) return;
|
||||
removeLayerEffect(selectedLayer.id, "face-blur");
|
||||
setFaceBlurPreview(null);
|
||||
}, [selectedLayer, setImageLayerFaceBlur]);
|
||||
}, [selectedLayer, removeLayerEffect]);
|
||||
|
||||
const blurFaces = React.useCallback(
|
||||
(indices: number[]) => {
|
||||
if (!selectedLayer || selectedLayer.type !== "image" || !selectedLayer.sourceUri) return;
|
||||
if (!selectedLayer || selectedLayer.type !== "raster" || !selectedLayer.sourceUri) return;
|
||||
if (faceDetectionsLayerId !== selectedLayer.id || faceDetections.length === 0) return;
|
||||
const regions = buildBlurRegions(indices);
|
||||
setImageLayerFaceBlur(selectedLayer.id, {
|
||||
setLayerEffect(selectedLayer.id, {
|
||||
kind: "face-blur",
|
||||
enabled: true,
|
||||
method: blurMethod,
|
||||
amount: blurAmount,
|
||||
regions,
|
||||
@@ -76,7 +81,7 @@ export function useFaceBlurWorkflow(options: UseFaceBlurWorkflowOptions) {
|
||||
setSelectedFaceIndices([]);
|
||||
setFaceBlurPreview(null);
|
||||
},
|
||||
[blurAmount, blurMethod, buildBlurRegions, censorColor, faceDetections.length, faceDetectionsLayerId, selectedLayer, setImageLayerFaceBlur],
|
||||
[blurAmount, blurMethod, buildBlurRegions, censorColor, faceDetections.length, faceDetectionsLayerId, selectedLayer, setLayerEffect],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -85,7 +90,7 @@ export function useFaceBlurWorkflow(options: UseFaceBlurWorkflowOptions) {
|
||||
setFaceBlurPreview(null);
|
||||
return;
|
||||
}
|
||||
if (selectedLayer?.faceBlur) {
|
||||
if (faceBlurEffect) {
|
||||
setSelectedFaceIndices((prev) => (prev.length === 0 ? prev : []));
|
||||
return;
|
||||
}
|
||||
@@ -94,10 +99,10 @@ export function useFaceBlurWorkflow(options: UseFaceBlurWorkflowOptions) {
|
||||
if (prev.length === next.length && prev.every((value, index) => value === next[index])) return prev;
|
||||
return next;
|
||||
});
|
||||
}, [faceDetections, hasDetectableSelection, selectedLayer?.faceBlur]);
|
||||
}, [faceDetections, hasDetectableSelection, faceBlurEffect]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!hasDetectableSelection || !selectedLayer || selectedLayer.type !== "image") {
|
||||
if (!hasDetectableSelection || !selectedLayer || selectedLayer.type !== "raster") {
|
||||
setFaceBlurPreview(null);
|
||||
return;
|
||||
}
|
||||
@@ -109,10 +114,14 @@ export function useFaceBlurWorkflow(options: UseFaceBlurWorkflowOptions) {
|
||||
|
||||
setFaceBlurPreview({
|
||||
layerId: selectedLayer.id,
|
||||
method: blurMethod,
|
||||
amount: blurAmount,
|
||||
regions: buildBlurRegions(selectedFaceIndices),
|
||||
censorColor,
|
||||
effects: [{
|
||||
kind: "face-blur",
|
||||
enabled: true,
|
||||
method: blurMethod,
|
||||
amount: blurAmount,
|
||||
regions: buildBlurRegions(selectedFaceIndices),
|
||||
censorColor,
|
||||
}],
|
||||
});
|
||||
}, [blurAmount, blurMethod, buildBlurRegions, censorColor, hasDetectableSelection, selectedFaceIndices, selectedLayer]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user