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

SwiftUI中如何平滑连接波浪邮票形状的曲线边缘?

解决SwiftUI WavyPostageStampShape边缘平滑连接问题

当前实现的波浪邮票形状边缘由折线拼接,角落过渡生硬,无法达到平滑曲线的效果。以下是修改后的代码,实现边缘平滑过渡的目标:

修改后的完整代码

struct WavyPostageStampShape: Shape {
    var amplitude: CGFloat = 8
    var wavesX: Int = 6
    var wavesY: Int = 10
    var phase: CGFloat = 0
    var step: CGFloat = 2

    func path(in rect: CGRect) -> Path {
        var path = Path()

        let w = rect.width
        let h = rect.height

        // 基础波浪函数,返回偏移量
        func wave(_ t: CGFloat, waves: Int, inverted: Bool = false) -> CGFloat {
            let angle = phase + t * CGFloat(waves) * 2 * .pi
            let baseValue = sin(angle)
            return inverted ? -baseValue : baseValue
        }

        // 生成平滑曲线的点组,同时计算每个点的切线方向(用于曲线拟合)
        func generateSmoothPoints(for edge: Edge, rect: CGRect) -> [(point: CGPoint, tangent: CGPoint)] {
            var pointsWithTangent: [(CGPoint, CGPoint)] = []
            
            switch edge {
            case .top:
                for x in stride(from: rect.minX, through: rect.maxX, by: step) {
                    let t = (x - rect.minX) / w
                    let offset = wave(t, waves: wavesX) * amplitude
                    let point = CGPoint(x: x, y: rect.minY - offset)
                    // 计算切线:对波浪函数求导,得到切线斜率
                    let derivative = cos(phase + t * CGFloat(wavesX) * 2 * .pi) * CGFloat(wavesX) * 2 * .pi * amplitude
                    let tangent = CGPoint(x: step, y: -derivative * step) // 切线向量
                    pointsWithTangent.append((point, tangent))
                }
            case .right:
                for y in stride(from: rect.minY, through: rect.maxY, by: step) {
                    let t = (y - rect.minY) / h
                    let offset = wave(t, waves: wavesY, inverted: true) * amplitude
                    let point = CGPoint(x: rect.maxX + offset, y: y)
                    let derivative = cos(phase + t * CGFloat(wavesY) * 2 * .pi) * CGFloat(wavesY) * 2 * .pi * amplitude
                    let tangent = CGPoint(x: derivative * step, y: step)
                    pointsWithTangent.append((point, tangent))
                }
            case .bottom:
                for x in stride(from: rect.maxX, through: rect.minX, by: -step) {
                    let t = (rect.maxX - x) / w
                    let offset = wave(t, waves: wavesX, inverted: true) * amplitude
                    let point = CGPoint(x: x, y: rect.maxY + offset)
                    let derivative = cos(phase + t * CGFloat(wavesX) * 2 * .pi) * CGFloat(wavesX) * 2 * .pi * amplitude
                    let tangent = CGPoint(x: -step, y: derivative * step)
                    pointsWithTangent.append((point, tangent))
                }
            case .left:
                for y in stride(from: rect.maxY, through: rect.minY, by: -step) {
                    let t = (rect.maxY - y) / h
                    let offset = wave(t, waves: wavesY) * amplitude
                    let point = CGPoint(x: rect.minX - offset, y: y)
                    let derivative = cos(phase + t * CGFloat(wavesY) * 2 * .pi) * CGFloat(wavesY) * 2 * .pi * amplitude
                    let tangent = CGPoint(x: -derivative * step, y: -step)
                    pointsWithTangent.append((point, tangent))
                }
            }
            
            return pointsWithTangent
        }

        // 生成四个边的平滑点和切线
        let topPoints = generateSmoothPoints(for: .top, rect: rect)
        let rightPoints = generateSmoothPoints(for: .right, rect: rect)
        let bottomPoints = generateSmoothPoints(for: .bottom, rect: rect)
        let leftPoints = generateSmoothPoints(for: .left, rect: rect)

        guard let firstPoint = topPoints.first?.point else { return path }
        path.move(to: firstPoint)

        // 绘制顶部平滑曲线
        for i in 1..<topPoints.count {
            let prev = topPoints[i-1]
            let curr = topPoints[i]
            // 使用三次贝塞尔曲线,控制点基于前一个点的切线和当前点的切线
            let control1 = CGPoint(x: prev.point.x + prev.tangent.x/2, y: prev.point.y + prev.tangent.y/2)
            let control2 = CGPoint(x: curr.point.x - curr.tangent.x/2, y: curr.point.y - curr.tangent.y/2)
            path.addCurve(to: curr.point, control1: control1, control2: control2)
        }

        // 顶部到右侧的平滑过渡(三次贝塞尔曲线,保证切线连续)
        if let lastTop = topPoints.last, let firstRight = rightPoints.first {
            let corner = CGPoint(x: rect.maxX, y: rect.minY)
            // 顶部最后一个点的切线方向和右侧第一个点的切线方向在角落处衔接
            let control1 = CGPoint(x: lastTop.point.x + lastTop.tangent.x/2, y: lastTop.point.y + lastTop.tangent.y/2)
            let control2 = CGPoint(x: firstRight.point.x - firstRight.tangent.x/2, y: firstRight.point.y - firstRight.tangent.y/2)
            path.addCurve(to: firstRight.point, control1: control1, control2: control2)
        }

        // 绘制右侧平滑曲线
        for i in 1..<rightPoints.count {
            let prev = rightPoints[i-1]
            let curr = rightPoints[i]
            let control1 = CGPoint(x: prev.point.x + prev.tangent.x/2, y: prev.point.y + prev.tangent.y/2)
            let control2 = CGPoint(x: curr.point.x - curr.tangent.x/2, y: curr.point.y - curr.tangent.y/2)
            path.addCurve(to: curr.point, control1: control1, control2: control2)
        }

        // 右侧到底部的平滑过渡
        if let lastRight = rightPoints.last, let firstBottom = bottomPoints.first {
            let corner = CGPoint(x: rect.maxX, y: rect.maxY)
            let control1 = CGPoint(x: lastRight.point.x + lastRight.tangent.x/2, y: lastRight.point.y + lastRight.tangent.y/2)
            let control2 = CGPoint(x: firstBottom.point.x - firstBottom.tangent.x/2, y: firstBottom.point.y - firstBottom.tangent.y/2)
            path.addCurve(to: firstBottom.point, control1: control1, control2: control2)
        }

        // 绘制底部平滑曲线
        for i in 1..<bottomPoints.count {
            let prev = bottomPoints[i-1]
            let curr = bottomPoints[i]
            let control1 = CGPoint(x: prev.point.x + prev.tangent.x/2, y: prev.point.y + prev.tangent.y/2)
            let control2 = CGPoint(x: curr.point.x - curr.tangent.x/2, y: curr.point.y - curr.tangent.y/2)
            path.addCurve(to: curr.point, control1: control1, control2: control2)
        }

        // 底部到左侧的平滑过渡
        if let lastBottom = bottomPoints.last, let firstLeft = leftPoints.first {
            let corner = CGPoint(x: rect.minX, y: rect.maxY)
            let control1 = CGPoint(x: lastBottom.point.x + lastBottom.tangent.x/2, y: lastBottom.point.y + lastBottom.tangent.y/2)
            let control2 = CGPoint(x: firstLeft.point.x - firstLeft.tangent.x/2, y: firstLeft.point.y - firstLeft.tangent.y/2)
            path.addCurve(to: firstLeft.point, control1: control1, control2: control2)
        }

        // 绘制左侧平滑曲线
        for i in 1..<leftPoints.count {
            let prev = leftPoints[i-1]
            let curr = leftPoints[i]
            let control1 = CGPoint(x: prev.point.x + prev.tangent.x/2, y: prev.point.y + prev.tangent.y/2)
            let control2 = CGPoint(x: curr.point.x - curr.tangent.x/2, y: curr.point.y - curr.tangent.y/2)
            path.addCurve(to: curr.point, control1: control1, control2: control2)
        }

        // 左侧到顶部的平滑过渡
        if let lastLeft = leftPoints.last {
            let corner = CGPoint(x: rect.minX, y: rect.minY)
            let control1 = CGPoint(x: lastLeft.point.x + lastLeft.tangent.x/2, y: lastLeft.point.y + lastLeft.tangent.y/2)
            let control2 = CGPoint(x: firstPoint.x - topPoints.first!.tangent.x/2, y: firstPoint.y - topPoints.first!.tangent.y/2)
            path.addCurve(to: firstPoint, control1: control1, control2: control2)
        }

        path.closeSubpath()
        return path
    }
}

// 新增Edge枚举用于区分边
enum Edge {
    case top, right, bottom, left
}

预览代码

#Preview {
    WavyPostageStampShape(
        amplitude: 2,
        wavesX: 6,
        wavesY: 8,
        phase: Double.pi / 2
    )
    .stroke(Color.blue, lineWidth: 2)
    .frame(width: 120, height: 200)
}

关键修改说明

  • 用三次贝塞尔曲线替代直线:不再用addLine连接波浪点,而是通过计算每个波浪点的切线方向,生成平滑的三次贝塞尔曲线,让波浪边缘本身就是连续平滑的曲线。
  • 切线连续的角落过渡:角落处的过渡曲线不再用简单的二次曲线,而是基于相邻两边波浪点的切线方向生成三次贝塞尔曲线,保证角落处的切线和两边波浪的切线完全衔接,消除生硬转折。
  • 优化波浪相位与方向:为底部和右侧的波浪添加了反转参数,让四个边的波浪在角落处的偏移量和切线方向自然匹配,避免衔接时的突兀感。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.01 12:34:52