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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 19:58:10