| 1 | /** |
| 2 | * useCropEditor - 裁剪编辑器核心逻辑 Hook |
| 3 | * 处理裁剪框状态、比例锁定、确认/取消裁剪等功能 |
| 4 | */ |
| 5 | |
| 6 | import { useCallback, useRef, useState } from 'react' |
| 7 | |
| 8 | /** 裁剪比例预设 */ |
| 9 | export const CROP_RATIOS = [ |
| 10 | { label: 'free', value: 0 }, |
| 11 | { label: '1:1', value: 1 }, |
| 12 | { label: '1.91:1', value: 1.91 }, |
| 13 | { label: '4:5', value: 0.8 }, |
| 14 | { label: '16:9', value: 16 / 9 }, |
| 15 | { label: '9:16', value: 9 / 16 }, |
| 16 | { label: '4:3', value: 4 / 3 }, |
| 17 | { label: '3:4', value: 3 / 4 }, |
| 18 | { label: '2:3', value: 2 / 3 }, |
| 19 | { label: '3:2', value: 3 / 2 }, |
| 20 | ] as const |
| 21 | |
| 22 | export interface CropRect { |
| 23 | x: number |
| 24 | y: number |
| 25 | width: number |
| 26 | height: number |
| 27 | } |
| 28 | |
| 29 | export interface UseCropEditorOptions { |
| 30 | /** 获取当前 canvas 像素尺寸 */ |
| 31 | getCanvasSize: () => { width: number, height: number } |
| 32 | /** 裁剪确认回调 */ |
| 33 | onCropConfirm: (cropRect: CropRect) => void |
| 34 | } |
| 35 | |
| 36 | export function useCropEditor({ getCanvasSize, onCropConfirm }: UseCropEditorOptions) { |
| 37 | const [isCropping, setIsCropping] = useState(false) |
| 38 | const [cropRect, setCropRect] = useState<CropRect>({ x: 0, y: 0, width: 0, height: 0 }) |
| 39 | const [aspectRatio, setAspectRatio] = useState(0) // 0 = free |
| 40 | const aspectRatioRef = useRef(0) |
| 41 | |
| 42 | /** 开始裁剪 */ |
| 43 | const startCrop = useCallback(() => { |
| 44 | const { width, height } = getCanvasSize() |
| 45 | if (width <= 0 || height <= 0) |
| 46 | return |
| 47 | |
| 48 | // 初始裁剪框为图片 80% 居中 |
| 49 | const margin = 0.1 |
| 50 | let cw = width * (1 - margin * 2) |
| 51 | let ch = height * (1 - margin * 2) |
| 52 | |
| 53 | if (aspectRatioRef.current > 0) { |
| 54 | const ratio = aspectRatioRef.current |
| 55 | if (cw / ch > ratio) { |
| 56 | cw = ch * ratio |
| 57 | } |
| 58 | else { |
| 59 | ch = cw / ratio |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | setCropRect({ |
| 64 | x: (width - cw) / 2, |
| 65 | y: (height - ch) / 2, |
| 66 | width: cw, |
| 67 | height: ch, |
| 68 | }) |
| 69 | setIsCropping(true) |
| 70 | }, [getCanvasSize]) |
| 71 | |
| 72 | /** 确认裁剪 */ |
| 73 | const confirmCrop = useCallback(() => { |
| 74 | if (!isCropping) |
| 75 | return |
| 76 | |
| 77 | // 确保裁剪区域有效 |
| 78 | if (cropRect.width < 10 || cropRect.height < 10) |
| 79 | return |
| 80 | |
| 81 | onCropConfirm(cropRect) |
| 82 | setIsCropping(false) |
| 83 | }, [isCropping, cropRect, onCropConfirm]) |
| 84 | |
| 85 | /** 取消裁剪 */ |
| 86 | const cancelCrop = useCallback(() => { |
| 87 | setIsCropping(false) |
| 88 | }, []) |
| 89 | |
| 90 | /** 设置比例并调整裁剪框 */ |
| 91 | const changeAspectRatio = useCallback((ratio: number) => { |
| 92 | setAspectRatio(ratio) |
| 93 | aspectRatioRef.current = ratio |
| 94 | |
| 95 | if (!isCropping) |
| 96 | return |
| 97 | |
| 98 | const { width: canvasW, height: canvasH } = getCanvasSize() |
| 99 | if (canvasW <= 0 || canvasH <= 0) |
| 100 | return |
| 101 | |
| 102 | setCropRect((prev) => { |
| 103 | if (ratio === 0) |
| 104 | return prev |
| 105 | |
| 106 | // 以当前裁剪框中心为基点,按新比例调整 |
| 107 | const cx = prev.x + prev.width / 2 |
| 108 | const cy = prev.y + prev.height / 2 |
| 109 | |
| 110 | let newW = prev.width |
| 111 | let newH = newW / ratio |
| 112 | |
| 113 | if (newH > canvasH) { |
| 114 | newH = canvasH |
| 115 | newW = newH * ratio |
| 116 | } |
| 117 | if (newW > canvasW) { |
| 118 | newW = canvasW |
| 119 | newH = newW / ratio |
| 120 | } |
| 121 | |
| 122 | let newX = cx - newW / 2 |
| 123 | let newY = cy - newH / 2 |
| 124 | |
| 125 | // 边界约束 |
| 126 | newX = Math.max(0, Math.min(newX, canvasW - newW)) |
| 127 | newY = Math.max(0, Math.min(newY, canvasH - newH)) |
| 128 | |
| 129 | return { x: newX, y: newY, width: newW, height: newH } |
| 130 | }) |
| 131 | }, [isCropping, getCanvasSize]) |
| 132 | |
| 133 | return { |
| 134 | isCropping, |
| 135 | cropRect, |
| 136 | setCropRect, |
| 137 | aspectRatio, |
| 138 | startCrop, |
| 139 | confirmCrop, |
| 140 | cancelCrop, |
| 141 | changeAspectRatio, |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | export type UseCropEditorReturn = ReturnType<typeof useCropEditor> |
| 146 |