是否可以在Swift中将UILabel或CATextLayer添加到CGPath,类似于Photoshop的路径类型? [英] Is it possible to add a UILabel or CATextLayer to a CGPath in Swift, similar to Photoshop's type to path feature?

查看:80
本文介绍了是否可以在Swift中将UILabel或CATextLayer添加到CGPath,类似于Photoshop的路径类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想添加文本,无论是 UILabel 还是 CATextLayer CGPath 。我意识到该功能背后的数学运算相当复杂,但是我想知道苹果是否提供了现成的功能,或者是否有开放源代码的SDK可以在Swift中实现。谢谢!

I would like to add text, whether it be a UILabel or CATextLayer to a CGPath. I realize that the math behind this feature is fairly complicated but wondering if Apple provides this feature out of the box or if there is an open-source SDK out there that makes this possible in Swift. Thanks!

示例:

推荐答案

您将需要手工执行此操作,方法是在您关心的每个点上计算Bezier函数及其斜率,然后在该点处绘制一个字形并旋转它。您需要知道4点(传统上称为P0-P3)。 P0是曲线的起点。 P1和P2是控制点。 P3是曲线的终点。

You'll need to do this by hand, by computing the Bezier function and its slope at each point you care about, and then drawing a glyph at that point and rotation. You'll need to know 4 points (traditionally called P0-P3). P0 is the starting point of the curve. P1 and P2 are the control points. And P3 is the ending point in the curve.

定义Bezier函数,以便随着 t参数从0到1的移动,输出将跟踪所需的曲线。在此重要的是要知道 t不是线性的。 t = 0.25不一定表示沿曲线的1/4。 (实际上,这几乎是不可能的。)这意味着在曲线上测量距离很棘手。

The Bezier function is defined such that as the "t" parameter moves from 0 to 1, the output will trace the desired curve. It's important to know here that "t" is not linear. t=0.25 does not necessarily mean "1/4 of the way along the curve." (In fact, that's almost never true.) This means that measuring distances long the curve is a little tricky. But we'll cover that.

首先,您需要CGPoint的核心功能和有用的扩展:

First, you'll need the core functions and a helpful extension on 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)
    }
}

t * t * t 比使用 pow 函数,这就是为什么这样编写代码的原因。这些函数将被大量调用,因此它们需要相当快。

t*t*t is dramatically faster than using the pow function, which is why the code is written this way. These functions will be called a lot, so they need to be reasonably fast.

然后是视图本身:

class PathTextView: UIView { ... }

首先,它包括控制点和文本:

First it includes the control points, and the text:

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
    }
}

每次更改文本时,layoutManager都会重新计算所有字形。稍后我们将调整这些值以适合曲线,但这只是基线。位置是每个字形相对于片段原点的位置,这就是为什么我们也需要对其进行跟踪的原因。

Every time the text is changed, the layoutManager recomputes the locations of all of the glyphs. We'll later adjust those values to fit the curve, but these are the baseline. The positions are the positions of each glyph relative to the fragment origin, which is why we need to keep track of that, too.

有些零碎:

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") }

贝塞尔函数实际上是一维函数。为了在二维中使用它,我们将其称为两次,一次用于x,一次用于y,并类似地计算每个点的旋转。

The Bezier function is actually a one-dimensional function. In order to use it in two dimensions, we call it twice, once for x and once for y, and similarly to compute the rotations at each point.

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)
}

最后一项内务处理,是时候深入了解真正的功能了。我们需要一种方法来计算必须更改 t(偏移量)以沿路径移动一定距离的方式。我不相信有任何简单的方法可以计算出来,因此我们迭代近似。

One last piece of housekeeping, and it'll be time to dive into the real function. We need a way to compute how much we must change "t" (the offset) in order to move a certain distance along the path. I do not believe there is any simple way to compute this, so instead we iterate to approximate it.

// 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
}

最后!是时候画些东西了。

OK, finally! Time to draw something.

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()
    }
}

因此,您可以沿任意立方贝塞尔曲线绘制文本。

And with that you can draw text along any cubic Bezier.

这不能处理任意CGPath。专门针对立方贝塞尔曲线。对其进行调整以使其沿着任何其他类型的路径(四边形曲线,圆弧,直线,甚至是圆角的矩形)都非常简单明了。但是,处理多元素路径会增加很多复杂性。

This doesn't handle arbitrary CGPaths. It's explicitly for cubic Bezier. It's pretty straightforward to adjust this to work along any of the other types of paths (quad curves, arcs, lines, and even rounded rects). However, dealing with multi-element paths opens up a lot more complexity.

有关使用SwiftUI的完整示例,请参见 CurvyText

For a complete example using SwiftUI, see CurvyText.

这篇关于是否可以在Swift中将UILabel或CATextLayer添加到CGPath,类似于Photoshop的路径类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