react-native-expo-cropper 1.2.48 → 1.2.50
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/ImageCropper.js +101 -161
- package/dist/ImageCropperStyles.js +28 -17
- package/package.json +1 -1
- package/src/ImageCropper.js +115 -138
- package/src/ImageCropperStyles.js +32 -20
package/dist/ImageCropper.js
CHANGED
|
@@ -37,8 +37,8 @@ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" !=
|
|
|
37
37
|
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
|
|
38
38
|
var PRIMARY_GREEN = '#198754';
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
var
|
|
40
|
+
// Base dimension for responsive scaling (e.g. ~375pt design width)
|
|
41
|
+
var RESPONSIVE_BASE = 375;
|
|
42
42
|
var ImageCropper = function ImageCropper(_ref) {
|
|
43
43
|
var _cameraFrameData$curr3;
|
|
44
44
|
var onConfirm = _ref.onConfirm,
|
|
@@ -47,8 +47,11 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
47
47
|
addheight = _ref.addheight,
|
|
48
48
|
rotationLabel = _ref.rotationLabel;
|
|
49
49
|
var _useWindowDimensions = (0, _reactNative.useWindowDimensions)(),
|
|
50
|
-
windowWidth = _useWindowDimensions.width
|
|
51
|
-
|
|
50
|
+
windowWidth = _useWindowDimensions.width,
|
|
51
|
+
windowHeight = _useWindowDimensions.height;
|
|
52
|
+
|
|
53
|
+
// Responsive scale: image and UI adapt to small (phone) and large (tablet) screens
|
|
54
|
+
var responsiveScale = Math.max(0.65, Math.min(2, Math.min(windowWidth, windowHeight) / RESPONSIVE_BASE));
|
|
52
55
|
var guessExtensionFromUri = function guessExtensionFromUri(uri) {
|
|
53
56
|
if (!uri || typeof uri !== 'string') return null;
|
|
54
57
|
var match = uri.match(/\.([a-zA-Z0-9]+)(?:[?#].*)?$/);
|
|
@@ -454,92 +457,11 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
454
457
|
}
|
|
455
458
|
|
|
456
459
|
// ✅ CRITICAL FIX: Calculate crop box as percentage of VISIBLE IMAGE AREA (imageDisplayRect)
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
var
|
|
460
|
-
var imageRectY = wrapper.y + imageRect.y;
|
|
461
|
-
|
|
462
|
-
// ✅ PRIORITY RULE #3: IF image comes from camera → Use EXACT green frame coordinates
|
|
463
|
-
// Image is displayed in "cover" mode (full wrapper), so green frame coords = white frame coords.
|
|
464
|
-
// Store points in WRAPPER-RELATIVE coordinates (greenFrame.x, greenFrame.y) so they match
|
|
465
|
-
// touch events (locationX/locationY are relative to the wrapper). This keeps display→image
|
|
466
|
-
// conversion correct in Confirm.
|
|
467
|
-
if (cameraFrameData.current && cameraFrameData.current.greenFrame && originalImageDimensions.current.width > 0) {
|
|
468
|
-
var greenFrame = cameraFrameData.current.greenFrame;
|
|
469
|
-
var _boxX = greenFrame.x;
|
|
470
|
-
var _boxY = greenFrame.y;
|
|
471
|
-
var _boxWidth = greenFrame.width;
|
|
472
|
-
var _boxHeight = greenFrame.height;
|
|
473
|
-
|
|
474
|
-
// ✅ SAFETY: Validate calculated coordinates before creating points
|
|
475
|
-
var _isValidCoordinate = function _isValidCoordinate(val) {
|
|
476
|
-
return typeof val === 'number' && isFinite(val) && !isNaN(val);
|
|
477
|
-
};
|
|
478
|
-
if (!_isValidCoordinate(_boxX) || !_isValidCoordinate(_boxY) || !_isValidCoordinate(_boxWidth) || !_isValidCoordinate(_boxHeight)) {
|
|
479
|
-
console.warn("⚠️ Invalid coordinates calculated for crop box, skipping initialization");
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// ✅ Points in wrapper-relative coords (same as touch events)
|
|
484
|
-
var _newPoints = [{
|
|
485
|
-
x: _boxX,
|
|
486
|
-
y: _boxY
|
|
487
|
-
},
|
|
488
|
-
// Top-left
|
|
489
|
-
{
|
|
490
|
-
x: _boxX + _boxWidth,
|
|
491
|
-
y: _boxY
|
|
492
|
-
},
|
|
493
|
-
// Top-right
|
|
494
|
-
{
|
|
495
|
-
x: _boxX + _boxWidth,
|
|
496
|
-
y: _boxY + _boxHeight
|
|
497
|
-
},
|
|
498
|
-
// Bottom-right
|
|
499
|
-
{
|
|
500
|
-
x: _boxX,
|
|
501
|
-
y: _boxY + _boxHeight
|
|
502
|
-
} // Bottom-left
|
|
503
|
-
];
|
|
504
|
-
|
|
505
|
-
// ✅ SAFETY: Validate all points before setting
|
|
506
|
-
var _validPoints = _newPoints.filter(function (p) {
|
|
507
|
-
return _isValidCoordinate(p.x) && _isValidCoordinate(p.y);
|
|
508
|
-
});
|
|
509
|
-
if (_validPoints.length !== _newPoints.length) {
|
|
510
|
-
console.warn("⚠️ Some points have invalid coordinates, skipping initialization");
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
console.log("✅ Initializing crop box for camera image (COVER MODE - exact green frame):", {
|
|
514
|
-
greenFrame: {
|
|
515
|
-
x: greenFrame.x,
|
|
516
|
-
y: greenFrame.y,
|
|
517
|
-
width: greenFrame.width,
|
|
518
|
-
height: greenFrame.height
|
|
519
|
-
},
|
|
520
|
-
whiteFrame: {
|
|
521
|
-
x: _boxX.toFixed(2),
|
|
522
|
-
y: _boxY.toFixed(2),
|
|
523
|
-
width: _boxWidth.toFixed(2),
|
|
524
|
-
height: _boxHeight.toFixed(2)
|
|
525
|
-
},
|
|
526
|
-
note: "Points in wrapper-relative coords - same as touch events and crop_image_size"
|
|
527
|
-
});
|
|
528
|
-
setPoints(_newPoints);
|
|
529
|
-
hasInitializedCropBox.current = true; // ✅ CRITICAL: Mark as initialized
|
|
530
|
-
// ✅ CRITICAL: DO NOT nullify cameraFrameData here - keep it for Image.getSize() callback check
|
|
531
|
-
// It will be cleared when loading a new image
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// ✅ PRIORITY RULE #3: DEFAULT logic ONLY for gallery images (NOT camera)
|
|
536
|
-
// If we reach here and imageSource is 'camera', something went wrong
|
|
537
|
-
if (imageSource.current === 'camera') {
|
|
538
|
-
console.warn("⚠️ Camera image but no greenFrame found - this should not happen");
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
460
|
+
// in WRAPPER-RELATIVE coordinates (same space as points/touches and SVG).
|
|
461
|
+
var imageRectX = imageRect.x;
|
|
462
|
+
var imageRectY = imageRect.y;
|
|
541
463
|
|
|
542
|
-
// ✅ DEFAULT: Crop box (70% of VISIBLE IMAGE AREA - centered)
|
|
464
|
+
// ✅ DEFAULT: Crop box (70% of VISIBLE IMAGE AREA - centered)
|
|
543
465
|
var boxWidth = imageRect.width * 0.70; // 70% of visible image width
|
|
544
466
|
var boxHeight = imageRect.height * 0.70; // 70% of visible image height
|
|
545
467
|
var boxX = imageRectX + (imageRect.width - boxWidth) / 2; // Centered in image area
|
|
@@ -581,7 +503,7 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
581
503
|
height: wrapper.height
|
|
582
504
|
},
|
|
583
505
|
imageDisplayRect: imageRect,
|
|
584
|
-
|
|
506
|
+
boxInWrapperCoords: {
|
|
585
507
|
x: boxX,
|
|
586
508
|
y: boxY,
|
|
587
509
|
width: boxWidth,
|
|
@@ -770,12 +692,12 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
770
692
|
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
771
693
|
imageRect = imageDisplayRect.current;
|
|
772
694
|
}
|
|
773
|
-
// If still not available, use wrapper as fallback
|
|
695
|
+
// If still not available, use full wrapper as fallback (wrapper-relative coords)
|
|
774
696
|
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
775
697
|
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
776
698
|
imageRect = {
|
|
777
|
-
x:
|
|
778
|
-
y:
|
|
699
|
+
x: 0,
|
|
700
|
+
y: 0,
|
|
779
701
|
width: wrapper.width,
|
|
780
702
|
height: wrapper.height
|
|
781
703
|
};
|
|
@@ -787,12 +709,10 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
787
709
|
}
|
|
788
710
|
|
|
789
711
|
// ✅ Clamp to real displayed image content (imageDisplayRect dans le wrapper)
|
|
790
|
-
// Les coordonnées tapX/tapY sont
|
|
791
|
-
var imageRectX = wrapper.x + imageRect.x;
|
|
792
|
-
var imageRectY = wrapper.y + imageRect.y;
|
|
712
|
+
// Les coordonnées tapX/tapY et imageRect sont RELATIVES au wrapper commun (origine 0,0)
|
|
793
713
|
var _x$y$width$height = {
|
|
794
|
-
x:
|
|
795
|
-
y:
|
|
714
|
+
x: imageRect.x,
|
|
715
|
+
y: imageRect.y,
|
|
796
716
|
width: imageRect.width,
|
|
797
717
|
height: imageRect.height
|
|
798
718
|
},
|
|
@@ -800,8 +720,8 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
800
720
|
cy = _x$y$width$height.y,
|
|
801
721
|
cw = _x$y$width$height.width,
|
|
802
722
|
ch = _x$y$width$height.height;
|
|
803
|
-
// ✅
|
|
804
|
-
var selectRadius =
|
|
723
|
+
// ✅ Responsive select radius: scales with screen so touch target matches handle size
|
|
724
|
+
var selectRadius = Math.max(28, Math.round(50 * responsiveScale));
|
|
805
725
|
|
|
806
726
|
// ✅ CRITICAL: Check for existing point selection FIRST (using raw tap coordinates)
|
|
807
727
|
// Don't clamp tapX/Y for point selection - points can be anywhere in wrapper now
|
|
@@ -935,12 +855,12 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
935
855
|
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
936
856
|
imageRect = imageDisplayRect.current;
|
|
937
857
|
}
|
|
938
|
-
// If still not available, use wrapper as fallback
|
|
858
|
+
// If still not available, use full wrapper as fallback (wrapper-relative coords)
|
|
939
859
|
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
940
860
|
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
941
861
|
imageRect = {
|
|
942
|
-
x:
|
|
943
|
-
y:
|
|
862
|
+
x: 0,
|
|
863
|
+
y: 0,
|
|
944
864
|
width: wrapper.width,
|
|
945
865
|
height: wrapper.height
|
|
946
866
|
};
|
|
@@ -951,13 +871,11 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
951
871
|
}
|
|
952
872
|
}
|
|
953
873
|
|
|
954
|
-
// ✅
|
|
955
|
-
//
|
|
956
|
-
var imageRectX = wrapper.x + imageRect.x;
|
|
957
|
-
var imageRectY = wrapper.y + imageRect.y;
|
|
874
|
+
// ✅ Bounds in wrapper-relative coordinates (touch/points use wrapper as origin)
|
|
875
|
+
// Full picture = entire wrapper; visible image = imageDisplayRect
|
|
958
876
|
var contentRect = {
|
|
959
|
-
x:
|
|
960
|
-
y:
|
|
877
|
+
x: imageRect.x,
|
|
878
|
+
y: imageRect.y,
|
|
961
879
|
width: imageRect.width,
|
|
962
880
|
height: imageRect.height
|
|
963
881
|
};
|
|
@@ -985,28 +903,19 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
985
903
|
var newX = currentX + touchOffset.current.x;
|
|
986
904
|
var newY = currentY + touchOffset.current.y;
|
|
987
905
|
|
|
988
|
-
// ✅
|
|
989
|
-
var
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
// ✅
|
|
995
|
-
var
|
|
996
|
-
var
|
|
997
|
-
var
|
|
998
|
-
var
|
|
999
|
-
|
|
1000
|
-
// ✅
|
|
1001
|
-
// Points can move freely across the entire screen for maximum flexibility
|
|
1002
|
-
// They will be clamped to imageDisplayRect only on release for safe cropping
|
|
1003
|
-
var wrapperRect = wrapper;
|
|
1004
|
-
var overshootMinX = wrapperRect.x;
|
|
1005
|
-
var overshootMaxX = wrapperRect.x + wrapperRect.width;
|
|
1006
|
-
var overshootMinY = wrapperRect.y;
|
|
1007
|
-
var overshootMaxY = wrapperRect.y + wrapperRect.height;
|
|
1008
|
-
|
|
1009
|
-
// ✅ DRAG BOUNDS: Clamp ONLY to overshootBounds during drag (NOT strictBounds)
|
|
906
|
+
// ✅ STRICT BOUNDS: visible image area (wrapper-relative) for lastValidPosition
|
|
907
|
+
var strictMinX = contentRect.x;
|
|
908
|
+
var strictMaxX = contentRect.x + contentRect.width;
|
|
909
|
+
var strictMinY = contentRect.y;
|
|
910
|
+
var strictMaxY = contentRect.y + contentRect.height;
|
|
911
|
+
|
|
912
|
+
// ✅ DRAG BOUNDS: keep points strictly on the visible picture (no going outside image)
|
|
913
|
+
var overshootMinX = strictMinX;
|
|
914
|
+
var overshootMaxX = strictMaxX;
|
|
915
|
+
var overshootMinY = strictMinY;
|
|
916
|
+
var overshootMaxY = strictMaxY;
|
|
917
|
+
|
|
918
|
+
// ✅ Clamp to visible image so points always stay on the picture
|
|
1010
919
|
var dragX = Math.max(overshootMinX, Math.min(newX, overshootMaxX));
|
|
1011
920
|
var dragY = Math.max(overshootMinY, Math.min(newY, overshootMaxY));
|
|
1012
921
|
|
|
@@ -1146,12 +1055,13 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1146
1055
|
}
|
|
1147
1056
|
}
|
|
1148
1057
|
if (imageRect.width > 0 && imageRect.height > 0) {
|
|
1149
|
-
|
|
1150
|
-
var
|
|
1151
|
-
var
|
|
1152
|
-
var
|
|
1058
|
+
// Bounds in wrapper-relative coords (same as point coords)
|
|
1059
|
+
var minX = imageRect.x;
|
|
1060
|
+
var minY = imageRect.y;
|
|
1061
|
+
var maxX = imageRect.x + imageRect.width;
|
|
1062
|
+
var maxY = imageRect.y + imageRect.height;
|
|
1153
1063
|
|
|
1154
|
-
// Clamp the dragged point back to
|
|
1064
|
+
// Clamp the dragged point back to visible image area (full picture in wrapper-relative coords)
|
|
1155
1065
|
setPoints(function (prev) {
|
|
1156
1066
|
// ✅ SAFETY: Ensure prev is a valid array
|
|
1157
1067
|
if (!Array.isArray(prev) || prev.length === 0) {
|
|
@@ -1168,8 +1078,8 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1168
1078
|
return prev;
|
|
1169
1079
|
}
|
|
1170
1080
|
var clampedPoint = {
|
|
1171
|
-
x: Math.max(
|
|
1172
|
-
y: Math.max(
|
|
1081
|
+
x: Math.max(minX, Math.min(point.x, maxX)),
|
|
1082
|
+
y: Math.max(minY, Math.min(point.y, maxY))
|
|
1173
1083
|
};
|
|
1174
1084
|
|
|
1175
1085
|
// Only update if point was outside bounds
|
|
@@ -1184,10 +1094,10 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1184
1094
|
y: clampedPoint.y.toFixed(2)
|
|
1185
1095
|
},
|
|
1186
1096
|
bounds: {
|
|
1187
|
-
minX:
|
|
1188
|
-
maxX:
|
|
1189
|
-
minY:
|
|
1190
|
-
maxY:
|
|
1097
|
+
minX: minX.toFixed(2),
|
|
1098
|
+
maxX: maxX.toFixed(2),
|
|
1099
|
+
minY: minY.toFixed(2),
|
|
1100
|
+
maxY: maxY.toFixed(2)
|
|
1191
1101
|
}
|
|
1192
1102
|
});
|
|
1193
1103
|
return prev.map(function (p, i) {
|
|
@@ -1336,10 +1246,12 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1336
1246
|
onCancel: function onCancel() {
|
|
1337
1247
|
return setShowCustomCamera(false);
|
|
1338
1248
|
}
|
|
1339
|
-
}) : /*#__PURE__*/_react["default"].createElement(
|
|
1340
|
-
style:
|
|
1341
|
-
|
|
1342
|
-
|
|
1249
|
+
}) : /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
1250
|
+
style: _ImageCropperStyles["default"].content
|
|
1251
|
+
}, image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
1252
|
+
style: _ImageCropperStyles["default"].imageRegion
|
|
1253
|
+
}, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
1254
|
+
style: _ImageCropperStyles["default"].commonWrapper,
|
|
1343
1255
|
ref: commonWrapperRef,
|
|
1344
1256
|
onLayout: onCommonWrapperLayout
|
|
1345
1257
|
}, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
@@ -1420,8 +1332,17 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1420
1332
|
pointerEvents: "none"
|
|
1421
1333
|
}, function () {
|
|
1422
1334
|
// ✅ Use wrapper dimensions for SVG path (wrapper coordinates)
|
|
1423
|
-
var wrapperWidth = commonWrapperLayout.current.width ||
|
|
1424
|
-
var wrapperHeight = commonWrapperLayout.current.height ||
|
|
1335
|
+
var wrapperWidth = commonWrapperLayout.current.width || windowWidth;
|
|
1336
|
+
var wrapperHeight = commonWrapperLayout.current.height || windowHeight;
|
|
1337
|
+
|
|
1338
|
+
// ✅ Size scale: when picture area is smaller, make stroke/points smaller too
|
|
1339
|
+
var base = Math.min(windowWidth, windowHeight) || 1;
|
|
1340
|
+
var pictureDiagonal = Math.sqrt(wrapperWidth * wrapperWidth + wrapperHeight * wrapperHeight);
|
|
1341
|
+
var sizeScaleRaw = pictureDiagonal / base;
|
|
1342
|
+
var sizeScale = Math.max(0.6, Math.min(1.4, sizeScaleRaw)); // clamp
|
|
1343
|
+
|
|
1344
|
+
var stroke = Math.max(0.75, 2 * responsiveScale * sizeScale);
|
|
1345
|
+
var handleRadius = Math.max(4, 10 * responsiveScale * sizeScale);
|
|
1425
1346
|
return /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Path, {
|
|
1426
1347
|
d: "M 0 0 H ".concat(wrapperWidth, " V ").concat(wrapperHeight, " H 0 Z ").concat(createPath()),
|
|
1427
1348
|
fill: showResult ? 'white' : 'rgba(0, 0, 0, 0.8)',
|
|
@@ -1430,13 +1351,13 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1430
1351
|
d: createPath(),
|
|
1431
1352
|
fill: "transparent",
|
|
1432
1353
|
stroke: "white",
|
|
1433
|
-
strokeWidth:
|
|
1354
|
+
strokeWidth: stroke
|
|
1434
1355
|
}), !showResult && points.map(function (point, index) {
|
|
1435
1356
|
return /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Circle, {
|
|
1436
1357
|
key: index,
|
|
1437
1358
|
cx: point.x,
|
|
1438
1359
|
cy: point.y,
|
|
1439
|
-
r:
|
|
1360
|
+
r: handleRadius,
|
|
1440
1361
|
fill: "white"
|
|
1441
1362
|
});
|
|
1442
1363
|
}));
|
|
@@ -1457,15 +1378,22 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1457
1378
|
}), /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
|
|
1458
1379
|
style: {
|
|
1459
1380
|
color: PRIMARY_GREEN,
|
|
1460
|
-
marginTop: 8,
|
|
1461
|
-
fontSize: 14
|
|
1381
|
+
marginTop: 8 * responsiveScale,
|
|
1382
|
+
fontSize: Math.max(12, Math.round(14 * responsiveScale))
|
|
1462
1383
|
}
|
|
1463
|
-
}, rotationLabel !== null && rotationLabel !== void 0 ? rotationLabel : 'Rotation...'))), !showResult && image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
1384
|
+
}, rotationLabel !== null && rotationLabel !== void 0 ? rotationLabel : 'Rotation...')))), !showResult && image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
1464
1385
|
style: [_ImageCropperStyles["default"].buttonContainerBelow, {
|
|
1465
|
-
paddingBottom: Math.max(insets.bottom, 16)
|
|
1386
|
+
paddingBottom: Math.max(insets.bottom, Math.round(16 * responsiveScale)),
|
|
1387
|
+
paddingTop: Math.round(12 * responsiveScale),
|
|
1388
|
+
paddingHorizontal: Math.round(12 * responsiveScale),
|
|
1389
|
+
gap: Math.round(8 * responsiveScale)
|
|
1466
1390
|
}]
|
|
1467
1391
|
}, _reactNative.Platform.OS === 'android' && /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
|
|
1468
|
-
style: [_ImageCropperStyles["default"].rotationButton,
|
|
1392
|
+
style: [_ImageCropperStyles["default"].rotationButton, {
|
|
1393
|
+
width: Math.round(56 * responsiveScale),
|
|
1394
|
+
height: Math.round(48 * responsiveScale),
|
|
1395
|
+
borderRadius: Math.round(28 * responsiveScale)
|
|
1396
|
+
}, isRotating && {
|
|
1469
1397
|
opacity: 0.7
|
|
1470
1398
|
}],
|
|
1471
1399
|
onPress: function onPress() {
|
|
@@ -1477,15 +1405,20 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1477
1405
|
color: "white"
|
|
1478
1406
|
}) : /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
|
|
1479
1407
|
name: "sync",
|
|
1480
|
-
size: 24,
|
|
1408
|
+
size: Math.round(24 * responsiveScale),
|
|
1481
1409
|
color: "white"
|
|
1482
1410
|
})), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
|
|
1483
|
-
style: _ImageCropperStyles["default"].button,
|
|
1411
|
+
style: [_ImageCropperStyles["default"].button, {
|
|
1412
|
+
minHeight: Math.round(48 * responsiveScale),
|
|
1413
|
+
paddingVertical: Math.round(12 * responsiveScale),
|
|
1414
|
+
paddingHorizontal: Math.round(10 * responsiveScale)
|
|
1415
|
+
}],
|
|
1484
1416
|
onPress: handleReset
|
|
1485
1417
|
}, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
|
|
1486
|
-
style: _ImageCropperStyles["default"].buttonText
|
|
1418
|
+
style: [_ImageCropperStyles["default"].buttonText, {
|
|
1419
|
+
fontSize: Math.max(14, Math.round(18 * responsiveScale))
|
|
1420
|
+
}]
|
|
1487
1421
|
}, "Reset")), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
|
|
1488
|
-
style: _ImageCropperStyles["default"].button,
|
|
1489
1422
|
onPress: /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
|
|
1490
1423
|
var _cameraFrameData$curr4, actualImageWidth, actualImageHeight, isCoverMode, captured, layout, contentRect, displayedWidth, displayedHeight, scale, coverOffsetX, coverOffsetY, scaledWidth, scaledHeight, originalUri, cropMeta, imagePoints, minX, minY, maxX, maxY, cropX, cropY, cropEndX, cropEndY, cropWidth, cropHeight, bbox, polygon, ext, safeExt, name, _t2;
|
|
1491
1424
|
return _regenerator().w(function (_context2) {
|
|
@@ -1651,9 +1584,16 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1651
1584
|
return _context2.a(2);
|
|
1652
1585
|
}
|
|
1653
1586
|
}, _callee2, null, [[1, 5, 6, 7]]);
|
|
1654
|
-
}))
|
|
1587
|
+
})),
|
|
1588
|
+
style: [_ImageCropperStyles["default"].button, {
|
|
1589
|
+
minHeight: Math.round(48 * responsiveScale),
|
|
1590
|
+
paddingVertical: Math.round(12 * responsiveScale),
|
|
1591
|
+
paddingHorizontal: Math.round(10 * responsiveScale)
|
|
1592
|
+
}]
|
|
1655
1593
|
}, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
|
|
1656
|
-
style: _ImageCropperStyles["default"].buttonText
|
|
1594
|
+
style: [_ImageCropperStyles["default"].buttonText, {
|
|
1595
|
+
fontSize: Math.max(14, Math.round(18 * responsiveScale))
|
|
1596
|
+
}]
|
|
1657
1597
|
}, "Confirm"))), !showResult && !image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
1658
1598
|
style: _ImageCropperStyles["default"].centerButtonsContainer
|
|
1659
1599
|
}, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
|
|
@@ -16,21 +16,36 @@ var GLOW_WHITE = 'rgba(255, 255, 255, 0.85)';
|
|
|
16
16
|
var styles = _reactNative.StyleSheet.create({
|
|
17
17
|
container: {
|
|
18
18
|
flex: 1,
|
|
19
|
-
|
|
19
|
+
width: '100%',
|
|
20
|
+
height: '100%',
|
|
21
|
+
alignItems: 'stretch',
|
|
20
22
|
justifyContent: 'flex-start',
|
|
21
|
-
// ✅ Start from top, allow content to flow down
|
|
22
23
|
backgroundColor: DEEP_BLACK
|
|
23
24
|
},
|
|
24
|
-
//
|
|
25
|
+
// Main content area: full screen; image centered, buttons below
|
|
26
|
+
content: {
|
|
27
|
+
flex: 1,
|
|
28
|
+
width: '100%',
|
|
29
|
+
alignSelf: 'stretch',
|
|
30
|
+
flexDirection: 'column',
|
|
31
|
+
minHeight: 0
|
|
32
|
+
},
|
|
33
|
+
// Wrapper that occupies all space above the buttons
|
|
34
|
+
imageRegion: {
|
|
35
|
+
flex: 1,
|
|
36
|
+
width: '100%',
|
|
37
|
+
alignSelf: 'stretch',
|
|
38
|
+
justifyContent: 'flex-start',
|
|
39
|
+
alignItems: 'stretch'
|
|
40
|
+
},
|
|
41
|
+
// Crop preview: fill all available space in imageRegion
|
|
25
42
|
commonWrapper: {
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
flex: 1,
|
|
44
|
+
width: '100%',
|
|
28
45
|
overflow: 'hidden',
|
|
29
46
|
alignItems: 'center',
|
|
30
47
|
justifyContent: 'center',
|
|
31
|
-
|
|
32
|
-
backgroundColor: 'black',
|
|
33
|
-
marginBottom: 0
|
|
48
|
+
backgroundColor: 'black'
|
|
34
49
|
},
|
|
35
50
|
buttonContainer: {
|
|
36
51
|
position: 'absolute',
|
|
@@ -46,18 +61,14 @@ var styles = _reactNative.StyleSheet.create({
|
|
|
46
61
|
gap: 10
|
|
47
62
|
},
|
|
48
63
|
buttonContainerBelow: {
|
|
49
|
-
// ✅ Buttons positioned BELOW image, not overlapping, above Android navigation bar
|
|
50
|
-
position: 'absolute',
|
|
51
|
-
bottom: 0,
|
|
52
|
-
left: 0,
|
|
53
|
-
right: 0,
|
|
54
64
|
flexDirection: 'row',
|
|
55
|
-
justifyContent: 'center',
|
|
56
65
|
alignItems: 'center',
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
justifyContent: 'space-between',
|
|
67
|
+
width: '100%',
|
|
68
|
+
paddingHorizontal: 12,
|
|
69
|
+
paddingTop: 12,
|
|
59
70
|
gap: 8,
|
|
60
|
-
backgroundColor: DEEP_BLACK
|
|
71
|
+
backgroundColor: DEEP_BLACK
|
|
61
72
|
},
|
|
62
73
|
iconButton: {
|
|
63
74
|
backgroundColor: PRIMARY_GREEN,
|
package/package.json
CHANGED
package/src/ImageCropper.js
CHANGED
|
@@ -10,12 +10,14 @@ import { applyMaskToImage, MaskView } from './ImageMaskProcessor';
|
|
|
10
10
|
|
|
11
11
|
const PRIMARY_GREEN = '#198754';
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
const
|
|
13
|
+
// Base dimension for responsive scaling (e.g. ~375pt design width)
|
|
14
|
+
const RESPONSIVE_BASE = 375;
|
|
15
15
|
|
|
16
16
|
const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rotationLabel }) => {
|
|
17
|
-
const { width: windowWidth } = useWindowDimensions();
|
|
18
|
-
|
|
17
|
+
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
|
18
|
+
|
|
19
|
+
// Responsive scale: image and UI adapt to small (phone) and large (tablet) screens
|
|
20
|
+
const responsiveScale = Math.max(0.65, Math.min(2, Math.min(windowWidth, windowHeight) / RESPONSIVE_BASE));
|
|
19
21
|
|
|
20
22
|
const guessExtensionFromUri = (uri) => {
|
|
21
23
|
if (!uri || typeof uri !== 'string') return null;
|
|
@@ -343,69 +345,11 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
|
|
|
343
345
|
}
|
|
344
346
|
|
|
345
347
|
// ✅ CRITICAL FIX: Calculate crop box as percentage of VISIBLE IMAGE AREA (imageDisplayRect)
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
const imageRectY = wrapper.y + imageRect.y;
|
|
350
|
-
|
|
351
|
-
// ✅ PRIORITY RULE #3: IF image comes from camera → Use EXACT green frame coordinates
|
|
352
|
-
// Image is displayed in "cover" mode (full wrapper), so green frame coords = white frame coords.
|
|
353
|
-
// Store points in WRAPPER-RELATIVE coordinates (greenFrame.x, greenFrame.y) so they match
|
|
354
|
-
// touch events (locationX/locationY are relative to the wrapper). This keeps display→image
|
|
355
|
-
// conversion correct in Confirm.
|
|
356
|
-
if (cameraFrameData.current && cameraFrameData.current.greenFrame && originalImageDimensions.current.width > 0) {
|
|
357
|
-
const greenFrame = cameraFrameData.current.greenFrame;
|
|
358
|
-
|
|
359
|
-
const boxX = greenFrame.x;
|
|
360
|
-
const boxY = greenFrame.y;
|
|
361
|
-
const boxWidth = greenFrame.width;
|
|
362
|
-
const boxHeight = greenFrame.height;
|
|
363
|
-
|
|
364
|
-
// ✅ SAFETY: Validate calculated coordinates before creating points
|
|
365
|
-
const isValidCoordinate = (val) => typeof val === 'number' && isFinite(val) && !isNaN(val);
|
|
366
|
-
|
|
367
|
-
if (!isValidCoordinate(boxX) || !isValidCoordinate(boxY) ||
|
|
368
|
-
!isValidCoordinate(boxWidth) || !isValidCoordinate(boxHeight)) {
|
|
369
|
-
console.warn("⚠️ Invalid coordinates calculated for crop box, skipping initialization");
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// ✅ Points in wrapper-relative coords (same as touch events)
|
|
374
|
-
const newPoints = [
|
|
375
|
-
{ x: boxX, y: boxY }, // Top-left
|
|
376
|
-
{ x: boxX + boxWidth, y: boxY }, // Top-right
|
|
377
|
-
{ x: boxX + boxWidth, y: boxY + boxHeight }, // Bottom-right
|
|
378
|
-
{ x: boxX, y: boxY + boxHeight }, // Bottom-left
|
|
379
|
-
];
|
|
380
|
-
|
|
381
|
-
// ✅ SAFETY: Validate all points before setting
|
|
382
|
-
const validPoints = newPoints.filter(p => isValidCoordinate(p.x) && isValidCoordinate(p.y));
|
|
383
|
-
if (validPoints.length !== newPoints.length) {
|
|
384
|
-
console.warn("⚠️ Some points have invalid coordinates, skipping initialization");
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
console.log("✅ Initializing crop box for camera image (COVER MODE - exact green frame):", {
|
|
389
|
-
greenFrame: { x: greenFrame.x, y: greenFrame.y, width: greenFrame.width, height: greenFrame.height },
|
|
390
|
-
whiteFrame: { x: boxX.toFixed(2), y: boxY.toFixed(2), width: boxWidth.toFixed(2), height: boxHeight.toFixed(2) },
|
|
391
|
-
note: "Points in wrapper-relative coords - same as touch events and crop_image_size",
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
setPoints(newPoints);
|
|
395
|
-
hasInitializedCropBox.current = true; // ✅ CRITICAL: Mark as initialized
|
|
396
|
-
// ✅ CRITICAL: DO NOT nullify cameraFrameData here - keep it for Image.getSize() callback check
|
|
397
|
-
// It will be cleared when loading a new image
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// ✅ PRIORITY RULE #3: DEFAULT logic ONLY for gallery images (NOT camera)
|
|
402
|
-
// If we reach here and imageSource is 'camera', something went wrong
|
|
403
|
-
if (imageSource.current === 'camera') {
|
|
404
|
-
console.warn("⚠️ Camera image but no greenFrame found - this should not happen");
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
348
|
+
// in WRAPPER-RELATIVE coordinates (same space as points/touches and SVG).
|
|
349
|
+
const imageRectX = imageRect.x;
|
|
350
|
+
const imageRectY = imageRect.y;
|
|
407
351
|
|
|
408
|
-
// ✅ DEFAULT: Crop box (70% of VISIBLE IMAGE AREA - centered)
|
|
352
|
+
// ✅ DEFAULT: Crop box (70% of VISIBLE IMAGE AREA - centered)
|
|
409
353
|
const boxWidth = imageRect.width * 0.70; // 70% of visible image width
|
|
410
354
|
const boxHeight = imageRect.height * 0.70; // 70% of visible image height
|
|
411
355
|
const boxX = imageRectX + (imageRect.width - boxWidth) / 2; // Centered in image area
|
|
@@ -437,8 +381,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
|
|
|
437
381
|
console.log("✅ Initializing crop box (default - 70% of visible image area, gallery only):", {
|
|
438
382
|
wrapper: { width: wrapper.width, height: wrapper.height },
|
|
439
383
|
imageDisplayRect: imageRect,
|
|
440
|
-
|
|
441
|
-
points: newPoints
|
|
384
|
+
boxInWrapperCoords: { x: boxX, y: boxY, width: boxWidth, height: boxHeight },
|
|
385
|
+
points: newPoints,
|
|
442
386
|
});
|
|
443
387
|
|
|
444
388
|
setPoints(newPoints);
|
|
@@ -620,14 +564,14 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
|
|
|
620
564
|
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
621
565
|
imageRect = imageDisplayRect.current;
|
|
622
566
|
}
|
|
623
|
-
// If still not available, use wrapper as fallback
|
|
567
|
+
// If still not available, use full wrapper as fallback (wrapper-relative coords)
|
|
624
568
|
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
625
569
|
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
626
570
|
imageRect = {
|
|
627
|
-
x:
|
|
628
|
-
y:
|
|
571
|
+
x: 0,
|
|
572
|
+
y: 0,
|
|
629
573
|
width: wrapper.width,
|
|
630
|
-
height: wrapper.height
|
|
574
|
+
height: wrapper.height,
|
|
631
575
|
};
|
|
632
576
|
} else {
|
|
633
577
|
console.warn("⚠️ Cannot handle tap: wrapper or imageDisplayRect not available");
|
|
@@ -637,17 +581,15 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
|
|
|
637
581
|
}
|
|
638
582
|
|
|
639
583
|
// ✅ Clamp to real displayed image content (imageDisplayRect dans le wrapper)
|
|
640
|
-
// Les coordonnées tapX/tapY sont
|
|
641
|
-
const imageRectX = wrapper.x + imageRect.x;
|
|
642
|
-
const imageRectY = wrapper.y + imageRect.y;
|
|
584
|
+
// Les coordonnées tapX/tapY et imageRect sont RELATIVES au wrapper commun (origine 0,0)
|
|
643
585
|
const { x: cx, y: cy, width: cw, height: ch } = {
|
|
644
|
-
x:
|
|
645
|
-
y:
|
|
586
|
+
x: imageRect.x,
|
|
587
|
+
y: imageRect.y,
|
|
646
588
|
width: imageRect.width,
|
|
647
|
-
height: imageRect.height
|
|
589
|
+
height: imageRect.height,
|
|
648
590
|
};
|
|
649
|
-
// ✅
|
|
650
|
-
const selectRadius =
|
|
591
|
+
// ✅ Responsive select radius: scales with screen so touch target matches handle size
|
|
592
|
+
const selectRadius = Math.max(28, Math.round(50 * responsiveScale));
|
|
651
593
|
|
|
652
594
|
// ✅ CRITICAL: Check for existing point selection FIRST (using raw tap coordinates)
|
|
653
595
|
// Don't clamp tapX/Y for point selection - points can be anywhere in wrapper now
|
|
@@ -770,14 +712,14 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
|
|
|
770
712
|
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
771
713
|
imageRect = imageDisplayRect.current;
|
|
772
714
|
}
|
|
773
|
-
// If still not available, use wrapper as fallback
|
|
715
|
+
// If still not available, use full wrapper as fallback (wrapper-relative coords)
|
|
774
716
|
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
775
717
|
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
776
718
|
imageRect = {
|
|
777
|
-
x:
|
|
778
|
-
y:
|
|
719
|
+
x: 0,
|
|
720
|
+
y: 0,
|
|
779
721
|
width: wrapper.width,
|
|
780
|
-
height: wrapper.height
|
|
722
|
+
height: wrapper.height,
|
|
781
723
|
};
|
|
782
724
|
} else {
|
|
783
725
|
console.warn("⚠️ Cannot move point: wrapper or imageDisplayRect not available");
|
|
@@ -786,13 +728,11 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
|
|
|
786
728
|
}
|
|
787
729
|
}
|
|
788
730
|
|
|
789
|
-
// ✅
|
|
790
|
-
//
|
|
791
|
-
const imageRectX = wrapper.x + imageRect.x;
|
|
792
|
-
const imageRectY = wrapper.y + imageRect.y;
|
|
731
|
+
// ✅ Bounds in wrapper-relative coordinates (touch/points use wrapper as origin)
|
|
732
|
+
// Full picture = entire wrapper; visible image = imageDisplayRect
|
|
793
733
|
const contentRect = {
|
|
794
|
-
x:
|
|
795
|
-
y:
|
|
734
|
+
x: imageRect.x,
|
|
735
|
+
y: imageRect.y,
|
|
796
736
|
width: imageRect.width,
|
|
797
737
|
height: imageRect.height
|
|
798
738
|
};
|
|
@@ -820,25 +760,19 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
|
|
|
820
760
|
const newX = currentX + touchOffset.current.x;
|
|
821
761
|
const newY = currentY + touchOffset.current.y;
|
|
822
762
|
|
|
823
|
-
// ✅
|
|
824
|
-
const
|
|
763
|
+
// ✅ STRICT BOUNDS: visible image area (wrapper-relative) for lastValidPosition
|
|
764
|
+
const strictMinX = contentRect.x;
|
|
765
|
+
const strictMaxX = contentRect.x + contentRect.width;
|
|
766
|
+
const strictMinY = contentRect.y;
|
|
767
|
+
const strictMaxY = contentRect.y + contentRect.height;
|
|
825
768
|
|
|
826
|
-
// ✅
|
|
827
|
-
const
|
|
828
|
-
const
|
|
829
|
-
const
|
|
830
|
-
const
|
|
769
|
+
// ✅ DRAG BOUNDS: keep points strictly on the visible picture (no going outside image)
|
|
770
|
+
const overshootMinX = strictMinX;
|
|
771
|
+
const overshootMaxX = strictMaxX;
|
|
772
|
+
const overshootMinY = strictMinY;
|
|
773
|
+
const overshootMaxY = strictMaxY;
|
|
831
774
|
|
|
832
|
-
// ✅
|
|
833
|
-
// Points can move freely across the entire screen for maximum flexibility
|
|
834
|
-
// They will be clamped to imageDisplayRect only on release for safe cropping
|
|
835
|
-
const wrapperRect = wrapper;
|
|
836
|
-
const overshootMinX = wrapperRect.x;
|
|
837
|
-
const overshootMaxX = wrapperRect.x + wrapperRect.width;
|
|
838
|
-
const overshootMinY = wrapperRect.y;
|
|
839
|
-
const overshootMaxY = wrapperRect.y + wrapperRect.height;
|
|
840
|
-
|
|
841
|
-
// ✅ DRAG BOUNDS: Clamp ONLY to overshootBounds during drag (NOT strictBounds)
|
|
775
|
+
// ✅ Clamp to visible image so points always stay on the picture
|
|
842
776
|
const dragX = Math.max(overshootMinX, Math.min(newX, overshootMaxX));
|
|
843
777
|
const dragY = Math.max(overshootMinY, Math.min(newY, overshootMaxY));
|
|
844
778
|
|
|
@@ -974,12 +908,13 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
|
|
|
974
908
|
}
|
|
975
909
|
|
|
976
910
|
if (imageRect.width > 0 && imageRect.height > 0) {
|
|
977
|
-
|
|
978
|
-
const
|
|
979
|
-
const
|
|
980
|
-
const
|
|
911
|
+
// Bounds in wrapper-relative coords (same as point coords)
|
|
912
|
+
const minX = imageRect.x;
|
|
913
|
+
const minY = imageRect.y;
|
|
914
|
+
const maxX = imageRect.x + imageRect.width;
|
|
915
|
+
const maxY = imageRect.y + imageRect.height;
|
|
981
916
|
|
|
982
|
-
// Clamp the dragged point back to
|
|
917
|
+
// Clamp the dragged point back to visible image area (full picture in wrapper-relative coords)
|
|
983
918
|
setPoints(prev => {
|
|
984
919
|
// ✅ SAFETY: Ensure prev is a valid array
|
|
985
920
|
if (!Array.isArray(prev) || prev.length === 0) {
|
|
@@ -999,8 +934,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
|
|
|
999
934
|
}
|
|
1000
935
|
|
|
1001
936
|
const clampedPoint = {
|
|
1002
|
-
x: Math.max(
|
|
1003
|
-
y: Math.max(
|
|
937
|
+
x: Math.max(minX, Math.min(point.x, maxX)),
|
|
938
|
+
y: Math.max(minY, Math.min(point.y, maxY))
|
|
1004
939
|
};
|
|
1005
940
|
|
|
1006
941
|
// Only update if point was outside bounds
|
|
@@ -1008,7 +943,7 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
|
|
|
1008
943
|
console.log("🔒 Clamping point back to image bounds on release:", {
|
|
1009
944
|
before: { x: point.x.toFixed(2), y: point.y.toFixed(2) },
|
|
1010
945
|
after: { x: clampedPoint.x.toFixed(2), y: clampedPoint.y.toFixed(2) },
|
|
1011
|
-
bounds: { minX:
|
|
946
|
+
bounds: { minX: minX.toFixed(2), maxX: maxX.toFixed(2), minY: minY.toFixed(2), maxY: maxY.toFixed(2) }
|
|
1012
947
|
});
|
|
1013
948
|
|
|
1014
949
|
return prev.map((p, i) => i === pointIndex ? clampedPoint : p);
|
|
@@ -1132,16 +1067,14 @@ const rotatePreviewImage = async (degrees) => {
|
|
|
1132
1067
|
onCancel={() => setShowCustomCamera(false)}
|
|
1133
1068
|
/>
|
|
1134
1069
|
) : (
|
|
1135
|
-
|
|
1070
|
+
<View style={styles.content}>
|
|
1136
1071
|
{image && (
|
|
1137
|
-
<View
|
|
1138
|
-
|
|
1139
|
-
styles.commonWrapper
|
|
1140
|
-
{
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
onLayout={onCommonWrapperLayout}
|
|
1144
|
-
>
|
|
1072
|
+
<View style={styles.imageRegion}>
|
|
1073
|
+
<View
|
|
1074
|
+
style={styles.commonWrapper}
|
|
1075
|
+
ref={commonWrapperRef}
|
|
1076
|
+
onLayout={onCommonWrapperLayout}
|
|
1077
|
+
>
|
|
1145
1078
|
<View
|
|
1146
1079
|
ref={viewRef}
|
|
1147
1080
|
collapsable={false}
|
|
@@ -1217,8 +1150,18 @@ const rotatePreviewImage = async (degrees) => {
|
|
|
1217
1150
|
<Svg style={styles.overlay} pointerEvents="none">
|
|
1218
1151
|
{(() => {
|
|
1219
1152
|
// ✅ Use wrapper dimensions for SVG path (wrapper coordinates)
|
|
1220
|
-
const wrapperWidth = commonWrapperLayout.current.width ||
|
|
1221
|
-
const wrapperHeight = commonWrapperLayout.current.height ||
|
|
1153
|
+
const wrapperWidth = commonWrapperLayout.current.width || windowWidth;
|
|
1154
|
+
const wrapperHeight = commonWrapperLayout.current.height || windowHeight;
|
|
1155
|
+
|
|
1156
|
+
// ✅ Size scale: when picture area is smaller, make stroke/points smaller too
|
|
1157
|
+
const base = Math.min(windowWidth, windowHeight) || 1;
|
|
1158
|
+
const pictureDiagonal = Math.sqrt(wrapperWidth * wrapperWidth + wrapperHeight * wrapperHeight);
|
|
1159
|
+
const sizeScaleRaw = pictureDiagonal / base;
|
|
1160
|
+
const sizeScale = Math.max(0.6, Math.min(1.4, sizeScaleRaw)); // clamp
|
|
1161
|
+
|
|
1162
|
+
const stroke = Math.max(0.75, 2 * responsiveScale * sizeScale);
|
|
1163
|
+
const handleRadius = Math.max(4, 10 * responsiveScale * sizeScale);
|
|
1164
|
+
|
|
1222
1165
|
return (
|
|
1223
1166
|
<>
|
|
1224
1167
|
<Path
|
|
@@ -1227,10 +1170,10 @@ const rotatePreviewImage = async (degrees) => {
|
|
|
1227
1170
|
fillRule="evenodd"
|
|
1228
1171
|
/>
|
|
1229
1172
|
{!showResult && points.length > 0 && (
|
|
1230
|
-
<Path d={createPath()} fill="transparent" stroke="white" strokeWidth={
|
|
1173
|
+
<Path d={createPath()} fill="transparent" stroke="white" strokeWidth={stroke} />
|
|
1231
1174
|
)}
|
|
1232
1175
|
{!showResult && points.map((point, index) => (
|
|
1233
|
-
<Circle key={index} cx={point.x} cy={point.y} r={
|
|
1176
|
+
<Circle key={index} cx={point.x} cy={point.y} r={handleRadius} fill="white" />
|
|
1234
1177
|
))}
|
|
1235
1178
|
</>
|
|
1236
1179
|
);
|
|
@@ -1240,35 +1183,61 @@ const rotatePreviewImage = async (degrees) => {
|
|
|
1240
1183
|
{isRotating && (
|
|
1241
1184
|
<View style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center' }}>
|
|
1242
1185
|
<ActivityIndicator size="large" color={PRIMARY_GREEN} />
|
|
1243
|
-
<Text style={{ color: PRIMARY_GREEN, marginTop: 8, fontSize: 14 }}>{rotationLabel ?? 'Rotation...'}</Text>
|
|
1186
|
+
<Text style={{ color: PRIMARY_GREEN, marginTop: 8 * responsiveScale, fontSize: Math.max(12, Math.round(14 * responsiveScale)) }}>{rotationLabel ?? 'Rotation...'}</Text>
|
|
1244
1187
|
</View>
|
|
1245
1188
|
)}
|
|
1246
1189
|
</View>
|
|
1190
|
+
</View>
|
|
1247
1191
|
)}
|
|
1248
1192
|
|
|
1249
|
-
{/* ✅ Buttons positioned
|
|
1193
|
+
{/* ✅ Buttons positioned as a responsive bottom bar (scale with screen size) */}
|
|
1250
1194
|
{!showResult && image && (
|
|
1251
|
-
<View style={[
|
|
1195
|
+
<View style={[
|
|
1196
|
+
styles.buttonContainerBelow,
|
|
1197
|
+
{
|
|
1198
|
+
paddingBottom: Math.max(insets.bottom, Math.round(16 * responsiveScale)),
|
|
1199
|
+
paddingTop: Math.round(12 * responsiveScale),
|
|
1200
|
+
paddingHorizontal: Math.round(12 * responsiveScale),
|
|
1201
|
+
gap: Math.round(8 * responsiveScale),
|
|
1202
|
+
},
|
|
1203
|
+
]}>
|
|
1252
1204
|
{Platform.OS === 'android' && (
|
|
1253
1205
|
<TouchableOpacity
|
|
1254
|
-
style={[
|
|
1206
|
+
style={[
|
|
1207
|
+
styles.rotationButton,
|
|
1208
|
+
{
|
|
1209
|
+
width: Math.round(56 * responsiveScale),
|
|
1210
|
+
height: Math.round(48 * responsiveScale),
|
|
1211
|
+
borderRadius: Math.round(28 * responsiveScale),
|
|
1212
|
+
},
|
|
1213
|
+
isRotating && { opacity: 0.7 },
|
|
1214
|
+
]}
|
|
1255
1215
|
onPress={() => enableRotation && rotatePreviewImage(90)}
|
|
1256
1216
|
disabled={isRotating}
|
|
1257
1217
|
>
|
|
1258
1218
|
{isRotating ? (
|
|
1259
1219
|
<ActivityIndicator size="small" color="white" />
|
|
1260
1220
|
) : (
|
|
1261
|
-
<Ionicons name="sync" size={24} color="white" />
|
|
1221
|
+
<Ionicons name="sync" size={Math.round(24 * responsiveScale)} color="white" />
|
|
1262
1222
|
)}
|
|
1263
1223
|
</TouchableOpacity>
|
|
1264
1224
|
)}
|
|
1265
1225
|
|
|
1266
|
-
<TouchableOpacity
|
|
1267
|
-
|
|
1226
|
+
<TouchableOpacity
|
|
1227
|
+
style={[
|
|
1228
|
+
styles.button,
|
|
1229
|
+
{
|
|
1230
|
+
minHeight: Math.round(48 * responsiveScale),
|
|
1231
|
+
paddingVertical: Math.round(12 * responsiveScale),
|
|
1232
|
+
paddingHorizontal: Math.round(10 * responsiveScale),
|
|
1233
|
+
},
|
|
1234
|
+
]}
|
|
1235
|
+
onPress={handleReset}
|
|
1236
|
+
>
|
|
1237
|
+
<Text style={[styles.buttonText, { fontSize: Math.max(14, Math.round(18 * responsiveScale)) }]}>Reset</Text>
|
|
1268
1238
|
</TouchableOpacity>
|
|
1269
1239
|
|
|
1270
1240
|
<TouchableOpacity
|
|
1271
|
-
style={styles.button}
|
|
1272
1241
|
onPress={async () => {
|
|
1273
1242
|
setIsLoading(true);
|
|
1274
1243
|
try {
|
|
@@ -1399,8 +1368,16 @@ const rotatePreviewImage = async (degrees) => {
|
|
|
1399
1368
|
setShowFullScreenCapture(false);
|
|
1400
1369
|
}
|
|
1401
1370
|
}}
|
|
1371
|
+
style={[
|
|
1372
|
+
styles.button,
|
|
1373
|
+
{
|
|
1374
|
+
minHeight: Math.round(48 * responsiveScale),
|
|
1375
|
+
paddingVertical: Math.round(12 * responsiveScale),
|
|
1376
|
+
paddingHorizontal: Math.round(10 * responsiveScale),
|
|
1377
|
+
},
|
|
1378
|
+
]}
|
|
1402
1379
|
>
|
|
1403
|
-
<Text style={styles.buttonText}>Confirm</Text>
|
|
1380
|
+
<Text style={[styles.buttonText, { fontSize: Math.max(14, Math.round(18 * responsiveScale)) }]}>Confirm</Text>
|
|
1404
1381
|
</TouchableOpacity>
|
|
1405
1382
|
</View>
|
|
1406
1383
|
)}
|
|
@@ -1411,7 +1388,7 @@ const rotatePreviewImage = async (degrees) => {
|
|
|
1411
1388
|
<Text style={styles.welcomeText}>Sélectionnez une image</Text>
|
|
1412
1389
|
</View>
|
|
1413
1390
|
)}
|
|
1414
|
-
|
|
1391
|
+
</View>
|
|
1415
1392
|
)}
|
|
1416
1393
|
|
|
1417
1394
|
{/* ✅ CORRECTION : Vue de masque temporaire pour la capture
|
|
@@ -11,20 +11,36 @@ const GLOW_WHITE = 'rgba(255, 255, 255, 0.85)';
|
|
|
11
11
|
const styles = StyleSheet.create({
|
|
12
12
|
container: {
|
|
13
13
|
flex: 1,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
width: '100%',
|
|
15
|
+
height: '100%',
|
|
16
|
+
alignItems: 'stretch',
|
|
17
|
+
justifyContent: 'flex-start',
|
|
16
18
|
backgroundColor: DEEP_BLACK,
|
|
17
19
|
},
|
|
18
|
-
//
|
|
20
|
+
// Main content area: full screen; image centered, buttons below
|
|
21
|
+
content: {
|
|
22
|
+
flex: 1,
|
|
23
|
+
width: '100%',
|
|
24
|
+
alignSelf: 'stretch',
|
|
25
|
+
flexDirection: 'column',
|
|
26
|
+
minHeight: 0,
|
|
27
|
+
},
|
|
28
|
+
// Wrapper that occupies all space above the buttons
|
|
29
|
+
imageRegion: {
|
|
30
|
+
flex: 1,
|
|
31
|
+
width: '100%',
|
|
32
|
+
alignSelf: 'stretch',
|
|
33
|
+
justifyContent: 'flex-start',
|
|
34
|
+
alignItems: 'stretch',
|
|
35
|
+
},
|
|
36
|
+
// Crop preview: fill all available space in imageRegion
|
|
19
37
|
commonWrapper: {
|
|
20
|
-
|
|
21
|
-
|
|
38
|
+
flex: 1,
|
|
39
|
+
width: '100%',
|
|
22
40
|
overflow: 'hidden',
|
|
23
41
|
alignItems: 'center',
|
|
24
42
|
justifyContent: 'center',
|
|
25
|
-
position: 'relative',
|
|
26
43
|
backgroundColor: 'black',
|
|
27
|
-
marginBottom: 0,
|
|
28
44
|
},
|
|
29
45
|
buttonContainer: {
|
|
30
46
|
position: 'absolute',
|
|
@@ -40,18 +56,14 @@ const styles = StyleSheet.create({
|
|
|
40
56
|
gap: 10,
|
|
41
57
|
},
|
|
42
58
|
buttonContainerBelow: {
|
|
43
|
-
// ✅ Buttons positioned BELOW image, not overlapping, above Android navigation bar
|
|
44
|
-
position: 'absolute',
|
|
45
|
-
bottom: 0,
|
|
46
|
-
left: 0,
|
|
47
|
-
right: 0,
|
|
48
59
|
flexDirection: 'row',
|
|
49
|
-
justifyContent: 'center',
|
|
50
60
|
alignItems: 'center',
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
justifyContent: 'space-between',
|
|
62
|
+
width: '100%',
|
|
63
|
+
paddingHorizontal: 12,
|
|
64
|
+
paddingTop: 12,
|
|
53
65
|
gap: 8,
|
|
54
|
-
backgroundColor: DEEP_BLACK,
|
|
66
|
+
backgroundColor: DEEP_BLACK,
|
|
55
67
|
},
|
|
56
68
|
iconButton: {
|
|
57
69
|
backgroundColor: PRIMARY_GREEN,
|
|
@@ -103,10 +115,10 @@ const styles = StyleSheet.create({
|
|
|
103
115
|
},
|
|
104
116
|
|
|
105
117
|
image: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
},
|
|
118
|
+
width: '100%',
|
|
119
|
+
height: '100%',
|
|
120
|
+
resizeMode: 'contain',
|
|
121
|
+
},
|
|
110
122
|
|
|
111
123
|
overlay: {
|
|
112
124
|
position: 'absolute',
|