返回 AiToEarn
useCropEditor.ts
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
146 lines TYPESCRIPT