Joh*_*rtz 1 calayer uilabel cgpath catextlayer swift
我想添加文本,无论是 aUILabel还是CATextLayera CGPath。我意识到此功能背后的数学相当复杂,但想知道 Apple 是否提供了开箱即用的此功能,或者是否有开源 SDK 可以在 Swift 中实现这一点。谢谢!
您需要手动执行此操作,方法是在您关心的每个点计算贝塞尔函数及其斜率,然后在该点绘制字形并进行旋转。您需要知道 4 个点(传统上称为 P0-P3)。P0 是曲线的起点。P1和P2是控制点。P3 是曲线的终点。
Bezier 函数被定义为当“t”参数从 0 移动到 1 时,输出将跟踪所需的曲线。重要的是要知道“t”不是线性的。t=0.25 并不一定意味着“沿着曲线的 1/4”。(事实上,这几乎从来都不是真的。)这意味着测量长曲线的距离有点棘手。但我们会介绍这一点。
首先,您需要 CGPoint 的核心功能和有用的扩展:
// The Bezier function at t
func bezier(_ t: CGFloat, _ P0: CGFloat, _ P1: CGFloat, _ P2: CGFloat, _ P3: CGFloat) -> CGFloat {
(1-t)*(1-t)*(1-t) * P0
+ 3 * (1-t)*(1-t) * t * P1
+ 3 * (1-t) * t*t * P2
+ t*t*t * P3
}
// The slope of the Bezier function at t
func bezierPrime(_ t: CGFloat, _ P0: CGFloat, _ P1: CGFloat, _ P2: CGFloat, _ P3: CGFloat) -> CGFloat {
0
- 3 * (1-t)*(1-t) * P0
+ (3 * (1-t)*(1-t) * P1) - (6 * t * (1-t) * P1)
- (3 * t*t * P2) + (6 * t * (1-t) * P2)
+ 3 * t*t * P3
}
extension CGPoint {
func distance(to other: CGPoint) -> CGFloat {
let dx = x - other.x
let dy = y - other.y
return hypot(dx, dy)
}
}
Run Code Online (Sandbox Code Playgroud)
t*t*t比使用该pow函数快得多,这就是代码以这种方式编写的原因。这些函数将被大量调用,因此它们需要相当快。
然后是视图本身:
class PathTextView: UIView { ... }
Run Code Online (Sandbox Code Playgroud)
首先它包括控制点和文本:
var P0 = CGPoint.zero
var P1 = CGPoint.zero
var P2 = CGPoint.zero
var P3 = CGPoint.zero
var text: NSAttributedString {
get { textStorage }
set {
textStorage.setAttributedString(newValue)
locations = (0..<layoutManager.numberOfGlyphs).map { [layoutManager] glyphIndex in
layoutManager.location(forGlyphAt: glyphIndex)
}
lineFragmentOrigin = layoutManager
.lineFragmentRect(forGlyphAt: 0, effectiveRange: nil)
.origin
}
}
Run Code Online (Sandbox Code Playgroud)
每次更改文本时,layoutManager 都会重新计算所有字形的位置。我们稍后将调整这些值以适应曲线,但这些是基线。位置是每个字形相对于片段原点的位置,这也是我们需要跟踪的原因。
一些零碎的事情:
private let layoutManager = NSLayoutManager()
private let textStorage = NSTextStorage()
private var locations: [CGPoint] = []
private var lineFragmentOrigin = CGPoint.zero
init() {
textStorage.addLayoutManager(layoutManager)
super.init(frame: .zero)
backgroundColor = .clear
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
Run Code Online (Sandbox Code Playgroud)
Bezier 函数实际上是一个一维函数。为了在二维中使用它,我们调用它两次,一次用于 x,一次用于 y,类似地计算每个点的旋转。
func getPoint(forOffset t: CGFloat) -> CGPoint {
CGPoint(x: bezier(t, P0.x, P1.x, P2.x, P3.x),
y: bezier(t, P0.y, P1.y, P2.y, P3.y))
}
func getAngle(forOffset t: CGFloat) -> CGFloat {
let dx = bezierPrime(t, P0.x, P1.x, P2.x, P3.x)
let dy = bezierPrime(t, P0.y, P1.y, P2.y, P3.y)
return atan2(dy, dx)
}
Run Code Online (Sandbox Code Playgroud)
最后一件内务,是时候深入了解真正的功能了。我们需要一种方法来计算我们必须改变多少“t”(偏移量)才能沿着路径移动一定的距离。我不相信有任何简单的方法来计算这个,所以我们迭代来近似它。
// Simplistic routine to find the offset along Bezier that is
// aDistance away from aPoint. anOffset is the offset used to
// generate aPoint, and saves us the trouble of recalculating it
// This routine just walks forward until it finds a point at least
// aDistance away. Good optimizations here would reduce the number
// of guesses, but this is tricky since if we go too far out, the
// curve might loop back on leading to incorrect results. Tuning
// kStep is good start.
func getOffset(atDistance distance: CGFloat, from point: CGPoint, offset: CGFloat) -> CGFloat {
let kStep: CGFloat = 0.001 // 0.0001 - 0.001 work well
var newDistance: CGFloat = 0
var newOffset = offset + kStep
while newDistance <= distance && newOffset < 1.0 {
newOffset += kStep
newDistance = point.distance(to: getPoint(forOffset: newOffset))
}
return newOffset
}
Run Code Online (Sandbox Code Playgroud)
好,终于!是时候画点东西了。
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
var offset: CGFloat = 0.0
var lastGlyphPoint = P0
var lastX: CGFloat = 0.0
// Compute location for each glyph, transform the context, and then draw
for (index, location) in locations.enumerated() {
context.saveGState()
let distance = location.x - lastX
offset = getOffset(atDistance: distance, from: lastGlyphPoint, offset: offset)
let glyphPoint = getPoint(forOffset: offset)
let angle = getAngle(forOffset: offset)
lastGlyphPoint = glyphPoint
lastX = location.x
context.translateBy(x: glyphPoint.x, y: glyphPoint.y)
context.rotate(by: angle)
// The "at:" in drawGlyphs is the origin of the line fragment. We've already adjusted the
// context, so take that back out.
let adjustedOrigin = CGPoint(x: -(lineFragmentOrigin.x + location.x),
y: -(lineFragmentOrigin.y + location.y))
layoutManager.drawGlyphs(forGlyphRange: NSRange(location: index, length: 1),
at: adjustedOrigin)
context.restoreGState()
}
}
Run Code Online (Sandbox Code Playgroud)
有了它,您可以沿着任何三次贝塞尔曲线绘制文本。
这不处理任意 CGPaths。它明确用于三次贝塞尔。调整它以沿着任何其他类型的路径(四边形曲线、圆弧、直线,甚至圆角矩形)工作非常简单。然而,处理多元素路径会带来更多的复杂性。
有关使用 SwiftUI 的完整示例,请参阅CurvyText。
| 归档时间: |
|
| 查看次数: |
272 次 |
| 最近记录: |