"use client"; import React from "react"; import NextImage from "next/image"; import { RotateCw } from "lucide-react"; import { CANVAS_HANDLE_BASE_SIZE, CANVAS_ROTATE_HANDLE_BASE_SIZE, } from "../lib/editor-constants"; import { buildFaceLabelOverlays } from "../lib/canvas-geometry"; import { renderImageWithFaceBlur } from "../lib/face-blur-renderer"; import { useCanvasInteractions } from "../hooks/use-canvas-interactions"; import { useTranslations } from "../hooks/use-translations"; import type { FaceBlurMethod, Layer } from "@pien-studio/types"; interface CanvasRendererProps { layers: Layer[]; canvasWidth: number; canvasHeight: number; selectedLayerId: string | null; onSelectLayer: (id: string | null) => void; onMoveLayer: (id: string, x: number, y: number) => void; onMoveLayerEnd?: (id: string, x: number, y: number) => void; onResizeLayer?: (id: string, width: number, height: number) => void; onResizeLayerEnd?: (id: string, width: number, height: number) => void; onRotateLayer?: (id: string, rotation: number) => void; onRotateLayerEnd?: (id: string, rotation: number) => void; onInteractionStart?: () => void; onInteractionEnd?: () => void; onContextMenu?: (x: number, y: number) => void; isDark: boolean; tool?: "pointer" | "hand" | "face"; faceDetections?: { x: number; y: number; width: number; height: number; label?: string; }[]; faceOverlayLayerId?: string | null; faceBlurPreview?: { layerId: string; method: FaceBlurMethod; amount: number; regions: { x: number; y: number; width: number; height: number }[]; } | null; } function BlurredImageLayer({ layer, width, height, faceBlurOverride, }: { layer: Layer; width: number; height: number; faceBlurOverride?: { method: FaceBlurMethod; amount: number; regions: { x: number; y: number; width: number; height: number; censorColor?: string; }[]; censorColor?: string; } | null; }) { const canvasRef = React.useRef(null); const imageRef = React.useRef(null); const draw = React.useCallback(() => { const canvas = canvasRef.current; const image = imageRef.current; if (!canvas || !image) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const cw = canvas.width; const ch = canvas.height; ctx.clearRect(0, 0, cw, ch); const blur = faceBlurOverride ?? layer.faceBlur; renderImageWithFaceBlur(ctx, image, blur, cw, ch); }, [faceBlurOverride, layer.faceBlur]); React.useEffect(() => { if (!layer.sourceUri) return; let canceled = false; const image = new Image(); image.crossOrigin = "anonymous"; image.onload = () => { if (canceled) return; imageRef.current = image; draw(); }; image.src = layer.sourceUri; return () => { canceled = true; }; }, [draw, layer.sourceUri]); React.useEffect(() => { draw(); }, [draw, width, height]); return ( ); } export function CanvasRenderer({ layers, canvasWidth, canvasHeight, selectedLayerId, onSelectLayer, onMoveLayer, onMoveLayerEnd, onResizeLayer, onResizeLayerEnd, onRotateLayer, onRotateLayerEnd, onInteractionStart, onInteractionEnd, onContextMenu, isDark, tool = "pointer", faceDetections = [], faceOverlayLayerId = null, faceBlurPreview = null, }: CanvasRendererProps) { const { t } = useTranslations(); const { containerRef, viewport, isSpacePan, onContainerPointerDown, onContainerPointerMove, onContainerPointerUp, onLayerPointerDown, onResizeHandleDown, onRotateHandleDown, onTouchStart, onTouchMove, onTouchEnd, onContextMenuOpen, } = useCanvasInteractions({ canvasWidth, canvasHeight, tool, onSelectLayer, onMoveLayer, onMoveLayerEnd, onResizeLayer, onResizeLayerEnd, onRotateLayer, onRotateLayerEnd, onInteractionStart, onInteractionEnd, onContextMenu, }); const faceLabelOverlays = React.useMemo(() => { if (tool !== "face" || !faceOverlayLayerId || faceDetections.length === 0) return []; return buildFaceLabelOverlays( layers, faceOverlayLayerId, faceDetections, viewport, ); }, [ faceDetections, faceOverlayLayerId, layers, tool, viewport.scale, viewport.x, viewport.y, ]); return (
{ if (tool === "pointer" && e.button === 0) onSelectLayer(null); onContainerPointerDown(e); }} onMouseDown={(e) => e.preventDefault()} onPointerMove={onContainerPointerMove} onPointerUp={onContainerPointerUp} onPointerCancel={onContainerPointerUp} onDoubleClick={(e) => { e.preventDefault(); e.stopPropagation(); }} onContextMenu={onContextMenuOpen} onTouchStart={onTouchStart} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd} >
{layers.map((layer, idx) => { const isSelected = layer.id === selectedLayerId; const isImage = layer.type === "image"; const layerWidth = layer.width ?? (isImage ? Math.round(200 * layer.scale) : undefined); const layerHeight = layer.height ?? (isImage ? Math.round(150 * layer.scale) : undefined); const handleSize = CANVAS_HANDLE_BASE_SIZE / viewport.scale; const handleSizePx = `${handleSize}px`; const largeHandleSize = CANVAS_ROTATE_HANDLE_BASE_SIZE / viewport.scale; const largeHandleSizePx = `${largeHandleSize}px`; return (
onLayerPointerDown(e, layer)} onClick={() => onSelectLayer(layer.id)} > {isImage && layer.sourceUri ? ( (faceBlurPreview && faceBlurPreview.layerId === layer.id && faceBlurPreview.regions.length > 0) || (layer.faceBlur && layer.faceBlur.regions.length > 0) ? ( ) : ( ) ) : (
{layer.type}
)} {tool === "face" && faceOverlayLayerId === layer.id ? faceDetections.map((face, index) => (
)) : null} {isSelected && tool === "pointer" && isImage && onResizeLayer ? ( <>
) : null}
); })}
{faceLabelOverlays.map((label) => ( {label.text} ))}
); }