Skip to content

Commit

Permalink
Merge pull request #49 from bma73/crop-and-mask-path
Browse files Browse the repository at this point in the history
Add maskShape parameter
  • Loading branch information
ikbendewilliam authored Dec 30, 2024
2 parents 92dc1fa + b7286c5 commit bc8905d
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 70 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ The color behind the image. This color will also be used when there are gaps/emp

The shape of the cropping path.

### maskShape

The shape of the UI masking.

### cropPercentage

How big the crop should be in regards to the width and height available to the cropping widget.
Expand Down
125 changes: 55 additions & 70 deletions lib/src/widgets/custom_image_crop_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ class CustomImageCrop extends StatefulWidget {
/// - [CustomCropShape.Ratio] Crop area will have a specified aspect ratio.
final CustomCropShape shape;

/// The shape of the mask area.
/// Possible values:
/// - [CustomCropShape.Circle] Mask area will be circular.
/// - [CustomCropShape.Square] Mask area will be a square.
/// - [CustomCropShape.Ratio] Mask area will have a specified aspect ratio.
final CustomCropShape? maskShape;

/// Ratio of the cropping area.
/// If [shape] is set to [CustomCropShape.Ratio], this property is required.
/// For example, to create a square crop area, use [Ratio(width: 1, height: 1)].
Expand Down Expand Up @@ -119,6 +126,7 @@ class CustomImageCrop extends StatefulWidget {
this.overlayColor = const Color.fromRGBO(0, 0, 0, 0.5),
this.backgroundColor = Colors.white,
this.shape = CustomCropShape.Circle,
this.maskShape,
this.imageFit = CustomImageFit.fitCropSpace,
this.cropPercentage = 0.8,
this.drawPath = DottedCropPathPainter.drawPath,
Expand All @@ -133,8 +141,7 @@ class CustomImageCrop extends StatefulWidget {
Paint? imagePaintDuringCrop,
this.forceInsideCropArea = false,
Key? key,
}) : this.imagePaintDuringCrop = imagePaintDuringCrop ??
(Paint()..filterQuality = FilterQuality.high),
}) : this.imagePaintDuringCrop = imagePaintDuringCrop ?? (Paint()..filterQuality = FilterQuality.high),
assert(
!(shape == CustomCropShape.Ratio && ratio == null),
"If shape is set to Ratio, ratio should not be null.",
Expand All @@ -145,10 +152,10 @@ class CustomImageCrop extends StatefulWidget {
_CustomImageCropState createState() => _CustomImageCropState();
}

class _CustomImageCropState extends State<CustomImageCrop>
with CustomImageCropListener {
class _CustomImageCropState extends State<CustomImageCrop> with CustomImageCropListener {
CropImageData? _dataTransitionStart;
late Path _path;
late Path _maskPath;
late double _width, _height;
ui.Image? _imageAsUIImage;
ImageStream? _imageStream;
Expand Down Expand Up @@ -227,7 +234,20 @@ class _CustomImageCropState extends State<CustomImageCrop>
width: _width,
height: _height,
borderRadius: widget.borderRadius,
shape: widget.shape,
);

_maskPath = widget.maskShape == null
? _path
: _getPath(
cropWidth: cropFitParams.cropSizeWidth,
cropHeight: cropFitParams.cropSizeHeight,
width: _width,
height: _height,
borderRadius: widget.borderRadius,
shape: widget.maskShape!,
);

return XGestureDetector(
onMoveStart: onMoveStart,
onMoveUpdate: onMoveUpdate,
Expand All @@ -243,8 +263,7 @@ class _CustomImageCropState extends State<CustomImageCrop>
left: data.x + _width / 2,
top: data.y + _height / 2,
child: Transform(
transform: Matrix4.diagonal3(
vector_math.Vector3(scale, scale, scale))
transform: Matrix4.diagonal3(vector_math.Vector3(scale, scale, scale))
..rotateZ(data.angle)
..translate(-image.width / 2, -image.height / 2),
child: Image(
Expand All @@ -254,13 +273,13 @@ class _CustomImageCropState extends State<CustomImageCrop>
),
IgnorePointer(
child: ClipPath(
clipper: InvertedClipper(_path, _width, _height),
clipper: InvertedClipper(_maskPath, _width, _height),
child: Container(
color: widget.overlayColor,
),
),
),
widget.drawPath(_path, pathPaint: widget.pathPaint),
widget.drawPath(_maskPath, pathPaint: widget.pathPaint),
],
),
),
Expand All @@ -274,8 +293,7 @@ class _CustomImageCropState extends State<CustomImageCrop>
}

