Skip to content

Commit dd11f50

Browse files
committed
forceInsideCropArea support any angle
1 parent 7a0186c commit dd11f50

File tree

2 files changed

+126
-49
lines changed

2 files changed

+126
-49
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ To create a rectangular crop area with a 16:9 aspect ratio, use `[`Ratio(width:
7171
### borderRadius
7272
The radius for rounded corners of the cropping area.
7373

74+
### forceInsideCropArea
75+
Whether image area must cover clip path.
76+
7477

7578
# Controller Methods
7679

lib/src/widgets/custom_image_crop_widget.dart

Lines changed: 123 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ class CustomImageCrop extends StatefulWidget {
9292
/// By default, the value is `true`
9393
final bool clipShapeOnCrop;
9494

95+
/// Whether image area must cover clip path
96+
/// By default, the value is `false`
97+
/// If use CustomCropShape.circle, the cropped image may have white blank.
98+
final bool forceInsideCropArea;
99+
95100
/// A custom image cropper widget
96101
///
97102
/// Uses a `CustomImageCropController` to crop the image.
@@ -126,9 +131,10 @@ class CustomImageCrop extends StatefulWidget {
126131
this.ratio,
127132
this.borderRadius = 0,
128133
Paint? imagePaintDuringCrop,
134+
this.forceInsideCropArea = false,
129135
Key? key,
130-
}) : this.imagePaintDuringCrop = imagePaintDuringCrop ??
131-
(Paint()..filterQuality = FilterQuality.high),
136+
}) : this.imagePaintDuringCrop =
137+
imagePaintDuringCrop ?? (Paint()..filterQuality = FilterQuality.high),
132138
assert(
133139
!(shape == CustomCropShape.Ratio && ratio == null),
134140
"If shape is set to Ratio, ratio should not be null.",
@@ -320,82 +326,150 @@ class _CustomImageCropState extends State<CustomImageCrop>
320326
);
321327
}
322328

