ios – Struggling to Apply Perspective Warp to Textual content Path in SwiftUI, Textual content Simply Strikes As an alternative of Stretching or Squeezing

ios – Struggling to Apply Perspective Warp to Textual content Path in SwiftUI, Textual content Simply Strikes As an alternative of Stretching or Squeezing


I’m making an attempt to implement a perspective warp impact on a textual content in SwiftUI. Nonetheless, when I attempt to remodel the textual content path, it solely strikes the textual content slightly than stretching or squeezing it, as anticipated in perspective transformation. My purpose is to distort the textual content alongside a set of factors (top-left, top-right, bottom-right, and bottom-left) to create a perspective impact just like a photograph editor.

Here is the code I’m utilizing to realize the impact:

import SwiftUI

struct PerspectiveWarpView: View {
    @State non-public var factors: [CGPoint] = [] // Initially empty
    @State var shade: Colour = .white
    @State var place: CGPoint = CGPoint(x: 100, y: 300)
    @State var position2: CGPoint = CGPoint(x: 100, y: 250)
    @State non-public var initialPosition: CGPoint = .zero
    
    var physique: some View {
        GeometryReader { geometry in
            ZStack {
                Colour.black.edgesIgnoringSafeArea(.all)

                if !factors.isEmpty, let warpedPath = transformTextPath() {
                    warpedPath
                        .fill(shade)
                        .place(place)
                        .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.fundamental.async {
                                place = newPosition
                                position2 = CGPoint(x: newPosition.x, y: newPosition.y - 50)
                            }
                        }.onEnded({ _ in
                            initialPosition = .zero
                        })
                        )

                    PointsView(factors: $factors, path: warpedPath)
                        .place(position2)
                        .onAppear(){
                            factors =  getCorners(of: warpedPath)
                        }
                }
            }
            .onAppear {
                // Initialize factors based mostly on the display screen measurement
                let screenWidth = geometry.measurement.width
                let screenHeight = geometry.measurement.peak
                let offsetX = (screenWidth - 300) / 2 // Middle horizontally
                let offsetY = (screenHeight - 200) / 2 // Middle vertically

                factors = [
                    CGPoint(x: offsetX + 0, y: offsetY + 0),       // Top-left
                    CGPoint(x: offsetX + 300, y: offsetY + 0),     // Top-right
                    CGPoint(x: offsetX + 300, y: offsetY + 200),   // Bottom-right
                    CGPoint(x: offsetX + 0, y: offsetY + 200)      // Bottom-left
                ]
            }
        }
    }
    
    

    func getCorners(of path: Path) -> [CGPoint] {
        let boundingBox = path.boundingRect
        return [
            CGPoint(x: boundingBox.minX, y: boundingBox.minY - 10), // Top-left
            CGPoint(x: boundingBox.maxX, y: boundingBox.minY - 10), // Top-right
            CGPoint(x: boundingBox.maxX, y: boundingBox.maxY + 10), // Bottom-right
            CGPoint(x: boundingBox.minX, y: boundingBox.maxY + 10)  // Bottom-left
        ]
    }

    func transformTextPath() -> Path? {
        guard !factors.isEmpty else { return nil } // Guarantee factors are usually not empty
        guard let originalPath = textToPath(textual content: "ELEVATED", font: .systemFont(ofSize: 80, weight: .daring)) else {
            return nil
        }

        // Apply perspective remodel to the trail
        return warpPath(originalPath, from: defaultRect(), to: factors)
    }

    func textToPath(textual content: String, font: UIFont) -> Path? {
        let attributedString = NSAttributedString(string: textual content, attributes: [.font: font])
        let line = CTLineCreateWithAttributedString(attributedString)
        let runArray = CTLineGetGlyphRuns(line) as NSArray

        let path = CGMutablePath()
        for run in runArray {
            let run = run as! CTRun
            let rely = CTRunGetGlyphCount(run)

            for index in 0..<rely {
                let vary = CFRangeMake(index, 1)
                var glyph: CGGlyph = 0
                var place: CGPoint = .zero
                CTRunGetGlyphs(run, vary, &glyph)
                CTRunGetPositions(run, vary, &place)

                if let glyphPath = CTFontCreatePathForGlyph(font, glyph, nil) {
                    var remodel = CGAffineTransform(translationX: place.x, y: place.y)
                    remodel = remodel.scaledBy(x: 1, y: -1)
                    path.addPath(glyphPath, remodel: remodel)
                }
            }
        }
        return Path(path)
    }

    func defaultRect() -> [CGPoint] {
        return [
            CGPoint(x: 0, y: 0),       // Top-left
            CGPoint(x: 300, y: 0),     // Top-right
            CGPoint(x: 300, y: 200),   // Bottom-right
            CGPoint(x: 0, y: 200)      // Bottom-left
        ]
    }

    func warpPath(_ path: Path, from src: [CGPoint], to dst: [CGPoint]) -> Path {
        var newPath = Path()
        let remodel = computePerspectiveTransform(from: src, to: dst)

        path.forEach { ingredient in
            change ingredient {
            case .transfer(to: let level):
                newPath.transfer(to: applyPerspective(level, utilizing: remodel))
            case .line(to: let level):
                newPath.addLine(to: applyPerspective(level, utilizing: remodel))
            case .quadCurve(to: let level, management: let management):
                newPath.addQuadCurve(to: applyPerspective(level, utilizing: remodel),
                                     management: applyPerspective(management, utilizing: remodel))
            case .curve(to: let level, control1: let control1, control2: let control2):
                newPath.addCurve(to: applyPerspective(level, utilizing: remodel),
                                 control1: applyPerspective(control1, utilizing: remodel),
                                 control2: applyPerspective(control2, utilizing: remodel))
            case .closeSubpath:
                newPath.closeSubpath()
            }
        }
        return newPath
    }

    func computePerspectiveTransform(from src: [CGPoint], to dst: [CGPoint]) -> [[CGFloat]] {
        let x0 = src[0].x, y0 = src[0].y
        let x1 = src[1].x, y1 = src[1].y
        let x2 = src[2].x, y2 = src[2].y
        let x3 = src[3].x, y3 = src[3].y

        let X0 = dst[0].x, Y0 = dst[0].y
        let X1 = dst[1].x, Y1 = dst[1].y
        let X2 = dst[2].x, Y2 = dst[2].y
        let X3 = dst[3].x, Y3 = dst[3].y

        
        let A = [
            [x0, y0, 1, 0, 0, 0, -X0*x0, -X0*y0],
            [x1, y1, 1, 0, 0, 0, -X1*x1, -X1*y1],
            [x2, y2, 1, 0, 0, 0, -X2*x2, -X2*y2],
            [x3, y3, 1, 0, 0, 0, -X3*x3, -X3*y3],
            [0, 0, 0, x0, y0, 1, -Y0*x0, -Y0*y0],
            [0, 0, 0, x1, y1, 1, -Y1*x1, -Y1*y1],
            [0, 0, 0, x2, y2, 1, -Y2*x2, -Y2*y2],
            [0, 0, 0, x3, y3, 1, -Y3*x3, -Y3*y3]
        ]
        
        let B = [X0, X1, X2, X3, Y0, Y1, Y2, Y3]

        guard let h = solveLinearSystem(A: A, B: B) else {
            return [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
        }

        return [
            [h[0], h[1], h[2]],
            [h[3], h[4], h[5]],
            [h[6], h[7], 1]
        ]
    }

    func solveLinearSystem(A: [[CGFloat]], B: [CGFloat]) -> [CGFloat]? {
        let rowCount = A.rely
        var matrix = A
        var outcome = B

        for i in 0..<rowCount {
            var maxRow = i
            for j in (i+1)..<rowCount {
                if abs(matrix[j][i]) > abs(matrix[maxRow][i]) {
                    maxRow = j
                }
            }

            matrix.swapAt(i, maxRow)
            outcome.swapAt(i, maxRow)

            let pivot = matrix[i][i]
            if pivot == 0 { return nil }

            for j in i..<rowCount {
                matrix[i][j] /= pivot
            }
            outcome[i] /= pivot

            for j in (i+1)..<rowCount {
                let issue = matrix[j][i]
                for ok in i..<rowCount {
                    matrix[j][k] -= issue * matrix[i][k]
                }
                outcome[j] -= issue * outcome[i]
            }
        }

        for i in stride(from: rowCount-1, by way of: 0, by: -1) {
            for j in (i+1)..<rowCount {
                outcome[i] -= matrix[i][j] * outcome[j]
            }
        }
        
        return outcome
    }

    func applyPerspective(_ level: CGPoint, utilizing matrix: [[CGFloat]]) -> CGPoint {
        let x = level.x
        let y = level.y
        let denominator = (matrix[2][0] * x + matrix[2][1] * y + matrix[2][2])
        
        let newX = (matrix[0][0] * x + matrix[0][1] * y + matrix[0][2]) / denominator
        let newY = (matrix[1][0] * x + matrix[1][1] * y + matrix[1][2]) / denominator
        
        return CGPoint(x: newX, y: newY)
    }
}

