如何以给定的顺序动画/填充 UIBezierPath?
Posted
技术标签:
【中文标题】如何以给定的顺序动画/填充 UIBezierPath?【英文标题】:How to animate/fill an UIBezierPath in a given order? 【发布时间】:2021-01-09 09:13:39 【问题描述】:从一个文件中,我有一堆 SVG 路径,我将它们转换为 UIBezierPath。为了使我的示例简单,我手动创建了路径:
struct myShape: Shape
func path(in rect: CGRect) -> Path
let p = UIBezierPath()
p.move(to: CGPoint(x: 147, y: 32))
p.addQuadCurve(to: CGPoint(x: 203, y: 102), controlPoint: CGPoint(x: 181, y: 74))
p.addQuadCurve(to: CGPoint(x: 271, y: 189), controlPoint: CGPoint(x: 242, y: 166))
p.addQuadCurve(to: CGPoint(x: 274, y: 217), controlPoint: CGPoint(x: 287, y: 204))
p.addQuadCurve(to: CGPoint(x: 229, y: 235), controlPoint: CGPoint(x: 258, y: 229))
p.addQuadCurve(to: CGPoint(x: 193, y: 235), controlPoint: CGPoint(x: 204, y: 241))
p.addQuadCurve(to: CGPoint(x: 190, y: 219), controlPoint: CGPoint(x: 183, y: 231))
p.addQuadCurve(to: CGPoint(x: 143, y: 71), controlPoint: CGPoint(x: 199, y: 195))
p.addQuadCurve(to: CGPoint(x: 125, y: 33), controlPoint: CGPoint(x: 134, y: 55))
p.addCurve(to: CGPoint(x: 147, y: 32), controlPoint1: CGPoint(x: 113, y: 5), controlPoint2: CGPoint(x: 128, y: 9))
p.close()
return Path(p.cgPath)
结果如下图:
对于这个数字,我有一个单独的路径/形状,代表数字的“中位数”和这个数字应该填充的顺序。路径只是行的串联。
struct myMedian: Shape
func path(in rect: CGRect) -> Path
let p = UIBezierPath()
p.move(to: CGPoint(x: 196, y: 226))
p.addLine(to: CGPoint(x: 209, y: 220))
p.addLine(to: CGPoint(x: 226, y: 195))
p.addLine(to: CGPoint(x: 170, y: 86))
p.addLine(to: CGPoint(x: 142, y: 43))
p.addLine(to: CGPoint(x: 131, y: 39))
return Path(p.cgPath)
为了可视化线条的顺序,我添加了红色箭头:
现在我需要按照与“中位笔画”相同的顺序填写“大图”。我知道如何一步一步填充整个图形,但不会拆分,尤其是我不知道如何管理动画的方向。
最终的结果应该是这样的:
由于我使用的是 SwiftUI,它应该与它兼容。
主要观点是:
struct DrawCharacter: View
var body: some View
ZStack(alignment: .topLeading)
myShape()
myMedian()
【问题讨论】:
您能发布一个指向您的 SwiftUI 最小项目的链接吗?因为即使有人知道如何使用 UIKit,也很难猜测 SwiftUI 对这个问题的限制 其实它只是显示两个形状。我已经添加了主视图的代码。 【参考方案1】:您可以在 Shape 中定义 animatableData
属性,以使 SwiftUI 能够在状态(例如,填充和未填充)之间进行插值。从哪里开始绘制每个笔画对于方向很重要。如果您愿意,也可以在路径上使用 .trim 来截断它,并将该值绑定到 animatableData。
对于一个完整的字符/汉字,您可能需要组合具有定义位置的多个子形状的元形状或视图,但从长远来看,这实际上对您来说工作量较小,因为您可以创建一个笔画库,很容易重组,不是吗?
在下面的示例中,我通过更改其他视图中的 percentFullMoon
属性将月亮从满月移动到新月。路径绘制函数使用该属性来设置一些弧。 SwiftUI 会读取 animatableData 属性,以确定如何绘制动画时它选择插入多少帧。
这很简单,即使这个 API 的名字很隐晦。
这有意义吗?注意:下面的代码使用了一些辅助函数,如钳位、北/南和中心/半径,但与概念无关。
import SwiftUI
/// Percent full moon is from 0 to 1
struct WaningMoon: Shape
/// From 0 to 1
var percentFullMoon: Double
var animatableData: Double
get percentFullMoon
set self.percentFullMoon = newValue
func path(in rect: CGRect) -> Path
var path = Path()
addExteriorArc(&path, rect)
let cycle = percentFullMoon * 180
switch cycle
case 90:
return path
case ..<90:
let crescent = Angle(degrees: 90 + .clamp(0.nextUp, 90, 90 - cycle))
addInteriorArc(&path, rect, angle: crescent)
return path
case 90.nextUp...:
let gibbous = Angle(degrees: .clamp(0, 90.nextDown, 180 - cycle))
addInteriorArc(&path, rect, angle: gibbous)
return path
default: return path
private func addInteriorArc(_ path: inout Path, _ rect: CGRect, angle: Angle)
let xOffset = rect.radius * angle.tan()
let offsetCenter = CGPoint(x: rect.midX - xOffset, y: rect.midY)
return path.addArc(
center: offsetCenter,
radius: rect.radius / angle.cos(),
startAngle: .south() - angle,
endAngle: .north() + angle,
clockwise: angle.degrees < 90) // False == Crescent, True == Gibbous
private func addExteriorArc(_ path: inout Path, _ rect: CGRect)
path.addArc(center: rect.center,
radius: rect.radius,
startAngle: .north(),
endAngle: .south(),
clockwise: true)
帮手
extension Comparable
static func clamp<N:Comparable>(_ min: N, _ max: N, _ variable: N) -> N
Swift.max(min, Swift.min(variable, max))
func clamp<N:Comparable>(_ min: N, _ max: N, _ variable: N) -> N
Swift.max(min, Swift.min(variable, max))
extension Angle
func sin() -> CGFloat
CoreGraphics.sin(CGFloat(self.radians))
func cos() -> CGFloat
CoreGraphics.cos(CGFloat(self.radians))
func tan() -> CGFloat
CoreGraphics.tan(CGFloat(self.radians))
static func north() -> Angle
Angle(degrees: -90)
static func south() -> Angle
Angle(degrees: 90)
extension CGRect
var center: CGPoint
CGPoint(x: midX, y: midY)
var radius: CGFloat
min(width, height) / 2
var diameter: CGFloat
min(width, height)
var N: CGPoint CGPoint(x: midX, y: minY)
var E: CGPoint CGPoint(x: minX, y: midY)
var W: CGPoint CGPoint(x: maxX, y: midY)
var S: CGPoint CGPoint(x: midX, y: maxY)
var NE: CGPoint CGPoint(x: maxX, y: minY)
var NW: CGPoint CGPoint(x: minX, y: minY)
var SE: CGPoint CGPoint(x: maxX, y: maxY)
var SW: CGPoint CGPoint(x: minX, y: maxY)
func insetN(_ denominator: CGFloat) -> CGPoint
CGPoint(x: midX, y: minY + height / denominator)
func insetE(_ denominator: CGFloat) -> CGPoint
CGPoint(x: minX - width / denominator, y: midY)
func insetW(_ denominator: CGFloat) -> CGPoint
CGPoint(x: maxX + width / denominator, y: midY)
func insetS(_ denominator: CGFloat) -> CGPoint
CGPoint(x: midX, y: maxY - height / denominator)
func insetNE(_ denominator: CGFloat) -> CGPoint
CGPoint(x: maxX + width / denominator, y: minY + height / denominator)
func insetNW(_ denominator: CGFloat) -> CGPoint
CGPoint(x: minX - width / denominator, y: minY + height / denominator)
func insetSE(_ denominator: CGFloat) -> CGPoint
CGPoint(x: maxX + width / denominator, y: maxY - height / denominator)
func insetSW(_ denominator: CGFloat) -> CGPoint
CGPoint(x: minX - width / denominator, y: maxY - height / denominator)
【讨论】:
是的,如果您可以添加north()
、south()
和 clamp()
,那将会很有帮助。另外XCode告诉我CGRect has no member 'center'
和'radius'
我已经发布了辅助函数。
尝试制作单个形状可能不是您的最佳选择,但您可能最终会组合一个形状视图,其中动画在每个单独的形状上都有延迟。
@kirkyoyx 有什么问题吗?
谢谢。我不确定我做错了什么,但是当我使用此代码时月亮没有动画@State var percent: Double = 0.1 var body: some View WaningMoon(percentFullMoon: percent) .onTapGesture percent = 1.0
以上是关于如何以给定的顺序动画/填充 UIBezierPath?的主要内容,如果未能解决你的问题,请参考以下文章