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

SwiftUI中为任意形状实现涂鸦效果的最优方案探讨

通用涂鸦形状实现方案

你已经实现的矩形涂鸦效果很扎实,要扩展到任意形状,核心是复用一套基于原始形状路径的涂鸦生成逻辑,不需要为每个形状单独编写代码。下面是实现方案:

核心思路

  1. 包装任意Shape类型,获取其原始路径的边界范围
  2. 根据指定角度生成平行采样线,对原始形状的边界进行随机偏移采样
  3. 使用种子随机数保证视图刷新时涂鸦形状不变
  4. 通过贝塞尔曲线连接采样点,模拟手绘涂鸦的不规则效果

完整实现代码

import SwiftUI

// 通用涂鸦形状,支持任意基础形状与任意角度线条
struct ScribbleShape<Base: Shape>: Shape {
    let baseShape: Base
    let brushSize: CGFloat
    let angle: Angle
    let outer: Bool
    private let randomSeed: Int
    
    init(baseShape: Base, brushSize: CGFloat, angle: Angle = .zero, outer: Bool = false) {
        self.baseShape = baseShape
        self.brushSize = brushSize
        self.angle = angle
        self.outer = outer
        self.randomSeed = Int(brushSize) + Int(angle.degrees)
    }
    
    func path(in rect: CGRect) -> Path {
        let basePath = baseShape.path(in: rect)
        let bounds = basePath.boundingRect
        let distance = brushSize * (outer ? 0.6 : 0.5)
        
        // 计算采样线的方向向量
        let angleRad = angle.radians
        let direction = CGVector(dx: cos(angleRad), dy: sin(angleRad))
        
        // 计算采样线的数量与分布
        let maxDimension = max(bounds.width, bounds.height)
        let count = Int(maxDimension / distance) + 2
        
        var rnd = SeededRandom(seed: randomSeed)
        var path = Path()
        
        // 生成第一条采样线的起点
        let startPoint = CGPoint(
            x: bounds.minX - (outer ? distance : 0),
            y: bounds.minY - (outer ? distance : 0)
        )
        path.move(to: startPoint)
        
        var flag = true
        
        for i in 0..<count {
            // 计算当前采样线的位置
            let offset = distance * CGFloat(i)
            let lineOrigin = CGPoint(
                x: startPoint.x + direction.dx * offset,
                y: startPoint.y + direction.dy * offset
            )
            
            // 计算与原始形状边界的交点偏移
            let borderOffset = CGFloat(rnd.next(upperBound: 8)) * (outer ? 1 : -1) * (flag ? 1 : -1)
            let lineOffset = CGFloat(rnd.next(upperBound: UInt(distance)))
            
            // 计算目标点(基于原始形状边界的随机偏移)
            let targetPoint = CGPoint(
                x: lineOrigin.x + direction.dy * (bounds.height + borderOffset) - direction.dx * lineOffset,
                y: lineOrigin.y - direction.dx * (bounds.width + borderOffset) - direction.dy * lineOffset
            )
            
            // 添加贝塞尔曲线模拟涂鸦效果
            let controlPoint = CGPoint(
                x: lineOrigin.x + direction.dx * offset / 2,
                y: lineOrigin.y + direction.dy * offset / 2
            )
            path.addQuadCurve(to: targetPoint, control: controlPoint)
            
            flag.toggle()
        }
        
        return path
    }
}

// 视图扩展,方便直接使用
extension View {
    func scribble(
        color: Color = .green,
        brushSize: CGFloat = 3,
        angle: Angle = .zero,
        outer: Bool = false
    ) -> some View {
        self.overlay(
            ScribbleShape(
                baseShape: self.shape,
                brushSize: brushSize,
                angle: angle,
                outer: outer
            )
            .stroke(style: StrokeStyle(lineWidth: brushSize, lineCap: .square, lineJoin: .bevel))
            .foregroundColor(color)
        )
    }
}

// 辅助:获取View的Shape表示(用于overlay)
private extension View {
    var shape: some Shape {
        ShapeOfView(content: self)
    }
}

private struct ShapeOfView<Content: View>: Shape {
    let content: Content
    
    func path(in rect: CGRect) -> Path {
        let size = rect.size
        return UIBezierPath(rect: CGRect(origin: .zero, size: size)).cgPath
    }
}

// 种子随机数生成器(复用你的实现)
fileprivate struct SeededRandom: RandomNumberGenerator {
    init(seed: Int) { srand48(seed) }
    func next() -> UInt64 { return UInt64(drand48() * Double(UInt64.max)) }
}

// 预览:展示不同形状的涂鸦效果
struct ScribbleShape_Previews: PreviewProvider {
    static var previews: some View {
        VStack(spacing: 40) {
            // 圆形
            Circle()
                .scribble(color: .red, brushSize: 4, angle: .degrees(45))
                .frame(width: 80, height: 80)
            
            // 胶囊形
            Capsule()
                .scribble(color: .blue, brushSize: 5, angle: .degrees(90))
                .frame(width: 150, height: 60)
            
            // 圆角矩形
            RoundedRectangle(cornerRadius: 15)
                .scribble(color: .green, brushSize: 3, angle: .degrees(0))
                .frame(width: 120, height: 80)
            
            // 菱形
            Diamond()
                .scribble(color: .orange, brushSize: 4, angle: .degrees(30))
                .frame(width: 80, height: 80)
        }
        .padding(50)
        .background(Color.white)
    }
}

// 自定义菱形形状
struct Diamond: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: rect.midX, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
        path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
        path.addLine(to: CGPoint(x: rect.minX, y: rect.midY))
        path.closeSubpath()
        return path
    }
}

关键特性说明

  1. 任意形状支持:通过ScribbleShape包装任意遵循Shape协议的类型,包括系统自带的Circle、Capsule、RoundedRectangle,以及自定义的Diamond等
  2. 任意角度线条:通过angle参数控制涂鸦线条的方向,支持0°、45°、90°等任意角度
  3. 刷新稳定性:使用基于brushSize和angle的种子随机数,保证视图刷新时涂鸦形状完全一致
  4. 灵活配置:支持调整画笔大小、颜色、内外涂鸦方向等参数

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 08:20:08