Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
1258f5dcd3
commit
05aedadfc3
@ -46,6 +46,9 @@ struct PhotoCropperSheet: View {
|
||||
@State private var lastScale: CGFloat = 1.0
|
||||
@State private var offset: CGSize = .zero
|
||||
@State private var lastOffset: CGSize = .zero
|
||||
@State private var rotation: Angle = .zero
|
||||
@State private var lastRotation: Angle = .zero
|
||||
@State private var fixedRotation: Angle = .zero // For 90° toolbar rotations
|
||||
@State private var containerSize: CGSize = .zero
|
||||
|
||||
// Horizontal padding for the crop area
|
||||
@ -84,10 +87,11 @@ struct PhotoCropperSheet: View {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.rotationEffect(rotation + fixedRotation)
|
||||
.scaleEffect(scale)
|
||||
.offset(offset)
|
||||
.gesture(dragGesture)
|
||||
.gesture(magnificationGesture)
|
||||
.gesture(combinedPinchRotateGesture)
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
.clipped()
|
||||
}
|
||||
@ -119,12 +123,31 @@ struct PhotoCropperSheet: View {
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
HStack(spacing: Design.Spacing.xLarge) {
|
||||
// Rotate 90° left
|
||||
Button {
|
||||
rotate90Left()
|
||||
} label: {
|
||||
Image(systemName: "rotate.left")
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
|
||||
// Reset all transforms
|
||||
Button {
|
||||
resetTransform()
|
||||
} label: {
|
||||
Image(systemName: "arrow.counterclockwise")
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
|
||||
// Rotate 90° right
|
||||
Button {
|
||||
rotate90Right()
|
||||
} label: {
|
||||
Image(systemName: "rotate.right")
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(String.localized("Done")) {
|
||||
@ -153,15 +176,27 @@ struct PhotoCropperSheet: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var magnificationGesture: some Gesture {
|
||||
MagnificationGesture()
|
||||
private var combinedPinchRotateGesture: some Gesture {
|
||||
SimultaneousGesture(
|
||||
MagnificationGesture(),
|
||||
RotationGesture()
|
||||
)
|
||||
.onChanged { value in
|
||||
let newScale = lastScale * value
|
||||
// Handle pinch (magnification)
|
||||
if let magnification = value.first {
|
||||
let newScale = lastScale * magnification
|
||||
// Limit scale between 0.5x and 5x
|
||||
scale = min(max(newScale, 0.5), 5.0)
|
||||
}
|
||||
|
||||
// Handle rotation
|
||||
if let rotationValue = value.second {
|
||||
rotation = lastRotation + rotationValue
|
||||
}
|
||||
}
|
||||
.onEnded { _ in
|
||||
lastScale = scale
|
||||
lastRotation = rotation
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,6 +208,21 @@ struct PhotoCropperSheet: View {
|
||||
lastScale = 1.0
|
||||
offset = .zero
|
||||
lastOffset = .zero
|
||||
rotation = .zero
|
||||
lastRotation = .zero
|
||||
fixedRotation = .zero
|
||||
}
|
||||
}
|
||||
|
||||
private func rotate90Left() {
|
||||
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
||||
fixedRotation -= .degrees(90)
|
||||
}
|
||||
}
|
||||
|
||||
private func rotate90Right() {
|
||||
withAnimation(.spring(duration: Design.Animation.springDuration)) {
|
||||
fixedRotation += .degrees(90)
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,7 +236,11 @@ struct PhotoCropperSheet: View {
|
||||
}
|
||||
|
||||
// First, normalize the image orientation so we're working with correctly oriented pixels
|
||||
let normalizedImage = normalizeImageOrientation(uiImage)
|
||||
let orientedImage = normalizeImageOrientation(uiImage)
|
||||
|
||||
// Apply total rotation (gesture rotation + fixed rotation)
|
||||
let totalRotation = rotation + fixedRotation
|
||||
let normalizedImage = rotateImage(orientedImage, by: totalRotation)
|
||||
let imageSize = normalizedImage.size
|
||||
|
||||
// Guard against zero container size
|
||||
@ -321,6 +375,44 @@ struct PhotoCropperSheet: View {
|
||||
image.draw(at: .zero)
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotates an image by the specified angle
|
||||
private func rotateImage(_ image: UIImage, by angle: Angle) -> UIImage {
|
||||
// If no rotation needed, return original
|
||||
guard abs(angle.radians) > 0.001 else { return image }
|
||||
|
||||
let radians = CGFloat(angle.radians)
|
||||
|
||||
// Calculate the size of the rotated image
|
||||
let rotatedRect = CGRect(origin: .zero, size: image.size)
|
||||
.applying(CGAffineTransform(rotationAngle: radians))
|
||||
let rotatedSize = CGSize(
|
||||
width: abs(rotatedRect.width),
|
||||
height: abs(rotatedRect.height)
|
||||
)
|
||||
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = 1 // Use 1:1 scale for output
|
||||
|
||||
let renderer = UIGraphicsImageRenderer(size: rotatedSize, format: format)
|
||||
return renderer.image { context in
|
||||
let cgContext = context.cgContext
|
||||
|
||||
// Move to center
|
||||
cgContext.translateBy(x: rotatedSize.width / 2, y: rotatedSize.height / 2)
|
||||
|
||||
// Rotate
|
||||
cgContext.rotate(by: radians)
|
||||
|
||||
// Draw image centered
|
||||
image.draw(in: CGRect(
|
||||
x: -image.size.width / 2,
|
||||
y: -image.size.height / 2,
|
||||
width: image.size.width,
|
||||
height: image.size.height
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Crop Overlay
|
||||
|
||||
Loading…
Reference in New Issue
Block a user