How do I draw an NSString at an angle? - cocoa

Is there a set of string attributes I can specify that will draw the text at an angle when I call:
[label drawAtPoint:textStart withAttributes:attributes];

Here's an example that uses a transform to rotate the drawing context. Essentially it's just like setting a color or shadow, just make sure to use -concat instead of -set.
CGFloat rotateDeg = 4.0f;
NSAffineTransform *rotate = [[NSAffineTransform alloc] init];
[rotate rotateByDegrees:rotateDeg];
[rotate concat];
// Lock focus if needed and draw strings, images here.
[rotate release];

NSString itself doesn't have rotation, but you can rotate the context. The string will always be drawn "horizontally" as far as the coordinate space goes, but what actual direction that corresponds to depends on the context. Just use NSAffineTransform to spin it as needed.

Related

Apply NSAffineTransform to NSView

I want to do a couple of simple transforms on an NSView subclass to flip it on the X axis, the Y axis, or both. I am an experienced iOS developer but I just can't figure out how to do this in macOS. I have created an NSAffineTransform with the required translations and scales, but cannot determine how to actually apply this to the NSView. The only property I can find which will accept any kind of transform is [[NSView layer] transform], but this requires a CATransform3D.
The only success I have had is using the transform to flip the image if an NSImageView, by calling lockFocus on a new, empty NSImage, creating the transform, then drawing the unflipped image inside the locked image. This is far from satisfactory however, as it does not handle any subviews and is presumably more costly than applying the transform directly to the NSView/NSImageView.
This was the solution:
- (void)setXScaleFactor:(CGFloat)xScaleFactor {
_xScaleFactor = xScaleFactor;
[self setNeedsDisplay];
}
- (void)setYScaleFactor:(CGFloat)yScaleFactor {
_yScaleFactor = yScaleFactor;
[self setNeedsDisplay];
}
- (void)drawRect:(NSRect)dirtyRect {
NSAffineTransform *transform = [[NSAffineTransform alloc] init];
[transform scaleXBy:self.xScaleFactor yBy:self.yScaleFactor];
[transform set];
[super drawRect:dirtyRect];
}
Thank you to l'L'l for the hint about using NSGraphicsContext.
I can't believe how many hours of searching and experimenting I had to do before I was able to simply flip an image horizontally in AppKit. I cannot upvote this question and mashers' answer enough.
Here is an updated version of my solution for swift to flip an image horizontally. This method is implemented in an NSImageView subclass.
override func draw(_ dirtyRect: NSRect) {
// NSViews are not backed by CALayer by default in AppKit. Must request a layer
self.wantsLayer = true
if self.flippedHoriz {
// If a horizontal flip is desired, first multiple every X coordinate by -1. This flips the image, but does it around the origin (lower left), not the center
var trans = AffineTransform(scaledByX: -1, byY: 1)
// Add a transform that moves the image by the width so that its lower left is at the origin
trans.append(AffineTransform(translationByX: self.frame.size.width, byY: 0)
// AffineTransform is bridged to NSAffineTransform, but it seems only NSAffineTransform has the set() and concat() methods, so convert it and add the transform to the current graphics context
(trans as NSAffineTransform).concat()
}
// Don't be fooled by the Xcode placehoder. This must be *after* the above code
super.draw(dirtyRect)
}
The behavior of the transforms also took a bit of experimentation to understand. The help for NSAffineTransform.set() explains:
it removes the existing transformation matrix, which is an accumulation of transformation matrices for the screen, window, and any superviews.
This will very likely break something. Since I wanted to still respect all the transformations applied by the window and superviews, the concat() method is more appropriate.
concat() multiplies the existing transform matrix by your custom transform. This is not exactly cumulative, though. Each time draw is called, your transform is applied to the original transform for the view. So repeatedly calling draw doesn't continuously flip the image. Because of this, to not flip the image, simply don't apply the transform.

CATextLayer gets rasterized too early and it is blurred

I have some troubles with CATextLayer, that could be due to me, but I didn't find any help on this topic. I am on OS X (on iOS it should be the same).
I create a CATextLayer layers with scale factor > 1 and what I get is a blurred text. The layer is rasterized before applying the scale, I think. Is this the expected behavior? I hope it is not, because it just makes no sense... A CAShapeLayer is rasterized after that its transformation matrix is applied, why the CATextLayer should be different?
In case I am doing something wrong... what is it??
CATextLayer *layer = [CATextLayer layer];
layer.string = #"I like what I am doing";
layer.font = (__bridge CFTypeRef)[NSFont systemFontOfSize:24];
layer.fontSize = 24;
layer.anchorPoint = CGPointZero;
layer.frame = CGRectMake(0, 0, 400, 100);
layer.foregroundColor = [NSColor blackColor].CGColor;
layer.transform = CATransform3DMakeScale(2., 2., 1.);
layer.shouldRasterize = NO;
[self.layer addSublayer:layer];
The solution I use at the moment is to set the contentsScale property of the layer to the scale factor. The problem is that this solution doesn't scale: if the scale factor of any of the parent layers changes, then contentsScale should be updated too. I should write code to traverse the layers tree to update the contentsScale properties of all CATextLayers... not exactly what I would like to do.
Another solution, that is not really a solution, is to convert the text to a shape and use a CAShapeLayer. But then I don't see the point of having CATextLayers.
A custom subclass of CALayer could help in solving this problem?
EDIT: Even CAGradientLayer is able to render its contents, like CAShapeLayer, after that its transformation matrix is applied. Can someone explain how it is possible?
EDIT 2: My guess is that paths and gradients are rendered as OpenGL display lists, so they are rasterized at the actual size on the screen by OpenGL itself. Texts are rasterized by Core Animation, so they are bitmaps for OpenGL.
I think that I will go with the contentsScale solution for the moment. Maybe, in the future, I will convert texts to shapes. In order to get best results with little work, this is the code I use now:
[CATransaction setDisableActions:YES];
CGFloat contentsScale = ceilf(scaleOfParentLayer);
// _scalableTextLayer is a CATextLayer
_scalableTextLayer.contentsScale = contentsScale;
[_scalableTextLayer displayIfNeeded];
[CATransaction setDisableActions:NO];
After trying all the approaches, the solution I am using now is a custom subclass of CALayer. I don't use CATextLayer at all.
I override the contentsScale property with this custom setter method:
- (void)setContentsScale:(CGFloat)cs
{
CGFloat scale = MAX(ceilf(cs), 1.); // never less than 1, always integer
if (scale != self.contentsScale) {
[super setContentsScale:scale];
[self setNeedsDisplay];
}
}
The value of the property is always rounded to the upper integer value. When the rounded value changes, then the layer must be redrawn.
The display method of my CALayer subclass creates a bitmap image of the size of the text multiplied by the contentsScale factor and by the screen scale factor.
- (void)display
{
CGFloat scale = self.contentsScale * [MyUtils screenScale];
CGFloat width = self.bounds.size.width * scale;
CGFloat height = self.bounds.size.height * scale;
CGContextRef bitmapContext = [MyUtils createBitmapContextWithSize:CGSizeMake(width, height)];
CGContextScaleCTM(bitmapContext, scale, scale);
CGContextSetShouldSmoothFonts(bitmapContext, 0);
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)(_text));
CGContextSetTextPosition(bitmapContext, 0., self.bounds.size.height-_ascender);
CTLineDraw(line, bitmapContext);
CFRelease(line);
CGImageRef image = CGBitmapContextCreateImage(bitmapContext);
self.contents = (__bridge id)(image);
CGImageRelease(image);
CGContextRelease(bitmapContext);
}
When I change the scale factor of the root layer of my hierarchy, I loop on all text layers and set the contentsScale property to the same factor. The display method is called only if the rounded value of the scale factor changes (i.e. if the previous value was 1.6 and now I set 1.7, nothing happens. But if the new value is 2.1, then the layer is redisplayed).
The cost in terms of speed of the redraw is little. My test is to change continuously the scale factor of a hierarchy of 40 text layers on an 3rd gen. iPad. It works like butter.
CATextLayer is different because the underlying CoreText renders the glyphs with the specified font size (educated guess based on experiments).
You could add an action to the parent layer so as soon as it's scale changes, it changes the font size of the text layer.
Blurriness could also come from misaligned pixels. That can happen if you put the text layer to non integral position or any transformation in the superlayer hierarchy.
Alternatively you could subclass CALayer and then draw the text using Cocoa in drawInContext:
see example here:
http://lists.apple.com/archives/Cocoa-dev/2009/Jan/msg02300.html
http://people.omnigroup.com/bungi/TextDrawing-20090129.zip
If you want to have the exact behaviour of a CAShapeLayer then you will need to convert your string into a bezier path and have CAShapeLayer render it. It's a bit of work but then you will have the exact behaviour you are looking for. An alternate approach, is to scale the fontSize instead. This yields crisp text every time but it might not fit to you exact situation.
To draw text as CAShapeLayer have a look at Apple Sample Code "CoreAnimationText":
http://developer.apple.com/library/mac/#samplecode/CoreAnimationText/Listings/Readme_txt.html

