SwiftUI中为任意形状实现涂鸦效果的最优方案探讨
通用涂鸦形状实现方案
你已经实现的矩形涂鸦效果很扎实,要扩展到任意形状,核心是复用一套基于原始形状路径的涂鸦生成逻辑,不需要为每个形状单独编写代码。下面是实现方案:
核心思路
- 包装任意
Shape类型,获取其原始路径的边界范围 - 根据指定角度生成平行采样线,对原始形状的边界进行随机偏移采样
- 使用种子随机数保证视图刷新时涂鸦形状不变
- 通过贝塞尔曲线连接采样点,模拟手绘涂鸦的不规则效果
完整实现代码
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 } }
关键特性说明
- 任意形状支持:通过
ScribbleShape包装任意遵循Shape协议的类型,包括系统自带的Circle、Capsule、RoundedRectangle,以及自定义的Diamond等 - 任意角度线条:通过
angle参数控制涂鸦线条的方向,支持0°、45°、90°等任意角度 - 刷新稳定性:使用基于
brushSize和angle的种子随机数,保证视图刷新时涂鸦形状完全一致 - 灵活配置:支持调整画笔大小、颜色、内外涂鸦方向等参数
内容的提问来源于stack exchange,提问作者Andrew
相关产品推荐
相关产品推荐

