-0.4 C
New York
Saturday, February 22, 2025

ios – SwiftUI Perspective Warp – Preserve Management Factors Inside Picture Bounds


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

MyImage

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles