I’m making an attempt to implement a perspective warp impact on a textual content in SwiftUI. Nonetheless, when I attempt to rework the textual content path, it solely strikes the textual content somewhat than stretching or squeezing it, as anticipated in perspective transformation. My objective 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 much like a photograph editor.
Here is the code I’m utilizing to attain the impact:
import SwiftUI
struct PerspectiveWarpView: View {
@State personal var factors: [CGPoint] = [] // Initially empty
@State var shade: Shade = .white
@State var place: CGPoint = CGPoint(x: 100, y: 300)
@State var position2: CGPoint = CGPoint(x: 100, y: 250)
@State personal var initialPosition: CGPoint = .zero
var physique: some View {
GeometryReader { geometry in
ZStack {
Shade.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.top
)
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.top
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 will not be empty
guard let originalPath = textToPath(textual content: "ELEVATED", font: .systemFont(ofSize: 80, weight: .daring)) else {
return nil
}
// Apply perspective rework 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.. [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 rework = computePerspectiveTransform(from: src, to: dst)
path.forEach { component in
swap component {
case .transfer(to: let level):
newPath.transfer(to: applyPerspective(level, utilizing: rework))
case .line(to: let level):
newPath.addLine(to: applyPerspective(level, utilizing: rework))
case .quadCurve(to: let level, management: let management):
newPath.addQuadCurve(to: applyPerspective(level, utilizing: rework),
management: applyPerspective(management, utilizing: rework))
case .curve(to: let level, control1: let control1, control2: let control2):
newPath.addCurve(to: applyPerspective(level, utilizing: rework),
control1: applyPerspective(control1, utilizing: rework),
control2: applyPerspective(control2, utilizing: rework))
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 end result = B
for i in 0.. abs(matrix[maxRow][i]) {
maxRow = j
}
}
matrix.swapAt(i, maxRow)
end result.swapAt(i, maxRow)
let pivot = matrix[i][i]
if pivot == 0 { return nil }
for j in i.. 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 remodeled 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(Shade.white.opacity(0.5), lineWidth: 2)
// Draw draggable factors
ForEach(0..
Drawback:
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 rework utilizing matrix manipulation.
Attempting alternative ways to use the warp impact utilizing Path and CGAffineTransform.
Might somebody level out the place I could be going improper, or supply options on how I can obtain the proper perspective stretching/squeezing impact?