mjpic 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <meta name="description" content="敏捷图片(mjpic)是一个轻量级网页版图片处理工具,设计灵感来源于光影魔术手。" />
8
8
  <title>敏捷图片 (mjpic) - 轻量级网页版图片处理工具</title>
9
- <script type="module" crossorigin src="/assets/index-Tbz-to9P.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-DORSlWYO.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="/assets/index-C6nMBMvY.css">
11
11
  </head>
12
12
  <body>
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "mjpic",
3
3
  "description": "敏捷图片(mjpic)是一个轻量级网页版图片处理工具,设计灵感来源于光影魔术手。",
4
- "version": "1.0.19",
4
+ "version": "1.0.21",
5
5
  "type": "module",
6
6
  "bin": {
7
- "mjpic": "./dist/cli/cli.js"
7
+ "mjpic": "dist/cli/cli.js"
8
8
  },
9
9
  "scripts": {
10
10
  "client:dev": "vite",
@@ -30,7 +30,6 @@
30
30
  "i18next-browser-languagedetector": "^8.2.1",
31
31
  "konva": "^9.3.16",
32
32
  "lucide-react": "^0.511.0",
33
- "mjpic": "^1.0.7",
34
33
  "open": "^11.0.0",
35
34
  "react": "^18.3.1",
36
35
  "react-dom": "^18.3.1",
@@ -10,6 +10,19 @@ interface CanvasAreaProps {
10
10
  stageRef: React.MutableRefObject<Konva.Stage | null>;
11
11
  }
12
12
 
13
+ type TransformBox = {
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ rotation: number;
19
+ };
20
+
21
+ type Point = {
22
+ x: number;
23
+ y: number;
24
+ };
25
+
13
26
  export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
14
27
  const { previewImage, config, loadImage, setOriginalSize, originalWidth, originalHeight, cropRect, setCropRect, updateConfig } = useImageStore();
15
28
  const { isStraightenToolActive, setStraightenToolActive, activeTool } = useUIStore();
@@ -29,6 +42,8 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
29
42
  const imgDimensionsRef = useRef({ width: 0, height: 0 });
30
43
  const cropDragRafRef = useRef<number | null>(null);
31
44
  const pendingCropRectRef = useRef<{ x: number; y: number; width: number; height: number } | null>(null);
45
+ const pendingTransformBoxRef = useRef<TransformBox | null>(null);
46
+ const transformStartBoxRef = useRef<TransformBox | null>(null);
32
47
 
33
48
  // Straighten Tool State
34
49
  const [straightenLine, setStraightenLine] = useState<{ start: {x: number, y: number}, end: {x: number, y: number} } | null>(null);
@@ -108,6 +123,300 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
108
123
  contentX = cx - displayTotalWidth / 2;
109
124
  contentY = cy - displayTotalHeight / 2;
110
125
  }