323-
// just support 'pi/2 * n' angle for now
324-
bool _isAngleSupported() {
325-
final angle = data.angle;
326-
return angle % (pi/2) == 0;
327-
}
328-
329329
void _correctTransition(CropImageData transition, VoidCallback callback) {
330-
if (_imageAsUIImage == null ||
331-
widget.imageFit != CustomImageFit.fillCropSpace ||
332-
!_isAngleSupported()) {
330+
if (!widget.forceInsideCropArea || _imageAsUIImage == null) {
333331
callback();
334332
return;
335333
}
336-
final startData = CropImageData(
337-
scale: data.scale,
338-
angle: data.angle,
339-
x: data.x,
340-
y: data.y,
341-
);
334+
final startX = data.x;
335+
final startY = data.y;
336+
final startScale = data.scale;
342337
callback();
338+
final pathRect = _path.getBounds();
343339
final initialImageRect = _getInitialImageRect();
344-
final diffScale = (1 - data.scale) / 2;
340+
bool isContainPath = _isContainPath(initialImageRect, pathRect, data.scale);
341+
bool isRotated = data.angle != 0;
342+
343+
if (isContainPath) {
344+
return;
345+
}
346+
347+
if (transition.x != 0 || transition.y != 0) {
348+
if (isRotated) {
349+
_addTransitionInternal(CropImageData(x: startX - data.x, y: startY - data.y));
350+
} else {
351+
final imageRect = _getImageRect(initialImageRect, data.scale);
352+
double deltaX = min(pathRect.left - imageRect.left, 0);
353+
deltaX = pathRect.right > imageRect.right ? pathRect.right - imageRect.right : deltaX;
354+
double deltaY = min(pathRect.top - imageRect.top, 0);
355+
deltaY = pathRect.bottom > imageRect.bottom ? pathRect.bottom - imageRect.bottom : deltaY;
356+
_addTransitionInternal(CropImageData(x: deltaX, y: deltaY));
357+
}
358+
return;
359+
}
360+
if (transition.angle == 0) {
361+
_addTransitionInternal(CropImageData(scale: startScale / data.scale));
362+
return;
363+
}
364+
double minEdgeHalf = min(initialImageRect.width, initialImageRect.height) / 2;
365+
double adaptScale = _calculateScaleAfterRotate(pathRect, startScale, initialImageRect, minEdgeHalf);
366+
_addTransitionInternal(CropImageData(scale: adaptScale / data.scale));
367+
}
368+
369+
Rect _getImageRect(Rect initialImageRect, double currentScale) {
370+
final diffScale = (1 - currentScale) / 2;
345371
final left = initialImageRect.left + diffScale * initialImageRect.width + data.x;
346372
final top = initialImageRect.top + diffScale * initialImageRect.height + data.y;
347-
Rect imageRect = Rect.fromLTWH(left, top, data.scale * initialImageRect.width, data.scale * initialImageRect.height);
373+
Rect imageRect = Rect.fromLTWH(
374+
left, top, currentScale * initialImageRect.width, currentScale * initialImageRect.height);
375+
return imageRect;
376+
}
377+
378+
double _getDistanceBetweenPointAndLine(Offset point, Offset lineStart, Offset lineEnd) {
379+
double line1Slop = (lineEnd.dy - lineStart.dy) / (lineEnd.dx - lineStart.dx);
380+
381+
if (line1Slop == 0) {
382+
return (point.dy - lineStart.dy).abs();
383+
}
384+
double line1Delta = lineEnd.dy - lineEnd.dx * line1Slop;
385+
double line2Slop = -1 / line1Slop;
386+
double line2Delta = point.dy - point.dx * line2Slop;
387+
double crossPointX = (line2Delta - line1Delta) / (line1Slop - line2Slop);
388+
double crossPointY = line1Slop * crossPointX + line1Delta;
389+
return (Offset(crossPointX, crossPointY) - point).distance;
390+
}
391+
392+
bool _isContainPath(Rect initialImageRect, Rect pathRect, double currentScale) {
393+
final imageRect = _getImageRect(initialImageRect, currentScale);
348394
Offset topLeft, topRight, bottomLeft, bottomRight;
349395
final rad = atan(imageRect.height / imageRect.width);
350396
final len = sqrt(pow(imageRect.width / 2, 2) + pow(imageRect.height / 2, 2));
397+
bool isRotated = data.angle != 0;
351398

352-
if (data.angle != 0) {
399+
if (isRotated) {
353400
final clockAngle = rad + data.angle;
354401
final counterClockAngle = rad - data.angle;
355402
final cosClockValue = len * cos(clockAngle);
356403
final sinClockValue = len * sin(clockAngle);
357404
final cosCounterClockValue = len * cos(counterClockAngle);
358405
final sinCounterClockValue = len * sin(counterClockAngle);
359-
bottomRight =
360-
imageRect.center.translate(cosClockValue, sinClockValue);
361-
topRight =
362-
imageRect.center.translate(cosCounterClockValue, -sinCounterClockValue);
363-
topLeft =
364-
imageRect.center.translate(-cosClockValue, -sinClockValue);
365-
bottomLeft =
366-
imageRect.center.translate(-cosCounterClockValue, sinCounterClockValue);
406+
bottomRight = imageRect.center.translate(cosClockValue, sinClockValue);
407+
topRight = imageRect.center.translate(cosCounterClockValue, -sinCounterClockValue);
408+
topLeft = imageRect.center.translate(-cosClockValue, -sinClockValue);
409+
bottomLeft = imageRect.center.translate(-cosCounterClockValue, sinCounterClockValue);
410+
} else {
411+
bottomRight = imageRect.bottomRight;
412+
topRight = imageRect.topRight;
413+
topLeft = imageRect.topLeft;
414+
bottomLeft = imageRect.bottomLeft;
415+
}
416+
417+
if (widget.shape == CustomCropShape.Circle) {
418+
final anchor = max(pathRect.width, pathRect.height) / 2;
419+
final pathCenter = pathRect.center;
420+
return _getDistanceBetweenPointAndLine(pathCenter, topLeft, topRight) >= anchor &&
421+
_getDistanceBetweenPointAndLine(pathCenter, topRight, bottomRight) >= anchor &&
422+
_getDistanceBetweenPointAndLine(pathCenter, bottomLeft, bottomRight) >= anchor &&
423+
_getDistanceBetweenPointAndLine(pathCenter, topLeft, bottomLeft) >= anchor;
424+
}
367425

426+
if (isRotated) {
368427
Path imagePath = Path()
369428
..moveTo(topLeft.dx, topLeft.dy)
370429
..lineTo(topRight.dx, topRight.dy)
371430
..lineTo(bottomRight.dx, bottomRight.dy)
372431
..lineTo(bottomLeft.dx, bottomLeft.dy)
373432
..close();
374-
imageRect = imagePath.getBounds();
433+
return imagePath.contains(pathRect.topLeft) &&
434+
imagePath.contains(pathRect.topRight) &&
435+
imagePath.contains(pathRect.bottomLeft) &&
436+
imagePath.contains(pathRect.bottomRight);
437+
} else {
438+
return imageRect.contains(pathRect.topLeft) &&
439+
imageRect.contains(pathRect.topRight) &&
440+
imageRect.contains(pathRect.bottomLeft) &&
441+
imageRect.contains(pathRect.bottomRight);
375442
}
376-
final pathRect = _path.getBounds();
377-
378-
double toPrecision(num n) => double.parse(n.toStringAsFixed(1));
443+
}
379444

380-
num compareTwoDecimal(double a, double b) => toPrecision(a) - toPrecision(b);
381-
382-
if (compareTwoDecimal(imageRect.left, pathRect.left) <= 0 &&
383-
compareTwoDecimal(imageRect.top, pathRect.top) <= 0 &&
384-
compareTwoDecimal(imageRect.right, pathRect.right) >= 0 &&
385-
compareTwoDecimal(imageRect.bottom, pathRect.bottom) >= 0) {
386-
return;
387-
}
388-
if (transition.x != 0 || transition.y != 0 || transition.angle != 0) {
389-
double deltaX = min(pathRect.left - imageRect.left, 0);
390-
deltaX = pathRect.right > imageRect.right ? pathRect.right - imageRect.right : deltaX;
391-
double deltaY = min(pathRect.top - imageRect.top, 0);
392-
deltaY = pathRect.bottom > imageRect.bottom ? pathRect.bottom - imageRect.bottom : deltaY;
393-
_addTransitionInternal(CropImageData(x: deltaX, y: deltaY));
394-
return;
445+
double _calculateScaleAfterRotate(
446+
Rect pathRect, double startScale, Rect initialImageRect, double minEdgeHalf) {
447+
final imageCenter = initialImageRect.center.translate(data.x, data.y);
448+
final topLeftDistance = (pathRect.topLeft - imageCenter).distance;
449+
final topRightDistance = (pathRect.topRight - imageCenter).distance;
450+
final bottomLeftDistance = (pathRect.bottomLeft - imageCenter).distance;
451+
final bottomRightDistance = (pathRect.bottomRight - imageCenter).distance;
452+
final maxDistance =
453+
max(max(max(topLeftDistance, topRightDistance), bottomLeftDistance), bottomRightDistance);
454+
double endScale = maxDistance / minEdgeHalf;
455+
456+
if (startScale >= endScale) {
457+
return endScale;
395458
}
396-
if (transition.scale != 1.0) {
397-
_addTransitionInternal(CropImageData(scale: startData.scale / data.scale));
459+
///use binary search to find best scale which just contain path.
460+
///Also, we can use imageCenter、imageLine(longest one) and path vertex to calculate.
461+
double step = 1 / minEdgeHalf;
462+
463+
while ((endScale - startScale).abs() > step) {
464+
double midScale = (endScale + startScale) / 2;
465+
466+
if (_isContainPath(initialImageRect, pathRect, midScale)) {
467+
endScale = midScale;
468+
} else {
469+
startScale = midScale + step;
470+
}
398471
}
472+
return endScale;
399473
}
400474

401475
Path _getPath({

0 commit comments

Comments
 (0)