How can I animate a UIBezierPath drawing to appear slowly as opposed to right away?

I'm writing an app (XCode 4.6) that displays drawings of various different paths - for now it's just straight lines and bezier paths, but eventually it will get more complicated. I am currently not using any layers and the display is actually pretty simple.
my drawRect code looks like this:
- (void)drawRect:(CGRect)rect :(int) points :(drawingTypes) type //:(Boolean) initial
{
//CGRect bounds = [[UIScreen mainScreen] bounds];
CGRect appframe= [[UIScreen mainScreen] applicationFrame];
CGContextRef context = UIGraphicsGetCurrentContext();
_helper = [[Draw2DHelper alloc ] initWithBounds :appframe.size.width :appframe.size.height :type];
CGPoint startPoint = [_helper generatePoint] ;
[_uipath moveToPoint:startPoint];
[_uipath setLineWidth: 1.5];
CGContextSetStrokeColorWithColor(context, [UIColor lightGrayColor].CGColor);
CGPoint center = CGPointMake(self.center.y, self.center.x) ;
[_helper createDrawing :type :_uipath :( (points>0) ? points : defaultPointCount) :center];
[_uipath stroke];
}
- (void)drawRect:(CGRect)rect
{
if (_uipath == NULL)
_uipath = [[UIBezierPath alloc] init];
else
[_uipath removeAllPoints];
[self drawRect:rect :self.graphPoints :self.drawingType ];
}
The actual path is generated by a helper object (_helper). I would like to animate the display of this path to appear slowly over a few seconds as it is being drawn - what is the easiest and fastest way of doing this?
What do you mean "appear slowly?" I assume you do not mean fade in all over at once, but rather mean that you want it to look like the path is being drawn with a pen?
To do that, do the following:
Create a CAShapeLayer the same size as your view and add it as a sublayer of your view.
Get the CGPath from your bezier path.
Install the CGPath into your shapeLayer.
Create a CABasicAnimation that animates the value of the shape layer's strokeEnd property from 0.0 to 1.0. That will cause the shape to be drawn as if it was being traced from beginning to end. You probably want a path that contains a single, contiguous sub-path. If you want it to circle back and connect, make it a closed path.
There are all kinds of cool tricks you can do with shape layers and animating changes to the path. and it's settings (like strokeStart and strokeEnd, stokeColor, fillColor, etc.) If you animate the path itself, you have to make sure the beginning path and ending path have the same number of control points internally. (And path arcs are tricky because the path has a different number of control points depending on the angle of the arc.)

Draw NSView background partially, with a gradient

I have a NSView, subclassed, with custom background drawing, filling it with a gradient.
In IB, I've put a checkbox on it, somewhere in the middle.
This is the drawRect method.
-(void) drawRect:(NSRect)dirtyRect {
CGFloat sc = 0.9f;
CGFloat ec = 0.6f;
NSColor* startingColor = [NSColor colorWithDeviceRed:sc green:sc blue:sc alpha:1];
NSColor* endingColor = [NSColor colorWithDeviceRed:ec green:ec blue:ec alpha:1];
NSGradient *grad = [[NSGradient alloc] initWithStartingColor:startingColor endingColor:endingColor];
[grad drawInRect:dirtyRect angle:270];
}
What happens is, this same method gets called to draw the whole view area first and then for the part, where NSButton (checkbox) lies on top of it. OF course the checkbox background is drawn with a complete gradient and it is not right, since the portion is much smaller. The same happens with other controls I put on the said NSView.
What is the suggested approach on such thing?
One option is to make controls height the same as the views' but this will result in problems in the future.
The answer is, always draw the WHOLE area of the view, not just the dirtyRect
[grad drawInRect:[self bounds] angle:270];

cocoa: Read pixel color of NSImage

I have an NSImage. I would like to read the NSColor for a pixel at some x and y. Xcode seems to thing that there is a colorAtX:y: method on NSImage, but this causes a crash saying that there is no such method for NSImage. I have seen some examples where you create an NSBitmapImageRep and call the same method on that, but I have not been able to successfully convert my NSImage to an NSBitmapImageRep. The pixels on the NSBitmapImageRep are different for some reason.
There must be a simple way to do this. It cannot be this complicated.
Without seeing your code it's difficult to know what's going wrong.
You can draw the image to an NSBitmapImageRep using the initWithData: method and pass in the image's TIFFRepresentation.
You can then get the pixel value using the method colorAtX:y:, which is a method of NSBitmapImageRep, not NSImage:
NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] initWithData:[yourImage TIFFRepresentation]];
NSSize imageSize = [yourImage size];
CGFloat y = imageSize.height - 100.0;
NSColor* color = [imageRep colorAtX:100.0 y:y];
[imageRep release];
Note that you must make an adjustment for the y value because the colorAtX:y method uses a coordinate system that starts in the top left of the image, whereas the NSImage coordinate system starts at the bottom left.
Alternatively, if the pixel is visible on-screen then you can use the NSReadPixel() function to get the color of a pixel in the current coordinate system.
Function colorAtX of NSBitmapImageRep seems not to use the device color space, which may lead to color values that are slightly different from what you actually see. Use this code to get the correct color in the current device color space:
[yourImage lockFocus]; // yourImage is just your NSImage variable
NSColor *pixelColor = NSReadPixel(NSMakePoint(1, 1)); // Or another point
[yourImage unlockFocus];

Resources