126
+
127
+ const clampValue = (value: number, min: number, max: number) => {
128
+ if (min > max) return min;
129
+ return Math.min(Math.max(value, min), max);
130
+ };
131
+
132
+ const resolveCropTransformBox = (oldBox: TransformBox, proposedBox: TransformBox, activeAnchor?: string | null): TransformBox => {
133
+ const boundsLeft = contentX;
134
+ const boundsTop = contentY;
135
+ const boundsRight = contentX + displayTotalWidth;
136
+ const boundsBottom = contentY + displayTotalHeight;
137
+
138
+ const oldLeft = oldBox.x;
139
+ const oldTop = oldBox.y;
140
+ const oldRight = oldBox.x + oldBox.width;
141
+ const oldBottom = oldBox.y + oldBox.height;
142
+ const oldCenterX = oldBox.x + oldBox.width / 2;
143
+ const oldCenterY = oldBox.y + oldBox.height / 2;
144
+ const makeBox = (x: number, y: number, width: number, height: number): TransformBox => ({
145
+ x,
146
+ y,
147
+ width,
148
+ height,
149
+ rotation: oldBox.rotation
150
+ });
151
+
152
+ const minFreeWidth = 20;
153
+ const minFreeHeight = 20;
154
+
155
+ const aspectRatio = !isFreeCrop && config.crop?.aspectRatio
156
+ ? (() => {
157
+ const [w, h] = config.crop.aspectRatio.split(':').map((value) => parseInt(value, 10));
158
+ return w > 0 && h > 0 ? w / h : null;
159
+ })()
160
+ : null;
161
+
162
+ if (!activeAnchor) {
163
+ const width = clampValue(proposedBox.width, minFreeWidth, boundsRight - boundsLeft);
164
+ const height = clampValue(proposedBox.height, minFreeHeight, boundsBottom - boundsTop);
165
+ return {
166
+ ...makeBox(
167
+ clampValue(proposedBox.x, boundsLeft, boundsRight - width),
168
+ clampValue(proposedBox.y, boundsTop, boundsBottom - height),
169
+ width,
170
+ height
171
+ )
172
+ };
173
+ }
174
+
175
+ if (!aspectRatio) {
176
+ if (activeAnchor === 'top-left') {
177
+ const left = clampValue(proposedBox.x, boundsLeft, oldRight - minFreeWidth);
178
+ const top = clampValue(proposedBox.y, boundsTop, oldBottom - minFreeHeight);
179
+ return makeBox(left, top, oldRight - left, oldBottom - top);
180
+ }
181
+ if (activeAnchor === 'top-right') {
182
+ const right = clampValue(proposedBox.x + proposedBox.width, oldLeft + minFreeWidth, boundsRight);
183
+ const top = clampValue(proposedBox.y, boundsTop, oldBottom - minFreeHeight);
184
+ return makeBox(oldLeft, top, right - oldLeft, oldBottom - top);
185
+ }
186
+ if (activeAnchor === 'bottom-left') {
187
+ const left = clampValue(proposedBox.x, boundsLeft, oldRight - minFreeWidth);
188
+ const bottom = clampValue(proposedBox.y + proposedBox.height, oldTop + minFreeHeight, boundsBottom);
189
+ return makeBox(left, oldTop, oldRight - left, bottom - oldTop);
190
+ }
191
+ if (activeAnchor === 'bottom-right') {
192
+ const right = clampValue(proposedBox.x + proposedBox.width, oldLeft + minFreeWidth, boundsRight);
193
+ const bottom = clampValue(proposedBox.y + proposedBox.height, oldTop + minFreeHeight, boundsBottom);
194
+ return makeBox(oldLeft, oldTop, right - oldLeft, bottom - oldTop);
195
+ }
196
+ if (activeAnchor === 'top-center') {
197
+ const top = clampValue(proposedBox.y, boundsTop, oldBottom - minFreeHeight);
198
+ return makeBox(oldLeft, top, oldBox.width, oldBottom - top);
199
+ }
200
+ if (activeAnchor === 'bottom-center') {
201
+ const bottom = clampValue(proposedBox.y + proposedBox.height, oldTop + minFreeHeight, boundsBottom);
202
+ return makeBox(oldLeft, oldTop, oldBox.width, bottom - oldTop);
203
+ }
204
+ if (activeAnchor === 'middle-left') {
205
+ const left = clampValue(proposedBox.x, boundsLeft, oldRight - minFreeWidth);
206
+ return makeBox(left, oldTop, oldRight - left, oldBox.height);
207
+ }
208
+ if (activeAnchor === 'middle-right') {
209
+ const right = clampValue(proposedBox.x + proposedBox.width, oldLeft + minFreeWidth, boundsRight);
210
+ return makeBox(oldLeft, oldTop, right - oldLeft, oldBox.height);
211
+ }
212
+ }
213
+
214
+ const ratio = aspectRatio || 1;
215
+ const minRatioWidth = Math.max(minFreeWidth, minFreeHeight * ratio);
216
+
217
+ if (activeAnchor === 'top-left') {
218
+ const widthFromX = clampValue(oldRight - proposedBox.x, minRatioWidth, oldRight - boundsLeft);
219
+ const widthFromY = clampValue((oldBottom - proposedBox.y) * ratio, minRatioWidth, (oldBottom - boundsTop) * ratio);
220
+ const width = Math.min(Math.max(widthFromX, widthFromY), oldRight - boundsLeft, (oldBottom - boundsTop) * ratio);
221
+ const height = width / ratio;
222
+ return makeBox(oldRight - width, oldBottom - height, width, height);
223
+ }
224
+ if (activeAnchor === 'top-right') {
225
+ const widthFromX = clampValue(proposedBox.x + proposedBox.width - oldLeft, minRatioWidth, boundsRight - oldLeft);
226
+ const widthFromY = clampValue((oldBottom - proposedBox.y) * ratio, minRatioWidth, (oldBottom - boundsTop) * ratio);
227
+ const width = Math.min(Math.max(widthFromX, widthFromY), boundsRight - oldLeft, (oldBottom - boundsTop) * ratio);
228
+ const height = width / ratio;
229
+ return makeBox(oldLeft, oldBottom - height, width, height);
230
+ }
231
+ if (activeAnchor === 'bottom-left') {
232
+ const widthFromX = clampValue(oldRight - proposedBox.x, minRatioWidth, oldRight - boundsLeft);
233
+ const widthFromY = clampValue((proposedBox.y + proposedBox.height - oldTop) * ratio, minRatioWidth, (boundsBottom - oldTop) * ratio);
234
+ const width = Math.min(Math.max(widthFromX, widthFromY), oldRight - boundsLeft, (boundsBottom - oldTop) * ratio);
235
+ const height = width / ratio;
236
+ return makeBox(oldRight - width, oldTop, width, height);
237
+ }
238
+ if (activeAnchor === 'bottom-right') {
239
+ const widthFromX = clampValue(proposedBox.x + proposedBox.width - oldLeft, minRatioWidth, boundsRight - oldLeft);
240
+ const widthFromY = clampValue((proposedBox.y + proposedBox.height - oldTop) * ratio, minRatioWidth, (boundsBottom - oldTop) * ratio);
241
+ const width = Math.min(Math.max(widthFromX, widthFromY), boundsRight - oldLeft, (boundsBottom - oldTop) * ratio);
242
+ const height = width / ratio;
243
+ return makeBox(oldLeft, oldTop, width, height);
244
+ }
245
+ if (activeAnchor === 'top-center') {
246
+ const height = clampValue(oldBottom - proposedBox.y, minFreeHeight, oldBottom - boundsTop);
247
+ const width = Math.min(Math.max(height * ratio, minRatioWidth), displayTotalWidth);
248
+ const x = clampValue(oldCenterX - width / 2, boundsLeft, boundsRight - width);
249
+ return makeBox(x, oldBottom - height, width, height);
250
+ }
251
+ if (activeAnchor === 'bottom-center') {
252
+ const height = clampValue(proposedBox.y + proposedBox.height - oldTop, minFreeHeight, boundsBottom - oldTop);
253
+ const width = Math.min(Math.max(height * ratio, minRatioWidth), displayTotalWidth);
254
+ const x = clampValue(oldCenterX - width / 2, boundsLeft, boundsRight - width);
255
+ return makeBox(x, oldTop, width, height);
256
+ }
257
+ if (activeAnchor === 'middle-left') {
258
+ const width = clampValue(oldRight - proposedBox.x, minRatioWidth, oldRight - boundsLeft);
259
+ const height = width / ratio;
260
+ const y = clampValue(oldCenterY - height / 2, boundsTop, boundsBottom - height);
261
+ return makeBox(oldRight - width, y, width, height);
262
+ }
263
+ if (activeAnchor === 'middle-right') {
264
+ const width = clampValue(proposedBox.x + proposedBox.width - oldLeft, minRatioWidth, boundsRight - oldLeft);
265
+ const height = width / ratio;
266
+ const y = clampValue(oldCenterY - height / 2, boundsTop, boundsBottom - height);
267
+ return makeBox(oldLeft, y, width, height);
268
+ }
269
+
270
+ return oldBox;
271
+ };
272
+
273
+ const resolveAnchorDragPosition = (baseBox: TransformBox, proposedPos: Point, activeAnchor?: string | null): Point => {
274
+ const boundsLeft = contentX;
275
+ const boundsTop = contentY;
276
+ const boundsRight = contentX + displayTotalWidth;
277
+ const boundsBottom = contentY + displayTotalHeight;
278
+
279
+ const left = baseBox.x;
280
+ const top = baseBox.y;
281
+ const right = baseBox.x + baseBox.width;
282
+ const bottom = baseBox.y + baseBox.height;
283
+ const centerX = baseBox.x + baseBox.width / 2;
284
+ const centerY = baseBox.y + baseBox.height / 2;
285
+
286
+ const minFreeWidth = 20;
287
+ const minFreeHeight = 20;
288
+ const aspectRatio = !isFreeCrop && config.crop?.aspectRatio
289
+ ? (() => {
290
+ const [w, h] = config.crop.aspectRatio.split(':').map((value) => parseInt(value, 10));
291
+ return w > 0 && h > 0 ? w / h : null;
292
+ })()
293
+ : null;
294
+
295
+ if (!activeAnchor) {
296
+ return proposedPos;
297
+ }
298
+
299
+ if (!aspectRatio) {
300
+ if (activeAnchor === 'top-left') {
301
+ return {
302
+ x: clampValue(proposedPos.x, boundsLeft, right - minFreeWidth),
303
+ y: clampValue(proposedPos.y, boundsTop, bottom - minFreeHeight)
304
+ };
305
+ }
306
+ if (activeAnchor === 'top-right') {
307
+ return {
308
+ x: clampValue(proposedPos.x, left + minFreeWidth, boundsRight),
309
+ y: clampValue(proposedPos.y, boundsTop, bottom - minFreeHeight)
310
+ };
311
+ }
312
+ if (activeAnchor === 'bottom-left') {
313
+ return {
314
+ x: clampValue(proposedPos.x, boundsLeft, right - minFreeWidth),
315
+ y: clampValue(proposedPos.y, top + minFreeHeight, boundsBottom)
316
+ };
317
+ }
318
+ if (activeAnchor === 'bottom-right') {
319
+ return {
320
+ x: clampValue(proposedPos.x, left + minFreeWidth, boundsRight),
321
+ y: clampValue(proposedPos.y, top + minFreeHeight, boundsBottom)
322
+ };
323
+ }
324
+ if (activeAnchor === 'top-center') {
325
+ return {
326
+ x: centerX,
327
+ y: clampValue(proposedPos.y, boundsTop, bottom - minFreeHeight)
328
+ };
329
+ }
330
+ if (activeAnchor === 'bottom-center') {
331
+ return {
332
+ x: centerX,
333
+ y: clampValue(proposedPos.y, top + minFreeHeight, boundsBottom)
334
+ };
335
+ }
336
+ if (activeAnchor === 'middle-left') {
337
+ return {
338
+ x: clampValue(proposedPos.x, boundsLeft, right - minFreeWidth),
339
+ y: centerY
340
+ };
341
+ }
342
+ if (activeAnchor === 'middle-right') {
343
+ return {
344
+ x: clampValue(proposedPos.x, left + minFreeWidth, boundsRight),
345
+ y: centerY
346
+ };
347
+ }
348
+ }
349
+
350
+ const ratio = aspectRatio || 1;
351
+ const minRatioWidth = Math.max(minFreeWidth, minFreeHeight * ratio);
352
+
353
+ if (activeAnchor === 'top-left') {
354
+ const width = Math.min(
355
+ Math.max(right - proposedPos.x, (bottom - proposedPos.y) * ratio, minRatioWidth),
356
+ right - boundsLeft,
357
+ (bottom - boundsTop) * ratio
358
+ );
359
+ return { x: right - width, y: bottom - width / ratio };
360
+ }
361
+ if (activeAnchor === 'top-right') {
362
+ const width = Math.min(
363
+ Math.max(proposedPos.x - left, (bottom - proposedPos.y) * ratio, minRatioWidth),
364
+ boundsRight - left,
365
+ (bottom - boundsTop) * ratio
366
+ );
367
+ return { x: left + width, y: bottom - width / ratio };
368
+ }
369
+ if (activeAnchor === 'bottom-left') {
370
+ const width = Math.min(
371
+ Math.max(right - proposedPos.x, (proposedPos.y - top) * ratio, minRatioWidth),
372
+ right - boundsLeft,
373
+ (boundsBottom - top) * ratio
374
+ );
375
+ return { x: right - width, y: top + width / ratio };
376
+ }
377
+ if (activeAnchor === 'bottom-right') {
378
+ const width = Math.min(
379
+ Math.max(proposedPos.x - left, (proposedPos.y - top) * ratio, minRatioWidth),
380
+ boundsRight - left,
381
+ (boundsBottom - top) * ratio
382
+ );
383
+ return { x: left + width, y: top + width / ratio };
384
+ }
385
+ if (activeAnchor === 'top-center') {
386
+ const height = Math.min(
387
+ Math.max(bottom - proposedPos.y, minFreeHeight),
388
+ bottom - boundsTop,
389
+ displayTotalWidth / ratio
390
+ );
391
+ return { x: centerX, y: bottom - height };
392
+ }
393
+ if (activeAnchor === 'bottom-center') {
394
+ const height = Math.min(
395
+ Math.max(proposedPos.y - top, minFreeHeight),
396
+ boundsBottom - top,
397
+ displayTotalWidth / ratio
398
+ );
399
+ return { x: centerX, y: top + height };
400
+ }
401
+ if (activeAnchor === 'middle-left') {
402
+ const width = Math.min(
403
+ Math.max(right - proposedPos.x, minRatioWidth),
404
+ right - boundsLeft,
405
+ displayTotalHeight * ratio
406
+ );
407
+ return { x: right - width, y: centerY };
408
+ }
409
+ if (activeAnchor === 'middle-right') {
410
+ const width = Math.min(
411
+ Math.max(proposedPos.x - left, minRatioWidth),
412
+ boundsRight - left,
413
+ displayTotalHeight * ratio
414
+ );
415
+ return { x: left + width, y: centerY };
416
+ }
417
+
418
+ return proposedPos;
419
+ };
111
420
 
