Most performant way to draw text on a curve, and animate it?
Asked Answered
B

1

6

I'm guessing that it's to make a string out of individual CATextLayers and then position them as required along the curve, then animate. Because that's what's I've got working now, but it loses Kerning. Here's how:

Why isn't my curved text centering itself?

But is Core Text more performant and able to avoid the whole "drawing into a context" nonsense that slows everything down compared to the lean, mean Core Animation way of doing things, and respect kerning? i.e. avoiding drawRect: and all other aspects that greatly slow things down, as in this manner of drawing to the screen:

https://github.com/darcyliu/CocoaSampleCode/tree/master/CoreTextArcCocoa

Imagine a string of 200 characters, bent around a circle, with the ability to animate the spacing between characters, hopefully at a stable 60fps. This is possible with Core Animation, but that's by breaking the string up into individual characters and placing them around the circle with equal spacing, which causes a complete loss of kerning information.

I'm hoping for a way to do this without losing the kerning information and still being able to dynamically adjust spacing at 60fps.

Breakaway answered 28/8, 2014 at 19:8 Comment(0)
R
12

Sure, you can do that. With iOS 7, you don't need to go all the way down to Core Text, though. NSLayoutManager can handle it in many cases. See the CurvyText demo that I wrote for iOS:PTL. You can drag all the control points around and see the text layout along the curve.

To see just how fast this layout can get in pure Core Text and Core Animation, see the PinchText demo from Rich Text, Core Text. This one shows how to adjust Core Text layout to respond to multi-touch, so the text seems to bend towards your fingers. It includes examples of how to animate with Core Animation to get smooth adjustments (and even a small "splash" effect when you remove your finger).

I don't quite know what you mean by "the whole drawing into a context nonsense that slows everything down." I draw these into a context very, very quickly (and Core Animation also does a lot of drawing into contexts).

Bending text around a circle is easier than either of these demos. The trick is to calculate the points along your circle, and use those points to translate and rotate your context before asking the layout manager to draw the glyph. Here's an example drawText from CurvyTextView (which draws along a Bézier curve).

- (void)drawText {
  if ([self.attributedString length] == 0) { return; }

  NSLayoutManager *layoutManager = self.layoutManager;

  CGContextRef context = UIGraphicsGetCurrentContext();
  NSRange glyphRange;
  CGRect lineRect = [layoutManager lineFragmentRectForGlyphAtIndex:0
                                                    effectiveRange:&glyphRange];

  double offset = 0;
  CGPoint lastGlyphPoint = self.P0;
  CGFloat lastX = 0;
  for (NSUInteger glyphIndex = glyphRange.location;
       glyphIndex < NSMaxRange(glyphRange);
       ++glyphIndex) {
    CGContextSaveGState(context);

    CGPoint location = [layoutManager locationForGlyphAtIndex:glyphIndex];

    CGFloat distance = location.x - lastX;  // Assume single line
    offset = [self offsetAtDistance:distance
                          fromPoint:lastGlyphPoint
                          andOffset:offset];
    CGPoint glyphPoint = [self pointForOffset:offset];
    double angle = [self angleForOffset:offset];

    lastGlyphPoint = glyphPoint;
    lastX = location.x;

    CGContextTranslateCTM(context, glyphPoint.x, glyphPoint.y);
    CGContextRotateCTM(context, angle);

    [layoutManager drawGlyphsForGlyphRange:NSMakeRange(glyphIndex, 1)
                                   atPoint:CGPointMake(-(lineRect.origin.x + location.x),
                                                       -(lineRect.origin.y + location.y))];

    CGContextRestoreGState(context);
  }
}

The "magic" of this is in calculating the transforms you need, which is done in offsetAtDistance:fromPoint:andOffset:, pointForOffset: and angleForOffset:. Those routines are much simpler to write for a circle than a generic Bézier curve, so this is probably a very good starting point. Note that this code is not particularly optimized. It was designed for readability more than speed, but it is still very fast on an iPad 3. If you need it to be faster, there are several techniques, including a lot of pre-calculating that can be done.

The PinchText demo is in pure Core Text and Core Animation, and is quite a bit more complicated since it does all of its math in Accelerate (and really needs to). I doubt you need that since your layout problem isn't that complicated. Some straightforward C can probably calculate everything you need in plenty of time. But the PinchText demo does show how to let Core Animation manage transitions more beautifully. Look at addTouches:inView:scale::

- (void)addTouches:(NSSet *)touches inView:(UIView *)view scale:(CGFloat)scale
{
  for (UITouch *touch in touches) {
    TouchPoint *touchPoint = [TouchPoint touchPointForTouch:touch inView:view scale:scale];
    NSString *keyPath = [self touchPointScaleKeyPathForTouchPoint:touchPoint];

    CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:keyPath];
    anim.duration = kStartTouchAnimationDuration;
    anim.fromValue = @0;
    anim.toValue = @(touchPoint.scale);
    anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    [self addAnimation:anim forKey:keyPath];

    [self.touchPointForIdentifier setObject:touchPoint forKey:touchPoint.identifier];
  }
}

What's going on here is that it's animating the model data ("scale" here is "how much does this touch impact the layout;" it has nothing to do with transforms). needsDisplayForKey: indicates that when that model data structure is modified, the layer needs to redraw itself. And it completely recomputes and redraws itself into its context every frame. Done correctly, this can be incredibly fast.

This code should hopefully get you started. Not to overly push the book, but the CurvyText demo is discussed extensively in iOS Pushing the Limits chapter 21.

Roughshod answered 28/8, 2014 at 20:2 Comment(3)
This approach, of effectively drawing out a line of text and then using its spacing to determine the ratios of each glyph to one another on the curvature of the line is probably the ideal way to solve the placement of CATextLayers made up of each glyph, so will experiment with this and let you know how it works out. THANK YOU!!!Breakaway
This method of laying out text works best (for performance) with CATextLayer as the target. Thank you Rob!Breakaway
@rob can you check this #42383691Privacy

© 2022 - 2024 — McMap. All rights reserved.