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.
- package/dist/client/assets/index-DORSlWYO.js +197 -0
- package/dist/client/index.html +1 -1
- package/package.json +2 -3
- package/src/components/layout/CanvasArea.tsx +367 -75
- package/src/components/layout/Header.tsx +170 -40
- package/src/store/useImageStore.ts +100 -27
- package/test-/350/276/203/351/253/230/345/210/206/350/276/250/347/216/207/347/232/204/347/205/247/347/211/207/345/216/213/347/274/251/345/210/206/350/276/250/347/216/207/345/220/216/347/224/273/351/235/242/344/270/212/345/207/272/347/216/260/346/235/241/347/272/271/_DSC2177_/345/211/257/346/234/254_/345/211/257/346/234/254 (1).jpg +0 -0
- package/test-/350/276/203/351/253/230/345/210/206/350/276/250/347/216/207/347/232/204/347/205/247/347/211/207/345/216/213/347/274/251/345/210/206/350/276/250/347/216/207/345/220/216/347/224/273/351/235/242/344/270/212/345/207/272/347/216/260/346/235/241/347/272/271/_DSC2177_/345/211/257/346/234/254_/345/211/257/346/234/254 (2).jpg +0 -0
- package/dist/client/assets/index-Tbz-to9P.js +0 -197
package/dist/client/index.html
CHANGED
|
@@ -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-
|
|
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.
|
|
4
|
+
"version": "1.0.21",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"mjpic": "
|
|
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
|
|
804
|
-
const scaleY = node.scaleY();
|
|
1120
|
+
const finalBox = pendingTransformBoxRef.current;
|
|
805
1121
|
|
|
806
1122
|
node.scaleX(1);
|
|
807
1123
|
node.scaleY(1);
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
-
|
|
830
|
-
const
|
|
831
|
-
const
|
|
832
|
-
const
|
|
833
|
-
const
|
|
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
|
|
839
|
-
height: newHeight
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
|
1238
|
+
return resolvedBox;
|
|
947
1239
|
}}
|
|
948
1240
|
/>
|
|
949
1241
|
</Group>
|