112
421
  useEffect(() => {
113
422
  if (image && image.width && image.height) {
@@ -760,8 +1069,8 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
760
1069
  }}
761
1070
  onDragMove={(e) => {
762
1071
  const node = e.target;
763
- const newX = (node.x() - contentX) / scale;
764
- const newY = (node.y() - contentY) / scale;
1072
+ const newX = Math.round((node.x() - contentX) / scale);
1073
+ const newY = Math.round((node.y() - contentY) / scale);
765
1074
 
766
1075
  pendingCropRectRef.current = { ...cropRect, x: newX, y: newY };
767
1076
  if (cropDragRafRef.current !== null) return;
@@ -782,8 +1091,8 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
782
1091
  pendingCropRectRef.current = null;
783
1092
 
784
1093
  const node = e.target;
785
- const newX = (node.x() - contentX) / scale;
786
- const newY = (node.y() - contentY) / scale;
1094
+ const newX = Math.round((node.x() - contentX) / scale);
1095
+ const newY = Math.round((node.y() - contentY) / scale);
787
1096
  setCropRect({ ...cropRect, x: newX, y: newY });
788
1097
 
789
1098
  requestAnimationFrame(() => {
@@ -797,48 +1106,51 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
797
1106
  }}
798
1107
  onTransformStart={() => {
799
1108
  isTransformingRef.current = true;
1109
+ pendingTransformBoxRef.current = null;
1110
+ transformStartBoxRef.current = {
1111
+ x: contentX + cropRect.x * scale,
1112
+ y: contentY + cropRect.y * scale,
1113
+ width: cropRect.width * scale,
1114
+ height: cropRect.height * scale,
1115
+ rotation: 0
1116
+ };
800
1117
  }}
801
1118
  onTransformEnd={(e) => {
802
1119
  const node = e.target;
803
- const scaleX = node.scaleX();
804
- const scaleY = node.scaleY();
1120
+ const finalBox = pendingTransformBoxRef.current;
805
1121
 
806
1122
  node.scaleX(1);
807
1123
  node.scaleY(1);
808
-
809
- // Base size comes from current cropRect state * display scale
810
- const baseWidth = cropRect.width * scale;
811
- const baseHeight = cropRect.height * scale;
812
-
813
- let newWidth = Math.max(20, baseWidth * scaleX);
814
- let newHeight = Math.max(20, baseHeight * scaleY);
815
-
816
- if (!isFreeCrop && config.crop?.aspectRatio) {
817
- const aspectRatioParts = config.crop.aspectRatio.split(':');
818
- const ratioW = parseInt(aspectRatioParts[0]);
819
- const ratioH = parseInt(aspectRatioParts[1]);
820
- const targetRatio = ratioW / ratioH;
821
-
822
- if (scaleX > scaleY) {
823
- newHeight = newWidth / targetRatio;
824
- } else {
825
- newWidth = newHeight * targetRatio;
826
- }
1124
+
1125
+ if (!finalBox) {
1126
+ requestAnimationFrame(() => {
1127
+ transformerRef.current?.forceUpdate();
1128
+ transformerRef.current?.getLayer()?.batchDraw();
1129
+ });
1130
+
1131
+ setTimeout(() => {
1132
+ isTransformingRef.current = false;
1133
+ }, 50);
1134
+ return;
827
1135
  }
828
-
829
- // Constrain to TOTAL width/height
830
- const maxX = totalWidth - newWidth / scale;
831
- const maxY = totalHeight - newHeight / scale;
832
- const newX = Math.min(Math.max(0, (node.x() - contentX) / scale), maxX);
833
- const newY = Math.min(Math.max(0, (node.y() - contentY) / scale), maxY);
1136
+
1137
+ const constrainedX = Math.min(Math.max(contentX, finalBox.x), contentX + displayTotalWidth - finalBox.width);
1138
+ const constrainedY = Math.min(Math.max(contentY, finalBox.y), contentY + displayTotalHeight - finalBox.height);
1139
+ const newX = Math.round((constrainedX - contentX) / scale);
1140
+ const newY = Math.round((constrainedY - contentY) / scale);
1141
+ const newWidth = Math.round(finalBox.width / scale);
1142
+ const newHeight = Math.round(finalBox.height / scale);
834
1143
 
835
1144
  setCropRect({
836
1145
  x: newX,
837
1146
  y: newY,
838
- width: newWidth / scale,
839
- height: newHeight / scale
1147
+ width: newWidth,
1148
+ height: newHeight
840
1149
  });
841
1150
 
1151
+ pendingTransformBoxRef.current = null;
1152
+ transformStartBoxRef.current = null;
1153
+
842
1154
  requestAnimationFrame(() => {
843
1155
  transformerRef.current?.forceUpdate();
844
1156
  transformerRef.current?.getLayer()?.batchDraw();
@@ -894,56 +1206,36 @@ export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
894
1206
  anchorStroke="#3b82f6"
895
1207
  anchorFill="#3b82f6"
896
1208
  anchorSize={8}
1209
+ anchorDragBoundFunc={(oldAbsPos, newAbsPos) => {
1210
+ const activeAnchor = transformerRef.current?.getActiveAnchor();
1211
+ const fallbackBaseBox: TransformBox = {
1212
+ x: contentX + cropRect.x * scale,
1213
+ y: contentY + cropRect.y * scale,
1214
+ width: cropRect.width * scale,
1215
+ height: cropRect.height * scale,
1216
+ rotation: 0
1217
+ };
1218
+ const baseBox = transformStartBoxRef.current || fallbackBaseBox;
1219
+ return resolveAnchorDragPosition(baseBox, newAbsPos as Point, activeAnchor);
1220
+ }}
897
1221
  // 所有裁剪模式都支持 8 个方向的手柄,方便对称裁剪
898
1222
  enabledAnchors={[
899
1223
  'top-left', 'top-right', 'bottom-left', 'bottom-right',
900
1224
  'top-center', 'bottom-center', 'middle-left', 'middle-right'
901
1225
  ]}
902
1226
  boundBoxFunc={(oldBox, newBox) => {
903
- if (newBox.width < 20 || newBox.height < 20) {
904
- return oldBox;
905
- }
906
-
907
- if (!isFreeCrop && config.crop?.aspectRatio) {
908
- const aspectRatioParts = config.crop.aspectRatio.split(':');
909
- const ratioW = parseInt(aspectRatioParts[0]);
910
- const ratioH = parseInt(aspectRatioParts[1]);
911
- const targetRatio = ratioW / ratioH;
912
-
913
- if (Math.abs(newBox.width - oldBox.width) > Math.abs(newBox.height - oldBox.height)) {
914
- newBox.height = newBox.width / targetRatio;
915
- } else {
916
- newBox.width = newBox.height * targetRatio;
917
- }
918
- }
919
-
920
- // 检测正在使用的手柄,保持中心位置不变
921
1227
  const transformerInstance = transformerRef.current;
922
- if (transformerInstance) {
923
- const activeAnchor = transformerInstance.getActiveAnchor();
924
-
925
- if (activeAnchor === 'top-center' || activeAnchor === 'bottom-center') {
926
- // 使用上/下手柄时,保持水平中心位置不变
927
- const oldCenterX = oldBox.x + oldBox.width / 2;
928
- const newCenterX = newBox.x + newBox.width / 2;
929
- newBox.x += oldCenterX - newCenterX;
930
- } else if (activeAnchor === 'middle-left' || activeAnchor === 'middle-right') {
931
- // 使用左/右手柄时,保持垂直中心位置不变
932
- const oldCenterY = oldBox.y + oldBox.height / 2;
933
- const newCenterY = newBox.y + newBox.height / 2;
934
- newBox.y += oldCenterY - newCenterY;
935
- }
936
- }
937
-
938
- const relX = newBox.x - contentX;
939
- const relY = newBox.y - contentY;
940
- if (relX < -5 || relY < -5 ||
941
- relX + newBox.width > displayTotalWidth + 5 ||
942
- relY + newBox.height > displayTotalHeight + 5) {
943
- return oldBox;
944
- }
1228
+ const activeAnchor = transformerInstance?.getActiveAnchor();
1229
+ const baseBox = transformStartBoxRef.current || (oldBox as TransformBox);
1230
+ const resolvedBox = resolveCropTransformBox(
1231
+ baseBox,
1232
+ newBox as TransformBox,
1233
+ activeAnchor
1234
+ );
1235
+
1236
+ pendingTransformBoxRef.current = { ...resolvedBox };
945
1237
 
946
- return newBox;
1238
+ return resolvedBox;
947
1239
  }}
948
1240
  />
949
1241
  </Group>