在iOS上绘制扭曲文本 [英] Drawing warped text on iOS

查看:139
本文介绍了在iOS上绘制扭曲文本的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

使用iOS 9和更高版本上可用的标准API,在绘制文本时如何获得翘曲效果(如下图所示)?

Using standard APIs available on iOS 9 and later, how can I achieve a warp effect (something like the following image) when drawing text?

我想这可能是通过本质上指定四个路径段,它们可以是贝塞尔曲线或直线段(通常可以在 CGPath UIBezierPath )来定义文本边界框的四个边缘的形状。

How I would imagine this might work is by specifying essentially four "path segments" which could be either Bézier curves or straight line segments (whatever single "elements" you can normally create within a CGPath or UIBezierPath) defining the shape of the four edges of the text's bounding box.

此文本不需要是可选的。它也可能是一幅图像,但是我希望找到一种用代码绘制它的方法,因此我们不必为每个本地化设置单独的图像。我希望得到一个使用CoreGraphics, NSString / NSAttributedString 绘图添加,UIKit / TextKit甚至CoreText的答案。我只是想在使用OpenGL或Metal之前先使用图像,但这并不意味着我仍然不会接受一个好的OpenGL或Metal答案,如果这实际上是唯一的方法。

This text doesn't need to be selectable. It may as well be an image, but I'm hoping to find a way to draw it in code, so we don't have to have separate images for each of our localizations. I'd love an answer that uses CoreGraphics, NSString/NSAttributedString drawing additions, UIKit/TextKit, or even CoreText. I'd just settle on using images before going as far as OpenGL or Metal, but that doesn't mean I wouldn't still accept a good OpenGL or Metal answer if it is literally the only way to do this.

推荐答案

您可以仅使用CoreText和CoreGraphics来实现此效果。

You can achieve this effect using only CoreText and CoreGraphics.

我能够使用很多近似技术来实现我使用近似法(通过CGPathCreateCopyByDashingPath)所做的大部分工作,从理论上讲,您可以用更聪明的数学代替。这样既可以提高性能,又可以使生成的路径更平滑。

I was able to achieve it using a lot of approximation techniques. Much of what I did using approximation (via CGPathCreateCopyByDashingPath), you could theoretically replace with more clever math. This could both increase performance and make the resultant path smoother.

基本上,您可以对顶线和基线路径进行参数化(或者像我所做的那样对参数化进行近似处理) 。 (您可以定义一个函数来获取沿路径给定百分比的点。)

