You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI拖拽式透视变换右下角控制点失效问题求助

SwiftUI 四点透视变换:右下角点无效问题修复

问题根源

你判断的完全正确:

  1. CGAffineTransform是仿射变换,仅能生成平行四边形变换,三个点(左上、右上、左下)就能完全确定变换结果,第四个点会被强制保持平行,因此拖动右下角点不会产生任何效果。
  2. 你的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()
}

关键修改说明

  1. 替换变换类型:用自定义的透视变换矩阵计算替代CGAffineTransform,支持任意四边形的扭曲效果,四个角点都会参与变换计算。
  2. 完善动画数据:animatableData包含了四个点的所有坐标信息,确保拖动任意点都能有平滑的动画过渡。
  3. 透视矩阵计算:通过解线性方程组的方式,计算出从原始图片四角到目标拖动四角的透视变换矩阵,这是实现任意四点扭曲的核心逻辑。

内容的提问来源于stack exchange,提问作者SeanKly

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 18:44:51