如何在SwiftUI中实现手绘铅笔风格的涂鸦边框?
SwiftUI实现手绘铅笔风格边框
你当前用strokeBorder(ImagePaint())生成的边框过于规整,是因为图片被均匀平铺填充了路径。要实现类似手绘铅笔的不规则边框,可以试试以下几种方案:
方法一:自定义手绘风格Shape
通过对原始形状的路径点添加随机偏移,模拟手绘的抖动效果,生成不规则边框:
import SwiftUI struct SketchBorderShape<S: Shape>: Shape { let shape: S let roughness: CGFloat // 控制抖动幅度,值越大越粗糙 let lineWidth: CGFloat func path(in rect: CGRect) -> Path { let originalPath = shape.path(in: rect.insetBy(dx: lineWidth/2, dy: lineWidth/2)) var sketchPath = Path() originalPath.forEach { element in switch element { case .move(to: let point): let offsetPoint = point.applying(CGAffineTransform(translationX: CGFloat.random(in: -roughness...roughness), y: CGFloat.random(in: -roughness...roughness))) sketchPath.move(to: offsetPoint) case .line(to: let point): let offsetPoint = point.applying(CGAffineTransform(translationX: CGFloat.random(in: -roughness...roughness), y: CGFloat.random(in: -roughness...roughness))) sketchPath.addLine(to: offsetPoint) case .quadCurve(to: let to, control: let control): let offsetTo = to.applying(CGAffineTransform(translationX: CGFloat.random(in: -roughness...roughness), y: CGFloat.random(in: -roughness...roughness))) let offsetControl = control.applying(CGAffineTransform(translationX: CGFloat.random(in: -roughness...roughness), y: CGFloat.random(in: -roughness...roughness))) sketchPath.addQuadCurve(to: offsetTo, control: offsetControl) case .curve(to: let to, control1: let control1, control2: let control2): let offsetTo = to.applying(CGAffineTransform(translationX: CGFloat.random(in: -roughness...roughness), y: CGFloat.random(in: -roughness...roughness))) let offsetControl1 = control1.applying(CGAffineTransform(translationX: CGFloat.random(in: -roughness...roughness), y: CGFloat.random(in: -roughness...roughness))) let offsetControl2 = control2.applying(CGAffineTransform(translationX: CGFloat.random(in: -roughness...roughness), y: CGFloat.random(in: -roughness...roughness))) sketchPath.addCurve(to: offsetTo, control1: offsetControl1, control2: offsetControl2) case .closeSubpath: sketchPath.closeSubpath() } } return sketchPath } } // 给Shape添加扩展,方便调用 extension Shape { func sketchBorder(roughness: CGFloat = 2, lineWidth: CGFloat = 20, color: Color = .black) -> some View { SketchBorderShape(shape: self, roughness: roughness, lineWidth: lineWidth) .stroke(color, lineWidth: lineWidth) .padding(lineWidth/2) } } // 使用示例 struct TestView: View { var body: some View{ Circle() .sketchBorder(roughness: 3, lineWidth: 20, color: .gray) .frame(width: 100, height: 100) } } struct TestView_Previews: PreviewProvider { static var previews: some View { TestView() } }
方法二:CoreGraphics手绘边框(更逼真)
用CoreGraphics对路径进行分段采样,给每个采样点添加随机偏移,模拟铅笔手绘的连续抖动效果:
import SwiftUI import UIKit class SketchBorderView: UIView { var shapeType: ShapeType = .circle var lineWidth: CGFloat = 20 var roughness: CGFloat = 3 var strokeColor: UIColor = .gray enum ShapeType { case circle case rectangle(CGRect) } override func draw(_ rect: CGRect) { guard let context = UIGraphicsGetCurrentContext() else { return } context.setStrokeColor(strokeColor.cgColor) context.setLineWidth(lineWidth) context.setLineCap(.round) context.setLineJoin(.round) var path = UIBezierPath() switch shapeType { case .circle: let insetRect = rect.insetBy(dx: lineWidth/2, dy: lineWidth/2) path = UIBezierPath(ovalIn: insetRect) case .rectangle(let rect): let insetRect = rect.insetBy(dx: lineWidth/2, dy: lineWidth/2) path = UIBezierPath(rect: insetRect) } let sketchPath = createSketchPath(from: path, roughness: roughness) sketchPath.stroke() } private func createSketchPath(from path: UIBezierPath, roughness: CGFloat) -> UIBezierPath { let sketchPath = UIBezierPath() var previousPoint: CGPoint? path.cgPath.applyWithBlock { element in let points = element.pointee.points switch element.pointee.type { case .moveToPoint: let offsetPoint = points[0].offsetBy(dx: CGFloat.random(in: -roughness...roughness), dy: CGFloat.random(in: -roughness...roughness)) sketchPath.move(to: offsetPoint) previousPoint = offsetPoint case .addLineToPoint: let segments = 10 for i in 1...segments { let t = CGFloat(i)/CGFloat(segments) let intermediatePoint = CGPoint(x: previousPoint!.x + (points[0].x - previousPoint!.x)*t, y: previousPoint!.y + (points[0].y - previousPoint!.y)*t) let offsetPoint = intermediatePoint.offsetBy(dx: CGFloat.random(in: -roughness...roughness), dy: CGFloat.random(in: -roughness...roughness)) sketchPath.addLine(to: offsetPoint) previousPoint = offsetPoint } case .addQuadCurveToPoint: let segments = 15 for i in 1...segments { let t = CGFloat(i)/CGFloat(segments) let x = pow(1-t, 2)*previousPoint!.x + 2*(1-t)*t*points[0].x + pow(t,2)*points[1].x let y = pow(1-t, 2)*previousPoint!.y + 2*(1-t)*t*points[0].y + pow(t,2)*points[1].y let offsetPoint = CGPoint(x: x, y: y).offsetBy(dx: CGFloat.random(in: -roughness...roughness), dy: CGFloat.random(in: -roughness...roughness)) sketchPath.addLine(to: offsetPoint) previousPoint = offsetPoint } case .closeSubpath: sketchPath.close() default: break } } return sketchPath } } extension CGPoint { func offsetBy(dx: CGFloat, dy: CGFloat) -> CGPoint { CGPoint(x: x + dx, y: y + dy) } } // SwiftUI包装器 struct SketchBorderUIView: UIViewRepresentable { var shapeType: SketchBorderView.ShapeType var lineWidth: CGFloat var roughness: CGFloat var strokeColor: Color func makeUIView(context: Context) -> SketchBorderView { let view = SketchBorderView() view.shapeType = shapeType view.lineWidth = lineWidth view.roughness = roughness view.strokeColor = UIColor(strokeColor) return view } func updateUIView(_ uiView: SketchBorderView, context: Context) { uiView.shapeType = shapeType uiView.lineWidth = lineWidth uiView.roughness = roughness uiView.strokeColor = UIColor(strokeColor) uiView.setNeedsDisplay() } } // 使用示例 struct TestView: View { var body: some View{ SketchBorderUIView( shapeType: .circle, lineWidth: 20, roughness: 3, strokeColor: .gray ) .frame(width: 100, height: 100) } } struct TestView_Previews: PreviewProvider { static var previews: some View { TestView() } }
方法三:叠加随机变换的ImagePaint边框(简化版)
如果想继续用ImagePaint,可以叠加多个带随机旋转、偏移的边框图层,模拟不规则效果:
struct TestView: View { var body: some View{ ZStack { Circle() .strokeBorder(ImagePaint(image: Image("pencil3"), scale: 0.3), lineWidth: 20) .rotationEffect(Angle(degrees: Double.random(in: -5...5))) .offset(x: CGFloat.random(in: -2...2), y: CGFloat.random(in: -2...2)) Circle() .strokeBorder(ImagePaint(image: Image("pencil3"), scale: 0.3), lineWidth: 18) .rotationEffect(Angle(degrees: Double.random(in: -5...5))) .offset(x: CGFloat.random(in: -2...2), y: CGFloat.random(in: -2...2)) .opacity(0.7) } .frame(width:100, height:100) } }
内容的提问来源于stack exchange,提问作者Jude
相关产品推荐
相关产品推荐

