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 lastScale: CGFloat = 1.0
|
||||||
@State private var offset: CGSize = .zero
|
@State private var offset: CGSize = .zero
|
||||||
@State private var lastOffset: 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
|
@State private var containerSize: CGSize = .zero
|
||||||
|
|
||||||
// Horizontal padding for the crop area
|
// Horizontal padding for the crop area
|
||||||
@ -84,10 +87,11 @@ struct PhotoCropperSheet: View {
|
|||||||
Image(uiImage: uiImage)
|
Image(uiImage: uiImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
|
.rotationEffect(rotation + fixedRotation)
|
||||||
.scaleEffect(scale)
|
.scaleEffect(scale)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.gesture(dragGesture)
|
.gesture(dragGesture)
|
||||||
.gesture(magnificationGesture)
|
.gesture(combinedPinchRotateGesture)
|
||||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||||
.clipped()
|
.clipped()
|
||||||
}
|
}
|
||||||
@ -119,11 +123,30 @@ struct PhotoCropperSheet: View {
|
|||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
}
|
}
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
Button {
|
HStack(spacing: Design.Spacing.xLarge) {
|
||||||
resetTransform()
|
// Rotate 90° left
|
||||||
} label: {
|
Button {
|
||||||
Image(systemName: "arrow.counterclockwise")
|
rotate90Left()
|
||||||
.foregroundStyle(.white)
|
} 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) {
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
@ -153,16 +176,28 @@ struct PhotoCropperSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var magnificationGesture: some Gesture {
|
private var combinedPinchRotateGesture: some Gesture {
|
||||||
MagnificationGesture()
|
SimultaneousGesture(
|
||||||
.onChanged { value in
|
MagnificationGesture(),
|
||||||
let newScale = lastScale * value
|
RotationGesture()
|
||||||
|
)
|
||||||
|
.onChanged { value in
|
||||||
|
// Handle pinch (magnification)
|
||||||
|
if let magnification = value.first {
|
||||||
|
let newScale = lastScale * magnification
|
||||||
// Limit scale between 0.5x and 5x
|
// Limit scale between 0.5x and 5x
|
||||||
scale = min(max(newScale, 0.5), 5.0)
|
scale = min(max(newScale, 0.5), 5.0)
|
||||||
}
|
}
|
||||||
.onEnded { _ in
|
|
||||||
lastScale = scale
|
// Handle rotation
|
||||||
|
if let rotationValue = value.second {
|
||||||
|
rotation = lastRotation + rotationValue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
lastScale = scale
|
||||||
|
lastRotation = rotation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
@ -173,6 +208,21 @@ struct PhotoCropperSheet: View {
|
|||||||
lastScale = 1.0
|
lastScale = 1.0
|
||||||
offset = .zero
|
offset = .zero
|
||||||
lastOffset = .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
|
// 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
|
let imageSize = normalizedImage.size
|
||||||
|
|
||||||
// Guard against zero container size
|
// Guard against zero container size
|
||||||
@ -321,6 +375,44 @@ struct PhotoCropperSheet: View {
|
|||||||
image.draw(at: .zero)
|
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
|
// MARK: - Crop Overlay
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user