SwiftUI拖拽式透视变换右下角控制点失效问题求助
SwiftUI 四点透视变换:右下角点无效问题修复
问题根源
你判断的完全正确:
CGAffineTransform是仿射变换,仅能生成平行四边形变换,三个点(左上、右上、左下)就能完全确定变换结果,第四个点会被强制保持平行,因此拖动右下角点不会产生任何效果。- 你的
animatableData只处理了左上和右下点的动画数据,其他点的拖动动画也存在潜在问题。
要实现任意四点的透视扭曲,必须使用透视投影矩阵,而非仿射变换。
修复后的完整代码
import SwiftUI struct DraggableView: View { @State private var topLeft = CGPoint(x: 50, y: 50) @State private var topRight = CGPoint(x: 150, y: 50) @State private var bottomLeft = CGPoint(x: 50, y: 150) @State private var bottomRight = CGPoint(x: 150, y: 150) var body: some View { ZStack { Image("overlay") .resizable() .frame(width: 400, height: 400) Image("screenshot") .resizable() .frame(width: 400, height: 400) .modifier(PerspectiveModifier( topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight )) CornerPointsView( topLeft: $topLeft, topRight: $topRight, bottomLeft: $bottomLeft, bottomRight: $bottomRight ) } .frame(width: 400, height: 400) } } struct CornerPointsView: View { @Binding var topLeft: CGPoint @Binding var topRight: CGPoint @Binding var bottomLeft: CGPoint @Binding var bottomRight: CGPoint var body: some View { ZStack { DraggablePoint(position: $topLeft, color: .red) DraggablePoint(position: $topRight, color: .blue) DraggablePoint(position: $bottomLeft, color: .green) DraggablePoint(position: $bottomRight, color: .yellow) } } } struct DraggablePoint: View { @Binding var position: CGPoint let color: Color var body: some View { Circle() .fill(color) .frame(width: 20, height: 20) .position(position) .gesture( DragGesture() .onChanged { value in position = value.location } ) } } struct PerspectiveModifier: GeometryEffect { var topLeft: CGPoint var topRight: CGPoint var bottomLeft: CGPoint var bottomRight: CGPoint // 完善动画数据,包含四个点的所有坐标 var animatableData: AnimatablePair<AnimatablePair<CGPoint.AnimatableData, CGPoint.AnimatableData>, AnimatablePair<CGPoint.AnimatableData, CGPoint.AnimatableData>> { get { AnimatablePair( AnimatablePair(topLeft.animatableData, topRight.animatableData), AnimatablePair(bottomLeft.animatableData, bottomRight.animatableData) ) } set { topLeft.animatableData = newValue.first.first topRight.animatableData = newValue.first.second bottomLeft.animatableData = newValue.second.first bottomRight.animatableData = newValue.second.second } } func effectValue(size: CGSize) -> ProjectionTransform { // 原始图片的四个角点 let srcPoints = [ CGPoint(x: 0, y: 0), CGPoint(x: size.width, y: 0), CGPoint(x: 0, y: size.height), CGPoint(x: size.width, y: size.height) ] // 目标四个角点 let dstPoints = [topLeft, topRight, bottomLeft, bottomRight] // 计算透视变换矩阵 guard let transform = calculatePerspectiveTransform(from: srcPoints, to: dstPoints) else { return ProjectionTransform(.identity) } return ProjectionTransform(transform) } // 核心:计算四点透视变换矩阵 private func calculatePerspectiveTransform(from src: [CGPoint], to dst: [CGPoint]) -> CGAffineTransform? { guard src.count == 4, dst.count == 4 else { return nil } var matrix = [Double](repeating: 0, count: 8) let points = zip(src, dst) for (i, (s, d)) in points.enumerated() { matrix[i*2] = Double(s.x) matrix[i*2+1] = Double(s.y) } var rhs = [Double]() for d in dst { rhs.append(Double(d.x)) rhs.append(Double(d.y)) } // 解线性方程组求变换参数 var solution = [Double](repeating: 0, count: 8) guard solveLinearSystem(matrix: matrix, rhs: rhs, solution: &solution) else { return nil } return CGAffineTransform( a: CGFloat(solution[0]), b: CGFloat(solution[1]), c: CGFloat(solution[2]), d: CGFloat(solution[3]), tx: CGFloat(solution[4]), ty: CGFloat(solution[5]) ).concatenating(CGAffineTransform( m11: CGFloat(solution[6]), m12: CGFloat(solution[7]), m21: 0, m22: 0, m31: 0, m32: 0, m33: 1 )) } // 解线性方程组(用于透视变换矩阵计算) private func solveLinearSystem(matrix: [Double], rhs: [Double], solution: inout [Double]) -> Bool { // 简化的高斯消元实现,适配8x8方程组 var mat = matrix var rhsCopy = rhs for i in 0..<8 { // 找主元 var pivot = i for j in i..<8 { if abs(mat[j*8+i]) > abs(mat[pivot*8+i]) { pivot = j } } if abs(mat[pivot*8+i]) < 1e-6 { return false // 奇异矩阵,无解 } // 交换行 if pivot != i { for k in i..<8 { swap(&mat[i*8+k], &mat[pivot*8+k]) } swap(&rhsCopy[i], &rhsCopy[pivot]) } // 归一化主元行 let div = mat[i*8+i] for k in i..<8 { mat[i*8+k] /= div } rhsCopy[i] /= div // 消去其他行 for j in 0..<8 { if j != i && abs(mat[j*8+i]) > 1e-6 { let factor = mat[j*8+i] for k in i..<8 { mat[j*8+k] -= factor * mat[i*8+k] } rhsCopy[j] -= factor * rhsCopy[i] } } } solution = rhsCopy return true } } #Preview { DraggableView() }
关键修改说明
- 替换变换类型:用自定义的透视变换矩阵计算替代
CGAffineTransform,支持任意四边形的扭曲效果,四个角点都会参与变换计算。 - 完善动画数据:
animatableData包含了四个点的所有坐标信息,确保拖动任意点都能有平滑的动画过渡。 - 透视矩阵计算:通过解线性方程组的方式,计算出从原始图片四角到目标拖动四角的透视变换矩阵,这是实现任意四点扭曲的核心逻辑。
内容的提问来源于stack exchange,提问作者SeanKly
相关产品推荐
相关产品推荐