Basically, you can parameterize the top line and baseline paths (or approximate the parameterization, as I've done). (You can define a function that gets the point at a given percentage along the path.)

CoreText可以将每个字形转换为CGPath。使用将字形路径上的每个点映射到文本行上的匹配百分比的函数,在每个字形路径上运行CGPathApply。一旦将点映射到水平百分比,就可以沿着由2个点定义的直线沿其顶线和基线按比例缩放它。根据线的长度和字形的高度沿该线缩放点,这将创建新的点。将每个缩放点保存到新的CGPath。填充该路径。

CoreText can convert each glyph into a CGPath. Run CGPathApply on each of the glyphs' paths with a function that maps each point along the path to the matching percentage along the line of text. Once you have the point mapped to a horizontal percentage, you can scale it along the line defined by the 2 points at that percentage along your top line and baseline. Scale the point along that line based on the length of the line vs the height of the glyph, and that creates your new point. Save each scaled point to a new CGPath. Fill that path.

我在每个字形上也使用了CGPathCreateCopyByDashingPath来创建足够的点,在这些点上我不必处理数学就可以弯曲一个长LineTo元素(例如)。这使数学运算更简单,但可能会使路径看起来有些参差不齐。要解决此问题,您可以将生成的图像传递到平滑过滤器(例如CoreImage),或将路径传递到可以平滑和简化路径的库。

I've used CGPathCreateCopyByDashingPath on each glyph as well to create enough points where I don't have to handle the math to curve a long LineTo element (for example). This makes the math more simple, but can leave the path looking a little jagged. To fix this, you could pass the resultant image into a smoothing filter (CoreImage for example), or pass the path to a library that can smooth and simplify the path.

(我本来只是尝试使用CoreImage变形滤镜来解决整个问题,但是效果从来没有完全产生正确的效果。)

(I did originally just try CoreImage distortion filters to solve the whole problem, but the effects never quite produced the right effect.)

这里是结果(请注意有些锯齿边缘):

Here is the result (note the slightly jagged edges from using approximation):





这是在两条线的每个百分比之间绘制的线:




Here it is with lines drawn between each percent of the two lines:

此处这就是我的工作方式(180行,滚动):

Here is how I made it work (180 lines, scrolls):

static CGPoint pointAtPercent(CGFloat percent, NSArray<NSValue *> *pointArray) {
    percent = MAX(percent, 0.f);
    percent = MIN(percent, 1.f);

    int floorIndex = floor(([pointArray count] - 1) * percent);
    int ceilIndex = ceil(([pointArray count] - 1) * percent);

    CGPoint floorPoint = [pointArray[floorIndex] CGPointValue];
    CGPoint ceilPoint = [pointArray[ceilIndex] CGPointValue];

    CGPoint midpoint = CGPointMake((floorPoint.x + ceilPoint.x) / 2.f, (floorPoint.y + ceilPoint.y) / 2.f);

    return midpoint;
}

static void applierSavePoints(void* info, const CGPathElement* element) {
    NSMutableArray *pointArray = (__bridge NSMutableArray*)info;
    // Possible to get higher resolution out of this with more point types,
    // or by using math to walk the path instead of just saving a bunch of points.
    if (element->type == kCGPathElementMoveToPoint) {
        [pointArray addObject:[NSValue valueWithCGPoint:element->points[0]]];
    }
}

static CGPoint warpPoint(CGPoint origPoint, CGRect pathBounds, CGFloat minPercent, CGFloat maxPercent, NSArray<NSValue*> *baselinePointArray, NSArray<NSValue*> *toplinePointArray) {

    CGFloat mappedPercentWidth = (((origPoint.x - pathBounds.origin.x)/pathBounds.size.width) * (maxPercent-minPercent)) + minPercent;
    CGPoint baselinePoint = pointAtPercent(mappedPercentWidth, baselinePointArray);
    CGPoint toplinePoint = pointAtPercent(mappedPercentWidth, toplinePointArray);

    CGFloat mappedPercentHeight = -origPoint.y/(pathBounds.size.height);

    CGFloat newX = baselinePoint.x + (mappedPercentHeight * (toplinePoint.x - baselinePoint.x));
    CGFloat newY = baselinePoint.y + (mappedPercentHeight * (toplinePoint.y - baselinePoint.y));

    return CGPointMake(newX, newY);
}

static void applierWarpPoints(void* info, const CGPathElement* element) {
    WPWarpInfo *warpInfo = (__bridge WPWarpInfo*) info;

    CGMutablePathRef warpedPath = warpInfo.warpedPath;
    CGRect pathBounds = warpInfo.pathBounds;
    CGFloat minPercent = warpInfo.minPercent;
    CGFloat maxPercent = warpInfo.maxPercent;
    NSArray<NSValue*> *baselinePointArray = warpInfo.baselinePointArray;
    NSArray<NSValue*> *toplinePointArray = warpInfo.toplinePointArray;

    if (element->type == kCGPathElementCloseSubpath) {
        CGPathCloseSubpath(warpedPath);
    }
    // Only allow MoveTo at the beginning. Keep everything else connected to remove the dashing.
    else if (element->type == kCGPathElementMoveToPoint && CGPathIsEmpty(warpedPath)) {
        CGPoint origPoint = element->points[0];
        CGPoint warpedPoint = warpPoint(origPoint, pathBounds, minPercent, maxPercent, baselinePointArray, toplinePointArray);
        CGPathMoveToPoint(warpedPath, NULL, warpedPoint.x, warpedPoint.y);
    }
    else if (element->type == kCGPathElementAddLineToPoint || element->type == kCGPathElementMoveToPoint) {
        CGPoint origPoint = element->points[0];
        CGPoint warpedPoint = warpPoint(origPoint, pathBounds, minPercent, maxPercent, baselinePointArray, toplinePointArray);
        CGPathAddLineToPoint(warpedPath, NULL, warpedPoint.x, warpedPoint.y);
    }
    else if (element->type == kCGPathElementAddQuadCurveToPoint) {
        CGPoint origCtrlPoint = element->points[0];
        CGPoint warpedCtrlPoint = warpPoint(origCtrlPoint, pathBounds, minPercent, maxPercent, baselinePointArray, toplinePointArray);
        CGPoint origPoint = element->points[1];
        CGPoint warpedPoint = warpPoint(origPoint, pathBounds, minPercent, maxPercent, baselinePointArray, toplinePointArray);
        CGPathAddQuadCurveToPoint(warpedPath, NULL, warpedCtrlPoint.x, warpedCtrlPoint.y, warpedPoint.x, warpedPoint.y);
    }
    else if (element->type == kCGPathElementAddCurveToPoint) {
        CGPoint origCtrlPoint1 = element->points[0];
        CGPoint warpedCtrlPoint1 = warpPoint(origCtrlPoint1, pathBounds, minPercent, maxPercent, baselinePointArray, toplinePointArray);
        CGPoint origCtrlPoint2 = element->points[1];
        CGPoint warpedCtrlPoint2 = warpPoint(origCtrlPoint2, pathBounds, minPercent, maxPercent, baselinePointArray, toplinePointArray);
        CGPoint origPoint = element->points[2];
        CGPoint warpedPoint = warpPoint(origPoint, pathBounds, minPercent, maxPercent, baselinePointArray, toplinePointArray);
        CGPathAddCurveToPoint(warpedPath, NULL, warpedCtrlPoint1.x, warpedCtrlPoint1.y, warpedCtrlPoint2.x, warpedCtrlPoint2.y, warpedPoint.x, warpedPoint.y);
    }
    else {
        NSLog(@"Error: Unknown Point Type");
    }
}

- (NSArray<NSValue *> *)pointArrayFromPath:(CGPathRef)path {
    NSMutableArray<NSValue*> *pointArray = [[NSMutableArray alloc] init];
    CGFloat lengths[2] = { 1, 0 };
    CGPathRef dashedPath = CGPathCreateCopyByDashingPath(path, NULL, 0.f, lengths, 2);
    CGPathApply(dashedPath, (__bridge void * _Nullable)(pointArray), applierSavePoints);
    CGPathRelease(dashedPath);
    return pointArray;
}

- (CGPathRef)createWarpedPathFromPath:(CGPathRef)origPath withBaseline:(NSArray<NSValue *> *)baseline topLine:(NSArray<NSValue *> *)topLine fromPercent:(CGFloat)startPercent toPercent:(CGFloat)endPercent {
    CGFloat lengths[2] = { 1, 0 };
    CGPathRef dashedPath = CGPathCreateCopyByDashingPath(origPath, NULL, 0.f, lengths, 2);

    // WPWarpInfo is just a class I made to hold some stuff.
    // I needed it to hold some NSArrays, so a struct wouldn't work.
    WPWarpInfo *warpInfo = [[WPWarpInfo alloc] initWithOrigPath:origPath minPercent:startPercent maxPercent:endPercent baselinePointArray:baseline toplinePointArray:topLine];

    CGPathApply(dashedPath, (__bridge void * _Nullable)(warpInfo), applierWarpPoints);
    CGPathRelease(dashedPath);

    return warpInfo.warpedPath;
}

- (void)drawRect:(CGRect)rect {
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    CGMutablePathRef toplinePath = CGPathCreateMutable();
    CGPathAddArc(toplinePath, NULL, 187.5, 210.f, 187.5, M_PI, 2 * M_PI, NO);
    NSArray<NSValue *> * toplinePoints = [self pointArrayFromPath:toplinePath];
    CGContextAddPath(ctx, toplinePath);
    CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
    CGContextStrokePath(ctx);
    CGPathRelease(toplinePath);

    CGMutablePathRef baselinePath = CGPathCreateMutable();
    CGPathAddArc(baselinePath, NULL, 170.f, 250.f, 50.f, M_PI, 2 * M_PI, NO);
    CGPathAddArc(baselinePath, NULL, 270.f, 250.f, 50.f, M_PI, 2 * M_PI, YES);
    NSArray<NSValue *> * baselinePoints = [self pointArrayFromPath:baselinePath];
    CGContextAddPath(ctx, baselinePath);
    CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
    CGContextStrokePath(ctx);
    CGPathRelease(baselinePath);


    // Draw 100 of the connecting lines between the strokes.
    /*for (int i = 0; i < 100; i++) {
        CGPoint point1 = pointAtPercent(i * 0.01, toplinePoints);
        CGPoint point2 = pointAtPercent(i * 0.01, baselinePoints);

        CGContextMoveToPoint(ctx, point1.x, point1.y);
        CGContextAddLineToPoint(ctx, point2.x, point2.y);

        CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
        CGContextStrokePath(ctx);
    }*/


    NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:@"WARP"];
    UIFont *font = [UIFont fontWithName:@"Helvetica" size:144];
    [attrString addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, [attrString length])];

    CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attrString);
    CFArrayRef runArray = CTLineGetGlyphRuns(line);
    // Just get the first run for this.
    CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, 0);
    CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);
    CGFloat fullWidth = (CGFloat)CTRunGetTypographicBounds(run, CFRangeMake(0, CTRunGetGlyphCount(run)), NULL, NULL, NULL);
    CGFloat currentOffset = 0.f;

    for (int curGlyph = 0; curGlyph < CTRunGetGlyphCount(run); curGlyph++) {
        CFRange glyphRange = CFRangeMake(curGlyph, 1);
        CGFloat currentGlyphWidth = (CGFloat)CTRunGetTypographicBounds(run, glyphRange, NULL, NULL, NULL);

        CGFloat currentGlyphOffsetPercent = currentOffset/fullWidth;
        CGFloat currentGlyphPercentWidth = currentGlyphWidth/fullWidth;
        currentOffset += currentGlyphWidth;

        CGGlyph glyph;
        CGPoint position;
        CTRunGetGlyphs(run, glyphRange, &glyph);
        CTRunGetPositions(run, glyphRange, &position);

        CGAffineTransform flipTransform = CGAffineTransformMakeScale(1, -1);

        CGPathRef glyphPath = CTFontCreatePathForGlyph(runFont, glyph, &flipTransform);
        CGPathRef warpedGylphPath = [self createWarpedPathFromPath:glyphPath withBaseline:baselinePoints topLine:toplinePoints fromPercent:currentGlyphOffsetPercent toPercent:currentGlyphOffsetPercent+currentGlyphPercentWidth];
        CGPathRelease(glyphPath);

        CGContextAddPath(ctx, warpedGylphPath);
        CGContextSetFillColorWithColor(ctx, [UIColor blackColor].CGColor);
        CGContextFillPath(ctx);

        CGPathRelease(warpedGylphPath);
    }

    CFRelease(line);
}

所包含的代码也远非完整。例如,我略过了CoreText的许多部分。具有后代的字形可以正常工作,但效果不佳。必须考虑如何处理这些问题。另外,我的字母间距很脏。

The included code is also far from "complete". For example, there are many parts of CoreText I skimmed over. Glyphs with descenders do work, but not well. Some thought would have to go into how to handle those. Also, my letter spacing is sloppy.

很显然,这不是一个简单的问题。我敢肯定,使用能够有效扭曲Bezier路径的第三方库,还有更好的方法。但是,出于智力研究的目的,看看是否可以在没有第三方库的情况下完成此操作,我认为这表明了它可以做到。

Clearly this is a non-trivial problem. I'm sure there are better ways to do this with 3rd party libraries capable of efficiently distorting Bezier paths. However, for the purposes of the intellectual exercise of seeing if it can be done without 3rd party libraries, I think this demonstrates that it can.

来源: https://developer.apple.com/library/mac/samplecode/CoreTextArcCocoa/ Introduction / Intro.html

来源: http://www.planetclegg.com/projects/WarpingTextToSplines.html

来源(用于使数学更聪明):及时获取路径位置

Source (for making math more clever): Get position of path at time

这篇关于在iOS上绘制扭曲文本的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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