Drawing garbled text on iOS

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

warped text

How could I suggest that this might work is to define essentially four “path segments” that can be either Bezier curves or straight line segments (any individual “elements” that you can usually create within CGPath or UIBezierPath ) , defining the shape of the four edges of the text bounding box.

This text does not need to be selected. It may be an image, but I hope to find a way to make it in the code, so we do not need to have separate images for each of our localizations. I would really like the answer that uses the additions of CoreGraphics, NSString / NSAttributedString , UIKit / TextKit or even CoreText. I would just decide to use images before moving on to OpenGL or Metal, but that does not mean that I would not accept a good answer from OpenGL or Metal if this is the only way to do this.

+7
ios uikit core-graphics
source share
1 answer

You can achieve this effect using only CoreText and CoreGraphics.

I was able to achieve this using a variety of approximation methods. Most of what I used with the approximation (via CGPathCreateCopyByDashingPath) could theoretically be replaced with smarter math. This can both increase productivity and make the resulting path smoother.

In principle, you can parameterize the top and base paths (or zoom in the parameterization, as I did). (You can define a function that gets a point in a given percentage along the way.)

CoreText can convert each glyph to CGPath. Run CGPathApply on each of the glyph paths using a function that maps each point along the path to the appropriate percentage along the line of text. Once you have a point with a horizontal percentage, you can scale it along the line defined by two points in this percentage along the top line and the baseline. Scale the point along this line depending on the line length and glyph height, and this will create a new point. Save each scaled point in the new CGPath. Fill this way.

I used CGPathCreateCopyByDashingPath for each glyph, and also to create enough points where I don't need to handle the math for the curve of a long LineTo element (for example). This makes math easier, but can leave the path a little jagged. To fix this, you can transfer the resulting image to a smoothing filter (for example, CoreImage) or pass the path to the library, which can smooth and simplify the path.

(First I tried CoreImage distortion filters to solve the whole problem, but the effects never gave the right effect.)

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




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

Here's how I earned (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); } 

The incoming code is also far from "complete." For example, there are many parts of CoreText that I have been looking through. Characters with descenders work, but not very well. Some thought it would be necessary to figure out how to deal with them. Also, my spacing between letters is messy.

Clearly, this is a non-trivial task. I am sure there are better ways to do this with third-party libraries that can effectively distort Bezier's paths. However, in order to intellectually examine whether this can be done without third-party libraries, I think this demonstrates that this is possible.

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

Source: http://www.planetclegg.com/projects/WarpingTextToSplines.html

Source (to enhance mathematics): Get the position of the path in time

+4
source share

All Articles