"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 { renderLayerWithEffects } from "../lib/effects/registry"; import { getToolUiDefinition } from "../lib/tools/registry"; import { getToolDefinition } from "@pien-studio/editor-core"; import { useCanvasInteractions } from "../hooks/use-canvas-interactions"; import { useTranslations } from "../hooks/use-translations"; import { createStroke, paintSegment, type BrushStroke, type BrushOptions } from "../lib/brush-painter"; import type { Layer, LayerEffect } 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?: string; onFillLayer?: (layerId: string, x: number, y: number) => void; brushOptions?: BrushOptions; onBrushCommit?: (layerId: string, stroke: BrushStroke) => void; faceDetections?: { x: number; y: number; width: number; height: number; label?: string; }[]; faceOverlayLayerId?: string | null; faceBlurPreview?: { layerId: string; effects: LayerEffect[]; } | null; } function BrushOverlayCanvas({ stroke, width, height }: { stroke: HTMLCanvasElement; width: number; height: number }) { const canvasRef = React.useRef(null); React.useEffect(() => { const el = canvasRef.current; if (!el) return; const ctx = el.getContext("2d"); if (!ctx) return; ctx.clearRect(0, 0, el.width, el.height); ctx.drawImage(stroke, 0, 0, el.width, el.height); }); return ( ); } function EffectImageLayer({ layer, width, height, effectsOverride, }: { layer: Layer; width: number; height: number; effectsOverride?: LayerEffect[] | null; }) { const canvasRef = React.useRef(null); const imageRef = React.useRef(null); const activeEffects = effectsOverride ?? layer.effects; const draw = React.useCallback(() => { const canvas = canvasRef.current; const image = imageRef.current; if (!canvas || !image) return; const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); renderLayerWithEffects(ctx, image, activeEffects, canvas.width, canvas.height); }, [activeEffects]); 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, onFillLayer, brushOptions, onBrushCommit, isDark, tool = "pointer", faceDetections = [], faceOverlayLayerId = null, faceBlurPreview = null, }: CanvasRendererProps) { const { t } = useTranslations(); const brushStrokeRef = React.useRef(null); const brushLastPosRef = React.useRef<{ x: number; y: number } | null>(null); const [brushOverlay, setBrushOverlay] = React.useState<{ layerId: string; canvas: HTMLCanvasElement } | null>(null); const handleBrushStrokeStart = React.useCallback((layerId: string, x: number, y: number, layerWidth: number, layerHeight: number): void => { if (!brushOptions) return; const stroke = createStroke(layerWidth, layerHeight); brushStrokeRef.current = stroke; brushLastPosRef.current = { x, y }; paintSegment(stroke, x, y, x, y, brushOptions); setBrushOverlay({ layerId, canvas: stroke.canvas }); }, [brushOptions]); const handleBrushStrokeMove = React.useCallback((_layerId: string, x: number, y: number) => { if (!brushStrokeRef.current || !brushLastPosRef.current || !brushOptions) return; const { x: lx, y: ly } = brushLastPosRef.current; paintSegment(brushStrokeRef.current, lx, ly, x, y, brushOptions); brushLastPosRef.current = { x, y }; setBrushOverlay((prev) => prev ? { ...prev } : prev); }, [brushOptions]); const handleBrushStrokeEnd = React.useCallback((layerId: string) => { const stroke = brushStrokeRef.current; brushStrokeRef.current = null; brushLastPosRef.current = null; setBrushOverlay(null); if (stroke && onBrushCommit) onBrushCommit(layerId, stroke); }, [onBrushCommit]); 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, onFillLayer, onBrushStrokeStart: handleBrushStrokeStart, onBrushStrokeMove: handleBrushStrokeMove, onBrushStrokeEnd: handleBrushStrokeEnd, }); 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 (getToolDefinition(tool)?.interactionMode === "select" && 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 === "raster"; 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 ? ( layer.effects.length > 0 || (faceBlurPreview && faceBlurPreview.layerId === layer.id) ? ( ) : ( ) ) : (
{layer.type}
)} {brushOverlay && brushOverlay.layerId === layer.id ? ( ) : null} {tool === "face" && faceOverlayLayerId === layer.id ? faceDetections.map((face, index) => (
)) : null} {isSelected && tool === "pointer" && isImage && onResizeLayer ? ( <>
) : null}
); })}
{faceLabelOverlays.map((label) => ( {label.text} ))}
); }