void onScaleUpdate(ScaleEvent event) {
final scale =
widget.canScale ? event.scale : (_dataTransitionStart?.scale ?? 1.0);
final scale = widget.canScale ? event.scale : (_dataTransitionStart?.scale ?? 1.0);

final angle = widget.canRotate ? event.rotationAngle : 0.0;

Expand All @@ -301,8 +319,7 @@ class _CustomImageCropState extends State<CustomImageCrop>
void onMoveUpdate(MoveEvent event) {
if (!widget.canMove) return;

widget.cropController
.addTransition(CropImageData(x: event.delta.dx, y: event.delta.dy));
widget.cropController.addTransition(CropImageData(x: event.delta.dx, y: event.delta.dy));
}

Rect _getInitialImageRect() {
Expand All @@ -318,8 +335,7 @@ class _CustomImageCropState extends State<CustomImageCrop>
aspectRatio: (widget.ratio?.width ?? 1) / (widget.ratio?.height ?? 1),
);
final initialWidth = _imageAsUIImage!.width * cropFitParams.additionalScale;
final initialHeight =
_imageAsUIImage!.height * cropFitParams.additionalScale;
final initialHeight = _imageAsUIImage!.height * cropFitParams.additionalScale;
return Rect.fromLTWH(
(_width - initialWidth) / 2,
(_height - initialHeight) / 2,
Expand Down Expand Up @@ -347,53 +363,38 @@ class _CustomImageCropState extends State<CustomImageCrop>

if (transition.x != 0 || transition.y != 0) {
if (isRotated) {
_addTransitionInternal(
CropImageData(x: startX - data.x, y: startY - data.y));
_addTransitionInternal(CropImageData(x: startX - data.x, y: startY - data.y));
} else {
final imageRect = _getImageRect(initialImageRect, data.scale);
double deltaX = min(pathRect.left - imageRect.left, 0);
deltaX = pathRect.right > imageRect.right
? pathRect.right - imageRect.right
: deltaX;
deltaX = pathRect.right > imageRect.right ? pathRect.right - imageRect.right : deltaX;
double deltaY = min(pathRect.top - imageRect.top, 0);
deltaY = pathRect.bottom > imageRect.bottom
? pathRect.bottom - imageRect.bottom
: deltaY;
deltaY = pathRect.bottom > imageRect.bottom ? pathRect.bottom - imageRect.bottom : deltaY;
_addTransitionInternal(CropImageData(x: deltaX, y: deltaY));
}
return;
}
double minEdgeHalf =
min(initialImageRect.width, initialImageRect.height) / 2;
double adaptScale = _calculateScaleAfterRotate(
pathRect, data.scale, initialImageRect, minEdgeHalf);
double minEdgeHalf = min(initialImageRect.width, initialImageRect.height) / 2;
double adaptScale = _calculateScaleAfterRotate(pathRect, data.scale, initialImageRect, minEdgeHalf);
_addTransitionInternal(CropImageData(scale: adaptScale / data.scale));
}

Rect _getImageRect(Rect initialImageRect, double currentScale) {
final diffScale = (1 - currentScale) / 2;
final left =
initialImageRect.left + diffScale * initialImageRect.width + data.x;
final top =
initialImageRect.top + diffScale * initialImageRect.height + data.y;
Rect imageRect = Rect.fromLTWH(
left,
top,
currentScale * initialImageRect.width,
currentScale * initialImageRect.height);
final left = initialImageRect.left + diffScale * initialImageRect.width + data.x;
final top = initialImageRect.top + diffScale * initialImageRect.height + data.y;
Rect imageRect = Rect.fromLTWH(left, top, currentScale * initialImageRect.width, currentScale * initialImageRect.height);
return imageRect;
}

double _getDistanceBetweenPointAndLine(
Offset point, Offset lineStart, Offset lineEnd) {
double _getDistanceBetweenPointAndLine(Offset point, Offset lineStart, Offset lineEnd) {
if (lineEnd.dy == lineStart.dy) {
return (point.dy - lineStart.dy).abs();
}
if (lineEnd.dx == lineStart.dx) {
return (point.dx - lineStart.dx).abs();
}
double line1Slop =
(lineEnd.dy - lineStart.dy) / (lineEnd.dx - lineStart.dx);
double line1Slop = (lineEnd.dy - lineStart.dy) / (lineEnd.dx - lineStart.dx);
double line1Delta = lineEnd.dy - lineEnd.dx * line1Slop;
double line2Slop = -1 / line1Slop;
double line2Delta = point.dy - point.dx * line2Slop;
Expand All @@ -402,13 +403,11 @@ class _CustomImageCropState extends State<CustomImageCrop>
return (Offset(crossPointX, crossPointY) - point).distance;
}

bool _isContainPath(
Rect initialImageRect, Rect pathRect, double currentScale) {
bool _isContainPath(Rect initialImageRect, Rect pathRect, double currentScale) {
final imageRect = _getImageRect(initialImageRect, currentScale);
Offset topLeft, topRight, bottomLeft, bottomRight;
final rad = atan(imageRect.height / imageRect.width);
final len =
sqrt(pow(imageRect.width / 2, 2) + pow(imageRect.height / 2, 2));
final len = sqrt(pow(imageRect.width / 2, 2) + pow(imageRect.height / 2, 2));
bool isRotated = data.angle != 0;

if (isRotated) {
Expand All @@ -419,11 +418,9 @@ class _CustomImageCropState extends State<CustomImageCrop>
final cosCounterClockValue = len * cos(counterClockAngle);
final sinCounterClockValue = len * sin(counterClockAngle);
bottomRight = imageRect.center.translate(cosClockValue, sinClockValue);
topRight = imageRect.center
.translate(cosCounterClockValue, -sinCounterClockValue);
topRight = imageRect.center.translate(cosCounterClockValue, -sinCounterClockValue);
topLeft = imageRect.center.translate(-cosClockValue, -sinClockValue);
bottomLeft = imageRect.center
.translate(-cosCounterClockValue, sinCounterClockValue);
bottomLeft = imageRect.center.translate(-cosCounterClockValue, sinCounterClockValue);
} else {
bottomRight = imageRect.bottomRight;
topRight = imageRect.topRight;
Expand All @@ -434,15 +431,10 @@ class _CustomImageCropState extends State<CustomImageCrop>
if (widget.shape == CustomCropShape.Circle) {
final anchor = max(pathRect.width, pathRect.height) / 2;
final pathCenter = pathRect.center;
return _getDistanceBetweenPointAndLine(pathCenter, topLeft, topRight) >=
anchor &&
_getDistanceBetweenPointAndLine(pathCenter, topRight, bottomRight) >=
anchor &&
_getDistanceBetweenPointAndLine(
pathCenter, bottomLeft, bottomRight) >=
anchor &&
_getDistanceBetweenPointAndLine(pathCenter, topLeft, bottomLeft) >=
anchor;
return _getDistanceBetweenPointAndLine(pathCenter, topLeft, topRight) >= anchor &&
_getDistanceBetweenPointAndLine(pathCenter, topRight, bottomRight) >= anchor &&
_getDistanceBetweenPointAndLine(pathCenter, bottomLeft, bottomRight) >= anchor &&
_getDistanceBetweenPointAndLine(pathCenter, topLeft, bottomLeft) >= anchor;
}

if (isRotated) {
Expand All @@ -452,28 +444,19 @@ class _CustomImageCropState extends State<CustomImageCrop>
..lineTo(bottomRight.dx, bottomRight.dy)
..lineTo(bottomLeft.dx, bottomLeft.dy)
..close();
return imagePath.contains(pathRect.topLeft) &&
imagePath.contains(pathRect.topRight) &&
imagePath.contains(pathRect.bottomLeft) &&
imagePath.contains(pathRect.bottomRight);
return imagePath.contains(pathRect.topLeft) && imagePath.contains(pathRect.topRight) && imagePath.contains(pathRect.bottomLeft) && imagePath.contains(pathRect.bottomRight);
} else {
return imageRect.contains(pathRect.topLeft) &&
imageRect.contains(pathRect.topRight) &&
imageRect.contains(pathRect.bottomLeft) &&
imageRect.contains(pathRect.bottomRight);
return imageRect.contains(pathRect.topLeft) && imageRect.contains(pathRect.topRight) && imageRect.contains(pathRect.bottomLeft) && imageRect.contains(pathRect.bottomRight);
}
}

double _calculateScaleAfterRotate(Rect pathRect, double startScale,
Rect initialImageRect, double minEdgeHalf) {
double _calculateScaleAfterRotate(Rect pathRect, double startScale, Rect initialImageRect, double minEdgeHalf) {
final imageCenter = initialImageRect.center.translate(data.x, data.y);
final topLeftDistance = (pathRect.topLeft - imageCenter).distance;
final topRightDistance = (pathRect.topRight - imageCenter).distance;
final bottomLeftDistance = (pathRect.bottomLeft - imageCenter).distance;
final bottomRightDistance = (pathRect.bottomRight - imageCenter).distance;
final maxDistance = max(
max(max(topLeftDistance, topRightDistance), bottomLeftDistance),
bottomRightDistance);
final maxDistance = max(max(max(topLeftDistance, topRightDistance), bottomLeftDistance), bottomRightDistance);
double endScale = maxDistance / minEdgeHalf;

if (startScale >= endScale) {
Expand Down Expand Up @@ -502,6 +485,7 @@ class _CustomImageCropState extends State<CustomImageCrop>
required double width,
required double height,
required double borderRadius,
required CustomCropShape shape,
bool clipShape = true,
}) {
if (!clipShape) {
Expand All @@ -515,7 +499,7 @@ class _CustomImageCropState extends State<CustomImageCrop>
);
}

switch (widget.shape) {
switch (shape) {
case CustomCropShape.Circle:
return Path()
..addOval(
Expand Down Expand Up @@ -577,6 +561,7 @@ class _CustomImageCropState extends State<CustomImageCrop>
height: onCropParams.cropSizeHeight,
borderRadius: widget.borderRadius,
clipShape: widget.clipShapeOnCrop,
shape: widget.shape,
));
final matrix4Image = Matrix4.diagonal3(vector_math.Vector3.all(1))
..translate(
Expand Down

0 comments on commit bc8905d

Please sign in to comment.