I’m engaged on a SwiftUI mission the place I apply a perspective warp to a picture utilizing Core Picture. I enable customers to maneuver the management factors (corners of the picture) to regulate the transformation.
Challenge:
After I stretch or transfer the management factors, they will go exterior the picture boundary, which ends up in undesirable distortions. I need to prohibit the management factors in order that they all the time stay on the picture’s border.
Code:
import SwiftUI
import CoreImage
import CoreImage.CIFilterBuiltins
struct PerspectiveWarpImageView: View {
// Management factors for the 4 corners of the picture’s body.
@State personal var factors: [CGPoint] = []
// Place used to maneuver the warped picture round
@State var place: CGPoint = CGPoint(x: 200, y: 300)
@State var position2: CGPoint = CGPoint(x: 200, y: 300)
@State personal var initialPosition: CGPoint = .zero
// This offset shops the picture’s prime‐left origin (computed onAppear).
@State personal var offset: CGPoint = .zero
// Change this to your asset’s identify
let imageName = "imgMusic"
var physique: some View {
GeometryReader { geometry in
ZStack {
Coloration.grey
.edgesIgnoringSafeArea(.all)
// When the management factors are set, show the warped picture.
if !factors.isEmpty, let transformedImage = transformImage() {
Picture(uiImage: transformedImage)
.resizable()
.scaledToFit()
// .body(width: 300, peak: 200)
// The picture view is positioned utilizing the identical state as earlier than.
.place(place)
// Enable dragging your entire picture (and its management factors)
// Overlay the management factors so you may alter the corners.
PointsView(factors: $factors)
.place(position2)
.onAppear {
// If the management factors haven’t been set but, initialize them.
if factors.isEmpty {
let screenWidth = geometry.dimension.width
let screenHeight = geometry.dimension.peak
let offsetX = (screenWidth - 400) / 2 // heart horizontally
let offsetY = (screenHeight - 300) / 2 // heart vertically
offset = CGPoint(x: offsetX, y: offsetY)
factors = [
CGPoint(x: offsetX + 0, y: offsetY + 0), // Top-left
CGPoint(x: offsetX + 400, y: offsetY + 0), // Top-right
CGPoint(x: offsetX + 400, y: offsetY + 300), // Bottom-right
CGPoint(x: offsetX + 0, y: offsetY + 300) // Bottom-left
]
}
}
.gesture(
DragGesture()
.onChanged { worth in
if initialPosition == .zero {
initialPosition = place
}
let newPosition = CGPoint(
x: initialPosition.x + worth.translation.width,
y: initialPosition.y + worth.translation.peak
)
DispatchQueue.major.async {
place = newPosition
position2 = newPosition
}
}
.onEnded { _ in
initialPosition = .zero
}
)
}
}
.onAppear {
// In case the PointsView onAppear didn't run, initialize right here.
if factors.isEmpty {
let screenWidth = geometry.dimension.width
let screenHeight = geometry.dimension.peak
let offsetX = (screenWidth - 400) / 2
let offsetY = (screenHeight - 300) / 2
offset = CGPoint(x: offsetX, y: offsetY)
factors = [
CGPoint(x: offsetX + 0, y: offsetY + 0), // Top-left
CGPoint(x: offsetX + 400, y: offsetY + 0), // Top-right
CGPoint(x: offsetX + 400, y: offsetY + 300), // Bottom-right
CGPoint(x: offsetX + 0, y: offsetY + 300) // Bottom-left
]
}
}
}
}
/// Applies a perspective warp to the picture utilizing Core Picture.
func transformImage() -> UIImage? {
// Load the picture out of your property.
guard let uiImage = UIImage(named: imageName) else { return nil }
guard let ciImage = CIImage(picture: uiImage) else { return nil }
// We assume the picture is proven in a 300×200 body.
let displaySize = CGSize(width: uiImage.dimension.width, peak: uiImage.dimension.peak)
// Arrange the attitude remodel filter.
let filter = CIFilter(identify: "CIPerspectiveTransform")!
filter.setValue(ciImage, forKey: kCIInputImageKey)
// Convert every of the 4 management factors into the picture’s coordinate system.
// (Keep in mind: SwiftUI’s coordinate area origin is on the top-left, whereas Core Picture’s
// coordinate area origin is on the bottom-left.)
guard factors.rely == 4 else { return nil }
let topLeft = convertToImageCoordinates(factors[0], displaySize: displaySize, offset: offset)
let topRight = convertToImageCoordinates(factors[1], displaySize: displaySize, offset: offset)
let bottomRight = convertToImageCoordinates(factors[2], displaySize: displaySize, offset: offset)
let bottomLeft = convertToImageCoordinates(factors[3], displaySize: displaySize, offset: offset)
let clampFilter = CIFilter.affineClamp()
clampFilter.inputImage = ciImage
clampFilter.remodel = .id
guard let clampedImage = clampFilter.outputImage else { return nil }
guard factors.rely == 4 else { return nil }
filter.setValue(CIVector(cgPoint: topLeft), forKey: "inputTopLeft")
filter.setValue(CIVector(cgPoint: topRight), forKey: "inputTopRight")
filter.setValue(CIVector(cgPoint: bottomRight), forKey: "inputBottomRight")
filter.setValue(CIVector(cgPoint: bottomLeft), forKey: "inputBottomLeft")
// Render the output picture.
guard let outputImage = filter.outputImage else { return nil }
// let context = CIContext(choices: nil)
//
// if let cgimg = context.createCGImage(outputImage, from: CGRect(origin: .zero, dimension: displaySize)) {
// return UIImage(cgImage: cgimg)
// }
return outputImage.toUIImage()
}
func scaleImage(_ picture: UIImage, to newSize: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(dimension: newSize)
return renderer.picture { _ in
picture.draw(in: CGRect(origin: .zero, dimension: newSize))
}
}
/// Converts a degree from SwiftUI’s coordinate area (world) into the picture’s coordinate area.
func convertToImageCoordinates(_ level: CGPoint, displaySize: CGSize, offset: CGPoint) -> CGPoint {
// The picture’s body begins on the given offset.
let relativeX = level.x - offset.x
let relativeY = level.y - offset.y
// Flip the y coordinate in order that the origin is on the backside.
let flippedY = displaySize.peak - relativeY
return CGPoint(x: relativeX, y: flippedY)
}
}
extension CIImage {
func toUIImage() -> UIImage? {
let context = CIContext(choices: nil)
if let cgImage = context.createCGImage(self, from: self.extent) {
return UIImage(cgImage: cgImage, scale: 1.0, orientation: .up)
}
return nil
}
}
struct PerspectiveWarpImageView_Previews: PreviewProvider {
static var previews: some View {
PerspectiveWarpImageView()
}
}
Anticipated Habits:
- The management factors ought to by no means go away the picture bounds.
- Customers ought to be capable of drag them solely alongside the picture edges.
Picture