mjpic 1.0.0
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/.trae/documents/mjpic-prd.md +111 -0
- package/.trae/documents/mjpic-technical-architecture.md +234 -0
- package/README.md +57 -0
- package/api/app.ts +60 -0
- package/api/cli.ts +61 -0
- package/api/index.ts +19 -0
- package/api/routes/auth.ts +33 -0
- package/api/routes/image.ts +27 -0
- package/api/server.ts +45 -0
- package/dist/cli/app.js +43 -0
- package/dist/cli/cli.js +49 -0
- package/dist/cli/index.js +13 -0
- package/dist/cli/routes/auth.js +28 -0
- package/dist/cli/routes/image.js +21 -0
- package/dist/cli/server.js +38 -0
- package/dist/client/assets/index-BUIYLOn-.js +197 -0
- package/dist/client/assets/index-BoiS81Ei.css +1 -0
- package/dist/client/favicon.svg +4 -0
- package/dist/client/index.html +354 -0
- package/eslint.config.js +28 -0
- package/index.html +24 -0
- package/nodemon.json +10 -0
- package/package.json +68 -0
- package/postcss.config.js +10 -0
- package/public/favicon.svg +4 -0
- package/src/App.tsx +13 -0
- package/src/assets/react.svg +1 -0
- package/src/components/Empty.tsx +8 -0
- package/src/components/dialogs/AspectRatioDialog.tsx +218 -0
- package/src/components/dialogs/SaveDialog.tsx +150 -0
- package/src/components/layout/CanvasArea.tsx +874 -0
- package/src/components/layout/Header.tsx +156 -0
- package/src/components/layout/RightPanel.tsx +886 -0
- package/src/components/layout/Sidebar.tsx +36 -0
- package/src/components/layout/StatusBar.tsx +44 -0
- package/src/hooks/useDebounce.ts +17 -0
- package/src/hooks/useTheme.ts +29 -0
- package/src/i18n/index.ts +26 -0
- package/src/i18n/locales/en.json +56 -0
- package/src/i18n/locales/zh.json +59 -0
- package/src/index.css +14 -0
- package/src/lib/utils.ts +73 -0
- package/src/main.tsx +11 -0
- package/src/pages/Home.tsx +72 -0
- package/src/store/useImageStore.ts +316 -0
- package/src/store/usePresetStore.ts +65 -0
- package/src/store/useUIStore.ts +17 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +13 -0
- package/tmp/guangxi.jpg +0 -0
- package/tsconfig.json +40 -0
- package/tsconfig.server.json +15 -0
- package/vercel.json +12 -0
- package/vite.config.ts +50 -0
- package//345/217/202/350/200/203/345/233/276/347/211/207//346/210/252/345/261/2172026-02-18 16.47.45_/345/233/276/347/211/207/345/260/272/345/257/270/350/260/203/350/212/202_/351/242/204/350/256/276/345/260/272/345/257/270.jpg +0 -0
- package//345/217/202/350/200/203/345/233/276/347/211/207//346/210/252/345/261/2172026-02-18 16.47.51_/345/233/276/347/211/207/345/260/272/345/257/270/350/260/203/350/212/202_/346/211/213/345/267/245/350/276/223/345/205/245/345/260/272/345/257/270.jpg +0 -0
- package//345/217/202/350/200/203/345/233/276/347/211/207//346/210/252/345/261/2172026-02-18 16.54.56_/345/233/276/347/211/207/345/260/272/345/257/270/350/260/203/350/212/202_/346/267/273/345/212/240/345/270/270/347/224/250/345/260/272/345/257/270.jpg +0 -0
- package//345/217/202/350/200/203/345/233/276/347/211/207//346/210/252/345/261/2172026-02-18 16.55.11_/345/233/276/347/211/207/345/260/272/345/257/270/350/260/203/350/212/202_/345/210/240/351/231/244/345/270/270/347/224/250/345/260/272/345/257/270.jpg +0 -0
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useLayoutEffect } from 'react';
|
|
2
|
+
import { Stage, Layer, Image as KonvaImage, Rect, Transformer, Group, Line } from 'react-konva';
|
|
3
|
+
import useImage from 'use-image';
|
|
4
|
+
import { useImageStore } from '@/store/useImageStore';
|
|
5
|
+
import { useUIStore } from '@/store/useUIStore';
|
|
6
|
+
import Konva from 'konva';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
|
|
9
|
+
interface CanvasAreaProps {
|
|
10
|
+
stageRef: React.MutableRefObject<Konva.Stage | null>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const CanvasArea = ({ stageRef }: CanvasAreaProps) => {
|
|
14
|
+
const { previewImage, config, loadImage, setOriginalSize, originalWidth, originalHeight, cropRect, setCropRect, updateConfig } = useImageStore();
|
|
15
|
+
const { isStraightenToolActive, setStraightenToolActive, activeTool } = useUIStore();
|
|
16
|
+
const [image] = useImage(previewImage || '', 'anonymous');
|
|
17
|
+
const imageRef = useRef<Konva.Image>(null);
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
|
|
20
|
+
const [stageSize, setStageSize] = useState({ width: 0, height: 0 });
|
|
21
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
|
|
23
|
+
const cropRectRef = useRef<Konva.Rect>(null);
|
|
24
|
+
const cropGroupRef = useRef<Konva.Group>(null);
|
|
25
|
+
const transformerRef = useRef<Konva.Transformer>(null);
|
|
26
|
+
|
|
27
|
+
const lastAspectRatioRef = useRef<string | undefined>(undefined);
|
|
28
|
+
const isTransformingRef = useRef(false);
|
|
29
|
+
const imgDimensionsRef = useRef({ width: 0, height: 0 });
|
|
30
|
+
|
|
31
|
+
// Straighten Tool State
|
|
32
|
+
const [straightenLine, setStraightenLine] = useState<{ start: {x: number, y: number}, end: {x: number, y: number} } | null>(null);
|
|
33
|
+
const isDrawingStraightenLine = useRef(false);
|
|
34
|
+
|
|
35
|
+
const hasCrop = config.crop && config.crop.aspectRatio;
|
|
36
|
+
const isFreeCrop = config.crop?.aspectRatio === 'Free';
|
|
37
|
+
|
|
38
|
+
// Define variables at the top level of the render function
|
|
39
|
+
let imgWidth = 0;
|
|
40
|
+
let imgHeight = 0;
|
|
41
|
+
let scale = 1;
|
|
42
|
+
let displayWidth = 0;
|
|
43
|
+
let displayHeight = 0;
|
|
44
|
+
|
|
45
|
+
// Content variables (Image + Border)
|
|
46
|
+
let contentX = 0;
|
|
47
|
+
let contentY = 0;
|
|
48
|
+
let totalWidth = 0;
|
|
49
|
+
let totalHeight = 0;
|
|
50
|
+
let displayTotalWidth = 0;
|
|
51
|
+
let displayTotalHeight = 0;
|
|
52
|
+
|
|
53
|
+
// Border vars
|
|
54
|
+
let borderW = 0;
|
|
55
|
+
let borderH = 0;
|
|
56
|
+
let displayBorderW = 0;
|
|
57
|
+
let displayBorderH = 0;
|
|
58
|
+
|
|
59
|
+
if (image) {
|
|
60
|
+
const hasResize = config.resize && config.resize.width > 0 && config.resize.height > 0;
|
|
61
|
+
|
|
62
|
+
// 1. Determine base image size
|
|
63
|
+
if (hasResize) {
|
|
64
|
+
imgWidth = config.resize.width;
|
|
65
|
+
imgHeight = config.resize.height;
|
|
66
|
+
} else {
|
|
67
|
+
imgWidth = image.width;
|
|
68
|
+
imgHeight = image.height;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Calculate border size
|
|
72
|
+
if (config.border) {
|
|
73
|
+
// Use default values to prevent undefined issues
|
|
74
|
+
const { size = 0, applyHorizontal = true, applyVertical = false } = config.border;
|
|
75
|
+
if (size > 0) {
|
|
76
|
+
if (applyHorizontal) borderW = imgWidth * (size / 100);
|
|
77
|
+
if (applyVertical) borderH = imgHeight * (size / 100);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
totalWidth = imgWidth + borderW * 2;
|
|
82
|
+
totalHeight = imgHeight + borderH * 2;
|
|
83
|
+
|
|
84
|
+
// 3. Calculate scale based on TOTAL size
|
|
85
|
+
if (stageSize.width > 0 && stageSize.height > 0) {
|
|
86
|
+
const scaleX = (stageSize.width - 40) / totalWidth;
|
|
87
|
+
const scaleY = (stageSize.height - 40) / totalHeight;
|
|
88
|
+
scale = Math.min(scaleX, scaleY, 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 4. Calculate display sizes
|
|
92
|
+
displayWidth = imgWidth * scale;
|
|
93
|
+
displayHeight = imgHeight * scale;
|
|
94
|
+
displayBorderW = borderW * scale;
|
|
95
|
+
displayBorderH = borderH * scale;
|
|
96
|
+
|
|
97
|
+
displayTotalWidth = totalWidth * scale;
|
|
98
|
+
displayTotalHeight = totalHeight * scale;
|
|
99
|
+
|
|
100
|
+
// 5. Calculate positions (centered)
|
|
101
|
+
const cx = stageSize.width / 2;
|
|
102
|
+
const cy = stageSize.height / 2;
|
|
103
|
+
|
|
104
|
+
// Content Top-Left (including border)
|
|
105
|
+
contentX = cx - displayTotalWidth / 2;
|
|
106
|
+
contentY = cy - displayTotalHeight / 2;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (image && image.width && image.height) {
|
|
111
|
+
setOriginalSize(image.width, image.height);
|
|
112
|
+
imgDimensionsRef.current = { width: image.width, height: image.height };
|
|
113
|
+
}
|
|
114
|
+
}, [image, setOriginalSize]);
|
|
115
|
+
|
|
116
|
+
const handleDragOver = (e: React.DragEvent) => {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const handleDrop = (e: React.DragEvent) => {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
e.stopPropagation();
|
|
124
|
+
|
|
125
|
+
const files = e.dataTransfer.files;
|
|
126
|
+
if (files && files.length > 0) {
|
|
127
|
+
const file = files[0];
|
|
128
|
+
if (file.type.startsWith('image/')) {
|
|
129
|
+
const reader = new FileReader();
|
|
130
|
+
reader.onload = (event) => {
|
|
131
|
+
if (event.target?.result) {
|
|
132
|
+
loadImage(event.target.result as string);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
reader.readAsDataURL(file);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
useLayoutEffect(() => {
|
|
141
|
+
const updateSize = () => {
|
|
142
|
+
if (containerRef.current) {
|
|
143
|
+
const { offsetWidth, offsetHeight } = containerRef.current;
|
|
144
|
+
if (offsetWidth > 0 && offsetHeight > 0) {
|
|
145
|
+
setStageSize({
|
|
146
|
+
width: offsetWidth,
|
|
147
|
+
height: offsetHeight
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
updateSize();
|
|
154
|
+
|
|
155
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
156
|
+
requestAnimationFrame(updateSize);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (containerRef.current) {
|
|
160
|
+
resizeObserver.observe(containerRef.current);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
window.addEventListener('resize', updateSize);
|
|
164
|
+
|
|
165
|
+
return () => {
|
|
166
|
+
window.removeEventListener('resize', updateSize);
|
|
167
|
+
resizeObserver.disconnect();
|
|
168
|
+
};
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (image && containerRef.current) {
|
|
173
|
+
setStageSize({
|
|
174
|
+
width: containerRef.current.offsetWidth,
|
|
175
|
+
height: containerRef.current.offsetHeight
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
requestAnimationFrame(() => {
|
|
179
|
+
if (stageRef.current) {
|
|
180
|
+
stageRef.current.batchDraw();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}, [image]);
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (image && imageRef.current) {
|
|
188
|
+
const node = imageRef.current;
|
|
189
|
+
|
|
190
|
+
node.clearCache();
|
|
191
|
+
|
|
192
|
+
const filters = [];
|
|
193
|
+
|
|
194
|
+
if (config.brightness !== 0) filters.push(Konva.Filters.Brighten);
|
|
195
|
+
if (config.contrast !== 0) filters.push(Konva.Filters.Contrast);
|
|
196
|
+
|
|
197
|
+
if (config.sharpness > 0) {
|
|
198
|
+
// @ts-ignore
|
|
199
|
+
const sharpnessFilter = function(imageData) {
|
|
200
|
+
const amount = config.sharpness / 100;
|
|
201
|
+
if (amount <= 0) return;
|
|
202
|
+
|
|
203
|
+
const data = imageData.data;
|
|
204
|
+
const width = imageData.width;
|
|
205
|
+
const height = imageData.height;
|
|
206
|
+
const oldData = new Uint8ClampedArray(data);
|
|
207
|
+
|
|
208
|
+
const center = 1 + 4 * amount;
|
|
209
|
+
const neighbor = -amount;
|
|
210
|
+
|
|
211
|
+
for (let i = 1; i < height - 1; i++) {
|
|
212
|
+
for (let j = 1; j < width - 1; j++) {
|
|
213
|
+
const idx = (i * width + j) * 4;
|
|
214
|
+
const up = idx - width * 4;
|
|
215
|
+
const down = idx + width * 4;
|
|
216
|
+
const left = idx - 4;
|
|
217
|
+
const right = idx + 4;
|
|
218
|
+
|
|
219
|
+
for (let k = 0; k < 3; k++) {
|
|
220
|
+
const val =
|
|
221
|
+
oldData[idx + k] * center +
|
|
222
|
+
(oldData[up + k] + oldData[down + k] + oldData[left + k] + oldData[right + k]) * neighbor;
|
|
223
|
+
data[idx + k] = val;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
filters.push(sharpnessFilter);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Auto Enhance - 全面提升图片质量(优化版本)
|
|
233
|
+
if (config.enhancements?.autoEnhance) {
|
|
234
|
+
// @ts-ignore
|
|
235
|
+
const autoEnhanceFilter = function(imageData) {
|
|
236
|
+
const data = imageData.data;
|
|
237
|
+
const width = imageData.width;
|
|
238
|
+
const height = imageData.height;
|
|
239
|
+
const pixelCount = width * height;
|
|
240
|
+
|
|
241
|
+
// 使用采样来加速统计(每8个像素采样一次)
|
|
242
|
+
const sampleStep = 8;
|
|
243
|
+
let sumR = 0, sumG = 0, sumB = 0;
|
|
244
|
+
let minVal = 255, maxVal = 0;
|
|
245
|
+
let totalLuminance = 0;
|
|
246
|
+
let sampleCount = 0;
|
|
247
|
+
|
|
248
|
+
for (let i = 0; i < data.length; i += 4 * sampleStep) {
|
|
249
|
+
const r = data[i], g = data[i+1], b = data[i+2];
|
|
250
|
+
sumR += r; sumG += g; sumB += b;
|
|
251
|
+
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
252
|
+
if (lum < minVal) minVal = lum;
|
|
253
|
+
if (lum > maxVal) maxVal = lum;
|
|
254
|
+
totalLuminance += lum;
|
|
255
|
+
sampleCount++;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const avgR = sumR / sampleCount;
|
|
259
|
+
const avgG = sumG / sampleCount;
|
|
260
|
+
const avgB = sumB / sampleCount;
|
|
261
|
+
const avgLuminance = totalLuminance / sampleCount;
|
|
262
|
+
|
|
263
|
+
// 白平衡校正
|
|
264
|
+
const wbR = avgG / (avgR || 1);
|
|
265
|
+
const wbB = avgG / (avgB || 1);
|
|
266
|
+
|
|
267
|
+
// 计算调整参数
|
|
268
|
+
let brightnessAdjust = 0;
|
|
269
|
+
if (avgLuminance < 100) brightnessAdjust = (100 - avgLuminance) * 0.15;
|
|
270
|
+
else if (avgLuminance > 160) brightnessAdjust = -(avgLuminance - 160) * 0.1;
|
|
271
|
+
|
|
272
|
+
const contrastAdjust = (maxVal - minVal) < 150 ? 15 : 8;
|
|
273
|
+
const contrastFactor = (259 * (contrastAdjust + 255)) / (255 * (259 - contrastAdjust));
|
|
274
|
+
|
|
275
|
+
// 使用采样应用效果
|
|
276
|
+
const applyStep = 4; // 逐像素处理
|
|
277
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
278
|
+
// 白平衡
|
|
279
|
+
data[i] = Math.min(255, data[i] * wbR);
|
|
280
|
+
data[i+2] = Math.min(255, data[i+2] * wbB);
|
|
281
|
+
|
|
282
|
+
// 亮度+对比度
|
|
283
|
+
const lum = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
|
|
284
|
+
const newLum = lum + brightnessAdjust;
|
|
285
|
+
const contrastLum = contrastFactor * (newLum - 128) + 128;
|
|
286
|
+
const ratio = contrastLum / (lum || 1);
|
|
287
|
+
|
|
288
|
+
data[i] = Math.min(255, data[i] * ratio);
|
|
289
|
+
data[i+1] = Math.min(255, data[i+1] * ratio);
|
|
290
|
+
data[i+2] = Math.min(255, data[i+2] * ratio);
|
|
291
|
+
|
|
292
|
+
// 饱和度提升
|
|
293
|
+
const gray = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
|
|
294
|
+
const sat = 1.15;
|
|
295
|
+
data[i] = Math.min(255, gray + (data[i] - gray) * sat);
|
|
296
|
+
data[i+1] = Math.min(255, gray + (data[i+1] - gray) * sat);
|
|
297
|
+
data[i+2] = Math.min(255, gray + (data[i+2] - gray) * sat);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 简化锐化(只处理边缘像素)
|
|
301
|
+
const oldData = new Uint8ClampedArray(data);
|
|
302
|
+
const sharpenAmount = 0.3;
|
|
303
|
+
const center = 1 + 4 * sharpenAmount;
|
|
304
|
+
const neighbor = -sharpenAmount;
|
|
305
|
+
|
|
306
|
+
for (let y = 1; y < height - 1; y += 2) {
|
|
307
|
+
for (let x = 1; x < width - 1; x += 2) {
|
|
308
|
+
const idx = (y * width + x) * 4;
|
|
309
|
+
const upIdx = ((y - 1) * width + x) * 4;
|
|
310
|
+
const downIdx = ((y + 1) * width + x) * 4;
|
|
311
|
+
const leftIdx = (y * width + (x - 1)) * 4;
|
|
312
|
+
const rightIdx = (y * width + (x + 1)) * 4;
|
|
313
|
+
|
|
314
|
+
for (let k = 0; k < 3; k++) {
|
|
315
|
+
const val = oldData[idx + k] * center +
|
|
316
|
+
(oldData[upIdx + k] + oldData[downIdx + k] +
|
|
317
|
+
oldData[leftIdx + k] + oldData[rightIdx + k]) * neighbor;
|
|
318
|
+
data[idx + k] = Math.min(255, Math.max(0, val));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
filters.push(autoEnhanceFilter);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Fill Light - 一键补光(优化版本)
|
|
327
|
+
if (config.enhancements?.fillLight) {
|
|
328
|
+
// @ts-ignore
|
|
329
|
+
const fillLightFilter = function(imageData) {
|
|
330
|
+
const data = imageData.data;
|
|
331
|
+
const width = imageData.width;
|
|
332
|
+
const height = imageData.height;
|
|
333
|
+
|
|
334
|
+
// 采样统计暗部占比
|
|
335
|
+
const sampleStep = 8;
|
|
336
|
+
let darkPixels = 0;
|
|
337
|
+
let totalLuminance = 0;
|
|
338
|
+
let sampleCount = 0;
|
|
339
|
+
|
|
340
|
+
for (let i = 0; i < data.length; i += 4 * sampleStep) {
|
|
341
|
+
const lum = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
|
|
342
|
+
if (lum < 80) darkPixels++;
|
|
343
|
+
totalLuminance += lum;
|
|
344
|
+
sampleCount++;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const darkRatio = darkPixels / sampleCount;
|
|
348
|
+
|
|
349
|
+
// 根据暗部占比计算伽马值
|
|
350
|
+
let gamma;
|
|
351
|
+
if (darkRatio > 0.5) gamma = 0.6;
|
|
352
|
+
else if (darkRatio > 0.3) gamma = 0.7;
|
|
353
|
+
else gamma = 0.85;
|
|
354
|
+
|
|
355
|
+
// 构建伽马查找表
|
|
356
|
+
const gammaLUT = new Array(256);
|
|
357
|
+
for (let i = 0; i < 256; i++) {
|
|
358
|
+
gammaLUT[i] = Math.pow(i / 255, gamma) * 255;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 应用补光
|
|
362
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
363
|
+
const lum = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
|
|
364
|
+
|
|
365
|
+
let weight;
|
|
366
|
+
if (lum < 80) weight = 0.7;
|
|
367
|
+
else if (lum < 160) weight = 0.4;
|
|
368
|
+
else weight = 0.1;
|
|
369
|
+
|
|
370
|
+
const correctedR = gammaLUT[data[i]];
|
|
371
|
+
const correctedG = gammaLUT[data[i+1]];
|
|
372
|
+
const correctedB = gammaLUT[data[i+2]];
|
|
373
|
+
|
|
374
|
+
data[i] = Math.min(245, data[i] + (correctedR - data[i]) * weight);
|
|
375
|
+
data[i+1] = Math.min(245, data[i+1] + (correctedG - data[i+1]) * weight);
|
|
376
|
+
data[i+2] = Math.min(245, data[i+2] + (correctedB - data[i+2]) * weight);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
filters.push(fillLightFilter);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Auto White Balance - adjust color temperature
|
|
383
|
+
if (config.enhancements?.autoWhiteBalance) {
|
|
384
|
+
// @ts-ignore
|
|
385
|
+
const whiteBalanceFilter = function(imageData) {
|
|
386
|
+
const data = imageData.data;
|
|
387
|
+
const width = imageData.width;
|
|
388
|
+
const height = imageData.height;
|
|
389
|
+
|
|
390
|
+
// Calculate average of each channel
|
|
391
|
+
let avgR = 0, avgG = 0, avgB = 0;
|
|
392
|
+
const pixelCount = width * height;
|
|
393
|
+
|
|
394
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
395
|
+
avgR += data[i];
|
|
396
|
+
avgG += data[i+1];
|
|
397
|
+
avgB += data[i+2];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
avgR /= pixelCount;
|
|
401
|
+
avgG /= pixelCount;
|
|
402
|
+
avgB /= pixelCount;
|
|
403
|
+
|
|
404
|
+
// Calculate correction factors (normalize to green channel)
|
|
405
|
+
const correctionR = avgG / (avgR || 1);
|
|
406
|
+
const correctionB = avgG / (avgB || 1);
|
|
407
|
+
|
|
408
|
+
// Apply white balance
|
|
409
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
410
|
+
data[i] = Math.min(255, Math.max(0, data[i] * correctionR));
|
|
411
|
+
data[i+1] = data[i+1]; // Green stays the same
|
|
412
|
+
data[i+2] = Math.min(255, Math.max(0, data[i+2] * correctionB));
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
filters.push(whiteBalanceFilter);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
node.filters(filters);
|
|
419
|
+
|
|
420
|
+
node.brightness(config.brightness / 100);
|
|
421
|
+
node.contrast(config.contrast);
|
|
422
|
+
|
|
423
|
+
if (filters.length > 0) {
|
|
424
|
+
try {
|
|
425
|
+
node.cache({
|
|
426
|
+
pixelRatio: 1
|
|
427
|
+
});
|
|
428
|
+
} catch (e) {
|
|
429
|
+
console.error('Failed to cache image:', e);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
node.getLayer()?.batchDraw();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return () => {
|
|
437
|
+
if (imageRef.current) {
|
|
438
|
+
imageRef.current.clearCache();
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}, [config, image]);
|
|
442
|
+
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
if (transformerRef.current && cropGroupRef.current && hasCrop) {
|
|
445
|
+
requestAnimationFrame(() => {
|
|
446
|
+
if (transformerRef.current && cropGroupRef.current) {
|
|
447
|
+
transformerRef.current.nodes([cropGroupRef.current]);
|
|
448
|
+
transformerRef.current.getLayer()?.batchDraw();
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}, [hasCrop]);
|
|
453
|
+
|
|
454
|
+
// 这个 useEffect 只在 aspectRatio 真正变化时初始化裁剪框
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
const currentAspectRatio = config.crop?.aspectRatio;
|
|
457
|
+
|
|
458
|
+
// 如果没有裁剪,重置状态并返回
|
|
459
|
+
if (!currentAspectRatio) {
|
|
460
|
+
lastAspectRatioRef.current = undefined;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 如果 aspect ratio 没变,直接返回(避免覆盖用户手动调整)
|
|
465
|
+
if (currentAspectRatio === lastAspectRatioRef.current) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// 如果用户正在调整,直接返回(避免冲突)
|
|
470
|
+
if (isTransformingRef.current) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 如果没有加载图片或图片尺寸为0,直接返回
|
|
475
|
+
if (!image || image.width === 0 || image.height === 0) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 更新引用,标记当前 aspectRatio 已处理
|
|
480
|
+
lastAspectRatioRef.current = currentAspectRatio;
|
|
481
|
+
|
|
482
|
+
// Calculate based on TOTAL size (Image + Border)
|
|
483
|
+
// NOTE: We recalculate these here because we need the latest values
|
|
484
|
+
let currentImgWidth = image.width;
|
|
485
|
+
let currentImgHeight = image.height;
|
|
486
|
+
|
|
487
|
+
if (config.resize && config.resize.width > 0 && config.resize.height > 0) {
|
|
488
|
+
currentImgWidth = config.resize.width;
|
|
489
|
+
currentImgHeight = config.resize.height;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let currentBorderW = 0;
|
|
493
|
+
let currentBorderH = 0;
|
|
494
|
+
if (config.border) {
|
|
495
|
+
const { size = 0, applyHorizontal = true, applyVertical = false } = config.border;
|
|
496
|
+
if (size > 0) {
|
|
497
|
+
if (applyHorizontal) currentBorderW = currentImgWidth * (size / 100);
|
|
498
|
+
if (applyVertical) currentBorderH = currentImgHeight * (size / 100);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const targetTotalWidth = currentImgWidth + currentBorderW * 2;
|
|
503
|
+
const targetTotalHeight = currentImgHeight + currentBorderH * 2;
|
|
504
|
+
|
|
505
|
+
if (currentAspectRatio === 'Free') {
|
|
506
|
+
setCropRect({
|
|
507
|
+
x: Math.round(targetTotalWidth * 0.1),
|
|
508
|
+
y: Math.round(targetTotalHeight * 0.1),
|
|
509
|
+
width: Math.round(targetTotalWidth * 0.8),
|
|
510
|
+
height: Math.round(targetTotalHeight * 0.8)
|
|
511
|
+
});
|
|
512
|
+
} else {
|
|
513
|
+
const aspectRatioParts = currentAspectRatio.split(':');
|
|
514
|
+
const ratioW = parseInt(aspectRatioParts[0]);
|
|
515
|
+
const ratioH = parseInt(aspectRatioParts[1]);
|
|
516
|
+
const targetRatio = ratioW / ratioH;
|
|
517
|
+
const currentRatio = targetTotalWidth / targetTotalHeight;
|
|
518
|
+
|
|
519
|
+
let cropW, cropH;
|
|
520
|
+
if (currentRatio > targetRatio) {
|
|
521
|
+
cropH = targetTotalHeight;
|
|
522
|
+
cropW = Math.round(targetTotalHeight * targetRatio);
|
|
523
|
+
} else {
|
|
524
|
+
cropW = targetTotalWidth;
|
|
525
|
+
cropH = Math.round(targetTotalWidth / targetRatio);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
setCropRect({
|
|
529
|
+
x: Math.round((targetTotalWidth - cropW) / 2),
|
|
530
|
+
y: Math.round((targetTotalHeight - cropH) / 2),
|
|
531
|
+
width: cropW,
|
|
532
|
+
height: cropH
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}, [config.crop?.aspectRatio, image?.width, image?.height, config.border, config.resize]); // Added config.border and config.resize dependency
|
|
536
|
+
|
|
537
|
+
const handleStageMouseDown = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
|
538
|
+
if (!isStraightenToolActive) return;
|
|
539
|
+
const pos = e.target.getStage()?.getPointerPosition();
|
|
540
|
+
if (pos) {
|
|
541
|
+
isDrawingStraightenLine.current = true;
|
|
542
|
+
setStraightenLine({ start: pos, end: pos });
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const handleStageMouseMove = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
|
547
|
+
if (!isDrawingStraightenLine.current || !straightenLine) return;
|
|
548
|
+
const pos = e.target.getStage()?.getPointerPosition();
|
|
549
|
+
if (pos) {
|
|
550
|
+
setStraightenLine(prev => prev ? { ...prev, end: pos } : null);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const handleStageMouseUp = () => {
|
|
555
|
+
if (!isDrawingStraightenLine.current || !straightenLine) return;
|
|
556
|
+
isDrawingStraightenLine.current = false;
|
|
557
|
+
|
|
558
|
+
const dx = straightenLine.end.x - straightenLine.start.x;
|
|
559
|
+
const dy = straightenLine.end.y - straightenLine.start.y;
|
|
560
|
+
|
|
561
|
+
if (Math.sqrt(dx*dx + dy*dy) > 10) {
|
|
562
|
+
const angleRad = Math.atan2(dy, dx);
|
|
563
|
+
let angleDeg = angleRad * (180 / Math.PI);
|
|
564
|
+
|
|
565
|
+
// Calculate new rotation
|
|
566
|
+
let newRot = config.rotation - angleDeg;
|
|
567
|
+
|
|
568
|
+
// Normalize to -180 ~ 180
|
|
569
|
+
// First mod to -360 ~ 360 range
|
|
570
|
+
newRot = newRot % 360;
|
|
571
|
+
// Then adjust to -180 ~ 180
|
|
572
|
+
if (newRot > 180) newRot -= 360;
|
|
573
|
+
if (newRot <= -180) newRot += 360;
|
|
574
|
+
|
|
575
|
+
updateConfig({ rotation: newRot });
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
setStraightenLine(null);
|
|
579
|
+
// Don't exit straighten mode automatically
|
|
580
|
+
// setStraightenToolActive(false);
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
if (!previewImage) {
|
|
584
|
+
return (
|
|
585
|
+
<div
|
|
586
|
+
className="flex-1 flex items-center justify-center bg-zinc-950 text-zinc-500"
|
|
587
|
+
onDragOver={handleDragOver}
|
|
588
|
+
onDrop={handleDrop}
|
|
589
|
+
>
|
|
590
|
+
{t('common.dragDrop')}
|
|
591
|
+
</div>
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return (
|
|
596
|
+
<div
|
|
597
|
+
className="flex-1 bg-zinc-950 overflow-hidden relative"
|
|
598
|
+
ref={containerRef}
|
|
599
|
+
onDragOver={handleDragOver}
|
|
600
|
+
onDrop={handleDrop}
|
|
601
|
+
>
|
|
602
|
+
<Stage
|
|
603
|
+
width={stageSize.width}
|
|
604
|
+
height={stageSize.height}
|
|
605
|
+
ref={stageRef}
|
|
606
|
+
onMouseDown={handleStageMouseDown}
|
|
607
|
+
onMouseMove={handleStageMouseMove}
|
|
608
|
+
onMouseUp={handleStageMouseUp}
|
|
609
|
+
style={{ cursor: isStraightenToolActive ? 'crosshair' : 'default' }}
|
|
610
|
+
>
|
|
611
|
+
<Layer>
|
|
612
|
+
{image && (
|
|
613
|
+
<Group
|
|
614
|
+
id="content-group"
|
|
615
|
+
x={stageSize.width / 2}
|
|
616
|
+
y={stageSize.height / 2}
|
|
617
|
+
rotation={config.rotation}
|
|
618
|
+
>
|
|
619
|
+
{(displayBorderW > 0 || displayBorderH > 0) && (
|
|
620
|
+
<Rect
|
|
621
|
+
width={displayWidth + displayBorderW * 2}
|
|
622
|
+
height={displayHeight + displayBorderH * 2}
|
|
623
|
+
offsetX={(displayWidth + displayBorderW * 2) / 2}
|
|
624
|
+
offsetY={(displayHeight + displayBorderH * 2) / 2}
|
|
625
|
+
fill={config.border?.color || '#ffffff'}
|
|
626
|
+
listening={false}
|
|
627
|
+
/>
|
|
628
|
+
)}
|
|
629
|
+
|
|
630
|
+
<KonvaImage
|
|
631
|
+
image={image}
|
|
632
|
+
ref={imageRef}
|
|
633
|
+
x={0}
|
|
634
|
+
y={0}
|
|
635
|
+
width={imgWidth}
|
|
636
|
+
height={imgHeight}
|
|
637
|
+
scaleX={scale}
|
|
638
|
+
scaleY={scale}
|
|
639
|
+
offsetX={imgWidth / 2}
|
|
640
|
+
offsetY={imgHeight / 2}
|
|
641
|
+
/>
|
|
642
|
+
</Group>
|
|
643
|
+
)}
|
|
644
|
+
|
|
645
|
+
{/* Crop Overlay - covers TOTAL content (Image + Border) */}
|
|
646
|
+
{image && hasCrop && displayTotalWidth > 0 && displayTotalHeight > 0 && (
|
|
647
|
+
<Group>
|
|
648
|
+
<Rect
|
|
649
|
+
x={contentX}
|
|
650
|
+
y={contentY}
|
|
651
|
+
width={displayTotalWidth}
|
|
652
|
+
height={displayTotalHeight}
|
|
653
|
+
fill="rgba(0, 0, 0, 0.5)"
|
|
654
|
+
listening={false}
|
|
655
|
+
/>
|
|
656
|
+
|
|
657
|
+
<Group
|
|
658
|
+
ref={cropGroupRef}
|
|
659
|
+
x={contentX + cropRect.x * scale}
|
|
660
|
+
y={contentY + cropRect.y * scale}
|
|
661
|
+
draggable
|
|
662
|
+
dragBoundFunc={(pos) => {
|
|
663
|
+
const imgLeft = contentX;
|
|
664
|
+
const imgTop = contentY;
|
|
665
|
+
const imgRight = contentX + displayTotalWidth;
|
|
666
|
+
const imgBottom = contentY + displayTotalHeight;
|
|
667
|
+
|
|
668
|
+
// Use current scale of the group (if transforming) or 1
|
|
669
|
+
const groupScaleX = cropGroupRef.current?.scaleX() || 1;
|
|
670
|
+
const groupScaleY = cropGroupRef.current?.scaleY() || 1;
|
|
671
|
+
|
|
672
|
+
const cropW = cropRect.width * scale * groupScaleX;
|
|
673
|
+
const cropH = cropRect.height * scale * groupScaleY;
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
x: Math.max(imgLeft, Math.min(pos.x, imgRight - cropW)),
|
|
677
|
+
y: Math.max(imgTop, Math.min(pos.y, imgBottom - cropH))
|
|
678
|
+
};
|
|
679
|
+
}}
|
|
680
|
+
onDragStart={() => {
|
|
681
|
+
isTransformingRef.current = true;
|
|
682
|
+
}}
|
|
683
|
+
onDragMove={(e) => {
|
|
684
|
+
const node = e.target;
|
|
685
|
+
const newX = (node.x() - contentX) / scale;
|
|
686
|
+
const newY = (node.y() - contentY) / scale;
|
|
687
|
+
setCropRect({ ...cropRect, x: newX, y: newY });
|
|
688
|
+
}}
|
|
689
|
+
onDragEnd={() => {
|
|
690
|
+
setTimeout(() => {
|
|
691
|
+
isTransformingRef.current = false;
|
|
692
|
+
}, 50);
|
|
693
|
+
}}
|
|
694
|
+
onTransformStart={() => {
|
|
695
|
+
isTransformingRef.current = true;
|
|
696
|
+
}}
|
|
697
|
+
onTransformEnd={(e) => {
|
|
698
|
+
const node = e.target;
|
|
699
|
+
const scaleX = node.scaleX();
|
|
700
|
+
const scaleY = node.scaleY();
|
|
701
|
+
|
|
702
|
+
node.scaleX(1);
|
|
703
|
+
node.scaleY(1);
|
|
704
|
+
|
|
705
|
+
// Base size comes from current cropRect state * display scale
|
|
706
|
+
const baseWidth = cropRect.width * scale;
|
|
707
|
+
const baseHeight = cropRect.height * scale;
|
|
708
|
+
|
|
709
|
+
let newWidth = Math.max(20, baseWidth * scaleX);
|
|
710
|
+
let newHeight = Math.max(20, baseHeight * scaleY);
|
|
711
|
+
|
|
712
|
+
if (!isFreeCrop && config.crop?.aspectRatio) {
|
|
713
|
+
const aspectRatioParts = config.crop.aspectRatio.split(':');
|
|
714
|
+
const ratioW = parseInt(aspectRatioParts[0]);
|
|
715
|
+
const ratioH = parseInt(aspectRatioParts[1]);
|
|
716
|
+
const targetRatio = ratioW / ratioH;
|
|
717
|
+
|
|
718
|
+
if (scaleX > scaleY) {
|
|
719
|
+
newHeight = newWidth / targetRatio;
|
|
720
|
+
} else {
|
|
721
|
+
newWidth = newHeight * targetRatio;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Constrain to TOTAL width/height
|
|
726
|
+
const maxX = totalWidth - newWidth / scale;
|
|
727
|
+
const maxY = totalHeight - newHeight / scale;
|
|
728
|
+
const newX = Math.min(Math.max(0, (node.x() - contentX) / scale), maxX);
|
|
729
|
+
const newY = Math.min(Math.max(0, (node.y() - contentY) / scale), maxY);
|
|
730
|
+
|
|
731
|
+
setCropRect({
|
|
732
|
+
x: newX,
|
|
733
|
+
y: newY,
|
|
734
|
+
width: newWidth / scale,
|
|
735
|
+
height: newHeight / scale
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
lastAspectRatioRef.current = config.crop?.aspectRatio;
|
|
739
|
+
|
|
740
|
+
setTimeout(() => {
|
|
741
|
+
isTransformingRef.current = false;
|
|
742
|
+
}, 50);
|
|
743
|
+
}}
|
|
744
|
+
>
|
|
745
|
+
{/* Crop Border */}
|
|
746
|
+
<Rect
|
|
747
|
+
width={cropRect.width * scale}
|
|
748
|
+
height={cropRect.height * scale}
|
|
749
|
+
stroke="#3b82f6"
|
|
750
|
+
strokeWidth={2}
|
|
751
|
+
fill="transparent"
|
|
752
|
+
/>
|
|
753
|
+
|
|
754
|
+
{/* Crop Grid Lines */}
|
|
755
|
+
<Line
|
|
756
|
+
points={[0, (cropRect.height * scale)/3, cropRect.width * scale, (cropRect.height * scale)/3]}
|
|
757
|
+
stroke="rgba(255, 255, 255, 0.5)"
|
|
758
|
+
strokeWidth={1}
|
|
759
|
+
listening={false}
|
|
760
|
+
/>
|
|
761
|
+
<Line
|
|
762
|
+
points={[0, (cropRect.height * scale)*2/3, cropRect.width * scale, (cropRect.height * scale)*2/3]}
|
|
763
|
+
stroke="rgba(255, 255, 255, 0.5)"
|
|
764
|
+
strokeWidth={1}
|
|
765
|
+
listening={false}
|
|
766
|
+
/>
|
|
767
|
+
<Line
|
|
768
|
+
points={[(cropRect.width * scale)/3, 0, (cropRect.width * scale)/3, cropRect.height * scale]}
|
|
769
|
+
stroke="rgba(255, 255, 255, 0.5)"
|
|
770
|
+
strokeWidth={1}
|
|
771
|
+
listening={false}
|
|
772
|
+
/>
|
|
773
|
+
<Line
|
|
774
|
+
points={[(cropRect.width * scale)*2/3, 0, (cropRect.width * scale)*2/3, cropRect.height * scale]}
|
|
775
|
+
stroke="rgba(255, 255, 255, 0.5)"
|
|
776
|
+
strokeWidth={1}
|
|
777
|
+
listening={false}
|
|
778
|
+
/>
|
|
779
|
+
</Group>
|
|
780
|
+
|
|
781
|
+
<Transformer
|
|
782
|
+
ref={transformerRef}
|
|
783
|
+
enabledAnchors={isFreeCrop
|
|
784
|
+
? ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'middle-left', 'middle-right', 'top-center', 'bottom-center']
|
|
785
|
+
: ['top-left', 'top-right', 'bottom-left', 'bottom-right']
|
|
786
|
+
}
|
|
787
|
+
boundBoxFunc={(oldBox, newBox) => {
|
|
788
|
+
if (newBox.width < 20 || newBox.height < 20) {
|
|
789
|
+
return oldBox;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (!isFreeCrop && config.crop?.aspectRatio) {
|
|
793
|
+
const aspectRatioParts = config.crop.aspectRatio.split(':');
|
|
794
|
+
const ratioW = parseInt(aspectRatioParts[0]);
|
|
795
|
+
const ratioH = parseInt(aspectRatioParts[1]);
|
|
796
|
+
const targetRatio = ratioW / ratioH;
|
|
797
|
+
|
|
798
|
+
if (Math.abs(newBox.width - oldBox.width) > Math.abs(newBox.height - oldBox.height)) {
|
|
799
|
+
newBox.height = newBox.width / targetRatio;
|
|
800
|
+
} else {
|
|
801
|
+
newBox.width = newBox.height * targetRatio;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const relX = newBox.x - contentX;
|
|
806
|
+
const relY = newBox.y - contentY;
|
|
807
|
+
if (relX < -5 || relY < -5 ||
|
|
808
|
+
relX + newBox.width > displayTotalWidth + 5 ||
|
|
809
|
+
relY + newBox.height > displayTotalHeight + 5) {
|
|
810
|
+
return oldBox;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return newBox;
|
|
814
|
+
}}
|
|
815
|
+
/>
|
|
816
|
+
</Group>
|
|
817
|
+
)}
|
|
818
|
+
|
|
819
|
+
{/* Rotation Grid Overlay - covers TOTAL content */}
|
|
820
|
+
{activeTool === 'rotate' && displayTotalWidth > 0 && displayTotalHeight > 0 && (
|
|
821
|
+
<Group
|
|
822
|
+
x={contentX}
|
|
823
|
+
y={contentY}
|
|
824
|
+
width={displayTotalWidth}
|
|
825
|
+
height={displayTotalHeight}
|
|
826
|
+
listening={false}
|
|
827
|
+
>
|
|
828
|
+
{/* Outer Border */}
|
|
829
|
+
<Rect
|
|
830
|
+
width={displayTotalWidth}
|
|
831
|
+
height={displayTotalHeight}
|
|
832
|
+
stroke="rgba(255, 255, 255, 0.5)"
|
|
833
|
+
strokeWidth={1}
|
|
834
|
+
/>
|
|
835
|
+
|
|
836
|
+
{/* Horizontal Lines */}
|
|
837
|
+
<Line
|
|
838
|
+
points={[0, displayTotalHeight/3, displayTotalWidth, displayTotalHeight/3]}
|
|
839
|
+
stroke="rgba(255, 255, 255, 0.3)"
|
|
840
|
+
strokeWidth={1}
|
|
841
|
+
/>
|
|
842
|
+
<Line
|
|
843
|
+
points={[0, displayTotalHeight*2/3, displayTotalWidth, displayTotalHeight*2/3]}
|
|
844
|
+
stroke="rgba(255, 255, 255, 0.3)"
|
|
845
|
+
strokeWidth={1}
|
|
846
|
+
/>
|
|
847
|
+
|
|
848
|
+
{/* Vertical Lines */}
|
|
849
|
+
<Line
|
|
850
|
+
points={[displayTotalWidth/3, 0, displayTotalWidth/3, displayTotalHeight]}
|
|
851
|
+
stroke="rgba(255, 255, 255, 0.3)"
|
|
852
|
+
strokeWidth={1}
|
|
853
|
+
/>
|
|
854
|
+
<Line
|
|
855
|
+
points={[displayTotalWidth*2/3, 0, displayTotalWidth*2/3, displayTotalHeight]}
|
|
856
|
+
stroke="rgba(255, 255, 255, 0.3)"
|
|
857
|
+
strokeWidth={1}
|
|
858
|
+
/>
|
|
859
|
+
</Group>
|
|
860
|
+
)}
|
|
861
|
+
|
|
862
|
+
{straightenLine && (
|
|
863
|
+
<Line
|
|
864
|
+
points={[straightenLine.start.x, straightenLine.start.y, straightenLine.end.x, straightenLine.end.y]}
|
|
865
|
+
stroke="#3b82f6"
|
|
866
|
+
strokeWidth={2}
|
|
867
|
+
dash={[5, 5]}
|
|
868
|
+
/>
|
|
869
|
+
)}
|
|
870
|
+
</Layer>
|
|
871
|
+
</Stage>
|
|
872
|
+
</div>
|
|
873
|
+
);
|
|
874
|
+
};
|