struct PointsView: View {
    @Binding var factors: [CGPoint]
    var path: Path

    var physique: some View {
        ZStack {
            // Draw the reworked path
            Path { path in
                path.transfer(to: factors[0])
                path.addLine(to: factors[1])
                path.addLine(to: factors[2])
                path.addLine(to: factors[3])
                path.closeSubpath()
            }
            .stroke(Colour.white.opacity(0.5), lineWidth: 2)

            // Draw draggable factors
            ForEach(0..<factors.rely, id: .self) { index in
                Circle()
                    .fill(Colour.white)
                    .body(width: 12, peak: 12)
                    .place(factors[index])
                    .gesture(
                        DragGesture()
                            .onChanged { worth in
                                factors[index] = worth.location
                            }
                    )
            }
        }
    }
}

#Preview {
    PerspectiveWarpView()
}


Downside:
After I apply the transformTextPath() operate to the textual content, it merely strikes across the display screen as a substitute of stretching or squeezing based mostly on the attitude factors. I anticipated the textual content path to distort prefer it does in photograph editors when a perspective impact is utilized.

What I’ve tried:

Implementing a customized perspective remodel utilizing matrix manipulation.
Attempting alternative ways to use the warp impact utilizing Path and CGAffineTransform.
May somebody level out the place I is perhaps going mistaken, or provide recommendations on how I can obtain the proper perspective stretching/squeezing impact?

That is the way it’s appears to be like
Out Put Image

rooshohttps://www.roosho.com
I am Rakib Raihan RooSho, Jack of all IT Trades. You got it right. Good for nothing. I try a lot of things and fail more than that. That's how I learn. Whenever I succeed, I note that in my cookbook. Eventually, that became my blog. 

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here


Latest Articles