import type Konva from "konva"; import { useCallback, useEffect, useRef, useState } from "react"; import { Group, Line, Rect, Transformer } from "react-konva"; import { useEditorStore } from "@/stores/editor-store"; import type { CropState } from "@/types/editor"; // --------------------------------------------------------------------------- // Aspect ratio presets // --------------------------------------------------------------------------- export const ASPECT_RATIOS = [ { label: "Free", value: null }, { label: "1:1", value: 1 }, { label: "4:3", value: 4 / 3 }, { label: "3:4", value: 3 / 4 }, { label: "16:9", value: 16 / 9 }, { label: "9:16", value: 9 / 16 }, { label: "3:2", value: 3 / 2 }, { label: "2:3", value: 2 / 3 }, ] as const; // --------------------------------------------------------------------------- // Hook: useCropTool // --------------------------------------------------------------------------- export interface CropToolApi { cropRef: React.RefObject; transformerRef: React.RefObject; cropState: CropState | null; aspectRatio: string; setAspectRatio: (label: string) => void; initCrop: () => void; applyCrop: () => void; cancelCrop: () => void; updateCropSize: (w: number, h: number) => void; swapDimensions: () => void; } export function useCropTool(): CropToolApi { const cropRef = useRef(null); const transformerRef = useRef(null); const [aspectRatio, setAspectRatioState] = useState("Free"); const cropState = useEditorStore((s) => s.cropState); const setCropState = useEditorStore((s) => s.setCropState); const applyCropAction = useEditorStore((s) => s.applyCrop); const canvasSize = useEditorStore((s) => s.canvasSize); // Attach transformer to crop rect useEffect(() => { const tr = transformerRef.current; const node = cropRef.current; if (tr && node && cropState) { tr.nodes([node]); tr.getLayer()?.batchDraw(); } }, [cropState]); const initCrop = useCallback(() => { const margin = 0.1; setCropState({ x: canvasSize.width * margin, y: canvasSize.height * margin, width: canvasSize.width * (1 - 2 * margin), height: canvasSize.height * (1 - 2 * margin), aspectRatio: null, }); }, [canvasSize, setCropState]); const setAspectRatio = useCallback( (label: string) => { setAspectRatioState(label); const preset = ASPECT_RATIOS.find((p) => p.label === label); if (!preset || !preset.value || !cropState) return; const ratio = preset.value; let w = cropState.width; let h = w / ratio; if (h > canvasSize.height) { h = canvasSize.height * 0.8; w = h * ratio; } setCropState({ ...cropState, width: w, height: h, aspectRatio: label, }); }, [cropState, canvasSize, setCropState], ); const applyCrop = useCallback(() => { applyCropAction(); }, [applyCropAction]); const cancelCrop = useCallback(() => { setCropState(null); }, [setCropState]); const updateCropSize = useCallback( (w: number, h: number) => { if (!cropState) return; setCropState({ ...cropState, width: w, height: h }); }, [cropState, setCropState], ); const swapDimensions = useCallback(() => { if (!cropState) return; setCropState({ ...cropState, width: cropState.height, height: cropState.width, }); }, [cropState, setCropState]); return { cropRef, transformerRef, cropState, aspectRatio, setAspectRatio, initCrop, applyCrop, cancelCrop, updateCropSize, swapDimensions, }; } // --------------------------------------------------------------------------- // CropOverlay -- renders darkened overlay + crop region + rule-of-thirds // --------------------------------------------------------------------------- export function CropOverlay() { const cropState = useEditorStore((s) => s.cropState); const canvasSize = useEditorStore((s) => s.canvasSize); const setCropState = useEditorStore((s) => s.setCropState); const cropRectRef = useRef(null); const trRef = useRef(null); useEffect(() => { if (trRef.current && cropRectRef.current && cropState) { trRef.current.nodes([cropRectRef.current]); trRef.current.getLayer()?.batchDraw(); } }, [cropState]); if (!cropState) return null; const { x, y, width, height } = cropState; const cw = canvasSize.width; const ch = canvasSize.height; const overlayFill = "rgba(0, 0, 0, 0.5)"; // Rule-of-thirds grid lines const thirdW = width / 3; const thirdH = height / 3; const handleTransformEnd = () => { const node = cropRectRef.current; if (!node) return; const scaleX = node.scaleX(); const scaleY = node.scaleY(); const newW = Math.max(10, node.width() * scaleX); const newH = Math.max(10, node.height() * scaleY); node.scaleX(1); node.scaleY(1); setCropState({ ...cropState, x: node.x(), y: node.y(), width: newW, height: newH, }); }; const handleDragEnd = () => { const node = cropRectRef.current; if (!node) return; setCropState({ ...cropState, x: Math.max(0, Math.min(node.x(), cw - width)), y: Math.max(0, Math.min(node.y(), ch - height)), }); }; return ( {/* Darkened overlays: top, bottom, left, right */} {/* Crop region */} {/* Rule-of-thirds grid */} {/* Transformer */} ({ ...newBox, width: Math.max(10, newBox.width), height: Math.max(10, newBox.height), })} /> ); }