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

如何为macOS中NSBezierPath绘制的缺口路径实现手绘动画?

实现NSBezierPath缺口路径的手绘风格动画

要给你的缺口路径添加手绘风格动画,核心是模拟真实手绘的不规则线条、变速节奏、细微笔触变化这几个特点,以下是针对你现有代码的具体修改方案:

1. 生成带随机扰动的手绘路径

真实手绘的线条不会完全平滑,我们可以给路径的每个顶点和控制点添加微小的随机偏移,模拟手绘的抖动感:

首先添加一个生成随机偏移的工具方法:

private func randomOffset() -> CGFloat {
    // 控制偏移范围,数值越小抖动越细微
    return CGFloat.random(in: -0.3...0.3)
}

然后修改drawOutline里的路径生成逻辑,给每个点的坐标添加随机偏移:

private func drawOutline() {
    let size = self.frame.size
    let gap: CGFloat = 0.8
    let notch = NSBezierPath()
    
    // 给每个点添加随机偏移
    notch.move(to: NSPoint(x: 0.0 + randomOffset(), y: size.height + 1.0 + randomOffset()))
    notch.curve(to: NSPoint(x: notchRadius + randomOffset(), y: size.height - notchRadius + randomOffset()),
                controlPoint1: NSPoint(x: notchRadius - gap + randomOffset(), y: size.height + randomOffset()),
                controlPoint2: NSPoint(x: notchRadius + randomOffset(), y: size.height - gap + randomOffset()))
    notch.line(to: NSPoint(x: notchRadius + randomOffset(), y: notchRadius + randomOffset()))
    notch.curve(to: NSPoint(x: notchRadius * 2.0 + randomOffset(), y: 1.0 + randomOffset()),
                controlPoint1: NSPoint(x: notchRadius + randomOffset(), y: gap + randomOffset()),
                controlPoint2: NSPoint(x: notchRadius + gap + randomOffset(), y: 1.0 + randomOffset()))
    notch.line(to: NSPoint(x: size.width - 2 * notchRadius + randomOffset(), y: 1.0 + randomOffset()))
    notch.curve(to: NSPoint(x: size.width - notchRadius + randomOffset(), y: notchRadius + randomOffset()),
                controlPoint1: NSPoint(x: size.width - notchRadius - gap + randomOffset(), y: 1.0 + randomOffset()),
                controlPoint2: NSPoint(x: size.width - notchRadius + randomOffset(), y: gap + randomOffset()))
    notch.line(to: NSPoint(x: size.width - notchRadius + randomOffset(), y: size.height - notchRadius + randomOffset()))
    notch.curve(to: NSPoint(x: size.width + randomOffset(), y: size.height + 1.0 + randomOffset()),
                controlPoint1: NSPoint(x: size.width - notchRadius + randomOffset(), y: size.height - gap + randomOffset()),
                controlPoint2: NSPoint(x: size.width - notchRadius + gap + randomOffset(), y: size.height + randomOffset()))
    notch.close()
    
    // 原有描边逻辑保留
    notch.lineWidth = 1.6
    outlineColor.setStroke()
    notch.stroke()
    thePath = notch.cgPath
    
    // 更新或创建CAShapeLayer
    if shapeLayer == nil {
        shapeLayer = CAShapeLayer()
        shapeLayer.lineCap = .round // 设置圆角笔触,更贴近手绘笔锋
        shapeLayer.lineJoin = .round
        shapeLayer.lineWidth = notch.lineWidth
        shapeLayer.strokeColor = NSColor.notchPurple.cgColor
        shapeLayer.fillColor = NSColor.clear.cgColor
        
        // 开启轻微外发光增强手绘质感
        shapeLayer.shadowColor = NSColor.white.cgColor
        shapeLayer.shadowOffset = .zero
        shapeLayer.shadowRadius = 6.0
        shapeLayer.shadowOpacity = 0.3
        
        self.layer?.addSublayer(shapeLayer)
    }
    // 路径更新时同步shapeLayer的path
    shapeLayer.path = notch.cgPath
    
    animatePathDrawing()
}

2. 替换线性动画为变速手绘节奏

原来的strokeEnd线性动画太规整,我们用**关键帧动画(CAKeyframeAnimation)**模拟手绘时的快慢停顿:

func animatePathDrawing() {
    self.alphaValue = 1.0
    shapeLayer.strokeEnd = 0.0
    
    // 1. 变速描边动画:模拟手绘的快慢节奏
    let strokeAnimation = CAKeyframeAnimation(keyPath: "strokeEnd")
    // 控制描边进度的关键帧
    strokeAnimation.values = [0.0, 0.12, 0.25, 0.4, 0.55, 0.7, 0.82, 0.9, 1.0]
    // 对应关键帧的时间点,不均匀分布模拟停顿和加速
    strokeAnimation.keyTimes = [0.0, 0.1, 0.22, 0.38, 0.55, 0.7, 0.83, 0.9, 1.0]
    strokeAnimation.duration = 3.5 // 延长动画时间,更符合手绘速度
    // 每个阶段用不同的缓动函数,增强自然感
    strokeAnimation.timingFunctions = [
        CAMediaTimingFunction(name: .easeInEaseOut),
        CAMediaTimingFunction(name: .easeIn),
        CAMediaTimingFunction(name: .easeOut),
        CAMediaTimingFunction(name: .easeInEaseOut),
        CAMediaTimingFunction(name: .easeIn),
        CAMediaTimingFunction(name: .easeOut),
        CAMediaTimingFunction(name: .easeInEaseOut),
        CAMediaTimingFunction(name: .easeIn)
    ]
    
    // 2. 笔触粗细变化动画:模拟手绘时的压力变化
    let widthAnimation = CAKeyframeAnimation(keyPath: "lineWidth")
    widthAnimation.values = [1.2, 1.7, 1.4, 1.8, 1.5, 1.6]
    widthAnimation.keyTimes = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]
    widthAnimation.duration = 3.5
    
    // 配置动画循环和反转
    strokeAnimation.repeatCount = .infinity
    strokeAnimation.autoreverses = true
    widthAnimation.repeatCount = .infinity
    widthAnimation.autoreverses = true
    
    // 添加动画到图层
    shapeLayer.add(strokeAnimation, forKey: "strokeEndAnimation")
    shapeLayer.add(widthAnimation, forKey: "lineWidthAnimation")
    isAnimated = true
}

3. 其他优化点

  • 开启shapeLayer的shadowOpacity(设置为0.2-0.4),让线条有轻微光晕,更贴近手绘的质感
  • 设置lineCap和lineJoin为.round,模拟手绘笔锋的圆角效果
  • 调整随机偏移的范围(randomOffset的区间),可以控制抖动的幅度,数值越小越细腻,越大越夸张

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 03:45:01