ioscore-animationtransformretina-displaycatextlayer

CATextLayer blurry text after rotation


I have problem related with question
I've set contentsScale and after that text looking good but if I apply the 3d rotation transformation the text comes out blurry.

image here

initialization code

    // init text
    textLayer_ = [CATextLayer layer];
    …
    textLayer_.contentsScale = [[UIScreen mainScreen] scale];

    // init body path
    pathLayer_ = [CAShapeLayer layer];
    …
    [pathLayer_ addSublayer:textLayer_];

rotation code

    // make the mirror
    pathLayer_.transform = CATransform3DRotate(pathLayer_.transform, M_PI, 0, 1, 0);
    textLayer_.transform = CATransform3DRotate(textLayer_.transform, M_PI, 0, 1, 0);
    [textLayer_ setNeedsDisplay];

For test i've rotated text separately during the initialization.

    // init text
    textLayer_ = [CATextLayer layer];
    …
    textLayer_.transform = CATransform3DRotate(textLayer_.transform, M_PI, 0, 1, 0);
    textLayer_.contentsScale = [[UIScreen mainScreen] scale];

Text can be rotated and remains clear
image here


Solution

  • Rasterizing

    What's probably happening here is that it decides it has to render the textLayer to pixels. Note the warning for shouldRasterize in the CALayer Class Reference:

    When the value of this property is NO, the layer is composited directly into the destination whenever possible. The layer may still be rasterized prior to compositing if certain features of the compositing model (such as the inclusion of filters) require it.

    So, CATextLayer may suddenly decide to rasterize. It decides to rasterize if it's a sublayer of a rotated layer. So, don't make that happen.

    Single-Sided Layers

    That takes you back to your solution that causes the reversed text. You can prevent this by turning off doubleSided on the text layers. Your signs will now be blank on the far side, so add a second text layer, rotated 180 degrees relative to the first.

    Declare two text layers:

    @property (retain) CAShapeLayer *pathLayer;
    @property (retain) CATextLayer *textLayerFront;
    @property (retain) CATextLayer *textLayerBack;
    

    Then, initialize them to be single-sided, with the back layer rotated 180 degrees:

    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    // Also need to store a UIBezierPath in the pathLayer.
    
    CATextLayer *textLayerFront = [CATextLayer layer];
    textLayerFront.doubleSided = NO;
    textLayerFront.string = @"Front";
    textLayerFront.contentsScale = [[UIScreen mainScreen] scale];
    
    CATextLayer *textLayerBack = [CATextLayer layer];
    textLayerBack.doubleSided = NO;
    // Eventually both sides will have the same text, but for demonstration purposes we will label them differently.
    textLayerBack.string = @"Back";
    // Rotate the back layer 180 degrees relative to the front layer.
    textLayerBack.transform = CATransform3DRotate(textLayerBack.transform, M_PI, 0, 1, 0);
    textLayerBack.contentsScale = [[UIScreen mainScreen] scale];
    
    // Make all the layers siblings.  These means they must all be rotated independently of each other.
    
    // The layers can flicker if their Z position is close to the background, so move them forward.
    // This will not work if the main layer has a perspective transform on it.
    textLayerFront.zPosition = 256;
    textLayerBack.zPosition = 256;
    
    // It would make sense to make the text layers siblings of the path layer, but this seems to mean they get pre-rendered, blurring them.
    [self.layer addSublayer:pathLayer];
    [self.layer addSublayer:textLayerBack];
    [self.layer addSublayer:textLayerFront];
    
    // Store the layers constructed at this time for later use.
    [self setTextLayerFront:textLayerFront];
    [self setTextLayerBack:textLayerBack];
    [self setPathLayer:pathLayer];
    

    You can then rotate the layers. They will appear correct as long as you always rotate by the same amount.

    CGFloat angle = M_PI;
    self.pathLayer.transform = CATransform3DRotate(self.pathLayer.transform, angle, 0, 1, 0);
    self.textLayerFront.transform = CATransform3DRotate(self.textLayerFront.transform, angle, 0, 1, 0);
    self.textLayerBack.transform = CATransform3DRotate(self.textLayerBack.transform, angle, 0, 1, 0);
    

    You should then find that you can rotate your sign to any angle while the text remains sharp.

    Rotating sign with text on each side

    Text to Path

    There is an alternative, if you really need to manipulate your text display in ways that cause CATextLayer to rasterize: convert the text to a UIBezierPath representation. This can then be placed in a CAShapeLayer. Doing so requires delving deep into Core Text, but the results are powerful. For example, you can animate the text being drawn.

    // - (UIBezierPath*) bezierPathWithString:(NSString*) string font:(UIFont*) font inRect:(CGRect) rect;
    // Requires CoreText.framework
    // This creates a graphical version of the input screen, line wrapped to the input rect.
    // Core Text involves a whole hierarchy of objects, all requiring manual management.
    - (UIBezierPath*) bezierPathWithString:(NSString*) string font:(UIFont*) font inRect:(CGRect) rect;
    {
        UIBezierPath *combinedGlyphsPath = nil;
        CGMutablePathRef combinedGlyphsPathRef = CGPathCreateMutable();
        if (combinedGlyphsPathRef)
        {
            // It would be easy to wrap the text into a different shape, including arbitrary bezier paths, if needed.
            UIBezierPath *frameShape = [UIBezierPath bezierPathWithRect:rect];
    
            // If the font name wasn't found while creating the font object, the result is a crash.
            // Avoid this by falling back to the system font.
            CTFontRef fontRef;
            if ([font fontName])
                fontRef = CTFontCreateWithName((__bridge CFStringRef) [font fontName], [font pointSize], NULL);
            else if (font)
                fontRef = CTFontCreateUIFontForLanguage(kCTFontUserFontType, [font pointSize], NULL);
            else
                fontRef = CTFontCreateUIFontForLanguage(kCTFontUserFontType, [UIFont systemFontSize], NULL);
    
            if (fontRef)
            {
                CGPoint basePoint = CGPointMake(0, CTFontGetAscent(fontRef));
                CFStringRef keys[] = { kCTFontAttributeName };
                CFTypeRef values[] = { fontRef };
                CFDictionaryRef attributesRef = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values,
                                                                   sizeof(keys) / sizeof(keys[0]), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    
                if (attributesRef)
                {
                    CFAttributedStringRef attributedStringRef = CFAttributedStringCreate(NULL, (__bridge CFStringRef) string, attributesRef);
    
                    if (attributedStringRef)
                    {
                        CTFramesetterRef frameSetterRef = CTFramesetterCreateWithAttributedString(attributedStringRef);
    
                        if (frameSetterRef)
                        {
                            CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetterRef, CFRangeMake(0,0), [frameShape CGPath], NULL);
    
                            if (frameRef)
                            {
                                CFArrayRef lines = CTFrameGetLines(frameRef);
                                CFIndex lineCount = CFArrayGetCount(lines);
                                CGPoint lineOrigins[lineCount];
                                CTFrameGetLineOrigins(frameRef, CFRangeMake(0, lineCount), lineOrigins);
    
                                for (CFIndex lineIndex = 0; lineIndex<lineCount; lineIndex++)
                                {
                                    CTLineRef lineRef = CFArrayGetValueAtIndex(lines, lineIndex);
                                    CGPoint lineOrigin = lineOrigins[lineIndex];
    
                                    CFArrayRef runs = CTLineGetGlyphRuns(lineRef);
    
                                    CFIndex runCount = CFArrayGetCount(runs);
                                    for (CFIndex runIndex = 0; runIndex<runCount; runIndex++)
                                    {
                                        CTRunRef runRef = CFArrayGetValueAtIndex(runs, runIndex);
    
                                        CFIndex glyphCount = CTRunGetGlyphCount(runRef);
                                        CGGlyph glyphs[glyphCount];
                                        CGSize glyphAdvances[glyphCount];
                                        CGPoint glyphPositions[glyphCount];
    
                                        CFRange runRange = CFRangeMake(0, glyphCount);
                                        CTRunGetGlyphs(runRef, CFRangeMake(0, glyphCount), glyphs);
                                        CTRunGetPositions(runRef, runRange, glyphPositions);
    
                                        CTFontGetAdvancesForGlyphs(fontRef, kCTFontDefaultOrientation, glyphs, glyphAdvances, glyphCount);
    
                                        for (CFIndex glyphIndex = 0; glyphIndex<glyphCount; glyphIndex++)
                                        {
                                            CGGlyph glyph = glyphs[glyphIndex];
    
                                            // For regular UIBezierPath drawing, we need to invert around the y axis.
                                            CGAffineTransform glyphTransform = CGAffineTransformMakeTranslation(lineOrigin.x+glyphPositions[glyphIndex].x, rect.size.height-lineOrigin.y-glyphPositions[glyphIndex].y);
                                            glyphTransform = CGAffineTransformScale(glyphTransform, 1, -1);
    
                                            CGPathRef glyphPathRef = CTFontCreatePathForGlyph(fontRef, glyph, &glyphTransform);
                                            if (glyphPathRef)
                                            {
                                                // Finally carry out the appending.
                                                CGPathAddPath(combinedGlyphsPathRef, NULL, glyphPathRef);
    
                                                CFRelease(glyphPathRef);
                                            }
    
                                            basePoint.x += glyphAdvances[glyphIndex].width;
                                            basePoint.y += glyphAdvances[glyphIndex].height;
                                        }
                                    }
                                    basePoint.x = 0;
                                    basePoint.y += CTFontGetAscent(fontRef) + CTFontGetDescent(fontRef) + CTFontGetLeading(fontRef);
                                }
    
                                CFRelease(frameRef);
                            }
    
                            CFRelease(frameSetterRef);
                        }
                        CFRelease(attributedStringRef);
                    }
                    CFRelease(attributesRef);
                }
                CFRelease(fontRef);
            }
            // Casting a CGMutablePathRef to a CGPathRef seems to be the only way to convert what was just built into a UIBezierPath.
            combinedGlyphsPath = [UIBezierPath bezierPathWithCGPath:(CGPathRef) combinedGlyphsPathRef];
    
            CGPathRelease(combinedGlyphsPathRef);
        }
        return combinedGlyphsPath;
    }
    

    Here is rotating outlined text, created with the method above. It was also possible to add perspective without the z positions of the text layers becoming apparent.

    Rotating sign outlined text and perspective